From 89a7f84b30f9b0fb79411c9743992d91adc03c17 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 11 Jan 2026 14:23:33 -0800 Subject: [PATCH 01/53] core2: Start rewrite of the compiler and VM This change adds a new "core2" crate that captures the evolution of the core language compiler and VM. Part of this crate (the lexer, parser, and AST) is an almost-identical copy/paste of the "core" crate -- but the compiler, VM, and approach to testing is all new. This new implementation is still incomplete. The compiler does not yet handle the majority of statement types, does not compile upcall arguments, and can only deal with arithmetic additions. But the foundations are now good-enough to be persisted and iterated on. --- Cargo.toml | 1 + core2/Cargo.toml | 25 + core2/LICENSE | 202 ++ core2/NOTICE | 2 + core2/README.md | 85 + core2/src/ast.rs | 817 +++++ core2/src/bytecode.rs | 695 ++++ core2/src/callable.rs | 624 ++++ core2/src/compiler/args.rs | 94 + core2/src/compiler/codegen.rs | 165 + core2/src/compiler/exprs.rs | 192 + core2/src/compiler/ids.rs | 157 + core2/src/compiler/mod.rs | 131 + core2/src/compiler/syms.rs | 799 +++++ core2/src/compiler/top.rs | 316 ++ core2/src/image.rs | 160 + core2/src/lexer.rs | 1636 +++++++++ core2/src/lib.rs | 37 + core2/src/mem.rs | 110 + core2/src/num.rs | 110 + core2/src/parser.rs | 4876 ++++++++++++++++++++++++++ core2/src/reader.rs | 319 ++ core2/src/testutils.rs | 107 + core2/src/vm/context.rs | 330 ++ core2/src/vm/mod.rs | 243 ++ core2/tests/integration_test.rs | 566 +++ core2/tests/test_arithmetic.md | 161 + core2/tests/test_assignments.md | 210 ++ core2/tests/test_empty.md | 14 + core2/tests/test_end.md | 134 + core2/tests/test_functions.md | 265 ++ core2/tests/test_globals.md | 295 ++ core2/tests/test_gosub.md | 177 + core2/tests/test_goto.md | 96 + core2/tests/test_out_of_registers.md | 359 ++ core2/tests/test_strings.md | 53 + core2/tests/test_subs.md | 214 ++ core2/tests/test_types.md | 165 + core2/tests/test_varargs.md | 139 + 39 files changed, 15081 insertions(+) create mode 100644 core2/Cargo.toml create mode 100644 core2/LICENSE create mode 100644 core2/NOTICE create mode 100644 core2/README.md create mode 100644 core2/src/ast.rs create mode 100644 core2/src/bytecode.rs create mode 100644 core2/src/callable.rs create mode 100644 core2/src/compiler/args.rs create mode 100644 core2/src/compiler/codegen.rs create mode 100644 core2/src/compiler/exprs.rs create mode 100644 core2/src/compiler/ids.rs create mode 100644 core2/src/compiler/mod.rs create mode 100644 core2/src/compiler/syms.rs create mode 100644 core2/src/compiler/top.rs create mode 100644 core2/src/image.rs create mode 100644 core2/src/lexer.rs create mode 100644 core2/src/lib.rs create mode 100644 core2/src/mem.rs create mode 100644 core2/src/num.rs create mode 100644 core2/src/parser.rs create mode 100644 core2/src/reader.rs create mode 100644 core2/src/testutils.rs create mode 100644 core2/src/vm/context.rs create mode 100644 core2/src/vm/mod.rs create mode 100644 core2/tests/integration_test.rs create mode 100644 core2/tests/test_arithmetic.md create mode 100644 core2/tests/test_assignments.md create mode 100644 core2/tests/test_empty.md create mode 100644 core2/tests/test_end.md create mode 100644 core2/tests/test_functions.md create mode 100644 core2/tests/test_globals.md create mode 100644 core2/tests/test_gosub.md create mode 100644 core2/tests/test_goto.md create mode 100644 core2/tests/test_out_of_registers.md create mode 100644 core2/tests/test_strings.md create mode 100644 core2/tests/test_subs.md create mode 100644 core2/tests/test_types.md create mode 100644 core2/tests/test_varargs.md diff --git a/Cargo.toml b/Cargo.toml index dd828d1f..c188f977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "cli", "client", "core", + "core2", "repl", "rpi", "sdl", diff --git a/core2/Cargo.toml b/core2/Cargo.toml new file mode 100644 index 00000000..1337932a --- /dev/null +++ b/core2/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "endbasic-core2" +version = "0.11.99" # ENDBASIC-VERSION +license = "Apache-2.0" +authors = ["Julio Merino "] +categories = ["development-tools", "parser-implementations"] +keywords = ["basic", "interpreter", "learning", "programming"] +description = "The EndBASIC programming language - core" +homepage = "https://www.endbasic.dev/" +repository = "https://github.com/endbasic/endbasic" +readme = "README.md" +edition = "2024" +publish = false + +[lints] +workspace = true + +[dependencies] +async-trait = "0.1" +thiserror = "1.0" + +[dev-dependencies] +futures-lite = "2.2" +tempfile = "3" +tokio = { version = "1", features = ["full"] } diff --git a/core2/LICENSE b/core2/LICENSE new file mode 100644 index 00000000..d6456956 --- /dev/null +++ b/core2/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/core2/NOTICE b/core2/NOTICE new file mode 100644 index 00000000..a6ebc462 --- /dev/null +++ b/core2/NOTICE @@ -0,0 +1,2 @@ +EndBASIC +Copyright 2020-2026 Julio Merino diff --git a/core2/README.md b/core2/README.md new file mode 100644 index 00000000..c0c68c99 --- /dev/null +++ b/core2/README.md @@ -0,0 +1,85 @@ +# The EndBASIC programming language - core + +[![Crates.io](https://img.shields.io/crates/v/endbasic-core.svg)](https://crates.io/crates/endbasic-core/) +[![Docs.rs](https://docs.rs/endbasic-core/badge.svg)](https://docs.rs/endbasic-core/) + +EndBASIC is an interpreter for a BASIC-like language and is inspired by +Amstrad's Locomotive BASIC 1.1 and Microsoft's QuickBASIC 4.5. Like the former, +EndBASIC intends to provide an interactive environment that seamlessly merges +coding with immediate visual feedback. Like the latter, EndBASIC offers +higher-level programming constructs and strong typing. + +EndBASIC offers a simplified and restricted environment to learn the foundations +of programming and focuses on features that can quickly reward the programmer. +These features include things like a built-in text editor, commands to +render graphics, and commands to interact with the hardware of a Raspberry +Pi. Implementing this kind of features has priority over others such as +performance or a much richer language. + +EndBASIC is written in Rust and runs both on the web and locally on a variety of +operating systems and platforms, including macOS, Windows, and Linux. + +EndBASIC is free software under the [Apache 2.0 License](LICENSE). + +## What's in this crate? + +`endbasic-core` provides the language parser and interpreter. By design, this +crate provides zero commands and zero functions. + +## Language features + +EndBASIC's language features are inspired by other BASIC interpreters but the +language does not intend to be fully compatible with them. The language +currently supports: + +* Variable types: boolean (`?`), double (`#`), integer (`%`), and string + (`$`). +* Arrays via `DIM name(1, 2, 3) AS type`. +* Strong typing with optional variable type annotations. +* `DATA` statements for literal primitive values. Booleans, numbers, and + strings are supported, but strings must be double-quoted. +* `DO` / `LOOP` statements with optional `UNTIL` / `WHILE` pre- and + post-guards and optional `EXIT DO` early terminations. +* `DIM SHARED` for global variables. +* `FUNCTION name` / `EXIT FUNCTION` / `END FUNCTION`. +* `IF ... THEN ... [ELSE ...]` uniline statements. +* `IF ... THEN` / `ELSEIF ... THEN` / `ELSE` / `END IF` multiline + statements. +* `FOR x = ... TO ... [STEP ...]` / `NEXT` loops with optional `EXIT FOR` + early terminations. +* `GOSUB line` / `GOSUB @label` / `RETURN` for procedure execution. +* `GOTO line` / `GOTO @label` statements and `@label` annotations. +* `SELECT CASE` / `CASE ...` / `CASE IS ...` / `CASE ... TO ...` / + `END SELECT` statements. +* `SUB name` / `EXIT SUB` / `END SUB`. +* `WHILE ...` / `WEND` loops. +* Error handling via `ON ERROR GOTO` and `ON ERROR RESUME NEXT`. +* UTF-8 everywhere (I think). + +## Design principles + +Some highlights about the EndBASIC implementation are: + +* Minimalist core. The interpreter knows how to execute the logic of the + language but, by default, it exposes no builtins to the scripts—not even + `INPUT` or `PRINT`. This makes EndBASIC ideal for embedding into other + programs, as it is possible to execute external code without side-effects or + by precisely controlling how such code interacts with the host program. + +* Async support. The interpreter is async-compatible, making it trivial to + embed it into Javascript via WASM. + +## Examples + +The `examples` directory contains sample code to show how to embed the EndBASIC +interpreter into your own programs. In particular: + +* [`examples/config.rs`](examples/config.rs): Shows how to instantiate a + minimal EndBASIC interpreter and uses it to implement what could be a + configuration file parser. + +* [`examples/dsl.rs`](example/dsl.rs): Shows how to instantiate an EndBASIC + interpreter with custom functions and commands to construct what could be a + domain-specific language. This language is then used to control some + hypothetical hardware lights and exemplifies how to bridge the Rust world + and the EndBASIC world. diff --git a/core2/src/ast.rs b/core2/src/ast.rs new file mode 100644 index 00000000..39e64230 --- /dev/null +++ b/core2/src/ast.rs @@ -0,0 +1,817 @@ +// EndBASIC +// Copyright 2020 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. + +//! Abstract Syntax Tree (AST) for the EndBASIC language. + +use crate::reader::LineCol; +use std::convert::TryFrom; +use std::fmt; + +/// Components of a boolean literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct BooleanSpan { + /// The boolean literal. + pub value: bool, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a double literal expression. +#[derive(Clone, Debug, PartialEq)] +pub struct DoubleSpan { + /// The double literal. + pub value: f64, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of an integer literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IntegerSpan { + /// The integer literal. + pub value: i32, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a string literal expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TextSpan { + /// The string literal. + pub value: String, + + /// Starting position of the literal. + pub pos: LineCol, +} + +/// Components of a symbol reference expression. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct SymbolSpan { + /// The symbol reference. + pub vref: VarRef, + + /// Starting position of the symbol reference. + pub pos: LineCol, +} + +/// Components of a unary operation expression. +#[derive(Clone, Debug, PartialEq)] +pub struct UnaryOpSpan { + /// Expression affected by the operator. + pub expr: Expr, + + /// Starting position of the operator. + pub pos: LineCol, +} + +/// Components of a binary operation expression. +#[derive(Clone, Debug, PartialEq)] +pub struct BinaryOpSpan { + /// Expression on the left side of the operator. + pub lhs: Expr, + + /// Expression on the right side of the operator. + pub rhs: Expr, + + /// Starting position of the operator. + pub pos: LineCol, +} + +/// Represents an expression and provides mechanisms to evaluate it. +#[derive(Clone, Debug, PartialEq)] +pub enum Expr { + /// A literal boolean value. + Boolean(BooleanSpan), + /// A literal double-precision floating point value. + Double(DoubleSpan), + /// A literal integer value. + Integer(IntegerSpan), + /// A literal string value. + Text(TextSpan), + + /// A reference to a variable. + Symbol(SymbolSpan), + + /// Arithmetic addition of two expressions. + Add(Box), + /// Arithmetic subtraction of two expressions. + Subtract(Box), + /// Arithmetic multiplication of two expressions. + Multiply(Box), + /// Arithmetic division of two expressions. + Divide(Box), + /// Arithmetic modulo operation of two expressions. + Modulo(Box), + /// Arithmetic power operation of two expressions. + Power(Box), + /// Arithmetic sign flip of an expression. + Negate(Box), + + /// Relational equality comparison of two expressions. + Equal(Box), + /// Relational inequality comparison of two expressions. + NotEqual(Box), + /// Relational less-than comparison of two expressions. + Less(Box), + /// Relational less-than or equal-to comparison of two expressions. + LessEqual(Box), + /// Relational greater-than comparison of two expressions. + Greater(Box), + /// Relational greater-than or equal-to comparison of two expressions. + GreaterEqual(Box), + + /// Logical and of two expressions. + And(Box), + /// Logical not of an expression. + Not(Box), + /// Logical or of two expressions. + Or(Box), + /// Logical xor of two expressions. + Xor(Box), + + /// Shift left of a signed integer by a number of bits without rotation. + ShiftLeft(Box), + /// Shift right of a signed integer by a number of bits without rotation. + ShiftRight(Box), + + /// A function call or an array reference. + Call(CallSpan), +} + +impl Expr { + /// Returns the start position of the expression. + pub fn start_pos(&self) -> LineCol { + match self { + Expr::Boolean(span) => span.pos, + Expr::Double(span) => span.pos, + Expr::Integer(span) => span.pos, + Expr::Text(span) => span.pos, + + Expr::Symbol(span) => span.pos, + + Expr::And(span) => span.lhs.start_pos(), + Expr::Or(span) => span.lhs.start_pos(), + Expr::Xor(span) => span.lhs.start_pos(), + Expr::Not(span) => span.pos, + + Expr::ShiftLeft(span) => span.lhs.start_pos(), + Expr::ShiftRight(span) => span.lhs.start_pos(), + + Expr::Equal(span) => span.lhs.start_pos(), + Expr::NotEqual(span) => span.lhs.start_pos(), + Expr::Less(span) => span.lhs.start_pos(), + Expr::LessEqual(span) => span.lhs.start_pos(), + Expr::Greater(span) => span.lhs.start_pos(), + Expr::GreaterEqual(span) => span.lhs.start_pos(), + + Expr::Add(span) => span.lhs.start_pos(), + Expr::Subtract(span) => span.lhs.start_pos(), + Expr::Multiply(span) => span.lhs.start_pos(), + Expr::Divide(span) => span.lhs.start_pos(), + Expr::Modulo(span) => span.lhs.start_pos(), + Expr::Power(span) => span.lhs.start_pos(), + Expr::Negate(span) => span.pos, + + Expr::Call(span) => span.vref_pos, + } + } +} + +/// Represents type of an expression. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ExprType { + /// Type for an expression that evaluates to a boolean. + Boolean = 0, + + /// Type for an expression that evaluates to a double. + Double = 1, + + /// Type for an expression that evaluates to an integer. + Integer = 2, + + /// Type for an expression that evaluates to a string. + Text = 3, +} + +impl TryFrom for ExprType { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::Boolean), + 1 => Ok(Self::Double), + 2 => Ok(Self::Integer), + 3 => Ok(Self::Text), + _ => Err(()), + } + } +} + +impl ExprType { + /// Returns true if this expression type is numerical. + pub(crate) fn is_numerical(self) -> bool { + self == Self::Double || self == Self::Integer + } + + /// Returns the textual representation of the annotation for this type. + pub fn annotation(&self) -> char { + match self { + ExprType::Boolean => '?', + ExprType::Double => '#', + ExprType::Integer => '%', + ExprType::Text => '$', + } + } +} + +impl fmt::Display for ExprType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ExprType::Boolean => write!(f, "BOOLEAN"), + ExprType::Double => write!(f, "DOUBLE"), + ExprType::Integer => write!(f, "INTEGER"), + ExprType::Text => write!(f, "STRING"), + } + } +} + +/// Represents a reference to a variable (which doesn't have to exist). +/// +/// Variable references are different from `SymbolKey`s because they maintain the case of the +/// reference (for error display purposes) and because they carry an optional type annotation. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VarRef { + /// Name of the variable this points to. + pub name: String, + + /// Type of the variable this points to, if explicitly specified. + /// + /// If `None`, the type of the variable is subject to type inference. + pub ref_type: Option, +} + +impl VarRef { + /// Creates a new reference to the variable with `name` and the optional `ref_type` type. + pub fn new>(name: T, ref_type: Option) -> Self { + Self { name: name.into(), ref_type } + } + + /// Returns true if this reference is compatible with the given type. + pub fn accepts(&self, other: ExprType) -> bool { + match self.ref_type { + None => true, + Some(vtype) => vtype == other, + } + } + + /// Returns true if this reference is compatible with the return type of a callable. + pub fn accepts_callable(&self, other: Option) -> bool { + match self.ref_type { + None => true, + Some(vtype) => match other { + Some(other) => vtype == other, + None => false, + }, + } + } +} + +impl fmt::Display for VarRef { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.ref_type { + None => self.name.fmt(f), + Some(vtype) => write!(f, "{}{}", self.name, vtype.annotation()), + } + } +} + +/// Types of separators between arguments to a `BuiltinCall`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +pub enum ArgSep { + /// Filler for the separator in the last argument. + End = 0, + + /// Short separator (`;`). + Short = 1, + + /// Long separator (`,`). + Long = 2, + + /// `AS` separator. + As = 3, +} + +impl TryFrom for ArgSep { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Self::End), + 1 => Ok(Self::Short), + 2 => Ok(Self::Long), + 3 => Ok(Self::As), + _ => Err(()), + } + } +} + +impl fmt::Display for ArgSep { + // TODO(jmmv): Can this be removed in favor of describe()? + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + ArgSep::End => write!(f, ""), + ArgSep::Short => write!(f, ";"), + ArgSep::Long => write!(f, ","), + ArgSep::As => write!(f, "AS"), + } + } +} + +impl ArgSep { + /// Formats the separator for a syntax specification. + /// + /// The return value contains the textual representation of the separator and a boolean that + /// indicates whether the separator requires a leading space. + pub(crate) fn describe(&self) -> (&str, bool) { + match self { + ArgSep::End => ("", false), + ArgSep::Short => (";", false), + ArgSep::Long => (",", false), + ArgSep::As => ("AS", true), + } + } +} + +/// Components of an array assignment statement. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct ArrayAssignmentSpan { + /// Reference to the array to modify. + pub vref: VarRef, + + /// Position of the `vref`. + pub vref_pos: LineCol, + + /// Expressions to compute the subscripts to index the array. + pub subscripts: Vec, + + /// Expression to compute the value of the modified element. + pub expr: Expr, +} + +/// Components of an assignment statement. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct AssignmentSpan { + /// Reference to the variable to set. + pub vref: VarRef, + + /// Position of the `vref`. + pub vref_pos: LineCol, + + /// Expression to compute the value of the modified variable. + pub expr: Expr, +} + +/// Single argument to a builtin call statement. +#[derive(Clone, Debug, PartialEq)] +pub struct ArgSpan { + /// Expression to compute the argument's value. This expression is optional to support calls + /// of the form `PRINT a, , b` where some arguments are empty. + pub expr: Option, + + /// Separator between this argument and the *next*. The last instance of this type in a call + /// always carries a value of `ArgSep::End`. + pub sep: ArgSep, + + /// Position of the `sep`. + pub sep_pos: LineCol, +} + +/// Components of a call statement or expression. +#[derive(Clone, Debug, PartialEq)] +pub struct CallSpan { + /// Reference to the callable (a command or a function), or the array to index. + pub vref: VarRef, + + /// Position of the reference. + pub vref_pos: LineCol, + + /// Sequence of arguments to pass to the callable. + pub args: Vec, +} + +/// Components of a `FUNCTION` or `SUB` definition. +#[derive(Debug, PartialEq)] +pub struct CallableSpan { + /// Name of the callable, expressed as a variable reference. For functions, this contains + /// a type, and for subroutines, it does not. + pub name: VarRef, + + /// Position of the name of the callable. + pub name_pos: LineCol, + + /// Definition of the callable parameters. + pub params: Vec, + + /// Statements within the callable's body. + pub body: Vec, + + /// Position of the end of the callable, used when injecting the implicit return. + pub end_pos: LineCol, +} + +/// Components of a data statement. +#[derive(Debug, PartialEq)] +pub struct DataSpan { + /// Collection of optional literal values. + pub values: Vec>, +} + +/// Components of a variable definition. +/// +/// Given that a definition causes the variable to be initialized to a default value, it is +/// tempting to model this statement as a simple assignment. However, we must be able to +/// detect variable redeclarations at runtime, so we must treat this statement as a separate +/// type from assignments. +#[derive(Debug, Eq, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct DimSpan { + /// Name of the variable to be defined. Type annotations are not allowed, hence why this is + /// not a `VarRef`. + pub name: String, + + /// Position of the name. + pub name_pos: LineCol, + + /// Whether the variable is global or not. + pub shared: bool, + + /// Type of the variable to be defined. + pub vtype: ExprType, + + /// Position of the type. + pub vtype_pos: LineCol, +} + +/// Components of an array definition. +#[derive(Debug, PartialEq)] +#[cfg_attr(test, derive(Clone))] +pub struct DimArraySpan { + /// Name of the array to define. Type annotations are not allowed, hence why this is not a + /// `VarRef`. + pub name: String, + + /// Position of the name. + pub name_pos: LineCol, + + /// Whether the array is global or not. + pub shared: bool, + + /// Expressions to compute the dimensions of the array. + pub dimensions: Vec, + + /// Type of the array to be defined. + pub subtype: ExprType, + + /// Position of the subtype. + pub subtype_pos: LineCol, +} + +/// Type of the `DO` loop. +#[derive(Debug, PartialEq)] +pub enum DoGuard { + /// Represents an infinite loop without guards. + Infinite, + + /// Represents a loop with an `UNTIL` guard in the `DO` clause. + PreUntil(Expr), + + /// Represents a loop with a `WHILE` guard in the `DO` clause. + PreWhile(Expr), + + /// Represents a loop with an `UNTIL` guard in the `LOOP` clause. + PostUntil(Expr), + + /// Represents a loop with a `WHILE` guard in the `LOOP` clause. + PostWhile(Expr), +} + +/// Components of a `DO` statement. +#[derive(Debug, PartialEq)] +pub struct DoSpan { + /// Expression to compute whether to execute the loop's body or not and where this appears in + /// the `DO` statement. + pub guard: DoGuard, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Components of an `END` statement. +#[derive(Debug, PartialEq)] +pub struct EndSpan { + /// Integer expression to compute the return code. + pub code: Option, + + /// Position of the statement. + pub pos: LineCol, +} + +/// Components of an `EXIT` statement. +#[derive(Debug, Eq, PartialEq)] +pub struct ExitSpan { + /// Position of the statement. + pub pos: LineCol, +} + +/// Components of a branch of an `IF` statement. +#[derive(Debug, PartialEq)] +pub struct IfBranchSpan { + /// Expression that guards execution of this branch. + pub guard: Expr, + + /// Statements within the branch. + pub body: Vec, +} + +/// Components of an `IF` statement. +#[derive(Debug, PartialEq)] +pub struct IfSpan { + /// Sequence of the branches in the conditional. + /// + /// Representation of the conditional branches. The final `ELSE` branch, if present, is also + /// included here and its guard clause is always a true expression. + pub branches: Vec, +} + +/// Components of a `FOR` statement. +/// +/// Note that we do not store the original end and step values, and instead use expressions to +/// represent the loop condition and the computation of the next iterator value. We do this +/// for run-time efficiency. The reason this is possible is because we force the step to be an +/// integer literal at parse time and do not allow it to be an expression. +#[derive(Debug, PartialEq)] +pub struct ForSpan { + /// Iterator name, expressed as a variable reference that must be either automatic or an + /// integer. + pub iter: VarRef, + + /// Position of the iterator. + pub iter_pos: LineCol, + + /// If true, the iterator computation needs to be performed as a double so that, when the + /// iterator variable is not yet defined, it gains the correct type. + pub iter_double: bool, + + /// Expression to compute the iterator's initial value. + pub start: Expr, + + /// Condition to test after each iteration. + pub end: Expr, + + /// Expression to compute the iterator's next value. + pub next: Expr, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Components of a `GOTO` or a `GOSUB` statement. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct GotoSpan { + /// Name of the label to jump to. + pub target: String, + + /// Position of the label. + pub target_pos: LineCol, +} + +/// Components of a label "statement". +/// +/// In principle, labels should be just a property of a statement but, for simplicity in the +/// current model, it's easiest to represent them as their own statement. +#[derive(Debug, Eq, PartialEq)] +pub struct LabelSpan { + /// Name of the label being defined. + pub name: String, + + /// Position of the label. + pub name_pos: LineCol, +} + +/// Components of an `ON ERROR` statement. +#[derive(Debug, Eq, PartialEq)] +pub enum OnErrorSpan { + /// Components of an `ON ERROR GOTO @label` statement. + Goto(GotoSpan), + + /// Components of an `ON ERROR GOTO 0` statement. + Reset, + + /// Components of an `ON ERROR RESUME NEXT` statement. + ResumeNext, +} + +/// Components of a `RETURN` statement. +#[derive(Debug, Eq, PartialEq)] +pub struct ReturnSpan { + /// Position of the statement. + pub pos: LineCol, +} + +/// Collection of relational operators that can appear in a `CASE IS` guard.. +#[derive(Debug, Eq, PartialEq)] +pub enum CaseRelOp { + /// Relational operator for `CASE IS =`. + Equal, + + /// Relational operator for `CASE IS <>`. + NotEqual, + + /// Relational operator for `CASE IS <`. + Less, + + /// Relational operator for `CASE IS <=`. + LessEqual, + + /// Relational operator for `CASE IS >`. + Greater, + + /// Relational operator for `CASE IS >=`. + GreaterEqual, +} + +/// Components of a `CASE` guard. +#[derive(Debug, PartialEq)] +pub enum CaseGuardSpan { + /// Represents an `IS ` guard or a simpler `` guard. + Is(CaseRelOp, Expr), + + /// Represents an ` TO ` guard. + To(Expr, Expr), +} + +/// Components of a branch of a `SELECT` statement. +#[derive(Debug, PartialEq)] +pub struct CaseSpan { + /// Expressions that guard execution of this case. + pub guards: Vec, + + /// Statements within the case block. + pub body: Vec, +} + +/// Components of a `SELECT` statement. +#[derive(Debug, PartialEq)] +pub struct SelectSpan { + /// Expression to test for. + pub expr: Expr, + + /// Representation of the cases to select from. The final `CASE ELSE`, if present, is also + /// included here without any guards. + pub cases: Vec, + + /// Position of the `END SELECT` statement. + pub end_pos: LineCol, +} + +/// Components of a `WHILE` statement. +#[derive(Debug, PartialEq)] +pub struct WhileSpan { + /// Expression to compute whether to execute the loop's body or not. + pub expr: Expr, + + /// Statements within the loop's body. + pub body: Vec, +} + +/// Represents a statement in the program along all data to execute it. +#[derive(Debug, PartialEq)] +pub enum Statement { + /// Represents an assignment to an element of an array. + ArrayAssignment(ArrayAssignmentSpan), + + /// Represents a variable assignment. + Assignment(AssignmentSpan), + + /// Represents a call to a builtin command such as `PRINT`. + Call(CallSpan), + + /// Represents a `FUNCTION` or `SUB` definition. The difference between the two lies in just + /// the presence or absence of a return type in the callable. + Callable(CallableSpan), + + /// Represents a `DATA` statement. + Data(DataSpan), + + /// Represents a variable definition. + Dim(DimSpan), + + /// Represents an array definition. + DimArray(DimArraySpan), + + /// Represents a `DO` statement. + Do(DoSpan), + + /// Represents an `END` statement. + End(EndSpan), + + /// Represents an `EXIT DO` statement. + ExitDo(ExitSpan), + + /// Represents an `EXIT FOR` statement. + ExitFor(ExitSpan), + + /// Represents an `EXIT FUNCTION` statement. + ExitFunction(ExitSpan), + + /// Represents an `EXIT SUB` statement. + ExitSub(ExitSpan), + + /// Represents a `FOR` statement. + For(ForSpan), + + /// Represents a `GOSUB` statement. + Gosub(GotoSpan), + + /// Represents a `GOTO` statement. + Goto(GotoSpan), + + /// Represents an `IF` statement. + If(IfSpan), + + /// Represents a label "statement". + Label(LabelSpan), + + /// Represents an `ON ERROR` statement. + OnError(OnErrorSpan), + + /// Represents a `RETURN` statement. + Return(ReturnSpan), + + /// Represents a `SELECT` statement. + Select(SelectSpan), + + /// Represents a `WHILE` statement. + While(WhileSpan), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_varref_display() { + assert_eq!("name", format!("{}", VarRef::new("name", None))); + assert_eq!("abc?", format!("{}", VarRef::new("abc", Some(ExprType::Boolean)))); + assert_eq!("cba#", format!("{}", VarRef::new("cba", Some(ExprType::Double)))); + assert_eq!("def%", format!("{}", VarRef::new("def", Some(ExprType::Integer)))); + assert_eq!("ghi$", format!("{}", VarRef::new("ghi", Some(ExprType::Text)))); + } + + #[test] + fn test_varref_accepts() { + assert!(VarRef::new("a", None).accepts(ExprType::Boolean)); + assert!(VarRef::new("a", None).accepts(ExprType::Double)); + assert!(VarRef::new("a", None).accepts(ExprType::Integer)); + assert!(VarRef::new("a", None).accepts(ExprType::Text)); + + assert!(VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Boolean)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Boolean)); + assert!(VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Double)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Double)); + assert!(VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Integer)); + assert!(!VarRef::new("a", Some(ExprType::Integer)).accepts(ExprType::Text)); + + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Boolean)); + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Double)); + assert!(!VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Integer)); + assert!(VarRef::new("a", Some(ExprType::Text)).accepts(ExprType::Text)); + } +} diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs new file mode 100644 index 00000000..f2e717f4 --- /dev/null +++ b/core2/src/bytecode.rs @@ -0,0 +1,695 @@ +// 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. + +//! Bytecode for a compiled EndBASIC program. + +use crate::ast::{ArgSep, ExprType}; +use crate::num::{unchecked_u32_as_u8, unchecked_u32_as_u16}; +use std::convert::TryFrom; +use std::fmt; + +/// Representation of the various register scopes. +#[derive(Debug)] +pub enum RegisterScope { + /// Global scope for variables visible from any scope. + Global, + + /// Local scope for variables visible only within a function or subroutine. + Local, + + /// Temporary scope for intermediate values during expression evaluation. + Temp, +} + +impl fmt::Display for RegisterScope { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Global => write!(f, "global"), + Self::Local => write!(f, "local"), + Self::Temp => write!(f, "temp"), + } + } +} + +/// Error to indicate that we have run out of registers. +#[derive(Debug, thiserror::Error)] +#[error("Out of registers")] +pub(crate) struct OutOfRegistersError(()); + +/// Error types for bytecode parsing. +#[derive(Debug, thiserror::Error)] +pub(crate) enum ParseError { + /// The type tag in the bytecode is not recognized. + #[error("{0}: Invalid type tag {0}")] + InvalidTypeTag(u64), +} + +/// Result type for bytecode parsing operations. +pub(crate) type ParseResult = Result; + +/// Conversions between a primitive type and a `u32` for insertion into an instruction. +trait RawValue: Sized { + /// Converts a `u32` to the primitive type `Self`. + /// + /// This operation is only performed to _parse_ bytecode and we assume that the bytecode is + /// correctly formed. As a result, this does not perform any range checks. + fn from_u32(v: u32) -> Self; + + /// Converts the primitive type `Self` to a u32. + /// + /// This operation is only performed to _generate_ bytecode during compilation, and all + /// instruction definitions need to have fields that always fit in a u32. Consequently, + /// this operation is always safe. + fn to_u32(self) -> u32; +} + +/// Implements `RawValue` for an unsigned primitive type that is narrower than `u32`. +macro_rules! impl_raw_value { + ( $ty:ty, $from_u32_conv:ident ) => { + impl RawValue for $ty { + fn from_u32(v: u32) -> Self { + $from_u32_conv(v) + } + + fn to_u32(self) -> u32 { + u32::from(self) + } + } + }; +} + +impl_raw_value!(u8, unchecked_u32_as_u8); +impl_raw_value!(u16, unchecked_u32_as_u16); + +/// Representation of a register number. +/// +/// Registers are represented as `u8` integers where the first `Self::MAX_GLOBAL` values +/// correspond to global registers and the numbers after those correspond to local registers. +/// +/// During compilation, local register numbers are assigned starting from "logical 0" for +/// every scope in the call stack. During execution, local register numbers must be interpreted +/// in the context of the Frame Pointer (FP) register, which indicates the offset in the register +/// bank where local registers start for the current scope. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct Register(pub(crate) u8); + +impl fmt::Display for Register { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "R{}", self.0) + } +} + +impl RawValue for Register { + fn from_u32(v: u32) -> Self { + Self(unchecked_u32_as_u8(v)) + } + + fn to_u32(self) -> u32 { + u32::from(self.0) + } +} + +impl Register { + /// Maximum number of supported global registers. + pub(crate) const MAX_GLOBAL: u8 = 64; + + /// Constructs an instance of `Register` to represent the global register `reg`. Returns an + /// error if we have run out of global registers. + pub(crate) fn global(reg: u8) -> Result { + if reg < Self::MAX_GLOBAL { Ok(Self(reg)) } else { Err(OutOfRegistersError(())) } + } + + /// Constructs an instance of `Register` to represent the local register `reg`. Returns an + /// error if we have run out of local registers. + pub(crate) fn local(reg: u8) -> Result { + match reg.checked_add(Self::MAX_GLOBAL) { + Some(num) => Ok(Self(num)), + None => Err(OutOfRegistersError(())), + } + } + + /// Breaks apart the internal register representation and returns a tuple indicating if the + /// register is global or not and its logical index. + pub(crate) fn to_parts(self) -> (bool, u8) { + if self.0 < Self::MAX_GLOBAL { (true, self.0) } else { (false, self.0 - Self::MAX_GLOBAL) } + } +} + +impl RawValue for ExprType { + fn from_u32(v: u32) -> Self { + #[allow(unsafe_code)] + unsafe { + let v = unchecked_u32_as_u8(v); + debug_assert!(v <= ExprType::Text as u8); + std::mem::transmute(v) + } + } + + fn to_u32(self) -> u32 { + u32::from(self as u8) + } +} + +/// Generates functions to construct an instruction's bytecode representation for the compiler's +/// benefit, to parse it for the VM's benefit, and to format it for debugging purposes. +macro_rules! instr { + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + ) => { + pub(crate) fn $make() -> u32 { + ($opcode as u32) << 24 + } + + pub(crate) fn $parse(op: u32) { + debug_assert_eq!($opcode as u32, op >> 24); + } + + pub(crate) fn $format(op: u32) -> String { + $parse(op); + $name.to_owned() + } + }; + + ( $opcode:expr, $name: expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + ) => { + pub(crate) fn $make(v1: $type1) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + (($opcode as u32) << 24) | v1 + } + + pub(crate) fn $parse(op: u32) -> $type1 { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + v1 + } + + pub(crate) fn $format(op: u32) -> String { + let v1 = $parse(op); + format!("{:11} {}", $name, v1) + } + }; + + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + $type2:ty, $mask2:expr, $offset2:expr, + ) => { + pub(crate) fn $make(v1: $type1, v2: $type2) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + let v2 = (RawValue::to_u32(v2) & $mask2) << $offset2; + (($opcode as u32) << 24) | v1 | v2 + } + + pub(crate) fn $parse(op: u32) -> ($type1, $type2) { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + let v2 = RawValue::from_u32((op >> $offset2) & $mask2); + (v1, v2) + } + + pub(crate) fn $format(op: u32) -> String { + let (v1, v2) = $parse(op); + format!("{:11} {}, {}", $name, v1, v2) + } + }; + + ( $opcode:expr, $name:expr, + $make:ident, $parse:ident, $format:ident, + $type1:ty, $mask1:expr, $offset1:expr, + $type2:ty, $mask2:expr, $offset2:expr, + $type3:ty, $mask3:expr, $offset3:expr, + ) => { + pub(crate) fn $make(v1: $type1, v2: $type2, v3: $type3) -> u32 { + let v1 = (RawValue::to_u32(v1) & $mask1) << $offset1; + let v2 = (RawValue::to_u32(v2) & $mask2) << $offset2; + let v3 = (RawValue::to_u32(v3) & $mask3) << $offset3; + (($opcode as u32) << 24) | v1 | v2 | v3 + } + + pub(crate) fn $parse(op: u32) -> ($type1, $type2, $type3) { + debug_assert_eq!($opcode as u32, op >> 24); + let v1 = RawValue::from_u32((op >> $offset1) & $mask1); + let v2 = RawValue::from_u32((op >> $offset2) & $mask2); + let v3 = RawValue::from_u32((op >> $offset3) & $mask3); + (v1, v2, v3) + } + + pub(crate) fn $format(op: u32) -> String { + let (v1, v2, v3) = $parse(op); + format!("{:11} {}, {}, {}", $name, v1, v2, v3) + } + }; +} + +/// Enumeration of all valid instruction types (opcodes). +/// +/// The specific numbers assigned to each instruction are not important at this moment because +/// we expect bytecode execution to always be coupled with generation (which means there is no +/// need to worry about stable values over time). +#[repr(u8)] +pub(crate) enum Opcode { + /// Adds two doubles and stores the result into a third one. + AddDouble, + + /// Adds two integers and stores the result into a third one. + AddInteger, + + /// Allocates an object on the heap. + Alloc, + + /// Calls an address relative to the PC. + Call, + + /// Concatenates two strings and stores the pointer to the result into a third one. + Concat, + + /// Converts the double value in a register to an integer. + DoubleToInteger, + + /// Allocates local registers (locals and temporaries) when entering a scope. + Enter, + + /// Jumps to a subroutine at an address relative to the PC. + Gosub, + + /// Converts the integer value in a register to a double. + IntegerToDouble, + + /// Jumps to an address relative to the PC. + Jump, + + /// Loads a constant into a register. + LoadConstant, + + /// Loads an integer immediate into a register. + LoadInteger, + + /// Moves (copies) data between two registers. + Move, + + /// The "null" instruction, used by the compiler to pad the code for fixups. + Nop, + + /// Returns from a previous `Call`. + Return, + + /// Requests the execution of an upcall, stopping VM execution. + Upcall, + + /// Terminates execution. + // KEEP THIS LAST. + End, +} + +#[rustfmt::skip] +instr!( + Opcode::AddDouble, "ADDD", + make_add_double, parse_add_double, format_add_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::AddInteger, "ADDI", + make_add_integer, parse_add_integer, format_add_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::Alloc, "ALLOC", + make_alloc, parse_alloc, format_alloc, + Register, 0x000000ff, 8, // Destination register in which to store the heap pointer. + ExprType, 0x000000ff, 0, // Type of the object to allocate. +); + +#[rustfmt::skip] +instr!( + Opcode::Call, "CALL", + make_call, parse_call, format_call, + Register, 0x000000ff, 16, // Destination register for the return value, if any. + u16, 0x0000ffff, 0, // Target address. +); + +#[rustfmt::skip] +instr!( + Opcode::Concat, "CONCAT", + make_concat, parse_concat, format_concat, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::DoubleToInteger, "DTOI", + make_double_to_integer, parse_double_to_integer, format_double_to_integer, + Register, 0x000000ff, 0, // Register with the value to convert. +); + +#[rustfmt::skip] +instr!( + Opcode::End, "END", + make_end, parse_end, format_end, + Register, 0x000000ff, 0, // Register with the return code. +); + +#[rustfmt::skip] +instr!( + Opcode::Enter, "ENTER", + make_enter, parse_enter, format_enter, + u8, 0x000000ff, 0, // Number of local registers to allocate. +); + +#[rustfmt::skip] +instr!( + Opcode::Gosub, "GOSUB", + make_gosub, parse_gosub, format_gosub, + u16, 0x0000ffff, 0, // Target address. +); + +#[rustfmt::skip] +instr!( + Opcode::IntegerToDouble, "ITOD", + make_integer_to_double, parse_integer_to_double, format_integer_to_double, + Register, 0x000000ff, 0, // Register with the value to convert. +); + +#[rustfmt::skip] +instr!( + Opcode::Jump, "JUMP", + make_jump, parse_jump, format_jump, + u16, 0x0000ffff, 0, // Target address. +); + +#[rustfmt::skip] +instr!( + Opcode::LoadConstant, "LOADC", + make_load_constant, parse_load_constant, format_load_constant, + Register, 0x000000ff, 16, // Destination register to load the constant into. + u16, 0x0000ffff, 0, // Index of the constant to load. +); + +#[rustfmt::skip] +instr!( + Opcode::LoadInteger, "LOADI", + make_load_integer, parse_load_integer, format_load_integer, + Register, 0x000000ff, 16, // Destination register to load the immediate into. + u16, 0x0000ffff, 0, // Immediate value. +); + +#[rustfmt::skip] +instr!( + Opcode::Move, "MOVE", + make_move, parse_move, format_move, + Register, 0x000000ff, 8, // Destination register. + Register, 0x000000ff, 0, // Source register. +); + +#[rustfmt::skip] +instr!( + Opcode::Nop, "NOP", + make_nop, parse_nop, format_nop, +); + +#[rustfmt::skip] +instr!( + Opcode::Return, "RETURN", + make_return, parse_return, format_return, +); + +#[rustfmt::skip] +instr!( + Opcode::Upcall, "UPCALL", + make_upcall, parse_upcall, format_upcall, + u16, 0x0000ffff, 8, // Index of the upcall to execute. + Register, 0x000000ff, 0, // First register with arguments. +); + +/// Returns the opcode of an instruction. +pub(crate) fn opcode_of(instr: u32) -> Opcode { + #[allow(unsafe_code)] + unsafe { + let num = unchecked_u32_as_u8(instr >> 24); + debug_assert!(num <= Opcode::End as u8); + std::mem::transmute::(num) + } +} + +/// Tags used as integer register values to identify the type stored in another register at +/// runtime. +/// +/// This is used in function and command calls that receive variadic arguments (such as `PRINT`) +/// to identify the types of the arguments (which can be missing) and separators. +#[derive(Clone, Copy, Debug, PartialEq)] +#[repr(u8)] +pub enum VarArgTag { + /// The argument is missing. This is only possible for command invocations. + Missing(ArgSep) = 0, + + /// The argument is an immediate of the given type. + Immediate(ArgSep, ExprType) = 1, + + /// The argument is a pointer. + Pointer(ArgSep) = 2, +} + +impl VarArgTag { + /// Parses a register `value` into a variadic argument tag. + // This is not `TryFrom` because that makes this interface public and forces us to make the + // result type public as well, but we don't need it to be. + pub(crate) fn parse_u64(value: u64) -> ParseResult { + if value & !(0x0fff) != 0 { + return Err(ParseError::InvalidTypeTag(value)); + }; + + let key_u8 = ((value & 0x0f00) >> 8) as u8; + let sep_u8 = ((value & 0x00f0) >> 4) as u8; + let other_u8 = (value & 0x000f) as u8; + + let Ok(sep) = ArgSep::try_from(sep_u8) else { + return Err(ParseError::InvalidTypeTag(value)); + }; + + match key_u8 { + 0 => { + if other_u8 == 0 { + Ok(Self::Missing(sep)) + } else { + Err(ParseError::InvalidTypeTag(value)) + } + } + 1 => match ExprType::try_from(other_u8) { + Ok(etype) => Ok(Self::Immediate(sep, etype)), + Err(_) => Err(ParseError::InvalidTypeTag(value)), + }, + 2 => { + if other_u8 == 0 { + Ok(Self::Pointer(sep)) + } else { + Err(ParseError::InvalidTypeTag(value)) + } + } + _ => Err(ParseError::InvalidTypeTag(value)), + } + } + + /// Makes a new tag for the type of a variadic argument. + pub(crate) fn make_u16(self) -> u16 { + let (key_u8, sep, other_u8): (u8, ArgSep, u8) = match self { + Self::Missing(sep) => (0, sep, 0), + Self::Immediate(sep, etype) => (1, sep, etype as u8), + Self::Pointer(sep) => (2, sep, 0), + }; + u16::from(key_u8) << 8 | u16::from(sep as u8) << 4 | u16::from(other_u8) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! test_instr { + ( $name:ident, $make:ident, $parse:ident ) => { + #[test] + fn $name() { + let instr = $make(); + $parse(instr); + } + }; + + ( $name:ident, $make:ident, $parse:ident, $v1:expr ) => { + #[test] + fn $name() { + let instr = $make($v1); + assert_eq!($v1, $parse(instr)); + } + }; + + ( $name:ident, $make:ident, $parse:ident, $v1:expr, $v2:expr ) => { + #[test] + fn $name() { + let instr = $make($v1, $v2); + assert_eq!(($v1, $v2), $parse(instr)); + } + }; + + ( $name:ident, $make:ident, $parse:ident, $v1:expr, $v2:expr, $v3:expr ) => { + #[test] + fn $name() { + let instr = $make($v1, $v2, $v3); + assert_eq!(($v1, $v2, $v3), $parse(instr)); + } + }; + } + + test_instr!( + test_add_double, + make_add_double, + parse_add_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_add_integer, + make_add_integer, + parse_add_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_alloc, + make_alloc, + parse_alloc, + Register::local(1).unwrap(), + ExprType::Integer + ); + + test_instr!(test_call, make_call, parse_call, Register::local(3).unwrap(), 12345); + + test_instr!( + test_concat, + make_concat, + parse_concat, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_double_to_integer, + make_double_to_integer, + parse_double_to_integer, + Register::local(1).unwrap() + ); + + test_instr!(test_end, make_end, parse_end, Register::local(1).unwrap()); + + test_instr!(test_enter, make_enter, parse_enter, 10); + + test_instr!(test_gosub, make_gosub, parse_gosub, 12345); + + test_instr!( + test_integer_to_double, + make_integer_to_double, + parse_integer_to_double, + Register::local(1).unwrap() + ); + + test_instr!(test_jump, make_jump, parse_jump, 12345); + + test_instr!( + test_load_constant, + make_load_constant, + parse_load_constant, + Register::local(1).unwrap(), + 12345 + ); + + test_instr!( + test_load_integer, + make_load_integer, + parse_load_integer, + Register::local(1).unwrap(), + 12345 + ); + + test_instr!( + test_move, + make_move, + parse_move, + Register::local(1).unwrap(), + Register::local(2).unwrap() + ); + + test_instr!(test_nop, make_nop, parse_nop); + + test_instr!(test_return, make_return, parse_return); + + test_instr!(test_upcall, make_upcall, parse_upcall, 12345, Register::local(3).unwrap()); + + #[test] + fn test_var_arg_tag_ok() { + for sep in [ArgSep::As, ArgSep::End, ArgSep::Long, ArgSep::Short] { + for vat in [ + VarArgTag::Missing(sep), + VarArgTag::Pointer(sep), + VarArgTag::Immediate(sep, ExprType::Boolean), + VarArgTag::Immediate(sep, ExprType::Double), + VarArgTag::Immediate(sep, ExprType::Integer), + VarArgTag::Immediate(sep, ExprType::Text), + ] { + assert_eq!(vat, VarArgTag::parse_u64(u64::from(VarArgTag::make_u16(vat))).unwrap()); + } + } + } + + #[test] + fn test_var_arg_tag_errors() { + // Larger than 12 bits. + VarArgTag::parse_u64(1 << 12).unwrap_err(); + + // Invalid tag type. + VarArgTag::parse_u64(0x00000500).unwrap_err(); + + // Missing tag with invalid payload. + VarArgTag::parse_u64(0x00000001).unwrap_err(); + + // Missing tag with invalid separator. + VarArgTag::parse_u64(0x00000040).unwrap_err(); + + // ExprType tag with invalid payload. + VarArgTag::parse_u64(0x00000104).unwrap_err(); + + // ExprType tag with invalid separator. + VarArgTag::parse_u64(0x00000140).unwrap_err(); + + // Pointer tag with invalid payload. + VarArgTag::parse_u64(0x00000201).unwrap_err(); + + // Pointer tag with invalid separator. + VarArgTag::parse_u64(0x00000240).unwrap_err(); + } +} diff --git a/core2/src/callable.rs b/core2/src/callable.rs new file mode 100644 index 00000000..7f65e77f --- /dev/null +++ b/core2/src/callable.rs @@ -0,0 +1,624 @@ +// EndBASIC +// Copyright 2021 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. + +//! Symbol definitions and symbols table representation. + +use crate::ast::ArgSep; +use crate::ast::ExprType; +use crate::bytecode::VarArgTag; +use crate::mem::{Datum, Pointer}; +use async_trait::async_trait; +use std::borrow::Cow; +use std::ops::RangeInclusive; +use std::str::Lines; + +/// Error types for callable execution. +#[derive(Debug, thiserror::Error)] +pub enum CallError { + /// Generic error with a static message. + #[error("{0}")] + Other(&'static str), +} + +/// Result type for callable execution. +pub type CallResult = Result; + +/// Syntax specification for a required scalar parameter. +#[derive(Clone, Debug)] +pub struct RequiredValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameter. + pub vtype: ExprType, +} + +/// Syntax specification for a required reference parameter. +#[derive(Clone, Debug)] +pub struct RequiredRefSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// If true, require an array reference; if false, a variable reference. + pub require_array: bool, + + /// If true, allow references to undefined variables because the command will define them when + /// missing. Can only be set to true for commands, not functions, and `require_array` must be + /// false. + pub define_undefined: bool, +} + +/// Syntax specification for an optional scalar parameter. +/// +/// Optional parameters are only supported in commands. +#[derive(Clone, Debug)] +pub struct OptionalValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameter. + pub vtype: ExprType, + + /// Value to push onto the stack when the parameter is missing. + pub missing_value: i32, + + /// Value to push onto the stack when the parameter is present, after which the stack contains + /// the parameter value. + pub present_value: i32, +} + +/// Specifies the type constraints for a repeated parameter. +#[derive(Clone, Debug)] +pub enum RepeatedTypeSyntax { + /// Allows any value type, including empty arguments. The values pushed onto the stack have + /// the same semantics as those pushed by `AnyValueSyntax`. + AnyValue, + + /// Expects a value of the given type. + TypedValue(ExprType), + + /// Expects a reference to a variable (not an array) and allows the variables to not be defined. + VariableRef, +} + +/// Syntax specification for a repeated parameter. +/// +/// The repeated parameter must appear after all singular positional parameters. +#[derive(Clone, Debug)] +pub struct RepeatedSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// The type of the expected parameters. + pub type_syn: RepeatedTypeSyntax, + + /// The separator to expect between the repeated parameters. For functions, this must be the + /// long separator (the comma). + pub sep: ArgSepSyntax, + + /// Whether the repeated parameter must at least have one element or not. + pub require_one: bool, + + /// Whether to allow any parameter to not be present or not. Can only be true for commands. + pub allow_missing: bool, +} + +impl RepeatedSyntax { + /// Formats the repeated argument syntax for help purposes into `output`. + /// + /// `last_singular_sep` contains the separator of the last singular argument syntax, if any, + /// which we need to place inside of the optional group. + fn describe(&self, output: &mut String, last_singular_sep: Option<&ArgSepSyntax>) { + if !self.require_one { + output.push('['); + } + + if let Some(sep) = last_singular_sep { + sep.describe(output); + } + + output.push_str(&self.name); + output.push('1'); + if let RepeatedTypeSyntax::TypedValue(vtype) = self.type_syn { + output.push(vtype.annotation()); + } + + if self.require_one { + output.push('['); + } + + self.sep.describe(output); + output.push_str(".."); + self.sep.describe(output); + + output.push_str(&self.name); + output.push('N'); + if let RepeatedTypeSyntax::TypedValue(vtype) = self.type_syn { + output.push(vtype.annotation()); + } + + output.push(']'); + } +} + +/// Syntax specification for a parameter that accepts any scalar type. +#[derive(Clone, Debug)] +pub struct AnyValueSyntax { + /// The name of the parameter for help purposes. + pub name: Cow<'static, str>, + + /// Whether to allow the parameter to not be present or not. Can only be true for commands. + pub allow_missing: bool, +} + +/// Specifies the expected argument separator in a callable's syntax. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ArgSepSyntax { + /// The argument separator must exactly be the one given. + Exactly(ArgSep), + + /// The argument separator may be any of the ones given. + OneOf(ArgSep, ArgSep), + + /// The argument separator is the end of the call. + End, +} + +impl ArgSepSyntax { + /// Formats the argument separator for help purposes into `output`. + fn describe(&self, output: &mut String) { + match self { + ArgSepSyntax::Exactly(sep) => { + let (text, needs_space) = sep.describe(); + + if !text.is_empty() && needs_space { + output.push(' '); + } + output.push_str(text); + if !text.is_empty() { + output.push(' '); + } + } + + ArgSepSyntax::OneOf(sep1, sep2) => { + let (text1, _needs_space1) = sep1.describe(); + let (text2, _needs_space2) = sep2.describe(); + + output.push(' '); + output.push_str(&format!("<{}|{}>", text1, text2)); + output.push(' '); + } + + ArgSepSyntax::End => (), + }; + } +} + +/// Syntax specification for a non-repeated argument. +/// +/// Every item in this enum is composed of a struct that provides the details on the parameter and +/// a struct that provides the details on how this parameter is separated from the next. +#[derive(Clone, Debug)] +pub enum SingularArgSyntax { + /// A required scalar value with the syntax details and the separator that follows. + RequiredValue(RequiredValueSyntax, ArgSepSyntax), + + /// A required reference with the syntax details and the separator that follows. + RequiredRef(RequiredRefSyntax, ArgSepSyntax), + + /// An optional scalar value with the syntax details and the separator that follows. + OptionalValue(OptionalValueSyntax, ArgSepSyntax), + + /// A required scalar value of any type with the syntax details and the separator that follows. + AnyValue(AnyValueSyntax, ArgSepSyntax), +} + +/// Complete syntax specification for a callable's arguments. +/// +/// Note that the description of function arguments is more restricted than that of commands. +/// The arguments compiler panics when these preconditions aren't met with the rationale that +/// builtin functions must never be ill-defined. +// TODO(jmmv): It might be nice to try to express these restrictions in the type system, but +// things are already too verbose as they are... +#[derive(Clone, Debug)] +pub(crate) struct CallableSyntax { + /// Ordered list of singular arguments that appear before repeated arguments. + singular: Cow<'static, [SingularArgSyntax]>, + + /// Details on the repeated argument allowed after singular arguments, if any. + repeated: Option>, +} + +impl CallableSyntax { + /// Creates a new callable arguments definition from its parts defined statically in the + /// code. + pub(crate) fn new_static( + singular: &'static [SingularArgSyntax], + repeated: Option<&'static RepeatedSyntax>, + ) -> Self { + Self { singular: Cow::Borrowed(singular), repeated: repeated.map(Cow::Borrowed) } + } + + /// Creates a new callable arguments definition from its parts defined dynamically at + /// runtime. + pub(crate) fn new_dynamic( + singular: Vec, + repeated: Option, + ) -> Self { + Self { singular: Cow::Owned(singular), repeated: repeated.map(Cow::Owned) } + } + + /// Computes the range of the expected number of parameters for this syntax. + pub(crate) fn expected_nargs(&self) -> RangeInclusive { + let mut min = self.singular.len(); + let mut max = self.singular.len(); + if let Some(syn) = self.repeated.as_ref() { + if syn.require_one { + min += 1; + } + max = usize::MAX; + } + min..=max + } + + /// Returns true if this syntax represents "no arguments". + pub(crate) fn is_empty(&self) -> bool { + self.singular.is_empty() && self.repeated.is_none() + } + + /// Produces a user-friendly description of this callable syntax. + pub(crate) fn describe(&self) -> String { + let mut description = String::new(); + let mut last_singular_sep = None; + for (i, s) in self.singular.iter().enumerate() { + let sep = match s { + SingularArgSyntax::RequiredValue(details, sep) => { + description.push_str(&details.name); + description.push(details.vtype.annotation()); + sep + } + + SingularArgSyntax::RequiredRef(details, sep) => { + description.push_str(&details.name); + sep + } + + SingularArgSyntax::OptionalValue(details, sep) => { + description.push('['); + description.push_str(&details.name); + description.push(details.vtype.annotation()); + description.push(']'); + sep + } + + SingularArgSyntax::AnyValue(details, sep) => { + if details.allow_missing { + description.push('['); + } + description.push_str(&details.name); + if details.allow_missing { + description.push(']'); + } + sep + } + }; + + if self.repeated.is_none() || i < self.singular.len() - 1 { + sep.describe(&mut description); + } + if i == self.singular.len() - 1 { + last_singular_sep = Some(sep); + } + } + + if let Some(syn) = &self.repeated { + syn.describe(&mut description, last_singular_sep); + } + + description + } +} + +/// Builder pattern for constructing a callable's metadata. +pub struct CallableMetadataBuilder { + /// Name of the callable, stored in uppercase. + name: Cow<'static, str>, + + /// Return type of the callable, or `None` for commands/subroutines. + return_type: Option, + + /// Category for grouping related callables in help messages. + category: Option<&'static str>, + + /// Syntax specifications for the callable's arguments. + syntaxes: Vec, + + /// Description of the callable for documentation purposes. + description: Option<&'static str>, +} + +impl CallableMetadataBuilder { + /// Constructs a new metadata builder with the minimum information necessary. + /// + /// All code except tests must populate the whole builder with details. This is enforced at + /// construction time, where we only allow some fields to be missing under the test + /// configuration. + pub fn new(name: &'static str) -> Self { + assert!(name == name.to_ascii_uppercase(), "Callable name must be in uppercase"); + + Self { + name: Cow::Borrowed(name), + return_type: None, + syntaxes: vec![], + category: None, + description: None, + } + } + + /// Constructs a new metadata builder with the minimum information necessary. + /// + /// This is the same as `new` but using a dynamically-allocated name, which is necessary for + /// user-defined symbols. + pub fn new_dynamic>(name: S) -> Self { + Self { + name: Cow::Owned(name.into().to_ascii_uppercase()), + return_type: None, + syntaxes: vec![], + category: Some("User defined"), + description: Some("User defined symbol."), + } + } + + /// Sets the return type of the callable. + pub fn with_return_type(mut self, return_type: ExprType) -> Self { + self.return_type = Some(return_type); + self + } + + /// Sets the syntax specifications for this callable. + pub fn with_syntax( + mut self, + syntaxes: &'static [(&'static [SingularArgSyntax], Option<&'static RepeatedSyntax>)], + ) -> Self { + self.syntaxes = syntaxes + .iter() + .map(|s| CallableSyntax::new_static(s.0, s.1)) + .collect::>(); + self + } + + /// Sets the syntax specifications for this callable. + pub(crate) fn with_syntaxes>>(mut self, syntaxes: S) -> Self { + self.syntaxes = syntaxes.into(); + self + } + + /// Sets the syntax specifications for this callable. + pub(crate) fn with_dynamic_syntax( + self, + syntaxes: Vec<(Vec, Option)>, + ) -> Self { + let syntaxes = syntaxes + .into_iter() + .map(|s| CallableSyntax::new_dynamic(s.0, s.1)) + .collect::>(); + self.with_syntaxes(syntaxes) + } + + /// Sets the category for this callable. All callables with the same category name will be + /// grouped together in help messages. + pub fn with_category(mut self, category: &'static str) -> Self { + self.category = Some(category); + self + } + + /// Sets the description for this callable. The `description` is a collection of paragraphs + /// separated by a single newline character, where the first paragraph is taken as the summary + /// of the description. The summary must be a short sentence that is descriptive enough to be + /// understood without further details. Empty lines (paragraphs) are not allowed. + pub fn with_description(mut self, description: &'static str) -> Self { + for l in description.lines() { + assert!(!l.is_empty(), "Description cannot contain empty lines"); + } + self.description = Some(description); + self + } + + /// Generates the final `CallableMetadata` object, ensuring all values are present. + pub fn build(self) -> CallableMetadata { + assert!(!self.syntaxes.is_empty(), "All callables must specify a syntax"); + CallableMetadata { + name: self.name, + return_type: self.return_type, + syntaxes: self.syntaxes, + category: self.category.expect("All callables must specify a category"), + description: self.description.expect("All callables must specify a description"), + } + } + + /// Generates the final `CallableMetadata` object, ensuring the minimal set of values are + /// present. Only useful for testing. + pub fn test_build(mut self) -> CallableMetadata { + if self.syntaxes.is_empty() { + self.syntaxes.push(CallableSyntax::new_static(&[], None)); + } + CallableMetadata { + name: self.name, + return_type: self.return_type, + syntaxes: self.syntaxes, + category: self.category.unwrap_or(""), + description: self.description.unwrap_or(""), + } + } +} + +/// Representation of a callable's metadata. +/// +/// The callable is expected to hold onto an instance of this object within its struct to make +/// queries fast. +#[derive(Clone, Debug)] +pub struct CallableMetadata { + /// Name of the callable, stored in uppercase. + name: Cow<'static, str>, + + /// Return type of the callable, or `None` for commands/subroutines. + return_type: Option, + + /// Syntax specifications for the callable's arguments. + syntaxes: Vec, + + /// Category for grouping related callables in help messages. + category: &'static str, + + /// Description of the callable for documentation purposes. + description: &'static str, +} + +impl CallableMetadata { + /// Gets the callable's name, all in uppercase. + pub(crate) fn name(&self) -> &str { + &self.name + } + + /// Gets the callable's return type. + pub(crate) fn return_type(&self) -> Option { + self.return_type + } + + /// Gets the callable's syntax specification. + pub(crate) fn syntax(&self) -> String { + fn format_one(cs: &CallableSyntax) -> String { + let mut syntax = cs.describe(); + if syntax.is_empty() { + syntax.push_str("no arguments"); + } + syntax + } + + 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(" | "), + } + } + + /// Returns the callable's syntax definitions. + pub(crate) fn syntaxes(&self) -> &[CallableSyntax] { + &self.syntaxes + } + + /// Gets the callable's category as a collection of lines. The first line is the title of the + /// category, and any extra lines are additional information for it. + #[allow(unused)] + pub(crate) fn category(&self) -> &'static str { + self.category + } + + /// Gets the callable's textual description as a collection of lines. The first line is the + /// summary of the callable's purpose. + #[allow(unused)] + pub(crate) fn description(&self) -> Lines<'static> { + self.description.lines() + } + + /// Returns true if this is a callable that takes no arguments. + #[allow(unused)] + pub(crate) fn is_argless(&self) -> bool { + self.syntaxes.is_empty() || (self.syntaxes.len() == 1 && self.syntaxes[0].is_empty()) + } + + /// Returns true if this callable is a function (not a command). + #[allow(unused)] + pub(crate) fn is_function(&self) -> bool { + self.return_type.is_some() + } + + /// Returns true if this callable is user-defined. + pub(crate) fn is_user_defined(&self) -> bool { + self.category == "User defined" + } +} + +/// Arguments provided to a callable during its execution. +pub struct Scope<'a> { + /// Slice of register values containing the callable's arguments. + pub(crate) regs: &'a [u64], + + /// Reference to the constants pool for resolving constant pointers. + pub(crate) constants: &'a [Datum], + + /// Reference to the heap for resolving heap pointers. + pub(crate) heap: &'a [Datum], +} + +impl<'a> Scope<'a> { + /// Gets the type tag of the argument at `arg`. + pub fn get_type(&self, arg: u8) -> VarArgTag { + VarArgTag::parse_u64(self.regs[arg as usize]).unwrap() + } + + /// Gets the boolean value of the argument at `arg`. + pub fn get_boolean(&self, arg: u8) -> bool { + self.regs[arg as usize] != 0 + } + + /// Gets the double value of the argument at `arg`. + pub fn get_double(&self, arg: u8) -> f64 { + f64::from_bits(self.regs[arg as usize]) + } + + /// Gets the integer value of the argument at `arg`. + pub fn get_integer(&self, arg: u8) -> i32 { + self.regs[arg as usize] as i32 + } + + /// Gets the string value of the argument at `arg`. + pub fn get_string(&self, arg: u8) -> &str { + let index = self.regs[arg as usize]; + let ptr = Pointer::from(index); + match ptr.resolve(self.constants, self.heap) { + Datum::Text(s) => s, + _ => panic!("Mismatched constant type"), + } + } +} + +/// A trait to define a callable that is executed by a `Machine`. +/// +/// The callable themselves are immutable but they can reference mutable state. Given that +/// EndBASIC is not threaded, it is sufficient for those references to be behind a `RefCell` +/// and/or an `Rc`. +/// +/// Idiomatically, these objects need to provide a `new()` method that returns an `Rc`, as +/// that's the type used throughout the execution engine. +#[async_trait(?Send)] +pub trait Callable { + /// Returns the metadata for this function. + /// + /// The return value takes the form of a reference to force the callable to store the metadata + /// as a struct field so that calls to this function are guaranteed to be cheap. + fn metadata(&self) -> &CallableMetadata; + + /// Executes the function. + /// + /// `args` contains the arguments to the function call. + /// + /// `machine` provides mutable access to the current state of the machine invoking the function. + async fn exec(&self, scope: Scope<'_>) -> CallResult<()>; +} diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs new file mode 100644 index 00000000..546a762a --- /dev/null +++ b/core2/src/compiler/args.rs @@ -0,0 +1,94 @@ +// 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. + +//! Common compilers for callable arguments. + +use crate::ast::{ArgSep, ArgSpan, CallSpan}; +use crate::bytecode::{self, Register, RegisterScope}; +use crate::callable::{CallableMetadata, CallableSyntax}; +use crate::compiler::codegen::Codegen; +use crate::compiler::exprs::compile_expr; +use crate::compiler::syms::TempSymtable; +use crate::compiler::{Error, Result, SymbolKey}; +use crate::reader::LineCol; + +/// Finds the syntax definition that matches the given argument count. +/// +/// Returns an error if no syntax matches, and panics if multiple syntaxes match (which would +/// indicate an ambiguous callable definition). +fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&CallableSyntax> { + let mut matches = md.syntaxes().iter().filter(|s| s.expected_nargs().contains(&nargs)); + let syntax = matches.next(); + match syntax { + Some(syntax) => { + debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); + Ok(syntax) + } + None => Err(Error::CallableSyntax(pos, md.clone())), + } +} + +/// Compiles the arguments of a callable invocation. +/// +/// Returns the first register containing the compiled arguments. Arguments are laid out as +/// pairs of type tag and value registers, allowing the callable to interpret them at runtime. +pub(super) fn compile_args( + span: CallSpan, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + codegen: &mut Codegen, +) -> Result { + let key = SymbolKey::from(&span.vref.name); + let key_pos = span.vref_pos; + + let Some(md) = symtable.get_callable(&key) else { + return Err(Error::UndefinedSymbol(key_pos, span.vref.clone(), RegisterScope::Global)); + }; + + let mut scope = symtable.temp_scope(); + + let _syntax = find_syntax(md, key_pos, span.args.len())?; + + // Arguments are represented as 1 or 2 consecutive registers. + // + // The first register always contains a `VarArgTag`, which indicates the type of + // separator following the argument and, if an argument is present, its type. + // The second register is only present if there is an argument. + // + // The caller must iterate over all tags until it finds `ArgSep::End`. + let nargs = span.args.len(); + for ArgSpan { expr, sep, sep_pos } in span.args { + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + + let tag = match expr { + None => bytecode::VarArgTag::Missing(sep), + Some(expr) => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + let etype = compile_expr(codegen, symtable, temp_value, expr)?; + bytecode::VarArgTag::Immediate(sep, etype) + } + }; + codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); + } + if nargs == 0 { + let temp = scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?; + codegen.emit( + bytecode::make_load_integer(temp, bytecode::VarArgTag::Missing(ArgSep::End).make_u16()), + key_pos, + ); + } + + scope.first().map_err(|e| Error::from_syms(e, key_pos)) +} diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs new file mode 100644 index 00000000..b8c8b8a0 --- /dev/null +++ b/core2/src/compiler/codegen.rs @@ -0,0 +1,165 @@ +// 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. + +//! Code generation for the EndBASIC compiler. + +use crate::ast::ExprType; +use crate::bytecode::{self, Register}; +use crate::compiler::ids::HashMapWithIds; +use crate::compiler::{Error, Result, SymbolKey}; +use crate::image::{DebugInfo, Image}; +use crate::mem::Datum; +use crate::reader::LineCol; +use std::collections::HashMap; +use std::convert::TryFrom; + +/// Alias for instruction addresses in the generated code. +type Address = usize; + +/// Represents a fixup that needs to be applied to an instruction after all symbols have been +/// located. +pub(super) enum Fixup { + /// Fixup to resolve a user-defined call target address into a `CALL` instruction. + Call(Register, SymbolKey), + + /// Fixup to record the number of local variables to allocate in an `ENTER` instruction. + Enter(u8), + + /// Fixup to resolve a label target address into a `GOSUB` instruction. + Gosub(SymbolKey), + + /// Fixup to resolve a label target address into a `GOTO` (jump) instruction. + Goto(SymbolKey), +} + +/// The code generator. +#[derive(Default)] +pub(super) struct Codegen { + /// The instructions being generated. + code: Vec, + + /// The constants pool for the image being generated. + constants: HashMapWithIds, + + /// Collection of fixups to apply after code generation. + fixups: HashMap, + + /// Line/column information for every instruction in `code`. + instr_linecols: Vec, + + /// Map of label names to their target addresses. + labels: HashMap, + + /// Map of user callable names to their target addresses. + user_callables_addresses: HashMap, +} + +impl Codegen { + /// Returns the address of the next instruction to be emitted. + pub(super) fn next_pc(&self) -> Address { + self.code.len() + } + + /// Appends a new instruction `op` generated at `pos` to the code and returns its address. + pub(super) fn emit(&mut self, op: u32, pos: LineCol) -> Address { + self.code.push(op); + self.instr_linecols.push(pos); + self.code.len() - 1 + } + + /// Records a `fixup` that needs to be applied at `addr`. + pub(super) fn add_fixup(&mut self, addr: usize, fixup: Fixup) { + let previous = self.fixups.insert(addr, fixup); + debug_assert!(previous.is_none(), "Cannot handle more than one fixup per address"); + } + + /// Gets the ID of a `constant`, adding it to the constants table if it isn't yet there. + pub(super) fn get_constant(&mut self, constant: Datum, pos: LineCol) -> Result { + match self.constants.get(&constant) { + Some((_etype, id)) => Ok(id), + None => { + let etype = constant.etype(); + match self.constants.insert(constant, etype) { + Some((_etype, id)) => Ok(id), + None => Err(Error::OutOfConstants(pos)), + } + } + } + } + + /// Records the location of a user-defined callable. + pub(super) fn define_user_callable(&mut self, key: SymbolKey, address: Address) { + self.user_callables_addresses.insert(key, address); + } + + /// Records the location of a label. + pub(super) fn define_label(&mut self, key: SymbolKey, address: Address) { + self.labels.insert(key, address); + } + + /// Converts a symbolic `target` address into a 16-bit relative address from `pos`. + fn make_target(target: usize, pos: LineCol) -> Result { + match u16::try_from(target) { + Ok(num) => Ok(num), + Err(_) => Err(Error::TargetTooFar(pos, target)), + } + } + + /// Applies all registered fixups to the generated code. + fn apply_fixups(&mut self) -> Result<()> { + for (addr, fixup) in self.fixups.drain() { + let pos = self.instr_linecols[addr]; + let instr = match fixup { + Fixup::Call(reg, key) => { + let target = self.user_callables_addresses.get(&key).expect("Must be present"); + bytecode::make_call(reg, Self::make_target(*target, pos)?) + } + Fixup::Enter(nargs) => bytecode::make_enter(nargs), + Fixup::Gosub(key) => { + let target = self.labels.get(&key).expect("Must be present"); + bytecode::make_gosub(Self::make_target(*target, pos)?) + } + Fixup::Goto(key) => { + let target = self.labels.get(&key).expect("Must be present"); + bytecode::make_jump(Self::make_target(*target, pos)?) + } + }; + self.code[addr] = instr; + } + debug_assert!(self.fixups.is_empty()); + Ok(()) + } + + /// Consumes the code generator and builds a ready-to-use `Image`. + pub(super) fn build_image( + mut self, + upcalls: HashMapWithIds, u16>, + ) -> Result { + self.apply_fixups()?; + + let mut callables = HashMap::default(); + for (key, pc) in self.user_callables_addresses { + let previous = callables.insert(pc, key); + debug_assert!(previous.is_none(), "An address can only start one callable"); + } + + Ok(Image::new( + self.code, + upcalls.keys_to_vec(), + self.constants.keys_to_vec(), + DebugInfo { instr_linecols: self.instr_linecols, callables }, + )) + } +} diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs new file mode 100644 index 00000000..0be454b0 --- /dev/null +++ b/core2/src/compiler/exprs.rs @@ -0,0 +1,192 @@ +// 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. + +//! Functions to convert expressions into bytecode. + +use crate::ast::{BinaryOpSpan, Expr, ExprType}; +use crate::bytecode::{self, Register, RegisterScope}; +use crate::compiler::codegen::{Codegen, Fixup}; +use crate::compiler::syms::{SymbolKey, TempSymtable}; +use crate::compiler::{Error, Result}; +use crate::mem::Datum; +use std::convert::TryFrom; + +/// Compiles an arithmetic binary operation `span` that returns its value into `reg`. +fn compile_arithmetic_binary_op( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + reg: Register, + span: BinaryOpSpan, + op_name: &'static str, +) -> Result { + let mut scope = symtable.temp_scope(); + + let lpos = span.lhs.start_pos(); + let ltemp = scope.alloc().map_err(|e| Error::from_syms(e, lpos))?; + let ltype = compile_expr(codegen, symtable, ltemp, span.lhs)?; + + let rpos = span.rhs.start_pos(); + let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; + let rtype = compile_expr(codegen, symtable, rtemp, span.rhs)?; + + let rtype = match (ltype, rtype) { + // Type-compatible operands. + (ExprType::Double, ExprType::Double) => ExprType::Double, + (ExprType::Integer, ExprType::Integer) => ExprType::Integer, + (ExprType::Text, ExprType::Text) if op_name == "+" => ExprType::Text, + + // Operands requiring type promotion. + (ExprType::Double, ExprType::Integer) => { + codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); + ExprType::Double + } + (ExprType::Integer, ExprType::Double) => { + codegen.emit(bytecode::make_integer_to_double(ltemp), lpos); + ExprType::Double + } + + // Unsupported operand types. + _ => { + return Err(Error::BinaryOpType(span.pos, "+", ltype, rtype)); + } + }; + + match rtype { + ExprType::Boolean => unreachable!(), + + ExprType::Double => { + codegen.emit(bytecode::make_add_double(reg, ltemp, rtemp), span.pos); + } + + ExprType::Integer => { + codegen.emit(bytecode::make_add_integer(reg, ltemp, rtemp), span.pos); + } + + ExprType::Text => { + codegen.emit(bytecode::make_concat(reg, ltemp, rtemp), span.pos); + } + } + + Ok(rtype) +} + +/// Compiles a single expression `expr` and leaves its value in `reg`. +pub(super) fn compile_expr( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + reg: Register, + expr: Expr, +) -> Result { + match expr { + Expr::Add(span) => compile_arithmetic_binary_op(codegen, symtable, reg, *span, "+"), + + Expr::Boolean(span) => { + let value = if span.value { 1 } else { 0 }; + codegen.emit(bytecode::make_load_integer(reg, value), span.pos); + Ok(ExprType::Boolean) + } + + Expr::Call(span) => { + let key = SymbolKey::from(&span.vref.name); + + let Some(md) = symtable.get_callable(&key) else { + return Err(Error::UndefinedSymbol( + span.vref_pos, + span.vref, + RegisterScope::Global, + )); + }; + + let Some(etype) = md.return_type() else { + return Err(Error::NotAFunction(span.vref_pos, span.vref)); + }; + + if md.is_user_defined() { + let addr = codegen.emit(bytecode::make_nop(), span.vref_pos); + codegen.add_fixup(addr, Fixup::Call(reg, key)); + } else { + todo!("Function upcalls not implemented yet"); + } + Ok(etype) + } + + Expr::Double(span) => { + let index = codegen.get_constant(Datum::Double(span.value), span.pos)?; + codegen.emit(bytecode::make_load_constant(reg, index), span.pos); + Ok(ExprType::Double) + } + + Expr::Integer(span) => { + match u16::try_from(span.value) { + Ok(i) => { + codegen.emit(bytecode::make_load_integer(reg, i), span.pos); + } + Err(_) => { + let index = codegen.get_constant(Datum::Integer(span.value), span.pos)?; + codegen.emit(bytecode::make_load_constant(reg, index), span.pos); + } + } + Ok(ExprType::Integer) + } + + Expr::Symbol(span) => { + let (local, etype) = symtable + .get_local_or_global(&span.vref) + .map_err(|e| Error::from_syms(e, span.pos))?; + codegen.emit(bytecode::make_move(reg, local), span.pos); + Ok(etype) + } + + Expr::Text(span) => { + let index = codegen.get_constant(Datum::Text(span.value), span.pos)?; + codegen.emit(bytecode::make_load_integer(reg, index), span.pos); + Ok(ExprType::Text) + } + + _ => todo!(), + } +} + +/// Compiles a single expression, expecting it to be of a `target` type. Applies casts if +/// possible. +pub(super) fn compile_expr_as_type( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + reg: Register, + expr: Expr, + target: ExprType, +) -> Result<()> { + let epos = expr.start_pos(); + let etype = compile_expr(codegen, symtable, reg, expr)?; + if etype == ExprType::Double && target.is_numerical() { + if target == ExprType::Integer { + codegen.emit(bytecode::make_double_to_integer(reg), epos); + } + Ok(()) + } else if etype == ExprType::Integer && target.is_numerical() { + if target == ExprType::Double { + codegen.emit(bytecode::make_integer_to_double(reg), epos); + } + Ok(()) + } else if etype == target { + Ok(()) + } else { + if target.is_numerical() { + Err(Error::NotANumber(epos, etype)) + } else { + Err(Error::TypeMismatch(epos, etype, target)) + } + } +} diff --git a/core2/src/compiler/ids.rs b/core2/src/compiler/ids.rs new file mode 100644 index 00000000..149dc784 --- /dev/null +++ b/core2/src/compiler/ids.rs @@ -0,0 +1,157 @@ +// 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. + +//! ID generators. + +use std::collections::HashMap; +use std::convert::TryFrom; +use std::hash::Hash; + +/// Hash map that assigns sequential identifiers to elements as they are inserted and +/// allows later retrieval of these identifiers and retrieving the inserted values in +/// insertion order. +pub(super) struct HashMapWithIds { + /// The underlying storage mapping keys to their values and assigned identifiers. + map: HashMap, +} + +impl Default for HashMapWithIds { + fn default() -> Self { + Self { map: HashMap::default() } + } +} + +impl HashMapWithIds +where + K: Clone + Eq + Hash, + I: Copy + std::fmt::Debug + Ord + TryFrom, + V: Copy + std::fmt::Debug + PartialEq, +{ + /// Gets the value and identifier for a `key`. + /// + /// Returns `None` if the key is not present. + pub(super) fn get(&self, key: &K) -> Option<(&V, I)> { + self.map.get(key).map(|(v, i)| (v, *i)) + } + + /// Gets mutable access to the value and identifier for a `key`. + /// + /// Returns `None` if the key is not present. + pub(super) fn get_mut(&mut self, key: &K) -> Option<(&mut V, I)> { + self.map.get_mut(key).map(|(v, i)| (v, *i)) + } + + /// Inserts the `key`/`value` pair into the hash map, assigning a new identifier + /// to the `key` if it does not yet have one. + /// + /// If the `key` is already present, returns a pair with the previous value and the already + /// assigned identifier. If the `keys` is not yet present, returns a pair with None and the + /// newly-assigned identifier. + /// + /// Returns `None` when the IDs run out. + pub(super) fn insert(&mut self, key: K, value: V) -> Option<(Option, I)> { + let id = match self.map.get(&key) { + Some((_value, id)) => *id, + None => match I::try_from(self.map.len()) { + Ok(id) => id, + Err(_) => return None, + }, + }; + self.map + .insert(key, (value, id)) + .map(|(prev_value, prev_id)| { + debug_assert_eq!(prev_id, id); + (Some(prev_value), id) + }) + .or(Some((None, id))) + } + + /// Returns the number of assigned identifiers. + pub(super) fn len(&self) -> usize { + self.map.len() + } + + /// Returns the keys in insertion order. + pub(super) fn keys_to_vec(self) -> Vec { + let mut reverse = self.map.into_iter().collect::>(); + reverse.sort_by_key(|(_key, (_value, index))| *index); + reverse.into_iter().map(|(key, _index)| key).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_hash_map_with_ids_basic_hashmap_api() { + let mut map = HashMapWithIds::<&'static str, &'static str, u8>::default(); + + assert_eq!(Some((None, 0)), map.insert("first", "v1")); + assert_eq!(Some((None, 1)), map.insert("second", "v2")); + + assert_eq!(Some((&"v1", 0)), map.get(&"first")); + assert_eq!(Some((&"v2", 1)), map.get(&"second")); + assert_eq!(None, map.get(&"third")); + + { + let mut_first = map.get_mut(&"first"); + assert_eq!(Some((&mut "v1", 0)), mut_first); + *mut_first.unwrap().0 = "edited"; + } + + assert_eq!(Some((&"edited", 0)), map.get(&"first")); + assert_eq!(Some((&"v2", 1)), map.get(&"second")); + assert_eq!(None, map.get(&"third")); + + assert_eq!(2, map.len()); + } + + #[test] + fn test_hash_map_with_ids_use_u8_ids() { + let mut map = HashMapWithIds::<&'static str, (), u8>::default(); + + assert_eq!(Some((None, 0)), map.insert("foo", ())); + assert_eq!(Some((None, 1)), map.insert("bar", ())); + assert_eq!(Some((None, 2)), map.insert("baz", ())); + + assert_eq!(Some((Some(()), 1)), map.insert("bar", ())); + + assert_eq!(["foo", "bar", "baz"], map.keys_to_vec().as_slice()); + } + + #[test] + fn test_hash_map_with_ids_use_usize_ids() { + let mut map = HashMapWithIds::<&'static str, (), usize>::default(); + + assert_eq!(Some((None, 0)), map.insert("foo", ())); + assert_eq!(Some((None, 1)), map.insert("bar", ())); + assert_eq!(Some((None, 2)), map.insert("baz", ())); + + assert_eq!(Some((Some(()), 1)), map.insert("bar", ())); + + assert_eq!(["foo", "bar", "baz"], map.keys_to_vec().as_slice()); + } + + #[test] + fn test_hash_map_with_ids_run_out_of_ids() { + let mut map = HashMapWithIds::::default(); + + for i in 0..(u16::from(u8::MAX) + 1) { + assert!(map.insert(i, ()).is_some()); + } + assert!(map.insert(u16::from(u8::MAX) + 1, ()).is_none()); + } +} diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs new file mode 100644 index 00000000..a33efef6 --- /dev/null +++ b/core2/src/compiler/mod.rs @@ -0,0 +1,131 @@ +// 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. + +//! Compiler for the EndBASIC language into bytecode. + +use crate::ast::{ExprType, VarRef}; +use crate::bytecode::RegisterScope; +use crate::callable::CallableMetadata; +use crate::parser; +use crate::reader::LineCol; +use std::io; + +mod args; + +mod codegen; + +mod exprs; + +mod ids; + +mod syms; +pub use syms::SymbolKey; + +mod top; +pub use top::{compile, only_metadata}; + +/// Errors that can occur during compilation. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Attempt to redefine an already-defined symbol. + #[error("{0}: Cannot redefine {1}")] + AlreadyDefined(LineCol, VarRef), + + /// Type mismatch in a binary operation. + #[error("{0}: Cannot {1} {2} and {3}")] + BinaryOpType(LineCol, &'static str, ExprType, ExprType), + + /// Callable invoked with incorrect syntax. + #[error("{0}: {} expected {}", .1.name(), .1.syntax())] + CallableSyntax(LineCol, CallableMetadata), + + /// Attempt to nest FUNCTION or SUB definitions. + #[error("{0}: Cannot nest FUNCTION or SUB definitions")] + CannotNestUserCallables(LineCol), + + /// Type annotation in a reference doesn't match the variable's type. + #[error("{0}: Incompatible type annotation in {1} reference")] + IncompatibleTypeAnnotationInReference(LineCol, VarRef), + + /// Type mismatch in an assignment. + #[error("{0}: Cannot assign value of type {1} to variable of type {2}")] + IncompatibleTypesInAssignment(LineCol, ExprType, ExprType), + + /// I/O error while reading the source. + #[error("{0}: I/O error during compilation: {1}")] + Io(LineCol, io::Error), + + /// Attempt to call something that is not a function. + #[error("{0}: Cannot call {1} (not a function)")] + NotAFunction(LineCol, VarRef), + + /// Expected a numeric type but got something else. + #[error("{0}: {1} is not a number")] + NotANumber(LineCol, ExprType), + + /// Constants pool has been exhausted. + #[error("{0}: Out of constants")] + OutOfConstants(LineCol), + + /// Register allocation has been exhausted. + #[error("{0}: Out of {1} registers")] + OutOfRegisters(LineCol, RegisterScope), + + /// Upcall table has been exhausted. + #[error("{0}: Out of upcalls")] + OutOfUpcalls(LineCol), + + /// Syntax error from the parser. + #[error("{0}: {1}")] + Parse(LineCol, String), + + /// Jump or call target is too far away. + #[error("{0}: Jump/call target is {1} which is too far")] + TargetTooFar(LineCol, usize), + + /// Type mismatch where a specific type was expected. + #[error("{0}: Expected {2} but found {1}")] + TypeMismatch(LineCol, ExprType, ExprType), + + /// Reference to an undefined symbol. + #[error("{0}: Undefined {2} symbol {1}")] + UndefinedSymbol(LineCol, VarRef, RegisterScope), +} + +impl Error { + /// Annotates an error from the symbol table with the position it arised from. + fn from_syms(value: syms::Error, pos: LineCol) -> Self { + match value { + syms::Error::AlreadyDefined(vref) => Error::AlreadyDefined(pos, vref), + syms::Error::IncompatibleTypeAnnotationInReference(vref) => { + Error::IncompatibleTypeAnnotationInReference(pos, vref) + } + syms::Error::OutOfRegisters(scope) => Error::OutOfRegisters(pos, scope), + syms::Error::UndefinedSymbol(vref, scope) => Error::UndefinedSymbol(pos, vref, scope), + } + } +} + +impl From for Error { + fn from(value: parser::Error) -> Self { + match value { + parser::Error::Bad(pos, message) => Self::Parse(pos, message), + parser::Error::Io(pos, e) => Self::Io(pos, e), + } + } +} + +/// Result type for compilation operations. +pub type Result = std::result::Result; diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs new file mode 100644 index 00000000..92278dc8 --- /dev/null +++ b/core2/src/compiler/syms.rs @@ -0,0 +1,799 @@ +// 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. + +//! Symbol table for EndBASIC compilation. + +use crate::ast::{ExprType, VarRef}; +use crate::bytecode::{Register, RegisterScope}; +use crate::compiler::ids::HashMapWithIds; +use crate::{CallableMetadata, bytecode}; +use std::cell::RefCell; +use std::cmp::max; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt; +use std::rc::Rc; + +/// Errors related to symbols handling. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] // The error messages and names are good enough. +pub(super) enum Error { + #[error("Cannot redefine {0}")] + AlreadyDefined(VarRef), + + #[error("Incompatible type annotation in {0} reference")] + IncompatibleTypeAnnotationInReference(VarRef), + + #[error("Out of {0} registers")] + OutOfRegisters(RegisterScope), + + #[error("Undefined {1} symbol {0}")] + UndefinedSymbol(VarRef, RegisterScope), +} + +/// Result type for symbol table operations. +type Result = std::result::Result; + +/// The key of a symbol in the symbols table. +/// +/// The key is stored in a canonicalized form (uppercase) to make all lookups case-insensitive. +#[derive(Clone, Debug, Hash, Eq, Ord, PartialEq, PartialOrd)] +pub struct SymbolKey(String); + +impl> From for SymbolKey { + fn from(value: R) -> Self { + Self(value.as_ref().to_ascii_uppercase()) + } +} + +impl fmt::Display for SymbolKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Gets the register and type of a local or global variable if it already exists. +fn get_var( + vref: &VarRef, + table: &HashMapWithIds, + make_register: MKR, + scope: RegisterScope, +) -> Result<(Register, ExprType)> +where + MKR: FnOnce(u8) -> std::result::Result, +{ + let key = SymbolKey::from(&vref.name); + match table.get(&key) { + Some((etype, reg)) => { + if !vref.accepts(*etype) { + return Err(Error::IncompatibleTypeAnnotationInReference(vref.clone())); + } + + let reg = make_register(reg).map_err(|_| Error::OutOfRegisters(scope))?; + Ok((reg, *etype)) + } + + None => Err(Error::UndefinedSymbol(vref.clone(), scope)), + } +} + +/// Defines a new local or global variable and assigns a register to it. +/// +/// Panics if the variable already exists. +fn put_var( + key: SymbolKey, + vtype: ExprType, + table: &mut HashMapWithIds, + make_register: MKR, + scope: RegisterScope, +) -> Result +where + MKR: FnOnce(u8) -> std::result::Result, +{ + match table.insert(key, vtype) { + Some((None, reg)) => Ok(make_register(reg).map_err(|_| Error::OutOfRegisters(scope))?), + + Some((Some(_old_etype), _reg)) => { + unreachable!("Cannot redefine variable; caller must check for presence first"); + } + + None => Err(Error::OutOfRegisters(scope)), + } +} + +/// Representation of the symbol table for global symbols. +/// +/// Globals are variables and callables that are visible from any scope. +pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { + /// Map of global variable names to their types and assigned registers. + globals: HashMapWithIds, + + /// Reference to the built-in callable metadata provided by the runtime. + upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>, + + /// Map of user-defined callable names to their metadata. + user_callables: HashMap, +} + +impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { + /// Creates a new global symbol table that knows about the given `upcalls`. + pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>) -> Self { + Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } + } + + /// Enters a new local scope. + pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, 'ukey, 'umd, '_> { + LocalSymtable::new(self) + } + + /// Gets a global variable by its `vref`. + pub(crate) fn get_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + get_var(vref, &self.globals, Register::global, RegisterScope::Global) + } + + /// Creates a new global variable `key` of `vtype`. + pub(crate) fn put_global(&mut self, key: SymbolKey, vtype: ExprType) -> Result { + put_var(key, vtype, &mut self.globals, Register::global, RegisterScope::Global) + } + + /// Defines a new user-defined `vref` callable with `md` metadata. + pub(crate) fn define_user_callable( + &mut self, + vref: &VarRef, + md: CallableMetadata, + ) -> Result<()> { + let key = SymbolKey::from(&vref.name); + let previous = self.user_callables.insert(key, md); + if previous.is_none() { Ok(()) } else { Err(Error::AlreadyDefined(vref.clone())) } + } + + /// Gets a callable by its name `key`. + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { + self.user_callables.get(key).or(self.upcalls.get(key).copied()) + } +} + +/// Representation of the symbol table for a local scope. +/// +/// A local scope can see all global symbols and defines its own symbols, which can shadow the +/// global ones. +pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { + /// Reference to the parent global symbol table. + symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>, + + /// Map of local variable names to their types and assigned registers. + locals: HashMapWithIds, + + /// Maximum number of allocated temporary registers in all possible evaluation scopes created + /// by this local symtable. This is used to determine the size of the scope for register + /// allocation purposes at runtime. + count_temps: u8, +} + +impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { + /// Creates a new local symbol table within the context of a global `symtable`. + fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>) -> Self { + Self { symtable, locals: HashMapWithIds::default(), count_temps: 0 } + } + + /// Consumes the local scope and returns the number of local variables defined, which includes + /// the locals themselves and any temporaries used by the scope. + pub(crate) fn leave_scope(self) -> Result { + match u8::try_from(self.locals.len() + usize::from(self.count_temps)) { + Ok(nregs) => Ok(nregs), + Err(_) => Err(Error::OutOfRegisters(RegisterScope::Local)), + } + } + + /// Defines a new user-defined `vref` callable with `md` metadata. + pub(crate) fn define_user_callable( + &mut self, + vref: &VarRef, + md: CallableMetadata, + ) -> Result<()> { + self.symtable.define_user_callable(vref, md) + } + + /// Freezes this table to get a `TempSymtable` that can be used to compile expressions. + pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, 'ukey, 'umd, '_, 'a> { + TempSymtable::new(self) + } + + /// Creates a new global variable `key` of `vtype`. + pub(crate) fn put_global(&mut self, key: SymbolKey, vtype: ExprType) -> Result { + self.symtable.put_global(key, vtype) + } + + /// Gets a variable by its `vref`, looking for it in the local and global scopes. + pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + match get_var(vref, &self.locals, Register::local, RegisterScope::Local) { + Ok(local) => Ok(local), + Err(Error::UndefinedSymbol(..)) => self.symtable.get_global(vref), + Err(e) => Err(e), + } + } + + /// Gets a callable by its name `key`. + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { + self.symtable.get_callable(key) + } + + /// Creates a new local variable `key` of `vtype`. + pub(crate) fn put_local(&mut self, key: SymbolKey, vtype: ExprType) -> Result { + put_var(key, vtype, &mut self.locals, Register::local, RegisterScope::Local) + } + + /// Changes the type of an existing local variable `vref` to `new_etype`. + /// + /// This is used for type inference on first assignment. + pub(crate) fn fixup_local_type(&mut self, vref: &VarRef, new_etype: ExprType) -> Result<()> { + let key = SymbolKey::from(&vref.name); + // TODO: Verify reference type. + match self.locals.get_mut(&key) { + Some((etype, _reg)) => { + *etype = new_etype; + Ok(()) + } + None => Err(Error::UndefinedSymbol(vref.clone(), RegisterScope::Local)), + } + } +} + +/// A read-only view into a `SymTable` that allows allocating temporary registers. +/// +/// This layer on top of `LocalSymtable` may seem redundant because all of the temporary +/// register manipulation happens in `TempScope`, but it is necessary to have this layer +/// to forbid mutations to local variables. We need to be able to pass a `TempSymtable` +/// across recursive function calls (for expression evaluation), but at the same time we +/// need each call site to have its own `TempScope` for temporary register cleanup. +pub(crate) struct TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { + /// Reference to the underlying local symbol table. + symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>, + + /// Index of the next temporary register to allocate. + next_temp: Rc>, + + /// Maximum number of allocated temporary registers in a given evaluation (recursion) path. + count_temps: Rc>, +} + +impl<'uref, 'ukey, 'umd, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { + fn drop(&mut self) { + debug_assert_eq!(0, *self.next_temp.borrow(), "Unbalanced temp drops"); + self.symtable.count_temps = max(self.symtable.count_temps, *self.count_temps.borrow()); + } +} + +impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { + /// Creates a new temporary symbol table from a `local` table. + fn new(symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>) -> Self { + Self { + symtable, + next_temp: Rc::from(RefCell::from(0)), + count_temps: Rc::from(RefCell::from(0)), + } + } + + /// Gets a variable by its `vref`, looking for it in the local and global scopes. + pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + self.symtable.get_local_or_global(vref) + } + + /// Gets a callable by its name `key`. + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { + self.symtable.get_callable(key) + } + + /// Enters a new temporary scope. + pub(crate) fn temp_scope(&self) -> TempScope { + let nlocals = u8::try_from(self.symtable.locals.len()) + .expect("Cannot have allocated more locals than u8"); + TempScope { + nlocals, + ntemps: 0, + next_temp: self.next_temp.clone(), + count_temps: self.count_temps.clone(), + } + } +} + +/// A scope for temporary registers. +/// +/// Temporaries are allocated on demand and are cleaned up when the scope is dropped. +pub(crate) struct TempScope { + /// Number of local variables in the enclosing scope, used as the base for temporary registers. + nlocals: u8, + + /// Number of temporary registers allocated by this scope. + ntemps: u8, + + /// Shared counter for the next temporary register index to allocate. + next_temp: Rc>, + + /// Shared counter tracking the maximum number of temporary registers used. + count_temps: Rc>, +} + +impl Drop for TempScope { + fn drop(&mut self) { + let mut next_temp = self.next_temp.borrow_mut(); + debug_assert!(*next_temp >= self.ntemps); + *next_temp -= self.ntemps; + } +} + +impl TempScope { + /// Returns the first register available for this scope. + pub(crate) fn first(&mut self) -> Result { + Register::local(self.nlocals).map_err(|_| Error::OutOfRegisters(RegisterScope::Temp)) + } + + /// Allocates a new temporary register. + pub(crate) fn alloc(&mut self) -> Result { + let temp; + let new_next_temp; + { + let mut next_temp = self.next_temp.borrow_mut(); + temp = *next_temp; + self.ntemps += 1; + new_next_temp = match next_temp.checked_add(1) { + Some(reg) => reg, + None => return Err(Error::OutOfRegisters(RegisterScope::Temp)), + }; + *next_temp = new_next_temp; + } + + { + let mut count_temps = self.count_temps.borrow_mut(); + *count_temps = max(*count_temps, new_next_temp); + } + + match u8::try_from(usize::from(self.nlocals) + usize::from(temp)) { + Ok(reg) => { + Ok(Register::local(reg).map_err(|_| Error::OutOfRegisters(RegisterScope::Temp))?) + } + Err(_) => Err(Error::OutOfRegisters(RegisterScope::Temp)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::CallableMetadataBuilder; + + #[test] + fn test_symbol_key_case_insensitive() { + assert_eq!(SymbolKey::from("foo"), SymbolKey::from("FOO")); + assert_eq!(SymbolKey::from("Foo"), SymbolKey::from("fOo")); + } + + #[test] + fn test_symbol_key_display() { + assert_eq!("FOO", format!("{}", SymbolKey::from("foo"))); + } + + #[test] + fn test_global_put_and_get() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + + let reg = global.put_global(SymbolKey::from("x"), ExprType::Integer)?; + assert_eq!(Register::global(0).unwrap(), reg); + + let reg = global.put_global(SymbolKey::from("y"), ExprType::Text)?; + assert_eq!(Register::global(1).unwrap(), reg); + + // Lookup with untyped ref succeeds. + let (reg, etype) = global.get_global(&VarRef::new("x", None))?; + assert_eq!(Register::global(0).unwrap(), reg); + assert_eq!(ExprType::Integer, etype); + + // Lookup with matching typed ref succeeds. + let (reg, etype) = global.get_global(&VarRef::new("y", Some(ExprType::Text)))?; + assert_eq!(Register::global(1).unwrap(), reg); + assert_eq!(ExprType::Text, etype); + + Ok(()) + } + + #[test] + fn test_global_get_case_insensitive() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + global.put_global(SymbolKey::from("MyVar"), ExprType::Double)?; + + let (reg, etype) = global.get_global(&VarRef::new("myvar", None))?; + assert_eq!(Register::global(0).unwrap(), reg); + assert_eq!(ExprType::Double, etype); + + let (reg2, _) = global.get_global(&VarRef::new("MYVAR", None))?; + assert_eq!(reg, reg2); + Ok(()) + } + + #[test] + fn test_global_get_incompatible_type() { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + global.put_global(SymbolKey::from("x"), ExprType::Integer).unwrap(); + + let err = global.get_global(&VarRef::new("x", Some(ExprType::Text))).unwrap_err(); + assert_eq!("Incompatible type annotation in x$ reference", err.to_string()); + } + + #[test] + fn test_global_get_undefined() { + let upcalls = HashMap::default(); + let global = GlobalSymtable::new(&upcalls); + + let err = global.get_global(&VarRef::new("x", None)).unwrap_err(); + assert_eq!("Undefined global symbol x", err.to_string()); + } + + #[test] + fn test_local_put_and_get() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let reg = local.put_local(SymbolKey::from("a"), ExprType::Boolean)?; + assert_eq!(Register::local(0).unwrap(), reg); + + let reg = local.put_local(SymbolKey::from("b"), ExprType::Double)?; + assert_eq!(Register::local(1).unwrap(), reg); + + let (reg, etype) = local.get_local_or_global(&VarRef::new("a", None))?; + assert_eq!(Register::local(0).unwrap(), reg); + assert_eq!(ExprType::Boolean, etype); + + Ok(()) + } + + #[test] + fn test_local_shadows_global() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + global.put_global(SymbolKey::from("x"), ExprType::Integer)?; + + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("x"), ExprType::Text)?; + + let (reg, etype) = local.get_local_or_global(&VarRef::new("x", None))?; + assert_eq!(Register::local(0).unwrap(), reg); + assert_eq!(ExprType::Text, etype); + + Ok(()) + } + + #[test] + fn test_local_falls_through_to_global() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + global.put_global(SymbolKey::from("g"), ExprType::Integer)?; + + let local = global.enter_scope(); + let (reg, etype) = local.get_local_or_global(&VarRef::new("g", None))?; + assert_eq!(Register::global(0).unwrap(), reg); + assert_eq!(ExprType::Integer, etype); + + Ok(()) + } + + #[test] + fn test_local_get_undefined() { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let local = global.enter_scope(); + + let err = local.get_local_or_global(&VarRef::new("nope", None)).unwrap_err(); + assert_eq!("Undefined global symbol nope", err.to_string()); + } + + #[test] + fn test_local_put_global_through_local() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let reg = local.put_global(SymbolKey::from("g"), ExprType::Integer)?; + assert_eq!(Register::global(0).unwrap(), reg); + + // Should be visible from the local scope via fallthrough. + let (reg, etype) = local.get_local_or_global(&VarRef::new("g", None))?; + assert_eq!(Register::global(0).unwrap(), reg); + assert_eq!(ExprType::Integer, etype); + + Ok(()) + } + + #[test] + fn test_fixup_local_type() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + local.put_local(SymbolKey::from("x"), ExprType::Integer)?; + local.fixup_local_type(&VarRef::new("x", None), ExprType::Double)?; + + let (_, etype) = local.get_local_or_global(&VarRef::new("x", None))?; + assert_eq!(ExprType::Double, etype); + + Ok(()) + } + + #[test] + fn test_fixup_local_type_undefined() { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let err = + local.fixup_local_type(&VarRef::new("nope", None), ExprType::Integer).unwrap_err(); + assert_eq!("Undefined local symbol nope", err.to_string()); + } + + #[test] + fn test_leave_scope_counts_locals_only() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), ExprType::Integer)?; + local.put_local(SymbolKey::from("b"), ExprType::Integer)?; + assert_eq!(2, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_leave_scope_counts_locals_and_temps() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), ExprType::Integer)?; + { + let temp = local.frozen(); + let mut scope = temp.temp_scope(); + scope.alloc()?; + scope.alloc()?; + } + assert_eq!(3, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_leave_scope_empty() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let local = global.enter_scope(); + assert_eq!(0, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_define_and_get_user_callable() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + + let md = CallableMetadataBuilder::new("MY_FUNC") + .with_return_type(ExprType::Integer) + .test_build(); + global.define_user_callable(&VarRef::new("my_func", None), md)?; + + let found = global.get_callable(&SymbolKey::from("my_func")); + assert!(found.is_some()); + assert_eq!("MY_FUNC", found.unwrap().name()); + + Ok(()) + } + + #[test] + fn test_define_user_callable_already_defined() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + + let md = CallableMetadataBuilder::new("DUP").test_build(); + global.define_user_callable(&VarRef::new("dup", None), md)?; + + let md2 = CallableMetadataBuilder::new("DUP").test_build(); + let err = global.define_user_callable(&VarRef::new("dup", None), md2).unwrap_err(); + assert_eq!("Cannot redefine dup", err.to_string()); + + Ok(()) + } + + #[test] + fn test_define_user_callable_via_local() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let md = CallableMetadataBuilder::new("SUB1").test_build(); + local.define_user_callable(&VarRef::new("sub1", None), md)?; + + let found = local.get_callable(&SymbolKey::from("sub1")); + assert!(found.is_some()); + + Ok(()) + } + + #[test] + fn test_get_callable_upcall() { + let key = SymbolKey::from("BUILTIN"); + let md = CallableMetadataBuilder::new("BUILTIN").test_build(); + let mut upcalls_map = HashMap::new(); + upcalls_map.insert(&key, &md); + + let global = GlobalSymtable::new(&upcalls_map); + let found = global.get_callable(&SymbolKey::from("builtin")); + assert!(found.is_some()); + assert_eq!("BUILTIN", found.unwrap().name()); + } + + #[test] + fn test_user_callable_shadows_upcall() { + let key = SymbolKey::from("SHARED"); + let builtin_md = + CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Boolean).test_build(); + let mut upcalls_map = HashMap::new(); + upcalls_map.insert(&key, &builtin_md); + + let mut global = GlobalSymtable::new(&upcalls_map); + let user_md = + CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Integer).test_build(); + global.define_user_callable(&VarRef::new("shared", None), user_md).unwrap(); + + let found = global.get_callable(&SymbolKey::from("shared")).unwrap(); + assert_eq!(Some(ExprType::Integer), found.return_type()); + } + + #[test] + fn test_get_callable_not_found() { + let upcalls = HashMap::default(); + let global = GlobalSymtable::new(&upcalls); + assert!(global.get_callable(&SymbolKey::from("nope")).is_none()); + } + + #[test] + fn test_temp_scope_first() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), ExprType::Integer)?; + local.put_local(SymbolKey::from("b"), ExprType::Integer)?; + { + let temp = local.frozen(); + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(2).unwrap(), scope.first()?); + } + Ok(()) + } + + #[test] + fn test_temp_scope_first_no_locals() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + { + let temp = local.frozen(); + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(0).unwrap(), scope.first()?); + } + Ok(()) + } + + #[test] + fn test_temp_scope() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + assert_eq!( + Register::local(0).unwrap(), + local.put_local(SymbolKey::from("foo"), ExprType::Integer)? + ); + { + let temp = local.frozen(); + { + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(1).unwrap(), scope.alloc()?); + { + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(2).unwrap(), scope.alloc()?); + assert_eq!(Register::local(3).unwrap(), scope.alloc()?); + assert_eq!(Register::local(4).unwrap(), scope.alloc()?); + } + { + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(2).unwrap(), scope.alloc()?); + assert_eq!(Register::local(3).unwrap(), scope.alloc()?); + } + assert_eq!(Register::local(2).unwrap(), scope.alloc()?); + } + } + { + let temp = local.frozen(); + { + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(1).unwrap(), scope.alloc()?); + } + } + assert_eq!(5, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_temp_scope_lookup_vars() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + global.put_global(SymbolKey::from("g"), ExprType::Integer)?; + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("l"), ExprType::Text)?; + + { + let temp = local.frozen(); + + let (reg, etype) = temp.get_local_or_global(&VarRef::new("l", None))?; + assert_eq!(Register::local(0).unwrap(), reg); + assert_eq!(ExprType::Text, etype); + + let (reg, etype) = temp.get_local_or_global(&VarRef::new("g", None))?; + assert_eq!(Register::global(0).unwrap(), reg); + assert_eq!(ExprType::Integer, etype); + } + + Ok(()) + } + + #[test] + fn test_temp_scope_lookup_callable() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let md = CallableMetadataBuilder::new("FOO").test_build(); + global.define_user_callable(&VarRef::new("foo", None), md)?; + + let mut local = global.enter_scope(); + { + let temp = local.frozen(); + assert!(temp.get_callable(&SymbolKey::from("foo")).is_some()); + assert!(temp.get_callable(&SymbolKey::from("nope")).is_none()); + } + + Ok(()) + } + + #[test] + fn test_multiple_scopes_independent_locals() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + + { + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("x"), ExprType::Integer)?; + assert_eq!(1, local.leave_scope()?); + } + + { + let mut local = global.enter_scope(); + // "x" should not exist in this new scope. + let err = local.get_local_or_global(&VarRef::new("x", None)).unwrap_err(); + assert_eq!("Undefined global symbol x", err.to_string()); + + let reg = local.put_local(SymbolKey::from("y"), ExprType::Double)?; + assert_eq!(Register::local(0).unwrap(), reg); + assert_eq!(1, local.leave_scope()?); + } + + Ok(()) + } +} diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs new file mode 100644 index 00000000..e5b48a2a --- /dev/null +++ b/core2/src/compiler/top.rs @@ -0,0 +1,316 @@ +// 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. + +//! Entry point to the compilation, handling top-level definitions. + +use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement}; +use crate::bytecode::{self, RegisterScope}; +use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; +use crate::compiler::args::compile_args; +use crate::compiler::codegen::{Codegen, Fixup}; +use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; +use crate::compiler::ids::HashMapWithIds; +use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey}; +use crate::compiler::{Error, Result}; +use crate::image::Image; +use crate::reader::LineCol; +use crate::{Callable, CallableMetadataBuilder, parser}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::io; +use std::iter::Iterator; +use std::rc::Rc; + +/// Bag of state required by various top-level compilation functions. +/// +/// This type exists to minimize the number of complex arguments passed across functions. +/// If possible, avoid passing it and instead pass the minimum set of required fields. +#[derive(Default)] +struct Context { + /// The code generator accumulating bytecode instructions. + codegen: Codegen, + + /// Map of built-in callable names to their return types and assigned upcall IDs. + upcalls: HashMapWithIds, u16>, + + /// Collection of user-defined callable definitions to be compiled after the main scope. + user_callables: Vec, +} + +impl Context { + /// Gets the existing upcall ID for the given `key` or creates a new one. + fn get_upcall(&mut self, key: SymbolKey, etype: Option, pos: LineCol) -> Result { + if etype.is_some() { + todo!("Function upcalls not implemented yet"); + } + match self.upcalls.get(&key) { + Some((_etype, id)) => Ok(id), + None => match self.upcalls.insert(key, etype) { + Some((_etype, id)) => Ok(id), + None => Err(Error::OutOfUpcalls(pos)), + }, + } + } +} + +/// Compiles an assignment statement `span` into the `codegen` block. +fn compile_assignment( + codegen: &mut Codegen, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: AssignmentSpan, +) -> Result<()> { + let vref_pos = span.vref_pos; + + let (reg, etype) = match symtable.get_local_or_global(&span.vref) { + Ok((reg, etype)) => (reg, Some(etype)), + + Err(syms::Error::UndefinedSymbol(..)) => { + let key = SymbolKey::from(span.vref.name.clone()); + let reg = symtable + .put_local(key, span.vref.ref_type.unwrap_or(ExprType::Integer)) + .map_err(|e| Error::from_syms(e, span.vref_pos))?; + match span.vref.ref_type { + Some(etype) => (reg, Some(etype)), + None => (reg, None), + } + } + + Err(e) => return Err(Error::from_syms(e, vref_pos)), + }; + + if let Some(etype) = etype { + // The destination variable already exists. Try to compile the expression into its target + // type and fail otherwise with a better error message. + match compile_expr_as_type(codegen, &mut symtable.frozen(), reg, span.expr, etype) { + Err(Error::TypeMismatch(pos, actual, expected)) => { + return Err(Error::IncompatibleTypesInAssignment(pos, actual, expected)); + } + r => return r, + } + } + + // The destination variable doesn't exist yet but `symtable.put_local` already inserted it + // with the default type we gave above as part of assigning it a register. Use the + // expression's type to fix up the type in the symbols table. + let etype = compile_expr(codegen, &mut symtable.frozen(), reg, span.expr)?; + symtable.fixup_local_type(&span.vref, etype).map_err(|e| Error::from_syms(e, vref_pos)) +} + +/// Compiles a single `stmt` into the `ctx`. +fn compile_stmt( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + stmt: Statement, +) -> Result<()> { + match stmt { + Statement::Assignment(span) => { + compile_assignment(&mut ctx.codegen, symtable, span)?; + } + + Statement::Call(span) => { + let key = SymbolKey::from(&span.vref.name); + let key_pos = span.vref_pos; + + let mut symtable = symtable.frozen(); + + let Some(md) = symtable.get_callable(&key) else { + return Err(Error::UndefinedSymbol( + key_pos, + span.vref.clone(), + RegisterScope::Global, + )); + }; + let is_user_defined = md.is_user_defined(); + + let first_temp = compile_args(span, &mut symtable, &mut ctx.codegen)?; + + if is_user_defined { + let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos); + ctx.codegen.add_fixup(addr, Fixup::Call(first_temp, key)); + } else { + let upcall = ctx.get_upcall(key, None, key_pos)?; + ctx.codegen.emit(bytecode::make_upcall(upcall, first_temp), key_pos); + } + } + + Statement::Callable(span) => { + let mut syntax = vec![]; + for (i, param) in span.params.iter().enumerate() { + let sep = if i == span.params.len() - 1 { + ArgSepSyntax::End + } else { + ArgSepSyntax::Exactly(ArgSep::Long) + }; + syntax.push(SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Owned(param.name.to_owned()), + vtype: param.ref_type.unwrap_or(ExprType::Integer), + }, + sep, + )); + } + + let mut builder = CallableMetadataBuilder::new_dynamic(span.name.name.to_owned()) + .with_dynamic_syntax(vec![(syntax, None)]); + if let Some(ctype) = span.name.ref_type { + builder = builder.with_return_type(ctype); + } + + symtable + .define_user_callable(&span.name, builder.build()) + .map_err(|e| Error::from_syms(e, span.name_pos))?; + ctx.user_callables.push(span); + } + + Statement::Dim(span) => { + let key = SymbolKey::from(span.name); + let name_pos = span.name_pos; + let reg = if span.shared { + symtable.put_global(key, span.vtype) + } else { + symtable.put_local(key, span.vtype) + } + .map_err(|e| Error::from_syms(e, name_pos))?; + let instr = match span.vtype { + ExprType::Boolean => bytecode::make_load_integer(reg, 0), + ExprType::Double => bytecode::make_load_integer(reg, 0), + ExprType::Integer => bytecode::make_load_integer(reg, 0), + ExprType::Text => bytecode::make_alloc(reg, ExprType::Text), + }; + ctx.codegen.emit(instr, name_pos); + } + + Statement::End(span) => { + let mut symtable = symtable.frozen(); + let mut scope = symtable.temp_scope(); + let reg = scope.alloc().map_err(|e| Error::from_syms(e, span.pos))?; + match span.code { + Some(expr) => { + compile_expr_as_type( + &mut ctx.codegen, + &mut symtable, + reg, + expr, + ExprType::Integer, + )?; + } + None => { + ctx.codegen.emit(bytecode::make_load_integer(reg, 0), span.pos); + } + } + ctx.codegen.emit(bytecode::make_end(reg), span.pos); + } + + Statement::Gosub(span) => { + let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); + ctx.codegen.add_fixup(addr, Fixup::Gosub(SymbolKey::from(span.target))); + } + + Statement::Goto(span) => { + let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); + ctx.codegen.add_fixup(addr, Fixup::Goto(SymbolKey::from(span.target))); + } + + Statement::Label(span) => { + ctx.codegen.define_label(SymbolKey::from(span.name), ctx.codegen.next_pc()); + } + + Statement::Return(span) => { + ctx.codegen.emit(bytecode::make_return(), span.pos); + } + + _ => todo!(), + } + Ok(()) +} + +/// Compiles a sequence of `stmts` that all live in the same `symtable` scope. +fn compile_scope( + ctx: &mut Context, + mut symtable: LocalSymtable<'_, '_, '_, '_>, + stmts: I, +) -> Result<()> +where + I: Iterator>, +{ + let enter = ctx.codegen.emit(bytecode::make_nop(), LineCol { line: 0, col: 0 }); + for stmt in stmts { + compile_stmt(ctx, &mut symtable, stmt?)?; + } + let nlocals = + symtable.leave_scope().map_err(|e| Error::from_syms(e, LineCol { line: 0, col: 0 }))?; + ctx.codegen.add_fixup(enter, Fixup::Enter(nlocals)); + Ok(()) +} + +/// Compiles all user-defined callables that have been captured in `ctx`. +fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> Result<()> { + let user_callables: Vec = ctx.user_callables.drain(..).collect(); + debug_assert!(ctx.user_callables.is_empty()); + + for callable in user_callables { + let start_pc = ctx.codegen.next_pc(); + + let key_pos = callable.name_pos; + let key = SymbolKey::from(callable.name.name); + + let mut symtable = symtable.enter_scope(); + if let Some(vtype) = callable.name.ref_type { + // The call protocol expects the return value to be in the _first_ local variable + // so allocate it early. + symtable.put_local(key.clone(), vtype).map_err(|e| Error::from_syms(e, key_pos))?; + } + + compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; + if let Some(span) = ctx.user_callables.first() { + return Err(Error::CannotNestUserCallables(span.name_pos)); + } + + ctx.codegen.emit(bytecode::make_return(), callable.end_pos); + ctx.codegen.define_user_callable(key, start_pc); + } + + Ok(()) +} + +/// Extracts the metadata of all provided `upcalls`. +pub fn only_metadata( + upcalls_by_name: &HashMap>, +) -> HashMap<&SymbolKey, &CallableMetadata> { + let mut upcalls = HashMap::with_capacity(upcalls_by_name.len()); + for (name, callable) in upcalls_by_name { + upcalls.insert(name, callable.metadata()); + } + upcalls +} + +/// Compiles the `input` into an `Image` that can be executed by the VM. +/// +/// `upcalls` contains the metadata of all built-in callables that the compiled code can use. +pub fn compile( + input: &mut dyn io::Read, + upcalls: &HashMap<&SymbolKey, &CallableMetadata>, +) -> Result { + let mut ctx = Context::default(); + + let mut symtable = GlobalSymtable::new(upcalls); + + let program_end = Statement::End(EndSpan { code: None, pos: LineCol { line: 0, col: 0 } }); + compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input).chain([Ok(program_end)]))?; + + compile_user_callables(&mut ctx, &mut symtable)?; + + ctx.codegen.build_image(ctx.upcalls) +} diff --git a/core2/src/image.rs b/core2/src/image.rs new file mode 100644 index 00000000..3751cce9 --- /dev/null +++ b/core2/src/image.rs @@ -0,0 +1,160 @@ +// 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. + +//! Compiled program representation. + +use crate::bytecode::{self, Opcode, Register, opcode_of}; +use crate::compiler::SymbolKey; +use crate::mem::Datum; +use crate::reader::LineCol; +use std::collections::HashMap; + +/// Formats an instruction for debugging. +/// +/// Panics if the instruction is malformed. +pub(crate) fn format_instr(instr: u32) -> String { + match opcode_of(instr) { + Opcode::AddDouble => bytecode::format_add_double(instr), + Opcode::AddInteger => bytecode::format_add_integer(instr), + Opcode::Alloc => bytecode::format_alloc(instr), + Opcode::Call => bytecode::format_call(instr), + Opcode::Concat => bytecode::format_concat(instr), + Opcode::DoubleToInteger => bytecode::format_double_to_integer(instr), + Opcode::End => bytecode::format_end(instr), + Opcode::Enter => bytecode::format_enter(instr), + Opcode::Gosub => bytecode::format_gosub(instr), + Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), + Opcode::Jump => bytecode::format_jump(instr), + Opcode::LoadConstant => bytecode::format_load_constant(instr), + Opcode::LoadInteger => bytecode::format_load_integer(instr), + Opcode::Move => bytecode::format_move(instr), + Opcode::Nop => bytecode::format_nop(instr), + Opcode::Return => bytecode::format_return(instr), + Opcode::Upcall => bytecode::format_upcall(instr), + } +} + +/// Debugging information for a compiled program. +#[derive(Default)] +pub struct DebugInfo { + /// Maps each instruction index to its source location (line and column). + pub(crate) instr_linecols: Vec, + + /// Maps instruction addresses to the names of user-defined callables that start at those + /// addresses. + pub(crate) callables: HashMap, +} + +/// Representation of a compiled EndBASIC program. +/// +/// Images always have at least one instruction so that the VM can make this assumption. +pub struct Image { + /// The bytecode instructions of the compiled program. + pub(crate) code: Vec, + + /// Names of external callables referenced by the program, indexed by upcall ID. + pub(crate) upcalls: Vec, + + /// Pool of constant values used by the program. + pub(crate) constants: Vec, + + /// Debugging information for error reporting and disassembly. + pub(crate) debug_info: DebugInfo, + + /// Marker to prevent external construction; ensures `code` is never empty. + _internal: (), +} + +impl Default for Image { + fn default() -> Self { + Self::new( + vec![ + // The minimum valid program requires an explicit `END` so that the VM knows to + // exit. We can directly reference register 0 because all registers would have + // been cleared and accessing them would result in their default values. + bytecode::make_end(Register::global(0).expect("Global 0 register be valid")), + ], + vec![], + vec![], + DebugInfo { + instr_linecols: vec![LineCol { line: 0, col: 0 }], + callables: HashMap::default(), + }, + ) + } +} + +impl Image { + pub(crate) fn new( + code: Vec, + upcalls: Vec, + constants: Vec, + debug_info: DebugInfo, + ) -> Self { + debug_assert!(!code.is_empty(), "Compiler must ensure the image is not empty"); + debug_assert_eq!(code.len(), debug_info.instr_linecols.len()); + Self { code, upcalls, constants, debug_info, _internal: () } + } + + /// Disassembles the image into a textual representation for debugging. + pub fn disasm(&self) -> Vec { + let mut lines = Vec::with_capacity(self.code.len()); + + for ((i, instr), pos) in self + .code + .iter() + .copied() + .enumerate() + .zip(self.debug_info.instr_linecols.iter().copied()) + { + if let Some(key) = self.debug_info.callables.get(&i) { + lines.push("".to_owned()); + lines.push(format!("-- {} ", key)); + } + + let mut line = format!("{:04}: {}", i, format_instr(instr)); + + while line.len() < 40 { + line.push(' '); + } + line.push_str(&format!("# {}", pos)); + + match opcode_of(instr) { + Opcode::Call => { + let (_reg, target) = bytecode::parse_call(instr); + let target = usize::from(target); + let name = self + .debug_info + .callables + .get(&target) + .expect("All CALL targets must be defined"); + line.push_str(&format!(", {}", name)) + } + + Opcode::Upcall => { + let (index, _first_reg) = bytecode::parse_upcall(instr); + let name = &self.upcalls[usize::from(index)]; + line.push_str(&format!(", {}", name)) + } + + _ => (), + } + + lines.push(line); + } + + lines + } +} diff --git a/core2/src/lexer.rs b/core2/src/lexer.rs new file mode 100644 index 00000000..ab95ee5d --- /dev/null +++ b/core2/src/lexer.rs @@ -0,0 +1,1636 @@ +// EndBASIC +// Copyright 2020 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. + +//! Tokenizer for the EndBASIC language. + +use crate::ast::{ExprType, VarRef}; +use crate::reader::{CharReader, CharSpan, LineCol}; +use std::{fmt, io}; + +/// Result type for the public methods of this module. +type Result = std::result::Result; + +/// Collection of valid tokens. +/// +/// Of special interest are the `Eof` and `Bad` tokens, both of which denote exceptional +/// conditions and require special care. `Eof` indicates that there are no more tokens. +/// `Bad` indicates that a token was bad and contains the reason behind the problem, but the +/// stream remains valid for extraction of further tokens. +#[derive(Clone, PartialEq)] +#[cfg_attr(test, derive(Debug))] +pub enum Token { + Eof, + Eol, + Bad(String), + + Boolean(bool), + Double(f64), + Integer(i32), + Text(String), + Symbol(VarRef), + + Label(String), + + Comma, + Semicolon, + LeftParen, + RightParen, + + Plus, + Minus, + Multiply, + Divide, + Modulo, + Exponent, + + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual, + + And, + Not, + Or, + Xor, + + ShiftLeft, + ShiftRight, + + Case, + Data, + Do, + Else, + Elseif, + End, + Error, + Exit, + For, + Function, + Gosub, + Goto, + If, + Is, + Loop, + Next, + On, + Resume, + Return, + Select, + Sub, + Step, + Then, + To, + Until, + Wend, + While, + + Dim, + Shared, + As, + BooleanName, + DoubleName, + IntegerName, + TextName, +} + +impl fmt::Display for Token { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // This implementation of Display returns the "canonical format" of a token. We could + // instead capture the original text that was in the input stream and store it in the + // TokenSpan and return that. However, most BASIC implementations make input canonical + // so this helps achieve that goal. + match self { + Token::Eof => write!(f, "<>"), + Token::Eol => write!(f, "<>"), + Token::Bad(s) => write!(f, "<<{}>>", s), + + Token::Boolean(false) => write!(f, "FALSE"), + Token::Boolean(true) => write!(f, "TRUE"), + Token::Double(d) => write!(f, "{}", d), + Token::Integer(i) => write!(f, "{}", i), + Token::Text(t) => write!(f, "{}", t), + Token::Symbol(vref) => write!(f, "{}", vref), + + Token::Label(l) => write!(f, "@{}", l), + + Token::Comma => write!(f, ","), + Token::Semicolon => write!(f, ";"), + Token::LeftParen => write!(f, "("), + Token::RightParen => write!(f, ")"), + + Token::Plus => write!(f, "+"), + Token::Minus => write!(f, "-"), + Token::Multiply => write!(f, "*"), + Token::Divide => write!(f, "/"), + Token::Modulo => write!(f, "MOD"), + Token::Exponent => write!(f, "^"), + + Token::Equal => write!(f, "="), + Token::NotEqual => write!(f, "<>"), + Token::Less => write!(f, "<"), + Token::LessEqual => write!(f, "<="), + Token::Greater => write!(f, ">"), + Token::GreaterEqual => write!(f, ">="), + + Token::And => write!(f, "AND"), + Token::Not => write!(f, "NOT"), + Token::Or => write!(f, "OR"), + Token::Xor => write!(f, "XOR"), + + Token::ShiftLeft => write!(f, "<<"), + Token::ShiftRight => write!(f, ">>"), + + Token::Case => write!(f, "CASE"), + Token::Data => write!(f, "DATA"), + Token::Do => write!(f, "DO"), + Token::Else => write!(f, "ELSE"), + Token::Elseif => write!(f, "ELSEIF"), + Token::End => write!(f, "END"), + Token::Error => write!(f, "ERROR"), + Token::Exit => write!(f, "EXIT"), + Token::For => write!(f, "FOR"), + Token::Function => write!(f, "FUNCTION"), + Token::Gosub => write!(f, "GOSUB"), + Token::Goto => write!(f, "GOTO"), + Token::If => write!(f, "IF"), + Token::Is => write!(f, "IS"), + Token::Loop => write!(f, "LOOP"), + Token::Next => write!(f, "NEXT"), + Token::On => write!(f, "ON"), + Token::Resume => write!(f, "RESUME"), + Token::Return => write!(f, "RETURN"), + Token::Select => write!(f, "SELECT"), + Token::Sub => write!(f, "SUB"), + Token::Step => write!(f, "STEP"), + Token::Then => write!(f, "THEN"), + Token::To => write!(f, "TO"), + Token::Until => write!(f, "UNTIL"), + Token::Wend => write!(f, "WEND"), + Token::While => write!(f, "WHILE"), + + Token::Dim => write!(f, "DIM"), + Token::Shared => write!(f, "SHARED"), + Token::As => write!(f, "AS"), + Token::BooleanName => write!(f, "BOOLEAN"), + Token::DoubleName => write!(f, "DOUBLE"), + Token::IntegerName => write!(f, "INTEGER"), + Token::TextName => write!(f, "STRING"), + } + } +} + +/// Extra operations to test properties of a `char` based on the language semantics. +trait CharOps { + /// Returns true if the current character should be considered as finishing a previous token. + fn is_separator(&self) -> bool; + + /// Returns true if the character is a space. + /// + /// Use this instead of `is_whitespace`, which accounts for newlines but we need to handle + /// those explicitly. + fn is_space(&self) -> bool; + + /// Returns true if the character can be part of an identifier. + fn is_word(&self) -> bool; +} + +impl CharOps for char { + fn is_separator(&self) -> bool { + match *self { + '\n' | ':' | '(' | ')' | '\'' | '=' | '<' | '>' | ';' | ',' | '+' | '-' | '*' | '/' + | '^' => true, + ch => ch.is_space(), + } + } + + fn is_space(&self) -> bool { + // TODO(jmmv): This is probably not correct regarding UTF-8 when comparing this function to + // the `is_whitespace` builtin. Figure out if that's true and what to do about it. + matches!(*self, ' ' | '\t' | '\r') + } + + fn is_word(&self) -> bool { + match *self { + '_' => true, + ch => ch.is_alphanumeric(), + } + } +} + +/// A token along with its position and length in the source. +/// +/// Note that the "context" is not truly available for some tokens such as `Token::Eof`, but we can +/// synthesize one for simplicity. Otherwise, we would need to extend the `Token` enum so that +/// every possible token contains extra fields, and that would be too complex. +#[cfg_attr(test, derive(PartialEq))] +pub struct TokenSpan { + /// The token itself. + pub(crate) token: Token, + + /// Start position of the token in the source. + pub(crate) pos: LineCol, + + /// Length of the token in characters. + #[allow(unused)] // TODO(jmmv): Use this in the parser. + length: usize, +} + +impl TokenSpan { + /// Creates a new `TokenSpan` from its parts. + fn new(token: Token, pos: LineCol, length: usize) -> Self { + Self { token, pos, length } + } +} + +/// Tokenizer that breaks an input stream into a sequence of language tokens. +pub struct Lexer<'a> { + /// Character reader for the input stream. + input: CharReader<'a>, +} + +impl<'a> Lexer<'a> { + /// Creates a new lexer from the given readable. + pub fn from(input: &'a mut dyn io::Read) -> Self { + Self { input: CharReader::from(input) } + } + + /// Handles an `input.next()` call that returned an unexpected character. + /// + /// This returns a `Token::Bad` with the provided `msg` and skips characters in the input + /// stream until a field separator is found. + fn handle_bad_read>( + &mut self, + msg: S, + first_pos: LineCol, + ) -> io::Result { + let mut len = 1; + loop { + match self.input.peek() { + Some(Ok(ch_span)) if ch_span.ch.is_separator() => break, + Some(Ok(_)) => { + self.input.next().unwrap()?; + len += 1; + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + Ok(TokenSpan::new(Token::Bad(msg.into()), first_pos, len)) + } + + /// Consumes the number at the current position, whose first digit is `first`. + fn consume_number(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + let mut found_dot = false; + s.push(first.ch); + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + '.' => { + if found_dot { + self.input.next().unwrap()?; + return self + .handle_bad_read("Too many dots in numeric literal", first.pos); + } + s.push(self.input.next().unwrap()?.ch); + found_dot = true; + } + ch if ch.is_ascii_digit() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in numeric literal: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + if found_dot { + if s.ends_with('.') { + // TODO(jmmv): Reconsider supporting double literals with a . that is not prefixed + // by a number or not followed by a number. For now, mimic the error we get when + // we encounter a dot not prefixed by a number. + return self.handle_bad_read("Unknown character: .", first.pos); + } + match s.parse::() { + Ok(d) => Ok(TokenSpan::new(Token::Double(d), first.pos, s.len())), + Err(e) => self.handle_bad_read(format!("Bad double {}: {}", s, e), first.pos), + } + } else { + match s.parse::() { + Ok(i) => Ok(TokenSpan::new(Token::Integer(i), first.pos, s.len())), + Err(e) => self.handle_bad_read(format!("Bad integer {}: {}", s, e), first.pos), + } + } + } + + /// Consumes the integer at the current position, whose first digit is `first` and which is + /// expected to be expressed in the given `base`. `prefix_len` indicates how many characters + /// were already consumed for this token, without counting `first`. + fn consume_integer( + &mut self, + base: u8, + pos: LineCol, + prefix_len: usize, + ) -> io::Result { + let mut s = String::new(); + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + '.' => { + self.input.next().unwrap()?; + return self + .handle_bad_read("Numbers in base syntax must be integers", pos); + } + ch if ch.is_ascii_digit() => s.push(self.input.next().unwrap()?.ch), + 'a'..='f' | 'A'..='F' => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in numeric literal: {}", ch); + return self.handle_bad_read(msg, pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + if s.is_empty() { + return self.handle_bad_read("No digits in integer literal", pos); + } + + match u32::from_str_radix(&s, u32::from(base)) { + Ok(i) => Ok(TokenSpan::new(Token::Integer(i as i32), pos, s.len() + prefix_len)), + Err(e) => self.handle_bad_read(format!("Bad integer {}: {}", s, e), pos), + } + } + + /// Consumes the integer at the current position `pos`. + fn consume_integer_with_base(&mut self, pos: LineCol) -> io::Result { + let mut prefix_len = 1; // Count '&'. + + let base = match self.input.peek() { + Some(Ok(ch_span)) => { + let base = match ch_span.ch { + 'b' | 'B' => 2, + 'd' | 'D' => 10, + 'o' | 'O' => 8, + 'x' | 'X' => 16, + ch if ch.is_separator() => { + return self.handle_bad_read("Missing base in integer literal", pos); + } + _ => { + let ch_span = self.input.next().unwrap()?; + return self.handle_bad_read( + format!("Unknown base {} in integer literal", ch_span.ch), + pos, + ); + } + }; + self.input.next().unwrap()?; + base + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => { + return self.handle_bad_read("Incomplete integer due to EOF", pos); + } + }; + prefix_len += 1; // Count the base. + + match self.input.peek() { + Some(Ok(ch_span)) if ch_span.ch == '_' => { + self.input.next().unwrap().unwrap(); + prefix_len += 1; // Count the '_'. + } + Some(Ok(_ch_span)) => (), + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => return self.handle_bad_read("Incomplete integer due to EOF", pos), + } + + self.consume_integer(base, pos, prefix_len) + } + + /// Consumes the operator at the current position, whose first character is `first`. + fn consume_operator(&mut self, first: CharSpan) -> io::Result { + match (first.ch, self.input.peek()) { + (_, Some(Err(_))) => Err(self.input.next().unwrap().unwrap_err()), + + ('<', Some(Ok(ch_span))) if ch_span.ch == '>' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::NotEqual, first.pos, 2)) + } + + ('<', Some(Ok(ch_span))) if ch_span.ch == '=' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::LessEqual, first.pos, 2)) + } + ('<', Some(Ok(ch_span))) if ch_span.ch == '<' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::ShiftLeft, first.pos, 2)) + } + ('<', _) => Ok(TokenSpan::new(Token::Less, first.pos, 1)), + + ('>', Some(Ok(ch_span))) if ch_span.ch == '=' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::GreaterEqual, first.pos, 2)) + } + ('>', Some(Ok(ch_span))) if ch_span.ch == '>' => { + self.input.next().unwrap()?; + Ok(TokenSpan::new(Token::ShiftRight, first.pos, 2)) + } + ('>', _) => Ok(TokenSpan::new(Token::Greater, first.pos, 1)), + + (_, _) => panic!("Should not have been called"), + } + } + + /// Consumes the symbol or keyword at the current position, whose first letter is `first`. + /// + /// The symbol may be a bare name, but it may also contain an optional type annotation. + fn consume_symbol(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + s.push(first.ch); + let mut vtype = None; + let mut token_len = 0; + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + '?' => { + vtype = Some(ExprType::Boolean); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '#' => { + vtype = Some(ExprType::Double); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '%' => { + vtype = Some(ExprType::Integer); + self.input.next().unwrap()?; + token_len += 1; + break; + } + '$' => { + vtype = Some(ExprType::Text); + self.input.next().unwrap()?; + token_len += 1; + break; + } + ch => { + self.input.next().unwrap()?; + let msg = format!("Unexpected character in symbol: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + debug_assert!(token_len <= 1); + + token_len += s.len(); + let token = match s.to_uppercase().as_str() { + "AND" => Token::And, + "AS" => Token::As, + "BOOLEAN" => Token::BooleanName, + "CASE" => Token::Case, + "DATA" => Token::Data, + "DIM" => Token::Dim, + "DO" => Token::Do, + "DOUBLE" => Token::DoubleName, + "ELSE" => Token::Else, + "ELSEIF" => Token::Elseif, + "END" => Token::End, + "ERROR" => Token::Error, + "EXIT" => Token::Exit, + "FALSE" => Token::Boolean(false), + "FOR" => Token::For, + "FUNCTION" => Token::Function, + "GOSUB" => Token::Gosub, + "GOTO" => Token::Goto, + "IF" => Token::If, + "IS" => Token::Is, + "INTEGER" => Token::IntegerName, + "LOOP" => Token::Loop, + "MOD" => Token::Modulo, + "NEXT" => Token::Next, + "NOT" => Token::Not, + "ON" => Token::On, + "OR" => Token::Or, + "REM" => return self.consume_rest_of_line(), + "RESUME" => Token::Resume, + "RETURN" => Token::Return, + "SELECT" => Token::Select, + "SHARED" => Token::Shared, + "STEP" => Token::Step, + "STRING" => Token::TextName, + "SUB" => Token::Sub, + "THEN" => Token::Then, + "TO" => Token::To, + "TRUE" => Token::Boolean(true), + "UNTIL" => Token::Until, + "WEND" => Token::Wend, + "WHILE" => Token::While, + "XOR" => Token::Xor, + _ => Token::Symbol(VarRef::new(s, vtype)), + }; + Ok(TokenSpan::new(token, first.pos, token_len)) + } + + /// Consumes the string at the current position, which was has to end with the same opening + /// character as specified by `delim`. + /// + /// This handles quoted characters within the string. + fn consume_text(&mut self, delim: CharSpan) -> io::Result { + let mut s = String::new(); + let mut escaping = false; + loop { + match self.input.peek() { + Some(Ok(ch_span)) => { + if escaping { + s.push(self.input.next().unwrap()?.ch); + escaping = false; + } else if ch_span.ch == '\\' { + self.input.next().unwrap()?; + escaping = true; + } else if ch_span.ch == delim.ch { + self.input.next().unwrap()?; + break; + } else { + s.push(self.input.next().unwrap()?.ch); + } + } + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => { + return self.handle_bad_read( + format!("Incomplete string due to EOF: {}", s), + delim.pos, + ); + } + } + } + let token_len = s.len() + 2; + Ok(TokenSpan::new(Token::Text(s), delim.pos, token_len)) + } + + /// Consumes the label definition at the current position. + fn consume_label(&mut self, first: CharSpan) -> io::Result { + let mut s = String::new(); + + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() && !ch.is_numeric() => s.push(self.input.next().unwrap()?.ch), + _ch => (), + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => (), + } + if s.is_empty() { + return Ok(TokenSpan::new(Token::Bad("Empty label name".to_owned()), first.pos, 1)); + } + + loop { + match self.input.peek() { + Some(Ok(ch_span)) => match ch_span.ch { + ch if ch.is_word() => s.push(self.input.next().unwrap()?.ch), + ch if ch.is_separator() => break, + ch => { + let msg = format!("Unexpected character in label: {}", ch); + return self.handle_bad_read(msg, first.pos); + } + }, + Some(Err(_)) => return Err(self.input.next().unwrap().unwrap_err()), + None => break, + } + } + + let token_len = s.len() + 1; + Ok(TokenSpan::new(Token::Label(s), first.pos, token_len)) + } + + /// Consumes the remainder of the line and returns the token that was encountered at the end + /// (which may be EOF or end of line). + fn consume_rest_of_line(&mut self) -> io::Result { + loop { + match self.input.next() { + None => { + let last_pos = self.input.next_pos(); + return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); + } + Some(Ok(ch_span)) if ch_span.ch == '\n' => { + return Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)); + } + Some(Err(e)) => return Err(e), + Some(Ok(_)) => (), + } + } + } + + /// Skips whitespace until it finds the beginning of the next token, and returns its first + /// character. + fn advance_and_read_next(&mut self) -> io::Result> { + loop { + match self.input.next() { + Some(Ok(ch_span)) if ch_span.ch.is_space() => (), + Some(Ok(ch_span)) => return Ok(Some(ch_span)), + Some(Err(e)) => return Err(e), + None => return Ok(None), + } + } + } + + /// Reads the next token from the input stream. + /// + /// Note that this returns errors only on fatal I/O conditions. EOF and malformed tokens are + /// both returned as the special token types `Token::Eof` and `Token::Bad` respectively. + pub fn read(&mut self) -> io::Result { + let ch_span = self.advance_and_read_next()?; + if ch_span.is_none() { + let last_pos = self.input.next_pos(); + return Ok(TokenSpan::new(Token::Eof, last_pos, 0)); + } + let ch_span = ch_span.unwrap(); + match ch_span.ch { + '\n' | ':' => Ok(TokenSpan::new(Token::Eol, ch_span.pos, 1)), + '\'' => self.consume_rest_of_line(), + + '"' => self.consume_text(ch_span), + + ';' => Ok(TokenSpan::new(Token::Semicolon, ch_span.pos, 1)), + ',' => Ok(TokenSpan::new(Token::Comma, ch_span.pos, 1)), + + '(' => Ok(TokenSpan::new(Token::LeftParen, ch_span.pos, 1)), + ')' => Ok(TokenSpan::new(Token::RightParen, ch_span.pos, 1)), + + '+' => Ok(TokenSpan::new(Token::Plus, ch_span.pos, 1)), + '-' => Ok(TokenSpan::new(Token::Minus, ch_span.pos, 1)), + '*' => Ok(TokenSpan::new(Token::Multiply, ch_span.pos, 1)), + '/' => Ok(TokenSpan::new(Token::Divide, ch_span.pos, 1)), + '^' => Ok(TokenSpan::new(Token::Exponent, ch_span.pos, 1)), + + '=' => Ok(TokenSpan::new(Token::Equal, ch_span.pos, 1)), + '<' | '>' => self.consume_operator(ch_span), + + '@' => self.consume_label(ch_span), + + '&' => self.consume_integer_with_base(ch_span.pos), + + ch if ch.is_ascii_digit() => self.consume_number(ch_span), + ch if ch.is_word() => self.consume_symbol(ch_span), + ch => self.handle_bad_read(format!("Unknown character: {}", ch), ch_span.pos), + } + } + + /// Returns a peekable adaptor for this lexer. + pub fn peekable(self) -> PeekableLexer<'a> { + PeekableLexer { lexer: self, peeked: None } + } +} + +/// A lexer wrapper that supports peeking at the next token without consuming it. +/// +/// Ideally, the `Lexer` would be an `Iterator` which would give us access to the standard +/// `Peekable` interface, but the ergonomics of that when dealing with a `Fallible` are less than +/// optimal. Hence we implement our own. +pub struct PeekableLexer<'a> { + /// The wrapped lexer instance. + lexer: Lexer<'a>, + + /// If not none, contains the token read by `peek`, which will be consumed by the next call + /// to `read` or `consume_peeked`. + peeked: Option, +} + +impl PeekableLexer<'_> { + /// Reads the previously-peeked token. + /// + /// Because `peek` reports read errors, this assumes that the caller already handled those + /// errors and is thus not going to call this when an error is present. + pub fn consume_peeked(&mut self) -> TokenSpan { + assert!(self.peeked.is_some()); + self.peeked.take().unwrap() + } + + /// Peeks the upcoming token. + /// + /// It is OK to call this function several times on the same token before extracting it from + /// the lexer. + pub fn peek(&mut self) -> Result<&TokenSpan> { + if self.peeked.is_none() { + let span = self.read()?; + self.peeked.replace(span); + } + Ok(self.peeked.as_ref().unwrap()) + } + + /// Reads the next token. + /// + /// If the next token is invalid and results in a read error, the stream will remain valid and + /// further tokens can be obtained with subsequent calls. + pub fn read(&mut self) -> Result { + match self.peeked.take() { + Some(t) => Ok(t), + None => match self.lexer.read() { + Ok(span) => Ok(span), + Err(e) => Err((self.lexer.input.next_pos(), e)), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fmt; + + /// Syntactic sugar to instantiate a `TokenSpan` for testing. + fn ts(token: Token, line: usize, col: usize, length: usize) -> TokenSpan { + TokenSpan::new(token, LineCol { line, col }, length) + } + + impl fmt::Debug for TokenSpan { + /// Mimic the way we write the tests with the `ts` helper in `TokenSpan` dumps. + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "ts(Token::{:?}, {}, {}, {})", + self.token, self.pos.line, self.pos.col, self.length + ) + } + } + + /// Runs the lexer on the given `input` and expects the returned tokens to match + /// `exp_token_spans`. + fn do_ok_test(input: &str, exp_token_spans: &[TokenSpan]) { + let mut input = input.as_bytes(); + let mut lexer = Lexer::from(&mut input); + + let mut token_spans: Vec = vec![]; + let mut eof = false; + while !eof { + let token_span = lexer.read().expect("Lexing failed"); + eof = token_span.token == Token::Eof; + token_spans.push(token_span); + } + + assert_eq!(exp_token_spans, token_spans.as_slice()); + } + + #[test] + fn test_empty() { + let mut input = b"".as_ref(); + let mut lexer = Lexer::from(&mut input); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + } + + #[test] + fn test_read_past_eof() { + do_ok_test("", &[ts(Token::Eof, 1, 1, 0)]); + } + + #[test] + fn test_whitespace_only() { + do_ok_test(" \t ", &[ts(Token::Eof, 1, 11, 0)]); + } + + #[test] + fn test_multiple_lines() { + do_ok_test( + " \n \t \n ", + &[ts(Token::Eol, 1, 4, 1), ts(Token::Eol, 2, 12, 1), ts(Token::Eof, 3, 3, 0)], + ); + do_ok_test( + " : \t : ", + &[ts(Token::Eol, 1, 4, 1), ts(Token::Eol, 1, 12, 1), ts(Token::Eof, 1, 15, 0)], + ); + } + + #[test] + fn test_tabs() { + do_ok_test("\t33", &[ts(Token::Integer(33), 1, 9, 2), ts(Token::Eof, 1, 11, 0)]); + do_ok_test( + "1234567\t8", + &[ + ts(Token::Integer(1234567), 1, 1, 7), + ts(Token::Integer(8), 1, 9, 1), + ts(Token::Eof, 1, 10, 0), + ], + ); + } + + /// Syntactic sugar to instantiate a `VarRef` without an explicit type annotation. + fn new_auto_symbol(name: &str) -> Token { + Token::Symbol(VarRef::new(name, None)) + } + + #[test] + fn test_some_tokens() { + do_ok_test( + "123 45 \n 6 3.012 abc a38z: a=3 with_underscores_1=_2", + &[ + ts(Token::Integer(123), 1, 1, 3), + ts(Token::Integer(45), 1, 5, 2), + ts(Token::Eol, 1, 8, 1), + ts(Token::Integer(6), 2, 2, 1), + ts(Token::Double(3.012), 2, 4, 5), + ts(new_auto_symbol("abc"), 2, 10, 3), + ts(new_auto_symbol("a38z"), 2, 14, 4), + ts(Token::Eol, 2, 18, 1), + ts(new_auto_symbol("a"), 2, 20, 1), + ts(Token::Equal, 2, 21, 1), + ts(Token::Integer(3), 2, 22, 1), + ts(new_auto_symbol("with_underscores_1"), 2, 24, 18), + ts(Token::Equal, 2, 42, 1), + ts(new_auto_symbol("_2"), 2, 43, 2), + ts(Token::Eof, 2, 45, 0), + ], + ); + } + + #[test] + fn test_boolean_literals() { + do_ok_test( + "true TRUE yes YES y false FALSE no NO n", + &[ + ts(Token::Boolean(true), 1, 1, 4), + ts(Token::Boolean(true), 1, 6, 4), + ts(new_auto_symbol("yes"), 1, 11, 3), + ts(new_auto_symbol("YES"), 1, 15, 3), + ts(new_auto_symbol("y"), 1, 19, 1), + ts(Token::Boolean(false), 1, 21, 5), + ts(Token::Boolean(false), 1, 27, 5), + ts(new_auto_symbol("no"), 1, 33, 2), + ts(new_auto_symbol("NO"), 1, 36, 2), + ts(new_auto_symbol("n"), 1, 39, 1), + ts(Token::Eof, 1, 40, 0), + ], + ); + } + + #[test] + fn test_integer_literals() { + do_ok_test( + "&b10 &B_10 &D10 &d_10 &o_10 &O_10 &X10 &x_10 &xabcdef &x0ABCDEF0 &x7a1", + &[ + ts(Token::Integer(2), 1, 1, 4), + ts(Token::Integer(2), 1, 6, 5), + ts(Token::Integer(10), 1, 12, 4), + ts(Token::Integer(10), 1, 17, 5), + ts(Token::Integer(8), 1, 23, 5), + ts(Token::Integer(8), 1, 29, 5), + ts(Token::Integer(16), 1, 35, 4), + ts(Token::Integer(16), 1, 40, 5), + ts(Token::Integer(11259375), 1, 46, 8), + ts(Token::Integer(180150000), 1, 55, 10), + ts(Token::Integer(1953), 1, 66, 5), + ts(Token::Eof, 1, 71, 0), + ], + ); + + do_ok_test( + "&b11111111111111111111111111111111 &xf0000000 &xffffffff", + &[ + ts(Token::Integer(-1), 1, 1, 34), + ts(Token::Integer(-268435456), 1, 36, 10), + ts(Token::Integer(-1), 1, 47, 10), + ts(Token::Eof, 1, 57, 0), + ], + ); + + do_ok_test( + "& &_ &__ &i10 &i_10 &d &d10.1 &b2 &da &o8 &xg", + &[ + ts(Token::Bad("Missing base in integer literal".to_owned()), 1, 1, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 3, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 6, 2), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 10, 3), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 15, 4), + ts(Token::Bad("No digits in integer literal".to_owned()), 1, 21, 1), + ts(Token::Bad("Numbers in base syntax must be integers".to_owned()), 1, 24, 2), + ts(Token::Bad("Bad integer 2: invalid digit found in string".to_owned()), 1, 31, 1), + ts(Token::Bad("Bad integer a: invalid digit found in string".to_owned()), 1, 35, 1), + ts(Token::Bad("Bad integer 8: invalid digit found in string".to_owned()), 1, 39, 1), + ts(Token::Bad("Unexpected character in numeric literal: g".to_owned()), 1, 43, 1), + ts(Token::Eof, 1, 46, 0), + ], + ); + + do_ok_test( + ">&< >&_< >&__< >&i10< >&i_10< >&d< >&d10.1<", + &[ + ts(Token::Greater, 1, 1, 1), + ts(Token::Bad("Missing base in integer literal".to_owned()), 1, 2, 1), + ts(Token::Less, 1, 3, 1), + // - + ts(Token::Greater, 1, 5, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 6, 1), + ts(Token::Less, 1, 8, 1), + // - + ts(Token::Greater, 1, 10, 1), + ts(Token::Bad("Unknown base _ in integer literal".to_owned()), 1, 11, 2), + ts(Token::Less, 1, 14, 1), + // - + ts(Token::Greater, 1, 16, 1), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 17, 3), + ts(Token::Less, 1, 21, 1), + // - + ts(Token::Greater, 1, 23, 1), + ts(Token::Bad("Unknown base i in integer literal".to_owned()), 1, 24, 4), + ts(Token::Less, 1, 29, 1), + // - + ts(Token::Greater, 1, 31, 1), + ts(Token::Bad("No digits in integer literal".to_owned()), 1, 32, 1), + ts(Token::Less, 1, 34, 1), + // - + ts(Token::Greater, 1, 36, 1), + ts(Token::Bad("Numbers in base syntax must be integers".to_owned()), 1, 37, 2), + ts(Token::Less, 1, 43, 1), + // - + ts(Token::Eof, 1, 44, 0), + ], + ); + } + + #[test] + fn test_utf8() { + do_ok_test( + "가 나=7 a다b \"라 마\"", + &[ + ts(new_auto_symbol("가"), 1, 1, 3), + ts(new_auto_symbol("나"), 1, 3, 3), + ts(Token::Equal, 1, 4, 1), + ts(Token::Integer(7), 1, 5, 1), + ts(new_auto_symbol("a다b"), 1, 7, 5), + ts(Token::Text("라 마".to_owned()), 1, 11, 9), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_remarks() { + do_ok_test( + "REM This is a comment\nNOT 'This is another comment\n", + &[ + ts(Token::Eol, 1, 22, 1), + ts(Token::Not, 2, 1, 3), + ts(Token::Eol, 2, 29, 1), + ts(Token::Eof, 3, 1, 0), + ], + ); + + do_ok_test( + "REM This is a comment: and the colon doesn't yield Eol\nNOT 'Another: comment\n", + &[ + ts(Token::Eol, 1, 55, 1), + ts(Token::Not, 2, 1, 3), + ts(Token::Eol, 2, 22, 1), + ts(Token::Eof, 3, 1, 0), + ], + ); + } + + #[test] + fn test_var_types() { + do_ok_test( + "a b? d# i% s$", + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(Token::Symbol(VarRef::new("b", Some(ExprType::Boolean))), 1, 3, 2), + ts(Token::Symbol(VarRef::new("d", Some(ExprType::Double))), 1, 6, 2), + ts(Token::Symbol(VarRef::new("i", Some(ExprType::Integer))), 1, 9, 2), + ts(Token::Symbol(VarRef::new("s", Some(ExprType::Text))), 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + } + + #[test] + fn test_strings() { + do_ok_test( + " \"this is a string\" 3", + &[ + ts(Token::Text("this is a string".to_owned()), 1, 2, 18), + ts(Token::Integer(3), 1, 22, 1), + ts(Token::Eof, 1, 23, 0), + ], + ); + + do_ok_test( + " \"this is a string with ; special : characters in it\"", + &[ + ts( + Token::Text("this is a string with ; special : characters in it".to_owned()), + 1, + 2, + 52, + ), + ts(Token::Eof, 1, 54, 0), + ], + ); + + do_ok_test( + "\"this \\\"is escaped\\\" \\\\ \\a\" 1", + &[ + ts(Token::Text("this \"is escaped\" \\ a".to_owned()), 1, 1, 23), + ts(Token::Integer(1), 1, 29, 1), + ts(Token::Eof, 1, 30, 0), + ], + ); + } + + #[test] + fn test_data() { + do_ok_test("DATA", &[ts(Token::Data, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + do_ok_test("data", &[ts(Token::Data, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + // Common BASIC interprets things like "2 + foo" as a single string but we interpret + // separate tokens. "Fixing" this to read data in the same way requires entering a + // separate lexing mode just for DATA statements, which is not very interesting. We can + // ask for strings to always be double-quoted. + do_ok_test( + "DATA 2 + foo", + &[ + ts(Token::Data, 1, 1, 4), + ts(Token::Integer(2), 1, 6, 1), + ts(Token::Plus, 1, 8, 1), + ts(new_auto_symbol("foo"), 1, 10, 3), + ts(Token::Eof, 1, 13, 0), + ], + ); + } + + #[test] + fn test_dim() { + do_ok_test( + "DIM SHARED AS", + &[ + ts(Token::Dim, 1, 1, 3), + ts(Token::Shared, 1, 5, 6), + ts(Token::As, 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + do_ok_test( + "BOOLEAN DOUBLE INTEGER STRING", + &[ + ts(Token::BooleanName, 1, 1, 7), + ts(Token::DoubleName, 1, 9, 6), + ts(Token::IntegerName, 1, 16, 7), + ts(Token::TextName, 1, 24, 6), + ts(Token::Eof, 1, 30, 0), + ], + ); + + do_ok_test( + "dim shared as", + &[ + ts(Token::Dim, 1, 1, 3), + ts(Token::Shared, 1, 5, 6), + ts(Token::As, 1, 12, 2), + ts(Token::Eof, 1, 14, 0), + ], + ); + do_ok_test( + "boolean double integer string", + &[ + ts(Token::BooleanName, 1, 1, 7), + ts(Token::DoubleName, 1, 9, 6), + ts(Token::IntegerName, 1, 16, 7), + ts(Token::TextName, 1, 24, 6), + ts(Token::Eof, 1, 30, 0), + ], + ); + } + + #[test] + fn test_do() { + do_ok_test( + "DO UNTIL WHILE EXIT LOOP", + &[ + ts(Token::Do, 1, 1, 2), + ts(Token::Until, 1, 4, 5), + ts(Token::While, 1, 10, 5), + ts(Token::Exit, 1, 16, 4), + ts(Token::Loop, 1, 21, 4), + ts(Token::Eof, 1, 25, 0), + ], + ); + + do_ok_test( + "do until while exit loop", + &[ + ts(Token::Do, 1, 1, 2), + ts(Token::Until, 1, 4, 5), + ts(Token::While, 1, 10, 5), + ts(Token::Exit, 1, 16, 4), + ts(Token::Loop, 1, 21, 4), + ts(Token::Eof, 1, 25, 0), + ], + ); + } + + #[test] + fn test_if() { + do_ok_test( + "IF THEN ELSEIF ELSE END IF", + &[ + ts(Token::If, 1, 1, 2), + ts(Token::Then, 1, 4, 4), + ts(Token::Elseif, 1, 9, 6), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::If, 1, 25, 2), + ts(Token::Eof, 1, 27, 0), + ], + ); + + do_ok_test( + "if then elseif else end if", + &[ + ts(Token::If, 1, 1, 2), + ts(Token::Then, 1, 4, 4), + ts(Token::Elseif, 1, 9, 6), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::If, 1, 25, 2), + ts(Token::Eof, 1, 27, 0), + ], + ); + } + + #[test] + fn test_for() { + do_ok_test( + "FOR TO STEP NEXT", + &[ + ts(Token::For, 1, 1, 3), + ts(Token::To, 1, 5, 2), + ts(Token::Step, 1, 8, 4), + ts(Token::Next, 1, 13, 4), + ts(Token::Eof, 1, 17, 0), + ], + ); + + do_ok_test( + "for to step next", + &[ + ts(Token::For, 1, 1, 3), + ts(Token::To, 1, 5, 2), + ts(Token::Step, 1, 8, 4), + ts(Token::Next, 1, 13, 4), + ts(Token::Eof, 1, 17, 0), + ], + ); + } + + #[test] + fn test_function() { + do_ok_test( + "FUNCTION FOO END FUNCTION", + &[ + ts(Token::Function, 1, 1, 8), + ts(Token::Symbol(VarRef::new("FOO", None)), 1, 10, 3), + ts(Token::End, 1, 14, 3), + ts(Token::Function, 1, 18, 8), + ts(Token::Eof, 1, 26, 0), + ], + ); + + do_ok_test( + "function foo end function", + &[ + ts(Token::Function, 1, 1, 8), + ts(Token::Symbol(VarRef::new("foo", None)), 1, 10, 3), + ts(Token::End, 1, 14, 3), + ts(Token::Function, 1, 18, 8), + ts(Token::Eof, 1, 26, 0), + ], + ); + } + + #[test] + fn test_gosub() { + do_ok_test("GOSUB", &[ts(Token::Gosub, 1, 1, 5), ts(Token::Eof, 1, 6, 0)]); + + do_ok_test("gosub", &[ts(Token::Gosub, 1, 1, 5), ts(Token::Eof, 1, 6, 0)]); + } + + #[test] + fn test_goto() { + do_ok_test("GOTO", &[ts(Token::Goto, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + + do_ok_test("goto", &[ts(Token::Goto, 1, 1, 4), ts(Token::Eof, 1, 5, 0)]); + } + + #[test] + fn test_label() { + do_ok_test( + "@Foo123 @a @Z @_ok", + &[ + ts(Token::Label("Foo123".to_owned()), 1, 1, 7), + ts(Token::Label("a".to_owned()), 1, 9, 2), + ts(Token::Label("Z".to_owned()), 1, 12, 2), + ts(Token::Label("_ok".to_owned()), 1, 15, 4), + ts(Token::Eof, 1, 19, 0), + ], + ); + } + + #[test] + fn test_on_error() { + for s in ["ON ERROR GOTO @foo", "on error goto @foo"] { + do_ok_test( + s, + &[ + ts(Token::On, 1, 1, 2), + ts(Token::Error, 1, 4, 5), + ts(Token::Goto, 1, 10, 4), + ts(Token::Label("foo".to_owned()), 1, 15, 4), + ts(Token::Eof, 1, 19, 0), + ], + ); + } + + for s in ["ON ERROR RESUME NEXT", "on error resume next"] { + do_ok_test( + s, + &[ + ts(Token::On, 1, 1, 2), + ts(Token::Error, 1, 4, 5), + ts(Token::Resume, 1, 10, 6), + ts(Token::Next, 1, 17, 4), + ts(Token::Eof, 1, 21, 0), + ], + ); + } + } + + #[test] + fn test_return() { + do_ok_test("RETURN", &[ts(Token::Return, 1, 1, 6), ts(Token::Eof, 1, 7, 0)]); + + do_ok_test("return", &[ts(Token::Return, 1, 1, 6), ts(Token::Eof, 1, 7, 0)]); + } + + #[test] + fn test_select() { + do_ok_test( + "SELECT CASE IS ELSE END", + &[ + ts(Token::Select, 1, 1, 6), + ts(Token::Case, 1, 8, 4), + ts(Token::Is, 1, 13, 2), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::Eof, 1, 24, 0), + ], + ); + + do_ok_test( + "select case is else end", + &[ + ts(Token::Select, 1, 1, 6), + ts(Token::Case, 1, 8, 4), + ts(Token::Is, 1, 13, 2), + ts(Token::Else, 1, 16, 4), + ts(Token::End, 1, 21, 3), + ts(Token::Eof, 1, 24, 0), + ], + ); + } + + #[test] + fn test_sub() { + do_ok_test( + "SUB FOO END SUB", + &[ + ts(Token::Sub, 1, 1, 3), + ts(Token::Symbol(VarRef::new("FOO", None)), 1, 5, 3), + ts(Token::End, 1, 9, 3), + ts(Token::Sub, 1, 13, 3), + ts(Token::Eof, 1, 16, 0), + ], + ); + + do_ok_test( + "sub foo end sub", + &[ + ts(Token::Sub, 1, 1, 3), + ts(Token::Symbol(VarRef::new("foo", None)), 1, 5, 3), + ts(Token::End, 1, 9, 3), + ts(Token::Sub, 1, 13, 3), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_while() { + do_ok_test( + "WHILE WEND", + &[ts(Token::While, 1, 1, 5), ts(Token::Wend, 1, 7, 4), ts(Token::Eof, 1, 11, 0)], + ); + + do_ok_test( + "while wend", + &[ts(Token::While, 1, 1, 5), ts(Token::Wend, 1, 7, 4), ts(Token::Eof, 1, 11, 0)], + ); + } + + /// Syntactic sugar to instantiate a test that verifies the parsing of a binary operator. + fn do_binary_operator_test(op: &str, t: Token) { + do_ok_test( + format!("a {} 2", op).as_ref(), + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(t, 1, 3, op.len()), + ts(Token::Integer(2), 1, 4 + op.len(), 1), + ts(Token::Eof, 1, 5 + op.len(), 0), + ], + ); + } + + /// Syntactic sugar to instantiate a test that verifies the parsing of a unary operator. + fn do_unary_operator_test(op: &str, t: Token) { + do_ok_test( + format!("{} 2", op).as_ref(), + &[ + ts(t, 1, 1, op.len()), + ts(Token::Integer(2), 1, 2 + op.len(), 1), + ts(Token::Eof, 1, 3 + op.len(), 0), + ], + ); + } + + #[test] + fn test_operator_relational_ops() { + do_binary_operator_test("=", Token::Equal); + do_binary_operator_test("<>", Token::NotEqual); + do_binary_operator_test("<", Token::Less); + do_binary_operator_test("<=", Token::LessEqual); + do_binary_operator_test(">", Token::Greater); + do_binary_operator_test(">=", Token::GreaterEqual); + } + + #[test] + fn test_operator_arithmetic_ops() { + do_binary_operator_test("+", Token::Plus); + do_binary_operator_test("-", Token::Minus); + do_binary_operator_test("*", Token::Multiply); + do_binary_operator_test("/", Token::Divide); + do_binary_operator_test("MOD", Token::Modulo); + do_binary_operator_test("mod", Token::Modulo); + do_binary_operator_test("^", Token::Exponent); + do_unary_operator_test("-", Token::Minus); + } + + #[test] + fn test_operator_logical_bitwise_ops() { + do_binary_operator_test("AND", Token::And); + do_binary_operator_test("OR", Token::Or); + do_binary_operator_test("XOR", Token::Xor); + do_unary_operator_test("NOT", Token::Not); + + do_binary_operator_test("<<", Token::ShiftLeft); + do_binary_operator_test(">>", Token::ShiftRight); + } + + #[test] + fn test_operator_no_spaces() { + do_ok_test( + "z=2 654<>a32 3.1<0.1 8^7", + &[ + ts(new_auto_symbol("z"), 1, 1, 1), + ts(Token::Equal, 1, 2, 1), + ts(Token::Integer(2), 1, 3, 1), + ts(Token::Integer(654), 1, 5, 3), + ts(Token::NotEqual, 1, 8, 2), + ts(new_auto_symbol("a32"), 1, 10, 3), + ts(Token::Double(3.1), 1, 14, 3), + ts(Token::Less, 1, 17, 1), + ts(Token::Double(0.1), 1, 18, 3), + ts(Token::Integer(8), 1, 22, 1), + ts(Token::Exponent, 1, 23, 1), + ts(Token::Integer(7), 1, 24, 1), + ts(Token::Eof, 1, 25, 0), + ], + ); + } + + #[test] + fn test_parenthesis() { + do_ok_test( + "(a) (\"foo\") (3)", + &[ + ts(Token::LeftParen, 1, 1, 1), + ts(new_auto_symbol("a"), 1, 2, 1), + ts(Token::RightParen, 1, 3, 1), + ts(Token::LeftParen, 1, 5, 1), + ts(Token::Text("foo".to_owned()), 1, 6, 5), + ts(Token::RightParen, 1, 11, 1), + ts(Token::LeftParen, 1, 13, 1), + ts(Token::Integer(3), 1, 14, 1), + ts(Token::RightParen, 1, 15, 1), + ts(Token::Eof, 1, 16, 0), + ], + ); + } + + #[test] + fn test_peekable_lexer() { + let mut input = b"a b 123".as_ref(); + let mut lexer = Lexer::from(&mut input).peekable(); + assert_eq!(new_auto_symbol("a"), lexer.peek().unwrap().token); + assert_eq!(new_auto_symbol("a"), lexer.peek().unwrap().token); + assert_eq!(new_auto_symbol("a"), lexer.read().unwrap().token); + assert_eq!(new_auto_symbol("b"), lexer.read().unwrap().token); + assert_eq!(Token::Integer(123), lexer.peek().unwrap().token); + assert_eq!(Token::Integer(123), lexer.read().unwrap().token); + assert_eq!(Token::Eof, lexer.peek().unwrap().token); + assert_eq!(Token::Eof, lexer.read().unwrap().token); + } + + #[test] + fn test_recoverable_errors() { + do_ok_test( + "0.1.28+5", + &[ + ts(Token::Bad("Too many dots in numeric literal".to_owned()), 1, 1, 3), + ts(Token::Plus, 1, 7, 1), + ts(Token::Integer(5), 1, 8, 1), + ts(Token::Eof, 1, 9, 0), + ], + ); + + do_ok_test( + "1 .3", + &[ + ts(Token::Integer(1), 1, 1, 1), + ts(Token::Bad("Unknown character: .".to_owned()), 1, 3, 2), + ts(Token::Eof, 1, 5, 0), + ], + ); + + do_ok_test( + "1 3. 2", + &[ + ts(Token::Integer(1), 1, 1, 1), + ts(Token::Bad("Unknown character: .".to_owned()), 1, 3, 1), + ts(Token::Integer(2), 1, 6, 1), + ts(Token::Eof, 1, 7, 0), + ], + ); + + do_ok_test( + "9999999999+5", + &[ + ts( + Token::Bad( + "Bad integer 9999999999: number too large to fit in target type".to_owned(), + ), + 1, + 1, + 1, + ), + ts(Token::Plus, 1, 11, 1), + ts(Token::Integer(5), 1, 12, 1), + ts(Token::Eof, 1, 13, 0), + ], + ); + + do_ok_test( + "\n3!2 1", + &[ + ts(Token::Eol, 1, 1, 1), + ts(Token::Bad("Unexpected character in numeric literal: !".to_owned()), 2, 1, 2), + ts(Token::Integer(1), 2, 5, 1), + ts(Token::Eof, 2, 6, 0), + ], + ); + + do_ok_test( + "a b|d 5", + &[ + ts(new_auto_symbol("a"), 1, 1, 1), + ts(Token::Bad("Unexpected character in symbol: |".to_owned()), 1, 3, 2), + ts(Token::Integer(5), 1, 7, 1), + ts(Token::Eof, 1, 8, 0), + ], + ); + + do_ok_test( + "( \"this is incomplete", + &[ + ts(Token::LeftParen, 1, 1, 1), + ts( + Token::Bad("Incomplete string due to EOF: this is incomplete".to_owned()), + 1, + 3, + 1, + ), + ts(Token::Eof, 1, 22, 0), + ], + ); + + do_ok_test( + "+ - ! * / MOD ^", + &[ + ts(Token::Plus, 1, 1, 1), + ts(Token::Minus, 1, 3, 1), + ts(Token::Bad("Unknown character: !".to_owned()), 1, 5, 1), + ts(Token::Multiply, 1, 7, 1), + ts(Token::Divide, 1, 9, 1), + ts(Token::Modulo, 1, 11, 3), + ts(Token::Exponent, 1, 15, 1), + ts(Token::Eof, 1, 16, 0), + ], + ); + + do_ok_test( + "@+", + &[ + ts(Token::Bad("Empty label name".to_owned()), 1, 1, 1), + ts(Token::Plus, 1, 2, 1), + ts(Token::Eof, 1, 3, 0), + ], + ); + + do_ok_test( + "@123", + &[ + ts(Token::Bad("Empty label name".to_owned()), 1, 1, 1), + ts(Token::Integer(123), 1, 2, 3), + ts(Token::Eof, 1, 5, 0), + ], + ); + } + + /// A reader that generates an error on the second read. + /// + /// Assumes that the buffered data in `good` is read in one go. + struct FaultyReader { + good: Option>, + } + + impl FaultyReader { + /// Creates a new faulty read with the given input data. + /// + /// `good` must be newline-terminated to prevent the caller from reading too much in one go. + fn new(good: &str) -> Self { + assert!(good.ends_with('\n')); + Self { good: Some(good.as_bytes().to_owned()) } + } + } + + impl io::Read for FaultyReader { + fn read(&mut self, buf: &mut [u8]) -> io::Result { + // This assumes that the good data fits within one read operation of the lexer. + if let Some(good) = self.good.take() { + assert!(buf.len() > good.len()); + buf[0..good.len()].clone_from_slice(&good[..]); + Ok(good.len()) + } else { + Err(io::Error::from(io::ErrorKind::InvalidData)) + } + } + } + + #[test] + fn test_unrecoverable_io_error() { + let mut reader = FaultyReader::new("3 + 5\n"); + let mut lexer = Lexer::from(&mut reader); + + assert_eq!(Token::Integer(3), lexer.read().unwrap().token); + assert_eq!(Token::Plus, lexer.read().unwrap().token); + assert_eq!(Token::Integer(5), lexer.read().unwrap().token); + assert_eq!(Token::Eol, lexer.read().unwrap().token); + let e = lexer.read().unwrap_err(); + assert_eq!(io::ErrorKind::InvalidData, e.kind()); + let e = lexer.read().unwrap_err(); + assert_eq!(io::ErrorKind::Other, e.kind()); + } +} diff --git a/core2/src/lib.rs b/core2/src/lib.rs new file mode 100644 index 00000000..52efee99 --- /dev/null +++ b/core2/src/lib.rs @@ -0,0 +1,37 @@ +// EndBASIC +// Copyright 2020 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. + +//! The EndBASIC core language parser, compiler, and virtual machine. + +mod ast; +mod bytecode; +mod callable; +mod compiler; +mod image; +mod lexer; +mod mem; +mod num; +mod parser; +mod reader; +mod vm; + +pub use ast::{ArgSep, ExprType}; +pub use bytecode::VarArgTag; +pub use callable::*; +pub use compiler::{SymbolKey, compile, only_metadata}; +pub use vm::{StopReason, Vm}; + +#[cfg(test)] +mod testutils; diff --git a/core2/src/mem.rs b/core2/src/mem.rs new file mode 100644 index 00000000..96e4915c --- /dev/null +++ b/core2/src/mem.rs @@ -0,0 +1,110 @@ +// 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. + +//! Memory representation and related types. + +use crate::ExprType; +use crate::num::{U24, unchecked_u24_as_usize}; +use std::convert::TryFrom; +use std::hash::Hash; + +/// A single value in the EndBASIC language. +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum Datum { + /// A boolean value. + Boolean(bool), + + /// A double-precision floating-point value. + Double(f64), + + /// A 32-bit signed integer value. + Integer(i32), + + /// A string value. + Text(String), +} + +impl Eq for Datum {} + +impl Hash for Datum { + fn hash(&self, state: &mut H) { + match self { + Self::Boolean(b) => b.hash(state), + Self::Double(d) => d.to_bits().hash(state), + Self::Integer(i) => i.hash(state), + Self::Text(s) => s.hash(state), + } + } +} + +impl Datum { + /// Creates a new datum of `etype` with a default value. + pub(crate) fn new(etype: ExprType) -> Self { + match etype { + ExprType::Boolean => Datum::Boolean(false), + ExprType::Double => Datum::Double(0.0), + ExprType::Integer => Datum::Integer(0), + ExprType::Text => Datum::Text(String::new()), + } + } + + /// Returns the type of the datum. + pub(crate) fn etype(&self) -> ExprType { + match self { + Self::Boolean(..) => ExprType::Boolean, + Self::Double(..) => ExprType::Double, + Self::Integer(..) => ExprType::Integer, + Self::Text(..) => ExprType::Text, + } + } +} + +/// Tagged pointers for constant and heap addresses. +#[derive(Clone, Copy)] +pub(crate) enum Pointer { + /// A pointer to an entry in the constants pool. + Constant(U24), + + /// A pointer to an entry in the heap. + Heap(U24), +} + +impl From for Pointer { + fn from(value: u64) -> Self { + let signed_value = value as i32; + if signed_value < 0 { + Self::Heap(U24::try_from((-signed_value - 1) as u32).unwrap()) + } else { + Self::Constant(U24::try_from(signed_value as u32).unwrap()) + } + } +} + +impl Pointer { + /// Creates a new pointer for a heap `index` and returns its `u64` representation. + pub(crate) fn for_heap(index: u32) -> u64 { + let raw = index as i32; + let raw = -raw - 1; + raw as u64 + } + + /// Gets the datum pointed to by this pointer from the `constants` and `heap`. + pub(crate) fn resolve<'b>(&self, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { + match self { + Self::Constant(index) => &constants[unchecked_u24_as_usize(*index)], + Self::Heap(index) => &heap[unchecked_u24_as_usize(*index)], + } + } +} diff --git a/core2/src/num.rs b/core2/src/num.rs new file mode 100644 index 00000000..e8fac9b5 --- /dev/null +++ b/core2/src/num.rs @@ -0,0 +1,110 @@ +// 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. + +//! Efficient conversion utilities with diagnostics. + +use std::convert::TryFrom; +use std::num::TryFromIntError; + +/// An unsigned integer constrained to 24 bits. +#[derive(Clone, Copy)] +pub(crate) struct U24(u32); + +impl TryFrom for U24 { + type Error = (); + + fn try_from(value: u32) -> Result { + if value >= (1 << 24) { Err(()) } else { Ok(Self(value)) } + } +} + +impl TryFrom for usize { + type Error = TryFromIntError; + + fn try_from(value: U24) -> Result { + Self::try_from(value.0) + } +} + +/// A trait to perform an unchecked cast from one type to another. +trait UncheckedFrom { + /// The source type to convert from. + type T; + + /// Converts `value` to `Self`. + /// + /// Must be implemented as an `as` cast in release builds but can do extra + /// sanity-checking in debug builds. + fn unchecked_from(value: Self::T) -> Self; +} + +/// Implements an unchecked conversion function between two integer types. +/// +/// In debug mode, this asserts that the input value fits in the return type. +/// In release mode, this makes the conversion with truncation. +macro_rules! impl_unchecked_cast { + ( $name:ident, $from_ty:ty, $to_ty:ty, primitive ) => { + pub(crate) fn $name(value: $from_ty) -> $to_ty { + if cfg!(debug_assertions) { + <$to_ty>::try_from(value).unwrap() + } else { + value as $to_ty + } + } + }; + + ( $name:ident, $from_ty:ty, $to_ty:ty, user_defined ) => { + pub(crate) fn $name(value: $from_ty) -> $to_ty { + if cfg!(debug_assertions) { + <$to_ty>::try_from(value).unwrap() + } else { + <$to_ty>::unchecked_from(value) + } + } + }; +} + +impl_unchecked_cast!(unchecked_u32_as_u8, u32, u8, primitive); +impl_unchecked_cast!(unchecked_u32_as_u16, u32, u16, primitive); + +impl UncheckedFrom for usize { + type T = U24; + + fn unchecked_from(value: Self::T) -> Self { + value.0 as Self + } +} + +impl_unchecked_cast!(unchecked_u24_as_usize, U24, usize, user_defined); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_unchecked_u32_as_u8() { + assert_eq!(10u8, unchecked_u32_as_u8(10u32)); + } + + #[test] + fn test_unchecked_u32_as_u16() { + assert_eq!(10u16, unchecked_u32_as_u16(10u32)); + } + + #[test] + fn test_unchecked_u24_as_usize() { + assert_eq!(10_usize, unchecked_u24_as_usize(U24(10))); + } +} diff --git a/core2/src/parser.rs b/core2/src/parser.rs new file mode 100644 index 00000000..3b299e5f --- /dev/null +++ b/core2/src/parser.rs @@ -0,0 +1,4876 @@ +// EndBASIC +// Copyright 2020 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. + +//! Statement and expression parser for the EndBASIC language. + +use crate::ast::*; +use crate::lexer::{Lexer, PeekableLexer, Token, TokenSpan}; +use crate::reader::LineCol; +use std::cmp::Ordering; +use std::io; + +/// Errors that can occur during parsing. +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Syntax error in the input program at the given position with a description. + #[error("{}: {}", .0, .1)] + Bad(LineCol, String), + + /// I/O error while reading the input program. + #[error("{0}: {1}")] + Io(LineCol, io::Error), +} + +impl From<(LineCol, io::Error)> for Error { + fn from(value: (LineCol, io::Error)) -> Self { + Self::Io(value.0, value.1) + } +} + +/// Result for parser return values. +pub type Result = std::result::Result; + +/// Transforms a `VarRef` into an unannotated name. +/// +/// This is only valid for references that have no annotations in them. +fn vref_to_unannotated_string(vref: VarRef, pos: LineCol) -> Result { + if vref.ref_type.is_some() { + return Err(Error::Bad(pos, format!("Type annotation not allowed in {}", vref))); + } + Ok(vref.name) +} + +/// Converts a collection of `ArgSpan`s passed to a function or array reference to a collection +/// of expressions with proper validation. +pub(crate) fn argspans_to_exprs(spans: Vec) -> Vec { + let nargs = spans.len(); + let mut exprs = Vec::with_capacity(spans.len()); + for (i, span) in spans.into_iter().enumerate() { + debug_assert!( + (span.sep == ArgSep::End || i < nargs - 1) + || (span.sep != ArgSep::End || i == nargs - 1) + ); + match span.expr { + Some(expr) => exprs.push(expr), + None => unreachable!(), + } + } + exprs +} + +/// Operators that can appear within an expression. +/// +/// The main difference between this and `lexer::Token` is that, in here, we differentiate the +/// meaning of a minus sign and separate it in its two variants: the 2-operand `Subtract` and the +/// 1-operand `Negate`. +/// +/// That said, this type also is the right place to abstract away operator-related logic to +/// implement the expression parsing algorithm, so it's not completely useless. +#[derive(Debug, Eq, PartialEq)] +enum ExprOp { + /// Left parenthesis, used as a grouping marker in the operator stack. + LeftParen, + + /// Binary addition operator. + Add, + /// Binary subtraction operator. + Subtract, + /// Binary multiplication operator. + Multiply, + /// Binary division operator. + Divide, + /// Binary modulo operator. + Modulo, + /// Binary exponentiation operator. + Power, + /// Unary negation operator. + Negate, + + /// Binary equality comparison operator. + Equal, + /// Binary inequality comparison operator. + NotEqual, + /// Binary less-than comparison operator. + Less, + /// Binary less-than-or-equal comparison operator. + LessEqual, + /// Binary greater-than comparison operator. + Greater, + /// Binary greater-than-or-equal comparison operator. + GreaterEqual, + + /// Binary logical AND operator. + And, + /// Unary logical NOT operator. + Not, + /// Binary logical OR operator. + Or, + /// Binary logical XOR operator. + Xor, + + /// Binary left shift operator. + ShiftLeft, + /// Binary right shift operator. + ShiftRight, +} + +impl ExprOp { + /// Constructs a new operator based on a token, which must have a valid correspondence. + fn from(t: Token) -> Self { + match t { + Token::Equal => ExprOp::Equal, + Token::NotEqual => ExprOp::NotEqual, + Token::Less => ExprOp::Less, + Token::LessEqual => ExprOp::LessEqual, + Token::Greater => ExprOp::Greater, + Token::GreaterEqual => ExprOp::GreaterEqual, + Token::Plus => ExprOp::Add, + Token::Multiply => ExprOp::Multiply, + Token::Divide => ExprOp::Divide, + Token::Modulo => ExprOp::Modulo, + Token::Exponent => ExprOp::Power, + Token::And => ExprOp::And, + Token::Or => ExprOp::Or, + Token::Xor => ExprOp::Xor, + Token::ShiftLeft => ExprOp::ShiftLeft, + Token::ShiftRight => ExprOp::ShiftRight, + Token::Minus => panic!("Ambiguous token; cannot derive ExprOp"), + _ => panic!("Called on an non-operator"), + } + } + + /// Returns the priority of this operator. The specific number's meaning is only valid when + /// comparing it against other calls to this function. Higher number imply higher priority. + fn priority(&self) -> i8 { + match self { + ExprOp::LeftParen => 6, + ExprOp::Power => 6, + + ExprOp::Negate => 5, + ExprOp::Not => 5, + + ExprOp::Multiply => 4, + ExprOp::Divide => 4, + ExprOp::Modulo => 4, + + ExprOp::Add => 3, + ExprOp::Subtract => 3, + + ExprOp::ShiftLeft => 2, + ExprOp::ShiftRight => 2, + + ExprOp::Equal => 1, + ExprOp::NotEqual => 1, + ExprOp::Less => 1, + ExprOp::LessEqual => 1, + ExprOp::Greater => 1, + ExprOp::GreaterEqual => 1, + + ExprOp::And => 0, + ExprOp::Or => 0, + ExprOp::Xor => 0, + } + } +} + +/// An expression operator paired with its source position. +struct ExprOpSpan { + /// The expression operator. + op: ExprOp, + + /// The position where the operator appears in the source. + pos: LineCol, +} + +impl ExprOpSpan { + /// Creates a new span from its parts. + fn new(op: ExprOp, pos: LineCol) -> Self { + Self { op, pos } + } + + /// Pops operands from the `expr` stack, applies this operation, and pushes the result back. + fn apply(&self, exprs: &mut Vec) -> Result<()> { + fn apply1( + exprs: &mut Vec, + pos: LineCol, + f: fn(Box) -> Expr, + ) -> Result<()> { + if exprs.is_empty() { + return Err(Error::Bad(pos, "Not enough values to apply operator".to_owned())); + } + let expr = exprs.pop().unwrap(); + exprs.push(f(Box::from(UnaryOpSpan { expr, pos }))); + Ok(()) + } + + fn apply2( + exprs: &mut Vec, + pos: LineCol, + f: fn(Box) -> Expr, + ) -> Result<()> { + if exprs.len() < 2 { + return Err(Error::Bad(pos, "Not enough values to apply operator".to_owned())); + } + let rhs = exprs.pop().unwrap(); + let lhs = exprs.pop().unwrap(); + exprs.push(f(Box::from(BinaryOpSpan { lhs, rhs, pos }))); + Ok(()) + } + + match self.op { + ExprOp::Add => apply2(exprs, self.pos, Expr::Add), + ExprOp::Subtract => apply2(exprs, self.pos, Expr::Subtract), + ExprOp::Multiply => apply2(exprs, self.pos, Expr::Multiply), + ExprOp::Divide => apply2(exprs, self.pos, Expr::Divide), + ExprOp::Modulo => apply2(exprs, self.pos, Expr::Modulo), + ExprOp::Power => apply2(exprs, self.pos, Expr::Power), + + ExprOp::Equal => apply2(exprs, self.pos, Expr::Equal), + ExprOp::NotEqual => apply2(exprs, self.pos, Expr::NotEqual), + ExprOp::Less => apply2(exprs, self.pos, Expr::Less), + ExprOp::LessEqual => apply2(exprs, self.pos, Expr::LessEqual), + ExprOp::Greater => apply2(exprs, self.pos, Expr::Greater), + ExprOp::GreaterEqual => apply2(exprs, self.pos, Expr::GreaterEqual), + + ExprOp::And => apply2(exprs, self.pos, Expr::And), + ExprOp::Or => apply2(exprs, self.pos, Expr::Or), + ExprOp::Xor => apply2(exprs, self.pos, Expr::Xor), + + ExprOp::ShiftLeft => apply2(exprs, self.pos, Expr::ShiftLeft), + ExprOp::ShiftRight => apply2(exprs, self.pos, Expr::ShiftRight), + + ExprOp::Negate => apply1(exprs, self.pos, Expr::Negate), + ExprOp::Not => apply1(exprs, self.pos, Expr::Not), + + ExprOp::LeftParen => Ok(()), + } + } +} + +/// Parser that converts a token stream into an AST of statements. +pub struct Parser<'a> { + /// The lexer providing tokens for parsing. + lexer: PeekableLexer<'a>, +} + +impl<'a> Parser<'a> { + /// Creates a new parser from the given readable. + fn from(input: &'a mut dyn io::Read) -> Self { + Self { lexer: Lexer::from(input).peekable() } + } + + /// Expects the peeked token to be `t` and consumes it. Otherwise, leaves the token in the + /// stream and fails with error `err`. + fn expect_and_consume>(&mut self, t: Token, err: E) -> Result { + let peeked = self.lexer.peek()?; + if peeked.token != t { + return Err(Error::Bad(peeked.pos, err.into())); + } + Ok(self.lexer.consume_peeked()) + } + + /// Expects the peeked token to be `t` and consumes it. Otherwise, leaves the token in the + /// stream and fails with error `err`, pointing at `pos` as the original location of the + /// problem. + fn expect_and_consume_with_pos>( + &mut self, + t: Token, + pos: LineCol, + err: E, + ) -> Result<()> { + let peeked = self.lexer.peek()?; + if peeked.token != t { + return Err(Error::Bad(pos, err.into())); + } + self.lexer.consume_peeked(); + Ok(()) + } + + /// Reads statements until the `delim` keyword is found. The delimiter is not consumed. + fn parse_until(&mut self, delim: Token) -> Result> { + let mut stmts = vec![]; + loop { + let peeked = self.lexer.peek()?; + if peeked.token == delim { + break; + } else if peeked.token == Token::Eol { + self.lexer.consume_peeked(); + continue; + } + match self.parse_one_safe()? { + Some(stmt) => stmts.push(stmt), + None => break, + } + } + Ok(stmts) + } + + /// Parses an assignment for the variable reference `vref` already read. + fn parse_assignment(&mut self, vref: VarRef, vref_pos: LineCol) -> Result { + let expr = self.parse_required_expr("Missing expression in assignment")?; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol | Token::Else => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in assignment", t))), + } + Ok(Statement::Assignment(AssignmentSpan { vref, vref_pos, expr })) + } + + /// Parses an assignment to the array `varref` with `subscripts`, both of which have already + /// been read. + fn parse_array_assignment( + &mut self, + vref: VarRef, + vref_pos: LineCol, + subscripts: Vec, + ) -> Result { + let expr = self.parse_required_expr("Missing expression in array assignment")?; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol | Token::Else => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in array assignment", t))), + } + Ok(Statement::ArrayAssignment(ArrayAssignmentSpan { vref, vref_pos, subscripts, expr })) + } + + /// Parses a builtin call (things of the form `INPUT a`). + fn parse_builtin_call( + &mut self, + vref: VarRef, + vref_pos: LineCol, + mut first: Option, + ) -> Result { + let mut name = vref_to_unannotated_string(vref, vref_pos)?; + name.make_ascii_uppercase(); + + let mut args = vec![]; + loop { + let expr = self.parse_expr(first.take())?; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof | Token::Eol | Token::Else => { + if expr.is_some() || !args.is_empty() { + args.push(ArgSpan { expr, sep: ArgSep::End, sep_pos: peeked.pos }); + } + break; + } + Token::Semicolon => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::Short, sep_pos: peeked.pos }); + } + Token::Comma => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::Long, sep_pos: peeked.pos }); + } + Token::As => { + let peeked = self.lexer.consume_peeked(); + args.push(ArgSpan { expr, sep: ArgSep::As, sep_pos: peeked.pos }); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, semicolon, or end of statement".to_owned(), + )); + } + } + } + Ok(Statement::Call(CallSpan { vref: VarRef::new(name, None), vref_pos, args })) + } + + /// Starts processing either an array reference or a builtin call and disambiguates between the + /// two. + fn parse_array_or_builtin_call( + &mut self, + vref: VarRef, + vref_pos: LineCol, + ) -> Result { + match self.lexer.peek()?.token { + Token::LeftParen => { + let left_paren = self.lexer.consume_peeked(); + let spans = self.parse_comma_separated_exprs()?; + let mut exprs = spans.into_iter().map(|span| span.expr.unwrap()).collect(); + match self.lexer.peek()?.token { + Token::Equal => { + self.lexer.consume_peeked(); + self.parse_array_assignment(vref, vref_pos, exprs) + } + _ => { + if exprs.len() != 1 { + return Err(Error::Bad( + left_paren.pos, + "Expected expression".to_owned(), + )); + } + self.parse_builtin_call(vref, vref_pos, Some(exprs.remove(0))) + } + } + } + _ => self.parse_builtin_call(vref, vref_pos, None), + } + } + + /// Parses the type name of an `AS` type definition. + /// + /// The `AS` token has already been consumed, so all this does is read a literal type name and + /// convert it to the corresponding expression type. + fn parse_as_type(&mut self) -> Result<(ExprType, LineCol)> { + let token_span = self.lexer.read()?; + match token_span.token { + Token::BooleanName => Ok((ExprType::Boolean, token_span.pos)), + Token::DoubleName => Ok((ExprType::Double, token_span.pos)), + Token::IntegerName => Ok((ExprType::Integer, token_span.pos)), + Token::TextName => Ok((ExprType::Text, token_span.pos)), + t => Err(Error::Bad( + token_span.pos, + format!("Invalid type name {} in AS type definition", t), + )), + } + } + + /// Parses a `DATA` statement. + fn parse_data(&mut self) -> Result { + let mut values = vec![]; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof | Token::Eol | Token::Else => { + values.push(None); + break; + } + _ => (), + } + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Boolean(b) => { + values.push(Some(Expr::Boolean(BooleanSpan { value: b, pos: token_span.pos }))) + } + Token::Double(d) => { + values.push(Some(Expr::Double(DoubleSpan { value: d, pos: token_span.pos }))) + } + Token::Integer(i) => { + values.push(Some(Expr::Integer(IntegerSpan { value: i, pos: token_span.pos }))) + } + Token::Text(t) => { + values.push(Some(Expr::Text(TextSpan { value: t, pos: token_span.pos }))) + } + + Token::Minus => { + let token_span2 = self.lexer.read()?; + match token_span2.token { + Token::Double(d) => values.push(Some(Expr::Double(DoubleSpan { + value: -d, + pos: token_span.pos, + }))), + Token::Integer(i) => values.push(Some(Expr::Integer(IntegerSpan { + value: -i, + pos: token_span.pos, + }))), + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected number after -".to_owned(), + )); + } + } + } + + Token::Eof | Token::Eol | Token::Else => { + panic!("Should not be consumed here; handled above") + } + + Token::Comma => { + values.push(None); + continue; + } + + t => { + return Err(Error::Bad( + token_span.pos, + format!("Unexpected {} in DATA statement", t), + )); + } + } + + let peeked = self.lexer.peek()?; + match &peeked.token { + Token::Eof | Token::Eol | Token::Else => { + break; + } + + Token::Comma => { + self.lexer.consume_peeked(); + } + + t => { + return Err(Error::Bad( + peeked.pos, + format!("Expected comma after datum but found {}", t), + )); + } + } + } + Ok(Statement::Data(DataSpan { values })) + } + + /// Parses the `AS typename` clause of a `DIM` statement. The caller has already consumed the + /// `AS` token. + fn parse_dim_as(&mut self) -> Result<(ExprType, LineCol)> { + let peeked = self.lexer.peek()?; + let (vtype, vtype_pos) = match peeked.token { + Token::Eof | Token::Eol => (ExprType::Integer, peeked.pos), + Token::As => { + self.lexer.consume_peeked(); + self.parse_as_type()? + } + _ => return Err(Error::Bad(peeked.pos, "Expected AS or end of statement".to_owned())), + }; + + let next = self.lexer.peek()?; + match &next.token { + Token::Eof | Token::Eol => (), + t => return Err(Error::Bad(next.pos, format!("Unexpected {} in DIM statement", t))), + } + + Ok((vtype, vtype_pos)) + } + + /// Parses a `DIM` statement. + fn parse_dim(&mut self) -> Result { + let peeked = self.lexer.peek()?; + let mut shared = false; + if peeked.token == Token::Shared { + self.lexer.consume_peeked(); + shared = true; + } + + let token_span = self.lexer.read()?; + let vref = match token_span.token { + Token::Symbol(vref) => vref, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected variable name after DIM".to_owned(), + )); + } + }; + // TODO(jmmv): Why do we require unannotated strings? We could also take one and then + // skip the `AS ` portion. + let name = vref_to_unannotated_string(vref, token_span.pos)?; + let name_pos = token_span.pos; + + match self.lexer.peek()?.token { + Token::LeftParen => { + let peeked = self.lexer.consume_peeked(); + let dimensions = self.parse_comma_separated_exprs()?; + if dimensions.is_empty() { + return Err(Error::Bad( + peeked.pos, + "Arrays require at least one dimension".to_owned(), + )); + } + let (subtype, subtype_pos) = self.parse_dim_as()?; + Ok(Statement::DimArray(DimArraySpan { + name, + name_pos, + shared, + dimensions: argspans_to_exprs(dimensions), + subtype, + subtype_pos, + })) + } + _ => { + let (vtype, vtype_pos) = self.parse_dim_as()?; + Ok(Statement::Dim(DimSpan { name, name_pos, shared, vtype, vtype_pos })) + } + } + } + + /// Parses the `UNTIL` or `WHILE` clause of a `DO` loop. + /// + /// `part` is a string indicating where the clause is expected (either after `DO` or after + /// `LOOP`). + /// + /// Returns the guard expression and a boolean indicating if this is an `UNTIL` clause. + fn parse_do_guard(&mut self, part: &str) -> Result> { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Until => { + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in UNTIL clause")?; + Ok(Some((expr, true))) + } + Token::While => { + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in WHILE clause")?; + Ok(Some((expr, false))) + } + Token::Eof | Token::Eol => Ok(None), + _ => { + let token_span = self.lexer.consume_peeked(); + Err(Error::Bad( + token_span.pos, + format!("Expecting newline, UNTIL or WHILE after {}", part), + )) + } + } + } + + /// Parses a `DO` statement. + fn parse_do(&mut self, do_pos: LineCol) -> Result { + let pre_guard = self.parse_do_guard("DO")?; + self.expect_and_consume(Token::Eol, "Expecting newline after DO")?; + + let stmts = self.parse_until(Token::Loop)?; + self.expect_and_consume_with_pos(Token::Loop, do_pos, "DO without LOOP")?; + + let post_guard = self.parse_do_guard("LOOP")?; + + let guard = match (pre_guard, post_guard) { + (None, None) => DoGuard::Infinite, + (Some((guard, true)), None) => DoGuard::PreUntil(guard), + (Some((guard, false)), None) => DoGuard::PreWhile(guard), + (None, Some((guard, true))) => DoGuard::PostUntil(guard), + (None, Some((guard, false))) => DoGuard::PostWhile(guard), + (Some(_), Some(_)) => { + return Err(Error::Bad( + do_pos, + "DO loop cannot have pre and post guards at the same time".to_owned(), + )); + } + }; + + Ok(Statement::Do(DoSpan { guard, body: stmts })) + } + + /// Advances until the next statement after failing to parse a `DO` statement. + fn reset_do(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Loop => { + self.lexer.consume_peeked(); + loop { + match self.lexer.peek()?.token { + Token::Eof | Token::Eol => break, + _ => { + self.lexer.consume_peeked(); + } + } + } + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses a potential `END` statement but, if this corresponds to a statement terminator such + /// as `END IF`, returns the token that followed `END`. + fn maybe_parse_end(&mut self, pos: LineCol) -> Result> { + match self.lexer.peek()?.token { + Token::Function => Ok(Err(Token::Function)), + Token::If => Ok(Err(Token::If)), + Token::Select => Ok(Err(Token::Select)), + Token::Sub => Ok(Err(Token::Sub)), + _ => { + let code = self.parse_expr(None)?; + Ok(Ok(Statement::End(EndSpan { code, pos }))) + } + } + } + + /// Parses an `END` statement. + fn parse_end(&mut self, pos: LineCol) -> Result { + match self.maybe_parse_end(pos)? { + Ok(stmt) => Ok(stmt), + Err(token) => Err(Error::Bad(pos, format!("END {} without {}", token, token))), + } + } + + /// Parses an `EXIT` statement. + fn parse_exit(&mut self, pos: LineCol) -> Result { + let peeked = self.lexer.peek()?; + let stmt = match peeked.token { + Token::Do => Statement::ExitDo(ExitSpan { pos }), + Token::For => Statement::ExitFor(ExitSpan { pos }), + Token::Function => Statement::ExitFunction(ExitSpan { pos }), + Token::Sub => Statement::ExitSub(ExitSpan { pos }), + _ => { + return Err(Error::Bad( + peeked.pos, + "Expecting DO, FOR, FUNCTION or SUB after EXIT".to_owned(), + )); + } + }; + self.lexer.consume_peeked(); + Ok(stmt) + } + + /// Parses a variable list of comma-separated expressions. The caller must have consumed the + /// open parenthesis and we stop processing when we encounter the terminating parenthesis (and + /// consume it). We expect at least one expression. + fn parse_comma_separated_exprs(&mut self) -> Result> { + let mut spans = vec![]; + + // The first expression is optional to support calls to functions without arguments. + let mut is_first = true; + let mut prev_expr = self.parse_expr(None)?; + + loop { + let peeked = self.lexer.peek()?; + let pos = peeked.pos; + match &peeked.token { + Token::RightParen => { + self.lexer.consume_peeked(); + + if let Some(expr) = prev_expr.take() { + spans.push(ArgSpan { expr: Some(expr), sep: ArgSep::End, sep_pos: pos }); + } else if !is_first { + return Err(Error::Bad(pos, "Missing expression".to_owned())); + } + + break; + } + Token::Comma => { + self.lexer.consume_peeked(); + + if let Some(expr) = prev_expr.take() { + // The first expression is optional to support calls to functions without + // arguments. + spans.push(ArgSpan { expr: Some(expr), sep: ArgSep::Long, sep_pos: pos }); + } else { + return Err(Error::Bad(pos, "Missing expression".to_owned())); + } + + prev_expr = self.parse_expr(None)?; + } + t => return Err(Error::Bad(pos, format!("Unexpected {}", t))), + } + + is_first = false; + } + + Ok(spans) + } + + /// Parses an expression. + /// + /// Returns `None` if no expression was found. This is necessary to treat the case of empty + /// arguments to statements, as is the case in `PRINT a , , b`. + /// + /// If the caller has already processed a parenthesized term of an expression like + /// `(first) + second`, then that term must be provided in `first`. + /// + /// This is an implementation of the Shunting Yard Algorithm by Edgar Dijkstra. + fn parse_expr(&mut self, first: Option) -> Result> { + let mut exprs: Vec = vec![]; + let mut op_spans: Vec = vec![]; + + let mut need_operand = true; // Also tracks whether an upcoming minus is unary. + if let Some(e) = first { + exprs.push(e); + need_operand = false; + } + + loop { + let mut handle_operand = |e, pos| { + if !need_operand { + return Err(Error::Bad(pos, "Unexpected value in expression".to_owned())); + } + need_operand = false; + exprs.push(e); + Ok(()) + }; + + // Stop processing if we encounter an expression separator, but don't consume it because + // the caller needs to have access to it. + match self.lexer.peek()?.token { + Token::Eof + | Token::Eol + | Token::As + | Token::Comma + | Token::Else + | Token::Semicolon + | Token::Then + | Token::To + | Token::Step => break, + Token::RightParen if !op_spans.iter().any(|eos| eos.op == ExprOp::LeftParen) => { + // We encountered an unbalanced parenthesis but we don't know if this is + // because we were called from within an argument list (in which case the + // caller consumed the opening parenthesis and is expecting to consume the + // closing parenthesis) or because we really found an invalid expression. + // Only the caller can know, so avoid consuming the token and exit. + break; + } + _ => (), + }; + + let ts = self.lexer.consume_peeked(); + match ts.token { + Token::Boolean(value) => { + handle_operand(Expr::Boolean(BooleanSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Double(value) => { + handle_operand(Expr::Double(DoubleSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Integer(value) => { + handle_operand(Expr::Integer(IntegerSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Text(value) => { + handle_operand(Expr::Text(TextSpan { value, pos: ts.pos }), ts.pos)? + } + Token::Symbol(vref) => { + handle_operand(Expr::Symbol(SymbolSpan { vref, pos: ts.pos }), ts.pos)? + } + + Token::LeftParen => { + // If the last operand we encountered was a symbol, collapse it and the left + // parenthesis into the beginning of a function call. + match exprs.pop() { + Some(Expr::Symbol(span)) => { + if !need_operand { + exprs.push(Expr::Call(CallSpan { + vref: span.vref, + vref_pos: span.pos, + args: self.parse_comma_separated_exprs()?, + })); + need_operand = false; + } else { + // We popped out the last expression to see if it this left + // parenthesis started a function call... but it did not (it is a + // symbol following a parenthesis) so put both the expression and + // the token back. + op_spans.push(ExprOpSpan::new(ExprOp::LeftParen, ts.pos)); + exprs.push(Expr::Symbol(span)); + need_operand = true; + } + } + e => { + if let Some(e) = e { + // We popped out the last expression to see if this left + // parenthesis started a function call... but if it didn't, we have + // to put the expression back. + exprs.push(e); + } + if !need_operand { + return Err(Error::Bad( + ts.pos, + format!("Unexpected {} in expression", ts.token), + )); + } + op_spans.push(ExprOpSpan::new(ExprOp::LeftParen, ts.pos)); + need_operand = true; + } + }; + } + Token::RightParen => { + let mut found = false; + while let Some(eos) = op_spans.pop() { + eos.apply(&mut exprs)?; + if eos.op == ExprOp::LeftParen { + found = true; + break; + } + } + assert!(found, "Unbalanced parenthesis should have been handled above"); + need_operand = false; + } + + Token::Not => { + op_spans.push(ExprOpSpan::new(ExprOp::Not, ts.pos)); + need_operand = true; + } + Token::Minus => { + let op; + if need_operand { + op = ExprOp::Negate; + } else { + op = ExprOp::Subtract; + while let Some(eos2) = op_spans.last() { + if eos2.op == ExprOp::LeftParen || eos2.op.priority() < op.priority() { + break; + } + let eos2 = op_spans.pop().unwrap(); + eos2.apply(&mut exprs)?; + } + } + op_spans.push(ExprOpSpan::new(op, ts.pos)); + need_operand = true; + } + + Token::Equal + | Token::NotEqual + | Token::Less + | Token::LessEqual + | Token::Greater + | Token::GreaterEqual + | Token::Plus + | Token::Multiply + | Token::Divide + | Token::Modulo + | Token::Exponent + | Token::And + | Token::Or + | Token::Xor + | Token::ShiftLeft + | Token::ShiftRight => { + let op = ExprOp::from(ts.token); + while let Some(eos2) = op_spans.last() { + if eos2.op == ExprOp::LeftParen || eos2.op.priority() < op.priority() { + break; + } + let eos2 = op_spans.pop().unwrap(); + eos2.apply(&mut exprs)?; + } + op_spans.push(ExprOpSpan::new(op, ts.pos)); + need_operand = true; + } + + Token::Bad(e) => return Err(Error::Bad(ts.pos, e)), + + Token::Eof + | Token::Eol + | Token::As + | Token::Comma + | Token::Else + | Token::Semicolon + | Token::Then + | Token::To + | Token::Step => { + panic!("Field separators handled above") + } + + Token::BooleanName + | Token::Case + | Token::Data + | Token::Do + | Token::Dim + | Token::DoubleName + | Token::Elseif + | Token::End + | Token::Error + | Token::Exit + | Token::For + | Token::Function + | Token::Gosub + | Token::Goto + | Token::If + | Token::Is + | Token::IntegerName + | Token::Label(_) + | Token::Loop + | Token::Next + | Token::On + | Token::Resume + | Token::Return + | Token::Select + | Token::Shared + | Token::Sub + | Token::TextName + | Token::Until + | Token::Wend + | Token::While => { + return Err(Error::Bad(ts.pos, "Unexpected keyword in expression".to_owned())); + } + }; + } + + while let Some(eos) = op_spans.pop() { + match eos.op { + ExprOp::LeftParen => { + return Err(Error::Bad(eos.pos, "Unbalanced parenthesis".to_owned())); + } + _ => eos.apply(&mut exprs)?, + } + } + + if let Some(expr) = exprs.pop() { Ok(Some(expr)) } else { Ok(None) } + } + + /// Wrapper over `parse_expr` that requires an expression to be present and returns an error + /// with `msg` otherwise. + fn parse_required_expr(&mut self, msg: &'static str) -> Result { + let next_pos = self.lexer.peek()?.pos; + match self.parse_expr(None)? { + Some(expr) => Ok(expr), + None => Err(Error::Bad(next_pos, msg.to_owned())), + } + } + + /// Parses a `GOSUB` statement. + fn parse_gosub(&mut self) -> Result { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(i) => { + let target = format!("{}", i); + Ok(Statement::Gosub(GotoSpan { target, target_pos: token_span.pos })) + } + Token::Label(target) => { + Ok(Statement::Gosub(GotoSpan { target, target_pos: token_span.pos })) + } + _ => Err(Error::Bad(token_span.pos, "Expected label name after GOSUB".to_owned())), + } + } + + /// Parses a `GOTO` statement. + fn parse_goto(&mut self) -> Result { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(i) => { + let target = format!("{}", i); + Ok(Statement::Goto(GotoSpan { target, target_pos: token_span.pos })) + } + Token::Label(target) => { + Ok(Statement::Goto(GotoSpan { target, target_pos: token_span.pos })) + } + _ => Err(Error::Bad(token_span.pos, "Expected label name after GOTO".to_owned())), + } + } + + /// Parses the branches of a uniline `IF` statement. + fn parse_if_uniline(&mut self, branches: &mut Vec) -> Result<()> { + debug_assert!(!branches.is_empty(), "Caller must populate the guard of the first branch"); + + let mut has_else = false; + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Else => has_else = true, + _ => { + let stmt = self + .parse_uniline()? + .expect("The caller already checked for a non-empty token"); + branches[0].body.push(stmt); + } + } + + let peeked = self.lexer.peek()?; + has_else |= peeked.token == Token::Else; + + if has_else { + let else_span = self.lexer.consume_peeked(); + let expr = Expr::Boolean(BooleanSpan { value: true, pos: else_span.pos }); + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + if let Some(stmt) = self.parse_uniline()? { + branches[1].body.push(stmt); + } + } + + Ok(()) + } + + /// Parses the branches of a multiline `IF` statement. + fn parse_if_multiline( + &mut self, + if_pos: LineCol, + branches: &mut Vec, + ) -> Result<()> { + debug_assert!(!branches.is_empty(), "Caller must populate the guard of the first branch"); + + let mut i = 0; + let mut last = false; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Elseif => { + if last { + return Err(Error::Bad( + peeked.pos, + "Unexpected ELSEIF after ELSE".to_owned(), + )); + } + + self.lexer.consume_peeked(); + let expr = self.parse_required_expr("No expression in ELSEIF statement")?; + self.expect_and_consume(Token::Then, "No THEN in ELSEIF statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after THEN")?; + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + i += 1; + } + + Token::Else => { + if last { + return Err(Error::Bad(peeked.pos, "Duplicate ELSE after ELSE".to_owned())); + } + + let else_span = self.lexer.consume_peeked(); + self.expect_and_consume(Token::Eol, "Expecting newline after ELSE")?; + + let expr = Expr::Boolean(BooleanSpan { value: true, pos: else_span.pos }); + branches.push(IfBranchSpan { guard: expr, body: vec![] }); + i += 1; + + last = true; + } + + Token::End => { + let token_span = self.lexer.consume_peeked(); + match self.maybe_parse_end(token_span.pos)? { + Ok(stmt) => { + branches[i].body.push(stmt); + } + Err(Token::If) => { + break; + } + Err(token) => { + return Err(Error::Bad( + token_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + + _ => match self.parse_one_safe()? { + Some(stmt) => { + branches[i].body.push(stmt); + } + None => { + break; + } + }, + } + } + + self.expect_and_consume_with_pos(Token::If, if_pos, "IF without END IF") + } + + /// Parses an `IF` statement. + fn parse_if(&mut self, if_pos: LineCol) -> Result { + let expr = self.parse_required_expr("No expression in IF statement")?; + self.expect_and_consume(Token::Then, "No THEN in IF statement")?; + + let mut branches = vec![IfBranchSpan { guard: expr, body: vec![] }]; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol | Token::Eof => self.parse_if_multiline(if_pos, &mut branches)?, + _ => self.parse_if_uniline(&mut branches)?, + } + + Ok(Statement::If(IfSpan { branches })) + } + + /// Advances until the next statement after failing to parse an `IF` statement. + fn reset_if(&mut self, if_pos: LineCol) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + self.expect_and_consume_with_pos(Token::If, if_pos, "IF without END IF")?; + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Extracts the optional `STEP` part of a `FOR` statement, with a default of 1. + /// + /// Returns the step as an expression, an `Ordering` value representing how the step value + /// compares to zero, and whether the step is a double or not. + fn parse_step(&mut self) -> Result<(Expr, Ordering, bool)> { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Step => self.lexer.consume_peeked(), + _ => { + // The position we return here for the step isn't truly the right value, but given + // that we know the hardcoded step of 1 is valid, the caller will not error out and + // will not print the slightly invalid position. + return Ok(( + Expr::Integer(IntegerSpan { value: 1, pos: peeked.pos }), + Ordering::Greater, + false, + )); + } + }; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Double(d) => { + let peeked = self.lexer.consume_peeked(); + let sign = if d == 0.0 { Ordering::Equal } else { Ordering::Greater }; + Ok((Expr::Double(DoubleSpan { value: d, pos: peeked.pos }), sign, true)) + } + Token::Integer(i) => { + let peeked = self.lexer.consume_peeked(); + Ok((Expr::Integer(IntegerSpan { value: i, pos: peeked.pos }), i.cmp(&0), false)) + } + Token::Minus => { + self.lexer.consume_peeked(); + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Double(d) => { + let peeked = self.lexer.consume_peeked(); + let sign = if d == 0.0 { Ordering::Equal } else { Ordering::Less }; + Ok((Expr::Double(DoubleSpan { value: -d, pos: peeked.pos }), sign, true)) + } + Token::Integer(i) => { + let peeked = self.lexer.consume_peeked(); + Ok(( + Expr::Integer(IntegerSpan { value: -i, pos: peeked.pos }), + (-i).cmp(&0), + false, + )) + } + _ => Err(Error::Bad(peeked.pos, "STEP needs a literal number".to_owned())), + } + } + _ => Err(Error::Bad(peeked.pos, "STEP needs a literal number".to_owned())), + } + } + + /// Parses a `FOR` statement. + fn parse_for(&mut self, for_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let iterator = match token_span.token { + Token::Symbol(iterator) => match iterator.ref_type { + None | Some(ExprType::Double) | Some(ExprType::Integer) => iterator, + _ => { + return Err(Error::Bad( + token_span.pos, + "Iterator name in FOR statement must be a numeric reference".to_owned(), + )); + } + }, + _ => { + return Err(Error::Bad( + token_span.pos, + "No iterator name in FOR statement".to_owned(), + )); + } + }; + let iterator_pos = token_span.pos; + + self.expect_and_consume(Token::Equal, "No equal sign in FOR statement")?; + let start = self.parse_required_expr("No start expression in FOR statement")?; + + let to_span = self.expect_and_consume(Token::To, "No TO in FOR statement")?; + let end = self.parse_required_expr("No end expression in FOR statement")?; + + let (step, step_sign, iter_double) = self.parse_step()?; + let end_condition = match step_sign { + Ordering::Greater => Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: end, + pos: to_span.pos, + })), + Ordering::Less => Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: end, + pos: to_span.pos, + })), + Ordering::Equal => { + return Err(Error::Bad( + step.start_pos(), + "Infinite FOR loop; STEP cannot be 0".to_owned(), + )); + } + }; + + let next_value = Expr::Add(Box::from(BinaryOpSpan { + lhs: Expr::Symbol(SymbolSpan { vref: iterator.clone(), pos: iterator_pos }), + rhs: step, + pos: to_span.pos, + })); + + self.expect_and_consume(Token::Eol, "Expecting newline after FOR")?; + + let stmts = self.parse_until(Token::Next)?; + self.expect_and_consume_with_pos(Token::Next, for_pos, "FOR without NEXT")?; + + Ok(Statement::For(ForSpan { + iter: iterator, + iter_pos: iterator_pos, + iter_double, + start, + end: end_condition, + next: next_value, + body: stmts, + })) + } + + /// Advances until the next statement after failing to parse a `FOR` statement. + fn reset_for(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Next => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses the optional parameter list that may appear after a `FUNCTION` or `SUB` definition, + /// including the opening and closing parenthesis. + fn parse_callable_args(&mut self) -> Result> { + let mut params = vec![]; + let peeked = self.lexer.peek()?; + if peeked.token == Token::LeftParen { + self.lexer.consume_peeked(); + + loop { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Symbol(param) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::As { + self.lexer.consume_peeked(); + + let name = vref_to_unannotated_string(param, token_span.pos)?; + let (vtype, _pos) = self.parse_as_type()?; + params.push(VarRef::new(name, Some(vtype))); + } else { + params.push(param); + } + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a parameter name".to_owned(), + )); + } + } + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Comma => (), + Token::RightParen => break, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected comma, AS, or end of parameters list".to_owned(), + )); + } + } + } + } + Ok(params) + } + + /// Parses the body of a callable and returns the collection of statements and the position + /// of the end of the body. + fn parse_callable_body( + &mut self, + start_pos: LineCol, + exp_token: Token, + ) -> Result<(Vec, LineCol)> { + debug_assert!(matches!(exp_token, Token::Function | Token::Sub)); + + let mut body = vec![]; + let end_pos; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof => { + end_pos = peeked.pos; + break; + } + + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Function | Token::Sub => { + return Err(Error::Bad( + peeked.pos, + "Cannot nest FUNCTION or SUB definitions".to_owned(), + )); + } + + Token::End => { + let end_span = self.lexer.consume_peeked(); + match self.maybe_parse_end(end_span.pos)? { + Ok(stmt) => { + body.push(stmt); + } + Err(token) if token == exp_token => { + end_pos = end_span.pos; + break; + } + Err(token) => { + return Err(Error::Bad( + end_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + + _ => match self.parse_one_safe()? { + Some(stmt) => body.push(stmt), + None => { + return Err(Error::Bad( + start_pos, + format!("{} without END {}", exp_token, exp_token), + )); + } + }, + } + } + + self.expect_and_consume_with_pos( + exp_token.clone(), + start_pos, + format!("{} without END {}", exp_token, exp_token), + )?; + + Ok((body, end_pos)) + } + + /// Parses a `FUNCTION` definition. + fn parse_function(&mut self, function_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let name = match token_span.token { + Token::Symbol(name) => { + if name.ref_type.is_none() { + VarRef::new(name.name, Some(ExprType::Integer)) + } else { + name + } + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a function name after FUNCTION".to_owned(), + )); + } + }; + let name_pos = token_span.pos; + + let params = self.parse_callable_args()?; + self.expect_and_consume(Token::Eol, "Expected newline after FUNCTION name")?; + + let (body, end_pos) = self.parse_callable_body(function_pos, Token::Function)?; + + Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + } + + /// Parses a `SUB` definition. + fn parse_sub(&mut self, sub_pos: LineCol) -> Result { + let token_span = self.lexer.read()?; + let name = match token_span.token { + Token::Symbol(name) => { + if name.ref_type.is_some() { + return Err(Error::Bad( + token_span.pos, + "SUBs cannot return a value so type annotations are not allowed".to_owned(), + )); + } + name + } + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected a function name after SUB".to_owned(), + )); + } + }; + let name_pos = token_span.pos; + + let params = self.parse_callable_args()?; + self.expect_and_consume(Token::Eol, "Expected newline after SUB name")?; + + let (body, end_pos) = self.parse_callable_body(sub_pos, Token::Sub)?; + + Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + } + + /// Advances until the next statement after failing to parse a `FUNCTION` or `SUB` definition. + fn reset_callable(&mut self, exp_token: Token) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + + let token_span = self.lexer.read()?; + if token_span.token == exp_token { + break; + } + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses an `ON ERROR` statement. Only `ON` has been consumed so far. + fn parse_on(&mut self) -> Result { + self.expect_and_consume(Token::Error, "Expected ERROR after ON")?; + + let token_span = self.lexer.read()?; + match token_span.token { + Token::Goto => { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Integer(0) => Ok(Statement::OnError(OnErrorSpan::Reset)), + Token::Integer(i) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: format!("{}", i), + target_pos: token_span.pos, + }))), + Token::Label(target) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target, + target_pos: token_span.pos, + }))), + _ => Err(Error::Bad( + token_span.pos, + "Expected label name or 0 after ON ERROR GOTO".to_owned(), + )), + } + } + Token::Resume => { + self.expect_and_consume(Token::Next, "Expected NEXT after ON ERROR RESUME")?; + Ok(Statement::OnError(OnErrorSpan::ResumeNext)) + } + _ => { + Err(Error::Bad(token_span.pos, "Expected GOTO or RESUME after ON ERROR".to_owned())) + } + } + } + + /// Parses the guards after a `CASE` keyword. + fn parse_case_guards(&mut self) -> Result> { + let mut guards = vec![]; + + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Else => { + let token_span = self.lexer.consume_peeked(); + + if !guards.is_empty() { + return Err(Error::Bad( + token_span.pos, + "CASE ELSE must be on its own".to_owned(), + )); + } + + let peeked = self.lexer.peek()?; + if peeked.token != Token::Eol && peeked.token != Token::Eof { + return Err(Error::Bad( + peeked.pos, + "Expected newline after CASE ELSE".to_owned(), + )); + } + + break; + } + + Token::Is => { + self.lexer.consume_peeked(); + + let token_span = self.lexer.read()?; + let rel_op = match token_span.token { + Token::Equal => CaseRelOp::Equal, + Token::NotEqual => CaseRelOp::NotEqual, + Token::Less => CaseRelOp::Less, + Token::LessEqual => CaseRelOp::LessEqual, + Token::Greater => CaseRelOp::Greater, + Token::GreaterEqual => CaseRelOp::GreaterEqual, + _ => { + return Err(Error::Bad( + token_span.pos, + "Expected relational operator".to_owned(), + )); + } + }; + + let expr = + self.parse_required_expr("Missing expression after relational operator")?; + guards.push(CaseGuardSpan::Is(rel_op, expr)); + } + + _ => { + let from_expr = self.parse_required_expr("Missing expression in CASE guard")?; + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol | Token::Comma => { + guards.push(CaseGuardSpan::Is(CaseRelOp::Equal, from_expr)); + } + Token::To => { + self.lexer.consume_peeked(); + let to_expr = self + .parse_required_expr("Missing expression after TO in CASE guard")?; + guards.push(CaseGuardSpan::To(from_expr, to_expr)); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, newline, or TO after expression".to_owned(), + )); + } + } + } + } + + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol => { + break; + } + Token::Comma => { + self.lexer.consume_peeked(); + } + _ => { + return Err(Error::Bad( + peeked.pos, + "Expected comma, newline, or TO after expression".to_owned(), + )); + } + } + } + + Ok(guards) + } + + /// Parses a `SELECT` statement. + fn parse_select(&mut self, select_pos: LineCol) -> Result { + self.expect_and_consume(Token::Case, "Expecting CASE after SELECT")?; + + let expr = self.parse_required_expr("No expression in SELECT CASE statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after SELECT CASE")?; + + let mut cases = vec![]; + + let mut i = 0; + let mut last = false; + let end_pos; + loop { + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eof => { + end_pos = peeked.pos; + break; + } + + Token::Eol => { + self.lexer.consume_peeked(); + } + + Token::Case => { + let peeked = self.lexer.consume_peeked(); + let guards = self.parse_case_guards()?; + self.expect_and_consume(Token::Eol, "Expecting newline after CASE")?; + + let is_last = guards.is_empty(); + if last { + if is_last { + return Err(Error::Bad( + peeked.pos, + "CASE ELSE must be unique".to_owned(), + )); + } else { + return Err(Error::Bad(peeked.pos, "CASE ELSE is not last".to_owned())); + } + } + last |= is_last; + + cases.push(CaseSpan { guards, body: vec![] }); + if cases.len() > 1 { + i += 1; + } + } + + Token::End => { + let end_span = self.lexer.consume_peeked(); + match self.maybe_parse_end(end_span.pos)? { + Ok(stmt) => { + if cases.is_empty() { + return Err(Error::Bad( + end_span.pos, + "Expected CASE after SELECT CASE before any statement" + .to_owned(), + )); + } + + cases[i].body.push(stmt); + } + Err(Token::Select) => { + end_pos = end_span.pos; + break; + } + Err(token) => { + if cases.is_empty() { + return Err(Error::Bad( + end_span.pos, + "Expected CASE after SELECT CASE before any statement" + .to_owned(), + )); + } else { + return Err(Error::Bad( + end_span.pos, + format!("END {} without {}", token, token), + )); + } + } + } + } + + _ => { + if cases.is_empty() { + return Err(Error::Bad( + peeked.pos, + "Expected CASE after SELECT CASE before any statement".to_owned(), + )); + } + + if let Some(stmt) = self.parse_one_safe()? { + cases[i].body.push(stmt); + } + } + } + } + + self.expect_and_consume_with_pos(Token::Select, select_pos, "SELECT without END SELECT")?; + + Ok(Statement::Select(SelectSpan { expr, cases, end_pos })) + } + + /// Advances until the next statement after failing to parse a `SELECT` statement. + fn reset_select(&mut self, select_pos: LineCol) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::End => { + self.lexer.consume_peeked(); + self.expect_and_consume_with_pos( + Token::Select, + select_pos, + "SELECT without END SELECT", + )?; + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Parses a `WHILE` statement. + fn parse_while(&mut self, while_pos: LineCol) -> Result { + let expr = self.parse_required_expr("No expression in WHILE statement")?; + self.expect_and_consume(Token::Eol, "Expecting newline after WHILE")?; + + let stmts = self.parse_until(Token::Wend)?; + self.expect_and_consume_with_pos(Token::Wend, while_pos, "WHILE without WEND")?; + + Ok(Statement::While(WhileSpan { expr, body: stmts })) + } + + /// Advances until the next statement after failing to parse a `WHILE` statement. + fn reset_while(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Wend => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + self.reset() + } + + /// Extracts the next available uniline statement from the input stream, or `None` if none is + /// available. + /// + /// The statement must be specifiable in a single line as part of a uniline `IF` statement, and + /// we currently expect this to only be used while parsing an `IF`. + /// + /// On success, the stream is left in a position where the next statement can be extracted. + /// On failure, the caller must advance the stream to the next statement by calling `reset`. + fn parse_uniline(&mut self) -> Result> { + let token_span = self.lexer.read()?; + match token_span.token { + Token::Data => Ok(Some(self.parse_data()?)), + Token::End => Ok(Some(self.parse_end(token_span.pos)?)), + Token::Eof | Token::Eol => Ok(None), + Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), + Token::Gosub => Ok(Some(self.parse_gosub()?)), + Token::Goto => Ok(Some(self.parse_goto()?)), + Token::On => Ok(Some(self.parse_on()?)), + Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), + Token::Symbol(vref) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::Equal { + self.lexer.consume_peeked(); + Ok(Some(self.parse_assignment(vref, token_span.pos)?)) + } else { + Ok(Some(self.parse_array_or_builtin_call(vref, token_span.pos)?)) + } + } + Token::Bad(msg) => Err(Error::Bad(token_span.pos, msg)), + t => Err(Error::Bad(token_span.pos, format!("Unexpected {} in uniline IF branch", t))), + } + } + + /// Extracts the next available statement from the input stream, or `None` if none is available. + /// + /// On success, the stream is left in a position where the next statement can be extracted. + /// On failure, the caller must advance the stream to the next statement by calling `reset`. + fn parse_one(&mut self) -> Result> { + loop { + match self.lexer.peek()?.token { + Token::Eol => { + self.lexer.consume_peeked(); + } + Token::Eof => return Ok(None), + _ => break, + } + } + let token_span = self.lexer.read()?; + let res = match token_span.token { + Token::Data => Ok(Some(self.parse_data()?)), + Token::Dim => Ok(Some(self.parse_dim()?)), + Token::Do => { + let result = self.parse_do(token_span.pos); + if result.is_err() { + self.reset_do()?; + } + Ok(Some(result?)) + } + Token::End => Ok(Some(self.parse_end(token_span.pos)?)), + Token::Eof => return Ok(None), + Token::Eol => Ok(None), + Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), + Token::If => { + let result = self.parse_if(token_span.pos); + if result.is_err() { + self.reset_if(token_span.pos)?; + } + Ok(Some(result?)) + } + Token::For => { + let result = self.parse_for(token_span.pos); + if result.is_err() { + self.reset_for()?; + } + Ok(Some(result?)) + } + Token::Function => { + let result = self.parse_function(token_span.pos); + if result.is_err() { + self.reset_callable(Token::Function)?; + } + Ok(Some(result?)) + } + Token::Gosub => { + let result = self.parse_gosub(); + Ok(Some(result?)) + } + Token::Goto => { + let result = self.parse_goto(); + Ok(Some(result?)) + } + Token::Integer(i) => { + let name = format!("{}", i); + // When we encounter a line number, we must return early to avoid looking for a line + // ending given that the next statement may start after the label we found. + return Ok(Some(Statement::Label(LabelSpan { name, name_pos: token_span.pos }))); + } + Token::Label(name) => { + // When we encounter a label, we must return early to avoid looking for a line + // ending given that the next statement may start after the label we found. + return Ok(Some(Statement::Label(LabelSpan { name, name_pos: token_span.pos }))); + } + Token::On => Ok(Some(self.parse_on()?)), + Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), + Token::Select => { + let result = self.parse_select(token_span.pos); + if result.is_err() { + self.reset_select(token_span.pos)?; + } + Ok(Some(result?)) + } + Token::Sub => { + let result = self.parse_sub(token_span.pos); + if result.is_err() { + self.reset_callable(Token::Sub)?; + } + Ok(Some(result?)) + } + Token::Symbol(vref) => { + let peeked = self.lexer.peek()?; + if peeked.token == Token::Equal { + self.lexer.consume_peeked(); + Ok(Some(self.parse_assignment(vref, token_span.pos)?)) + } else { + Ok(Some(self.parse_array_or_builtin_call(vref, token_span.pos)?)) + } + } + Token::While => { + let result = self.parse_while(token_span.pos); + if result.is_err() { + self.reset_while()?; + } + Ok(Some(result?)) + } + Token::Bad(msg) => return Err(Error::Bad(token_span.pos, msg)), + t => return Err(Error::Bad(token_span.pos, format!("Unexpected {} in statement", t))), + }; + + let token_span = self.lexer.peek()?; + match token_span.token { + Token::Eof => (), + Token::Eol => { + self.lexer.consume_peeked(); + } + _ => { + return Err(Error::Bad( + token_span.pos, + format!("Expected newline but found {}", token_span.token), + )); + } + }; + + res + } + + /// Advances until the next statement after failing to parse a single statement. + fn reset(&mut self) -> Result<()> { + loop { + match self.lexer.peek()?.token { + Token::Eof => break, + Token::Eol => { + self.lexer.consume_peeked(); + break; + } + _ => { + self.lexer.consume_peeked(); + } + } + } + Ok(()) + } + + /// Extracts the next available statement from the input stream, or `None` if none is available. + /// + /// The stream is always left in a position where the next statement extraction can be tried. + fn parse_one_safe(&mut self) -> Result> { + let result = self.parse_one(); + if result.is_err() { + self.reset()?; + } + result + } +} + +/// Iterator that yields parsed statements from an input stream. +pub(crate) struct StatementIter<'a> { + /// The underlying parser. + parser: Parser<'a>, +} + +impl Iterator for StatementIter<'_> { + type Item = Result; + + fn next(&mut self) -> Option { + self.parser.parse_one_safe().transpose() + } +} + +/// Extracts all statements from the input stream. +pub(crate) fn parse(input: &mut dyn io::Read) -> StatementIter<'_> { + StatementIter { parser: Parser::from(input) } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::ExprType; + + /// Syntactic sugar to instantiate a `LineCol` for testing. + fn lc(line: usize, col: usize) -> LineCol { + LineCol { line, col } + } + + /// Syntactic sugar to instantiate an `Expr::Boolean` for testing. + fn expr_boolean(value: bool, line: usize, col: usize) -> Expr { + Expr::Boolean(BooleanSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Double` for testing. + fn expr_double(value: f64, line: usize, col: usize) -> Expr { + Expr::Double(DoubleSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Integer` for testing. + fn expr_integer(value: i32, line: usize, col: usize) -> Expr { + Expr::Integer(IntegerSpan { value, pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Text` for testing. + fn expr_text>(value: S, line: usize, col: usize) -> Expr { + Expr::Text(TextSpan { value: value.into(), pos: LineCol { line, col } }) + } + + /// Syntactic sugar to instantiate an `Expr::Symbol` for testing. + fn expr_symbol(vref: VarRef, line: usize, col: usize) -> Expr { + Expr::Symbol(SymbolSpan { vref, pos: LineCol { line, col } }) + } + + #[test] + fn test_varref_to_unannotated_string() { + assert_eq!( + "print", + &vref_to_unannotated_string(VarRef::new("print", None), LineCol { line: 0, col: 0 }) + .unwrap() + ); + + assert_eq!( + "7:6: Type annotation not allowed in print$", + format!( + "{}", + &vref_to_unannotated_string( + VarRef::new("print", Some(ExprType::Text)), + LineCol { line: 7, col: 6 } + ) + .unwrap_err() + ) + ); + } + + /// Runs the parser on the given `input` and expects the returned statements to match + /// `exp_statements`. + fn do_ok_test(input: &str, exp_statements: &[Statement]) { + let mut input = input.as_bytes(); + let statements = + parse(&mut input).map(|r| r.expect("Parsing failed")).collect::>(); + assert_eq!(exp_statements, statements.as_slice()); + } + + /// Runs the parser on the given `input` and expects the `err` error message. + fn do_error_test(input: &str, expected_err: &str) { + let mut input = input.as_bytes(); + let mut parser = Parser::from(&mut input); + assert_eq!( + expected_err, + format!("{}", parser.parse_one_safe().expect_err("Parsing did not fail")) + ); + assert!(parser.parse_one_safe().unwrap().is_none()); + } + + /// Runs the parser on the given `input` and expects the `err` error message. + /// + /// Does not expect the parser to be reset to the next (EOF) statement. + // TODO(jmmv): Need better testing to ensure the parser is reset to something that can be + // parsed next. + fn do_error_test_no_reset(input: &str, expected_err: &str) { + let mut input = input.as_bytes(); + for result in parse(&mut input) { + if let Err(e) = result { + assert_eq!(expected_err, format!("{}", e)); + return; + } + } + panic!("Parsing did not fail") + } + + #[test] + fn test_empty() { + do_ok_test("", &[]); + } + + #[test] + fn test_statement_separators() { + do_ok_test( + "a=1\nb=2:c=3:' A comment: that follows\nd=4", + &[ + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + expr: expr_integer(1, 1, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("b", None), + vref_pos: lc(2, 1), + expr: expr_integer(2, 2, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("c", None), + vref_pos: lc(2, 5), + expr: expr_integer(3, 2, 7), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("d", None), + vref_pos: lc(3, 1), + expr: expr_integer(4, 3, 3), + }), + ], + ); + } + + #[test] + fn test_array_assignments() { + do_ok_test( + "a(1)=100\nfoo(2, 3)=\"text\"\nabc$ (5 + z, 6) = TRUE OR FALSE", + &[ + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + subscripts: vec![expr_integer(1, 1, 3)], + expr: expr_integer(100, 1, 6), + }), + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("foo", None), + vref_pos: lc(2, 1), + subscripts: vec![expr_integer(2, 2, 5), expr_integer(3, 2, 8)], + expr: expr_text("text", 2, 11), + }), + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("abc", Some(ExprType::Text)), + vref_pos: lc(3, 1), + subscripts: vec![ + Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 3, 7), + rhs: expr_symbol(VarRef::new("z".to_owned(), None), 3, 11), + pos: lc(3, 9), + })), + expr_integer(6, 3, 14), + ], + expr: Expr::Or(Box::from(BinaryOpSpan { + lhs: expr_boolean(true, 3, 19), + rhs: expr_boolean(false, 3, 27), + pos: lc(3, 24), + })), + }), + ], + ); + } + + #[test] + fn test_array_assignment_errors() { + do_error_test("a(", "1:3: Unexpected <>"); + do_error_test("a()", "1:2: Expected expression"); + do_error_test("a() =", "1:6: Missing expression in array assignment"); + do_error_test("a() IF", "1:2: Expected expression"); + do_error_test("a() = 3 4", "1:9: Unexpected value in expression"); + do_error_test("a() = 3 THEN", "1:9: Unexpected THEN in array assignment"); + do_error_test("a(,) = 3", "1:3: Missing expression"); + do_error_test("a(2;3) = 3", "1:4: Unexpected ;"); + do_error_test("(2) = 3", "1:1: Unexpected ( in statement"); + } + + #[test] + fn test_assignments() { + do_ok_test( + "a=1\nfoo$ = \"bar\"\nb$ = 3 + z", + &[ + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 1), + expr: expr_integer(1, 1, 3), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("foo", Some(ExprType::Text)), + vref_pos: lc(2, 1), + expr: expr_text("bar", 2, 8), + }), + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("b", Some(ExprType::Text)), + vref_pos: lc(3, 1), + expr: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 3, 6), + rhs: expr_symbol(VarRef::new("z", None), 3, 10), + pos: lc(3, 8), + })), + }), + ], + ); + } + + #[test] + fn test_assignment_errors() { + do_error_test("a =", "1:4: Missing expression in assignment"); + do_error_test("a = b 3", "1:7: Unexpected value in expression"); + do_error_test("a = b, 3", "1:6: Unexpected , in assignment"); + do_error_test("a = if 3", "1:5: Unexpected keyword in expression"); + do_error_test("true = 1", "1:1: Unexpected TRUE in statement"); + } + + #[test] + fn test_builtin_calls() { + do_ok_test( + "PRINT a\nPRINT ; 3 , c$\nNOARGS\nNAME 3 AS 4", + &[ + Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ArgSpan { + expr: Some(expr_symbol(VarRef::new("a", None), 1, 7)), + sep: ArgSep::End, + sep_pos: lc(1, 8), + }], + }), + Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(2, 1), + args: vec![ + ArgSpan { expr: None, sep: ArgSep::Short, sep_pos: lc(2, 7) }, + ArgSpan { + expr: Some(expr_integer(3, 2, 9)), + sep: ArgSep::Long, + sep_pos: lc(2, 11), + }, + ArgSpan { + expr: Some(expr_symbol(VarRef::new("c", Some(ExprType::Text)), 2, 13)), + sep: ArgSep::End, + sep_pos: lc(2, 15), + }, + ], + }), + Statement::Call(CallSpan { + vref: VarRef::new("NOARGS", None), + vref_pos: lc(3, 1), + args: vec![], + }), + Statement::Call(CallSpan { + vref: VarRef::new("NAME", None), + vref_pos: lc(4, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(3, 4, 6)), + sep: ArgSep::As, + sep_pos: lc(4, 8), + }, + ArgSpan { + expr: Some(expr_integer(4, 4, 11)), + sep: ArgSep::End, + sep_pos: lc(4, 12), + }, + ], + }), + ], + ); + } + + #[test] + fn test_builtin_calls_and_array_references_disambiguation() { + use Expr::*; + + do_ok_test( + "PRINT(1)", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::End, + sep_pos: lc(1, 9), + }], + })], + ); + + do_ok_test( + "PRINT(1), 2", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Long, + sep_pos: lc(1, 9), + }, + ArgSpan { + expr: Some(expr_integer(2, 1, 11)), + sep: ArgSep::End, + sep_pos: lc(1, 12), + }, + ], + })], + ); + + do_ok_test( + "PRINT(1); 2", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Short, + sep_pos: lc(1, 9), + }, + ArgSpan { + expr: Some(expr_integer(2, 1, 11)), + sep: ArgSep::End, + sep_pos: lc(1, 12), + }, + ], + })], + ); + + do_ok_test( + "PRINT(1);", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 7)), + sep: ArgSep::Short, + sep_pos: lc(1, 9), + }, + ArgSpan { expr: None, sep: ArgSep::End, sep_pos: lc(1, 10) }, + ], + })], + ); + + do_ok_test( + "PRINT(1) + 2; 3", + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(Add(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 10), + }))), + sep: ArgSep::Short, + sep_pos: lc(1, 13), + }, + ArgSpan { + expr: Some(expr_integer(3, 1, 15)), + sep: ArgSep::End, + sep_pos: lc(1, 16), + }, + ], + })], + ); + } + + #[test] + fn test_builtin_calls_errors() { + do_error_test("FOO 3 5\n", "1:7: Unexpected value in expression"); + do_error_test("INPUT$ a\n", "1:1: Type annotation not allowed in INPUT$"); + do_error_test("PRINT IF 1\n", "1:7: Unexpected keyword in expression"); + do_error_test("PRINT 3, IF 1\n", "1:10: Unexpected keyword in expression"); + do_error_test("PRINT 3 THEN\n", "1:9: Expected comma, semicolon, or end of statement"); + do_error_test("PRINT ()\n", "1:7: Expected expression"); + do_error_test("PRINT (2, 3)\n", "1:7: Expected expression"); + do_error_test("PRINT (2, 3); 4\n", "1:7: Expected expression"); + } + + #[test] + fn test_data() { + do_ok_test("DATA", &[Statement::Data(DataSpan { values: vec![None] })]); + + do_ok_test("DATA , ", &[Statement::Data(DataSpan { values: vec![None, None] })]); + do_ok_test( + "DATA , , ,", + &[Statement::Data(DataSpan { values: vec![None, None, None, None] })], + ); + + do_ok_test( + "DATA 1: DATA 2", + &[ + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 1, pos: lc(1, 6) }))], + }), + Statement::Data(DataSpan { + values: vec![Some(Expr::Integer(IntegerSpan { value: 2, pos: lc(1, 14) }))], + }), + ], + ); + + do_ok_test( + "DATA TRUE, -3, 5.1, \"foo\"", + &[Statement::Data(DataSpan { + values: vec![ + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 6) })), + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 12) })), + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 16) })), + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 21) })), + ], + })], + ); + + do_ok_test( + "DATA , TRUE, , 3, , 5.1, , \"foo\",", + &[Statement::Data(DataSpan { + values: vec![ + None, + Some(Expr::Boolean(BooleanSpan { value: true, pos: lc(1, 8) })), + None, + Some(Expr::Integer(IntegerSpan { value: 3, pos: lc(1, 16) })), + None, + Some(Expr::Double(DoubleSpan { value: 5.1, pos: lc(1, 21) })), + None, + Some(Expr::Text(TextSpan { value: "foo".to_owned(), pos: lc(1, 28) })), + None, + ], + })], + ); + + do_ok_test( + "DATA -3, -5.1", + &[Statement::Data(DataSpan { + values: vec![ + Some(Expr::Integer(IntegerSpan { value: -3, pos: lc(1, 6) })), + Some(Expr::Double(DoubleSpan { value: -5.1, pos: lc(1, 10) })), + ], + })], + ); + } + + #[test] + fn test_data_errors() { + do_error_test("DATA + 2", "1:6: Unexpected + in DATA statement"); + do_error_test("DATA ;", "1:6: Unexpected ; in DATA statement"); + do_error_test("DATA 5 + 1", "1:8: Expected comma after datum but found +"); + do_error_test("DATA 5 ; 1", "1:8: Expected comma after datum but found ;"); + do_error_test("DATA -FALSE", "1:6: Expected number after -"); + do_error_test("DATA -\"abc\"", "1:6: Expected number after -"); + do_error_test("DATA -foo", "1:6: Expected number after -"); + } + + #[test] + fn test_dim_default_type() { + do_ok_test( + "DIM i", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 6), + })], + ); + } + + #[test] + fn test_dim_as_simple_types() { + do_ok_test( + "DIM i AS BOOLEAN", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Boolean, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS DOUBLE", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Double, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS INTEGER", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 10), + })], + ); + do_ok_test( + "DIM i AS STRING", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Text, + vtype_pos: lc(1, 10), + })], + ); + } + + #[test] + fn test_dim_consecutive() { + do_ok_test( + "DIM i\nDIM j AS BOOLEAN\nDIM k", + &[ + Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(1, 6), + }), + Statement::Dim(DimSpan { + name: "j".to_owned(), + name_pos: lc(2, 5), + shared: false, + vtype: ExprType::Boolean, + vtype_pos: lc(2, 10), + }), + Statement::Dim(DimSpan { + name: "k".to_owned(), + name_pos: lc(3, 5), + shared: false, + vtype: ExprType::Integer, + vtype_pos: lc(3, 6), + }), + ], + ); + } + + #[test] + fn test_dim_shared() { + do_ok_test( + "DIM SHARED i", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + vtype: ExprType::Integer, + vtype_pos: lc(1, 13), + })], + ); + do_ok_test( + "DIM SHARED i AS BOOLEAN", + &[Statement::Dim(DimSpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + vtype: ExprType::Boolean, + vtype_pos: lc(1, 17), + })], + ); + } + + #[test] + fn test_dim_array() { + use Expr::*; + + do_ok_test( + "DIM i(10)", + &[Statement::DimArray(DimArraySpan { + name: "i".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![expr_integer(10, 1, 7)], + subtype: ExprType::Integer, + subtype_pos: lc(1, 10), + })], + ); + + do_ok_test( + "DIM foo(-5, 0) AS STRING", + &[Statement::DimArray(DimArraySpan { + name: "foo".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![ + Negate(Box::from(UnaryOpSpan { expr: expr_integer(5, 1, 10), pos: lc(1, 9) })), + expr_integer(0, 1, 13), + ], + subtype: ExprType::Text, + subtype_pos: lc(1, 19), + })], + ); + + do_ok_test( + "DIM foo(bar$() + 3, 8, -1)", + &[Statement::DimArray(DimArraySpan { + name: "foo".to_owned(), + name_pos: lc(1, 5), + shared: false, + dimensions: vec![ + Add(Box::from(BinaryOpSpan { + lhs: Call(CallSpan { + vref: VarRef::new("bar", Some(ExprType::Text)), + vref_pos: lc(1, 9), + args: vec![], + }), + rhs: expr_integer(3, 1, 18), + pos: lc(1, 16), + })), + expr_integer(8, 1, 21), + Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 25), pos: lc(1, 24) })), + ], + subtype: ExprType::Integer, + subtype_pos: lc(1, 27), + })], + ); + + do_ok_test( + "DIM SHARED i(10)", + &[Statement::DimArray(DimArraySpan { + name: "i".to_owned(), + name_pos: lc(1, 12), + shared: true, + dimensions: vec![expr_integer(10, 1, 14)], + subtype: ExprType::Integer, + subtype_pos: lc(1, 17), + })], + ); + } + + #[test] + fn test_dim_errors() { + do_error_test("DIM", "1:4: Expected variable name after DIM"); + do_error_test("DIM 3", "1:5: Expected variable name after DIM"); + do_error_test("DIM AS", "1:5: Expected variable name after DIM"); + do_error_test("DIM foo 3", "1:9: Expected AS or end of statement"); + do_error_test("DIM a AS", "1:9: Invalid type name <> in AS type definition"); + do_error_test("DIM a$ AS", "1:5: Type annotation not allowed in a$"); + do_error_test("DIM a AS 3", "1:10: Invalid type name 3 in AS type definition"); + do_error_test("DIM a AS INTEGER 3", "1:18: Unexpected 3 in DIM statement"); + + do_error_test("DIM a()", "1:6: Arrays require at least one dimension"); + do_error_test("DIM a(,)", "1:7: Missing expression"); + do_error_test("DIM a(, 3)", "1:7: Missing expression"); + do_error_test("DIM a(3, )", "1:10: Missing expression"); + do_error_test("DIM a(3, , 4)", "1:10: Missing expression"); + do_error_test("DIM a(1) AS INTEGER 3", "1:21: Unexpected 3 in DIM statement"); + } + + #[test] + fn test_do_until_empty() { + do_ok_test( + "DO UNTIL TRUE\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(true, 1, 10)), + body: vec![], + })], + ); + + do_ok_test( + "DO UNTIL FALSE\nREM foo\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(false, 1, 10)), + body: vec![], + })], + ); + } + + #[test] + fn test_do_infinite_empty() { + do_ok_test("DO\nLOOP", &[Statement::Do(DoSpan { guard: DoGuard::Infinite, body: vec![] })]); + } + + #[test] + fn test_do_pre_until_loops() { + do_ok_test( + "DO UNTIL TRUE\nA\nB\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreUntil(expr_boolean(true, 1, 10)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_pre_while_loops() { + do_ok_test( + "DO WHILE TRUE\nA\nB\nLOOP", + &[Statement::Do(DoSpan { + guard: DoGuard::PreWhile(expr_boolean(true, 1, 10)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_post_until_loops() { + do_ok_test( + "DO\nA\nB\nLOOP UNTIL TRUE", + &[Statement::Do(DoSpan { + guard: DoGuard::PostUntil(expr_boolean(true, 4, 12)), + + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_post_while_loops() { + do_ok_test( + "DO\nA\nB\nLOOP WHILE FALSE", + &[Statement::Do(DoSpan { + guard: DoGuard::PostWhile(expr_boolean(false, 4, 12)), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_do_nested() { + let code = r#" + DO WHILE TRUE + A + DO + B + LOOP UNTIL FALSE + C + LOOP + "#; + do_ok_test( + code, + &[Statement::Do(DoSpan { + guard: DoGuard::PreWhile(expr_boolean(true, 2, 22)), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::Do(DoSpan { + guard: DoGuard::PostUntil(expr_boolean(false, 6, 28)), + body: vec![make_bare_builtin_call("B", 5, 21)], + }), + make_bare_builtin_call("C", 7, 17), + ], + })], + ); + } + + #[test] + fn test_do_errors() { + do_error_test("DO\n", "1:1: DO without LOOP"); + do_error_test("DO FOR\n", "1:4: Expecting newline, UNTIL or WHILE after DO"); + + do_error_test("\n\nDO UNTIL TRUE\n", "3:1: DO without LOOP"); + do_error_test("\n\nDO WHILE TRUE\n", "3:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND", "1:1: DO without LOOP"); + do_error_test("DO WHILE TRUE\nEND", "1:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND\n", "1:1: DO without LOOP"); + do_error_test("DO WHILE TRUE\nEND\n", "1:1: DO without LOOP"); + do_error_test("DO UNTIL TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + do_error_test("DO WHILE TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + + do_error_test("DO UNTIL\n", "1:9: No expression in UNTIL clause"); + do_error_test("DO WHILE\n", "1:9: No expression in WHILE clause"); + do_error_test("DO UNTIL TRUE", "1:14: Expecting newline after DO"); + do_error_test("DO WHILE TRUE", "1:14: Expecting newline after DO"); + + do_error_test("DO\nLOOP UNTIL", "2:11: No expression in UNTIL clause"); + do_error_test("DO\nLOOP WHILE\n", "2:11: No expression in WHILE clause"); + + do_error_test("DO UNTIL ,\nLOOP", "1:10: No expression in UNTIL clause"); + do_error_test("DO WHILE ,\nLOOP", "1:10: No expression in WHILE clause"); + + do_error_test("DO\nLOOP UNTIL ,\n", "2:12: No expression in UNTIL clause"); + do_error_test("DO\nLOOP WHILE ,\n", "2:12: No expression in WHILE clause"); + + do_error_test( + "DO WHILE TRUE\nLOOP UNTIL FALSE", + "1:1: DO loop cannot have pre and post guards at the same time", + ); + } + + #[test] + fn test_exit_do() { + do_ok_test(" EXIT DO", &[Statement::ExitDo(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_for() { + do_ok_test(" EXIT FOR", &[Statement::ExitFor(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_function() { + do_ok_test(" EXIT FUNCTION", &[Statement::ExitFunction(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_sub() { + do_ok_test(" EXIT SUB", &[Statement::ExitSub(ExitSpan { pos: lc(1, 3) })]); + } + + #[test] + fn test_exit_errors() { + do_error_test("EXIT", "1:5: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + do_error_test("EXIT 5", "1:6: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + } + + /// Wrapper around `do_ok_test` to parse an expression. Given that expressions alone are not + /// valid statements, we have to put them in a statement to parse them. In doing so, we can + /// also put an extra statement after them to ensure we detect their end properly. + fn do_expr_ok_test(input: &str, expr: Expr) { + do_ok_test( + &format!("PRINT {}, 1", input), + &[Statement::Call(CallSpan { + vref: VarRef::new("PRINT", None), + vref_pos: lc(1, 1), + args: vec![ + ArgSpan { + expr: Some(expr), + sep: ArgSep::Long, + sep_pos: lc(1, 7 + input.len()), + }, + ArgSpan { + expr: Some(expr_integer(1, 1, 6 + input.len() + 3)), + sep: ArgSep::End, + sep_pos: lc(1, 10 + input.len()), + }, + ], + })], + ); + } + + /// Wrapper around `do_error_test` to parse an expression. Given that expressions alone are not + /// valid statements, we have to put them in a statement to parse them. In doing so, we can + /// also put an extra statement after them to ensure we detect their end properly. + fn do_expr_error_test(input: &str, msg: &str) { + do_error_test(&format!("PRINT {}, 1", input), msg) + } + + #[test] + fn test_expr_literals() { + do_expr_ok_test("TRUE", expr_boolean(true, 1, 7)); + do_expr_ok_test("FALSE", expr_boolean(false, 1, 7)); + do_expr_ok_test("5", expr_integer(5, 1, 7)); + do_expr_ok_test("\"some text\"", expr_text("some text", 1, 7)); + } + + #[test] + fn test_expr_symbols() { + do_expr_ok_test("foo", expr_symbol(VarRef::new("foo", None), 1, 7)); + do_expr_ok_test("bar$", expr_symbol(VarRef::new("bar", Some(ExprType::Text)), 1, 7)); + } + + #[test] + fn test_expr_parens() { + use Expr::*; + do_expr_ok_test("(1)", expr_integer(1, 1, 8)); + do_expr_ok_test("((1))", expr_integer(1, 1, 9)); + do_expr_ok_test(" ( ( 1 ) ) ", expr_integer(1, 1, 12)); + do_expr_ok_test( + "3 * (2 + 5)", + Multiply(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 7), + rhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(5, 1, 16), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "(7) - (1) + (-2)", + Add(Box::from(BinaryOpSpan { + lhs: Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(7, 1, 8), + rhs: expr_integer(1, 1, 14), + pos: lc(1, 11), + })), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(2, 1, 21), + pos: lc(1, 20), + })), + pos: lc(1, 17), + })), + ); + } + + #[test] + fn test_expr_arith_ops() { + use Expr::*; + let span = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 11), + pos: lc(1, 9), + }); + do_expr_ok_test("1 + 2", Add(span.clone())); + do_expr_ok_test("1 - 2", Subtract(span.clone())); + do_expr_ok_test("1 * 2", Multiply(span.clone())); + do_expr_ok_test("1 / 2", Divide(span.clone())); + do_expr_ok_test("1 ^ 2", Power(span)); + let span = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + }); + do_expr_ok_test("1 MOD 2", Modulo(span)); + } + + #[test] + fn test_expr_rel_ops() { + use Expr::*; + let span1 = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 11), + pos: lc(1, 9), + }); + let span2 = Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + }); + do_expr_ok_test("1 = 2", Equal(span1.clone())); + do_expr_ok_test("1 <> 2", NotEqual(span2.clone())); + do_expr_ok_test("1 < 2", Less(span1.clone())); + do_expr_ok_test("1 <= 2", LessEqual(span2.clone())); + do_expr_ok_test("1 > 2", Greater(span1)); + do_expr_ok_test("1 >= 2", GreaterEqual(span2)); + } + + #[test] + fn test_expr_logical_ops() { + use Expr::*; + do_expr_ok_test( + "1 AND 2", + And(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 OR 2", + Or(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 XOR 2", + Xor(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 13), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_logical_ops_not() { + use Expr::*; + do_expr_ok_test( + "NOT TRUE", + Not(Box::from(UnaryOpSpan { expr: expr_boolean(true, 1, 11), pos: lc(1, 7) })), + ); + do_expr_ok_test( + "NOT 6", + Not(Box::from(UnaryOpSpan { expr: expr_integer(6, 1, 11), pos: lc(1, 7) })), + ); + do_expr_ok_test( + "NOT NOT TRUE", + Not(Box::from(UnaryOpSpan { + expr: Not(Box::from(UnaryOpSpan { + expr: expr_boolean(true, 1, 15), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "1 - NOT 4", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Not(Box::from(UnaryOpSpan { expr: expr_integer(4, 1, 15), pos: lc(1, 11) })), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_bitwise_ops() { + use Expr::*; + do_expr_ok_test( + "1 << 2", + ShiftLeft(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "1 >> 2", + ShiftRight(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_integer(2, 1, 12), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_op_priorities() { + use Expr::*; + do_expr_ok_test( + "3 * (2 + 5) = (3 + 1 = 2 OR 1 = 3 XOR FALSE * \"a\")", + Equal(Box::from(BinaryOpSpan { + lhs: Multiply(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 7), + rhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(5, 1, 16), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + rhs: Xor(Box::from(BinaryOpSpan { + lhs: Or(Box::from(BinaryOpSpan { + lhs: Equal(Box::from(BinaryOpSpan { + lhs: Add(Box::from(BinaryOpSpan { + lhs: expr_integer(3, 1, 22), + rhs: expr_integer(1, 1, 26), + pos: lc(1, 24), + })), + rhs: expr_integer(2, 1, 30), + pos: lc(1, 28), + })), + rhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 35), + rhs: expr_integer(3, 1, 39), + pos: lc(1, 37), + })), + pos: lc(1, 32), + })), + rhs: Multiply(Box::from(BinaryOpSpan { + lhs: expr_boolean(false, 1, 45), + rhs: expr_text("a", 1, 53), + pos: lc(1, 51), + })), + pos: lc(1, 41), + })), + pos: lc(1, 19), + })), + ); + do_expr_ok_test( + "-1 ^ 3", + Negate(Box::from(UnaryOpSpan { + expr: Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 8), + rhs: expr_integer(3, 1, 12), + pos: lc(1, 10), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "-(1 ^ 3)", + Negate(Box::from(UnaryOpSpan { + expr: Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 9), + rhs: expr_integer(3, 1, 13), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + do_expr_ok_test( + "(-1) ^ 3", + Power(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 9), pos: lc(1, 8) })), + rhs: expr_integer(3, 1, 14), + pos: lc(1, 12), + })), + ); + do_expr_ok_test( + "1 ^ (-3)", + Power(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 13), + pos: lc(1, 12), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "0 <> 2 >> 1", + NotEqual(Box::from(BinaryOpSpan { + lhs: expr_integer(0, 1, 7), + rhs: ShiftRight(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 12), + rhs: expr_integer(1, 1, 17), + pos: lc(1, 14), + })), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_numeric_signs() { + use Expr::*; + + do_expr_ok_test( + "-a", + Negate(Box::from(UnaryOpSpan { + expr: expr_symbol(VarRef::new("a", None), 1, 8), + pos: lc(1, 7), + })), + ); + + do_expr_ok_test( + "1 - -3", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "-1 - 3", + Subtract(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(1, 1, 8), pos: lc(1, 7) })), + rhs: expr_integer(3, 1, 12), + pos: lc(1, 10), + })), + ); + do_expr_ok_test( + "5 + -1", + Add(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(1, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 9), + })), + ); + do_expr_ok_test( + "-5 + 1", + Add(Box::from(BinaryOpSpan { + lhs: Negate(Box::from(UnaryOpSpan { expr: expr_integer(5, 1, 8), pos: lc(1, 7) })), + rhs: expr_integer(1, 1, 12), + pos: lc(1, 10), + })), + ); + do_expr_ok_test( + "NOT -3", + Not(Box::from(UnaryOpSpan { + expr: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + + do_expr_ok_test( + "1.0 - -3.5", + Subtract(Box::from(BinaryOpSpan { + lhs: expr_double(1.0, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_double(3.5, 1, 14), + pos: lc(1, 13), + })), + pos: lc(1, 11), + })), + ); + do_expr_ok_test( + "5.12 + -0.50", + Add(Box::from(BinaryOpSpan { + lhs: expr_double(5.12, 1, 7), + rhs: Negate(Box::from(UnaryOpSpan { + expr: expr_double(0.50, 1, 15), + pos: lc(1, 14), + })), + pos: lc(1, 12), + })), + ); + do_expr_ok_test( + "NOT -3", + Not(Box::from(UnaryOpSpan { + expr: Negate(Box::from(UnaryOpSpan { + expr: expr_integer(3, 1, 12), + pos: lc(1, 11), + })), + pos: lc(1, 7), + })), + ); + } + + #[test] + fn test_expr_functions_variadic() { + use Expr::*; + do_expr_ok_test( + "zero()", + Call(CallSpan { vref: VarRef::new("zero", None), vref_pos: lc(1, 7), args: vec![] }), + ); + do_expr_ok_test( + "one%(1)", + Call(CallSpan { + vref: VarRef::new("one", Some(ExprType::Integer)), + vref_pos: lc(1, 7), + args: vec![ArgSpan { + expr: Some(expr_integer(1, 1, 12)), + sep: ArgSep::End, + sep_pos: lc(1, 13), + }], + }), + ); + do_expr_ok_test( + "many$(3, \"x\", TRUE)", + Call(CallSpan { + vref: VarRef::new("many", Some(ExprType::Text)), + vref_pos: lc(1, 7), + args: vec![ + ArgSpan { + expr: Some(expr_integer(3, 1, 13)), + sep: ArgSep::Long, + sep_pos: lc(1, 14), + }, + ArgSpan { + expr: Some(expr_text("x", 1, 16)), + sep: ArgSep::Long, + sep_pos: lc(1, 19), + }, + ArgSpan { + expr: Some(expr_boolean(true, 1, 21)), + sep: ArgSep::End, + sep_pos: lc(1, 25), + }, + ], + }), + ); + } + + #[test] + fn test_expr_functions_nested() { + use Expr::*; + do_expr_ok_test( + "consecutive(parenthesis())", + Call(CallSpan { + vref: VarRef::new("consecutive", None), + vref_pos: lc(1, 7), + args: vec![ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("parenthesis", None), + vref_pos: lc(1, 19), + args: vec![], + })), + sep: ArgSep::End, + sep_pos: lc(1, 32), + }], + }), + ); + do_expr_ok_test( + "outer?(1, inner1(2, 3), 4, inner2(), 5)", + Call(CallSpan { + vref: VarRef::new("outer", Some(ExprType::Boolean)), + vref_pos: lc(1, 7), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 14)), + sep: ArgSep::Long, + sep_pos: lc(1, 15), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("inner1", None), + vref_pos: lc(1, 17), + args: vec![ + ArgSpan { + expr: Some(expr_integer(2, 1, 24)), + sep: ArgSep::Long, + sep_pos: lc(1, 25), + }, + ArgSpan { + expr: Some(expr_integer(3, 1, 27)), + sep: ArgSep::End, + sep_pos: lc(1, 28), + }, + ], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 29), + }, + ArgSpan { + expr: Some(expr_integer(4, 1, 31)), + sep: ArgSep::Long, + sep_pos: lc(1, 32), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("inner2", None), + vref_pos: lc(1, 34), + args: vec![], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 42), + }, + ArgSpan { + expr: Some(expr_integer(5, 1, 44)), + sep: ArgSep::End, + sep_pos: lc(1, 45), + }, + ], + }), + ); + } + + #[test] + fn test_expr_functions_and_ops() { + use Expr::*; + do_expr_ok_test( + "b AND ask?(34 + 15, ask(1, FALSE), -5)", + And(Box::from(BinaryOpSpan { + lhs: expr_symbol(VarRef::new("b".to_owned(), None), 1, 7), + rhs: Call(CallSpan { + vref: VarRef::new("ask", Some(ExprType::Boolean)), + vref_pos: lc(1, 13), + args: vec![ + ArgSpan { + expr: Some(Add(Box::from(BinaryOpSpan { + lhs: expr_integer(34, 1, 18), + rhs: expr_integer(15, 1, 23), + pos: lc(1, 21), + }))), + sep: ArgSep::Long, + sep_pos: lc(1, 25), + }, + ArgSpan { + expr: Some(Call(CallSpan { + vref: VarRef::new("ask", None), + vref_pos: lc(1, 27), + args: vec![ + ArgSpan { + expr: Some(expr_integer(1, 1, 31)), + sep: ArgSep::Long, + sep_pos: lc(1, 32), + }, + ArgSpan { + expr: Some(expr_boolean(false, 1, 34)), + sep: ArgSep::End, + sep_pos: lc(1, 39), + }, + ], + })), + sep: ArgSep::Long, + sep_pos: lc(1, 40), + }, + ArgSpan { + expr: Some(Negate(Box::from(UnaryOpSpan { + expr: expr_integer(5, 1, 43), + pos: lc(1, 42), + }))), + sep: ArgSep::End, + sep_pos: lc(1, 44), + }, + ], + }), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_functions_not_confused_with_symbols() { + use Expr::*; + let iref = VarRef::new("i", None); + let jref = VarRef::new("j", None); + do_expr_ok_test( + "i = 0 OR i = (j - 1)", + Or(Box::from(BinaryOpSpan { + lhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_symbol(iref.clone(), 1, 7), + rhs: expr_integer(0, 1, 11), + pos: lc(1, 9), + })), + rhs: Equal(Box::from(BinaryOpSpan { + lhs: expr_symbol(iref, 1, 16), + rhs: Subtract(Box::from(BinaryOpSpan { + lhs: expr_symbol(jref, 1, 21), + rhs: expr_integer(1, 1, 25), + pos: lc(1, 23), + })), + pos: lc(1, 18), + })), + pos: lc(1, 13), + })), + ); + } + + #[test] + fn test_expr_errors() { + // Note that all column numbers in the errors below are offset by 6 (due to "PRINT ") as + // that's what the do_expr_error_test function is prefixing to the given strings. + + do_expr_error_test("+3", "1:7: Not enough values to apply operator"); + do_expr_error_test("2 + * 3", "1:9: Not enough values to apply operator"); + do_expr_error_test("(2(3))", "1:9: Unexpected ( in expression"); + do_expr_error_test("((3)2)", "1:11: Unexpected value in expression"); + do_expr_error_test("2 3", "1:9: Unexpected value in expression"); + + do_expr_error_test("(", "1:8: Missing expression"); + + do_expr_error_test(")", "1:7: Expected comma, semicolon, or end of statement"); + do_expr_error_test("(()", "1:10: Missing expression"); + do_expr_error_test("())", "1:7: Expected expression"); + do_expr_error_test("3 + (2 + 1) + (4 - 5", "1:21: Unbalanced parenthesis"); + do_expr_error_test( + "3 + 2 + 1) + (4 - 5)", + "1:16: Expected comma, semicolon, or end of statement", + ); + + do_expr_error_test("foo(,)", "1:11: Missing expression"); + do_expr_error_test("foo(, 3)", "1:11: Missing expression"); + do_expr_error_test("foo(3, )", "1:14: Missing expression"); + do_expr_error_test("foo(3, , 4)", "1:14: Missing expression"); + // TODO(jmmv): These are not the best error messages... + do_expr_error_test("(,)", "1:8: Missing expression"); + do_expr_error_test("(3, 4)", "1:7: Expected expression"); + do_expr_error_test("((), ())", "1:10: Missing expression"); + + // TODO(jmmv): This succeeds because `PRINT` is interned as a `Token::Symbol` so the + // expression parser sees it as a variable reference... but this should probably fail. + // Would need to intern builtin call names as a separate token to catch this, but that + // also means that the lexer must be aware of builtin call names upfront. + use Expr::*; + do_expr_ok_test( + "1 + PRINT", + Add(Box::from(BinaryOpSpan { + lhs: expr_integer(1, 1, 7), + rhs: expr_symbol(VarRef::new("PRINT", None), 1, 11), + pos: lc(1, 9), + })), + ); + } + + #[test] + fn test_expr_errors_due_to_keywords() { + for kw in &[ + "BOOLEAN", "CASE", "DATA", "DIM", "DOUBLE", "ELSEIF", "END", "ERROR", "EXIT", "FOR", + "GOSUB", "GOTO", "IF", "IS", "INTEGER", "LOOP", "NEXT", "ON", "RESUME", "RETURN", + "SELECT", "STRING", "UNTIL", "WEND", "WHILE", + ] { + do_expr_error_test( + &format!("2 + {} - 1", kw), + "1:11: Unexpected keyword in expression", + ); + } + } + + #[test] + fn test_if_empty_branches() { + do_ok_test( + "IF 1 THEN\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }], + })], + ); + do_ok_test( + "IF 1 THEN\nREM Some comment to skip over\n\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nELSE\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 3, 1), body: vec![] }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSE\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 2, 1), body: vec![] }, + ], + })], + ); + } + + /// Helper to instantiate a trivial `Statement::BuiltinCall` that has no arguments. + fn make_bare_builtin_call(name: &str, line: usize, col: usize) -> Statement { + Statement::Call(CallSpan { + vref: VarRef::new(name, None), + vref_pos: LineCol { line, col }, + args: vec![], + }) + } + + #[test] + fn test_if_with_one_statement_or_empty_lines() { + do_ok_test( + "IF 1 THEN\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("PRINT", 2, 1)], + }], + })], + ); + do_ok_test( + "IF 1 THEN\nREM foo\nELSEIF 2 THEN\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_integer(2, 3, 8), + body: vec![make_bare_builtin_call("PRINT", 4, 1)], + }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\nELSEIF 2 THEN\nELSE\n\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_integer(2, 2, 8), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 3, 1), + body: vec![make_bare_builtin_call("PRINT", 5, 1)], + }, + ], + })], + ); + do_ok_test( + "IF 1 THEN\n\n\nELSE\nPRINT\nEND IF", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 4, 1), + body: vec![make_bare_builtin_call("PRINT", 5, 1)], + }, + ], + })], + ); + } + + #[test] + fn test_if_complex() { + let code = r#" + IF 1 THEN 'First branch + A + B + ELSEIF 2 THEN 'Second branch + C + D + ELSEIF 3 THEN 'Third branch + E + F + ELSE 'Last branch + G + H + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![ + make_bare_builtin_call("A", 3, 17), + make_bare_builtin_call("B", 4, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(2, 5, 20), + body: vec![ + make_bare_builtin_call("C", 6, 17), + make_bare_builtin_call("D", 7, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(3, 8, 20), + body: vec![ + make_bare_builtin_call("E", 9, 17), + make_bare_builtin_call("F", 10, 17), + ], + }, + IfBranchSpan { + guard: expr_boolean(true, 11, 13), + body: vec![ + make_bare_builtin_call("G", 12, 17), + make_bare_builtin_call("H", 13, 17), + ], + }, + ], + })], + ); + } + + #[test] + fn test_if_with_interleaved_end_complex() { + let code = r#" + IF 1 THEN 'First branch + A + END + B + ELSEIF 2 THEN 'Second branch + C + END 8 + D + ELSEIF 3 THEN 'Third branch + E + END + F + ELSE 'Last branch + G + END 5 + H + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::End(EndSpan { code: None, pos: lc(4, 17) }), + make_bare_builtin_call("B", 5, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(2, 6, 20), + body: vec![ + make_bare_builtin_call("C", 7, 17), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(8, 21) })), + pos: lc(8, 17), + }), + make_bare_builtin_call("D", 9, 17), + ], + }, + IfBranchSpan { + guard: expr_integer(3, 10, 20), + body: vec![ + make_bare_builtin_call("E", 11, 17), + Statement::End(EndSpan { code: None, pos: lc(12, 17) }), + make_bare_builtin_call("F", 13, 17), + ], + }, + IfBranchSpan { + guard: expr_boolean(true, 14, 13), + body: vec![ + make_bare_builtin_call("G", 15, 17), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { + value: 5, + pos: lc(16, 21), + })), + pos: lc(16, 17), + }), + make_bare_builtin_call("H", 17, 17), + ], + }, + ], + })], + ); + } + + #[test] + fn test_if_nested() { + let code = r#" + IF 1 THEN + A + ELSEIF 2 THEN + IF 3 THEN + B + END IF + END IF + "#; + do_ok_test( + code, + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 2, 16), + body: vec![make_bare_builtin_call("A", 3, 17)], + }, + IfBranchSpan { + guard: expr_integer(2, 4, 20), + body: vec![Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(3, 5, 20), + body: vec![make_bare_builtin_call("B", 6, 21)], + }], + })], + }, + ], + })], + ); + } + + #[test] + fn test_if_errors() { + do_error_test("IF\n", "1:3: No expression in IF statement"); + do_error_test("IF 3 + 1", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1\n", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1 PRINT foo\n", "1:10: Unexpected value in expression"); + do_error_test("IF 3 + 1\nPRINT foo\n", "1:9: No THEN in IF statement"); + do_error_test("IF 3 + 1 THEN", "1:1: IF without END IF"); + + do_error_test("IF 1 THEN\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nELSEIF 1 THEN\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nELSE\n", "1:1: IF without END IF"); + do_error_test("REM\n IF 1 THEN\n", "2:4: IF without END IF"); + + do_error_test("IF 1 THEN\nELSEIF\n", "2:7: No expression in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1", "2:13: No THEN in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1\n", "2:13: No THEN in ELSEIF statement"); + do_error_test( + "IF 1 THEN\nELSEIF 3 + 1 PRINT foo\n", + "2:14: Unexpected value in expression", + ); + do_error_test("IF 1 THEN\nELSEIF 3 + 1\nPRINT foo\n", "2:13: No THEN in ELSEIF statement"); + do_error_test("IF 1 THEN\nELSEIF 3 + 1 THEN", "2:18: Expecting newline after THEN"); + + do_error_test("IF 1 THEN\nELSE", "2:5: Expecting newline after ELSE"); + do_error_test("IF 1 THEN\nELSE foo", "2:6: Expecting newline after ELSE"); + + do_error_test("IF 1 THEN\nEND", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nEND\n", "1:1: IF without END IF"); + do_error_test("IF 1 THEN\nEND IF foo", "2:8: Expected newline but found foo"); + do_error_test("IF 1 THEN\nEND SELECT\n", "2:1: END SELECT without SELECT"); + do_error_test("IF 1 THEN\nEND SELECT\nEND IF\n", "2:1: END SELECT without SELECT"); + + do_error_test( + "IF 1 THEN\nELSE\nELSEIF 2 THEN\nEND IF", + "3:1: Unexpected ELSEIF after ELSE", + ); + do_error_test("IF 1 THEN\nELSE\nELSE\nEND IF", "3:1: Duplicate ELSE after ELSE"); + + do_error_test_no_reset("ELSEIF 1 THEN\nEND IF", "1:1: Unexpected ELSEIF in statement"); + do_error_test_no_reset("ELSE 1\nEND IF", "1:1: Unexpected ELSE in statement"); + + do_error_test("IF 1 THEN\nEND 3 IF", "2:7: Unexpected keyword in expression"); + do_error_test("END 3 IF", "1:7: Unexpected keyword in expression"); + do_error_test("END IF", "1:1: END IF without IF"); + + do_error_test("IF TRUE THEN PRINT ELSE ELSE", "1:25: Unexpected ELSE in uniline IF branch"); + } + + #[test] + fn test_if_uniline_then() { + do_ok_test( + "IF 1 THEN A", + &[Statement::If(IfSpan { + branches: vec![IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }], + })], + ); + } + + #[test] + fn test_if_uniline_then_else() { + do_ok_test( + "IF 1 THEN A ELSE B", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }, + IfBranchSpan { + guard: expr_boolean(true, 1, 13), + body: vec![make_bare_builtin_call("B", 1, 18)], + }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_empty_then_else() { + do_ok_test( + "IF 1 THEN ELSE B", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { + guard: expr_boolean(true, 1, 11), + body: vec![make_bare_builtin_call("B", 1, 16)], + }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_then_empty_else() { + do_ok_test( + "IF 1 THEN A ELSE", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { + guard: expr_integer(1, 1, 4), + body: vec![make_bare_builtin_call("A", 1, 11)], + }, + IfBranchSpan { guard: expr_boolean(true, 1, 13), body: vec![] }, + ], + })], + ); + } + + #[test] + fn test_if_uniline_empty_then_empty_else() { + do_ok_test( + "IF 1 THEN ELSE", + &[Statement::If(IfSpan { + branches: vec![ + IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![] }, + IfBranchSpan { guard: expr_boolean(true, 1, 11), body: vec![] }, + ], + })], + ); + } + + /// Performs a test of a uniline if statement followed by a specific allowed statement type. + /// + /// `text` is the literal statement to append to the if statement, and `stmt` contains the + /// expected parsed statement. The expected positions for the parsed statement are line 1 and + /// columns offset by 11 characters. + fn do_if_uniline_allowed_test(text: &str, stmt: Statement) { + do_ok_test( + &format!("IF 1 THEN {}\nZ", text), + &[ + Statement::If(IfSpan { + branches: vec![IfBranchSpan { guard: expr_integer(1, 1, 4), body: vec![stmt] }], + }), + make_bare_builtin_call("Z", 2, 1), + ], + ); + } + + #[test] + fn test_if_uniline_allowed_data() { + do_if_uniline_allowed_test("DATA", Statement::Data(DataSpan { values: vec![None] })); + } + + #[test] + fn test_if_uniline_allowed_end() { + do_if_uniline_allowed_test( + "END 8", + Statement::End(EndSpan { code: Some(expr_integer(8, 1, 15)), pos: lc(1, 11) }), + ); + } + + #[test] + fn test_if_uniline_allowed_exit() { + do_if_uniline_allowed_test("EXIT DO", Statement::ExitDo(ExitSpan { pos: lc(1, 11) })); + + do_error_test("IF 1 THEN EXIT", "1:15: Expecting DO, FOR, FUNCTION or SUB after EXIT"); + } + + #[test] + fn test_if_uniline_allowed_gosub() { + do_if_uniline_allowed_test( + "GOSUB 10", + Statement::Gosub(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 17) }), + ); + + do_error_test("IF 1 THEN GOSUB", "1:16: Expected label name after GOSUB"); + } + + #[test] + fn test_if_uniline_allowed_goto() { + do_if_uniline_allowed_test( + "GOTO 10", + Statement::Goto(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 16) }), + ); + + do_error_test("IF 1 THEN GOTO", "1:15: Expected label name after GOTO"); + } + + #[test] + fn test_if_uniline_allowed_on_error() { + do_if_uniline_allowed_test( + "ON ERROR RESUME NEXT", + Statement::OnError(OnErrorSpan::ResumeNext), + ); + + do_error_test("IF 1 THEN ON", "1:13: Expected ERROR after ON"); + } + + #[test] + fn test_if_uniline_allowed_return() { + do_if_uniline_allowed_test("RETURN", Statement::Return(ReturnSpan { pos: lc(1, 11) })); + } + + #[test] + fn test_if_uniline_allowed_assignment() { + do_if_uniline_allowed_test( + "a = 3", + Statement::Assignment(AssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 11), + expr: expr_integer(3, 1, 15), + }), + ); + } + + #[test] + fn test_if_uniline_allowed_array_assignment() { + do_if_uniline_allowed_test( + "a(3) = 5", + Statement::ArrayAssignment(ArrayAssignmentSpan { + vref: VarRef::new("a", None), + vref_pos: lc(1, 11), + subscripts: vec![expr_integer(3, 1, 13)], + expr: expr_integer(5, 1, 18), + }), + ); + } + + #[test] + fn test_if_uniline_allowed_builtin_call() { + do_if_uniline_allowed_test( + "a 0", + Statement::Call(CallSpan { + vref: VarRef::new("A", None), + vref_pos: lc(1, 11), + args: vec![ArgSpan { + expr: Some(expr_integer(0, 1, 13)), + sep: ArgSep::End, + sep_pos: lc(1, 14), + }], + }), + ); + } + + #[test] + fn test_if_uniline_unallowed_statements() { + for t in ["DIM", "DO", "IF", "FOR", "10", "@label", "SELECT", "WHILE"] { + do_error_test( + &format!("IF 1 THEN {}", t), + &format!("1:11: Unexpected {} in uniline IF branch", t), + ); + } + } + + #[test] + fn test_for_empty() { + let auto_iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 1 TO 10\nNEXT", + &[Statement::For(ForSpan { + iter: auto_iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(1, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(auto_iter.clone(), 1, 5), + rhs: expr_integer(10, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(auto_iter, 1, 5), + rhs: expr_integer(1, 1, 16), + pos: lc(1, 11), + })), + body: vec![], + })], + ); + + let typed_iter = VarRef::new("d", Some(ExprType::Double)); + do_ok_test( + "FOR d# = 1.0 TO 10.2\nREM Nothing to do\nNEXT", + &[Statement::For(ForSpan { + iter: typed_iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_double(1.0, 1, 10), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(typed_iter.clone(), 1, 5), + rhs: expr_double(10.2, 1, 17), + pos: lc(1, 14), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(typed_iter, 1, 5), + rhs: expr_integer(1, 1, 21), + pos: lc(1, 14), + })), + body: vec![], + })], + ); + } + + #[test] + fn test_for_incrementing() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5\nA\nB\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(1, 1, 15), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_for_incrementing_with_step() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5 STEP 2\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(2, 1, 21), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 0 TO 5 STEP 2.5\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: true, + start: expr_integer(0, 1, 9), + end: Expr::LessEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(5, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_double(2.5, 1, 21), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + } + + #[test] + fn test_for_decrementing_with_step() { + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 5 TO 0 STEP -1\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: false, + start: expr_integer(5, 1, 9), + end: Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(0, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_integer(-1, 1, 22), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + + let iter = VarRef::new("i", None); + do_ok_test( + "FOR i = 5 TO 0 STEP -1.2\nA\nNEXT", + &[Statement::For(ForSpan { + iter: iter.clone(), + iter_pos: lc(1, 5), + iter_double: true, + start: expr_integer(5, 1, 9), + end: Expr::GreaterEqual(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter.clone(), 1, 5), + rhs: expr_integer(0, 1, 14), + pos: lc(1, 11), + })), + next: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_symbol(iter, 1, 5), + rhs: expr_double(-1.2, 1, 22), + pos: lc(1, 11), + })), + body: vec![make_bare_builtin_call("A", 2, 1)], + })], + ); + } + + #[test] + fn test_for_errors() { + do_error_test("FOR\n", "1:4: No iterator name in FOR statement"); + do_error_test("FOR =\n", "1:5: No iterator name in FOR statement"); + do_error_test( + "FOR a$\n", + "1:5: Iterator name in FOR statement must be a numeric reference", + ); + + do_error_test("FOR d#\n", "1:7: No equal sign in FOR statement"); + do_error_test("FOR i 3\n", "1:7: No equal sign in FOR statement"); + do_error_test("FOR i = TO\n", "1:9: No start expression in FOR statement"); + do_error_test("FOR i = NEXT\n", "1:9: Unexpected keyword in expression"); + + do_error_test("FOR i = 3 STEP\n", "1:11: No TO in FOR statement"); + do_error_test("FOR i = 3 TO STEP\n", "1:14: No end expression in FOR statement"); + do_error_test("FOR i = 3 TO NEXT\n", "1:14: Unexpected keyword in expression"); + + do_error_test("FOR i = 3 TO 1 STEP a\n", "1:21: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP -a\n", "1:22: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP NEXT\n", "1:21: STEP needs a literal number"); + do_error_test("FOR i = 3 TO 1 STEP 0\n", "1:21: Infinite FOR loop; STEP cannot be 0"); + do_error_test("FOR i = 3 TO 1 STEP 0.0\n", "1:21: Infinite FOR loop; STEP cannot be 0"); + + do_error_test("FOR i = 3 TO 1", "1:15: Expecting newline after FOR"); + do_error_test("FOR i = 1 TO 3 STEP 1", "1:22: Expecting newline after FOR"); + do_error_test("FOR i = 3 TO 1 STEP -1", "1:23: Expecting newline after FOR"); + + do_error_test(" FOR i = 0 TO 10\nPRINT i\n", "1:5: FOR without NEXT"); + } + + #[test] + fn test_function_empty() { + do_ok_test( + "FUNCTION foo$\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_some_content() { + do_ok_test( + r#" + FUNCTION foo$ + A + END + END 8 + B + END FUNCTION + "#, + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(2, 26), + params: vec![], + body: vec![ + make_bare_builtin_call("A", 3, 21), + Statement::End(EndSpan { code: None, pos: lc(4, 21) }), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(5, 25) })), + pos: lc(5, 21), + }), + make_bare_builtin_call("B", 6, 21), + ], + end_pos: lc(7, 17), + })], + ); + } + + #[test] + fn test_function_one_param() { + do_ok_test( + "FUNCTION foo$(x)\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![VarRef::new("x", None)], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_multiple_params() { + do_ok_test( + "FUNCTION foo$(x$, y, z AS BOOLEAN)\nEND FUNCTION", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 10), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_function_errors() { + do_error_test("FUNCTION", "1:9: Expected a function name after FUNCTION"); + do_error_test("FUNCTION foo", "1:13: Expected newline after FUNCTION name"); + do_error_test("FUNCTION foo 3", "1:14: Expected newline after FUNCTION name"); + do_error_test("FUNCTION foo\nEND", "1:1: FUNCTION without END FUNCTION"); + do_error_test("FUNCTION foo\nEND IF", "2:1: END IF without IF"); + do_error_test("FUNCTION foo\nEND SUB", "2:1: END SUB without SUB"); + do_error_test( + "FUNCTION foo\nFUNCTION bar\nEND FUNCTION\nEND FUNCTION", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test( + "FUNCTION foo\nSUB bar\nEND SUB\nEND FUNCTION", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test("FUNCTION foo (", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo ()", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (,)", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (a,)", "1:17: Expected a parameter name"); + do_error_test("FUNCTION foo (,b)", "1:15: Expected a parameter name"); + do_error_test("FUNCTION foo (a AS)", "1:19: Invalid type name ) in AS type definition"); + do_error_test( + "FUNCTION foo (a INTEGER)", + "1:17: Expected comma, AS, or end of parameters list", + ); + do_error_test("FUNCTION foo (a? AS BOOLEAN)", "1:15: Type annotation not allowed in a?"); + } + + #[test] + fn test_gosub_ok() { + do_ok_test( + "GOSUB 10", + &[Statement::Gosub(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 7) })], + ); + + do_ok_test( + "GOSUB @foo", + &[Statement::Gosub(GotoSpan { target: "foo".to_owned(), target_pos: lc(1, 7) })], + ); + } + + #[test] + fn test_gosub_errors() { + do_error_test("GOSUB\n", "1:6: Expected label name after GOSUB"); + do_error_test("GOSUB foo\n", "1:7: Expected label name after GOSUB"); + do_error_test("GOSUB \"foo\"\n", "1:7: Expected label name after GOSUB"); + do_error_test("GOSUB @foo, @bar\n", "1:11: Expected newline but found ,"); + do_error_test("GOSUB @foo, 3\n", "1:11: Expected newline but found ,"); + } + + #[test] + fn test_goto_ok() { + do_ok_test( + "GOTO 10", + &[Statement::Goto(GotoSpan { target: "10".to_owned(), target_pos: lc(1, 6) })], + ); + + do_ok_test( + "GOTO @foo", + &[Statement::Goto(GotoSpan { target: "foo".to_owned(), target_pos: lc(1, 6) })], + ); + } + + #[test] + fn test_goto_errors() { + do_error_test("GOTO\n", "1:5: Expected label name after GOTO"); + do_error_test("GOTO foo\n", "1:6: Expected label name after GOTO"); + do_error_test("GOTO \"foo\"\n", "1:6: Expected label name after GOTO"); + do_error_test("GOTO @foo, @bar\n", "1:10: Expected newline but found ,"); + do_error_test("GOTO @foo, 3\n", "1:10: Expected newline but found ,"); + } + + #[test] + fn test_label_own_line() { + do_ok_test( + "@foo\nPRINT", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + make_bare_builtin_call("PRINT", 2, 1), + ], + ); + } + + #[test] + fn test_label_before_statement() { + do_ok_test( + "@foo PRINT", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + make_bare_builtin_call("PRINT", 1, 6), + ], + ); + } + + #[test] + fn test_label_multiple_same_line() { + do_ok_test( + "@foo @bar", + &[ + Statement::Label(LabelSpan { name: "foo".to_owned(), name_pos: lc(1, 1) }), + Statement::Label(LabelSpan { name: "bar".to_owned(), name_pos: lc(1, 6) }), + ], + ); + } + + #[test] + fn test_label_errors() { + do_error_test("PRINT @foo", "1:7: Unexpected keyword in expression"); + } + + #[test] + fn test_parse_on_error_ok() { + do_ok_test("ON ERROR GOTO 0", &[Statement::OnError(OnErrorSpan::Reset)]); + + do_ok_test( + "ON ERROR GOTO 10", + &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: "10".to_owned(), + target_pos: lc(1, 15), + }))], + ); + + do_ok_test( + "ON ERROR GOTO @foo", + &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { + target: "foo".to_owned(), + target_pos: lc(1, 15), + }))], + ); + + do_ok_test("ON ERROR RESUME NEXT", &[Statement::OnError(OnErrorSpan::ResumeNext)]); + } + + #[test] + fn test_parse_on_error_errors() { + do_error_test("ON", "1:3: Expected ERROR after ON"); + do_error_test("ON NEXT", "1:4: Expected ERROR after ON"); + do_error_test("ON ERROR", "1:9: Expected GOTO or RESUME after ON ERROR"); + do_error_test("ON ERROR FOR", "1:10: Expected GOTO or RESUME after ON ERROR"); + + do_error_test("ON ERROR RESUME", "1:16: Expected NEXT after ON ERROR RESUME"); + do_error_test("ON ERROR RESUME 3", "1:17: Expected NEXT after ON ERROR RESUME"); + do_error_test("ON ERROR RESUME NEXT 3", "1:22: Expected newline but found 3"); + + do_error_test("ON ERROR GOTO", "1:14: Expected label name or 0 after ON ERROR GOTO"); + do_error_test("ON ERROR GOTO NEXT", "1:15: Expected label name or 0 after ON ERROR GOTO"); + do_error_test("ON ERROR GOTO 0 @a", "1:17: Expected newline but found @a"); + } + + #[test] + fn test_select_empty() { + do_ok_test( + "SELECT CASE 7\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![], + end_pos: lc(2, 1), + })], + ); + + do_ok_test( + "SELECT CASE 5 - TRUE\n \nEND SELECT", + &[Statement::Select(SelectSpan { + expr: Expr::Subtract(Box::from(BinaryOpSpan { + lhs: expr_integer(5, 1, 13), + rhs: expr_boolean(true, 1, 17), + pos: lc(1, 15), + })), + cases: vec![], + end_pos: lc(3, 1), + })], + ); + } + + #[test] + fn test_select_case_else_only() { + do_ok_test( + "SELECT CASE 7\nCASE ELSE\nA\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![], + body: vec![make_bare_builtin_call("A", 3, 1)], + }], + end_pos: lc(4, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_without_else() { + do_ok_test( + "SELECT CASE 7\nCASE 1\nA\nCASE 2\nB\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![make_bare_builtin_call("A", 3, 1)], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![make_bare_builtin_call("B", 5, 1)], + }, + ], + end_pos: lc(6, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_with_else() { + do_ok_test( + "SELECT CASE 7\nCASE 1\nA\nCASE 2\nB\nCASE ELSE\nC\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![make_bare_builtin_call("A", 3, 1)], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![make_bare_builtin_call("B", 5, 1)], + }, + CaseSpan { guards: vec![], body: vec![make_bare_builtin_call("C", 7, 1)] }, + ], + end_pos: lc(8, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_empty_bodies() { + do_ok_test( + "SELECT CASE 7\nCASE 1\n\nCASE 2\n\nCASE ELSE\n\nEND SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 2, 6))], + body: vec![], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 4, 6))], + body: vec![], + }, + CaseSpan { guards: vec![], body: vec![] }, + ], + end_pos: lc(8, 1), + })], + ); + } + + #[test] + fn test_select_multiple_cases_with_interleaved_end() { + let code = r#" + SELECT CASE 7 + CASE 1 + A + END + B + CASE 2 ' Second case. + C + END 8 + D + CASE ELSE + E + END + F + END SELECT + "#; + do_ok_test( + code, + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 2, 25), + cases: vec![ + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 3, 22))], + body: vec![ + make_bare_builtin_call("A", 4, 21), + Statement::End(EndSpan { code: None, pos: lc(5, 21) }), + make_bare_builtin_call("B", 6, 21), + ], + }, + CaseSpan { + guards: vec![CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(2, 7, 22))], + body: vec![ + make_bare_builtin_call("C", 8, 21), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(9, 25) })), + pos: lc(9, 21), + }), + make_bare_builtin_call("D", 10, 21), + ], + }, + CaseSpan { + guards: vec![], + body: vec![ + make_bare_builtin_call("E", 12, 21), + Statement::End(EndSpan { code: None, pos: lc(13, 21) }), + make_bare_builtin_call("F", 14, 21), + ], + }, + ], + end_pos: lc(15, 13), + })], + ); + } + + #[test] + fn test_select_case_guards_equals() { + do_ok_test( + "SELECT CASE 7: CASE 9, 10, FALSE: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(9, 1, 21)), + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(10, 1, 24)), + CaseGuardSpan::Is(CaseRelOp::Equal, expr_boolean(false, 1, 28)), + ], + body: vec![], + }], + end_pos: lc(1, 35), + })], + ); + } + + #[test] + fn test_select_case_guards_is() { + do_ok_test( + "SELECT CASE 7: CASE IS = 1, IS <> 2, IS < 3, IS <= 4, IS > 5, IS >= 6: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::Is(CaseRelOp::Equal, expr_integer(1, 1, 26)), + CaseGuardSpan::Is(CaseRelOp::NotEqual, expr_integer(2, 1, 35)), + CaseGuardSpan::Is(CaseRelOp::Less, expr_integer(3, 1, 43)), + CaseGuardSpan::Is(CaseRelOp::LessEqual, expr_integer(4, 1, 52)), + CaseGuardSpan::Is(CaseRelOp::Greater, expr_integer(5, 1, 60)), + CaseGuardSpan::Is(CaseRelOp::GreaterEqual, expr_integer(6, 1, 69)), + ], + body: vec![], + }], + end_pos: lc(1, 72), + })], + ); + } + + #[test] + fn test_select_case_guards_to() { + do_ok_test( + "SELECT CASE 7: CASE 1 TO 20, 10 TO 1: END SELECT", + &[Statement::Select(SelectSpan { + expr: expr_integer(7, 1, 13), + cases: vec![CaseSpan { + guards: vec![ + CaseGuardSpan::To(expr_integer(1, 1, 21), expr_integer(20, 1, 26)), + CaseGuardSpan::To(expr_integer(10, 1, 30), expr_integer(1, 1, 36)), + ], + body: vec![], + }], + end_pos: lc(1, 39), + })], + ); + } + + #[test] + fn test_select_errors() { + do_error_test("SELECT\n", "1:7: Expecting CASE after SELECT"); + do_error_test("SELECT CASE\n", "1:12: No expression in SELECT CASE statement"); + do_error_test("SELECT CASE 3 + 7", "1:18: Expecting newline after SELECT CASE"); + do_error_test("SELECT CASE 3 + 7 ,", "1:19: Expecting newline after SELECT CASE"); + do_error_test("SELECT CASE 3 + 7 IF", "1:19: Unexpected keyword in expression"); + + do_error_test("SELECT CASE 1\n", "1:1: SELECT without END SELECT"); + + do_error_test( + "SELECT CASE 1\nEND", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + do_error_test( + "SELECT CASE 1\nEND IF", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + do_error_test( + "SELECT CASE 1\na = 1", + "2:1: Expected CASE after SELECT CASE before any statement", + ); + + do_error_test( + "SELECT CASE 1\nCASE 1", + "2:7: Expected comma, newline, or TO after expression", + ); + do_error_test("SELECT CASE 1\nCASE ELSE", "2:10: Expecting newline after CASE"); + + do_error_test("SELECT CASE 1\nCASE ELSE\nEND", "1:1: SELECT without END SELECT"); + do_error_test("SELECT CASE 1\nCASE ELSE\nEND IF", "3:1: END IF without IF"); + + do_error_test("SELECT CASE 1\nCASE ELSE\nCASE ELSE\n", "3:1: CASE ELSE must be unique"); + do_error_test("SELECT CASE 1\nCASE ELSE\nCASE 1\n", "3:1: CASE ELSE is not last"); + } + + #[test] + fn test_select_case_errors() { + fn do_case_error_test(cases: &str, exp_error: &str) { + do_error_test(&format!("SELECT CASE 1\nCASE {}\n", cases), exp_error); + } + + do_case_error_test("ELSE, ELSE", "2:10: Expected newline after CASE ELSE"); + do_case_error_test("ELSE, 7", "2:10: Expected newline after CASE ELSE"); + do_case_error_test("7, ELSE", "2:9: CASE ELSE must be on its own"); + + do_case_error_test("IS 7", "2:9: Expected relational operator"); + do_case_error_test("IS AND", "2:9: Expected relational operator"); + do_case_error_test("IS END", "2:9: Expected relational operator"); + + do_case_error_test("IS <>", "2:11: Missing expression after relational operator"); + do_case_error_test("IS <> IF", "2:12: Unexpected keyword in expression"); + + do_case_error_test("", "2:6: Missing expression in CASE guard"); + do_case_error_test("2 + 5 TO", "2:14: Missing expression after TO in CASE guard"); + do_case_error_test("2 + 5 TO AS", "2:15: Missing expression after TO in CASE guard"); + do_case_error_test( + "2 + 5 TO 8 AS", + "2:17: Expected comma, newline, or TO after expression", + ); + } + + #[test] + fn test_sub_empty() { + do_ok_test( + "SUB foo\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_some_content() { + do_ok_test( + r#" + SUB foo + A + END + END 8 + B + END SUB + "#, + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(2, 21), + params: vec![], + body: vec![ + make_bare_builtin_call("A", 3, 21), + Statement::End(EndSpan { code: None, pos: lc(4, 21) }), + Statement::End(EndSpan { + code: Some(Expr::Integer(IntegerSpan { value: 8, pos: lc(5, 25) })), + pos: lc(5, 21), + }), + make_bare_builtin_call("B", 6, 21), + ], + end_pos: lc(7, 17), + })], + ); + } + + #[test] + fn test_sub_one_param() { + do_ok_test( + "SUB foo(x)\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![VarRef::new("x", None)], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_multiple_params() { + do_ok_test( + "SUB foo(x$, y, z AS BOOLEAN)\nEND SUB", + &[Statement::Callable(CallableSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 5), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + body: vec![], + end_pos: lc(2, 1), + })], + ); + } + + #[test] + fn test_sub_errors() { + do_error_test("SUB", "1:4: Expected a function name after SUB"); + do_error_test("SUB foo", "1:8: Expected newline after SUB name"); + do_error_test("SUB foo 3", "1:9: Expected newline after SUB name"); + do_error_test("SUB foo\nEND", "1:1: SUB without END SUB"); + do_error_test("SUB foo\nEND IF", "2:1: END IF without IF"); + do_error_test("SUB foo\nEND FUNCTION", "2:1: END FUNCTION without FUNCTION"); + do_error_test( + "SUB foo\nSUB bar\nEND SUB\nEND SUB", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test( + "SUB foo\nFUNCTION bar\nEND FUNCTION\nEND SUB", + "2:1: Cannot nest FUNCTION or SUB definitions", + ); + do_error_test("SUB foo (", "1:10: Expected a parameter name"); + do_error_test("SUB foo ()", "1:10: Expected a parameter name"); + do_error_test("SUB foo (,)", "1:10: Expected a parameter name"); + do_error_test("SUB foo (a,)", "1:12: Expected a parameter name"); + do_error_test("SUB foo (,b)", "1:10: Expected a parameter name"); + do_error_test("SUB foo (a AS)", "1:14: Invalid type name ) in AS type definition"); + do_error_test("SUB foo (a INTEGER)", "1:12: Expected comma, AS, or end of parameters list"); + do_error_test("SUB foo (a? AS BOOLEAN)", "1:10: Type annotation not allowed in a?"); + do_error_test( + "SUB foo$", + "1:5: SUBs cannot return a value so type annotations are not allowed", + ); + do_error_test( + "SUB foo$\nEND SUB", + "1:5: SUBs cannot return a value so type annotations are not allowed", + ); + } + + #[test] + fn test_while_empty() { + do_ok_test( + "WHILE 2 + 3\nWEND", + &[Statement::While(WhileSpan { + expr: Expr::Add(Box::from(BinaryOpSpan { + lhs: expr_integer(2, 1, 7), + rhs: expr_integer(3, 1, 11), + pos: lc(1, 9), + })), + body: vec![], + })], + ); + do_ok_test( + "WHILE 5\n\nREM foo\n\nWEND\n", + &[Statement::While(WhileSpan { expr: expr_integer(5, 1, 7), body: vec![] })], + ); + } + + #[test] + fn test_while_loops() { + do_ok_test( + "WHILE TRUE\nA\nB\nWEND", + &[Statement::While(WhileSpan { + expr: expr_boolean(true, 1, 7), + body: vec![make_bare_builtin_call("A", 2, 1), make_bare_builtin_call("B", 3, 1)], + })], + ); + } + + #[test] + fn test_while_nested() { + let code = r#" + WHILE TRUE + A + WHILE FALSE + B + WEND + C + WEND + "#; + do_ok_test( + code, + &[Statement::While(WhileSpan { + expr: expr_boolean(true, 2, 19), + body: vec![ + make_bare_builtin_call("A", 3, 17), + Statement::While(WhileSpan { + expr: expr_boolean(false, 4, 23), + body: vec![make_bare_builtin_call("B", 5, 21)], + }), + make_bare_builtin_call("C", 7, 17), + ], + })], + ); + } + + #[test] + fn test_while_errors() { + do_error_test("WHILE\n", "1:6: No expression in WHILE statement"); + do_error_test("WHILE TRUE", "1:11: Expecting newline after WHILE"); + do_error_test("\n\nWHILE TRUE\n", "3:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND", "1:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND\n", "1:1: WHILE without WEND"); + do_error_test("WHILE TRUE\nEND WHILE\n", "2:5: Unexpected keyword in expression"); + + do_error_test("WHILE ,\nWEND", "1:7: No expression in WHILE statement"); + do_error_test("WHILE ,\nEND", "1:7: No expression in WHILE statement"); + } +} diff --git a/core2/src/reader.rs b/core2/src/reader.rs new file mode 100644 index 00000000..27b6a856 --- /dev/null +++ b/core2/src/reader.rs @@ -0,0 +1,319 @@ +// EndBASIC +// Copyright 2020 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. + +//! Character-based reader for an input stream with position tracking. + +use std::char; +use std::fmt; +use std::io::{self, BufRead}; + +/// Tab length used to compute the current position within a line when encountering a tab character. +const TAB_LENGTH: usize = 8; + +/// A position within a source stream, represented as line and column numbers. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct LineCol { + /// Line number, starting from 1. + pub line: usize, + + /// Column number, starting from 1. + pub col: usize, +} + +impl fmt::Display for LineCol { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}:{}", self.line, self.col) + } +} + +/// A character along with its position in the source stream. +#[derive(Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct CharSpan { + /// The character value. + pub(crate) ch: char, + + /// The position where this character appears in the source. + pub(crate) pos: LineCol, +} + +/// State of buffered data in the character reader. +enum Pending { + /// Initial state where no data has been buffered yet. + Unknown, + + /// Holds a buffered line of characters and the index of the next character to return. + Chars(Vec, usize), + + /// Terminal state indicating end-of-file has been reached. + Eof, + + /// Terminal state indicating an I/O error occurred. Contains the original error if not yet + /// consumed, or `None` if the error was already returned to the caller. + Error(Option), +} + +/// Wraps an `io::Read` to provide an iterator over characters with position tracking. +pub struct CharReader<'a> { + /// The buffered reader wrapping the input stream. + reader: io::BufReader<&'a mut dyn io::Read>, + + /// Current state of buffered character data. + pending: Pending, + + /// If set, contains the result of a peek operation to be returned by the next `next()` call. + peeked: Option>>, + + /// Position of the next character to be read. + next_pos: LineCol, +} + +impl<'a> CharReader<'a> { + /// Constructs a new character reader from an `io::Read`. + pub fn from(reader: &'a mut dyn io::Read) -> Self { + Self { + reader: io::BufReader::new(reader), + pending: Pending::Unknown, + peeked: None, + next_pos: LineCol { line: 1, col: 1 }, + } + } + + /// Replenishes `pending` with the next line to process. + fn refill_and_next(&mut self) -> Option> { + self.pending = { + let mut line = String::new(); + match self.reader.read_line(&mut line) { + Ok(0) => Pending::Eof, + Ok(_) => Pending::Chars(line.chars().collect(), 0), + Err(e) => Pending::Error(Some(e)), + } + }; + self.next() + } + + /// Peeks into the next character without consuming it. + pub(crate) fn peek(&mut self) -> Option<&io::Result> { + if self.peeked.is_none() { + let next = self.next(); + self.peeked.replace(next); + } + self.peeked.as_ref().unwrap().as_ref() + } + + /// Gets the current position of the read, which is the position that the next character will + /// carry. + pub(crate) fn next_pos(&self) -> LineCol { + self.next_pos + } +} + +impl Iterator for CharReader<'_> { + type Item = io::Result; + + /// Returns the next character in the input stream. + fn next(&mut self) -> Option { + if let Some(peeked) = self.peeked.take() { + return peeked; + } + + match &mut self.pending { + Pending::Unknown => self.refill_and_next(), + Pending::Eof => None, + Pending::Chars(chars, last) => { + if *last == chars.len() { + self.refill_and_next() + } else { + let ch = chars[*last]; + *last += 1; + + let pos = self.next_pos; + match ch { + '\n' => { + self.next_pos.line += 1; + self.next_pos.col = 1; + } + '\t' => { + self.next_pos.col = + (self.next_pos.col - 1 + TAB_LENGTH) / TAB_LENGTH * TAB_LENGTH + 1; + } + _ => { + self.next_pos.col += 1; + } + } + + Some(Ok(CharSpan { ch, pos })) + } + } + Pending::Error(e) => match e.take() { + Some(e) => Some(Err(e)), + None => Some(Err(io::Error::other("Invalid state; error already consumed"))), + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Syntactic sugar to instantiate a `CharSpan` for testing. + fn cs(ch: char, line: usize, col: usize) -> CharSpan { + CharSpan { ch, pos: LineCol { line, col } } + } + + #[test] + fn test_empty() { + let mut input = b"".as_ref(); + let mut reader = CharReader::from(&mut input); + assert!(reader.next().is_none()); + } + + #[test] + fn test_multibyte_chars() { + let mut input = "Hi 훌리오".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('H', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('i', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs(' ', 1, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('훌', 1, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('리', 1, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('오', 1, 6), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_consecutive_newlines() { + let mut input = b"a\n\nbc\n".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('b', 3, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('c', 3, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 3, 3), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_tabs() { + let mut input = "1\t9\n1234567\t8\n12345678\t9".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('1', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('9', 1, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 10), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('2', 2, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('3', 2, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('4', 2, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('5', 2, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('6', 2, 6), reader.next().unwrap().unwrap()); + assert_eq!(cs('7', 2, 7), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 2, 8), reader.next().unwrap().unwrap()); + assert_eq!(cs('8', 2, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 10), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 3, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('2', 3, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('3', 3, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('4', 3, 4), reader.next().unwrap().unwrap()); + assert_eq!(cs('5', 3, 5), reader.next().unwrap().unwrap()); + assert_eq!(cs('6', 3, 6), reader.next().unwrap().unwrap()); + assert_eq!(cs('7', 3, 7), reader.next().unwrap().unwrap()); + assert_eq!(cs('8', 3, 8), reader.next().unwrap().unwrap()); + assert_eq!(cs('\t', 3, 9), reader.next().unwrap().unwrap()); + assert_eq!(cs('9', 3, 17), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_crlf() { + let mut input = b"a\r\nb".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\r', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 3), reader.next().unwrap().unwrap()); + assert_eq!(cs('b', 2, 1), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_past_eof_returns_eof() { + let mut input = b"a".as_ref(); + let mut reader = CharReader::from(&mut input); + assert_eq!(cs('a', 1, 1), reader.next().unwrap().unwrap()); + assert!(reader.next().is_none()); + assert!(reader.next().is_none()); + } + + #[test] + fn test_next_pos() { + let mut input = "Hi".as_bytes(); + let mut reader = CharReader::from(&mut input); + assert_eq!(LineCol { line: 1, col: 1 }, reader.next_pos()); + assert_eq!(cs('H', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(LineCol { line: 1, col: 2 }, reader.next_pos()); + assert_eq!(cs('i', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); + assert!(reader.next().is_none()); + assert_eq!(LineCol { line: 1, col: 3 }, reader.next_pos()); + } + + /// A reader that generates an error only on the Nth read operation. + /// + /// All other reads return a line with a single character in them with the assumption that the + /// `CharReader` issues a single read per line. If that assumption changes, the tests here may + /// start failing. + struct FaultyReader { + current_read: usize, + fail_at_read: usize, + } + + impl FaultyReader { + /// Creates a new reader that will fail at the `fail_at_read`th operation. + fn new(fail_at_read: usize) -> Self { + let current_read = 0; + FaultyReader { current_read, fail_at_read } + } + } + + impl io::Read for FaultyReader { + #[allow(clippy::branches_sharing_code)] + fn read(&mut self, buf: &mut [u8]) -> io::Result { + if self.current_read == self.fail_at_read { + self.current_read += 1; + Err(io::Error::from(io::ErrorKind::InvalidInput)) + } else { + self.current_read += 1; + buf[0] = b'1'; + buf[1] = b'\n'; + Ok(2) + } + } + } + + #[test] + fn test_errors_prevent_further_reads() { + let mut reader = FaultyReader::new(2); + let mut reader = CharReader::from(&mut reader); + assert_eq!(cs('1', 1, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 1, 2), reader.next().unwrap().unwrap()); + assert_eq!(cs('1', 2, 1), reader.next().unwrap().unwrap()); + assert_eq!(cs('\n', 2, 2), reader.next().unwrap().unwrap()); + assert_eq!(io::ErrorKind::InvalidInput, reader.next().unwrap().unwrap_err().kind()); + assert_eq!(io::ErrorKind::Other, reader.next().unwrap().unwrap_err().kind()); + assert_eq!(io::ErrorKind::Other, reader.next().unwrap().unwrap_err().kind()); + } +} diff --git a/core2/src/testutils.rs b/core2/src/testutils.rs new file mode 100644 index 00000000..5c3edf01 --- /dev/null +++ b/core2/src/testutils.rs @@ -0,0 +1,107 @@ +// EndBASIC +// Copyright 2021 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. + +//! Test utilities. + +use crate::ast::{ArgSep, ExprType}; +use crate::bytecode::VarArgTag; +use crate::callable::{ + ArgSepSyntax, CallResult, Callable, CallableMetadata, CallableMetadataBuilder, RepeatedSyntax, + RepeatedTypeSyntax, Scope, +}; +use async_trait::async_trait; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// Simplified version of `PRINT` that captures all calls to it into `data`. +/// +/// This command only accepts arguments separated by the `;` short separator and concatenates +/// them with a single space. +pub struct OutCommand { + /// Metadata describing the command's name and syntax. + metadata: CallableMetadata, + + /// Shared storage for captured output strings. + data: Rc>>, +} + +impl OutCommand { + /// Creates a new command that captures all calls into `data`. + pub fn new(data: Rc>>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Short), + require_one: false, + allow_missing: false, + }), + )]) + .test_build(), + data, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut first = true; + let mut text = String::new(); + let mut reg = 0; + loop { + if !first { + text += " "; + } + first = false; + + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + match etype { + ExprType::Boolean => text.push_str(&format!("{}", scope.get_boolean(reg))), + ExprType::Double => text.push_str(&format!("{}", scope.get_double(reg))), + ExprType::Integer => text.push_str(&format!("{}", scope.get_integer(reg))), + ExprType::Text => text.push_str(scope.get_string(reg)), + } + sep + } + VarArgTag::Missing(sep) => { + text.push_str(""); + sep + } + VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + }; + reg += 1; + + if sep == ArgSep::End { + break; + } + text.push(' '); + text.push_str(&sep.to_string()); + text.push(' '); + } + self.data.borrow_mut().push(text); + Ok(()) + } +} diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs new file mode 100644 index 00000000..6fa8dfa0 --- /dev/null +++ b/core2/src/vm/context.rs @@ -0,0 +1,330 @@ +// 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. + +//! Virtual processor for EndBASIC execution. + +use crate::bytecode::{self, Opcode, Register, opcode_of}; +use crate::image::Image; +use crate::mem::{Datum, Pointer}; +use crate::num::unchecked_u24_as_usize; + +/// Alias for the type representing a program address. +type Address = usize; + +/// Internal representation of a `StopReason` that requires further annotation by the caller. +pub(super) enum InternalStopReason { + /// Execution terminated due to an `END` instruction. + End(i32), + + /// Execution stopped due to an instruction-level exception. + Exception(Address, String), + + /// Execution stopped due to an upcall that requires service from the caller. + Upcall(u16, Register), +} + +/// Represents a call frame in the stack. +struct Frame { + /// Program counter of the instruction that caused the call. + old_pc: Address, + + /// Frame pointer of the previous frame. + old_fp: usize, + + /// Register to store the return value of the call, if any. + ret_reg: Option, +} + +/// Execution context for the virtual machine. +/// +/// This roughly corresponds to the concept of a "processor", making the VM the container of +/// various objects and the context the representation of the execution. +pub(super) struct Context { + /// Program counter. + pc: Address, + + /// Frame pointer. Contains the offset of the first local register for the current + /// scope. + fp: usize, + + /// Stop signal. If set, indicates why the execution stopped during instruction processing. + stop: Option, + + /// Register values. The first N registers hold global variables. After those, we find + /// the registers for all local variables and for all scopes. + regs: Vec, + + /// Stack of call frames for tracking subroutine and function calls. + call_stack: Vec, +} + +impl Default for Context { + fn default() -> Self { + Self { + pc: 0, + fp: usize::from(Register::MAX_GLOBAL), + stop: None, + regs: vec![0; usize::from(Register::MAX_GLOBAL)], + call_stack: vec![], + } + } +} + +impl Context { + /// Gets the value of register `reg`. + /// + /// Panics if the register is invalid. + fn get_reg(&self, reg: Register) -> u64 { + let (is_global, index) = reg.to_parts(); + let mut index = usize::from(index); + if !is_global { + index += self.fp; + } + self.regs[index] + } + + /// Sets the value of register `reg` to `value`. + /// + /// Panics if the register is invalid. + fn set_reg(&mut self, reg: Register, value: u64) { + let (is_global, index) = reg.to_parts(); + let mut index = usize::from(index); + if !is_global { + index += self.fp; + } + self.regs[index] = value; + } + + /// Dereferences a pointer. + fn deref_ptr<'b>(&self, reg: Register, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { + let raw_addr = self.get_reg(reg); + match Pointer::from(raw_addr) { + Pointer::Constant(index) => &constants[unchecked_u24_as_usize(index)], + Pointer::Heap(index) => &heap[unchecked_u24_as_usize(index)], + } + } + + /// Registers that the instruction being processed threw an exception `message`. + /// + /// It's the responsibility of the execution loop to check for the presence of exceptions and + /// to stop execution if needed. + fn set_exception>(&mut self, message: S) { + self.stop = Some(InternalStopReason::Exception(self.pc, message.into())); + } + + /// Gets a view of the local registers starting at `reg`. + pub(super) fn get_local_regs(&self, reg: Register) -> &[u64] { + let (is_global, index) = reg.to_parts(); + assert!(!is_global); + let index = usize::from(index); + let start = self.fp + index; + &self.regs[start..] + } + + /// Starts or resumes execution of `image`. + /// + /// Panics if the processor state is out of sync with `image` or if the contents of `image` + /// are invalid. We assume that the image comes from the result of an in-process compilation + /// (not stored bytecode) and that the compiler guarantees that the image is valid. + pub(super) fn exec(&mut self, image: &Image, heap: &mut Vec) -> InternalStopReason { + while self.stop.is_none() { + let instr = image.code[self.pc]; + + match opcode_of(instr) { + Opcode::AddDouble => self.do_add_double(instr), + Opcode::AddInteger => self.do_add_integer(instr), + Opcode::Alloc => self.do_alloc(instr, heap), + Opcode::Call => self.do_call(instr), + Opcode::Concat => self.do_concat(instr, &image.constants, heap), + Opcode::DoubleToInteger => self.do_double_to_integer(instr), + Opcode::End => self.do_end(instr), + Opcode::Enter => self.do_enter(instr), + Opcode::Gosub => self.do_gosub(instr), + Opcode::IntegerToDouble => self.do_integer_to_double(instr), + Opcode::Jump => self.do_jump(instr), + Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), + Opcode::LoadInteger => self.do_load_integer(instr), + Opcode::Move => self.do_move(instr), + Opcode::Nop => self.do_nop(instr), + Opcode::Return => self.do_return(instr), + Opcode::Upcall => self.do_upcall(instr), + } + } + self.stop.take().expect("The loop above can only exit when there is a stop reason") + } +} + +impl Context { + /// Implements the `AddDouble` opcode. + pub(super) fn do_add_double(&mut self, instr: u32) { + let (dest, src1, src2) = bytecode::parse_add_double(instr); + let lhs = f64::from_bits(self.get_reg(src1)); + let rhs = f64::from_bits(self.get_reg(src2)); + self.set_reg(dest, (lhs + rhs).to_bits()); + self.pc += 1; + } + + /// Implements the `AddInteger` opcode. + pub(super) fn do_add_integer(&mut self, instr: u32) { + let (dest, src1, src2) = bytecode::parse_add_integer(instr); + let lhs = self.get_reg(src1) as i32; + let rhs = self.get_reg(src2) as i32; + match lhs.checked_add(rhs) { + Some(result) => { + self.set_reg(dest, result as u64); + self.pc += 1; + } + None => { + self.set_exception("Integer overflow"); + } + } + } + + /// Implements the `Alloc` opcode. + pub(super) fn do_alloc(&mut self, instr: u32, heap: &mut Vec) { + let (dest, etype) = bytecode::parse_alloc(instr); + heap.push(Datum::new(etype)); + let ptr = Pointer::for_heap((heap.len() - 1) as u32); + self.set_reg(dest, ptr); + self.pc += 1; + } + + /// Implements the `Call` opcode. + pub(super) fn do_call(&mut self, instr: u32) { + let (reg, offset) = bytecode::parse_call(instr); + self.call_stack.push(Frame { old_pc: self.pc, old_fp: self.fp, ret_reg: Some(reg) }); + self.pc = Address::from(offset); + self.fp = self.regs.len(); + } + + /// Implements the `Concat` opcode. + pub(super) fn do_concat(&mut self, instr: u32, constants: &[Datum], heap: &mut Vec) { + let (dest, src1, src2) = bytecode::parse_concat(instr); + let lhs = self.deref_ptr(src1, constants, heap); + let rhs = self.deref_ptr(src2, constants, heap); + let result = match (lhs, rhs) { + (Datum::Text(lhs), Datum::Text(rhs)) => format!("{}{}", lhs, rhs), + _ => unreachable!(), + }; + heap.push(Datum::Text(result)); + let ptr = Pointer::for_heap((heap.len() - 1) as u32); + self.set_reg(dest, ptr); + self.pc += 1; + } + + /// Implements the `DoubleToInteger` opcode. + pub(super) fn do_double_to_integer(&mut self, instr: u32) { + let reg = bytecode::parse_double_to_integer(instr); + let dvalue = f64::from_bits(self.get_reg(reg)); + self.set_reg(reg, dvalue.round() as u64); + self.pc += 1; + } + + /// Implements the `End` opcode. + pub(super) fn do_end(&mut self, instr: u32) { + let reg = bytecode::parse_end(instr); + let code = self.get_reg(reg) as i32; + self.stop = Some(InternalStopReason::End(code)); + } + + /// Implements the `Enter` opcode. + pub(super) fn do_enter(&mut self, instr: u32) { + let nlocals = bytecode::parse_enter(instr); + self.regs.resize(self.regs.len() + usize::from(nlocals), 0); + self.pc += 1; + } + + /// Implements the `Gosub` opcode. + pub(super) fn do_gosub(&mut self, instr: u32) { + let offset = bytecode::parse_gosub(instr); + self.call_stack.push(Frame { old_pc: self.pc, old_fp: self.fp, ret_reg: None }); + self.pc = Address::from(offset); + } + + /// Implements the `IntegerToDouble` opcode. + pub(super) fn do_integer_to_double(&mut self, instr: u32) { + let reg = bytecode::parse_integer_to_double(instr); + let ivalue = self.get_reg(reg) as i32; + self.set_reg(reg, (ivalue as f64).to_bits()); + self.pc += 1; + } + + /// Implements the `Jump` opcode. + pub(super) fn do_jump(&mut self, instr: u32) { + let offset = bytecode::parse_jump(instr); + self.pc = Address::from(offset); + } + + /// Implements the `LoadConstant` opcode. + pub(super) fn do_load_constant(&mut self, instr: u32, constants: &[Datum]) { + let (register, i) = bytecode::parse_load_constant(instr); + match &constants[usize::from(i)] { + Datum::Boolean(_) => unreachable!("Booleans are always immediates"), + Datum::Double(d) => self.set_reg(register, d.to_bits()), + Datum::Integer(i) => self.set_reg(register, *i as u64), + Datum::Text(_) => unreachable!("Strings cannot be loaded into registers"), + } + self.pc += 1; + } + + /// Implements the `LoadInteger` opcode. + pub(super) fn do_load_integer(&mut self, instr: u32) { + let (register, i) = bytecode::parse_load_integer(instr); + self.set_reg(register, i as u64); + self.pc += 1; + } + + /// Implements the `Move` opcode. + pub(super) fn do_move(&mut self, instr: u32) { + let (dest, src) = bytecode::parse_move(instr); + let value = self.get_reg(src); + self.set_reg(dest, value); + self.pc += 1; + } + + /// Implements the `Nop` opcode. + pub(super) fn do_nop(&mut self, instr: u32) { + bytecode::parse_nop(instr); + self.pc += 1; + } + + /// Implements the `Return` opcode. + pub(super) fn do_return(&mut self, instr: u32) { + bytecode::parse_return(instr); + let frame = match self.call_stack.pop() { + Some(frame) => frame, + None => { + self.set_exception("RETURN without GOSUB or FUNCTION call"); + return; + } + }; + if let Some(ret_reg) = frame.ret_reg { + let return_value = self.get_reg(Register::local(0).unwrap()); + self.pc = frame.old_pc + 1; + self.fp = frame.old_fp; + self.set_reg(ret_reg, return_value); + } else { + self.pc = frame.old_pc + 1; + self.fp = frame.old_fp; + } + } + + /// Implements the `Upcall` opcode. + pub(super) fn do_upcall(&mut self, instr: u32) { + let (index, first_reg) = bytecode::parse_upcall(instr); + self.stop = Some(InternalStopReason::Upcall(index, first_reg)); + self.pc += 1; + } +} diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs new file mode 100644 index 00000000..ece9e910 --- /dev/null +++ b/core2/src/vm/mod.rs @@ -0,0 +1,243 @@ +// 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. + +//! Virtual machine for EndBASIC execution. + +use crate::CallResult; +use crate::bytecode::Register; +use crate::callable::{Callable, Scope}; +use crate::compiler::SymbolKey; +use crate::image::Image; +use crate::mem::Datum; +use crate::reader::LineCol; +use std::collections::HashMap; +use std::rc::Rc; + +mod context; +use context::{Context, InternalStopReason}; + +/// Opaque handle to invoke a pending upcall. +pub struct UpcallHandler<'a>(&'a mut Vm); + +impl<'a> UpcallHandler<'a> { + /// Invokes the pending upcall. + pub async fn invoke(mut self) -> CallResult<()> { + let vm = &mut self.0; + let (index, first_reg) = vm + .pending_upcall + .take() + .expect("This is only reachable when the VM has a pending upcall"); + vm.upcalls[usize::from(index)].exec(vm.upcall_scope(first_reg)).await + } +} + +/// Representation of termination states from program execution. +pub enum StopReason<'a> { + /// Execution terminated due to an `END` instruction. + End(i32), + + /// Execution stopped due to an instruction-level exception. + Exception(LineCol, String), + + /// Execution stopped due to an upcall that requires service from the caller. + Upcall(UpcallHandler<'a>), +} + +/// Virtual machine for EndBASIC program execution. +pub struct Vm { + /// Mapping of all available upcall names to their handlers. + upcalls_by_name: HashMap>, + + /// Active image for execution. + image: Option, + + /// Upcalls used by the loaded image in index order. + upcalls: Vec>, + + /// Heap memory for dynamic allocations. + heap: Vec, + + /// Processor context for execution. + context: Context, + + /// Details about the pending upcall that has to be handled by the caller. + pending_upcall: Option<(u16, Register)>, +} + +impl Vm { + /// Creates a new VM with the given `upcalls_by_name` as the available built-in callables. + pub fn new(upcalls_by_name: HashMap>) -> Self { + Self { + upcalls_by_name, + image: None, + upcalls: vec![], + heap: vec![], + context: Context::default(), + pending_upcall: None, + } + } + + /// Loads an `image` into the VM for execution, resetting any previous execution state. + pub fn load(&mut self, image: Image) { + self.upcalls.clear(); + for key in &image.upcalls { + self.upcalls.push( + self.upcalls_by_name + .get(key) + .expect("All upcalls exposed during compilation must be present at runtime") + .clone(), + ); + } + + self.image = Some(image); + + self.heap.clear(); + } + + /// Constructs a `Scope` for an upcall with arguments starting at `reg`. + fn upcall_scope<'a>(&'a self, reg: Register) -> Scope<'a> { + let constants = match self.image.as_ref() { + Some(image) => image.constants.as_slice(), + None => &[], + }; + Scope { regs: self.context.get_local_regs(reg), constants, heap: &self.heap } + } + + /// Starts or resumes execution of the loaded image. + /// + /// Returns a `StopReason` indicating why execution stopped, which may be due to program + /// termination, an exception, or a pending upcall that requires caller handling. + pub fn exec(&mut self) -> StopReason<'_> { + let Some(image) = self.image.as_ref() else { + return StopReason::End(0); + }; + + if self.pending_upcall.is_some() { + return StopReason::Upcall(UpcallHandler(self)); + }; + + match self.context.exec(image, &mut self.heap) { + InternalStopReason::End(code) => StopReason::End(code), + InternalStopReason::Exception(pc, e) => { + let pos = image.debug_info.instr_linecols[pc]; + StopReason::Exception(pos, e) + } + InternalStopReason::Upcall(index, first_reg) => { + self.pending_upcall = Some((index, first_reg)); + StopReason::Upcall(UpcallHandler(self)) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::compiler::{SymbolKey, compile, only_metadata}; + use crate::image::Image; + use crate::testutils::OutCommand; + use std::cell::RefCell; + use std::collections::HashMap; + use std::rc::Rc; + + #[test] + fn test_exec_without_load_is_eof() { + let mut vm = Vm::new(HashMap::default()); + match vm.exec() { + StopReason::End(0) => (), + _ => panic!("Unexpected stop reason"), + } + } + + #[test] + fn test_exec_empty_image_is_eof() { + let mut vm = Vm::new(HashMap::default()); + vm.load(Image::default()); + match vm.exec() { + StopReason::End(0) => (), + _ => panic!("Unexpected stop reason"), + } + } + + #[test] + fn test_exec_empty_compilation_is_eof() { + let mut vm = Vm::new(HashMap::default()); + let image = compile(&mut b"".as_slice(), &HashMap::default()).unwrap(); + vm.load(image); + match vm.exec() { + StopReason::End(0) => (), + _ => panic!("Unexpected stop reason"), + } + } + + #[tokio::test] + async fn test_exec_upcall_flow() { + let data = Rc::from(RefCell::from(vec![])); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); + + let image = + compile(&mut b"OUT 30: OUT 20".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + + match vm.exec() { + StopReason::Upcall(_handler) => (), + _ => panic!("First exec should stop at the first upcall"), + } + assert!(data.borrow().is_empty()); + + match vm.exec() { + StopReason::Upcall(handler) => handler.invoke().await.unwrap(), + _ => panic!("Second exec should stop at the same upcall (not yet executed)"), + } + assert_eq!(["30"], *data.borrow().as_slice()); + + match vm.exec() { + StopReason::Upcall(handler) => handler.invoke().await.unwrap(), + _ => panic!("Third exec should stop at the second upcall"), + } + assert_eq!(["30", "20"], *data.borrow().as_slice()); + + match vm.exec() { + StopReason::End(0) => (), + _ => panic!("Fourth exec should stop at EOF"), + } + assert_eq!(["30", "20"], *data.borrow().as_slice()); + } + + #[tokio::test] + async fn test_exec_end_code_default() { + let mut vm = Vm::new(HashMap::default()); + let image = compile(&mut b"END".as_slice(), &HashMap::default()).unwrap(); + vm.load(image); + match vm.exec() { + StopReason::End(0) => (), + _ => panic!("Unexpected stop reason"), + } + } + + #[tokio::test] + async fn test_exec_end_code_explicit() { + let mut vm = Vm::new(HashMap::default()); + let image = compile(&mut b"END 3".as_slice(), &HashMap::default()).unwrap(); + vm.load(image); + match vm.exec() { + StopReason::End(3) => (), + _ => panic!("Unexpected stop reason"), + } + } +} diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs new file mode 100644 index 00000000..c25b763d --- /dev/null +++ b/core2/tests/integration_test.rs @@ -0,0 +1,566 @@ +// 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. + +//! Integration tests for the core language. + +mod testutils { + use async_trait::async_trait; + use endbasic_core2::*; + use std::borrow::Cow; + use std::cell::RefCell; + use std::collections::HashMap; + use std::env; + use std::ffi::OsStr; + use std::fs::{self, File}; + use std::io::{self, BufRead, BufReader, Seek, Write}; + use std::path::{Path, PathBuf}; + use std::process; + use std::rc::Rc; + use tempfile::NamedTempFile; + + /// Computes the path to the directory where this test's binary lives. + fn self_dir() -> PathBuf { + let self_exe = env::current_exe().expect("Cannot get self's executable path"); + let dir = self_exe.parent().expect("Cannot get self's directory"); + assert!(dir.ends_with("target/debug/deps") || dir.ends_with("target/release/deps")); + dir.to_owned() + } + + /// Computes the path to the source file `name`. + fn src_path(name: &str) -> PathBuf { + let test_dir = self_dir(); + let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory"); + let target_dir = debug_or_release_dir.parent().expect("Failed to get parent directory"); + let dir = target_dir.parent().expect("Failed to get parent directory"); + + // Sanity-check that we landed in the right location. + assert!(dir.join("Cargo.lock").exists()); + + dir.join(name) + } + + /// A type describing the golden data of various tests in a file. + /// + /// The first string is the test's name and the second is the input source code. + type Tests = Vec<(String, String)>; + + /// Reads the source sections of a golden test description file. + fn read_sources(path: &Path) -> io::Result { + let file = File::open(path).expect("Failed to open golden data file"); + let reader = BufReader::new(file); + + fn add_test(tests: &mut Tests, name: String, source: Option) -> io::Result<()> { + match source { + Some(source) => { + tests.push((name, source.trim_end().to_owned())); + Ok(()) + } + None => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Test case '{}' has no Source section", name), + )), + } + } + + let mut tests = vec![]; + let mut current_test = None; + let mut source: Option = None; + for line in reader.lines() { + let line = line?; + + if let Some(stripped) = line.strip_prefix("# Test: ") { + if let Some(name) = current_test.take() { + add_test(&mut tests, name, source.take())?; + } + current_test = Some(stripped.to_owned()); + source = None; + continue; + } else if line.starts_with("# ") { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unexpected section header {}", line), + )); + } else if line == "```basic" { + if current_test.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Source section without test header", + )); + } + source = Some(String::new()); + continue; + } else if line == "```" { + if let Some(name) = current_test.take() { + add_test(&mut tests, name, source.take())?; + } + source = None; + continue; + } + + if let Some(source) = source.as_mut() { + source.push_str(&line); + source.push('\n'); + } + } + + if let Some(name) = current_test { + add_test(&mut tests, name, source.take())?; + } + + if tests.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Test file '{}' has no tests", path.display()), + )); + } + + Ok(tests) + } + + #[test] + fn test_read_sources_one() -> io::Result<()> { + let mut file = NamedTempFile::new()?; + write!( + file, + "junk +# Test: first + +## Source + +```basic +First line + +Second line +``` + +## Disassembly + +```asm +foo bar +``` +" + )?; + file.flush()?; + + assert_eq!( + [("first".to_owned(), "First line\n\nSecond line".to_owned())], + read_sources(file.path())?.as_slice() + ); + + Ok(()) + } + + #[test] + fn test_read_sources_two() -> io::Result<()> { + let mut file = NamedTempFile::new()?; + write!( + file, + "junk +# Test: first + +## Source + +```basic +First line + +Second line +``` + +## Disassembly + +```asm +foo bar +``` + +# Test: second + +## Source + +```basic +The line +``` +" + )?; + file.flush()?; + + assert_eq!( + [ + ("first".to_owned(), "First line\n\nSecond line".to_owned()), + ("second".to_owned(), "The line".to_owned()), + ], + read_sources(file.path())?.as_slice() + ); + + Ok(()) + } + + /// Generates a textual diff of `golden` and `generated`. The output is meant to be useful for + /// human consumption when a test fails and is not guaranteed to be in patch format. + /// + /// Returns the empty string when the two files match. + fn diff(golden: &Path, generated: &Path) -> io::Result { + match process::Command::new("diff") + .args([OsStr::new("-u"), golden.as_os_str(), generated.as_os_str()]) + .output() + { + Ok(result) => { + let Some(code) = result.status.code() else { + return Err(io::Error::other("diff crashed")); + }; + let Ok(stdout) = String::from_utf8(result.stdout) else { + return Err(io::Error::other("diff printed non-UTF8 content to stdout")); + }; + let Ok(stderr) = String::from_utf8(result.stderr) else { + return Err(io::Error::other("diff printed non-UTF8 content to stderr")); + }; + + let mut diff = stdout; + diff.push_str(&stderr); + if code == 0 && !diff.is_empty() { + return Err(io::Error::other("diff succeeded but output is not empty")); + } else if code != 0 && diff.is_empty() { + return Err(io::Error::other("diff succeeded but output is empty")); + } + + Ok(diff) + } + + Err(e) if e.kind() == io::ErrorKind::NotFound => { + let left = fs::read_to_string(golden)?; + let right = fs::read_to_string(generated)?; + + let mut diff = String::new(); + if left != right { + diff.push_str("Golden\n"); + diff.push_str("======\n"); + diff.push_str(&left); + diff.push_str("\n\nActual\n"); + diff.push_str("======\n"); + diff.push_str(&right); + } + Ok(diff) + } + + Err(e) => Err(e), + } + } + + #[test] + fn test_diff_same() -> io::Result<()> { + let mut f1 = NamedTempFile::new()?; + let mut f2 = NamedTempFile::new()?; + + writeln!(f1, "Line 1")?; + writeln!(f1, "Line 2")?; + f1.flush()?; + f1.seek(io::SeekFrom::Start(0))?; + + writeln!(f2, "Line 1")?; + writeln!(f2, "Line 2")?; + f2.flush()?; + f2.seek(io::SeekFrom::Start(0))?; + + let diff = diff(f1.path(), f2.path())?; + assert!(diff.is_empty()); + Ok(()) + } + + #[test] + fn test_diff_different() -> io::Result<()> { + let mut f1 = NamedTempFile::new()?; + let mut f2 = NamedTempFile::new()?; + + writeln!(f1, "Line 1")?; + writeln!(f1, "Line 2")?; + f1.flush()?; + f1.seek(io::SeekFrom::Start(0))?; + + writeln!(f2, "Line 1")?; + writeln!(f2, "Line2")?; + f2.flush()?; + f2.seek(io::SeekFrom::Start(0))?; + + let diff = diff(f1.path(), f2.path())?; + assert!(!diff.is_empty()); + Ok(()) + } + + /// A command that prints its arguments to a virtual console. + struct OutCommand { + metadata: CallableMetadata, + output: Rc>, + } + + impl OutCommand { + pub fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Short), + require_one: false, + allow_missing: false, + }), + )]) + .with_category("Testing") + .with_description("Prints arguments") + .build(), + output, + }) + } + } + + #[async_trait(?Send)] + impl Callable for OutCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut line = String::new(); + let mut argi = 0; + let mut reg = 0; + loop { + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + let formatted = match etype { + ExprType::Boolean => format!("{}", scope.get_boolean(reg)), + ExprType::Double => format!("{}", scope.get_double(reg)), + ExprType::Integer => format!("{}", scope.get_integer(reg)), + ExprType::Text => scope.get_string(reg).to_string(), + }; + line.push_str(&format!("{}={}{}", argi, formatted, etype.annotation())); + sep + } + VarArgTag::Missing(sep) => { + line.push_str(&format!("{}=()", argi)); + sep + } + VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + }; + argi += 1; + reg += 1; + + if sep == ArgSep::End { + break; + } + line.push(' '); + line.push_str(&sep.to_string()); + line.push(' '); + } + let mut output = self.output.borrow_mut(); + output.push_str(&line); + output.push('\n'); + Ok(()) + } + } + + /// Given a `golden` test definition, executes its source part and writes the corresponding + /// `generated` file. The test is expected to pass when both match, but the caller is responsible + /// for checking this condition. + #[allow(clippy::write_with_newline)] + async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<()> { + let tests = read_sources(golden)?; + + let mut first = true; + for (name, source) in tests { + if !first { + write!(generated, "\n")?; + } + write!(generated, "# Test: {}\n\n", name)?; + first = false; + + write!(generated, "## Source\n\n")?; + write!(generated, "```basic\n")?; + if !source.is_empty() { + write!(generated, "{}\n", source)?; + } + write!(generated, "```\n")?; + + let console = Rc::from(RefCell::from(String::new())); + let mut upcalls_by_name: HashMap> = HashMap::default(); + upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(console.clone())); + let image = { compile(&mut source.as_bytes(), &only_metadata(&upcalls_by_name)) }; + + let image = match image { + Ok(image) => image, + Err(e) => { + write!(generated, "\n## Compilation errors\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + continue; + } + }; + + write!(generated, "\n## Disassembly\n\n")?; + write!(generated, "```asm\n")?; + for line in image.disasm() { + write!(generated, "{}\n", line)?; + } + write!(generated, "```\n")?; + + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + let mut stop: Option> = None; + while stop.is_none() { + match vm.exec() { + StopReason::End(code) => stop = Some(Ok(code)), + StopReason::Upcall(handle) => { + if let Err(e) = handle.invoke().await { + stop = Some(Err(e.to_string())); + } + } + StopReason::Exception(pos, e) => { + stop = Some(Err(format!("{}: {}", pos, e))); + } + } + } + + match stop.expect("The loop can only exit when this is set") { + Ok(0) => { + // Keep quiet in the common case. + } + Ok(i) => { + write!(generated, "\n## Exit code\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", i)?; + write!(generated, "```\n")?; + } + Err(e) => { + write!(generated, "\n## Runtime errors\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + } + } + + let console = console.borrow(); + if !console.is_empty() { + write!(generated, "\n## Output\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}", console)?; + write!(generated, "```\n")?; + } + } + + Ok(()) + } + + /// Executes the test described in the `core2/tests/.md` file. + pub(super) async fn run_one_test(name: &'static str) -> io::Result<()> { + let golden = src_path(&format!("core2/tests/{}.md", name)); + + let mut generated = NamedTempFile::new()?; + regenerate(&golden, &mut generated).await?; + generated.flush()?; + + let diff = diff(&golden, generated.path())?; + if !diff.is_empty() { + if env::var("REGEN").as_ref().map(String::as_str) == Ok("true") { + { + let mut output = File::create(golden)?; + generated.as_file_mut().seek(io::SeekFrom::Start(0))?; + io::copy(&mut generated, &mut output)?; + } + panic!("Golden data regenerated; flip REGEN back to false"); + } else { + eprintln!("{}", diff); + panic!("Test failed; see stderr for details"); + } + } + + Ok(()) + } + + #[test] + fn test_all_md_files_registered() -> io::Result<()> { + let mut registered = vec![]; + { + let file = File::open(src_path("core2/tests/integration_test.rs"))?; + let reader = BufReader::new(file); + for line in reader.lines() { + let line = line?; + + if line.starts_with(" one_test!(") { + let name = &line[" one_test!(".len()..line.len() - 2]; + registered.push(format!("{}.md", name)); + } + } + } + + // Sanity-check to ensure that the code right above actually discovered what we expect. + assert!(registered.iter().any(|s| s == "test_types.md")); + + // Make sure that every md test case definition in the file system is in the list of + // tests discovered from code scanning. We don't have to do the opposite because, if + // this program registers a md file that doesn't actually exist, the test itself will + // fail. + let mut found = vec![]; + for entry in fs::read_dir(src_path("core2/tests"))? { + let entry = entry?; + + let Ok(name) = entry.file_name().into_string() else { + continue; + }; + + #[allow(clippy::collapsible_if)] + if name.starts_with("test_") && name.ends_with(".md") { + found.push(name); + } + } + + // We want the list of tests below to be sorted, so sort the list of found tests and + // use that when comparing against the list of registered tests. + found.sort(); + + assert_eq!(registered, found); + + Ok(()) + } +} + +mod tests { + use super::testutils::*; + use std::io; + + /// Instantiates a test case for the test described in `core2/tests/.md`. + macro_rules! one_test { + ( $name:ident ) => { + #[tokio::test] + async fn $name() -> io::Result<()> { + run_one_test(stringify!($name)).await + } + }; + } + + one_test!(test_arithmetic); + one_test!(test_assignments); + one_test!(test_empty); + one_test!(test_end); + one_test!(test_functions); + one_test!(test_globals); + one_test!(test_gosub); + one_test!(test_goto); + one_test!(test_out_of_registers); + one_test!(test_strings); + one_test!(test_subs); + one_test!(test_types); + one_test!(test_varargs); +} diff --git a/core2/tests/test_arithmetic.md b/core2/tests/test_arithmetic.md new file mode 100644 index 00000000..102ec627 --- /dev/null +++ b/core2/tests/test_arithmetic.md @@ -0,0 +1,161 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 4.5 + 2.3 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADC R66, 0 # 1:5 +0002: LOADC R67, 1 # 1:11 +0003: ADDD R65, R66, R67 # 1:9 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=6.8# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 2 + 3 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R66, 2 # 1:5 +0002: LOADI R67, 3 # 1:9 +0003: ADDI R65, R66, R67 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=5% +``` + +# Test: Concatenation of two immediate strings + +## Source + +```basic +OUT "a" + "b" +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R66, 0 # 1:5 +0002: LOADI R67, 1 # 1:11 +0003: CONCAT R65, R66, R67 # 1:9 +0004: LOADI R64, 259 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=ab$ +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 + 8.3 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R66, 2 # 1:5 +0002: LOADC R67, 0 # 1:9 +0003: ITOD R66 # 1:5 +0004: ADDD R65, R66, R67 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=10.3# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 8.3 + 2 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADC R66, 0 # 1:5 +0002: LOADI R67, 2 # 1:11 +0003: ITOD R67 # 1:11 +0004: ADDD R65, R66, R67 # 1:9 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=10.3# +``` + +# Test: Integer overflow + +## Source + +```basic +a = 2147483640 + 20 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 20 # 1:18 +0003: ADDI R64, R65, R66 # 1:16 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:16: Integer overflow +``` diff --git a/core2/tests/test_assignments.md b/core2/tests/test_assignments.md new file mode 100644 index 00000000..8d062a97 --- /dev/null +++ b/core2/tests/test_assignments.md @@ -0,0 +1,210 @@ +# Test: Repeated assignment + +## Source + +```basic +a = 1 +a = 2 + +OUT a +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:5 +0002: LOADI R64, 2 # 2:5 +0003: MOVE R66, R64 # 4:5 +0004: LOADI R65, 258 # 4:5 +0005: UPCALL 0, R65 # 4:1, OUT +0006: LOADI R65, 0 # 0:0 +0007: END R65 # 0:0 +``` + +## Output + +```plain +0=2% +``` + +# Test: First assignment has annotation, second infers it + +## Source + +```basic +a$ = "foo" +OUT a + +a = "bar" +OUT a +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:6 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 259 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R64, 1 # 4:5 +0006: MOVE R66, R64 # 5:5 +0007: LOADI R65, 259 # 5:5 +0008: UPCALL 0, R65 # 5:1, OUT +0009: LOADI R65, 0 # 0:0 +0010: END R65 # 0:0 +``` + +## Output + +```plain +0=foo$ +0=bar$ +``` + +# Test: First assignment infers type, second has annotation + +## Source + +```basic +a = "foo" +OUT a + +a$ = "bar" +OUT a +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 259 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R64, 1 # 4:6 +0006: MOVE R66, R64 # 5:5 +0007: LOADI R65, 259 # 5:5 +0008: UPCALL 0, R65 # 5:1, OUT +0009: LOADI R65, 0 # 0:0 +0010: END R65 # 0:0 +``` + +## Output + +```plain +0=foo$ +0=bar$ +``` + +# Test: Annotation mismatch after assignment + +## Source + +```basic +a# = 4.5 +a$ = "foo" +``` + +## Compilation errors + +```plain +2:1: Incompatible type annotation in a$ reference +``` + +# Test: Value type does not match annotation on first assignment + +## Source + +```basic +d$ = 3.4 +``` + +## Compilation errors + +```plain +1:6: Cannot assign value of type DOUBLE to variable of type STRING +``` + +# Test: Value type does not match target type after first definition + +## Source + +```basic +a = 4.5 +a = "foo" +``` + +## Compilation errors + +```plain +2:5: STRING is not a number +``` + +# Test: Integer to double promotion + +## Source + +```basic +d# = 6 +OUT d +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 6 # 1:6 +0002: ITOD R64 # 1:6 +0003: MOVE R66, R64 # 2:5 +0004: LOADI R65, 257 # 2:5 +0005: UPCALL 0, R65 # 2:1, OUT +0006: LOADI R65, 0 # 0:0 +0007: END R65 # 0:0 +``` + +## Output + +```plain +0=6# +``` + +# Test: Double to integer demotion + +## Source + +```basic +i1 = 0 +i1 = 3.2 + +i2 = 0 +i2 = 3.7 + +OUT i1, i2 +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 0 # 1:6 +0002: LOADC R64, 0 # 2:6 +0003: DTOI R64 # 2:6 +0004: LOADI R65, 0 # 4:6 +0005: LOADC R65, 1 # 5:6 +0006: DTOI R65 # 5:6 +0007: MOVE R67, R64 # 7:5 +0008: LOADI R66, 290 # 7:5 +0009: MOVE R69, R65 # 7:9 +0010: LOADI R68, 258 # 7:9 +0011: UPCALL 0, R66 # 7:1, OUT +0012: LOADI R66, 0 # 0:0 +0013: END R66 # 0:0 +``` + +## Output + +```plain +0=3% , 1=4% +``` diff --git a/core2/tests/test_empty.md b/core2/tests/test_empty.md new file mode 100644 index 00000000..b785b73c --- /dev/null +++ b/core2/tests/test_empty.md @@ -0,0 +1,14 @@ +# Test: Nothing to do + +## Source + +```basic +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 0:0 +0002: END R64 # 0:0 +``` diff --git a/core2/tests/test_end.md b/core2/tests/test_end.md new file mode 100644 index 00000000..15b11d78 --- /dev/null +++ b/core2/tests/test_end.md @@ -0,0 +1,134 @@ +# Test: Call to END and nothing else + +## Source + +```basic +END +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 1:1 +0002: END R64 # 1:1 +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +# Test: Exit code is an integer immediate + +## Source + +```basic +END 42 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 42 # 1:5 +0002: END R64 # 1:1 +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Exit code + +```plain +42 +``` + +# Test: Exit code is a double immediate and needs demotion + +## Source + +```basic +END 43.98 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADC R64, 0 # 1:5 +0002: DTOI R64 # 1:5 +0003: END R64 # 1:1 +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 +``` + +## Exit code + +```plain +44 +``` + +# Test: Exit code is in a global variable + +## Source + +```basic +DIM SHARED i +i = 5 +END i +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R0, 5 # 2:5 +0003: MOVE R64, R0 # 3:5 +0004: END R64 # 3:1 +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Exit code + +```plain +5 +``` + +# Test: Exit code is in a local variable + +## Source + +```basic +i = 3 +END i +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:5 +0003: END R65 # 2:1 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Exit code + +```plain +3 +``` + +# Test: Exit code is of an invalid type + +## Source + +```basic +END "foo" +``` + +## Compilation errors + +```plain +1:5: STRING is not a number +``` diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md new file mode 100644 index 00000000..436620b0 --- /dev/null +++ b/core2/tests/test_functions.md @@ -0,0 +1,265 @@ +# Test: Return value matches function type + +## Source + +```basic +FUNCTION foo$ + foo = "abc" +END FUNCTION + +OUT foo$(2) +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: CALL R65, 6 # 5:5, FOO +0002: LOADI R64, 259 # 5:5 +0003: UPCALL 0, R64 # 5:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 + +-- FOO +0006: ENTER 1 # 0:0 +0007: LOADI R64, 0 # 2:11 +0008: RETURN # 3:1 +``` + +## Output + +```plain +0=abc$ +``` + +# Test: Type mismatch in return value + +## Source + +```basic +FUNCTION foo$ + foo = 3 +END FUNCTION + +OUT foo$(2) +``` + +## Compilation errors + +```plain +2:11: Cannot assign value of type INTEGER to variable of type STRING +``` + +# Test: Elaborate execution flow + +## Source + +```basic +a = 10 + +FUNCTION foo + a = 20 + OUT "Inside", a + foo = 30 +END FUNCTION + +OUT "Before", a +OUT "Return value", foo(2) +OUT "After", a +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 10 # 1:5 +0002: LOADI R66, 0 # 9:5 +0003: LOADI R65, 291 # 9:5 +0004: MOVE R68, R64 # 9:15 +0005: LOADI R67, 258 # 9:15 +0006: UPCALL 0, R65 # 9:1, OUT +0007: LOADI R66, 1 # 10:5 +0008: LOADI R65, 291 # 10:5 +0009: CALL R68, 19 # 10:21, FOO +0010: LOADI R67, 258 # 10:21 +0011: UPCALL 0, R65 # 10:1, OUT +0012: LOADI R66, 2 # 11:5 +0013: LOADI R65, 291 # 11:5 +0014: MOVE R68, R64 # 11:14 +0015: LOADI R67, 258 # 11:14 +0016: UPCALL 0, R65 # 11:1, OUT +0017: LOADI R65, 0 # 0:0 +0018: END R65 # 0:0 + +-- FOO +0019: ENTER 6 # 0:0 +0020: LOADI R65, 20 # 4:9 +0021: LOADI R67, 3 # 5:9 +0022: LOADI R66, 291 # 5:9 +0023: MOVE R69, R65 # 5:19 +0024: LOADI R68, 258 # 5:19 +0025: UPCALL 0, R66 # 5:5, OUT +0026: LOADI R64, 30 # 6:11 +0027: RETURN # 7:1 +``` + +## Output + +```plain +0=Before$ , 1=10% +0=Inside$ , 1=20% +0=Return value$ , 1=30% +0=After$ , 1=10% +``` + +# Test: Function call requires jumping backwards + +## Source + +```basic +FUNCTION first + OUT "first" + first = 123 +END FUNCTION + +FUNCTION second + second = first(0) +END FUNCTION + +OUT second(0) +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: CALL R65, 12 # 10:5, SECOND +0002: LOADI R64, 258 # 10:5 +0003: UPCALL 0, R64 # 10:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 + +-- FIRST +0006: ENTER 3 # 0:0 +0007: LOADI R66, 0 # 2:9 +0008: LOADI R65, 259 # 2:9 +0009: UPCALL 0, R65 # 2:5, OUT +0010: LOADI R64, 123 # 3:13 +0011: RETURN # 4:1 + +-- SECOND +0012: ENTER 1 # 0:0 +0013: CALL R64, 6 # 7:14, FIRST +0014: RETURN # 8:1 +``` + +## Output + +```plain +0=first$ +0=123% +``` + +# Test: Local variables + +## Source + +```basic +FUNCTION modify_2 + var = 2 + OUT "Inside modify_2", var +END FUNCTION + +FUNCTION modify_1 + var = 1 + OUT "Before modify_2", var + OUT modify_2(2) + OUT "After modify_2", var +END FUNCTION + +var = 0 +OUT "Before modify_1", var +OUT modify_1(2) +OUT "After modify_1", var +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 0 # 13:7 +0002: LOADI R66, 0 # 14:5 +0003: LOADI R65, 291 # 14:5 +0004: MOVE R68, R64 # 14:24 +0005: LOADI R67, 258 # 14:24 +0006: UPCALL 0, R65 # 14:1, OUT +0007: CALL R66, 25 # 15:5, MODIFY_1 +0008: LOADI R65, 258 # 15:5 +0009: UPCALL 0, R65 # 15:1, OUT +0010: LOADI R66, 1 # 16:5 +0011: LOADI R65, 291 # 16:5 +0012: MOVE R68, R64 # 16:23 +0013: LOADI R67, 258 # 16:23 +0014: UPCALL 0, R65 # 16:1, OUT +0015: LOADI R65, 0 # 0:0 +0016: END R65 # 0:0 + +-- MODIFY_2 +0017: ENTER 6 # 0:0 +0018: LOADI R65, 2 # 2:11 +0019: LOADI R67, 2 # 3:9 +0020: LOADI R66, 291 # 3:9 +0021: MOVE R69, R65 # 3:28 +0022: LOADI R68, 258 # 3:28 +0023: UPCALL 0, R66 # 3:5, OUT +0024: RETURN # 4:1 + +-- MODIFY_1 +0025: ENTER 6 # 0:0 +0026: LOADI R65, 1 # 7:11 +0027: LOADI R67, 3 # 8:9 +0028: LOADI R66, 291 # 8:9 +0029: MOVE R69, R65 # 8:28 +0030: LOADI R68, 258 # 8:28 +0031: UPCALL 0, R66 # 8:5, OUT +0032: CALL R67, 17 # 9:9, MODIFY_2 +0033: LOADI R66, 258 # 9:9 +0034: UPCALL 0, R66 # 9:5, OUT +0035: LOADI R67, 4 # 10:9 +0036: LOADI R66, 291 # 10:9 +0037: MOVE R69, R65 # 10:27 +0038: LOADI R68, 258 # 10:27 +0039: UPCALL 0, R66 # 10:5, OUT +0040: RETURN # 11:1 +``` + +## Output + +```plain +0=Before modify_1$ , 1=0% +0=Before modify_2$ , 1=1% +0=Inside modify_2$ , 1=2% +0=0% +0=After modify_2$ , 1=1% +0=0% +0=After modify_1$ , 1=0% +``` + +# Test: Local is not global + +## Source + +```basic +FUNCTION set_local + local_var = 8 +END FUNCTION + +OUT set_local(2) +OUT local_var +``` + +## Compilation errors + +```plain +6:5: Undefined global symbol local_var +``` diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md new file mode 100644 index 00000000..6d7cb17b --- /dev/null +++ b/core2/tests/test_globals.md @@ -0,0 +1,295 @@ +# Test: Default zero values + +## Source + +```basic +DIM SHARED b1 AS BOOLEAN +DIM SHARED d1 AS DOUBLE +DIM SHARED i1 AS INTEGER +DIM SHARED i2 +DIM SHARED s1 AS STRING + +OUT b1, d1, i1, i2, s1 +``` + +## Disassembly + +```asm +0000: ENTER 10 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R1, 0 # 2:12 +0003: LOADI R2, 0 # 3:12 +0004: LOADI R3, 0 # 4:12 +0005: ALLOC R4, STRING # 5:12 +0006: MOVE R65, R0 # 7:5 +0007: LOADI R64, 288 # 7:5 +0008: MOVE R67, R1 # 7:9 +0009: LOADI R66, 289 # 7:9 +0010: MOVE R69, R2 # 7:13 +0011: LOADI R68, 290 # 7:13 +0012: MOVE R71, R3 # 7:17 +0013: LOADI R70, 290 # 7:17 +0014: MOVE R73, R4 # 7:21 +0015: LOADI R72, 259 # 7:21 +0016: UPCALL 0, R64 # 7:1, OUT +0017: LOADI R64, 0 # 0:0 +0018: END R64 # 0:0 +``` + +## Output + +```plain +0=false? , 1=0# , 2=0% , 3=0% , 4=$ +``` + +# Test: Set global variables to explicit values + +## Source + +```basic +DIM SHARED b1 AS BOOLEAN +DIM SHARED d1 AS DOUBLE +DIM SHARED i1 AS INTEGER +DIM SHARED i2 +DIM SHARED s1 AS STRING + +b1 = TRUE +d1 = 2.3 +i1 = 5 +i2 = 7 +s1 = "text" +OUT b1, d1, i1, i2, s1 +``` + +## Disassembly + +```asm +0000: ENTER 10 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R1, 0 # 2:12 +0003: LOADI R2, 0 # 3:12 +0004: LOADI R3, 0 # 4:12 +0005: ALLOC R4, STRING # 5:12 +0006: LOADI R0, 1 # 7:6 +0007: LOADC R1, 0 # 8:6 +0008: LOADI R2, 5 # 9:6 +0009: LOADI R3, 7 # 10:6 +0010: LOADI R4, 1 # 11:6 +0011: MOVE R65, R0 # 12:5 +0012: LOADI R64, 288 # 12:5 +0013: MOVE R67, R1 # 12:9 +0014: LOADI R66, 289 # 12:9 +0015: MOVE R69, R2 # 12:13 +0016: LOADI R68, 290 # 12:13 +0017: MOVE R71, R3 # 12:17 +0018: LOADI R70, 290 # 12:17 +0019: MOVE R73, R4 # 12:21 +0020: LOADI R72, 259 # 12:21 +0021: UPCALL 0, R64 # 12:1, OUT +0022: LOADI R64, 0 # 0:0 +0023: END R64 # 0:0 +``` + +## Output + +```plain +0=true? , 1=2.3# , 2=5% , 3=7% , 4=text$ +``` + +# Test: Global variables are accessible in functions + +## Source + +```basic +FUNCTION modify_global + i1 = 3 + OUT "Inside after", i1 +END FUNCTION + +DIM SHARED i1 +i1 = 2 +OUT "Before", i1 +OUT modify_global(2) +OUT "After", i1 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R0, 0 # 6:12 +0002: LOADI R0, 2 # 7:6 +0003: LOADI R65, 0 # 8:5 +0004: LOADI R64, 291 # 8:5 +0005: MOVE R67, R0 # 8:15 +0006: LOADI R66, 258 # 8:15 +0007: UPCALL 0, R64 # 8:1, OUT +0008: CALL R65, 18 # 9:5, MODIFY_GLOBAL +0009: LOADI R64, 258 # 9:5 +0010: UPCALL 0, R64 # 9:1, OUT +0011: LOADI R65, 1 # 10:5 +0012: LOADI R64, 291 # 10:5 +0013: MOVE R67, R0 # 10:14 +0014: LOADI R66, 258 # 10:14 +0015: UPCALL 0, R64 # 10:1, OUT +0016: LOADI R64, 0 # 0:0 +0017: END R64 # 0:0 + +-- MODIFY_GLOBAL +0018: ENTER 5 # 0:0 +0019: LOADI R0, 3 # 2:10 +0020: LOADI R66, 2 # 3:9 +0021: LOADI R65, 291 # 3:9 +0022: MOVE R68, R0 # 3:25 +0023: LOADI R67, 258 # 3:25 +0024: UPCALL 0, R65 # 3:5, OUT +0025: RETURN # 4:1 +``` + +## Output + +```plain +0=Before$ , 1=2% +0=Inside after$ , 1=3% +0=0% +0=After$ , 1=3% +``` + +# Test: Global variables are accessible in subroutines + +## Source + +```basic +SUB modify_global + i1 = 3 + OUT "Inside after", i1 +END SUB + +DIM SHARED i1 +i1 = 2 +OUT "Before", i1 +modify_global +OUT "After", i1 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R0, 0 # 6:12 +0002: LOADI R0, 2 # 7:6 +0003: LOADI R65, 0 # 8:5 +0004: LOADI R64, 291 # 8:5 +0005: MOVE R67, R0 # 8:15 +0006: LOADI R66, 258 # 8:15 +0007: UPCALL 0, R64 # 8:1, OUT +0008: LOADI R64, 0 # 9:1 +0009: CALL R64, 17 # 9:1, MODIFY_GLOBAL +0010: LOADI R65, 1 # 10:5 +0011: LOADI R64, 291 # 10:5 +0012: MOVE R67, R0 # 10:14 +0013: LOADI R66, 258 # 10:14 +0014: UPCALL 0, R64 # 10:1, OUT +0015: LOADI R64, 0 # 0:0 +0016: END R64 # 0:0 + +-- MODIFY_GLOBAL +0017: ENTER 4 # 0:0 +0018: LOADI R0, 3 # 2:10 +0019: LOADI R65, 2 # 3:9 +0020: LOADI R64, 291 # 3:9 +0021: MOVE R67, R0 # 3:25 +0022: LOADI R66, 258 # 3:25 +0023: UPCALL 0, R64 # 3:5, OUT +0024: RETURN # 4:1 +``` + +## Output + +```plain +0=Before$ , 1=2% +0=Inside after$ , 1=3% +0=After$ , 1=3% +``` + +# Test: Integer to double promotion + +## Source + +```basic +DIM SHARED d AS DOUBLE +d = 6 +OUT d +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R0, 6 # 2:5 +0003: ITOD R0 # 2:5 +0004: MOVE R65, R0 # 3:5 +0005: LOADI R64, 257 # 3:5 +0006: UPCALL 0, R64 # 3:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=6# +``` + +# Test: Double to integer demotion + +## Source + +```basic +DIM SHARED i1 AS INTEGER +DIM SHARED i2 AS INTEGER +i1 = 3.2 +i2 = 3.7 +OUT i1, i2 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R1, 0 # 2:12 +0003: LOADC R0, 0 # 3:6 +0004: DTOI R0 # 3:6 +0005: LOADC R1, 1 # 4:6 +0006: DTOI R1 # 4:6 +0007: MOVE R65, R0 # 5:5 +0008: LOADI R64, 290 # 5:5 +0009: MOVE R67, R1 # 5:9 +0010: LOADI R66, 258 # 5:9 +0011: UPCALL 0, R64 # 5:1, OUT +0012: LOADI R64, 0 # 0:0 +0013: END R64 # 0:0 +``` + +## Output + +```plain +0=3% , 1=4% +``` + +# Test: Type mismatch in global variable assignment + +## Source + +```basic +DIM SHARED d +d = "foo" +``` + +## Compilation errors + +```plain +2:5: STRING is not a number +``` diff --git a/core2/tests/test_gosub.md b/core2/tests/test_gosub.md new file mode 100644 index 00000000..2cf8d1c8 --- /dev/null +++ b/core2/tests/test_gosub.md @@ -0,0 +1,177 @@ +# Test: Basic flow + +## Source + +```basic +OUT "a" +GOSUB @sub +OUT "c" +END + +@sub: +OUT "b" +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R64, 259 # 1:5 +0003: UPCALL 0, R64 # 1:1, OUT +0004: GOSUB 10 # 2:7 +0005: LOADI R65, 1 # 3:5 +0006: LOADI R64, 259 # 3:5 +0007: UPCALL 0, R64 # 3:1, OUT +0008: LOADI R64, 0 # 4:1 +0009: END R64 # 4:1 +0010: LOADI R65, 2 # 7:5 +0011: LOADI R64, 259 # 7:5 +0012: UPCALL 0, R64 # 7:1, OUT +0013: RETURN # 8:1 +0014: LOADI R64, 0 # 0:0 +0015: END R64 # 0:0 +``` + +## Output + +```plain +0=a$ +0=b$ +0=c$ +``` + +# Test: Nested GOSUB calls and returns + +## Source + +```basic +OUT "a" +GOSUB @sub1 +OUT "f" +END + +@sub1: +OUT "b" +GOSUB @sub2 +OUT "e" +RETURN + +@sub2: +OUT "c" +GOSUB @sub3 +OUT "d" +RETURN + +@sub3: +OUT "hello" +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R64, 259 # 1:5 +0003: UPCALL 0, R64 # 1:1, OUT +0004: GOSUB 10 # 2:7 +0005: LOADI R65, 1 # 3:5 +0006: LOADI R64, 259 # 3:5 +0007: UPCALL 0, R64 # 3:1, OUT +0008: LOADI R64, 0 # 4:1 +0009: END R64 # 4:1 +0010: LOADI R65, 2 # 7:5 +0011: LOADI R64, 259 # 7:5 +0012: UPCALL 0, R64 # 7:1, OUT +0013: GOSUB 18 # 8:7 +0014: LOADI R65, 3 # 9:5 +0015: LOADI R64, 259 # 9:5 +0016: UPCALL 0, R64 # 9:1, OUT +0017: RETURN # 10:1 +0018: LOADI R65, 4 # 13:5 +0019: LOADI R64, 259 # 13:5 +0020: UPCALL 0, R64 # 13:1, OUT +0021: GOSUB 26 # 14:7 +0022: LOADI R65, 5 # 15:5 +0023: LOADI R64, 259 # 15:5 +0024: UPCALL 0, R64 # 15:1, OUT +0025: RETURN # 16:1 +0026: LOADI R65, 6 # 19:5 +0027: LOADI R64, 259 # 19:5 +0028: UPCALL 0, R64 # 19:1, OUT +0029: RETURN # 20:1 +0030: LOADI R64, 0 # 0:0 +0031: END R64 # 0:0 +``` + +## Output + +```plain +0=a$ +0=b$ +0=c$ +0=hello$ +0=d$ +0=e$ +0=f$ +``` + +# Test: RETURN without GOSUB + +## Source + +```basic +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: RETURN # 1:1 +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 +``` + +## Runtime errors + +```plain +1:1: RETURN without GOSUB or FUNCTION call +``` + +# Test: GOSUB target requires backwards jump + +## Source + +```basic +GOTO @skip + +@s: +OUT "In target" +RETURN + +@skip: +GOSUB @s +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 6 # 1:6 +0002: LOADI R65, 0 # 4:5 +0003: LOADI R64, 259 # 4:5 +0004: UPCALL 0, R64 # 4:1, OUT +0005: RETURN # 5:1 +0006: GOSUB 2 # 8:7 +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=In target$ +``` diff --git a/core2/tests/test_goto.md b/core2/tests/test_goto.md new file mode 100644 index 00000000..61232983 --- /dev/null +++ b/core2/tests/test_goto.md @@ -0,0 +1,96 @@ +# Test: Basic flow + +## Source + +```basic +OUT "a" +GOTO @foo +OUT "b" +@foo: +OUT "c" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R64, 259 # 1:5 +0003: UPCALL 0, R64 # 1:1, OUT +0004: JUMP 8 # 2:6 +0005: LOADI R65, 1 # 3:5 +0006: LOADI R64, 259 # 3:5 +0007: UPCALL 0, R64 # 3:1, OUT +0008: LOADI R65, 2 # 5:5 +0009: LOADI R64, 259 # 5:5 +0010: UPCALL 0, R64 # 5:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=a$ +0=c$ +``` + +# Test: GOTO jumps to label at end of file. + +## Source + +```basic +GOTO @end +OUT "a" +@end: +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 5 # 1:6 +0002: LOADI R65, 0 # 2:5 +0003: LOADI R64, 259 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +# Test: GOTO target requires backwards jump + +## Source + +```basic +GOTO @skip +OUT "Skipped" +@print_it: +OUT "Print something" +GOTO @end +@skip: +GOTO @print_it +@end: +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 9 # 1:6 +0002: LOADI R65, 0 # 2:5 +0003: LOADI R64, 259 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R65, 1 # 4:5 +0006: LOADI R64, 259 # 4:5 +0007: UPCALL 0, R64 # 4:1, OUT +0008: JUMP 10 # 5:6 +0009: JUMP 5 # 7:6 +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=Print something$ +``` diff --git a/core2/tests/test_out_of_registers.md b/core2/tests/test_out_of_registers.md new file mode 100644 index 00000000..9fc42ebe --- /dev/null +++ b/core2/tests/test_out_of_registers.md @@ -0,0 +1,359 @@ +# Test: Out of global registers + +## Source + +```basic +DIM SHARED g000 +DIM SHARED g001 +DIM SHARED g002 +DIM SHARED g003 +DIM SHARED g004 +DIM SHARED g005 +DIM SHARED g006 +DIM SHARED g007 +DIM SHARED g008 +DIM SHARED g009 + +DIM SHARED g010 +DIM SHARED g011 +DIM SHARED g012 +DIM SHARED g013 +DIM SHARED g014 +DIM SHARED g015 +DIM SHARED g016 +DIM SHARED g017 +DIM SHARED g018 +DIM SHARED g019 + +DIM SHARED g020 +DIM SHARED g021 +DIM SHARED g022 +DIM SHARED g023 +DIM SHARED g024 +DIM SHARED g025 +DIM SHARED g026 +DIM SHARED g027 +DIM SHARED g028 +DIM SHARED g029 + +DIM SHARED g030 +DIM SHARED g031 +DIM SHARED g032 +DIM SHARED g033 +DIM SHARED g034 +DIM SHARED g035 +DIM SHARED g036 +DIM SHARED g037 +DIM SHARED g038 +DIM SHARED g039 + +DIM SHARED g040 +DIM SHARED g041 +DIM SHARED g042 +DIM SHARED g043 +DIM SHARED g044 +DIM SHARED g045 +DIM SHARED g046 +DIM SHARED g047 +DIM SHARED g048 +DIM SHARED g049 + +DIM SHARED g050 +DIM SHARED g051 +DIM SHARED g052 +DIM SHARED g053 +DIM SHARED g054 +DIM SHARED g055 +DIM SHARED g056 +DIM SHARED g057 +DIM SHARED g058 +DIM SHARED g059 + +DIM SHARED g060 +DIM SHARED g061 +DIM SHARED g062 +DIM SHARED g063 +DIM SHARED g064 +DIM SHARED g065 +DIM SHARED g066 +DIM SHARED g067 +DIM SHARED g068 +DIM SHARED g069 + +DIM SHARED g070 +DIM SHARED g071 +DIM SHARED g072 +DIM SHARED g073 +DIM SHARED g074 +DIM SHARED g075 +DIM SHARED g076 +DIM SHARED g077 +DIM SHARED g078 +DIM SHARED g079 +``` + +## Compilation errors + +```plain +71:12: Out of global registers +``` + +# Test: Out of local registers + +## Source + +```basic +l000 = 0 +l001 = 0 +l002 = 0 +l003 = 0 +l004 = 0 +l005 = 0 +l006 = 0 +l007 = 0 +l008 = 0 +l009 = 0 + +l010 = 0 +l011 = 0 +l012 = 0 +l013 = 0 +l014 = 0 +l015 = 0 +l016 = 0 +l017 = 0 +l018 = 0 +l019 = 0 + +l020 = 0 +l021 = 0 +l022 = 0 +l023 = 0 +l024 = 0 +l025 = 0 +l026 = 0 +l027 = 0 +l028 = 0 +l029 = 0 + +l030 = 0 +l031 = 0 +l032 = 0 +l033 = 0 +l034 = 0 +l035 = 0 +l036 = 0 +l037 = 0 +l038 = 0 +l039 = 0 + +l040 = 0 +l041 = 0 +l042 = 0 +l043 = 0 +l044 = 0 +l045 = 0 +l046 = 0 +l047 = 0 +l048 = 0 +l049 = 0 + +l050 = 0 +l051 = 0 +l052 = 0 +l053 = 0 +l054 = 0 +l055 = 0 +l056 = 0 +l057 = 0 +l058 = 0 +l059 = 0 + +l060 = 0 +l061 = 0 +l062 = 0 +l063 = 0 +l064 = 0 +l065 = 0 +l066 = 0 +l067 = 0 +l068 = 0 +l069 = 0 + +l070 = 0 +l071 = 0 +l072 = 0 +l073 = 0 +l074 = 0 +l075 = 0 +l076 = 0 +l077 = 0 +l078 = 0 +l079 = 0 + +l080 = 0 +l081 = 0 +l082 = 0 +l083 = 0 +l084 = 0 +l085 = 0 +l086 = 0 +l087 = 0 +l088 = 0 +l089 = 0 + +l090 = 0 +l091 = 0 +l092 = 0 +l093 = 0 +l094 = 0 +l095 = 0 +l096 = 0 +l097 = 0 +l098 = 0 +l099 = 0 + +l100 = 0 +l101 = 0 +l102 = 0 +l103 = 0 +l104 = 0 +l105 = 0 +l106 = 0 +l107 = 0 +l108 = 0 +l109 = 0 + +l110 = 0 +l111 = 0 +l112 = 0 +l113 = 0 +l114 = 0 +l115 = 0 +l116 = 0 +l117 = 0 +l118 = 0 +l119 = 0 + +l120 = 0 +l121 = 0 +l122 = 0 +l123 = 0 +l124 = 0 +l125 = 0 +l126 = 0 +l127 = 0 +l128 = 0 +l129 = 0 + +l130 = 0 +l131 = 0 +l132 = 0 +l133 = 0 +l134 = 0 +l135 = 0 +l136 = 0 +l137 = 0 +l138 = 0 +l139 = 0 + +l140 = 0 +l141 = 0 +l142 = 0 +l143 = 0 +l144 = 0 +l145 = 0 +l146 = 0 +l147 = 0 +l148 = 0 +l149 = 0 + +l150 = 0 +l151 = 0 +l152 = 0 +l153 = 0 +l154 = 0 +l155 = 0 +l156 = 0 +l157 = 0 +l158 = 0 +l159 = 0 + +l160 = 0 +l161 = 0 +l162 = 0 +l163 = 0 +l164 = 0 +l165 = 0 +l166 = 0 +l167 = 0 +l168 = 0 +l169 = 0 + +l170 = 0 +l171 = 0 +l172 = 0 +l173 = 0 +l174 = 0 +l175 = 0 +l176 = 0 +l177 = 0 +l178 = 0 +l179 = 0 + +l180 = 0 +l181 = 0 +l182 = 0 +l183 = 0 +l184 = 0 +l185 = 0 +l186 = 0 +l187 = 0 +l188 = 0 +l189 = 0 + +l190 = 0 +l191 = 0 +l192 = 0 +l193 = 0 +l194 = 0 +l195 = 0 +l196 = 0 +l197 = 0 +l198 = 0 +l199 = 0 +``` + +## Compilation errors + +```plain +212:1: Out of local registers +``` + +# Test: Out of temporary registers in final assignment + +## Source + +```basic +result = 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 + 120 + 121 + 122 + 123 + 124 + 125 + 126 + 127 + 128 + 129 + 130 + 131 + 132 + 133 + 134 + 135 + 136 + 137 + 138 + 139 + 140 + 141 + 142 + 143 + 144 + 145 + 146 + 147 + 148 + 149 + 150 + 151 + 152 + 153 + 154 + 155 + 156 + 157 + 158 + 159 + 160 + 161 + 162 + 163 + 164 + 165 + 166 + 167 + 168 + 169 + 170 + 171 + 172 + 173 + 174 + 175 + 176 + 177 + 178 + 179 + 180 + 181 + 182 + 183 + 184 + 185 + 186 + 187 + 188 + 189 + 190 + 191 +``` + +## Compilation errors + +```plain +1:14: Out of temp registers +``` + +# Test: Out of temporary registers in expression evaluation + +## Source + +```basic +result = 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 + 120 + 121 + 122 + 123 + 124 + 125 + 126 + 127 + 128 + 129 + 130 + 131 + 132 + 133 + 134 + 135 + 136 + 137 + 138 + 139 + 140 + 141 + 142 + 143 + 144 + 145 + 146 + 147 + 148 + 149 + 150 + 151 + 152 + 153 + 154 + 155 + 156 + 157 + 158 + 159 + 160 + 161 + 162 + 163 + 164 + 165 + 166 + 167 + 168 + 169 + 170 + 171 + 172 + 173 + 174 + 175 + 176 + 177 + 178 + 179 + 180 + 181 + 182 + 183 + 184 + 185 + 186 + 187 + 188 + 189 + 190 + 191 + 192 +``` + +## Compilation errors + +```plain +1:10: Out of temp registers +``` diff --git a/core2/tests/test_strings.md b/core2/tests/test_strings.md new file mode 100644 index 00000000..2c450cc1 --- /dev/null +++ b/core2/tests/test_strings.md @@ -0,0 +1,53 @@ +# Test: Concatenation + +## Source + +```basic +c1 = "Constant string 1" +c2 = "Constant string 2" + +c3 = c1 + c2 +c4 = c3 + "." + +OUT c1 +OUT c2 +OUT c3 +OUT c4 +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 0 # 1:6 +0002: LOADI R65, 1 # 2:6 +0003: MOVE R67, R64 # 4:6 +0004: MOVE R68, R65 # 4:11 +0005: CONCAT R66, R67, R68 # 4:9 +0006: MOVE R68, R66 # 5:6 +0007: LOADI R69, 2 # 5:11 +0008: CONCAT R67, R68, R69 # 5:9 +0009: MOVE R69, R64 # 7:5 +0010: LOADI R68, 259 # 7:5 +0011: UPCALL 0, R68 # 7:1, OUT +0012: MOVE R69, R65 # 8:5 +0013: LOADI R68, 259 # 8:5 +0014: UPCALL 0, R68 # 8:1, OUT +0015: MOVE R69, R66 # 9:5 +0016: LOADI R68, 259 # 9:5 +0017: UPCALL 0, R68 # 9:1, OUT +0018: MOVE R69, R67 # 10:5 +0019: LOADI R68, 259 # 10:5 +0020: UPCALL 0, R68 # 10:1, OUT +0021: LOADI R68, 0 # 0:0 +0022: END R68 # 0:0 +``` + +## Output + +```plain +0=Constant string 1$ +0=Constant string 2$ +0=Constant string 1Constant string 2$ +0=Constant string 1Constant string 2.$ +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md new file mode 100644 index 00000000..cb8c4f53 --- /dev/null +++ b/core2/tests/test_subs.md @@ -0,0 +1,214 @@ +# Test: Elaborate execution flow + +## Source + +```basic +a = 10 + +SUB foo + a = 20 + OUT "Inside", a +END SUB + +OUT "Before", a +foo +OUT "After", a +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 10 # 1:5 +0002: LOADI R66, 0 # 8:5 +0003: LOADI R65, 291 # 8:5 +0004: MOVE R68, R64 # 8:15 +0005: LOADI R67, 258 # 8:15 +0006: UPCALL 0, R65 # 8:1, OUT +0007: LOADI R65, 0 # 9:1 +0008: CALL R65, 16 # 9:1, FOO +0009: LOADI R66, 1 # 10:5 +0010: LOADI R65, 291 # 10:5 +0011: MOVE R68, R64 # 10:14 +0012: LOADI R67, 258 # 10:14 +0013: UPCALL 0, R65 # 10:1, OUT +0014: LOADI R65, 0 # 0:0 +0015: END R65 # 0:0 + +-- FOO +0016: ENTER 5 # 0:0 +0017: LOADI R64, 20 # 4:9 +0018: LOADI R66, 2 # 5:9 +0019: LOADI R65, 291 # 5:9 +0020: MOVE R68, R64 # 5:19 +0021: LOADI R67, 258 # 5:19 +0022: UPCALL 0, R65 # 5:5, OUT +0023: RETURN # 6:1 +``` + +## Output + +```plain +0=Before$ , 1=10% +0=Inside$ , 1=20% +0=After$ , 1=10% +``` + +# Test: Subroutine call requires jumping backwards + +## Source + +```basic +SUB first + OUT "first" +END SUB + +SUB second + first +END SUB + +second +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 9:1 +0002: CALL R64, 10 # 9:1, SECOND +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 + +-- FIRST +0005: ENTER 2 # 0:0 +0006: LOADI R65, 0 # 2:9 +0007: LOADI R64, 259 # 2:9 +0008: UPCALL 0, R64 # 2:5, OUT +0009: RETURN # 3:1 + +-- SECOND +0010: ENTER 1 # 0:0 +0011: LOADI R64, 0 # 6:5 +0012: CALL R64, 5 # 6:5, FIRST +0013: RETURN # 7:1 +``` + +## Output + +```plain +0=first$ +``` + +# Test: Annotation not allowed in subroutine call + +## Source + +```basic +OUT$ 3 +``` + +## Compilation errors + +```plain +1:1: Type annotation not allowed in OUT$ +``` + +# Test: Local variables + +## Source + +```basic +SUB modify_2 + var = 2 + OUT "Inside modify_2", var +END SUB + +SUB modify_1 + var = 1 + OUT "Before modify_2", var + modify_2 + OUT "After modify_2", var +END SUB + +var = 0 +OUT "Before modify_1", var +modify_1 +OUT "After modify_1", var +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 0 # 13:7 +0002: LOADI R66, 0 # 14:5 +0003: LOADI R65, 291 # 14:5 +0004: MOVE R68, R64 # 14:24 +0005: LOADI R67, 258 # 14:24 +0006: UPCALL 0, R65 # 14:1, OUT +0007: LOADI R65, 0 # 15:1 +0008: CALL R65, 24 # 15:1, MODIFY_1 +0009: LOADI R66, 1 # 16:5 +0010: LOADI R65, 291 # 16:5 +0011: MOVE R68, R64 # 16:23 +0012: LOADI R67, 258 # 16:23 +0013: UPCALL 0, R65 # 16:1, OUT +0014: LOADI R65, 0 # 0:0 +0015: END R65 # 0:0 + +-- MODIFY_2 +0016: ENTER 5 # 0:0 +0017: LOADI R64, 2 # 2:11 +0018: LOADI R66, 2 # 3:9 +0019: LOADI R65, 291 # 3:9 +0020: MOVE R68, R64 # 3:28 +0021: LOADI R67, 258 # 3:28 +0022: UPCALL 0, R65 # 3:5, OUT +0023: RETURN # 4:1 + +-- MODIFY_1 +0024: ENTER 5 # 0:0 +0025: LOADI R64, 1 # 7:11 +0026: LOADI R66, 3 # 8:9 +0027: LOADI R65, 291 # 8:9 +0028: MOVE R68, R64 # 8:28 +0029: LOADI R67, 258 # 8:28 +0030: UPCALL 0, R65 # 8:5, OUT +0031: LOADI R65, 0 # 9:5 +0032: CALL R65, 16 # 9:5, MODIFY_2 +0033: LOADI R66, 4 # 10:9 +0034: LOADI R65, 291 # 10:9 +0035: MOVE R68, R64 # 10:27 +0036: LOADI R67, 258 # 10:27 +0037: UPCALL 0, R65 # 10:5, OUT +0038: RETURN # 11:1 +``` + +## Output + +```plain +0=Before modify_1$ , 1=0% +0=Before modify_2$ , 1=1% +0=Inside modify_2$ , 1=2% +0=After modify_2$ , 1=1% +0=After modify_1$ , 1=0% +``` + +# Test: Local is not global + +## Source + +```basic +SUB set_local + local_var = 8 +END SUB + +set_local +OUT local_var +``` + +## Compilation errors + +```plain +6:5: Undefined global symbol local_var +``` diff --git a/core2/tests/test_types.md b/core2/tests/test_types.md new file mode 100644 index 00000000..cda0ebff --- /dev/null +++ b/core2/tests/test_types.md @@ -0,0 +1,165 @@ +# Test: Boolean values + +## Source + +```basic +bool_1 = FALSE +bool_2 = TRUE +OUT bool_1, bool_2 +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 0 # 1:10 +0002: LOADI R65, 1 # 2:10 +0003: MOVE R67, R64 # 3:5 +0004: LOADI R66, 288 # 3:5 +0005: MOVE R69, R65 # 3:13 +0006: LOADI R68, 256 # 3:13 +0007: UPCALL 0, R66 # 3:1, OUT +0008: LOADI R66, 0 # 0:0 +0009: END R66 # 0:0 +``` + +## Output + +```plain +0=false? , 1=true? +``` + +# Test: Double values are always constants + +## Source + +```basic +zero_double = 0.0 +small_double = 1.2 +large_double = 10000000000000000.818239895 +tiny_double = 0.001729874916 +OUT zero_double, small_double, large_double, tiny_double +``` + +## Disassembly + +```asm +0000: ENTER 12 # 0:0 +0001: LOADC R64, 0 # 1:15 +0002: LOADC R65, 1 # 2:16 +0003: LOADC R66, 2 # 3:16 +0004: LOADC R67, 3 # 4:15 +0005: MOVE R69, R64 # 5:5 +0006: LOADI R68, 289 # 5:5 +0007: MOVE R71, R65 # 5:18 +0008: LOADI R70, 289 # 5:18 +0009: MOVE R73, R66 # 5:32 +0010: LOADI R72, 289 # 5:32 +0011: MOVE R75, R67 # 5:46 +0012: LOADI R74, 257 # 5:46 +0013: UPCALL 0, R68 # 5:1, OUT +0014: LOADI R68, 0 # 0:0 +0015: END R68 # 0:0 +``` + +## Output + +```plain +0=0# , 1=1.2# , 2=10000000000000000# , 3=0.001729874916# +``` + +# Test: Integer values that fit in an instruction + +## Source + +```basic +small_int = 123 +OUT small_int +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 123 # 1:13 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 258 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Output + +```plain +0=123% +``` + +# Test: Integer values that spill into a constant + +## Source + +```basic +large_int = 2147483640 +OUT large_int +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R64, 0 # 1:13 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 258 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Output + +```plain +0=2147483640% +``` + +# Test: String values + +## Source + +```basic +text = "Hello, world!" +OUT text +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:8 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 259 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Output + +```plain +0=Hello, world!$ +``` + +# Test: Invalid annotation for variable reference + +## Source + +```basic +d = 3.4 +d2 = d$ +``` + +## Compilation errors + +```plain +2:6: Incompatible type annotation in d$ reference +``` diff --git a/core2/tests/test_varargs.md b/core2/tests/test_varargs.md new file mode 100644 index 00000000..109242e0 --- /dev/null +++ b/core2/tests/test_varargs.md @@ -0,0 +1,139 @@ +# Test: No arguments + +## Source + +```basic +OUT +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 1:1 +0002: UPCALL 0, R64 # 1:1, OUT +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Output + +```plain +0=() +``` + +# Test: Multiple arguments, all present + +## Source + +```basic +OUT 100, 200, 300 +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R67, 200 # 1:10 +0004: LOADI R66, 290 # 1:10 +0005: LOADI R69, 300 # 1:15 +0006: LOADI R68, 258 # 1:15 +0007: UPCALL 0, R64 # 1:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=200% , 2=300% +``` + +# Test: Multiple arguments, some missing + +## Source + +```basic +OUT 100, , 300, +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R66, 32 # 1:10 +0004: LOADI R68, 300 # 1:12 +0005: LOADI R67, 290 # 1:12 +0006: LOADI R69, 0 # 1:16 +0007: UPCALL 0, R64 # 1:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=() , 2=300% , 3=() +``` + +# Test: Multiple arguments, different separators + +## Source + +```basic +OUT 100; 200 AS 300, 400 +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 274 # 1:5 +0003: LOADI R67, 200 # 1:10 +0004: LOADI R66, 306 # 1:10 +0005: LOADI R69, 300 # 1:17 +0006: LOADI R68, 290 # 1:17 +0007: LOADI R71, 400 # 1:22 +0008: LOADI R70, 258 # 1:22 +0009: UPCALL 0, R64 # 1:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=100% ; 1=200% AS 2=300% , 3=400% +``` + +# Test: Multiple arguments, different types + +## Source + +```basic +OUT 100, "Foo" +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R67, 0 # 1:10 +0004: LOADI R66, 259 # 1:10 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=Foo$ +``` From 698dde2581732365cd0944ee3ce1398ca858f92b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 13 Feb 2026 23:29:08 -0800 Subject: [PATCH 02/53] core2: Split integration_test into multiple files Create a new testutils crate for the integration test and split supporting code into separate files. I'm about to add more test-only callable types and that'll bloat the code significantly. --- core2/src/callable.rs | 2 +- core2/tests/integration_test.rs | 588 ++------------------ core2/tests/testutils/callables/.mod.rs.swp | Bin 0 -> 12288 bytes core2/tests/testutils/callables/mod.rs | 33 ++ core2/tests/testutils/callables/out_cmd.rs | 94 ++++ core2/tests/testutils/mod.rs | 415 ++++++++++++++ 6 files changed, 600 insertions(+), 532 deletions(-) create mode 100644 core2/tests/testutils/callables/.mod.rs.swp create mode 100644 core2/tests/testutils/callables/mod.rs create mode 100644 core2/tests/testutils/callables/out_cmd.rs create mode 100644 core2/tests/testutils/mod.rs diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 7f65e77f..187c579a 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -488,7 +488,7 @@ pub struct CallableMetadata { impl CallableMetadata { /// Gets the callable's name, all in uppercase. - pub(crate) fn name(&self) -> &str { + pub fn name(&self) -> &str { &self.name } diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index c25b763d..ce853937 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -15,552 +15,78 @@ //! Integration tests for the core language. -mod testutils { - use async_trait::async_trait; - use endbasic_core2::*; - use std::borrow::Cow; - use std::cell::RefCell; - use std::collections::HashMap; - use std::env; - use std::ffi::OsStr; - use std::fs::{self, File}; - use std::io::{self, BufRead, BufReader, Seek, Write}; - use std::path::{Path, PathBuf}; - use std::process; - use std::rc::Rc; - use tempfile::NamedTempFile; - - /// Computes the path to the directory where this test's binary lives. - fn self_dir() -> PathBuf { - let self_exe = env::current_exe().expect("Cannot get self's executable path"); - let dir = self_exe.parent().expect("Cannot get self's directory"); - assert!(dir.ends_with("target/debug/deps") || dir.ends_with("target/release/deps")); - dir.to_owned() - } - - /// Computes the path to the source file `name`. - fn src_path(name: &str) -> PathBuf { - let test_dir = self_dir(); - let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory"); - let target_dir = debug_or_release_dir.parent().expect("Failed to get parent directory"); - let dir = target_dir.parent().expect("Failed to get parent directory"); - - // Sanity-check that we landed in the right location. - assert!(dir.join("Cargo.lock").exists()); - - dir.join(name) - } - - /// A type describing the golden data of various tests in a file. - /// - /// The first string is the test's name and the second is the input source code. - type Tests = Vec<(String, String)>; - - /// Reads the source sections of a golden test description file. - fn read_sources(path: &Path) -> io::Result { - let file = File::open(path).expect("Failed to open golden data file"); - let reader = BufReader::new(file); - - fn add_test(tests: &mut Tests, name: String, source: Option) -> io::Result<()> { - match source { - Some(source) => { - tests.push((name, source.trim_end().to_owned())); - Ok(()) - } - None => Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Test case '{}' has no Source section", name), - )), - } +use std::fs::{self, File}; +use std::io::{self, BufRead, BufReader}; + +mod testutils; +use testutils::*; + +/// Instantiates a test case for the test described in `core2/tests/.md`. +macro_rules! one_test { + ( $name:ident ) => { + #[tokio::test] + async fn $name() -> io::Result<()> { + run_one_test(stringify!($name)).await } + }; +} - let mut tests = vec![]; - let mut current_test = None; - let mut source: Option = None; +one_test!(test_arithmetic); +one_test!(test_assignments); +one_test!(test_empty); +one_test!(test_end); +one_test!(test_functions); +one_test!(test_globals); +one_test!(test_gosub); +one_test!(test_goto); +one_test!(test_out_of_registers); +one_test!(test_strings); +one_test!(test_subs); +one_test!(test_types); +one_test!(test_varargs); + +#[test] +fn test_all_md_files_registered() -> io::Result<()> { + let mut registered = vec![]; + { + let file = File::open(src_path("core2/tests/integration_test.rs"))?; + let reader = BufReader::new(file); for line in reader.lines() { let line = line?; - if let Some(stripped) = line.strip_prefix("# Test: ") { - if let Some(name) = current_test.take() { - add_test(&mut tests, name, source.take())?; - } - current_test = Some(stripped.to_owned()); - source = None; - continue; - } else if line.starts_with("# ") { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Unexpected section header {}", line), - )); - } else if line == "```basic" { - if current_test.is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Source section without test header", - )); - } - source = Some(String::new()); - continue; - } else if line == "```" { - if let Some(name) = current_test.take() { - add_test(&mut tests, name, source.take())?; - } - source = None; - continue; - } - - if let Some(source) = source.as_mut() { - source.push_str(&line); - source.push('\n'); - } - } - - if let Some(name) = current_test { - add_test(&mut tests, name, source.take())?; - } - - if tests.is_empty() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - format!("Test file '{}' has no tests", path.display()), - )); - } - - Ok(tests) - } - - #[test] - fn test_read_sources_one() -> io::Result<()> { - let mut file = NamedTempFile::new()?; - write!( - file, - "junk -# Test: first - -## Source - -```basic -First line - -Second line -``` - -## Disassembly - -```asm -foo bar -``` -" - )?; - file.flush()?; - - assert_eq!( - [("first".to_owned(), "First line\n\nSecond line".to_owned())], - read_sources(file.path())?.as_slice() - ); - - Ok(()) - } - - #[test] - fn test_read_sources_two() -> io::Result<()> { - let mut file = NamedTempFile::new()?; - write!( - file, - "junk -# Test: first - -## Source - -```basic -First line - -Second line -``` - -## Disassembly - -```asm -foo bar -``` - -# Test: second - -## Source - -```basic -The line -``` -" - )?; - file.flush()?; - - assert_eq!( - [ - ("first".to_owned(), "First line\n\nSecond line".to_owned()), - ("second".to_owned(), "The line".to_owned()), - ], - read_sources(file.path())?.as_slice() - ); - - Ok(()) - } - - /// Generates a textual diff of `golden` and `generated`. The output is meant to be useful for - /// human consumption when a test fails and is not guaranteed to be in patch format. - /// - /// Returns the empty string when the two files match. - fn diff(golden: &Path, generated: &Path) -> io::Result { - match process::Command::new("diff") - .args([OsStr::new("-u"), golden.as_os_str(), generated.as_os_str()]) - .output() - { - Ok(result) => { - let Some(code) = result.status.code() else { - return Err(io::Error::other("diff crashed")); - }; - let Ok(stdout) = String::from_utf8(result.stdout) else { - return Err(io::Error::other("diff printed non-UTF8 content to stdout")); - }; - let Ok(stderr) = String::from_utf8(result.stderr) else { - return Err(io::Error::other("diff printed non-UTF8 content to stderr")); - }; - - let mut diff = stdout; - diff.push_str(&stderr); - if code == 0 && !diff.is_empty() { - return Err(io::Error::other("diff succeeded but output is not empty")); - } else if code != 0 && diff.is_empty() { - return Err(io::Error::other("diff succeeded but output is empty")); - } - - Ok(diff) - } - - Err(e) if e.kind() == io::ErrorKind::NotFound => { - let left = fs::read_to_string(golden)?; - let right = fs::read_to_string(generated)?; - - let mut diff = String::new(); - if left != right { - diff.push_str("Golden\n"); - diff.push_str("======\n"); - diff.push_str(&left); - diff.push_str("\n\nActual\n"); - diff.push_str("======\n"); - diff.push_str(&right); - } - Ok(diff) + if line.starts_with("one_test!(") { + let name = &line["one_test!(".len()..line.len() - 2]; + registered.push(format!("{}.md", name)); } - - Err(e) => Err(e), } } - #[test] - fn test_diff_same() -> io::Result<()> { - let mut f1 = NamedTempFile::new()?; - let mut f2 = NamedTempFile::new()?; - - writeln!(f1, "Line 1")?; - writeln!(f1, "Line 2")?; - f1.flush()?; - f1.seek(io::SeekFrom::Start(0))?; - - writeln!(f2, "Line 1")?; - writeln!(f2, "Line 2")?; - f2.flush()?; - f2.seek(io::SeekFrom::Start(0))?; - - let diff = diff(f1.path(), f2.path())?; - assert!(diff.is_empty()); - Ok(()) - } - - #[test] - fn test_diff_different() -> io::Result<()> { - let mut f1 = NamedTempFile::new()?; - let mut f2 = NamedTempFile::new()?; + // Sanity-check to ensure that the code right above actually discovered what we expect. + assert!(registered.iter().any(|s| s == "test_types.md")); - writeln!(f1, "Line 1")?; - writeln!(f1, "Line 2")?; - f1.flush()?; - f1.seek(io::SeekFrom::Start(0))?; + // Make sure that every md test case definition in the file system is in the list of + // tests discovered from code scanning. We don't have to do the opposite because, if + // this program registers a md file that doesn't actually exist, the test itself will + // fail. + let mut found = vec![]; + for entry in fs::read_dir(src_path("core2/tests"))? { + let entry = entry?; - writeln!(f2, "Line 1")?; - writeln!(f2, "Line2")?; - f2.flush()?; - f2.seek(io::SeekFrom::Start(0))?; - - let diff = diff(f1.path(), f2.path())?; - assert!(!diff.is_empty()); - Ok(()) - } - - /// A command that prints its arguments to a virtual console. - struct OutCommand { - metadata: CallableMetadata, - output: Rc>, - } - - impl OutCommand { - pub fn new(output: Rc>) -> Rc { - Rc::from(Self { - metadata: CallableMetadataBuilder::new("OUT") - .with_syntax(&[( - &[], - Some(&RepeatedSyntax { - name: Cow::Borrowed("arg"), - type_syn: RepeatedTypeSyntax::AnyValue, - sep: ArgSepSyntax::Exactly(ArgSep::Short), - require_one: false, - allow_missing: false, - }), - )]) - .with_category("Testing") - .with_description("Prints arguments") - .build(), - output, - }) - } - } - - #[async_trait(?Send)] - impl Callable for OutCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata - } - - async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { - let mut line = String::new(); - let mut argi = 0; - let mut reg = 0; - loop { - let sep = match scope.get_type(reg) { - VarArgTag::Immediate(sep, etype) => { - reg += 1; - let formatted = match etype { - ExprType::Boolean => format!("{}", scope.get_boolean(reg)), - ExprType::Double => format!("{}", scope.get_double(reg)), - ExprType::Integer => format!("{}", scope.get_integer(reg)), - ExprType::Text => scope.get_string(reg).to_string(), - }; - line.push_str(&format!("{}={}{}", argi, formatted, etype.annotation())); - sep - } - VarArgTag::Missing(sep) => { - line.push_str(&format!("{}=()", argi)); - sep - } - VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), - }; - argi += 1; - reg += 1; - - if sep == ArgSep::End { - break; - } - line.push(' '); - line.push_str(&sep.to_string()); - line.push(' '); - } - let mut output = self.output.borrow_mut(); - output.push_str(&line); - output.push('\n'); - Ok(()) - } - } - - /// Given a `golden` test definition, executes its source part and writes the corresponding - /// `generated` file. The test is expected to pass when both match, but the caller is responsible - /// for checking this condition. - #[allow(clippy::write_with_newline)] - async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<()> { - let tests = read_sources(golden)?; - - let mut first = true; - for (name, source) in tests { - if !first { - write!(generated, "\n")?; - } - write!(generated, "# Test: {}\n\n", name)?; - first = false; - - write!(generated, "## Source\n\n")?; - write!(generated, "```basic\n")?; - if !source.is_empty() { - write!(generated, "{}\n", source)?; - } - write!(generated, "```\n")?; - - let console = Rc::from(RefCell::from(String::new())); - let mut upcalls_by_name: HashMap> = HashMap::default(); - upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(console.clone())); - let image = { compile(&mut source.as_bytes(), &only_metadata(&upcalls_by_name)) }; - - let image = match image { - Ok(image) => image, - Err(e) => { - write!(generated, "\n## Compilation errors\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", e)?; - write!(generated, "```\n")?; - continue; - } - }; - - write!(generated, "\n## Disassembly\n\n")?; - write!(generated, "```asm\n")?; - for line in image.disasm() { - write!(generated, "{}\n", line)?; - } - write!(generated, "```\n")?; - - let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - let mut stop: Option> = None; - while stop.is_none() { - match vm.exec() { - StopReason::End(code) => stop = Some(Ok(code)), - StopReason::Upcall(handle) => { - if let Err(e) = handle.invoke().await { - stop = Some(Err(e.to_string())); - } - } - StopReason::Exception(pos, e) => { - stop = Some(Err(format!("{}: {}", pos, e))); - } - } - } - - match stop.expect("The loop can only exit when this is set") { - Ok(0) => { - // Keep quiet in the common case. - } - Ok(i) => { - write!(generated, "\n## Exit code\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", i)?; - write!(generated, "```\n")?; - } - Err(e) => { - write!(generated, "\n## Runtime errors\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", e)?; - write!(generated, "```\n")?; - } - } - - let console = console.borrow(); - if !console.is_empty() { - write!(generated, "\n## Output\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}", console)?; - write!(generated, "```\n")?; - } - } - - Ok(()) - } - - /// Executes the test described in the `core2/tests/.md` file. - pub(super) async fn run_one_test(name: &'static str) -> io::Result<()> { - let golden = src_path(&format!("core2/tests/{}.md", name)); - - let mut generated = NamedTempFile::new()?; - regenerate(&golden, &mut generated).await?; - generated.flush()?; - - let diff = diff(&golden, generated.path())?; - if !diff.is_empty() { - if env::var("REGEN").as_ref().map(String::as_str) == Ok("true") { - { - let mut output = File::create(golden)?; - generated.as_file_mut().seek(io::SeekFrom::Start(0))?; - io::copy(&mut generated, &mut output)?; - } - panic!("Golden data regenerated; flip REGEN back to false"); - } else { - eprintln!("{}", diff); - panic!("Test failed; see stderr for details"); - } - } - - Ok(()) - } - - #[test] - fn test_all_md_files_registered() -> io::Result<()> { - let mut registered = vec![]; - { - let file = File::open(src_path("core2/tests/integration_test.rs"))?; - let reader = BufReader::new(file); - for line in reader.lines() { - let line = line?; - - if line.starts_with(" one_test!(") { - let name = &line[" one_test!(".len()..line.len() - 2]; - registered.push(format!("{}.md", name)); - } - } - } - - // Sanity-check to ensure that the code right above actually discovered what we expect. - assert!(registered.iter().any(|s| s == "test_types.md")); - - // Make sure that every md test case definition in the file system is in the list of - // tests discovered from code scanning. We don't have to do the opposite because, if - // this program registers a md file that doesn't actually exist, the test itself will - // fail. - let mut found = vec![]; - for entry in fs::read_dir(src_path("core2/tests"))? { - let entry = entry?; - - let Ok(name) = entry.file_name().into_string() else { - continue; - }; + let Ok(name) = entry.file_name().into_string() else { + continue; + }; - #[allow(clippy::collapsible_if)] - if name.starts_with("test_") && name.ends_with(".md") { - found.push(name); - } + #[allow(clippy::collapsible_if)] + if name.starts_with("test_") && name.ends_with(".md") { + found.push(name); } - - // We want the list of tests below to be sorted, so sort the list of found tests and - // use that when comparing against the list of registered tests. - found.sort(); - - assert_eq!(registered, found); - - Ok(()) } -} -mod tests { - use super::testutils::*; - use std::io; + // We want the list of tests below to be sorted, so sort the list of found tests and + // use that when comparing against the list of registered tests. + found.sort(); - /// Instantiates a test case for the test described in `core2/tests/.md`. - macro_rules! one_test { - ( $name:ident ) => { - #[tokio::test] - async fn $name() -> io::Result<()> { - run_one_test(stringify!($name)).await - } - }; - } + assert_eq!(registered, found); - one_test!(test_arithmetic); - one_test!(test_assignments); - one_test!(test_empty); - one_test!(test_end); - one_test!(test_functions); - one_test!(test_globals); - one_test!(test_gosub); - one_test!(test_goto); - one_test!(test_out_of_registers); - one_test!(test_strings); - one_test!(test_subs); - one_test!(test_types); - one_test!(test_varargs); + Ok(()) } diff --git a/core2/tests/testutils/callables/.mod.rs.swp b/core2/tests/testutils/callables/.mod.rs.swp new file mode 100644 index 0000000000000000000000000000000000000000..603401a2f44b8404ac92a56dab6bcbea47d65e1d GIT binary patch literal 12288 zcmeI2?~5Bn7{{mDTD4ZizAVUG3N_JO_U@FTduJg{Pa50aL2_;F3rQ!tlVr>Oh&!`Q zL%EjH7mA=Ste_yMMg1F;S`>Vxud0^%P6U;T__he8FY0&l>vBo~djvft?9OTt02r3TOq!R3MU- z$!E8dr)Q?8RK%{t1l{r2kuig{n^r(8pcT*xXa%$aS^=$qRzNH8e^5ZyH<5Gb$UU9T z?CN}O8u;v->JP1eRzNGD70?Q31+)TM0j+>mKr5gX&ViC?5a+ z@BaRO^ASR>gYUrw@C;%6*OvnxJ0Vsei;PQ4tJ_a9w1~>&?0$K1B*a2=nM944T zXK)={0iT0qz`z8!gF0@5Yv6UD>bd~VfkR*)*bQ!><{!aT@ELd;Tm;LY0*--O4-oPj zxC$%it1t6)=zh6JRIU41U-`$fw{E z_z=7g-UV-fCMbavxU-p%KfpEc321^n;Lm#r`3k%P&VrZ0F0c)Jhd;`E3BCYVzqf>p#5N48y#d$W!2F$vh~lwm03N$XaD?aI?mEcS?w9 z9rMPw#|w}5iftKa7&skcecE`0TG7^yTDx`BD!K89{&m$haVs^FR^W@k<*C#xNzpUp z?&rMw8SiqHF$Kn$$DO<*)m6{Imf&&8hwa0bH6BVEavpfn#>EcXhb?QAG;kH>kDhlu zW!!=61E<}abF_KHLw&fk(V@<2^XjwJdzQ7@hW0aSIj^*>eCuqHdWPN?wRu`~(%ngo zG{$YOEYHTd6Qml*^H)+9|EAUE= zyY5_3hK^s|yEom+Hn8h6>25EQ^Z*mJ!>m5n+x(?f2D|CgT;~N|6(s$-@#nlEg3!TIg5=g^8+HfhPmsZPMOLz|`?&Ku-+Ra3VuUBsdKsSwcrmv$xf> zX9->#?l#P}-GqcoXqjfgnr+3k3|8Q}+>#DfPt)vlr@t9WAfhcvpS>J}VbCzmT+nD+ zyYDlXTJJNP=96tR6HVVL$&fkHH2Y3alVMEsR1%b*t_OnK6#YbJc{OCJrj{52{e()9 zZ}X7K8mEUGi(~o9C~N0Ctpx2hQRkLZaV+XGzZ$VBr`6yT4}A+RlOk>g5t7{ zS9cw5XDBUlt_oDiY>aW}lp~2g4OLDZvC07*Wj;-0i!@(M(0$orzL=)R@=FKsJA_Jh z#K)W90yF28$O)0K+@!3Ii(2XxarCsI%3#%ycLk)4(2?kJnu?$z8!S{csZ8pqNm(}y zqtR$&Se;olo(aOLfow`tVI0cm<`;_dlQWsAPEb(kmz^?cs#I#~$r^f6aZm(5ZSlIq z*x|XL^kBZlX#-mIheD#`K}0>)q(NCSI5W7{n<`yrAp6zIwyNv4r0G!}s==eEj3(7Y vNOXb{sae|WB=!T@DLaSEp;N6%Ix{u1hrSrOPCyTH+|_});Jj}urE}yjuNxLp literal 0 HcmV?d00001 diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs new file mode 100644 index 00000000..acb26565 --- /dev/null +++ b/core2/tests/testutils/callables/mod.rs @@ -0,0 +1,33 @@ +// 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. + +//! Callables exposed to integration tests. + +use endbasic_core2::{Callable, SymbolKey}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +mod out_cmd; +use out_cmd::OutCommand; + +/// Registers all test-only callables into `upcalls_by_name`. +pub(super) fn register_all( + upcalls_by_name: &mut HashMap>, + console: Rc>, +) { + let cmd = OutCommand::new(console); + upcalls_by_name.insert(SymbolKey::from(cmd.metadata().name()), cmd); +} diff --git a/core2/tests/testutils/callables/out_cmd.rs b/core2/tests/testutils/callables/out_cmd.rs new file mode 100644 index 00000000..f2187481 --- /dev/null +++ b/core2/tests/testutils/callables/out_cmd.rs @@ -0,0 +1,94 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints its arguments to a virtual console. +pub(super) struct OutCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Short), + require_one: false, + allow_missing: false, + }), + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut line = String::new(); + let mut argi = 0; + let mut reg = 0; + loop { + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + let formatted = match etype { + ExprType::Boolean => format!("{}", scope.get_boolean(reg)), + ExprType::Double => format!("{}", scope.get_double(reg)), + ExprType::Integer => format!("{}", scope.get_integer(reg)), + ExprType::Text => scope.get_string(reg).to_string(), + }; + line.push_str(&format!("{}={}{}", argi, formatted, etype.annotation())); + sep + } + VarArgTag::Missing(sep) => { + line.push_str(&format!("{}=()", argi)); + sep + } + VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + }; + argi += 1; + reg += 1; + + if sep == ArgSep::End { + break; + } + line.push(' '); + line.push_str(&sep.to_string()); + line.push(' '); + } + let mut output = self.output.borrow_mut(); + output.push_str(&line); + output.push('\n'); + Ok(()) + } +} diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs new file mode 100644 index 00000000..b5bcd298 --- /dev/null +++ b/core2/tests/testutils/mod.rs @@ -0,0 +1,415 @@ +// 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. + +//! Support functions to implement the integration tests. + +use endbasic_core2::*; +use std::cell::RefCell; +use std::collections::HashMap; +use std::env; +use std::ffi::OsStr; +use std::fs::{self, File}; +use std::io::{self, BufRead, BufReader, Seek, Write}; +use std::path::{Path, PathBuf}; +use std::process; +use std::rc::Rc; +use tempfile::NamedTempFile; + +mod callables; + +/// Computes the path to the directory where this test's binary lives. +fn self_dir() -> PathBuf { + let self_exe = env::current_exe().expect("Cannot get self's executable path"); + let dir = self_exe.parent().expect("Cannot get self's directory"); + assert!(dir.ends_with("target/debug/deps") || dir.ends_with("target/release/deps")); + dir.to_owned() +} + +/// Computes the path to the source file `name`. +pub(super) fn src_path(name: &str) -> PathBuf { + let test_dir = self_dir(); + let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory"); + let target_dir = debug_or_release_dir.parent().expect("Failed to get parent directory"); + let dir = target_dir.parent().expect("Failed to get parent directory"); + + // Sanity-check that we landed in the right location. + assert!(dir.join("Cargo.lock").exists()); + + dir.join(name) +} + +/// A type describing the golden data of various tests in a file. +/// +/// The first string is the test's name and the second is the input source code. +type Tests = Vec<(String, String)>; + +/// Reads the source sections of a golden test description file. +fn read_sources(path: &Path) -> io::Result { + let file = File::open(path).expect("Failed to open golden data file"); + let reader = BufReader::new(file); + + fn add_test(tests: &mut Tests, name: String, source: Option) -> io::Result<()> { + match source { + Some(source) => { + tests.push((name, source.trim_end().to_owned())); + Ok(()) + } + None => Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Test case '{}' has no Source section", name), + )), + } + } + + let mut tests = vec![]; + let mut current_test = None; + let mut source: Option = None; + for line in reader.lines() { + let line = line?; + + if let Some(stripped) = line.strip_prefix("# Test: ") { + if let Some(name) = current_test.take() { + add_test(&mut tests, name, source.take())?; + } + current_test = Some(stripped.to_owned()); + source = None; + continue; + } else if line.starts_with("# ") { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Unexpected section header {}", line), + )); + } else if line == "```basic" { + if current_test.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Source section without test header", + )); + } + source = Some(String::new()); + continue; + } else if line == "```" { + if let Some(name) = current_test.take() { + add_test(&mut tests, name, source.take())?; + } + source = None; + continue; + } + + if let Some(source) = source.as_mut() { + source.push_str(&line); + source.push('\n'); + } + } + + if let Some(name) = current_test { + add_test(&mut tests, name, source.take())?; + } + + if tests.is_empty() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Test file '{}' has no tests", path.display()), + )); + } + + Ok(tests) +} + +#[test] +fn test_read_sources_one() -> io::Result<()> { + let mut file = NamedTempFile::new()?; + write!( + file, + "junk +# Test: first + +## Source + +```basic +First line + +Second line +``` + +## Disassembly + +```asm +foo bar +``` +" + )?; + file.flush()?; + + assert_eq!( + [("first".to_owned(), "First line\n\nSecond line".to_owned())], + read_sources(file.path())?.as_slice() + ); + + Ok(()) +} + +#[test] +fn test_read_sources_two() -> io::Result<()> { + let mut file = NamedTempFile::new()?; + write!( + file, + "junk +# Test: first + +## Source + +```basic +First line + +Second line +``` + +## Disassembly + +```asm +foo bar +``` + +# Test: second + +## Source + +```basic +The line +``` +" + )?; + file.flush()?; + + assert_eq!( + [ + ("first".to_owned(), "First line\n\nSecond line".to_owned()), + ("second".to_owned(), "The line".to_owned()), + ], + read_sources(file.path())?.as_slice() + ); + + Ok(()) +} + +/// Generates a textual diff of `golden` and `generated`. The output is meant to be useful for +/// human consumption when a test fails and is not guaranteed to be in patch format. +/// +/// Returns the empty string when the two files match. +fn diff(golden: &Path, generated: &Path) -> io::Result { + match process::Command::new("diff") + .args([OsStr::new("-u"), golden.as_os_str(), generated.as_os_str()]) + .output() + { + Ok(result) => { + let Some(code) = result.status.code() else { + return Err(io::Error::other("diff crashed")); + }; + let Ok(stdout) = String::from_utf8(result.stdout) else { + return Err(io::Error::other("diff printed non-UTF8 content to stdout")); + }; + let Ok(stderr) = String::from_utf8(result.stderr) else { + return Err(io::Error::other("diff printed non-UTF8 content to stderr")); + }; + + let mut diff = stdout; + diff.push_str(&stderr); + if code == 0 && !diff.is_empty() { + return Err(io::Error::other("diff succeeded but output is not empty")); + } else if code != 0 && diff.is_empty() { + return Err(io::Error::other("diff succeeded but output is empty")); + } + + Ok(diff) + } + + Err(e) if e.kind() == io::ErrorKind::NotFound => { + let left = fs::read_to_string(golden)?; + let right = fs::read_to_string(generated)?; + + let mut diff = String::new(); + if left != right { + diff.push_str("Golden\n"); + diff.push_str("======\n"); + diff.push_str(&left); + diff.push_str("\n\nActual\n"); + diff.push_str("======\n"); + diff.push_str(&right); + } + Ok(diff) + } + + Err(e) => Err(e), + } +} + +#[test] +fn test_diff_same() -> io::Result<()> { + let mut f1 = NamedTempFile::new()?; + let mut f2 = NamedTempFile::new()?; + + writeln!(f1, "Line 1")?; + writeln!(f1, "Line 2")?; + f1.flush()?; + f1.seek(io::SeekFrom::Start(0))?; + + writeln!(f2, "Line 1")?; + writeln!(f2, "Line 2")?; + f2.flush()?; + f2.seek(io::SeekFrom::Start(0))?; + + let diff = diff(f1.path(), f2.path())?; + assert!(diff.is_empty()); + Ok(()) +} + +#[test] +fn test_diff_different() -> io::Result<()> { + let mut f1 = NamedTempFile::new()?; + let mut f2 = NamedTempFile::new()?; + + writeln!(f1, "Line 1")?; + writeln!(f1, "Line 2")?; + f1.flush()?; + f1.seek(io::SeekFrom::Start(0))?; + + writeln!(f2, "Line 1")?; + writeln!(f2, "Line2")?; + f2.flush()?; + f2.seek(io::SeekFrom::Start(0))?; + + let diff = diff(f1.path(), f2.path())?; + assert!(!diff.is_empty()); + Ok(()) +} + +/// Given a `golden` test definition, executes its source part and writes the corresponding +/// `generated` file. The test is expected to pass when both match, but the caller is responsible +/// for checking this condition. +#[allow(clippy::write_with_newline)] +async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<()> { + let tests = read_sources(golden)?; + + let mut first = true; + for (name, source) in tests { + if !first { + write!(generated, "\n")?; + } + write!(generated, "# Test: {}\n\n", name)?; + first = false; + + write!(generated, "## Source\n\n")?; + write!(generated, "```basic\n")?; + if !source.is_empty() { + write!(generated, "{}\n", source)?; + } + write!(generated, "```\n")?; + + let console = Rc::from(RefCell::from(String::new())); + let mut upcalls_by_name: HashMap> = HashMap::default(); + callables::register_all(&mut upcalls_by_name, console.clone()); + let image = { compile(&mut source.as_bytes(), &only_metadata(&upcalls_by_name)) }; + + let image = match image { + Ok(image) => image, + Err(e) => { + write!(generated, "\n## Compilation errors\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + continue; + } + }; + + write!(generated, "\n## Disassembly\n\n")?; + write!(generated, "```asm\n")?; + for line in image.disasm() { + write!(generated, "{}\n", line)?; + } + write!(generated, "```\n")?; + + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + let mut stop: Option> = None; + while stop.is_none() { + match vm.exec() { + StopReason::End(code) => stop = Some(Ok(code)), + StopReason::Upcall(handle) => { + if let Err(e) = handle.invoke().await { + stop = Some(Err(e.to_string())); + } + } + StopReason::Exception(pos, e) => { + stop = Some(Err(format!("{}: {}", pos, e))); + } + } + } + + match stop.expect("The loop can only exit when this is set") { + Ok(0) => { + // Keep quiet in the common case. + } + Ok(i) => { + write!(generated, "\n## Exit code\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", i)?; + write!(generated, "```\n")?; + } + Err(e) => { + write!(generated, "\n## Runtime errors\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + } + } + + let console = console.borrow(); + if !console.is_empty() { + write!(generated, "\n## Output\n\n")?; + write!(generated, "```plain\n")?; + write!(generated, "{}", console)?; + write!(generated, "```\n")?; + } + } + + Ok(()) +} + +/// Executes the test described in the `core2/tests/.md` file. +pub(super) async fn run_one_test(name: &'static str) -> io::Result<()> { + let golden = src_path(&format!("core2/tests/{}.md", name)); + + let mut generated = NamedTempFile::new()?; + regenerate(&golden, &mut generated).await?; + generated.flush()?; + + let diff = diff(&golden, generated.path())?; + if !diff.is_empty() { + if env::var("REGEN").as_ref().map(String::as_str) == Ok("true") { + { + let mut output = File::create(golden)?; + generated.as_file_mut().seek(io::SeekFrom::Start(0))?; + io::copy(&mut generated, &mut output)?; + } + panic!("Golden data regenerated; flip REGEN back to false"); + } else { + eprintln!("{}", diff); + panic!("Test failed; see stderr for details"); + } + } + + Ok(()) +} From ee9a63cb03d3fd1847c1c17dd5913c2c11bda079 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 12 Feb 2026 12:37:38 -0800 Subject: [PATCH 03/53] core2: Implement most of the arguments compiler This change implements callable arguments compilation in the new compiler. Most cases are handled, except for output variable references. These will come next. --- core2/src/callable.rs | 49 +- core2/src/compiler/args.rs | 236 ++++++++-- core2/src/compiler/exprs.rs | 69 ++- core2/src/compiler/top.rs | 31 +- core2/src/vm/context.rs | 4 +- core2/tests/integration_test.rs | 2 +- core2/tests/test_args.md | 434 ++++++++++++++++++ core2/tests/test_functions.md | 394 ++++++++++++---- core2/tests/test_globals.md | 52 +-- core2/tests/test_subs.md | 119 +++-- core2/tests/test_varargs.md | 139 ------ core2/tests/testutils/callables/.mod.rs.swp | Bin 12288 -> 0 bytes core2/tests/testutils/callables/mod.rs | 53 ++- .../testutils/callables/out_any_value_cmd.rs | 63 +++ .../callables/out_any_value_optional_cmd.rs | 63 +++ core2/tests/testutils/callables/out_cmd.rs | 12 +- .../testutils/callables/out_optional_cmd.rs | 63 +++ .../testutils/callables/out_positional_cmd.rs | 94 ++++ .../callables/out_required_value_cmd.rs | 63 +++ 19 files changed, 1560 insertions(+), 380 deletions(-) create mode 100644 core2/tests/test_args.md delete mode 100644 core2/tests/test_varargs.md delete mode 100644 core2/tests/testutils/callables/.mod.rs.swp create mode 100644 core2/tests/testutils/callables/out_any_value_cmd.rs create mode 100644 core2/tests/testutils/callables/out_any_value_optional_cmd.rs create mode 100644 core2/tests/testutils/callables/out_optional_cmd.rs create mode 100644 core2/tests/testutils/callables/out_positional_cmd.rs create mode 100644 core2/tests/testutils/callables/out_required_value_cmd.rs diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 187c579a..a859705b 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -70,13 +70,6 @@ pub struct OptionalValueSyntax { /// The type of the expected parameter. pub vtype: ExprType, - - /// Value to push onto the stack when the parameter is missing. - pub missing_value: i32, - - /// Value to push onto the stack when the parameter is present, after which the stack contains - /// the parameter value. - pub present_value: i32, } /// Specifies the type constraints for a repeated parameter. @@ -170,7 +163,7 @@ pub enum ArgSepSyntax { Exactly(ArgSep), /// The argument separator may be any of the ones given. - OneOf(ArgSep, ArgSep), + OneOf(&'static [ArgSep]), /// The argument separator is the end of the call. End, @@ -192,13 +185,16 @@ impl ArgSepSyntax { } } - ArgSepSyntax::OneOf(sep1, sep2) => { - let (text1, _needs_space1) = sep1.describe(); - let (text2, _needs_space2) = sep2.describe(); - - output.push(' '); - output.push_str(&format!("<{}|{}>", text1, text2)); - output.push(' '); + ArgSepSyntax::OneOf(seps) => { + output.push_str(" <"); + for (i, sep) in seps.iter().enumerate() { + let (text, _needs_space) = sep.describe(); + output.push_str(text); + if i < seps.len() - 1 { + output.push('|'); + } + } + output.push_str("> "); } ArgSepSyntax::End => (), @@ -235,10 +231,10 @@ pub enum SingularArgSyntax { #[derive(Clone, Debug)] pub(crate) struct CallableSyntax { /// Ordered list of singular arguments that appear before repeated arguments. - singular: Cow<'static, [SingularArgSyntax]>, + pub(crate) singular: Cow<'static, [SingularArgSyntax]>, /// Details on the repeated argument allowed after singular arguments, if any. - repeated: Option>, + pub(crate) repeated: Option>, } impl CallableSyntax { @@ -262,14 +258,29 @@ impl CallableSyntax { /// Computes the range of the expected number of parameters for this syntax. pub(crate) fn expected_nargs(&self) -> RangeInclusive { - let mut min = self.singular.len(); - let mut max = self.singular.len(); + let mut min = 0; + let mut max = 0; + + for syn in self.singular.iter() { + let may_be_missing = match syn { + SingularArgSyntax::RequiredValue(..) => false, + SingularArgSyntax::RequiredRef(..) => false, + SingularArgSyntax::OptionalValue(..) => true, + SingularArgSyntax::AnyValue(details, ..) => details.allow_missing, + }; + if !may_be_missing { + min += 1; + } + max += 1; + } + if let Some(syn) = self.repeated.as_ref() { if syn.require_one { min += 1; } max = usize::MAX; } + min..=max } diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 546a762a..bc410c6f 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -15,14 +15,15 @@ //! Common compilers for callable arguments. -use crate::ast::{ArgSep, ArgSpan, CallSpan}; -use crate::bytecode::{self, Register, RegisterScope}; +use crate::ast::{ArgSpan, CallSpan}; +use crate::bytecode::{self, Register}; use crate::callable::{CallableMetadata, CallableSyntax}; use crate::compiler::codegen::Codegen; -use crate::compiler::exprs::compile_expr; +use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; use crate::compiler::syms::TempSymtable; -use crate::compiler::{Error, Result, SymbolKey}; +use crate::compiler::{Error, Result}; use crate::reader::LineCol; +use crate::{ArgSep, ArgSepSyntax, RepeatedTypeSyntax, SingularArgSyntax}; /// Finds the syntax definition that matches the given argument count. /// @@ -40,54 +41,229 @@ fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&Cal } } +/// Compiles an argument separator with any necessary tagging. +/// +/// `instrs` is the list of instructions into which insert the separator tag at `sep_tag_pc` +/// when it is needed to disambiguate separators at runtime. +/// +/// `syn` contains the details about the separator syntax that is accepted. +/// +/// `sep` and `sep_pos` are the details about the separator being compiled. +/// +/// `is_last` indicates whether this is the last separator in the command call and is used +/// only for diagnostics purposes. +#[allow(clippy::too_many_arguments)] +fn validate_syn_argsep( + md: &CallableMetadata, + pos: LineCol, + syn: &ArgSepSyntax, + is_last: bool, + sep: ArgSep, +) -> Result<()> { + debug_assert!( + (!is_last || sep == ArgSep::End) && (is_last || sep != ArgSep::End), + "Parser can only supply an End separator in the last argument" + ); + + match syn { + ArgSepSyntax::Exactly(exp_sep) => { + debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); + if sep != ArgSep::End && sep != *exp_sep { + return Err(Error::CallableSyntax(pos, md.clone())); + } + Ok(()) + } + + ArgSepSyntax::OneOf(exp_seps) => { + let mut found = false; + for exp_sep in *exp_seps { + debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); + if sep == *exp_sep { + found = true; + break; + } + } + if !found { + return Err(Error::CallableSyntax(pos, md.clone())); + } + Ok(()) + } + + ArgSepSyntax::End => { + debug_assert!(is_last); + Ok(()) + } + } +} + /// Compiles the arguments of a callable invocation. /// /// Returns the first register containing the compiled arguments. Arguments are laid out as /// pairs of type tag and value registers, allowing the callable to interpret them at runtime. +/// +/// TODO(jmmv): The `md` metadata is passed by value, not because we want to, but because it's +/// necessary to appease the borrow checker. The `md` is obtained from the `symtable` in the caller +/// (as a reference) to perform various validations so it is not possible to pass it as input along +/// `symtable`. An alternative would be to take the symbol `key` as a parameter here and perform +/// another lookup from the symtable. Or maybe we could make `Metadata` objects static by +/// eliminating the `MetadataBuilder` and pass a static reference here. pub(super) fn compile_args( span: CallSpan, + md: CallableMetadata, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, codegen: &mut Codegen, ) -> Result { - let key = SymbolKey::from(&span.vref.name); let key_pos = span.vref_pos; - let Some(md) = symtable.get_callable(&key) else { - return Err(Error::UndefinedSymbol(key_pos, span.vref.clone(), RegisterScope::Global)); - }; + let syntax = find_syntax(&md, key_pos, span.args.len())?; let mut scope = symtable.temp_scope(); - let _syntax = find_syntax(md, key_pos, span.args.len())?; + let input_nargs = span.args.len(); + let mut arg_iter = span.args.into_iter().peekable(); - // Arguments are represented as 1 or 2 consecutive registers. + for syn in syntax.singular.iter() { + match syn { + SingularArgSyntax::RequiredValue(details, exp_sep) => { + let ArgSpan { expr, sep, sep_pos } = + arg_iter.next().expect("Args and their syntax must advance in unison"); + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + + match expr { + None => return Err(Error::CallableSyntax(key_pos, md)), + Some(expr) => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + compile_expr_as_type(codegen, symtable, temp_value, expr, details.vtype)?; + validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + } + } + } + + SingularArgSyntax::RequiredRef(_details, exp_sep) => { + let ArgSpan { expr, sep, sep_pos } = + arg_iter.next().expect("Args and their syntax must advance in unison"); + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + + match expr { + None => return Err(Error::CallableSyntax(key_pos, md)), + Some(_expr) => { + validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + todo!(); + } + } + } + + SingularArgSyntax::OptionalValue(details, exp_sep) => { + // The `CallSpan` is optimized (for simplicity) to not carry any arguments at all + // when callables are invoked without arguments. This leads to a little + // inconsistency though: a call like `PRINT ;` carries two arguments whereas + // `PRINT` carries none (instead of one). Deal with this here. + let (expr, sep, sep_pos) = match arg_iter.next() { + Some(ArgSpan { expr, sep, sep_pos }) => (expr, sep, sep_pos), + None => (None, ArgSep::End, key_pos), + }; + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + + let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + let tag = match expr { + None => bytecode::VarArgTag::Missing(sep), + Some(expr) => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + compile_expr_as_type(codegen, symtable, temp_value, expr, details.vtype)?; + bytecode::VarArgTag::Immediate(sep, details.vtype) + } + }; + validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); + } + + SingularArgSyntax::AnyValue(details, exp_sep) => { + // The `CallSpan` is optimized (for simplicity) to not carry any arguments at all + // when callables are invoked without arguments. This leads to a little + // inconsistency though: a call like `PRINT ;` carries two arguments whereas + // `PRINT` carries none (instead of one). Deal with this here. + let (expr, sep, sep_pos) = match arg_iter.next() { + Some(ArgSpan { expr, sep, sep_pos }) => (expr, sep, sep_pos), + None => { + if !details.allow_missing { + return Err(Error::CallableSyntax(key_pos, md)); + } + (None, ArgSep::End, key_pos) + } + }; + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + + let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + let tag = match expr { + None => bytecode::VarArgTag::Missing(sep), + Some(expr) => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + let etype = compile_expr(codegen, symtable, temp_value, expr)?; + bytecode::VarArgTag::Immediate(sep, etype) + } + }; + validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); + } + }; + } + + // Variable (repeated) arguments are represented as 1 or 2 consecutive registers. // // The first register always contains a `VarArgTag`, which indicates the type of // separator following the argument and, if an argument is present, its type. // The second register is only present if there is an argument. // // The caller must iterate over all tags until it finds `ArgSep::End`. - let nargs = span.args.len(); - for ArgSpan { expr, sep, sep_pos } in span.args { - let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); - let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; - - let tag = match expr { - None => bytecode::VarArgTag::Missing(sep), - Some(expr) => { - let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; - let etype = compile_expr(codegen, symtable, temp_value, expr)?; - bytecode::VarArgTag::Immediate(sep, etype) - } - }; - codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); + if let Some(syn) = syntax.repeated.as_ref() { + let mut min_nargs = syntax.singular.len(); + if syn.require_one { + min_nargs += 1; + } + if input_nargs < min_nargs { + return Err(Error::CallableSyntax(key_pos, md)); + } + + while arg_iter.peek().is_some() { + let ArgSpan { expr, sep, sep_pos } = + arg_iter.next().expect("Args and their syntax must advance in unison"); + + let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); + let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + + let tag = match expr { + None => { + if !syn.allow_missing { + return Err(Error::CallableSyntax(arg_pos, md)); + } + bytecode::VarArgTag::Missing(sep) + } + + Some(expr) => match syn.type_syn { + RepeatedTypeSyntax::AnyValue => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + let etype = compile_expr(codegen, symtable, temp_value, expr)?; + bytecode::VarArgTag::Immediate(sep, etype) + } + + RepeatedTypeSyntax::TypedValue(vtype) => { + let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + compile_expr_as_type(codegen, symtable, temp_value, expr, vtype)?; + bytecode::VarArgTag::Immediate(sep, vtype) + } + + RepeatedTypeSyntax::VariableRef => { + todo!(); + } + }, + }; + codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); + } } - if nargs == 0 { - let temp = scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?; - codegen.emit( - bytecode::make_load_integer(temp, bytecode::VarArgTag::Missing(ArgSep::End).make_u16()), - key_pos, - ); + + if arg_iter.peek().is_some() { + debug_assert!(arg_iter.next().is_some(), "Args and their syntax must advance in unison"); + return Err(Error::CallableSyntax(key_pos, md)); } scope.first().map_err(|e| Error::from_syms(e, key_pos)) diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 0be454b0..9c395b0d 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -17,8 +17,9 @@ use crate::ast::{BinaryOpSpan, Expr, ExprType}; use crate::bytecode::{self, Register, RegisterScope}; +use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; -use crate::compiler::syms::{SymbolKey, TempSymtable}; +use crate::compiler::syms::{self, SymbolKey, TempSymtable}; use crate::compiler::{Error, Result}; use crate::mem::Datum; use std::convert::TryFrom; @@ -100,6 +101,7 @@ pub(super) fn compile_expr( Expr::Call(span) => { let key = SymbolKey::from(&span.vref.name); + let key_pos = span.vref_pos; let Some(md) = symtable.get_callable(&key) else { return Err(Error::UndefinedSymbol( @@ -113,9 +115,31 @@ pub(super) fn compile_expr( return Err(Error::NotAFunction(span.vref_pos, span.vref)); }; + if md.is_argless() { + return Err(Error::CallableSyntax(span.vref_pos, md.clone())); + } + if md.is_user_defined() { - let addr = codegen.emit(bytecode::make_nop(), span.vref_pos); - codegen.add_fixup(addr, Fixup::Call(reg, key)); + let (is_global, _index) = reg.to_parts(); + + let mut alloc = symtable.temp_scope(); + let ret_reg = if is_global { + // The call instruction can only carry one register, and this register + // indicates where to store the result and where arguments start. So, + // if we are going to save the result to a global register, we must + // allocate a temp register first so that argument passing can work. + alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? + } else { + reg + }; + let _first_temp = compile_args(span, md.clone(), symtable, codegen)?; + + let addr = codegen.emit(bytecode::make_nop(), key_pos); + codegen.add_fixup(addr, Fixup::Call(ret_reg, key)); + + if is_global { + codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); + } } else { todo!("Function upcalls not implemented yet"); } @@ -141,13 +165,38 @@ pub(super) fn compile_expr( Ok(ExprType::Integer) } - Expr::Symbol(span) => { - let (local, etype) = symtable - .get_local_or_global(&span.vref) - .map_err(|e| Error::from_syms(e, span.pos))?; - codegen.emit(bytecode::make_move(reg, local), span.pos); - Ok(etype) - } + Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { + Ok((local, etype)) => { + codegen.emit(bytecode::make_move(reg, local), span.pos); + Ok(etype) + } + + Err(syms::Error::UndefinedSymbol(..)) => { + let key = SymbolKey::from(&span.vref.name); + + let Some(md) = symtable.get_callable(&key) else { + return Err(Error::UndefinedSymbol(span.pos, span.vref, RegisterScope::Global)); + }; + + let Some(etype) = md.return_type() else { + return Err(Error::NotAFunction(span.pos, span.vref)); + }; + + if !md.is_argless() { + return Err(Error::CallableSyntax(span.pos, md.clone())); + } + + if md.is_user_defined() { + let addr = codegen.emit(bytecode::make_nop(), span.pos); + codegen.add_fixup(addr, Fixup::Call(reg, key)); + } else { + todo!("Function upcalls not implemented yet"); + } + Ok(etype) + } + + Err(e) => Err(Error::from_syms(e, span.pos)), + }, Expr::Text(span) => { let index = codegen.get_constant(Datum::Text(span.value), span.pos)?; diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index e5b48a2a..d22bf090 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -25,6 +25,7 @@ use crate::compiler::ids::HashMapWithIds; use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey}; use crate::compiler::{Error, Result}; use crate::image::Image; +use crate::mem::Datum; use crate::reader::LineCol; use crate::{Callable, CallableMetadataBuilder, parser}; use std::borrow::Cow; @@ -134,7 +135,7 @@ fn compile_stmt( }; let is_user_defined = md.is_user_defined(); - let first_temp = compile_args(span, &mut symtable, &mut ctx.codegen)?; + let first_temp = compile_args(span, md.clone(), &mut symtable, &mut ctx.codegen)?; if is_user_defined { let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos); @@ -267,10 +268,32 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R let key = SymbolKey::from(callable.name.name); let mut symtable = symtable.enter_scope(); + + // The call protocol expects the return value to be in the first local variable + // so allocate it early, and then all arguments follow in order from left to right. if let Some(vtype) = callable.name.ref_type { - // The call protocol expects the return value to be in the _first_ local variable - // so allocate it early. - symtable.put_local(key.clone(), vtype).map_err(|e| Error::from_syms(e, key_pos))?; + let ret_reg = + symtable.put_local(key.clone(), vtype).map_err(|e| Error::from_syms(e, key_pos))?; + + // Set the default value of the function result. We could instead try to do this + // at runtime by clearning the return register... but the problem is that we need + // to handle non-primitive types like strings and the runtime doesn't know the type + // of the result to properly allocate it. + match vtype { + ExprType::Boolean | ExprType::Integer => { + ctx.codegen.emit(bytecode::make_load_integer(ret_reg, 0), key_pos); + } + ExprType::Double | ExprType::Text => { + let index = ctx.codegen.get_constant(Datum::new(vtype), key_pos)?; + ctx.codegen.emit(bytecode::make_load_integer(ret_reg, index), key_pos); + } + } + } + for param in callable.params { + let key = SymbolKey::from(param.name); + symtable + .put_local(key.clone(), param.ref_type.unwrap_or(ExprType::Integer)) + .map_err(|e| Error::from_syms(e, key_pos))?; } compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 6fa8dfa0..d729a9dd 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -206,7 +206,9 @@ impl Context { let (reg, offset) = bytecode::parse_call(instr); self.call_stack.push(Frame { old_pc: self.pc, old_fp: self.fp, ret_reg: Some(reg) }); self.pc = Address::from(offset); - self.fp = self.regs.len(); + let (is_global, index) = reg.to_parts(); + debug_assert!(!is_global, "Function results are always stored to a temp register"); + self.fp += usize::from(index); } /// Implements the `Concat` opcode. diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index ce853937..e20aae29 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -31,6 +31,7 @@ macro_rules! one_test { }; } +one_test!(test_args); one_test!(test_arithmetic); one_test!(test_assignments); one_test!(test_empty); @@ -43,7 +44,6 @@ one_test!(test_out_of_registers); one_test!(test_strings); one_test!(test_subs); one_test!(test_types); -one_test!(test_varargs); #[test] fn test_all_md_files_registered() -> io::Result<()> { diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md new file mode 100644 index 00000000..94d872df --- /dev/null +++ b/core2/tests/test_args.md @@ -0,0 +1,434 @@ +# Test: Singular required argument, not provided + +## Source + +```basic +OUT_REQUIRED_VALUE +``` + +## Compilation errors + +```plain +1:1: OUT_REQUIRED_VALUE expected arg% +``` + +# Test: Singular required argument, mismatched type + +## Source + +```basic +OUT_REQUIRED_VALUE "Foo" +``` + +## Compilation errors + +```plain +1:20: STRING is not a number +``` + +# Test: Singular required argument, correct type + +## Source + +```basic +OUT_REQUIRED_VALUE 4 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 4 # 1:20 +0002: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Output + +```plain +4 +``` + +# Test: Singular required argument, type casting + +## Source + +```basic +OUT_REQUIRED_VALUE 7.8 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADC R64, 0 # 1:20 +0002: DTOI R64 # 1:20 +0003: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 +``` + +## Output + +```plain +8 +``` + +# Test: Singular optional argument, not provided + +## Source + +```basic +OUT_OPTIONAL +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 1:1 +0002: UPCALL 0, R64 # 1:1, OUT_OPTIONAL +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Output + +```plain +() +``` + +# Test: Singular optional argument, provided + +## Source + +```basic +OUT_OPTIONAL "Foo" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 1:14 +0002: LOADI R64, 259 # 1:14 +0003: UPCALL 0, R64 # 1:1, OUT_OPTIONAL +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 +``` + +## Output + +```plain +Foo$ +``` + +# Test: Singular optional argument, too many provided + +## Source + +```basic +OUT_OPTIONAL "Foo", "Bar" +``` + +## Compilation errors + +```plain +1:1: OUT_OPTIONAL expected [arg$] +``` + +# Test: Singular argument of any type, not optional + +## Source + +```basic +OUT_ANY_VALUE TRUE +OUT_ANY_VALUE "Text" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 1 # 1:15 +0002: LOADI R64, 256 # 1:15 +0003: UPCALL 0, R64 # 1:1, OUT_ANY_VALUE +0004: LOADI R65, 0 # 2:15 +0005: LOADI R64, 259 # 2:15 +0006: UPCALL 0, R64 # 2:1, OUT_ANY_VALUE +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +true? +Text$ +``` + +# Test: Singular argument of any type, too many provided + +## Source + +```basic +OUT_ANY_VALUE TRUE, FALSE +``` + +## Compilation errors + +```plain +1:1: OUT_ANY_VALUE expected arg +``` + +# Test: Singular argument of any type, optional + +## Source + +```basic +OUT_ANY_VALUE_OPTIONAL +OUT_ANY_VALUE_OPTIONAL TRUE +OUT_ANY_VALUE_OPTIONAL "Text" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 0 # 1:1 +0002: UPCALL 0, R64 # 1:1, OUT_ANY_VALUE_OPTIONAL +0003: LOADI R65, 1 # 2:24 +0004: LOADI R64, 256 # 2:24 +0005: UPCALL 0, R64 # 2:1, OUT_ANY_VALUE_OPTIONAL +0006: LOADI R65, 0 # 3:24 +0007: LOADI R64, 259 # 3:24 +0008: UPCALL 0, R64 # 3:1, OUT_ANY_VALUE_OPTIONAL +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +() +true? +Text$ +``` + +# Test: Singular argument of any type, too many provided + +## Source + +```basic +OUT_ANY_VALUE_OPTIONAL "Text", 3 +``` + +## Compilation errors + +```plain +1:1: OUT_ANY_VALUE_OPTIONAL expected [arg] +``` + +# Test: Singular arguments of various kinds, with type casting + +## Source + +```basic +OUT_POSITIONAL 3, 5.6 AS "Foo" +OUT_POSITIONAL "A"; 4 AS "Foo" +OUT_POSITIONAL ; 0 AS 8.2 +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R65, 3 # 1:16 +0002: LOADI R64, 290 # 1:16 +0003: LOADC R66, 0 # 1:19 +0004: DTOI R66 # 1:19 +0005: LOADI R68, 1 # 1:26 +0006: LOADI R67, 259 # 1:26 +0007: UPCALL 0, R64 # 1:1, OUT_POSITIONAL +0008: LOADI R65, 2 # 2:16 +0009: LOADI R64, 275 # 2:16 +0010: LOADI R66, 4 # 2:21 +0011: LOADI R68, 1 # 2:26 +0012: LOADI R67, 259 # 2:26 +0013: UPCALL 0, R64 # 2:1, OUT_POSITIONAL +0014: LOADI R64, 16 # 3:16 +0015: LOADI R65, 0 # 3:18 +0016: LOADC R67, 3 # 3:23 +0017: LOADI R66, 257 # 3:23 +0018: UPCALL 0, R64 # 3:1, OUT_POSITIONAL +0019: LOADI R64, 0 # 0:0 +0020: END R64 # 0:0 +``` + +## Output + +```plain +3% +6 +Foo$ +A$ +4 +Foo$ +() +0 +8.2# +``` + +# Test: Singular arguments of various kinds, mismatched separators + +## Source + +```basic +OUT_POSITIONAL 3 AS 5.6 AS "Foo" +``` + +## Compilation errors + +```plain +1:16: OUT_POSITIONAL expected [arg1] <,|;> arg2% AS arg3 +``` + +# Test: Repeated arguments, none provided + +## Source + +```basic +OUT +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: UPCALL 0, R64 # 1:1, OUT +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 +``` + +## Output + +```plain +0=() +``` + +# Test: Repeated arguments, several present + +## Source + +```basic +OUT 100, 200, 300 +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R67, 200 # 1:10 +0004: LOADI R66, 290 # 1:10 +0005: LOADI R69, 300 # 1:15 +0006: LOADI R68, 258 # 1:15 +0007: UPCALL 0, R64 # 1:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=200% , 2=300% +``` + +# Test: Repeated arguments, some missing + +## Source + +```basic +OUT 100, , 300, +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R66, 32 # 1:10 +0004: LOADI R68, 300 # 1:12 +0005: LOADI R67, 290 # 1:12 +0006: LOADI R69, 0 # 1:16 +0007: UPCALL 0, R64 # 1:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=() , 2=300% , 3=() +``` + +# Test: Repeated arguments, different separators + +## Source + +```basic +OUT 100; 200 AS 300; 400 +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 274 # 1:5 +0003: LOADI R67, 200 # 1:10 +0004: LOADI R66, 306 # 1:10 +0005: LOADI R69, 300 # 1:17 +0006: LOADI R68, 274 # 1:17 +0007: LOADI R71, 400 # 1:22 +0008: LOADI R70, 258 # 1:22 +0009: UPCALL 0, R64 # 1:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=100% ; 1=200% AS 2=300% ; 3=400% +``` + +# Test: Repeated arguments, different types + +## Source + +```basic +OUT 100, "Foo" +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 100 # 1:5 +0002: LOADI R64, 290 # 1:5 +0003: LOADI R67, 0 # 1:10 +0004: LOADI R66, 259 # 1:10 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=100% , 1=Foo$ +``` diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 436620b0..2bbad18b 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -7,7 +7,7 @@ FUNCTION foo$ foo = "abc" END FUNCTION -OUT foo$(2) +OUT foo$ ``` ## Disassembly @@ -21,9 +21,10 @@ OUT foo$(2) 0005: END R64 # 0:0 -- FOO -0006: ENTER 1 # 0:0 -0007: LOADI R64, 0 # 2:11 -0008: RETURN # 3:1 +0006: LOADI R64, 0 # 1:10 +0007: ENTER 1 # 0:0 +0008: LOADI R64, 1 # 2:11 +0009: RETURN # 3:1 ``` ## Output @@ -41,7 +42,7 @@ FUNCTION foo$ foo = 3 END FUNCTION -OUT foo$(2) +OUT foo$ ``` ## Compilation errors @@ -64,7 +65,7 @@ FUNCTION foo END FUNCTION OUT "Before", a -OUT "Return value", foo(2) +OUT "Return value", foo OUT "After", a ``` @@ -92,15 +93,16 @@ OUT "After", a 0018: END R65 # 0:0 -- FOO -0019: ENTER 6 # 0:0 -0020: LOADI R65, 20 # 4:9 -0021: LOADI R67, 3 # 5:9 -0022: LOADI R66, 291 # 5:9 -0023: MOVE R69, R65 # 5:19 -0024: LOADI R68, 258 # 5:19 -0025: UPCALL 0, R66 # 5:5, OUT -0026: LOADI R64, 30 # 6:11 -0027: RETURN # 7:1 +0019: LOADI R64, 0 # 3:10 +0020: ENTER 6 # 0:0 +0021: LOADI R65, 20 # 4:9 +0022: LOADI R67, 3 # 5:9 +0023: LOADI R66, 291 # 5:9 +0024: MOVE R69, R65 # 5:19 +0025: LOADI R68, 258 # 5:19 +0026: UPCALL 0, R66 # 5:5, OUT +0027: LOADI R64, 30 # 6:11 +0028: RETURN # 7:1 ``` ## Output @@ -123,34 +125,36 @@ FUNCTION first END FUNCTION FUNCTION second - second = first(0) + second = first END FUNCTION -OUT second(0) +OUT second ``` ## Disassembly ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 12 # 10:5, SECOND +0001: CALL R65, 13 # 10:5, SECOND 0002: LOADI R64, 258 # 10:5 0003: UPCALL 0, R64 # 10:1, OUT 0004: LOADI R64, 0 # 0:0 0005: END R64 # 0:0 -- FIRST -0006: ENTER 3 # 0:0 -0007: LOADI R66, 0 # 2:9 -0008: LOADI R65, 259 # 2:9 -0009: UPCALL 0, R65 # 2:5, OUT -0010: LOADI R64, 123 # 3:13 -0011: RETURN # 4:1 +0006: LOADI R64, 0 # 1:10 +0007: ENTER 3 # 0:0 +0008: LOADI R66, 0 # 2:9 +0009: LOADI R65, 259 # 2:9 +0010: UPCALL 0, R65 # 2:5, OUT +0011: LOADI R64, 123 # 3:13 +0012: RETURN # 4:1 -- SECOND -0012: ENTER 1 # 0:0 -0013: CALL R64, 6 # 7:14, FIRST -0014: RETURN # 8:1 +0013: LOADI R64, 0 # 6:10 +0014: ENTER 1 # 0:0 +0015: CALL R64, 6 # 7:14, FIRST +0016: RETURN # 8:1 ``` ## Output @@ -160,26 +164,105 @@ OUT second(0) 0=123% ``` +# Test: Default return value is reset + +## Source + +```basic +FUNCTION default_double# +END FUNCTION + +FUNCTION default_integer +END FUNCTION + +FUNCTION default_string$ +END FUNCTION + +FUNCTION do_call + OUT 300 + OUT default_double ' Needs to print 0. + OUT default_integer ' Needs to print 0. + OUT default_string ' Needs to print "". +END FUNCTION + +OUT do_call +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: CALL R65, 15 # 17:5, DO_CALL +0002: LOADI R64, 258 # 17:5 +0003: UPCALL 0, R64 # 17:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 + +-- DEFAULT_DOUBLE +0006: LOADI R64, 0 # 1:10 +0007: ENTER 1 # 0:0 +0008: RETURN # 2:1 + +-- DEFAULT_INTEGER +0009: LOADI R64, 0 # 4:10 +0010: ENTER 1 # 0:0 +0011: RETURN # 5:1 + +-- DEFAULT_STRING +0012: LOADI R64, 1 # 7:10 +0013: ENTER 1 # 0:0 +0014: RETURN # 8:1 + +-- DO_CALL +0015: LOADI R64, 0 # 10:10 +0016: ENTER 3 # 0:0 +0017: LOADI R66, 300 # 11:9 +0018: LOADI R65, 258 # 11:9 +0019: UPCALL 0, R65 # 11:5, OUT +0020: CALL R66, 6 # 12:9, DEFAULT_DOUBLE +0021: LOADI R65, 257 # 12:9 +0022: UPCALL 0, R65 # 12:5, OUT +0023: CALL R66, 9 # 13:9, DEFAULT_INTEGER +0024: LOADI R65, 258 # 13:9 +0025: UPCALL 0, R65 # 13:5, OUT +0026: CALL R66, 12 # 14:9, DEFAULT_STRING +0027: LOADI R65, 259 # 14:9 +0028: UPCALL 0, R65 # 14:5, OUT +0029: RETURN # 15:1 +``` + +## Output + +```plain +0=300% +0=0# +0=0% +0=$ +0=0% +``` + # Test: Local variables ## Source ```basic FUNCTION modify_2 - var = 2 + var = 300 + modify_2 = 2000 OUT "Inside modify_2", var END FUNCTION FUNCTION modify_1 - var = 1 + var = 200 OUT "Before modify_2", var - OUT modify_2(2) + OUT modify_2 OUT "After modify_2", var + modify_1 = 1000 END FUNCTION -var = 0 +var = 100 OUT "Before modify_1", var -OUT modify_1(2) +OUT modify_1 OUT "After modify_1", var ``` @@ -187,62 +270,66 @@ OUT "After modify_1", var ```asm 0000: ENTER 5 # 0:0 -0001: LOADI R64, 0 # 13:7 -0002: LOADI R66, 0 # 14:5 -0003: LOADI R65, 291 # 14:5 -0004: MOVE R68, R64 # 14:24 -0005: LOADI R67, 258 # 14:24 -0006: UPCALL 0, R65 # 14:1, OUT -0007: CALL R66, 25 # 15:5, MODIFY_1 -0008: LOADI R65, 258 # 15:5 -0009: UPCALL 0, R65 # 15:1, OUT -0010: LOADI R66, 1 # 16:5 -0011: LOADI R65, 291 # 16:5 -0012: MOVE R68, R64 # 16:23 -0013: LOADI R67, 258 # 16:23 -0014: UPCALL 0, R65 # 16:1, OUT +0001: LOADI R64, 100 # 15:7 +0002: LOADI R66, 0 # 16:5 +0003: LOADI R65, 291 # 16:5 +0004: MOVE R68, R64 # 16:24 +0005: LOADI R67, 258 # 16:24 +0006: UPCALL 0, R65 # 16:1, OUT +0007: CALL R66, 27 # 17:5, MODIFY_1 +0008: LOADI R65, 258 # 17:5 +0009: UPCALL 0, R65 # 17:1, OUT +0010: LOADI R66, 1 # 18:5 +0011: LOADI R65, 291 # 18:5 +0012: MOVE R68, R64 # 18:23 +0013: LOADI R67, 258 # 18:23 +0014: UPCALL 0, R65 # 18:1, OUT 0015: LOADI R65, 0 # 0:0 0016: END R65 # 0:0 -- MODIFY_2 -0017: ENTER 6 # 0:0 -0018: LOADI R65, 2 # 2:11 -0019: LOADI R67, 2 # 3:9 -0020: LOADI R66, 291 # 3:9 -0021: MOVE R69, R65 # 3:28 -0022: LOADI R68, 258 # 3:28 -0023: UPCALL 0, R66 # 3:5, OUT -0024: RETURN # 4:1 +0017: LOADI R64, 0 # 1:10 +0018: ENTER 6 # 0:0 +0019: LOADI R65, 300 # 2:11 +0020: LOADI R64, 2000 # 3:16 +0021: LOADI R67, 2 # 4:9 +0022: LOADI R66, 291 # 4:9 +0023: MOVE R69, R65 # 4:28 +0024: LOADI R68, 258 # 4:28 +0025: UPCALL 0, R66 # 4:5, OUT +0026: RETURN # 5:1 -- MODIFY_1 -0025: ENTER 6 # 0:0 -0026: LOADI R65, 1 # 7:11 -0027: LOADI R67, 3 # 8:9 -0028: LOADI R66, 291 # 8:9 -0029: MOVE R69, R65 # 8:28 -0030: LOADI R68, 258 # 8:28 -0031: UPCALL 0, R66 # 8:5, OUT -0032: CALL R67, 17 # 9:9, MODIFY_2 -0033: LOADI R66, 258 # 9:9 +0027: LOADI R64, 0 # 7:10 +0028: ENTER 6 # 0:0 +0029: LOADI R65, 200 # 8:11 +0030: LOADI R67, 3 # 9:9 +0031: LOADI R66, 291 # 9:9 +0032: MOVE R69, R65 # 9:28 +0033: LOADI R68, 258 # 9:28 0034: UPCALL 0, R66 # 9:5, OUT -0035: LOADI R67, 4 # 10:9 -0036: LOADI R66, 291 # 10:9 -0037: MOVE R69, R65 # 10:27 -0038: LOADI R68, 258 # 10:27 -0039: UPCALL 0, R66 # 10:5, OUT -0040: RETURN # 11:1 +0035: CALL R67, 17 # 10:9, MODIFY_2 +0036: LOADI R66, 258 # 10:9 +0037: UPCALL 0, R66 # 10:5, OUT +0038: LOADI R67, 4 # 11:9 +0039: LOADI R66, 291 # 11:9 +0040: MOVE R69, R65 # 11:27 +0041: LOADI R68, 258 # 11:27 +0042: UPCALL 0, R66 # 11:5, OUT +0043: LOADI R64, 1000 # 12:16 +0044: RETURN # 13:1 ``` ## Output ```plain -0=Before modify_1$ , 1=0% -0=Before modify_2$ , 1=1% -0=Inside modify_2$ , 1=2% -0=0% -0=After modify_2$ , 1=1% -0=0% -0=After modify_1$ , 1=0% +0=Before modify_1$ , 1=100% +0=Before modify_2$ , 1=200% +0=Inside modify_2$ , 1=300% +0=2000% +0=After modify_2$ , 1=200% +0=1000% +0=After modify_1$ , 1=100% ``` # Test: Local is not global @@ -254,7 +341,7 @@ FUNCTION set_local local_var = 8 END FUNCTION -OUT set_local(2) +OUT set_local OUT local_var ``` @@ -263,3 +350,156 @@ OUT local_var ```plain 6:5: Undefined global symbol local_var ``` + +# Test: Argument passing + +## Source + +```basic +FUNCTION add(a, b) + add = a + b +END FUNCTION + +OUT add(3, 5) + add(10, 20) +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R67, 3 # 5:9 +0002: LOADI R68, 5 # 5:12 +0003: CALL R66, 12 # 5:5, ADD +0004: LOADI R68, 10 # 5:21 +0005: LOADI R69, 20 # 5:25 +0006: CALL R67, 12 # 5:17, ADD +0007: ADDI R65, R66, R67 # 5:15 +0008: LOADI R64, 258 # 5:5 +0009: UPCALL 0, R64 # 5:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 + +-- ADD +0012: LOADI R64, 0 # 1:10 +0013: ENTER 5 # 0:0 +0014: MOVE R67, R65 # 2:11 +0015: MOVE R68, R66 # 2:15 +0016: ADDI R64, R67, R68 # 2:13 +0017: RETURN # 3:1 +``` + +## Output + +```plain +0=38% +``` + +# Test: Argument passing with result saved to global + +## Source + +```basic +FUNCTION foo(i) + foo = 42 + i +END FUNCTION + +DIM SHARED ret +ret = foo(3) +OUT ret +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R0, 0 # 5:12 +0002: LOADI R65, 3 # 6:11 +0003: CALL R64, 10 # 6:7, FOO +0004: MOVE R0, R64 # 6:7 +0005: MOVE R65, R0 # 7:5 +0006: LOADI R64, 258 # 7:5 +0007: UPCALL 0, R64 # 7:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 + +-- FOO +0010: LOADI R64, 0 # 1:10 +0011: ENTER 4 # 0:0 +0012: LOADI R66, 42 # 2:11 +0013: MOVE R67, R65 # 2:16 +0014: ADDI R64, R66, R67 # 2:14 +0015: RETURN # 3:1 +``` + +## Output + +```plain +0=45% +``` + +# Test: Arguments are passed by value + +## Source + +```basic +FUNCTION change_integer(i%) + i = 3 +END FUNCTION + +FUNCTION change_string(s$) + s = "foo" +END FUNCTION + +i = 5 +OUT change_integer(i) +OUT i + +s = "bar" +OUT change_string(s) +OUT s +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 5 # 9:5 +0002: MOVE R67, R64 # 10:20 +0003: CALL R66, 19 # 10:5, CHANGE_INTEGER +0004: LOADI R65, 258 # 10:5 +0005: UPCALL 0, R65 # 10:1, OUT +0006: MOVE R66, R64 # 11:5 +0007: LOADI R65, 258 # 11:5 +0008: UPCALL 0, R65 # 11:1, OUT +0009: LOADI R65, 0 # 13:5 +0010: MOVE R68, R65 # 14:19 +0011: CALL R67, 23 # 14:5, CHANGE_STRING +0012: LOADI R66, 258 # 14:5 +0013: UPCALL 0, R66 # 14:1, OUT +0014: MOVE R67, R65 # 15:5 +0015: LOADI R66, 259 # 15:5 +0016: UPCALL 0, R66 # 15:1, OUT +0017: LOADI R66, 0 # 0:0 +0018: END R66 # 0:0 + +-- CHANGE_INTEGER +0019: LOADI R64, 0 # 1:10 +0020: ENTER 2 # 0:0 +0021: LOADI R65, 3 # 2:9 +0022: RETURN # 3:1 + +-- CHANGE_STRING +0023: LOADI R64, 0 # 5:10 +0024: ENTER 2 # 0:0 +0025: LOADI R65, 1 # 6:9 +0026: RETURN # 7:1 +``` + +## Output + +```plain +0=0% +0=5% +0=0% +0=bar$ +``` diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index 6d7cb17b..3bbc0871 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -109,7 +109,7 @@ END FUNCTION DIM SHARED i1 i1 = 2 OUT "Before", i1 -OUT modify_global(2) +OUT modify_global OUT "After", i1 ``` @@ -136,14 +136,15 @@ OUT "After", i1 0017: END R64 # 0:0 -- MODIFY_GLOBAL -0018: ENTER 5 # 0:0 -0019: LOADI R0, 3 # 2:10 -0020: LOADI R66, 2 # 3:9 -0021: LOADI R65, 291 # 3:9 -0022: MOVE R68, R0 # 3:25 -0023: LOADI R67, 258 # 3:25 -0024: UPCALL 0, R65 # 3:5, OUT -0025: RETURN # 4:1 +0018: LOADI R64, 0 # 1:10 +0019: ENTER 5 # 0:0 +0020: LOADI R0, 3 # 2:10 +0021: LOADI R66, 2 # 3:9 +0022: LOADI R65, 291 # 3:9 +0023: MOVE R68, R0 # 3:25 +0024: LOADI R67, 258 # 3:25 +0025: UPCALL 0, R65 # 3:5, OUT +0026: RETURN # 4:1 ``` ## Output @@ -183,25 +184,24 @@ OUT "After", i1 0005: MOVE R67, R0 # 8:15 0006: LOADI R66, 258 # 8:15 0007: UPCALL 0, R64 # 8:1, OUT -0008: LOADI R64, 0 # 9:1 -0009: CALL R64, 17 # 9:1, MODIFY_GLOBAL -0010: LOADI R65, 1 # 10:5 -0011: LOADI R64, 291 # 10:5 -0012: MOVE R67, R0 # 10:14 -0013: LOADI R66, 258 # 10:14 -0014: UPCALL 0, R64 # 10:1, OUT -0015: LOADI R64, 0 # 0:0 -0016: END R64 # 0:0 +0008: CALL R64, 16 # 9:1, MODIFY_GLOBAL +0009: LOADI R65, 1 # 10:5 +0010: LOADI R64, 291 # 10:5 +0011: MOVE R67, R0 # 10:14 +0012: LOADI R66, 258 # 10:14 +0013: UPCALL 0, R64 # 10:1, OUT +0014: LOADI R64, 0 # 0:0 +0015: END R64 # 0:0 -- MODIFY_GLOBAL -0017: ENTER 4 # 0:0 -0018: LOADI R0, 3 # 2:10 -0019: LOADI R65, 2 # 3:9 -0020: LOADI R64, 291 # 3:9 -0021: MOVE R67, R0 # 3:25 -0022: LOADI R66, 258 # 3:25 -0023: UPCALL 0, R64 # 3:5, OUT -0024: RETURN # 4:1 +0016: ENTER 4 # 0:0 +0017: LOADI R0, 3 # 2:10 +0018: LOADI R65, 2 # 3:9 +0019: LOADI R64, 291 # 3:9 +0020: MOVE R67, R0 # 3:25 +0021: LOADI R66, 258 # 3:25 +0022: UPCALL 0, R64 # 3:5, OUT +0023: RETURN # 4:1 ``` ## Output diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index cb8c4f53..f5ceccc1 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -25,25 +25,24 @@ OUT "After", a 0004: MOVE R68, R64 # 8:15 0005: LOADI R67, 258 # 8:15 0006: UPCALL 0, R65 # 8:1, OUT -0007: LOADI R65, 0 # 9:1 -0008: CALL R65, 16 # 9:1, FOO -0009: LOADI R66, 1 # 10:5 -0010: LOADI R65, 291 # 10:5 -0011: MOVE R68, R64 # 10:14 -0012: LOADI R67, 258 # 10:14 -0013: UPCALL 0, R65 # 10:1, OUT -0014: LOADI R65, 0 # 0:0 -0015: END R65 # 0:0 +0007: CALL R65, 15 # 9:1, FOO +0008: LOADI R66, 1 # 10:5 +0009: LOADI R65, 291 # 10:5 +0010: MOVE R68, R64 # 10:14 +0011: LOADI R67, 258 # 10:14 +0012: UPCALL 0, R65 # 10:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 -- FOO -0016: ENTER 5 # 0:0 -0017: LOADI R64, 20 # 4:9 -0018: LOADI R66, 2 # 5:9 -0019: LOADI R65, 291 # 5:9 -0020: MOVE R68, R64 # 5:19 -0021: LOADI R67, 258 # 5:19 -0022: UPCALL 0, R65 # 5:5, OUT -0023: RETURN # 6:1 +0015: ENTER 5 # 0:0 +0016: LOADI R64, 20 # 4:9 +0017: LOADI R66, 2 # 5:9 +0018: LOADI R65, 291 # 5:9 +0019: MOVE R68, R64 # 5:19 +0020: LOADI R67, 258 # 5:19 +0021: UPCALL 0, R65 # 5:5, OUT +0022: RETURN # 6:1 ``` ## Output @@ -74,23 +73,21 @@ second ```asm 0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 9:1 -0002: CALL R64, 10 # 9:1, SECOND -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0001: CALL R64, 9 # 9:1, SECOND +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 -- FIRST -0005: ENTER 2 # 0:0 -0006: LOADI R65, 0 # 2:9 -0007: LOADI R64, 259 # 2:9 -0008: UPCALL 0, R64 # 2:5, OUT -0009: RETURN # 3:1 +0004: ENTER 2 # 0:0 +0005: LOADI R65, 0 # 2:9 +0006: LOADI R64, 259 # 2:9 +0007: UPCALL 0, R64 # 2:5, OUT +0008: RETURN # 3:1 -- SECOND -0010: ENTER 1 # 0:0 -0011: LOADI R64, 0 # 6:5 -0012: CALL R64, 5 # 6:5, FIRST -0013: RETURN # 7:1 +0009: ENTER 0 # 0:0 +0010: CALL R64, 4 # 6:5, FIRST +0011: RETURN # 7:1 ``` ## Output @@ -146,42 +143,40 @@ OUT "After modify_1", var 0004: MOVE R68, R64 # 14:24 0005: LOADI R67, 258 # 14:24 0006: UPCALL 0, R65 # 14:1, OUT -0007: LOADI R65, 0 # 15:1 -0008: CALL R65, 24 # 15:1, MODIFY_1 -0009: LOADI R66, 1 # 16:5 -0010: LOADI R65, 291 # 16:5 -0011: MOVE R68, R64 # 16:23 -0012: LOADI R67, 258 # 16:23 -0013: UPCALL 0, R65 # 16:1, OUT -0014: LOADI R65, 0 # 0:0 -0015: END R65 # 0:0 +0007: CALL R65, 23 # 15:1, MODIFY_1 +0008: LOADI R66, 1 # 16:5 +0009: LOADI R65, 291 # 16:5 +0010: MOVE R68, R64 # 16:23 +0011: LOADI R67, 258 # 16:23 +0012: UPCALL 0, R65 # 16:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 -- MODIFY_2 -0016: ENTER 5 # 0:0 -0017: LOADI R64, 2 # 2:11 -0018: LOADI R66, 2 # 3:9 -0019: LOADI R65, 291 # 3:9 -0020: MOVE R68, R64 # 3:28 -0021: LOADI R67, 258 # 3:28 -0022: UPCALL 0, R65 # 3:5, OUT -0023: RETURN # 4:1 +0015: ENTER 5 # 0:0 +0016: LOADI R64, 2 # 2:11 +0017: LOADI R66, 2 # 3:9 +0018: LOADI R65, 291 # 3:9 +0019: MOVE R68, R64 # 3:28 +0020: LOADI R67, 258 # 3:28 +0021: UPCALL 0, R65 # 3:5, OUT +0022: RETURN # 4:1 -- MODIFY_1 -0024: ENTER 5 # 0:0 -0025: LOADI R64, 1 # 7:11 -0026: LOADI R66, 3 # 8:9 -0027: LOADI R65, 291 # 8:9 -0028: MOVE R68, R64 # 8:28 -0029: LOADI R67, 258 # 8:28 -0030: UPCALL 0, R65 # 8:5, OUT -0031: LOADI R65, 0 # 9:5 -0032: CALL R65, 16 # 9:5, MODIFY_2 -0033: LOADI R66, 4 # 10:9 -0034: LOADI R65, 291 # 10:9 -0035: MOVE R68, R64 # 10:27 -0036: LOADI R67, 258 # 10:27 -0037: UPCALL 0, R65 # 10:5, OUT -0038: RETURN # 11:1 +0023: ENTER 5 # 0:0 +0024: LOADI R64, 1 # 7:11 +0025: LOADI R66, 3 # 8:9 +0026: LOADI R65, 291 # 8:9 +0027: MOVE R68, R64 # 8:28 +0028: LOADI R67, 258 # 8:28 +0029: UPCALL 0, R65 # 8:5, OUT +0030: CALL R65, 15 # 9:5, MODIFY_2 +0031: LOADI R66, 4 # 10:9 +0032: LOADI R65, 291 # 10:9 +0033: MOVE R68, R64 # 10:27 +0034: LOADI R67, 258 # 10:27 +0035: UPCALL 0, R65 # 10:5, OUT +0036: RETURN # 11:1 ``` ## Output diff --git a/core2/tests/test_varargs.md b/core2/tests/test_varargs.md deleted file mode 100644 index 109242e0..00000000 --- a/core2/tests/test_varargs.md +++ /dev/null @@ -1,139 +0,0 @@ -# Test: No arguments - -## Source - -```basic -OUT -``` - -## Disassembly - -```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 1:1 -0002: UPCALL 0, R64 # 1:1, OUT -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 -``` - -## Output - -```plain -0=() -``` - -# Test: Multiple arguments, all present - -## Source - -```basic -OUT 100, 200, 300 -``` - -## Disassembly - -```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R67, 200 # 1:10 -0004: LOADI R66, 290 # 1:10 -0005: LOADI R69, 300 # 1:15 -0006: LOADI R68, 258 # 1:15 -0007: UPCALL 0, R64 # 1:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 -``` - -## Output - -```plain -0=100% , 1=200% , 2=300% -``` - -# Test: Multiple arguments, some missing - -## Source - -```basic -OUT 100, , 300, -``` - -## Disassembly - -```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R66, 32 # 1:10 -0004: LOADI R68, 300 # 1:12 -0005: LOADI R67, 290 # 1:12 -0006: LOADI R69, 0 # 1:16 -0007: UPCALL 0, R64 # 1:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 -``` - -## Output - -```plain -0=100% , 1=() , 2=300% , 3=() -``` - -# Test: Multiple arguments, different separators - -## Source - -```basic -OUT 100; 200 AS 300, 400 -``` - -## Disassembly - -```asm -0000: ENTER 8 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 274 # 1:5 -0003: LOADI R67, 200 # 1:10 -0004: LOADI R66, 306 # 1:10 -0005: LOADI R69, 300 # 1:17 -0006: LOADI R68, 290 # 1:17 -0007: LOADI R71, 400 # 1:22 -0008: LOADI R70, 258 # 1:22 -0009: UPCALL 0, R64 # 1:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 -``` - -## Output - -```plain -0=100% ; 1=200% AS 2=300% , 3=400% -``` - -# Test: Multiple arguments, different types - -## Source - -```basic -OUT 100, "Foo" -``` - -## Disassembly - -```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R67, 0 # 1:10 -0004: LOADI R66, 259 # 1:10 -0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 -``` - -## Output - -```plain -0=100% , 1=Foo$ -``` diff --git a/core2/tests/testutils/callables/.mod.rs.swp b/core2/tests/testutils/callables/.mod.rs.swp deleted file mode 100644 index 603401a2f44b8404ac92a56dab6bcbea47d65e1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI2?~5Bn7{{mDTD4ZizAVUG3N_JO_U@FTduJg{Pa50aL2_;F3rQ!tlVr>Oh&!`Q zL%EjH7mA=Ste_yMMg1F;S`>Vxud0^%P6U;T__he8FY0&l>vBo~djvft?9OTt02r3TOq!R3MU- z$!E8dr)Q?8RK%{t1l{r2kuig{n^r(8pcT*xXa%$aS^=$qRzNH8e^5ZyH<5Gb$UU9T z?CN}O8u;v->JP1eRzNGD70?Q31+)TM0j+>mKr5gX&ViC?5a+ z@BaRO^ASR>gYUrw@C;%6*OvnxJ0Vsei;PQ4tJ_a9w1~>&?0$K1B*a2=nM944T zXK)={0iT0qz`z8!gF0@5Yv6UD>bd~VfkR*)*bQ!><{!aT@ELd;Tm;LY0*--O4-oPj zxC$%it1t6)=zh6JRIU41U-`$fw{E z_z=7g-UV-fCMbavxU-p%KfpEc321^n;Lm#r`3k%P&VrZ0F0c)Jhd;`E3BCYVzqf>p#5N48y#d$W!2F$vh~lwm03N$XaD?aI?mEcS?w9 z9rMPw#|w}5iftKa7&skcecE`0TG7^yTDx`BD!K89{&m$haVs^FR^W@k<*C#xNzpUp z?&rMw8SiqHF$Kn$$DO<*)m6{Imf&&8hwa0bH6BVEavpfn#>EcXhb?QAG;kH>kDhlu zW!!=61E<}abF_KHLw&fk(V@<2^XjwJdzQ7@hW0aSIj^*>eCuqHdWPN?wRu`~(%ngo zG{$YOEYHTd6Qml*^H)+9|EAUE= zyY5_3hK^s|yEom+Hn8h6>25EQ^Z*mJ!>m5n+x(?f2D|CgT;~N|6(s$-@#nlEg3!TIg5=g^8+HfhPmsZPMOLz|`?&Ku-+Ra3VuUBsdKsSwcrmv$xf> zX9->#?l#P}-GqcoXqjfgnr+3k3|8Q}+>#DfPt)vlr@t9WAfhcvpS>J}VbCzmT+nD+ zyYDlXTJJNP=96tR6HVVL$&fkHH2Y3alVMEsR1%b*t_OnK6#YbJc{OCJrj{52{e()9 zZ}X7K8mEUGi(~o9C~N0Ctpx2hQRkLZaV+XGzZ$VBr`6yT4}A+RlOk>g5t7{ zS9cw5XDBUlt_oDiY>aW}lp~2g4OLDZvC07*Wj;-0i!@(M(0$orzL=)R@=FKsJA_Jh z#K)W90yF28$O)0K+@!3Ii(2XxarCsI%3#%ycLk)4(2?kJnu?$z8!S{csZ8pqNm(}y zqtR$&Se;olo(aOLfow`tVI0cm<`;_dlQWsAPEb(kmz^?cs#I#~$r^f6aZm(5ZSlIq z*x|XL^kBZlX#-mIheD#`K}0>)q(NCSI5W7{n<`yrAp6zIwyNv4r0G!}s==eEj3(7Y vNOXb{sae|WB=!T@DLaSEp;N6%Ix{u1hrSrOPCyTH+|_});Jj}urE}yjuNxLp diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index acb26565..3cb0029e 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -15,19 +15,66 @@ //! Callables exposed to integration tests. -use endbasic_core2::{Callable, SymbolKey}; +use endbasic_core2::*; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +mod out_any_value_cmd; +use out_any_value_cmd::OutAnyValueCommand; + +mod out_any_value_optional_cmd; +use out_any_value_optional_cmd::OutAnyValueOptionalCommand; + mod out_cmd; use out_cmd::OutCommand; +mod out_optional_cmd; +use out_optional_cmd::OutOptionalCommand; + +mod out_positional_cmd; +use out_positional_cmd::OutPositionalCommand; + +mod out_required_value_cmd; +use out_required_value_cmd::OutRequiredValueCommand; + +/// Formats the given argument `i` in `scope` as a string depending on its `etype`. +fn format_arg(scope: &Scope<'_>, i: u8, etype: ExprType) -> String { + match etype { + ExprType::Boolean => format!("{}", scope.get_boolean(i)), + ExprType::Double => format!("{}", scope.get_double(i)), + ExprType::Integer => format!("{}", scope.get_integer(i)), + ExprType::Text => scope.get_string(i).to_string(), + } +} + +/// Formats variable argument `i` in `Scope` and returns the formatted argument, whether the +/// argument was present, and the separator that was found. +fn format_vararg(scope: &Scope<'_>, i: u8) -> (String, bool, ArgSep) { + match scope.get_type(i) { + VarArgTag::Immediate(sep, etype) => { + let formatted = format_arg(scope, i + 1, etype); + (format!("{}{}", formatted, etype.annotation()), true, sep) + } + VarArgTag::Missing(sep) => ("()".to_owned(), false, sep), + VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + } +} + /// Registers all test-only callables into `upcalls_by_name`. pub(super) fn register_all( upcalls_by_name: &mut HashMap>, console: Rc>, ) { - let cmd = OutCommand::new(console); - upcalls_by_name.insert(SymbolKey::from(cmd.metadata().name()), cmd); + let cmds = [ + OutAnyValueCommand::new(console.clone()) as Rc, + OutAnyValueOptionalCommand::new(console.clone()) as Rc, + OutCommand::new(console.clone()) as Rc, + OutOptionalCommand::new(console.clone()) as Rc, + OutPositionalCommand::new(console.clone()) as Rc, + OutRequiredValueCommand::new(console) as Rc, + ]; + for cmd in cmds { + upcalls_by_name.insert(SymbolKey::from(cmd.metadata().name()), cmd); + } } diff --git a/core2/tests/testutils/callables/out_any_value_cmd.rs b/core2/tests/testutils/callables/out_any_value_cmd.rs new file mode 100644 index 00000000..042852d7 --- /dev/null +++ b/core2/tests/testutils/callables/out_any_value_cmd.rs @@ -0,0 +1,63 @@ +// 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. + +//! A callable exposed to integration tests. + +use super::format_vararg; +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints an argument of any type. +pub(super) struct OutAnyValueCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutAnyValueCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT_ANY_VALUE") + .with_syntax(&[( + &[SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg"), allow_missing: false }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutAnyValueCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let (formatted, _present, sep) = format_vararg(&scope, 0); + assert_eq!(ArgSep::End, sep, "Command only expects one argument"); + + let mut output = self.output.borrow_mut(); + output.push_str(&formatted); + output.push('\n'); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/out_any_value_optional_cmd.rs b/core2/tests/testutils/callables/out_any_value_optional_cmd.rs new file mode 100644 index 00000000..ffc383d2 --- /dev/null +++ b/core2/tests/testutils/callables/out_any_value_optional_cmd.rs @@ -0,0 +1,63 @@ +// 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. + +//! A callable exposed to integration tests. + +use super::format_vararg; +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints an argument of any type. +pub(super) struct OutAnyValueOptionalCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutAnyValueOptionalCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT_ANY_VALUE_OPTIONAL") + .with_syntax(&[( + &[SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg"), allow_missing: true }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutAnyValueOptionalCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let (formatted, _present, sep) = format_vararg(&scope, 0); + assert_eq!(ArgSep::End, sep, "Command only expects one argument"); + + let mut output = self.output.borrow_mut(); + output.push_str(&formatted); + output.push('\n'); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/out_cmd.rs b/core2/tests/testutils/callables/out_cmd.rs index f2187481..d565ed48 100644 --- a/core2/tests/testutils/callables/out_cmd.rs +++ b/core2/tests/testutils/callables/out_cmd.rs @@ -15,6 +15,7 @@ //! A callable exposed to integration tests. +use super::format_arg; use async_trait::async_trait; use endbasic_core2::*; use std::borrow::Cow; @@ -36,9 +37,9 @@ impl OutCommand { Some(&RepeatedSyntax { name: Cow::Borrowed("arg"), type_syn: RepeatedTypeSyntax::AnyValue, - sep: ArgSepSyntax::Exactly(ArgSep::Short), + sep: ArgSepSyntax::OneOf(&[ArgSep::As, ArgSep::Long, ArgSep::Short]), require_one: false, - allow_missing: false, + allow_missing: true, }), )]) .test_build(), @@ -61,12 +62,7 @@ impl Callable for OutCommand { let sep = match scope.get_type(reg) { VarArgTag::Immediate(sep, etype) => { reg += 1; - let formatted = match etype { - ExprType::Boolean => format!("{}", scope.get_boolean(reg)), - ExprType::Double => format!("{}", scope.get_double(reg)), - ExprType::Integer => format!("{}", scope.get_integer(reg)), - ExprType::Text => scope.get_string(reg).to_string(), - }; + let formatted = format_arg(&scope, reg, etype); line.push_str(&format!("{}={}{}", argi, formatted, etype.annotation())); sep } diff --git a/core2/tests/testutils/callables/out_optional_cmd.rs b/core2/tests/testutils/callables/out_optional_cmd.rs new file mode 100644 index 00000000..8e415506 --- /dev/null +++ b/core2/tests/testutils/callables/out_optional_cmd.rs @@ -0,0 +1,63 @@ +// 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. + +//! A callable exposed to integration tests. + +use super::format_vararg; +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints its single optional argument. +pub(super) struct OutOptionalCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutOptionalCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT_OPTIONAL") + .with_syntax(&[( + &[SingularArgSyntax::OptionalValue( + OptionalValueSyntax { name: Cow::Borrowed("arg"), vtype: ExprType::Text }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutOptionalCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let (formatted, _present, sep) = format_vararg(&scope, 0); + assert_eq!(ArgSep::End, sep, "Command only expects one argument"); + + let mut output = self.output.borrow_mut(); + output.push_str(&formatted); + output.push('\n'); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/out_positional_cmd.rs b/core2/tests/testutils/callables/out_positional_cmd.rs new file mode 100644 index 00000000..cfddba0d --- /dev/null +++ b/core2/tests/testutils/callables/out_positional_cmd.rs @@ -0,0 +1,94 @@ +// 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. + +//! A callable exposed to integration tests. + +use super::{format_arg, format_vararg}; +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints various positional arguments of different types. +pub(super) struct OutPositionalCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutPositionalCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT_POSITIONAL") + .with_syntax(&[( + &[ + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg1"), allow_missing: true }, + ArgSepSyntax::OneOf(&[ArgSep::Long, ArgSep::Short]), + ), + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg2"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::Exactly(ArgSep::As), + ), + SingularArgSyntax::AnyValue( + AnyValueSyntax { name: Cow::Borrowed("arg3"), allow_missing: false }, + ArgSepSyntax::End, + ), + ], + None, + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutPositionalCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut output = self.output.borrow_mut(); + + let mut i = 0; + + let (formatted, present, sep) = format_vararg(&scope, i); + assert_ne!(ArgSep::End, sep, "Command expects more arguments"); + output.push_str(&formatted); + output.push('\n'); + i += 1; + if present { + i += 1; + } + + let formatted = format_arg(&scope, i, ExprType::Integer); + output.push_str(&formatted); + output.push('\n'); + i += 1; + + let (formatted, present, sep) = format_vararg(&scope, i); + assert!(present, "Last argument is not optional"); + assert_eq!(ArgSep::End, sep, "No more arguments expected"); + output.push_str(&formatted); + output.push('\n'); + + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/out_required_value_cmd.rs b/core2/tests/testutils/callables/out_required_value_cmd.rs new file mode 100644 index 00000000..ff6c6241 --- /dev/null +++ b/core2/tests/testutils/callables/out_required_value_cmd.rs @@ -0,0 +1,63 @@ +// 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. + +//! A callable exposed to integration tests. + +use super::format_arg; +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that prints an argument of a specific type. +pub(super) struct OutRequiredValueCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl OutRequiredValueCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("OUT_REQUIRED_VALUE") + .with_syntax(&[( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + output, + }) + } +} + +#[async_trait(?Send)] +impl Callable for OutRequiredValueCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut output = self.output.borrow_mut(); + output.push_str(&format_arg(&scope, 0, ExprType::Integer)); + output.push('\n'); + Ok(()) + } +} From e7225007d02a5f726166df900d80810d46283709 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 15 Feb 2026 00:16:58 -0800 Subject: [PATCH 04/53] core2: Prepare the scope for mutability This gives the Scope mutable access to registers and the heap so that we can implement updates from upcalls. Additionally, this also gives the scope full visibility into the register bank so that we can access the current frame but also previous frames too (needed next for the addition of variable references). --- core2/src/callable.rs | 17 ++++++++++------- core2/src/vm/context.rs | 14 ++++++++++---- core2/src/vm/mod.rs | 11 ++++++----- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/core2/src/callable.rs b/core2/src/callable.rs index a859705b..e5bbfefd 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -569,39 +569,42 @@ impl CallableMetadata { /// Arguments provided to a callable during its execution. pub struct Scope<'a> { /// Slice of register values containing the callable's arguments. - pub(crate) regs: &'a [u64], + pub(crate) regs: &'a mut [u64], /// Reference to the constants pool for resolving constant pointers. pub(crate) constants: &'a [Datum], /// Reference to the heap for resolving heap pointers. - pub(crate) heap: &'a [Datum], + pub(crate) heap: &'a mut [Datum], + + /// Start of the current frame (where the arguments to the upcall start). + pub(crate) fp: usize, } impl<'a> Scope<'a> { /// Gets the type tag of the argument at `arg`. pub fn get_type(&self, arg: u8) -> VarArgTag { - VarArgTag::parse_u64(self.regs[arg as usize]).unwrap() + VarArgTag::parse_u64(self.regs[self.fp + (arg as usize)]).unwrap() } /// Gets the boolean value of the argument at `arg`. pub fn get_boolean(&self, arg: u8) -> bool { - self.regs[arg as usize] != 0 + self.regs[self.fp + (arg as usize)] != 0 } /// Gets the double value of the argument at `arg`. pub fn get_double(&self, arg: u8) -> f64 { - f64::from_bits(self.regs[arg as usize]) + f64::from_bits(self.regs[self.fp + (arg as usize)]) } /// Gets the integer value of the argument at `arg`. pub fn get_integer(&self, arg: u8) -> i32 { - self.regs[arg as usize] as i32 + self.regs[self.fp + (arg as usize)] as i32 } /// Gets the string value of the argument at `arg`. pub fn get_string(&self, arg: u8) -> &str { - let index = self.regs[arg as usize]; + let index = self.regs[self.fp + (arg as usize)]; let ptr = Pointer::from(index); match ptr.resolve(self.constants, self.heap) { Datum::Text(s) => s, diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index d729a9dd..90c507df 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -15,6 +15,7 @@ //! Virtual processor for EndBASIC execution. +use crate::Scope; use crate::bytecode::{self, Opcode, Register, opcode_of}; use crate::image::Image; use crate::mem::{Datum, Pointer}; @@ -124,13 +125,18 @@ impl Context { self.stop = Some(InternalStopReason::Exception(self.pc, message.into())); } - /// Gets a view of the local registers starting at `reg`. - pub(super) fn get_local_regs(&self, reg: Register) -> &[u64] { + /// Constructs a `Scope` for an upcall with arguments starting at `reg`. + pub(super) fn upcall_scope<'a>( + &'a mut self, + reg: Register, + constants: &'a [Datum], + heap: &'a mut [Datum], + ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); let index = usize::from(index); - let start = self.fp + index; - &self.regs[start..] + + Scope { regs: &mut self.regs, constants, heap, fp: self.fp + index } } /// Starts or resumes execution of `image`. diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index ece9e910..c0017a9e 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -33,13 +33,14 @@ pub struct UpcallHandler<'a>(&'a mut Vm); impl<'a> UpcallHandler<'a> { /// Invokes the pending upcall. - pub async fn invoke(mut self) -> CallResult<()> { - let vm = &mut self.0; + pub async fn invoke(self) -> CallResult<()> { + let vm = self.0; let (index, first_reg) = vm .pending_upcall .take() .expect("This is only reachable when the VM has a pending upcall"); - vm.upcalls[usize::from(index)].exec(vm.upcall_scope(first_reg)).await + let upcall = vm.upcalls[usize::from(index)].clone(); + upcall.exec(vm.upcall_scope(first_reg)).await } } @@ -107,12 +108,12 @@ impl Vm { } /// Constructs a `Scope` for an upcall with arguments starting at `reg`. - fn upcall_scope<'a>(&'a self, reg: Register) -> Scope<'a> { + fn upcall_scope<'a>(&'a mut self, reg: Register) -> Scope<'a> { let constants = match self.image.as_ref() { Some(image) => image.constants.as_slice(), None => &[], }; - Scope { regs: self.context.get_local_regs(reg), constants, heap: &self.heap } + self.context.upcall_scope(reg, constants, &mut self.heap) } /// Starts or resumes execution of the loaded image. From 4a5c9974b7409b3b941e29ae21581c3a4158128b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 14 Feb 2026 13:02:20 -0800 Subject: [PATCH 05/53] core2: Implement variable refs in callable arguments --- core2/src/bytecode.rs | 62 ++- core2/src/callable.rs | 141 ++++++- core2/src/compiler/args.rs | 120 +++++- core2/src/compiler/codegen.rs | 11 + core2/src/compiler/top.rs | 19 +- core2/src/image.rs | 1 + core2/src/num.rs | 18 + core2/src/testutils.rs | 7 +- core2/src/vm/context.rs | 11 +- core2/tests/test_args.md | 353 ++++++++++++++++++ .../callables/define_and_change_args_cmd.rs | 92 +++++ .../testutils/callables/define_arg_cmd.rs | 57 +++ .../callables/increment_required_int_cmd.rs | 67 ++++ core2/tests/testutils/callables/mod.rs | 17 +- core2/tests/testutils/callables/out_cmd.rs | 7 +- 15 files changed, 958 insertions(+), 25 deletions(-) create mode 100644 core2/tests/testutils/callables/define_and_change_args_cmd.rs create mode 100644 core2/tests/testutils/callables/define_arg_cmd.rs create mode 100644 core2/tests/testutils/callables/increment_required_int_cmd.rs diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index f2e717f4..a8beea45 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -16,7 +16,9 @@ //! Bytecode for a compiled EndBASIC program. use crate::ast::{ArgSep, ExprType}; -use crate::num::{unchecked_u32_as_u8, unchecked_u32_as_u16}; +use crate::num::{ + unchecked_u32_as_u8, unchecked_u32_as_u16, unchecked_u32_as_usize, unchecked_u64_as_u8, +}; use std::convert::TryFrom; use std::fmt; @@ -145,6 +147,41 @@ impl Register { pub(crate) fn to_parts(self) -> (bool, u8) { if self.0 < Self::MAX_GLOBAL { (true, self.0) } else { (false, self.0 - Self::MAX_GLOBAL) } } + + /// Creates a new tagged pointer for this register. + /// + /// A tagged pointer carries the type of the value pointed to by the register as well as the + /// absolute address of the register in the register bank. This address is calculated using + /// the current value of `fp`. + pub(crate) fn to_tagged_ptr(self, fp: usize, vtype: ExprType) -> u64 { + let (is_global, index) = self.to_parts(); + let mut index = usize::from(index); + if !is_global { + index += fp; + } + + let index = u32::try_from(index).expect("Cannot support that many registers"); + u64::from(vtype as u8) << 32 | u64::from(index) + } + + /// Parses a tagged pointer previously created by `to_tagged_ptr`, returning the absolute + /// address of the register and the type of the value pointed at. + /// + /// Panics if the tag is not value. + pub(crate) fn parse_tagged_ptr(ptr: u64) -> (usize, ExprType) { + let vtype: ExprType = { + #[allow(unsafe_code)] + unsafe { + let v = unchecked_u64_as_u8(ptr >> 32); + assert!(v <= ExprType::Text as u8); + std::mem::transmute(v) + } + }; + + let index = unchecked_u32_as_usize((ptr & 0xffffffff) as u32); + + (index, vtype) + } } impl RawValue for ExprType { @@ -152,7 +189,7 @@ impl RawValue for ExprType { #[allow(unsafe_code)] unsafe { let v = unchecked_u32_as_u8(v); - debug_assert!(v <= ExprType::Text as u8); + assert!(v <= ExprType::Text as u8); std::mem::transmute(v) } } @@ -298,6 +335,9 @@ pub(crate) enum Opcode { /// Loads an integer immediate into a register. LoadInteger, + /// Loads a register pointer into a register. + LoadRegisterPointer, + /// Moves (copies) data between two registers. Move, @@ -416,6 +456,15 @@ instr!( u16, 0x0000ffff, 0, // Immediate value. ); +#[rustfmt::skip] +instr!( + Opcode::LoadRegisterPointer, "LOADRP", + make_load_register_ptr, parse_load_register_ptr, format_load_register_ptr, + Register, 0x000000ff, 16, // Destination register to load the immediate into. + ExprType, 0x000000ff, 8, // Type of the value pointed to. + Register, 0x000000ff, 0, // Register to load. +); + #[rustfmt::skip] instr!( Opcode::Move, "MOVE", @@ -636,6 +685,15 @@ mod tests { 12345 ); + test_instr!( + test_load_register_ptr, + make_load_register_ptr, + parse_load_register_ptr, + Register::local(1).unwrap(), + ExprType::Double, + Register::local(2).unwrap() + ); + test_instr!( test_move, make_move, diff --git a/core2/src/callable.rs b/core2/src/callable.rs index e5bbfefd..0937b097 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -17,10 +17,13 @@ use crate::ast::ArgSep; use crate::ast::ExprType; +use crate::bytecode::Register; use crate::bytecode::VarArgTag; use crate::mem::{Datum, Pointer}; +use crate::num::unchecked_usize_as_u32; use async_trait::async_trait; use std::borrow::Cow; +use std::fmt; use std::ops::RangeInclusive; use std::str::Lines; @@ -566,6 +569,128 @@ impl CallableMetadata { } } +/// Provides some sort of validation when dereferencing register pointers. +pub struct TypedPointer<'a, 'vm> { + /// The scope through which to access the register. + scope: &'a Scope<'vm>, + + /// The absolute index of the register. + index: usize, + + /// The type of the value pointed to. + pub vtype: ExprType, +} + +impl<'a, 'vm> TypedPointer<'a, 'vm> { + /// Dereferences a pointer to a boolean. + pub fn deref_boolean(&self) -> bool { + assert_eq!(ExprType::Boolean, self.vtype); + self.scope.regs[self.index] != 0 + } + + /// Dereferences a pointer to a double. + pub fn deref_double(&self) -> f64 { + assert_eq!(ExprType::Double, self.vtype); + f64::from_bits(self.scope.regs[self.index]) + } + + /// Dereferences a pointer to an integer. + pub fn deref_integer(&self) -> i32 { + assert_eq!(ExprType::Integer, self.vtype); + self.scope.regs[self.index] as i32 + } + + /// Dereferences a pointer to a string. + pub fn deref_string(&self) -> &str { + assert_eq!(ExprType::Text, self.vtype); + let ptr = Pointer::from(self.scope.regs[self.index]); + match ptr.resolve(self.scope.constants, self.scope.heap) { + Datum::Text(s) => s, + _ => panic!("Mismatched constant type"), + } + } +} + +impl<'a, 'vm> fmt::Display for TypedPointer<'a, 'vm> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "&[R{}]{}", self.index, self.vtype) + } +} + +/// Provides some sort of validation when dereferencing register pointers. +pub struct TypedMutPointer<'a, 'vm> { + /// The scope through which to access the register. + scope: &'a mut Scope<'vm>, + + /// The absolute index of the register. + index: usize, + + /// The type of the value pointed to. + pub vtype: ExprType, +} + +impl<'a, 'vm> TypedMutPointer<'a, 'vm> { + /// Dereferences a pointer to a boolean. + pub fn deref_boolean(&self) -> bool { + assert_eq!(ExprType::Boolean, self.vtype); + self.scope.regs[self.index] != 0 + } + + /// Dereferences a pointer to a double. + pub fn deref_double(&self) -> f64 { + assert_eq!(ExprType::Double, self.vtype); + f64::from_bits(self.scope.regs[self.index]) + } + + /// Dereferences a pointer to an integer. + pub fn deref_integer(&self) -> i32 { + assert_eq!(ExprType::Integer, self.vtype); + self.scope.regs[self.index] as i32 + } + + /// Dereferences a pointer to a string. + pub fn deref_string(&self) -> &str { + assert_eq!(ExprType::Text, self.vtype); + let ptr = Pointer::from(self.scope.regs[self.index]); + match ptr.resolve(self.scope.constants, self.scope.heap) { + Datum::Text(s) => s, + _ => panic!("Mismatched constant type"), + } + } + + /// Sets a boolean via a pointer. + pub fn set_boolean(&mut self, b: bool) { + assert_eq!(ExprType::Boolean, self.vtype); + self.scope.regs[self.index] = if b { 1 } else { 0 }; + } + + /// Sets a double via a pointer. + pub fn set_double(&mut self, d: f64) { + assert_eq!(ExprType::Double, self.vtype); + self.scope.regs[self.index] = d.to_bits(); + } + + /// Sets an integer via a pointer. + pub fn set_integer(&mut self, i: i32) { + assert_eq!(ExprType::Integer, self.vtype); + self.scope.regs[self.index] = i as u64; + } + + /// Sets a string via a pointer. + pub fn set_string>(&mut self, s: S) { + assert_eq!(ExprType::Text, self.vtype); + let index = self.scope.heap.len(); + self.scope.heap.push(Datum::Text(s.into())); + self.scope.regs[self.index] = Pointer::for_heap(unchecked_usize_as_u32(index)); + } +} + +impl<'a, 'vm> fmt::Display for TypedMutPointer<'a, 'vm> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "&[R{}]{}", self.index, self.vtype) + } +} + /// Arguments provided to a callable during its execution. pub struct Scope<'a> { /// Slice of register values containing the callable's arguments. @@ -575,7 +700,7 @@ pub struct Scope<'a> { pub(crate) constants: &'a [Datum], /// Reference to the heap for resolving heap pointers. - pub(crate) heap: &'a mut [Datum], + pub(crate) heap: &'a mut Vec, /// Start of the current frame (where the arguments to the upcall start). pub(crate) fp: usize, @@ -602,6 +727,20 @@ impl<'a> Scope<'a> { self.regs[self.fp + (arg as usize)] as i32 } + /// Gets the pointer value of the argument at `arg`. + pub fn get_pointer(&self, arg: u8) -> TypedPointer<'_, 'a> { + let tagged_ptr = self.regs[self.fp + (arg as usize)]; + let (index, vtype) = Register::parse_tagged_ptr(tagged_ptr); + TypedPointer { scope: self, index, vtype } + } + + /// Gets the mutable pointer value of the argument at `arg`. + pub fn get_mut_pointer(&mut self, arg: u8) -> TypedMutPointer<'_, 'a> { + let tagged_ptr = self.regs[self.fp + (arg as usize)]; + let (index, vtype) = Register::parse_tagged_ptr(tagged_ptr); + TypedMutPointer { scope: self, index, vtype } + } + /// Gets the string value of the argument at `arg`. pub fn get_string(&self, arg: u8) -> &str { let index = self.regs[self.fp + (arg as usize)]; diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index bc410c6f..57465ac1 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -15,15 +15,18 @@ //! Common compilers for callable arguments. -use crate::ast::{ArgSpan, CallSpan}; +use crate::ast::{ArgSpan, CallSpan, Expr, VarRef}; use crate::bytecode::{self, Register}; use crate::callable::{CallableMetadata, CallableSyntax}; use crate::compiler::codegen::Codegen; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; -use crate::compiler::syms::TempSymtable; +use crate::compiler::syms::{self, TempSymtable}; use crate::compiler::{Error, Result}; use crate::reader::LineCol; -use crate::{ArgSep, ArgSepSyntax, RepeatedTypeSyntax, SingularArgSyntax}; +use crate::{ArgSep, ArgSepSyntax, ExprType, RepeatedTypeSyntax, SingularArgSyntax}; + +use super::SymbolKey; +use super::syms::LocalSymtable; /// Finds the syntax definition that matches the given argument count. /// @@ -96,11 +99,89 @@ fn validate_syn_argsep( } } +/// Pre-allocates one local variable for a command output argument, setting its to its default +/// value. +fn define_new_arg( + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + vref: &VarRef, + pos: LineCol, + codegen: &mut Codegen, +) -> Result<()> { + let key = SymbolKey::from(&vref.name); + let vtype = vref.ref_type.unwrap_or(ExprType::Integer); + let reg = symtable.put_local(key, vtype).map_err(|e| Error::from_syms(e, pos))?; + codegen.emit_default(reg, vtype, pos); + Ok(()) +} + +/// Pre-allocates local variables for command output arguments. +pub(super) fn define_new_args( + span: &CallSpan, + md: &CallableMetadata, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + codegen: &mut Codegen, +) -> Result<()> { + let syntax = find_syntax(md, span.vref_pos, span.args.len())?; + + let mut arg_iter = span.args.iter(); + + for syn in syntax.singular.iter() { + match syn { + SingularArgSyntax::RequiredValue(_details, _exp_sep) => { + arg_iter.next().expect("Args and their syntax must advance in unison"); + } + + SingularArgSyntax::RequiredRef(details, _exp_sep) => { + let ArgSpan { expr, .. } = + arg_iter.next().expect("Args and their syntax must advance in unison"); + + if let Some(Expr::Symbol(span)) = expr + && let Err(syms::Error::UndefinedSymbol(..)) = + symtable.get_local_or_global(&span.vref) + && details.define_undefined + { + define_new_arg(symtable, &span.vref, span.pos, codegen)?; + } + } + + SingularArgSyntax::OptionalValue(_details, _exp_sep) => { + arg_iter.next(); + } + + SingularArgSyntax::AnyValue(_details, _exp_sep) => { + arg_iter.next(); + } + }; + } + + if let Some(syn) = syntax.repeated.as_ref() + && let RepeatedTypeSyntax::VariableRef = syn.type_syn + { + for arg in arg_iter { + let Some(Expr::Symbol(span)) = &arg.expr else { + continue; + }; + + let Err(syms::Error::UndefinedSymbol(..)) = symtable.get_local_or_global(&span.vref) + else { + continue; + }; + + define_new_arg(symtable, &span.vref, span.pos, codegen)?; + } + } + + Ok(()) +} + /// Compiles the arguments of a callable invocation. /// /// Returns the first register containing the compiled arguments. Arguments are laid out as /// pairs of type tag and value registers, allowing the callable to interpret them at runtime. /// +/// The caller *must* invoke `define_new_args` beforehand when compiling arguments for commands. +/// This separate function is necessary to pre-allocate local variables for any output arguments. +/// /// TODO(jmmv): The `md` metadata is passed by value, not because we want to, but because it's /// necessary to appease the borrow checker. The `md` is obtained from the `symtable` in the caller /// (as a reference) to perform various validations so it is not possible to pass it as input along @@ -139,17 +220,29 @@ pub(super) fn compile_args( } } - SingularArgSyntax::RequiredRef(_details, exp_sep) => { + SingularArgSyntax::RequiredRef(details, exp_sep) => { let ArgSpan { expr, sep, sep_pos } = arg_iter.next().expect("Args and their syntax must advance in unison"); let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); match expr { None => return Err(Error::CallableSyntax(key_pos, md)), - Some(_expr) => { + Some(Expr::Symbol(span)) => { + let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { + Ok((reg, vtype)) => (reg, vtype), + Err(e @ syms::Error::UndefinedSymbol(..)) => { + if !details.define_undefined { + return Err(Error::from_syms(e, span.pos)); + } + unreachable!("Caller must use define_new_args first for commands"); + } + Err(e) => return Err(Error::from_syms(e, span.pos)), + }; + let temp = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; - todo!(); } + Some(expr) => return Err(Error::CallableSyntax(expr.start_pos(), md)), } } @@ -253,7 +346,20 @@ pub(super) fn compile_args( } RepeatedTypeSyntax::VariableRef => { - todo!(); + let Expr::Symbol(span) = expr else { + return Err(Error::CallableSyntax(arg_pos, md)); + }; + + let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { + Ok((reg, vtype)) => (reg, vtype), + Err(syms::Error::UndefinedSymbol(..)) => { + unreachable!("Caller must use define_new_args first for commands"); + } + Err(e) => return Err(Error::from_syms(e, span.pos)), + }; + let temp = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); + bytecode::VarArgTag::Pointer(sep) } }, }; diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index b8c8b8a0..04b10b2b 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -79,6 +79,17 @@ impl Codegen { self.code.len() - 1 } + /// Emits code to set `reg` to the default value for `vtype`. + pub(super) fn emit_default(&mut self, reg: Register, vtype: ExprType, pos: LineCol) { + let instr = match vtype { + ExprType::Boolean | ExprType::Double | ExprType::Integer => { + bytecode::make_load_integer(reg, 0) + } + ExprType::Text => bytecode::make_alloc(reg, ExprType::Text), + }; + self.emit(instr, pos); + } + /// Records a `fixup` that needs to be applied at `addr`. pub(super) fn add_fixup(&mut self, addr: usize, fixup: Fixup) { let previous = self.fixups.insert(addr, fixup); diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index d22bf090..9d1ba0b3 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -18,7 +18,7 @@ use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement}; use crate::bytecode::{self, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; -use crate::compiler::args::compile_args; +use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; use crate::compiler::ids::HashMapWithIds; @@ -124,8 +124,6 @@ fn compile_stmt( let key = SymbolKey::from(&span.vref.name); let key_pos = span.vref_pos; - let mut symtable = symtable.frozen(); - let Some(md) = symtable.get_callable(&key) else { return Err(Error::UndefinedSymbol( key_pos, @@ -134,8 +132,13 @@ fn compile_stmt( )); }; let is_user_defined = md.is_user_defined(); + let md = md.clone(); - let first_temp = compile_args(span, md.clone(), &mut symtable, &mut ctx.codegen)?; + define_new_args(&span, &md, symtable, &mut ctx.codegen)?; + let first_temp = { + let mut symtable = symtable.frozen(); + compile_args(span, md, &mut symtable, &mut ctx.codegen)? + }; if is_user_defined { let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos); @@ -184,13 +187,7 @@ fn compile_stmt( symtable.put_local(key, span.vtype) } .map_err(|e| Error::from_syms(e, name_pos))?; - let instr = match span.vtype { - ExprType::Boolean => bytecode::make_load_integer(reg, 0), - ExprType::Double => bytecode::make_load_integer(reg, 0), - ExprType::Integer => bytecode::make_load_integer(reg, 0), - ExprType::Text => bytecode::make_alloc(reg, ExprType::Text), - }; - ctx.codegen.emit(instr, name_pos); + ctx.codegen.emit_default(reg, span.vtype, name_pos); } Statement::End(span) => { diff --git a/core2/src/image.rs b/core2/src/image.rs index 3751cce9..fcd83210 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -39,6 +39,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::Jump => bytecode::format_jump(instr), Opcode::LoadConstant => bytecode::format_load_constant(instr), Opcode::LoadInteger => bytecode::format_load_integer(instr), + Opcode::LoadRegisterPointer => bytecode::format_load_register_ptr(instr), Opcode::Move => bytecode::format_move(instr), Opcode::Nop => bytecode::format_nop(instr), Opcode::Return => bytecode::format_return(instr), diff --git a/core2/src/num.rs b/core2/src/num.rs index e8fac9b5..a88c8966 100644 --- a/core2/src/num.rs +++ b/core2/src/num.rs @@ -78,6 +78,9 @@ macro_rules! impl_unchecked_cast { impl_unchecked_cast!(unchecked_u32_as_u8, u32, u8, primitive); impl_unchecked_cast!(unchecked_u32_as_u16, u32, u16, primitive); +impl_unchecked_cast!(unchecked_u32_as_usize, u32, usize, primitive); +impl_unchecked_cast!(unchecked_u64_as_u8, u64, u8, primitive); +impl_unchecked_cast!(unchecked_usize_as_u32, usize, u32, primitive); impl UncheckedFrom for usize { type T = U24; @@ -103,6 +106,21 @@ mod tests { assert_eq!(10u16, unchecked_u32_as_u16(10u32)); } + #[test] + fn test_unchecked_u32_as_usize() { + assert_eq!(10usize, unchecked_u32_as_usize(10u32)); + } + + #[test] + fn test_unchecked_u64_as_u8() { + assert_eq!(10u8, unchecked_u64_as_u8(10u64)); + } + + #[test] + fn test_unchecked_usize_as_u32() { + assert_eq!(10u32, unchecked_usize_as_u32(10_usize)); + } + #[test] fn test_unchecked_u24_as_usize() { assert_eq!(10_usize, unchecked_u24_as_usize(U24(10))); diff --git a/core2/src/testutils.rs b/core2/src/testutils.rs index 5c3edf01..fa9c4ebc 100644 --- a/core2/src/testutils.rs +++ b/core2/src/testutils.rs @@ -90,7 +90,12 @@ impl Callable for OutCommand { text.push_str(""); sep } - VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + VarArgTag::Pointer(sep) => { + reg += 1; + let typed_ptr = scope.get_pointer(reg); + text.push_str(&typed_ptr.to_string()); + sep + } }; reg += 1; diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 90c507df..e620185e 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -130,7 +130,7 @@ impl Context { &'a mut self, reg: Register, constants: &'a [Datum], - heap: &'a mut [Datum], + heap: &'a mut Vec, ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); @@ -162,6 +162,7 @@ impl Context { Opcode::Jump => self.do_jump(instr), Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), Opcode::LoadInteger => self.do_load_integer(instr), + Opcode::LoadRegisterPointer => self.do_load_register_ptr(instr), Opcode::Move => self.do_move(instr), Opcode::Nop => self.do_nop(instr), Opcode::Return => self.do_return(instr), @@ -294,6 +295,14 @@ impl Context { self.pc += 1; } + /// Implements the `LoadRegisterPointer` opcode. + pub(super) fn do_load_register_ptr(&mut self, instr: u32) { + let (dest, vtype, src) = bytecode::parse_load_register_ptr(instr); + let tagged_ptr = src.to_tagged_ptr(self.fp, vtype); + self.set_reg(dest, tagged_ptr); + self.pc += 1; + } + /// Implements the `Move` opcode. pub(super) fn do_move(&mut self, instr: u32) { let (dest, src) = bytecode::parse_move(instr); diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index 94d872df..3630ffe2 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -432,3 +432,356 @@ OUT 100, "Foo" ```plain 0=100% , 1=Foo$ ``` + +# Test: Singular required reference, not provided + +## Source + +```basic +INCREMENT_REQUIRED_INT +``` + +## Compilation errors + +```plain +1:1: INCREMENT_REQUIRED_INT expected arg +``` + +# Test: Singular required reference, not a variable + +## Source + +```basic +INCREMENT_REQUIRED_INT 8 +``` + +## Compilation errors + +```plain +1:24: INCREMENT_REQUIRED_INT expected arg +``` + +# Test: Singular required reference, global variable + +## Source + +```basic +DIM SHARED i +i = 8 +INCREMENT_REQUIRED_INT i +OUT i +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R0, 8 # 2:5 +0003: LOADRP R64, INTEGER, R0 # 3:24 +0004: UPCALL 0, R64 # 3:1, INCREMENT_REQUIRED_INT +0005: MOVE R65, R0 # 4:5 +0006: LOADI R64, 258 # 4:5 +0007: UPCALL 1, R64 # 4:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=9% +``` + +# Test: Singular required reference, local variable + +## Source + +```basic +i = 8 +INCREMENT_REQUIRED_INT i +OUT i +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 8 # 1:5 +0002: LOADRP R65, INTEGER, R64 # 2:24 +0003: UPCALL 0, R65 # 2:1, INCREMENT_REQUIRED_INT +0004: MOVE R66, R64 # 3:5 +0005: LOADI R65, 258 # 3:5 +0006: UPCALL 1, R65 # 3:1, OUT +0007: LOADI R65, 0 # 0:0 +0008: END R65 # 0:0 +``` + +## Output + +```plain +0=9% +``` + +# Test: Singular required reference, variable not defined + +## Source + +```basic +INCREMENT_REQUIRED_INT i +OUT i +``` + +## Compilation errors + +```plain +1:24: Undefined global symbol i +``` + +# Test: Singular required reference, define output variable with default type + +## Source + +```basic +DEFINE_ARG i +OUT i +i = 1 +DEFINE_ARG i +OUT i +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:12 +0002: LOADRP R65, INTEGER, R64 # 1:12 +0003: UPCALL 0, R65 # 1:1, DEFINE_ARG +0004: MOVE R66, R64 # 2:5 +0005: LOADI R65, 258 # 2:5 +0006: UPCALL 1, R65 # 2:1, OUT +0007: LOADI R64, 1 # 3:5 +0008: LOADRP R65, INTEGER, R64 # 4:12 +0009: UPCALL 0, R65 # 4:1, DEFINE_ARG +0010: MOVE R66, R64 # 5:5 +0011: LOADI R65, 258 # 5:5 +0012: UPCALL 1, R65 # 5:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=0% +0=1% +``` + +# Test: Singular required reference, define output variable with explicit type + +## Source + +```basic +DEFINE_ARG t$ +OUT t +t = "Foo" +DEFINE_ARG t +OUT t +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: ALLOC R64, STRING # 1:12 +0002: LOADRP R65, STRING, R64 # 1:12 +0003: UPCALL 0, R65 # 1:1, DEFINE_ARG +0004: MOVE R66, R64 # 2:5 +0005: LOADI R65, 259 # 2:5 +0006: UPCALL 1, R65 # 2:1, OUT +0007: LOADI R64, 0 # 3:5 +0008: LOADRP R65, STRING, R64 # 4:12 +0009: UPCALL 0, R65 # 4:1, DEFINE_ARG +0010: MOVE R66, R64 # 5:5 +0011: LOADI R65, 259 # 5:5 +0012: UPCALL 1, R65 # 5:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=$ +0=Foo$ +``` + +# Test: Repeated references, define output variables + +## Source + +```basic +DEFINE_AND_CHANGE_ARGS b?, d#, i%, s$ +OUT b, d, i, s + +DEFINE_AND_CHANGE_ARGS b, d, i, s +OUT b, d, i, s +``` + +## Disassembly + +```asm +0000: ENTER 12 # 0:0 +0001: LOADI R64, 0 # 1:24 +0002: LOADI R65, 0 # 1:28 +0003: LOADI R66, 0 # 1:32 +0004: ALLOC R67, STRING # 1:36 +0005: LOADRP R69, BOOLEAN, R64 # 1:24 +0006: LOADI R68, 544 # 1:24 +0007: LOADRP R71, DOUBLE, R65 # 1:28 +0008: LOADI R70, 544 # 1:28 +0009: LOADRP R73, INTEGER, R66 # 1:32 +0010: LOADI R72, 544 # 1:32 +0011: LOADRP R75, STRING, R67 # 1:36 +0012: LOADI R74, 512 # 1:36 +0013: UPCALL 0, R68 # 1:1, DEFINE_AND_CHANGE_ARGS +0014: MOVE R69, R64 # 2:5 +0015: LOADI R68, 288 # 2:5 +0016: MOVE R71, R65 # 2:8 +0017: LOADI R70, 289 # 2:8 +0018: MOVE R73, R66 # 2:11 +0019: LOADI R72, 290 # 2:11 +0020: MOVE R75, R67 # 2:14 +0021: LOADI R74, 259 # 2:14 +0022: UPCALL 1, R68 # 2:1, OUT +0023: LOADRP R69, BOOLEAN, R64 # 4:24 +0024: LOADI R68, 544 # 4:24 +0025: LOADRP R71, DOUBLE, R65 # 4:27 +0026: LOADI R70, 544 # 4:27 +0027: LOADRP R73, INTEGER, R66 # 4:30 +0028: LOADI R72, 544 # 4:30 +0029: LOADRP R75, STRING, R67 # 4:33 +0030: LOADI R74, 512 # 4:33 +0031: UPCALL 0, R68 # 4:1, DEFINE_AND_CHANGE_ARGS +0032: MOVE R69, R64 # 5:5 +0033: LOADI R68, 288 # 5:5 +0034: MOVE R71, R65 # 5:8 +0035: LOADI R70, 289 # 5:8 +0036: MOVE R73, R66 # 5:11 +0037: LOADI R72, 290 # 5:11 +0038: MOVE R75, R67 # 5:14 +0039: LOADI R74, 259 # 5:14 +0040: UPCALL 1, R68 # 5:1, OUT +0041: LOADI R68, 0 # 0:0 +0042: END R68 # 0:0 +``` + +## Output + +```plain +0=true? , 1=0.6# , 2=1% , 3=.$ +0=false? , 1=1.2# , 2=2% , 3=..$ +``` + +# Test: Singular required reference, wrong type annotation + +## Source + +```basic +i$ = "hello" +INCREMENT_REQUIRED_INT i% +``` + +## Compilation errors + +```plain +2:24: Incompatible type annotation in i% reference +``` + +# Test: Singular argument of any type, wrong type annotation + +## Source + +```basic +d# = 1.0 +OUT_ANY_VALUE d? +``` + +## Compilation errors + +```plain +2:15: Incompatible type annotation in d? reference +``` + +# Test: Singular argument of any type, not provided + +## Source + +```basic +OUT_ANY_VALUE +``` + +## Compilation errors + +```plain +1:1: OUT_ANY_VALUE expected arg +``` + +# Test: Repeated arguments with require_one, none provided + +## Source + +```basic +DEFINE_AND_CHANGE_ARGS +``` + +## Compilation errors + +```plain +1:1: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +``` + +# Test: Repeated references with require_one, value instead of ref + +## Source + +```basic +DEFINE_AND_CHANGE_ARGS 5 +``` + +## Compilation errors + +```plain +1:24: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +``` + +# Test: Repeated references with require_one, single argument + +## Source + +```basic +b? = TRUE +DEFINE_AND_CHANGE_ARGS b +OUT b +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:6 +0002: LOADRP R66, BOOLEAN, R64 # 2:24 +0003: LOADI R65, 512 # 2:24 +0004: UPCALL 0, R65 # 2:1, DEFINE_AND_CHANGE_ARGS +0005: MOVE R66, R64 # 3:5 +0006: LOADI R65, 256 # 3:5 +0007: UPCALL 1, R65 # 3:1, OUT +0008: LOADI R65, 0 # 0:0 +0009: END R65 # 0:0 +``` + +## Output + +```plain +0=false? +``` diff --git a/core2/tests/testutils/callables/define_and_change_args_cmd.rs b/core2/tests/testutils/callables/define_and_change_args_cmd.rs new file mode 100644 index 00000000..43fae236 --- /dev/null +++ b/core2/tests/testutils/callables/define_and_change_args_cmd.rs @@ -0,0 +1,92 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A command that defines the arguments passed in as a reference. +pub(super) struct DefineAndChangeArgsCommand { + metadata: CallableMetadata, +} + +impl DefineAndChangeArgsCommand { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("DEFINE_AND_CHANGE_ARGS") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::VariableRef, + sep: ArgSepSyntax::OneOf(&[ArgSep::Long, ArgSep::Short]), + require_one: true, + allow_missing: false, + }), + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for DefineAndChangeArgsCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> { + let mut i = 0; + loop { + let VarArgTag::Pointer(sep) = scope.get_type(i) else { + // TODO(jmmv): Replace with a proper error return and validate it. + panic!("Command expects variable references only"); + }; + i += 1; + + let mut typed_ptr = scope.get_mut_pointer(i); + match typed_ptr.vtype { + ExprType::Boolean => { + let b = typed_ptr.deref_boolean(); + typed_ptr.set_boolean(!b); + } + + ExprType::Double => { + let d = typed_ptr.deref_double(); + typed_ptr.set_double(d + 0.6); + } + + ExprType::Integer => { + let i = typed_ptr.deref_integer(); + typed_ptr.set_integer(i + 1); + } + + ExprType::Text => { + let s = typed_ptr.deref_string(); + typed_ptr.set_string(format!("{}.", s)); + } + } + i += 1; + + if sep == ArgSep::End { + break; + } + } + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/define_arg_cmd.rs b/core2/tests/testutils/callables/define_arg_cmd.rs new file mode 100644 index 00000000..9167eaad --- /dev/null +++ b/core2/tests/testutils/callables/define_arg_cmd.rs @@ -0,0 +1,57 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A command that defines the argument passed in as a reference. +pub(super) struct DefineArgCommand { + metadata: CallableMetadata, +} + +impl DefineArgCommand { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("DEFINE_ARG") + .with_syntax(&[( + &[SingularArgSyntax::RequiredRef( + RequiredRefSyntax { + name: Cow::Borrowed("arg"), + require_array: false, + define_undefined: true, + }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for DefineArgCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, _scope: Scope<'_>) -> CallResult<()> { + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/increment_required_int_cmd.rs b/core2/tests/testutils/callables/increment_required_int_cmd.rs new file mode 100644 index 00000000..1be44735 --- /dev/null +++ b/core2/tests/testutils/callables/increment_required_int_cmd.rs @@ -0,0 +1,67 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A command that increments the argument passed in as a reference. +pub(super) struct IncrementRequiredIntCommand { + metadata: CallableMetadata, +} + +impl IncrementRequiredIntCommand { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("INCREMENT_REQUIRED_INT") + .with_syntax(&[( + &[SingularArgSyntax::RequiredRef( + RequiredRefSyntax { + name: Cow::Borrowed("arg"), + require_array: false, + define_undefined: false, + }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for IncrementRequiredIntCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> { + let mut typed_ptr = scope.get_mut_pointer(0); + if typed_ptr.vtype != ExprType::Integer { + // TODO(jmmv): Make this error type more specific and determine the position of the + // problematic argument via the `DebugInfo` which we should propagate through the + // `Scope`. + return Err(CallError::Other("Invalid type in argument")); + } + let mut i = typed_ptr.deref_integer(); + i += 1; + typed_ptr.set_integer(i); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index 3cb0029e..aa647ae5 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -20,6 +20,15 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +mod define_arg_cmd; +use define_arg_cmd::DefineArgCommand; + +mod define_and_change_args_cmd; +use define_and_change_args_cmd::DefineAndChangeArgsCommand; + +mod increment_required_int_cmd; +use increment_required_int_cmd::IncrementRequiredIntCommand; + mod out_any_value_cmd; use out_any_value_cmd::OutAnyValueCommand; @@ -57,7 +66,10 @@ fn format_vararg(scope: &Scope<'_>, i: u8) -> (String, bool, ArgSep) { (format!("{}{}", formatted, etype.annotation()), true, sep) } VarArgTag::Missing(sep) => ("()".to_owned(), false, sep), - VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + VarArgTag::Pointer(sep) => { + let typed_ptr = scope.get_pointer(i + 1); + (typed_ptr.to_string(), true, sep) + } } } @@ -67,6 +79,9 @@ pub(super) fn register_all( console: Rc>, ) { let cmds = [ + DefineArgCommand::new() as Rc, + DefineAndChangeArgsCommand::new() as Rc, + IncrementRequiredIntCommand::new() as Rc, OutAnyValueCommand::new(console.clone()) as Rc, OutAnyValueOptionalCommand::new(console.clone()) as Rc, OutCommand::new(console.clone()) as Rc, diff --git a/core2/tests/testutils/callables/out_cmd.rs b/core2/tests/testutils/callables/out_cmd.rs index d565ed48..b5a1da03 100644 --- a/core2/tests/testutils/callables/out_cmd.rs +++ b/core2/tests/testutils/callables/out_cmd.rs @@ -70,7 +70,12 @@ impl Callable for OutCommand { line.push_str(&format!("{}=()", argi)); sep } - VarArgTag::Pointer(_sep) => todo!("Support to load pointers not needed yet"), + VarArgTag::Pointer(sep) => { + reg += 1; + let typed_ptr = scope.get_pointer(reg); + line.push_str(&format!("{}={}", argi, typed_ptr)); + sep + } }; argi += 1; reg += 1; From efa13575de1586c7af8b4013b7fe36d4df844ea7 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 16 Feb 2026 14:54:40 -0800 Subject: [PATCH 06/53] core2: Clarify pointer type names and reduce duplication Rename Pointer to DatumPtr to clarify it indexes into the constant pool or heap (data storage), not into the register file. Rename TypedPointer and TypedMutPointer to RegisterRef and RegisterRefMut, and rename their accessors from get_pointer/get_mut_pointer to get_ref/get_mut_ref, to make it clear these are references to registers (variables). Introduce a TaggedRegisterRef newtype to wrap the u64 encoding that carries a register's absolute address and type tag together, replacing the free methods on Register (to_tagged_ptr/parse_tagged_ptr). This prevents accidental misinterpretation of raw u64 values. Extract shared deref_boolean/deref_double/deref_integer/deref_string helper functions used by both RegisterRef and RegisterRefMut to eliminate the duplicated read logic between the two types. As a side effect, consolidate the VM's deref_ptr method to delegate to DatumPtr::resolve instead of duplicating the constant/heap match. --- core2/src/bytecode.rs | 46 ++++-- core2/src/callable.rs | 132 ++++++++++-------- core2/src/mem.rs | 21 ++- core2/src/testutils.rs | 2 +- core2/src/vm/context.rs | 18 +-- .../callables/define_and_change_args_cmd.rs | 2 +- .../callables/increment_required_int_cmd.rs | 2 +- core2/tests/testutils/callables/mod.rs | 2 +- core2/tests/testutils/callables/out_cmd.rs | 2 +- 9 files changed, 134 insertions(+), 93 deletions(-) diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index a8beea45..fcb9b974 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -147,41 +147,59 @@ impl Register { pub(crate) fn to_parts(self) -> (bool, u8) { if self.0 < Self::MAX_GLOBAL { (true, self.0) } else { (false, self.0 - Self::MAX_GLOBAL) } } +} - /// Creates a new tagged pointer for this register. - /// - /// A tagged pointer carries the type of the value pointed to by the register as well as the - /// absolute address of the register in the register bank. This address is calculated using - /// the current value of `fp`. - pub(crate) fn to_tagged_ptr(self, fp: usize, vtype: ExprType) -> u64 { - let (is_global, index) = self.to_parts(); +/// A tagged reference to a variable (register) encoding the register's absolute address +/// and the type of the value it holds. +/// +/// The encoding stores the `ExprType` tag in the upper 32 bits and the absolute register +/// address in the lower 32 bits of a `u64`. +/// +/// This is distinct from `DatumPtr`, which points to data in the constant pool or heap +/// rather than to a register in the register file. +pub(crate) struct TaggedRegisterRef(u64); + +impl TaggedRegisterRef { + /// Creates a new tagged register reference from a register, frame pointer, and type. + pub(crate) fn new(reg: Register, fp: usize, vtype: ExprType) -> Self { + let (is_global, index) = reg.to_parts(); let mut index = usize::from(index); if !is_global { index += fp; } let index = u32::try_from(index).expect("Cannot support that many registers"); - u64::from(vtype as u8) << 32 | u64::from(index) + Self(u64::from(vtype as u8) << 32 | u64::from(index)) } - /// Parses a tagged pointer previously created by `to_tagged_ptr`, returning the absolute - /// address of the register and the type of the value pointed at. + /// Parses a tagged register reference from a raw `u64`, returning the absolute register + /// index and the type of the value it holds. /// - /// Panics if the tag is not value. - pub(crate) fn parse_tagged_ptr(ptr: u64) -> (usize, ExprType) { + /// Panics if the type tag is invalid. + pub(crate) fn parse(self) -> (usize, ExprType) { let vtype: ExprType = { #[allow(unsafe_code)] unsafe { - let v = unchecked_u64_as_u8(ptr >> 32); + let v = unchecked_u64_as_u8(self.0 >> 32); assert!(v <= ExprType::Text as u8); std::mem::transmute(v) } }; - let index = unchecked_u32_as_usize((ptr & 0xffffffff) as u32); + let index = unchecked_u32_as_usize((self.0 & 0xffffffff) as u32); (index, vtype) } + + /// Returns the raw `u64` encoding. + pub(crate) fn as_u64(&self) -> u64 { + self.0 + } + + /// Wraps a raw `u64` as a `TaggedRegisterRef`. + pub(crate) fn from_u64(v: u64) -> Self { + Self(v) + } } impl RawValue for ExprType { diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 0937b097..19a7f53a 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -17,9 +17,9 @@ use crate::ast::ArgSep; use crate::ast::ExprType; -use crate::bytecode::Register; +use crate::bytecode::TaggedRegisterRef; use crate::bytecode::VarArgTag; -use crate::mem::{Datum, Pointer}; +use crate::mem::{Datum, DatumPtr}; use crate::num::unchecked_usize_as_u32; use async_trait::async_trait; use std::borrow::Cow; @@ -569,8 +569,43 @@ impl CallableMetadata { } } -/// Provides some sort of validation when dereferencing register pointers. -pub struct TypedPointer<'a, 'vm> { +/// Reads a boolean from the register at `index`, asserting that `vtype` is `Boolean`. +fn deref_boolean(regs: &[u64], index: usize, vtype: ExprType) -> bool { + assert_eq!(ExprType::Boolean, vtype); + regs[index] != 0 +} + +/// Reads a double from the register at `index`, asserting that `vtype` is `Double`. +fn deref_double(regs: &[u64], index: usize, vtype: ExprType) -> f64 { + assert_eq!(ExprType::Double, vtype); + f64::from_bits(regs[index]) +} + +/// Reads an integer from the register at `index`, asserting that `vtype` is `Integer`. +fn deref_integer(regs: &[u64], index: usize, vtype: ExprType) -> i32 { + assert_eq!(ExprType::Integer, vtype); + regs[index] as i32 +} + +/// Reads a string from the register at `index`, asserting that `vtype` is `Text`. +fn deref_string<'a>( + regs: &[u64], + index: usize, + vtype: ExprType, + constants: &'a [Datum], + heap: &'a [Datum], +) -> &'a str { + assert_eq!(ExprType::Text, vtype); + let ptr = DatumPtr::from(regs[index]); + match ptr.resolve(constants, heap) { + Datum::Text(s) => s, + _ => panic!("Mismatched constant type"), + } +} + +/// An immutable reference to a variable (register) in the register file, carrying +/// its type for runtime validation of dereference operations. +pub struct RegisterRef<'a, 'vm> { /// The scope through which to access the register. scope: &'a Scope<'vm>, @@ -581,44 +616,37 @@ pub struct TypedPointer<'a, 'vm> { pub vtype: ExprType, } -impl<'a, 'vm> TypedPointer<'a, 'vm> { - /// Dereferences a pointer to a boolean. +impl<'a, 'vm> RegisterRef<'a, 'vm> { + /// Dereferences this register reference as a boolean. pub fn deref_boolean(&self) -> bool { - assert_eq!(ExprType::Boolean, self.vtype); - self.scope.regs[self.index] != 0 + deref_boolean(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to a double. + /// Dereferences this register reference as a double. pub fn deref_double(&self) -> f64 { - assert_eq!(ExprType::Double, self.vtype); - f64::from_bits(self.scope.regs[self.index]) + deref_double(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to an integer. + /// Dereferences this register reference as an integer. pub fn deref_integer(&self) -> i32 { - assert_eq!(ExprType::Integer, self.vtype); - self.scope.regs[self.index] as i32 + deref_integer(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to a string. + /// Dereferences this register reference as a string. pub fn deref_string(&self) -> &str { - assert_eq!(ExprType::Text, self.vtype); - let ptr = Pointer::from(self.scope.regs[self.index]); - match ptr.resolve(self.scope.constants, self.scope.heap) { - Datum::Text(s) => s, - _ => panic!("Mismatched constant type"), - } + deref_string(self.scope.regs, self.index, self.vtype, self.scope.constants, self.scope.heap) } } -impl<'a, 'vm> fmt::Display for TypedPointer<'a, 'vm> { +impl<'a, 'vm> fmt::Display for RegisterRef<'a, 'vm> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "&[R{}]{}", self.index, self.vtype) } } -/// Provides some sort of validation when dereferencing register pointers. -pub struct TypedMutPointer<'a, 'vm> { +/// A mutable reference to a variable (register) in the register file, carrying +/// its type for runtime validation of dereference and set operations. +pub struct RegisterRefMut<'a, 'vm> { /// The scope through which to access the register. scope: &'a mut Scope<'vm>, @@ -629,63 +657,55 @@ pub struct TypedMutPointer<'a, 'vm> { pub vtype: ExprType, } -impl<'a, 'vm> TypedMutPointer<'a, 'vm> { - /// Dereferences a pointer to a boolean. +impl<'a, 'vm> RegisterRefMut<'a, 'vm> { + /// Dereferences this register reference as a boolean. pub fn deref_boolean(&self) -> bool { - assert_eq!(ExprType::Boolean, self.vtype); - self.scope.regs[self.index] != 0 + deref_boolean(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to a double. + /// Dereferences this register reference as a double. pub fn deref_double(&self) -> f64 { - assert_eq!(ExprType::Double, self.vtype); - f64::from_bits(self.scope.regs[self.index]) + deref_double(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to an integer. + /// Dereferences this register reference as an integer. pub fn deref_integer(&self) -> i32 { - assert_eq!(ExprType::Integer, self.vtype); - self.scope.regs[self.index] as i32 + deref_integer(self.scope.regs, self.index, self.vtype) } - /// Dereferences a pointer to a string. + /// Dereferences this register reference as a string. pub fn deref_string(&self) -> &str { - assert_eq!(ExprType::Text, self.vtype); - let ptr = Pointer::from(self.scope.regs[self.index]); - match ptr.resolve(self.scope.constants, self.scope.heap) { - Datum::Text(s) => s, - _ => panic!("Mismatched constant type"), - } + deref_string(self.scope.regs, self.index, self.vtype, self.scope.constants, self.scope.heap) } - /// Sets a boolean via a pointer. + /// Sets a boolean via this register reference. pub fn set_boolean(&mut self, b: bool) { assert_eq!(ExprType::Boolean, self.vtype); self.scope.regs[self.index] = if b { 1 } else { 0 }; } - /// Sets a double via a pointer. + /// Sets a double via this register reference. pub fn set_double(&mut self, d: f64) { assert_eq!(ExprType::Double, self.vtype); self.scope.regs[self.index] = d.to_bits(); } - /// Sets an integer via a pointer. + /// Sets an integer via this register reference. pub fn set_integer(&mut self, i: i32) { assert_eq!(ExprType::Integer, self.vtype); self.scope.regs[self.index] = i as u64; } - /// Sets a string via a pointer. + /// Sets a string via this register reference. pub fn set_string>(&mut self, s: S) { assert_eq!(ExprType::Text, self.vtype); let index = self.scope.heap.len(); self.scope.heap.push(Datum::Text(s.into())); - self.scope.regs[self.index] = Pointer::for_heap(unchecked_usize_as_u32(index)); + self.scope.regs[self.index] = DatumPtr::for_heap(unchecked_usize_as_u32(index)); } } -impl<'a, 'vm> fmt::Display for TypedMutPointer<'a, 'vm> { +impl<'a, 'vm> fmt::Display for RegisterRefMut<'a, 'vm> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "&[R{}]{}", self.index, self.vtype) } @@ -727,24 +747,24 @@ impl<'a> Scope<'a> { self.regs[self.fp + (arg as usize)] as i32 } - /// Gets the pointer value of the argument at `arg`. - pub fn get_pointer(&self, arg: u8) -> TypedPointer<'_, 'a> { + /// Gets an immutable register reference from the argument at `arg`. + pub fn get_ref(&self, arg: u8) -> RegisterRef<'_, 'a> { let tagged_ptr = self.regs[self.fp + (arg as usize)]; - let (index, vtype) = Register::parse_tagged_ptr(tagged_ptr); - TypedPointer { scope: self, index, vtype } + let (index, vtype) = TaggedRegisterRef::from_u64(tagged_ptr).parse(); + RegisterRef { scope: self, index, vtype } } - /// Gets the mutable pointer value of the argument at `arg`. - pub fn get_mut_pointer(&mut self, arg: u8) -> TypedMutPointer<'_, 'a> { + /// Gets a mutable register reference from the argument at `arg`. + pub fn get_mut_ref(&mut self, arg: u8) -> RegisterRefMut<'_, 'a> { let tagged_ptr = self.regs[self.fp + (arg as usize)]; - let (index, vtype) = Register::parse_tagged_ptr(tagged_ptr); - TypedMutPointer { scope: self, index, vtype } + let (index, vtype) = TaggedRegisterRef::from_u64(tagged_ptr).parse(); + RegisterRefMut { scope: self, index, vtype } } /// Gets the string value of the argument at `arg`. pub fn get_string(&self, arg: u8) -> &str { let index = self.regs[self.fp + (arg as usize)]; - let ptr = Pointer::from(index); + let ptr = DatumPtr::from(index); match ptr.resolve(self.constants, self.heap) { Datum::Text(s) => s, _ => panic!("Mismatched constant type"), diff --git a/core2/src/mem.rs b/core2/src/mem.rs index 96e4915c..290525a2 100644 --- a/core2/src/mem.rs +++ b/core2/src/mem.rs @@ -72,8 +72,15 @@ impl Datum { } /// Tagged pointers for constant and heap addresses. +/// +/// A `DatumPtr` indexes into the constant pool or the heap, where `Datum` values live. +/// The encoding uses the sign of the lower 32 bits of a `u64`: positive values are +/// constant pool indices, and negative values (two's complement) are heap indices. +/// +/// This is distinct from `TaggedRegisterRef`, which points to a register in the register +/// file rather than to data storage. #[derive(Clone, Copy)] -pub(crate) enum Pointer { +pub(crate) enum DatumPtr { /// A pointer to an entry in the constants pool. Constant(U24), @@ -81,18 +88,18 @@ pub(crate) enum Pointer { Heap(U24), } -impl From for Pointer { +impl From for DatumPtr { fn from(value: u64) -> Self { let signed_value = value as i32; if signed_value < 0 { - Self::Heap(U24::try_from((-signed_value - 1) as u32).unwrap()) + DatumPtr::Heap(U24::try_from((-signed_value - 1) as u32).unwrap()) } else { - Self::Constant(U24::try_from(signed_value as u32).unwrap()) + DatumPtr::Constant(U24::try_from(signed_value as u32).unwrap()) } } } -impl Pointer { +impl DatumPtr { /// Creates a new pointer for a heap `index` and returns its `u64` representation. pub(crate) fn for_heap(index: u32) -> u64 { let raw = index as i32; @@ -103,8 +110,8 @@ impl Pointer { /// Gets the datum pointed to by this pointer from the `constants` and `heap`. pub(crate) fn resolve<'b>(&self, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { match self { - Self::Constant(index) => &constants[unchecked_u24_as_usize(*index)], - Self::Heap(index) => &heap[unchecked_u24_as_usize(*index)], + DatumPtr::Constant(index) => &constants[unchecked_u24_as_usize(*index)], + DatumPtr::Heap(index) => &heap[unchecked_u24_as_usize(*index)], } } } diff --git a/core2/src/testutils.rs b/core2/src/testutils.rs index fa9c4ebc..d05573ce 100644 --- a/core2/src/testutils.rs +++ b/core2/src/testutils.rs @@ -92,7 +92,7 @@ impl Callable for OutCommand { } VarArgTag::Pointer(sep) => { reg += 1; - let typed_ptr = scope.get_pointer(reg); + let typed_ptr = scope.get_ref(reg); text.push_str(&typed_ptr.to_string()); sep } diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index e620185e..30851274 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -16,10 +16,9 @@ //! Virtual processor for EndBASIC execution. use crate::Scope; -use crate::bytecode::{self, Opcode, Register, opcode_of}; +use crate::bytecode::{self, Opcode, Register, TaggedRegisterRef, opcode_of}; use crate::image::Image; -use crate::mem::{Datum, Pointer}; -use crate::num::unchecked_u24_as_usize; +use crate::mem::{Datum, DatumPtr}; /// Alias for the type representing a program address. type Address = usize; @@ -111,10 +110,7 @@ impl Context { /// Dereferences a pointer. fn deref_ptr<'b>(&self, reg: Register, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { let raw_addr = self.get_reg(reg); - match Pointer::from(raw_addr) { - Pointer::Constant(index) => &constants[unchecked_u24_as_usize(index)], - Pointer::Heap(index) => &heap[unchecked_u24_as_usize(index)], - } + DatumPtr::from(raw_addr).resolve(constants, heap) } /// Registers that the instruction being processed threw an exception `message`. @@ -203,7 +199,7 @@ impl Context { pub(super) fn do_alloc(&mut self, instr: u32, heap: &mut Vec) { let (dest, etype) = bytecode::parse_alloc(instr); heap.push(Datum::new(etype)); - let ptr = Pointer::for_heap((heap.len() - 1) as u32); + let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); self.set_reg(dest, ptr); self.pc += 1; } @@ -228,7 +224,7 @@ impl Context { _ => unreachable!(), }; heap.push(Datum::Text(result)); - let ptr = Pointer::for_heap((heap.len() - 1) as u32); + let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); self.set_reg(dest, ptr); self.pc += 1; } @@ -298,8 +294,8 @@ impl Context { /// Implements the `LoadRegisterPointer` opcode. pub(super) fn do_load_register_ptr(&mut self, instr: u32) { let (dest, vtype, src) = bytecode::parse_load_register_ptr(instr); - let tagged_ptr = src.to_tagged_ptr(self.fp, vtype); - self.set_reg(dest, tagged_ptr); + let tagged_ref = TaggedRegisterRef::new(src, self.fp, vtype); + self.set_reg(dest, tagged_ref.as_u64()); self.pc += 1; } diff --git a/core2/tests/testutils/callables/define_and_change_args_cmd.rs b/core2/tests/testutils/callables/define_and_change_args_cmd.rs index 43fae236..c8ce9458 100644 --- a/core2/tests/testutils/callables/define_and_change_args_cmd.rs +++ b/core2/tests/testutils/callables/define_and_change_args_cmd.rs @@ -59,7 +59,7 @@ impl Callable for DefineAndChangeArgsCommand { }; i += 1; - let mut typed_ptr = scope.get_mut_pointer(i); + let mut typed_ptr = scope.get_mut_ref(i); match typed_ptr.vtype { ExprType::Boolean => { let b = typed_ptr.deref_boolean(); diff --git a/core2/tests/testutils/callables/increment_required_int_cmd.rs b/core2/tests/testutils/callables/increment_required_int_cmd.rs index 1be44735..a9d77af3 100644 --- a/core2/tests/testutils/callables/increment_required_int_cmd.rs +++ b/core2/tests/testutils/callables/increment_required_int_cmd.rs @@ -52,7 +52,7 @@ impl Callable for IncrementRequiredIntCommand { } async fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> { - let mut typed_ptr = scope.get_mut_pointer(0); + let mut typed_ptr = scope.get_mut_ref(0); if typed_ptr.vtype != ExprType::Integer { // TODO(jmmv): Make this error type more specific and determine the position of the // problematic argument via the `DebugInfo` which we should propagate through the diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index aa647ae5..6262570a 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -67,7 +67,7 @@ fn format_vararg(scope: &Scope<'_>, i: u8) -> (String, bool, ArgSep) { } VarArgTag::Missing(sep) => ("()".to_owned(), false, sep), VarArgTag::Pointer(sep) => { - let typed_ptr = scope.get_pointer(i + 1); + let typed_ptr = scope.get_ref(i + 1); (typed_ptr.to_string(), true, sep) } } diff --git a/core2/tests/testutils/callables/out_cmd.rs b/core2/tests/testutils/callables/out_cmd.rs index b5a1da03..f09b5626 100644 --- a/core2/tests/testutils/callables/out_cmd.rs +++ b/core2/tests/testutils/callables/out_cmd.rs @@ -72,7 +72,7 @@ impl Callable for OutCommand { } VarArgTag::Pointer(sep) => { reg += 1; - let typed_ptr = scope.get_pointer(reg); + let typed_ptr = scope.get_ref(reg); line.push_str(&format!("{}={}", argi, typed_ptr)); sep } From 9503673cbdf5bf15effbef438bdbcfd4711b1b06 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 16 Feb 2026 18:04:42 -0800 Subject: [PATCH 07/53] core2: Add support for function upcalls Add support for calling built-in functions that return values from EndBASIC code. This completes the upcall support by handling function upcalls in addition to the already-supported command upcalls. To make this possible, move the upcalls map from the compilation Context into Codegen so that compile_expr can register upcalls without needing access to Context. Implement the Expr::Call and Expr::Symbol paths for non-user-defined callables, handling both the local and global register cases. Add return_* methods to Scope to be able to set the return value. --- core2/src/callable.rs | 22 ++ core2/src/compiler/args.rs | 42 +++ core2/src/compiler/codegen.rs | 26 +- core2/src/compiler/exprs.rs | 50 +-- core2/src/compiler/top.rs | 24 +- core2/tests/test_functions.md | 285 ++++++++++++++++++ core2/tests/test_subs.md | 28 ++ core2/tests/testutils/callables/concat_fn.rs | 79 +++++ .../testutils/callables/is_positive_fn.rs | 56 ++++ .../testutils/callables/meaning_of_life_fn.rs | 48 +++ core2/tests/testutils/callables/mod.rs | 22 +- .../testutils/callables/sum_doubles_fn.rs | 80 +++++ .../testutils/callables/sum_integers_fn.rs | 79 +++++ 13 files changed, 793 insertions(+), 48 deletions(-) create mode 100644 core2/tests/testutils/callables/concat_fn.rs create mode 100644 core2/tests/testutils/callables/is_positive_fn.rs create mode 100644 core2/tests/testutils/callables/meaning_of_life_fn.rs create mode 100644 core2/tests/testutils/callables/sum_doubles_fn.rs create mode 100644 core2/tests/testutils/callables/sum_integers_fn.rs diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 19a7f53a..a607a188 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -770,6 +770,28 @@ impl<'a> Scope<'a> { _ => panic!("Mismatched constant type"), } } + + /// Sets the return value of the function to `b`. + pub fn return_boolean(self, b: bool) { + self.regs[self.fp] = if b { 1 } else { 0 }; + } + + /// Sets the return value of the function to `d`. + pub fn return_double(self, d: f64) { + self.regs[self.fp] = d.to_bits(); + } + + /// Sets the return value of the function to `i`. + pub fn return_integer(self, i: i32) { + self.regs[self.fp] = i as u64; + } + + /// Sets the return value of the function to `s`. + pub fn return_string>(self, s: S) { + let index = self.heap.len(); + self.heap.push(Datum::Text(s.into())); + self.regs[self.fp] = DatumPtr::for_heap(unchecked_usize_as_u32(index)); + } } /// A trait to define a callable that is executed by a `Machine`. diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 57465ac1..c8850d2e 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -28,6 +28,45 @@ use crate::{ArgSep, ArgSepSyntax, ExprType, RepeatedTypeSyntax, SingularArgSynta use super::SymbolKey; use super::syms::LocalSymtable; +/// Returns true if `sep` is valid for a function call (only `Long` and `End` are allowed because +/// the parser only produces comma separators for function arguments). +fn is_function_sep(sep: &ArgSepSyntax) -> bool { + match sep { + ArgSepSyntax::Exactly(ArgSep::Long) | ArgSepSyntax::End => true, + ArgSepSyntax::OneOf(seps) => seps.iter().all(|s| *s == ArgSep::Long), + _ => false, + } +} + +/// Checks that the syntax of a callable that returns a value only uses separators that can appear +/// in a function call (i.e. the comma separator). The parser only produces `ArgSep::Long` for +/// function arguments, so any other separator in the metadata would be dead/untestable. +fn debug_assert_function_seps(md: &CallableMetadata, syntax: &CallableSyntax) { + if md.return_type().is_none() { + return; + } + for syn in syntax.singular.iter() { + let sep = match syn { + SingularArgSyntax::RequiredValue(_, sep) => sep, + SingularArgSyntax::RequiredRef(_, sep) => sep, + SingularArgSyntax::OptionalValue(_, sep) => sep, + SingularArgSyntax::AnyValue(_, sep) => sep, + }; + debug_assert!( + is_function_sep(sep), + "Function {} has a non-comma separator in its singular args syntax", + md.name() + ); + } + if let Some(repeated) = syntax.repeated.as_ref() { + debug_assert!( + is_function_sep(&repeated.sep), + "Function {} has a non-comma separator in its repeated args syntax", + md.name() + ); + } +} + /// Finds the syntax definition that matches the given argument count. /// /// Returns an error if no syntax matches, and panics if multiple syntaxes match (which would @@ -38,6 +77,9 @@ fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&Cal match syntax { Some(syntax) => { debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); + if cfg!(debug_assertions) { + debug_assert_function_seps(md, syntax); + } Ok(syntax) } None => Err(Error::CallableSyntax(pos, md.clone())), diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 04b10b2b..0d1d0379 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -64,6 +64,9 @@ pub(super) struct Codegen { /// Map of user callable names to their target addresses. user_callables_addresses: HashMap, + + /// Map of built-in callable names to their return types and assigned upcall IDs. + upcalls: HashMapWithIds, u16>, } impl Codegen { @@ -153,11 +156,24 @@ impl Codegen { Ok(()) } + /// Gets the existing upcall ID for the given `key` or creates a new one. + pub(super) fn get_upcall( + &mut self, + key: SymbolKey, + etype: Option, + pos: LineCol, + ) -> Result { + match self.upcalls.get(&key) { + Some((_etype, id)) => Ok(id), + None => match self.upcalls.insert(key, etype) { + Some((_etype, id)) => Ok(id), + None => Err(Error::OutOfUpcalls(pos)), + }, + } + } + /// Consumes the code generator and builds a ready-to-use `Image`. - pub(super) fn build_image( - mut self, - upcalls: HashMapWithIds, u16>, - ) -> Result { + pub(super) fn build_image(mut self) -> Result { self.apply_fixups()?; let mut callables = HashMap::default(); @@ -168,7 +184,7 @@ impl Codegen { Ok(Image::new( self.code, - upcalls.keys_to_vec(), + self.upcalls.keys_to_vec(), self.constants.keys_to_vec(), DebugInfo { instr_linecols: self.instr_linecols, callables }, )) diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 9c395b0d..44b55128 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -119,30 +119,31 @@ pub(super) fn compile_expr( return Err(Error::CallableSyntax(span.vref_pos, md.clone())); } - if md.is_user_defined() { - let (is_global, _index) = reg.to_parts(); - - let mut alloc = symtable.temp_scope(); - let ret_reg = if is_global { - // The call instruction can only carry one register, and this register - // indicates where to store the result and where arguments start. So, - // if we are going to save the result to a global register, we must - // allocate a temp register first so that argument passing can work. - alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? - } else { - reg - }; - let _first_temp = compile_args(span, md.clone(), symtable, codegen)?; + let is_user_defined = md.is_user_defined(); + let (is_global, _index) = reg.to_parts(); + let mut alloc = symtable.temp_scope(); + let ret_reg = if is_global { + // The call instruction can only carry one register, and this register + // indicates where to store the result and where arguments start. So, + // if we are going to save the result to a global register, we must + // allocate a temp register first so that argument passing can work. + alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? + } else { + reg + }; + compile_args(span, md.clone(), symtable, codegen)?; + if is_user_defined { let addr = codegen.emit(bytecode::make_nop(), key_pos); codegen.add_fixup(addr, Fixup::Call(ret_reg, key)); - - if is_global { - codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); - } } else { - todo!("Function upcalls not implemented yet"); + let upcall = codegen.get_upcall(key, Some(etype), key_pos)?; + codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); } + if is_global { + codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); + } + Ok(etype) } @@ -190,7 +191,16 @@ pub(super) fn compile_expr( let addr = codegen.emit(bytecode::make_nop(), span.pos); codegen.add_fixup(addr, Fixup::Call(reg, key)); } else { - todo!("Function upcalls not implemented yet"); + let upcall = codegen.get_upcall(key, Some(etype), span.pos)?; + let (is_global, _) = reg.to_parts(); + if is_global { + let mut scope = symtable.temp_scope(); + let temp = scope.alloc().map_err(|e| Error::from_syms(e, span.pos))?; + codegen.emit(bytecode::make_upcall(upcall, temp), span.pos); + codegen.emit(bytecode::make_move(reg, temp), span.pos); + } else { + codegen.emit(bytecode::make_upcall(upcall, reg), span.pos); + } } Ok(etype) } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 9d1ba0b3..8773a2b3 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -21,7 +21,6 @@ use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, Singu use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; -use crate::compiler::ids::HashMapWithIds; use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey}; use crate::compiler::{Error, Result}; use crate::image::Image; @@ -43,29 +42,10 @@ struct Context { /// The code generator accumulating bytecode instructions. codegen: Codegen, - /// Map of built-in callable names to their return types and assigned upcall IDs. - upcalls: HashMapWithIds, u16>, - /// Collection of user-defined callable definitions to be compiled after the main scope. user_callables: Vec, } -impl Context { - /// Gets the existing upcall ID for the given `key` or creates a new one. - fn get_upcall(&mut self, key: SymbolKey, etype: Option, pos: LineCol) -> Result { - if etype.is_some() { - todo!("Function upcalls not implemented yet"); - } - match self.upcalls.get(&key) { - Some((_etype, id)) => Ok(id), - None => match self.upcalls.insert(key, etype) { - Some((_etype, id)) => Ok(id), - None => Err(Error::OutOfUpcalls(pos)), - }, - } - } -} - /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, @@ -144,7 +124,7 @@ fn compile_stmt( let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos); ctx.codegen.add_fixup(addr, Fixup::Call(first_temp, key)); } else { - let upcall = ctx.get_upcall(key, None, key_pos)?; + let upcall = ctx.codegen.get_upcall(key, None, key_pos)?; ctx.codegen.emit(bytecode::make_upcall(upcall, first_temp), key_pos); } } @@ -332,5 +312,5 @@ pub fn compile( compile_user_callables(&mut ctx, &mut symtable)?; - ctx.codegen.build_image(ctx.upcalls) + ctx.codegen.build_image() } diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 2bbad18b..3a3b3f42 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -503,3 +503,288 @@ OUT s 0=0% 0=bar$ ``` + +# Test: Upcall with repeated arguments and return of type double + +## Source + +```basic +OUT SUM_DOUBLES(3.4, 2, 7.1) +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADC R67, 0 # 1:17 +0002: LOADI R66, 289 # 1:17 +0003: LOADI R69, 2 # 1:22 +0004: LOADI R68, 290 # 1:22 +0005: LOADC R71, 1 # 1:25 +0006: LOADI R70, 257 # 1:25 +0007: UPCALL 0, R65 # 1:5, SUM_DOUBLES +0008: LOADI R64, 257 # 1:5 +0009: UPCALL 1, R64 # 1:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=12.5# +``` + +# Test: Upcall returning integer + +## Source + +```basic +OUT SUM_INTEGERS(3, 2, 7) +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R67, 3 # 1:18 +0002: LOADI R66, 290 # 1:18 +0003: LOADI R69, 2 # 1:21 +0004: LOADI R68, 290 # 1:21 +0005: LOADI R71, 7 # 1:24 +0006: LOADI R70, 258 # 1:24 +0007: UPCALL 0, R65 # 1:5, SUM_INTEGERS +0008: LOADI R64, 258 # 1:5 +0009: UPCALL 1, R64 # 1:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=12% +``` + +# Test: Upcall returning string + +## Source + +```basic +OUT CONCAT$("hello", " ", "world") +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R67, 0 # 1:13 +0002: LOADI R66, 291 # 1:13 +0003: LOADI R69, 1 # 1:22 +0004: LOADI R68, 291 # 1:22 +0005: LOADI R71, 2 # 1:27 +0006: LOADI R70, 259 # 1:27 +0007: UPCALL 0, R65 # 1:5, CONCAT +0008: LOADI R64, 259 # 1:5 +0009: UPCALL 1, R64 # 1:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=hello world$ +``` + +# Test: Upcall returning boolean + +## Source + +```basic +OUT IS_POSITIVE?(42) +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R66, 42 # 1:18 +0002: UPCALL 0, R65 # 1:5, IS_POSITIVE +0003: LOADI R64, 256 # 1:5 +0004: UPCALL 1, R64 # 1:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Argless upcall + +## Source + +```basic +OUT MEANING_OF_LIFE +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: UPCALL 0, R65 # 1:5, MEANING_OF_LIFE +0002: LOADI R64, 258 # 1:5 +0003: UPCALL 1, R64 # 1:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 +``` + +## Output + +```plain +0=42% +``` + +# Test: Function upcall result assigned to global + +## Source + +```basic +DIM SHARED x AS DOUBLE +x = SUM_DOUBLES(1.5, 2.5) +OUT x +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADC R66, 0 # 2:17 +0003: LOADI R65, 289 # 2:17 +0004: LOADC R68, 1 # 2:22 +0005: LOADI R67, 257 # 2:22 +0006: UPCALL 0, R64 # 2:5, SUM_DOUBLES +0007: MOVE R0, R64 # 2:5 +0008: MOVE R65, R0 # 3:5 +0009: LOADI R64, 257 # 3:5 +0010: UPCALL 1, R64 # 3:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=4# +``` + +# Test: Argless upcall result assigned to global + +## Source + +```basic +DIM SHARED x +x = MEANING_OF_LIFE +OUT x +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: UPCALL 0, R64 # 2:5, MEANING_OF_LIFE +0003: MOVE R0, R64 # 2:5 +0004: MOVE R65, R0 # 3:5 +0005: LOADI R64, 258 # 3:5 +0006: UPCALL 1, R64 # 3:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=42% +``` + +# Test: Function upcall in expression + +## Source + +```basic +OUT SUM_DOUBLES(1.0, 2.0) + SUM_DOUBLES(3.0, 4.0) +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADC R68, 0 # 1:17 +0002: LOADI R67, 289 # 1:17 +0003: LOADC R70, 1 # 1:22 +0004: LOADI R69, 257 # 1:22 +0005: UPCALL 0, R66 # 1:5, SUM_DOUBLES +0006: LOADC R69, 2 # 1:41 +0007: LOADI R68, 289 # 1:41 +0008: LOADC R71, 3 # 1:46 +0009: LOADI R70, 257 # 1:46 +0010: UPCALL 0, R67 # 1:29, SUM_DOUBLES +0011: ADDD R65, R66, R67 # 1:27 +0012: LOADI R64, 257 # 1:5 +0013: UPCALL 1, R64 # 1:1, OUT +0014: LOADI R64, 0 # 0:0 +0015: END R64 # 0:0 +``` + +## Output + +```plain +0=10# +``` + +# Test: Error: calling an argless function upcall with arguments + +## Source + +```basic +OUT MEANING_OF_LIFE(1) +``` + +## Compilation errors + +```plain +1:5: MEANING_OF_LIFE expected no arguments +``` + +# Test: Error: calling an argless function upcall with empty parens + +## Source + +```basic +OUT MEANING_OF_LIFE() +``` + +## Compilation errors + +```plain +1:5: MEANING_OF_LIFE expected no arguments +``` + +# Test: Error: using a function upcall that requires arguments without arguments + +## Source + +```basic +x = SUM_DOUBLES +``` + +## Compilation errors + +```plain +1:5: SUM_DOUBLES expected [arg1, .., argN] +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index f5ceccc1..c128db20 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -207,3 +207,31 @@ OUT local_var ```plain 6:5: Undefined global symbol local_var ``` + +# Test: Calling a command as a function with arguments + +## Source + +```basic +x = OUT(1) +``` + +## Compilation errors + +```plain +1:5: Cannot call OUT (not a function) +``` + +# Test: Using a command as an argless function + +## Source + +```basic +x = OUT +``` + +## Compilation errors + +```plain +1:5: Cannot call OUT (not a function) +``` diff --git a/core2/tests/testutils/callables/concat_fn.rs b/core2/tests/testutils/callables/concat_fn.rs new file mode 100644 index 00000000..f290a2f0 --- /dev/null +++ b/core2/tests/testutils/callables/concat_fn.rs @@ -0,0 +1,79 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A function that concatenates all of its string arguments. +pub(super) struct ConcatFunction { + metadata: CallableMetadata, +} + +impl ConcatFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("CONCAT") + .with_return_type(ExprType::Text) + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Long), + require_one: false, + allow_missing: true, + }), + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for ConcatFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut result = String::new(); + let mut reg = 1; + loop { + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + match etype { + ExprType::Text => result.push_str(scope.get_string(reg)), + _ => return Err(CallError::Other("Only accepts string values")), + } + sep + } + + _ => return Err(CallError::Other("Only accepts string values")), + }; + reg += 1; + + if sep == ArgSep::End { + break; + } + } + scope.return_string(result); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/is_positive_fn.rs b/core2/tests/testutils/callables/is_positive_fn.rs new file mode 100644 index 00000000..b0e67c1a --- /dev/null +++ b/core2/tests/testutils/callables/is_positive_fn.rs @@ -0,0 +1,56 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A function that returns whether its integer argument is positive. +pub(super) struct IsPositiveFunction { + metadata: CallableMetadata, +} + +impl IsPositiveFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("IS_POSITIVE") + .with_return_type(ExprType::Boolean) + .with_syntax(&[( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { name: Cow::Borrowed("n"), vtype: ExprType::Integer }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for IsPositiveFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let n = scope.get_integer(1); + scope.return_boolean(n > 0); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/meaning_of_life_fn.rs b/core2/tests/testutils/callables/meaning_of_life_fn.rs new file mode 100644 index 00000000..ef2a2b70 --- /dev/null +++ b/core2/tests/testutils/callables/meaning_of_life_fn.rs @@ -0,0 +1,48 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::rc::Rc; + +/// An argless function that returns the meaning of life (42). +pub(super) struct MeaningOfLifeFunction { + metadata: CallableMetadata, +} + +impl MeaningOfLifeFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("MEANING_OF_LIFE") + .with_return_type(ExprType::Integer) + .with_syntax(&[(&[], None)]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for MeaningOfLifeFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + scope.return_integer(42); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index 6262570a..e49b9d97 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -20,6 +20,9 @@ use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; +mod concat_fn; +use concat_fn::ConcatFunction; + mod define_arg_cmd; use define_arg_cmd::DefineArgCommand; @@ -29,6 +32,12 @@ use define_and_change_args_cmd::DefineAndChangeArgsCommand; mod increment_required_int_cmd; use increment_required_int_cmd::IncrementRequiredIntCommand; +mod is_positive_fn; +use is_positive_fn::IsPositiveFunction; + +mod meaning_of_life_fn; +use meaning_of_life_fn::MeaningOfLifeFunction; + mod out_any_value_cmd; use out_any_value_cmd::OutAnyValueCommand; @@ -47,6 +56,12 @@ use out_positional_cmd::OutPositionalCommand; mod out_required_value_cmd; use out_required_value_cmd::OutRequiredValueCommand; +mod sum_doubles_fn; +use sum_doubles_fn::SumDoublesFunction; + +mod sum_integers_fn; +use sum_integers_fn::SumIntegersFunction; + /// Formats the given argument `i` in `scope` as a string depending on its `etype`. fn format_arg(scope: &Scope<'_>, i: u8, etype: ExprType) -> String { match etype { @@ -79,15 +94,20 @@ pub(super) fn register_all( console: Rc>, ) { let cmds = [ - DefineArgCommand::new() as Rc, + ConcatFunction::new() as Rc, DefineAndChangeArgsCommand::new() as Rc, + DefineArgCommand::new() as Rc, IncrementRequiredIntCommand::new() as Rc, + IsPositiveFunction::new() as Rc, + MeaningOfLifeFunction::new() as Rc, OutAnyValueCommand::new(console.clone()) as Rc, OutAnyValueOptionalCommand::new(console.clone()) as Rc, OutCommand::new(console.clone()) as Rc, OutOptionalCommand::new(console.clone()) as Rc, OutPositionalCommand::new(console.clone()) as Rc, OutRequiredValueCommand::new(console) as Rc, + SumDoublesFunction::new() as Rc, + SumIntegersFunction::new() as Rc, ]; for cmd in cmds { upcalls_by_name.insert(SymbolKey::from(cmd.metadata().name()), cmd); diff --git a/core2/tests/testutils/callables/sum_doubles_fn.rs b/core2/tests/testutils/callables/sum_doubles_fn.rs new file mode 100644 index 00000000..55023147 --- /dev/null +++ b/core2/tests/testutils/callables/sum_doubles_fn.rs @@ -0,0 +1,80 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A function that adds all of its arguments. +pub(super) struct SumDoublesFunction { + metadata: CallableMetadata, +} + +impl SumDoublesFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("SUM_DOUBLES") + .with_return_type(ExprType::Double) + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Long), + require_one: false, + allow_missing: true, + }), + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for SumDoublesFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut total = 0.0; + let mut reg = 1; + loop { + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + match etype { + ExprType::Double => total += scope.get_double(reg), + ExprType::Integer => total += f64::from(scope.get_integer(reg)), + _ => return Err(CallError::Other("Only accepts numerical values")), + } + sep + } + + _ => return Err(CallError::Other("Only accepts numerical values")), + }; + reg += 1; + + if sep == ArgSep::End { + break; + } + } + scope.return_double(total); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/sum_integers_fn.rs b/core2/tests/testutils/callables/sum_integers_fn.rs new file mode 100644 index 00000000..36698634 --- /dev/null +++ b/core2/tests/testutils/callables/sum_integers_fn.rs @@ -0,0 +1,79 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A function that adds all of its integer arguments. +pub(super) struct SumIntegersFunction { + metadata: CallableMetadata, +} + +impl SumIntegersFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("SUM_INTEGERS") + .with_return_type(ExprType::Integer) + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Long), + require_one: false, + allow_missing: true, + }), + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for SumIntegersFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut total: i32 = 0; + let mut reg = 1; + loop { + let sep = match scope.get_type(reg) { + VarArgTag::Immediate(sep, etype) => { + reg += 1; + match etype { + ExprType::Integer => total += scope.get_integer(reg), + _ => return Err(CallError::Other("Only accepts integer values")), + } + sep + } + + _ => return Err(CallError::Other("Only accepts integer values")), + }; + reg += 1; + + if sep == ArgSep::End { + break; + } + } + scope.return_integer(total); + Ok(()) + } +} From 3ce69d92bc53114c73a2177ec4b94262fa76a252 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Wed, 25 Feb 2026 18:16:56 -0800 Subject: [PATCH 08/53] core2: Implement integer and double negation Add NegateDouble (NEGD) and NegateInteger (NEGI) bytecode instructions and wire them through the disassembler, compiler, and VM. --- core2/src/bytecode.rs | 34 ++++++ core2/src/compiler/exprs.rs | 17 +++ core2/src/image.rs | 2 + core2/src/vm/context.rs | 25 ++++ core2/tests/integration_test.rs | 3 +- ...t_arithmetic.md => test_arithmetic_add.md} | 0 core2/tests/test_arithmetic_neg.md | 115 ++++++++++++++++++ 7 files changed, 195 insertions(+), 1 deletion(-) rename core2/tests/{test_arithmetic.md => test_arithmetic_add.md} (100%) create mode 100644 core2/tests/test_arithmetic_neg.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index fcb9b974..9340d3fe 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -359,6 +359,12 @@ pub(crate) enum Opcode { /// Moves (copies) data between two registers. Move, + /// Negates a double value in place. + NegateDouble, + + /// Negates an integer value in place. + NegateInteger, + /// The "null" instruction, used by the compiler to pad the code for fixups. Nop, @@ -491,6 +497,20 @@ instr!( Register, 0x000000ff, 0, // Source register. ); +#[rustfmt::skip] +instr!( + Opcode::NegateDouble, "NEGD", + make_negate_double, parse_negate_double, format_negate_double, + Register, 0x000000ff, 0, // Register with the value to negate in place. +); + +#[rustfmt::skip] +instr!( + Opcode::NegateInteger, "NEGI", + make_negate_integer, parse_negate_integer, format_negate_integer, + Register, 0x000000ff, 0, // Register with the value to negate in place. +); + #[rustfmt::skip] instr!( Opcode::Nop, "NOP", @@ -720,6 +740,20 @@ mod tests { Register::local(2).unwrap() ); + test_instr!( + test_negate_double, + make_negate_double, + parse_negate_double, + Register::local(1).unwrap() + ); + + test_instr!( + test_negate_integer, + make_negate_integer, + parse_negate_integer, + Register::local(1).unwrap() + ); + test_instr!(test_nop, make_nop, parse_nop); test_instr!(test_return, make_return, parse_return); diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 44b55128..2b26fdb0 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -166,6 +166,23 @@ pub(super) fn compile_expr( Ok(ExprType::Integer) } + Expr::Negate(span) => { + let pos = span.pos; + let etype = compile_expr(codegen, symtable, reg, span.expr)?; + match etype { + ExprType::Double => { + codegen.emit(bytecode::make_negate_double(reg), pos); + } + ExprType::Integer => { + codegen.emit(bytecode::make_negate_integer(reg), pos); + } + _ => { + return Err(Error::NotANumber(pos, etype)); + } + } + Ok(etype) + } + Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { Ok((local, etype)) => { codegen.emit(bytecode::make_move(reg, local), span.pos); diff --git a/core2/src/image.rs b/core2/src/image.rs index fcd83210..78bd185e 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -41,6 +41,8 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::LoadInteger => bytecode::format_load_integer(instr), Opcode::LoadRegisterPointer => bytecode::format_load_register_ptr(instr), Opcode::Move => bytecode::format_move(instr), + Opcode::NegateDouble => bytecode::format_negate_double(instr), + Opcode::NegateInteger => bytecode::format_negate_integer(instr), Opcode::Nop => bytecode::format_nop(instr), Opcode::Return => bytecode::format_return(instr), Opcode::Upcall => bytecode::format_upcall(instr), diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 30851274..9875b408 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -160,6 +160,8 @@ impl Context { Opcode::LoadInteger => self.do_load_integer(instr), Opcode::LoadRegisterPointer => self.do_load_register_ptr(instr), Opcode::Move => self.do_move(instr), + Opcode::NegateDouble => self.do_negate_double(instr), + Opcode::NegateInteger => self.do_negate_integer(instr), Opcode::Nop => self.do_nop(instr), Opcode::Return => self.do_return(instr), Opcode::Upcall => self.do_upcall(instr), @@ -307,6 +309,29 @@ impl Context { self.pc += 1; } + /// Implements the `NegateDouble` opcode. + pub(super) fn do_negate_double(&mut self, instr: u32) { + let reg = bytecode::parse_negate_double(instr); + let value = f64::from_bits(self.get_reg(reg)); + self.set_reg(reg, (-value).to_bits()); + self.pc += 1; + } + + /// Implements the `NegateInteger` opcode. + pub(super) fn do_negate_integer(&mut self, instr: u32) { + let reg = bytecode::parse_negate_integer(instr); + let value = self.get_reg(reg) as i32; + match value.checked_neg() { + Some(result) => { + self.set_reg(reg, result as u64); + self.pc += 1; + } + None => { + self.set_exception("Integer overflow"); + } + } + } + /// Implements the `Nop` opcode. pub(super) fn do_nop(&mut self, instr: u32) { bytecode::parse_nop(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index e20aae29..dc9e8e7a 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -32,7 +32,8 @@ macro_rules! one_test { } one_test!(test_args); -one_test!(test_arithmetic); +one_test!(test_arithmetic_add); +one_test!(test_arithmetic_neg); one_test!(test_assignments); one_test!(test_empty); one_test!(test_end); diff --git a/core2/tests/test_arithmetic.md b/core2/tests/test_arithmetic_add.md similarity index 100% rename from core2/tests/test_arithmetic.md rename to core2/tests/test_arithmetic_add.md diff --git a/core2/tests/test_arithmetic_neg.md b/core2/tests/test_arithmetic_neg.md new file mode 100644 index 00000000..328ca3bb --- /dev/null +++ b/core2/tests/test_arithmetic_neg.md @@ -0,0 +1,115 @@ +# Test: Immediate double + +## Source + +```basic +OUT -3.5 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R65, 0 # 1:6 +0002: NEGD R65 # 1:5 +0003: LOADI R64, 257 # 1:5 +0004: UPCALL 0, R64 # 1:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=-3.5# +``` + +# Test: Immediate integer + +## Source + +```basic +OUT -7 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 7 # 1:6 +0002: NEGI R65 # 1:5 +0003: LOADI R64, 258 # 1:5 +0004: UPCALL 0, R64 # 1:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=-7% +``` + +# Test: Zero + +## Source + +```basic +OUT -0 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 1:6 +0002: NEGI R65 # 1:5 +0003: LOADI R64, 258 # 1:5 +0004: UPCALL 0, R64 # 1:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=0% +``` + +# Test: Non-numeric type + +## Source + +```basic +OUT -"hello" +``` + +## Compilation errors + +```plain +1:5: STRING is not a number +``` + +# Test: Integer overflow + +## Source + +```basic +a = -(&x80000000) +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:7 +0002: NEGI R64 # 1:5 +0003: LOADI R65, 0 # 0:0 +0004: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:5: Integer overflow +``` From 987da8eb29fa1fa9373da59a529d59ac47070dee Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 17 Feb 2026 13:36:34 -0800 Subject: [PATCH 09/53] core2: Implement multidimensional array support Add support for arrays in the core2 compiler and VM. Arrays use a flattened row-major storage on the heap (matching core's semantics) and are accessed via three new bytecode instructions: DIMA (allocate), ALOAD (read element), and ASTORE (write element). The compiler tracks arrays separately from scalar variables in the symbol tables, supporting both local and global (DIM SHARED) arrays. Array element access is handled in expression compilation by falling through from callable lookup to array lookup in Expr::Call. --- core2/src/bytecode.rs | 134 ++++++ core2/src/compiler/exprs.rs | 68 ++- core2/src/compiler/mod.rs | 16 + core2/src/compiler/syms.rs | 98 +++- core2/src/compiler/top.rs | 78 ++- core2/src/image.rs | 3 + core2/src/mem.rs | 65 ++- core2/src/num.rs | 6 + core2/src/vm/context.rs | 113 ++++- core2/tests/integration_test.rs | 1 + core2/tests/test_arrays.md | 819 ++++++++++++++++++++++++++++++++ 11 files changed, 1393 insertions(+), 8 deletions(-) create mode 100644 core2/tests/test_arrays.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 9340d3fe..862e7a70 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -50,6 +50,11 @@ impl fmt::Display for RegisterScope { #[error("Out of registers")] pub(crate) struct OutOfRegistersError(()); +/// Error to indicate that an array has too many dimensions. +#[derive(Debug, thiserror::Error)] +#[error("Too many dimensions")] +pub(crate) struct TooManyArrayDimensionsError(()); + /// Error types for bytecode parsing. #[derive(Debug, thiserror::Error)] pub(crate) enum ParseError { @@ -202,6 +207,53 @@ impl TaggedRegisterRef { } } +/// A packed representation of an array's element type and number of dimensions. +/// +/// The encoding stores the `ExprType` in the upper 4 bits and the dimension count in the +/// lower 4 bits of a single `u8`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct PackedArrayType(u8); + +impl PackedArrayType { + /// Creates a new packed array type from a subtype and dimension count. + pub(crate) fn new( + subtype: ExprType, + ndims: usize, + ) -> Result { + if ndims > 15 { + return Err(TooManyArrayDimensionsError(())); + } + let ndims = ndims as u8; + Ok(Self(((subtype as u8) << 4) | (ndims & 0x0f))) + } + + /// Returns the element type. + pub(crate) fn subtype(self) -> ExprType { + ExprType::from_u32(u32::from(self.0 >> 4)) + } + + /// Returns the number of dimensions. + pub(crate) fn ndims(self) -> u8 { + self.0 & 0x0f + } +} + +impl fmt::Display for PackedArrayType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "[{}]{}", self.ndims(), self.subtype().annotation()) + } +} + +impl RawValue for PackedArrayType { + fn from_u32(v: u32) -> Self { + Self(unchecked_u32_as_u8(v)) + } + + fn to_u32(self) -> u32 { + u32::from(self.0) + } +} + impl RawValue for ExprType { fn from_u32(v: u32) -> Self { #[allow(unsafe_code)] @@ -326,6 +378,9 @@ pub(crate) enum Opcode { /// Allocates an object on the heap. Alloc, + /// Allocates a multidimensional array on the heap. + AllocArray, + /// Calls an address relative to the PC. Call, @@ -347,6 +402,9 @@ pub(crate) enum Opcode { /// Jumps to an address relative to the PC. Jump, + /// Loads an element from an array. + LoadArray, + /// Loads a constant into a register. LoadConstant, @@ -371,6 +429,9 @@ pub(crate) enum Opcode { /// Returns from a previous `Call`. Return, + /// Stores a value into an array element. + StoreArray, + /// Requests the execution of an upcall, stopping VM execution. Upcall, @@ -405,6 +466,15 @@ instr!( ExprType, 0x000000ff, 0, // Type of the object to allocate. ); +#[rustfmt::skip] +instr!( + Opcode::AllocArray, "ALLOCA", + make_alloc_array, parse_alloc_array, format_alloc_array, + Register, 0x000000ff, 16, // Destination register to store the array pointer. + PackedArrayType, 0x000000ff, 8, // Packed element type and dimension count. + Register, 0x000000ff, 0, // First register containing dimension sizes. +); + #[rustfmt::skip] instr!( Opcode::Call, "CALL", @@ -464,6 +534,15 @@ instr!( u16, 0x0000ffff, 0, // Target address. ); +#[rustfmt::skip] +instr!( + Opcode::LoadArray, "LOADA", + make_load_array, parse_load_array, format_load_array, + Register, 0x000000ff, 16, // Destination register for the loaded value. + Register, 0x000000ff, 8, // Register containing the array pointer. + Register, 0x000000ff, 0, // First register containing subscript values. +); + #[rustfmt::skip] instr!( Opcode::LoadConstant, "LOADC", @@ -523,6 +602,15 @@ instr!( make_return, parse_return, format_return, ); +#[rustfmt::skip] +instr!( + Opcode::StoreArray, "STOREA", + make_store_array, parse_store_array, format_store_array, + Register, 0x000000ff, 16, // Register containing the array pointer. + Register, 0x000000ff, 8, // Register containing the value to store. + Register, 0x000000ff, 0, // First register containing subscript values. +); + #[rustfmt::skip] instr!( Opcode::Upcall, "UPCALL", @@ -674,6 +762,15 @@ mod tests { ExprType::Integer ); + test_instr!( + test_alloc_array, + make_alloc_array, + parse_alloc_array, + Register::local(1).unwrap(), + PackedArrayType::new(ExprType::Integer, 3).unwrap(), + Register::local(2).unwrap() + ); + test_instr!(test_call, make_call, parse_call, Register::local(3).unwrap(), 12345); test_instr!( @@ -707,6 +804,15 @@ mod tests { test_instr!(test_jump, make_jump, parse_jump, 12345); + test_instr!( + test_load_array, + make_load_array, + parse_load_array, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_load_constant, make_load_constant, @@ -758,8 +864,36 @@ mod tests { test_instr!(test_return, make_return, parse_return); + test_instr!( + test_store_array, + make_store_array, + parse_store_array, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_upcall, make_upcall, parse_upcall, 12345, Register::local(3).unwrap()); + #[test] + fn test_packed_array_type_round_trip() { + for subtype in [ExprType::Boolean, ExprType::Double, ExprType::Integer, ExprType::Text] { + for ndims in [1, 2, 5, 15] { + let packed = PackedArrayType::new(subtype, ndims).unwrap(); + assert_eq!(subtype, packed.subtype()); + assert_eq!(ndims, usize::from(packed.ndims())); + } + } + } + + #[test] + fn test_packed_array_type_display() { + let p = PackedArrayType::new(ExprType::Integer, 2).unwrap(); + assert_eq!("[2]%", format!("{}", p)); + let p = PackedArrayType::new(ExprType::Text, 1).unwrap(); + assert_eq!("[1]$", format!("{}", p)); + } + #[test] fn test_var_arg_tag_ok() { for sep in [ArgSep::As, ArgSep::End, ArgSep::Long, ArgSep::Short] { diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 2b26fdb0..dd3741b3 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -19,11 +19,70 @@ use crate::ast::{BinaryOpSpan, Expr, ExprType}; use crate::bytecode::{self, Register, RegisterScope}; use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; -use crate::compiler::syms::{self, SymbolKey, TempSymtable}; +use crate::compiler::syms::{self, SymbolKey, TempScope, TempSymtable}; use crate::compiler::{Error, Result}; use crate::mem::Datum; use std::convert::TryFrom; +/// Checks that a variable reference does not refer to an array being used without subscripts. +fn check_not_array( + symtable: &TempSymtable<'_, '_, '_, '_, '_>, + vref: &crate::ast::VarRef, + pos: crate::reader::LineCol, +) -> Result<()> { + let key = SymbolKey::from(&vref.name); + if symtable.is_array(&key) { + return Err(Error::ArrayUsedAsScalar(pos, vref.clone())); + } + Ok(()) +} + +/// Compiles `exprs` into consecutive integer registers allocated from `scope` and returns the +/// first register. The caller must guarantee that `exprs` is non-empty. +pub(super) fn compile_integer_exprs( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + scope: &mut TempScope, + pos: crate::reader::LineCol, + exprs: impl Iterator, +) -> Result { + let mut first_reg = None; + for expr in exprs { + let reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + if first_reg.is_none() { + first_reg = Some(reg); + } + compile_expr_as_type(codegen, symtable, reg, expr, ExprType::Integer)?; + } + Ok(first_reg.expect("Must have at least one expression")) +} + +/// Compiles an array element access expression into `reg`. +fn compile_array_access( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + reg: Register, + key_pos: crate::reader::LineCol, + arr_reg: Register, + info: &syms::ArrayInfo, + args: Vec, +) -> Result { + if args.len() != info.ndims { + return Err(Error::WrongNumberOfSubscripts(key_pos, info.ndims, args.len())); + } + + let mut outer_scope = symtable.temp_scope(); + let first_sub_reg = compile_integer_exprs( + codegen, + symtable, + &mut outer_scope, + key_pos, + args.into_iter().map(|a| a.expr.expect("Array subscripts must have expressions")), + )?; + codegen.emit(bytecode::make_load_array(reg, arr_reg, first_sub_reg), key_pos); + Ok(info.subtype) +} + /// Compiles an arithmetic binary operation `span` that returns its value into `reg`. fn compile_arithmetic_binary_op( codegen: &mut Codegen, @@ -104,6 +163,12 @@ pub(super) fn compile_expr( let key_pos = span.vref_pos; let Some(md) = symtable.get_callable(&key) else { + if let Some((arr_reg, info)) = symtable.get_array(&key) { + return compile_array_access( + codegen, symtable, reg, key_pos, arr_reg, &info, span.args, + ); + } + return Err(Error::UndefinedSymbol( span.vref_pos, span.vref, @@ -185,6 +250,7 @@ pub(super) fn compile_expr( Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { Ok((local, etype)) => { + check_not_array(symtable, &span.vref, span.pos)?; codegen.emit(bytecode::make_move(reg, local), span.pos); Ok(etype) } diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index a33efef6..453c49c4 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -43,6 +43,10 @@ pub enum Error { #[error("{0}: Cannot redefine {1}")] AlreadyDefined(LineCol, VarRef), + /// Array name used without subscripts (as a scalar). + #[error("{0}: {1} is an array and requires subscripts")] + ArrayUsedAsScalar(LineCol, VarRef), + /// Type mismatch in a binary operation. #[error("{0}: Cannot {1} {2} and {3}")] BinaryOpType(LineCol, &'static str, ExprType, ExprType), @@ -71,6 +75,10 @@ pub enum Error { #[error("{0}: Cannot call {1} (not a function)")] NotAFunction(LineCol, VarRef), + /// Attempt to index something that is not an array. + #[error("{0}: {1} is not an array")] + NotAnArray(LineCol, VarRef), + /// Expected a numeric type but got something else. #[error("{0}: {1} is not a number")] NotANumber(LineCol, ExprType), @@ -95,6 +103,10 @@ pub enum Error { #[error("{0}: Jump/call target is {1} which is too far")] TargetTooFar(LineCol, usize), + /// An array has too many dimensions. + #[error("{0}: Array cannot have {1} dimensions")] + TooManyArrayDimensions(LineCol, usize), + /// Type mismatch where a specific type was expected. #[error("{0}: Expected {2} but found {1}")] TypeMismatch(LineCol, ExprType, ExprType), @@ -102,6 +114,10 @@ pub enum Error { /// Reference to an undefined symbol. #[error("{0}: Undefined {2} symbol {1}")] UndefinedSymbol(LineCol, VarRef, RegisterScope), + + /// Wrong number of subscripts for an array access. + #[error("{0}: Array requires {1} subscripts but got {2}")] + WrongNumberOfSubscripts(LineCol, usize, usize), } impl Error { diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 92278dc8..6955e883 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -26,6 +26,16 @@ use std::convert::TryFrom; use std::fmt; use std::rc::Rc; +/// Information about an array tracked in the symbol table. +#[derive(Clone, Debug)] +pub(super) struct ArrayInfo { + /// Element type of the array. + pub(super) subtype: ExprType, + + /// Number of dimensions. + pub(super) ndims: usize, +} + /// Errors related to symbols handling. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] // The error messages and names are good enough. @@ -120,6 +130,9 @@ pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { /// Map of global variable names to their types and assigned registers. globals: HashMapWithIds, + /// Map of global array names to their array info. + global_arrays: HashMap, + /// Reference to the built-in callable metadata provided by the runtime. upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>, @@ -130,7 +143,12 @@ pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { /// Creates a new global symbol table that knows about the given `upcalls`. pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>) -> Self { - Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } + Self { + globals: HashMapWithIds::default(), + global_arrays: HashMap::default(), + upcalls, + user_callables: HashMap::default(), + } } /// Enters a new local scope. @@ -148,6 +166,30 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { put_var(key, vtype, &mut self.globals, Register::global, RegisterScope::Global) } + /// Creates a new global array `key` with `info` and assigns a register to it. + pub(crate) fn put_global_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { + let reg = put_var( + key.clone(), + info.subtype, + &mut self.globals, + Register::global, + RegisterScope::Global, + )?; + self.global_arrays.insert(key, info); + Ok(reg) + } + + /// Gets a global array by its `key`. + fn get_global_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { + if let Some(info) = self.global_arrays.get(key) + && let Some((_etype, reg)) = self.globals.get(key) + { + let reg = Register::global(reg).expect("Must be valid"); + return Some((reg, info.clone())); + } + None + } + /// Defines a new user-defined `vref` callable with `md` metadata. pub(crate) fn define_user_callable( &mut self, @@ -176,6 +218,9 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// Map of local variable names to their types and assigned registers. locals: HashMapWithIds, + /// Map of local array names to their array info. + local_arrays: HashMap, + /// Maximum number of allocated temporary registers in all possible evaluation scopes created /// by this local symtable. This is used to determine the size of the scope for register /// allocation purposes at runtime. @@ -185,7 +230,12 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// Creates a new local symbol table within the context of a global `symtable`. fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>) -> Self { - Self { symtable, locals: HashMapWithIds::default(), count_temps: 0 } + Self { + symtable, + locals: HashMapWithIds::default(), + local_arrays: HashMap::default(), + count_temps: 0, + } } /// Consumes the local scope and returns the number of local variables defined, which includes @@ -249,6 +299,40 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { None => Err(Error::UndefinedSymbol(vref.clone(), RegisterScope::Local)), } } + + /// Creates a new local array `key` with `info` and assigns a register to it. + pub(crate) fn put_local_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { + let reg = put_var( + key.clone(), + info.subtype, + &mut self.locals, + Register::local, + RegisterScope::Local, + )?; + self.local_arrays.insert(key, info); + Ok(reg) + } + + /// Creates a new global array `key` with `info` via the parent global symbol table. + pub(crate) fn put_global_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { + self.symtable.put_global_array(key, info) + } + + /// Gets an array by its `key`, looking in local arrays first, then global. + pub(crate) fn get_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { + if let Some(info) = self.local_arrays.get(key) + && let Some((_etype, reg)) = self.locals.get(key) + { + let reg = Register::local(reg).expect("Must be valid"); + return Some((reg, info.clone())); + } + self.symtable.get_global_array(key) + } + + /// Returns `true` if `key` refers to an array (local or global). + pub(crate) fn is_array(&self, key: &SymbolKey) -> bool { + self.local_arrays.contains_key(key) || self.symtable.global_arrays.contains_key(key) + } } /// A read-only view into a `SymTable` that allows allocating temporary registers. @@ -296,6 +380,16 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, self.symtable.get_callable(key) } + /// Gets an array by its `key`, looking in local arrays first, then global. + pub(crate) fn get_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { + self.symtable.get_array(key) + } + + /// Returns `true` if `key` refers to an array (local or global). + pub(crate) fn is_array(&self, key: &SymbolKey) -> bool { + self.symtable.is_array(key) + } + /// Enters a new temporary scope. pub(crate) fn temp_scope(&self) -> TempScope { let nlocals = u8::try_from(self.symtable.locals.len()) diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 8773a2b3..3d28e6c1 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,11 +16,11 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement}; -use crate::bytecode::{self, RegisterScope}; +use crate::bytecode::{self, PackedArrayType, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; -use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; +use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer_exprs}; use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey}; use crate::compiler::{Error, Result}; use crate::image::Image; @@ -55,7 +55,13 @@ fn compile_assignment( let vref_pos = span.vref_pos; let (reg, etype) = match symtable.get_local_or_global(&span.vref) { - Ok((reg, etype)) => (reg, Some(etype)), + Ok((reg, etype)) => { + let key = SymbolKey::from(&span.vref.name); + if symtable.is_array(&key) { + return Err(Error::ArrayUsedAsScalar(vref_pos, span.vref)); + } + (reg, Some(etype)) + } Err(syms::Error::UndefinedSymbol(..)) => { let key = SymbolKey::from(span.vref.name.clone()); @@ -96,6 +102,44 @@ fn compile_stmt( stmt: Statement, ) -> Result<()> { match stmt { + Statement::ArrayAssignment(span) => { + let key = SymbolKey::from(&span.vref.name); + let key_pos = span.vref_pos; + + let Some((arr_reg, info)) = symtable.get_array(&key) else { + return Err(Error::NotAnArray(key_pos, span.vref)); + }; + + if span.subscripts.len() != info.ndims { + return Err(Error::WrongNumberOfSubscripts( + key_pos, + info.ndims, + span.subscripts.len(), + )); + } + + let mut symtable = symtable.frozen(); + let mut outer_scope = symtable.temp_scope(); + + let val_reg = outer_scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?; + compile_expr_as_type( + &mut ctx.codegen, + &mut symtable, + val_reg, + span.expr, + info.subtype, + )?; + + let first_sub_reg = compile_integer_exprs( + &mut ctx.codegen, + &mut symtable, + &mut outer_scope, + key_pos, + span.subscripts.into_iter(), + )?; + ctx.codegen.emit(bytecode::make_store_array(arr_reg, val_reg, first_sub_reg), key_pos); + } + Statement::Assignment(span) => { compile_assignment(&mut ctx.codegen, symtable, span)?; } @@ -170,6 +214,34 @@ fn compile_stmt( ctx.codegen.emit_default(reg, span.vtype, name_pos); } + Statement::DimArray(span) => { + let key = SymbolKey::from(span.name); + let name_pos = span.name_pos; + let ndims = span.dimensions.len(); + + let info = syms::ArrayInfo { subtype: span.subtype, ndims }; + let reg = if span.shared { + symtable.put_global_array(key, info) + } else { + symtable.put_local_array(key, info) + } + .map_err(|e| Error::from_syms(e, name_pos))?; + + let mut symtable = symtable.frozen(); + let mut outer_scope = symtable.temp_scope(); + + let first_dim_reg = compile_integer_exprs( + &mut ctx.codegen, + &mut symtable, + &mut outer_scope, + name_pos, + span.dimensions.into_iter(), + )?; + let packed = PackedArrayType::new(span.subtype, ndims) + .map_err(|_| Error::TooManyArrayDimensions(span.name_pos, ndims))?; + ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), name_pos); + } + Statement::End(span) => { let mut symtable = symtable.frozen(); let mut scope = symtable.temp_scope(); diff --git a/core2/src/image.rs b/core2/src/image.rs index 78bd185e..620f34a9 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -29,6 +29,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::AddDouble => bytecode::format_add_double(instr), Opcode::AddInteger => bytecode::format_add_integer(instr), Opcode::Alloc => bytecode::format_alloc(instr), + Opcode::AllocArray => bytecode::format_alloc_array(instr), Opcode::Call => bytecode::format_call(instr), Opcode::Concat => bytecode::format_concat(instr), Opcode::DoubleToInteger => bytecode::format_double_to_integer(instr), @@ -37,6 +38,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::Gosub => bytecode::format_gosub(instr), Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), Opcode::Jump => bytecode::format_jump(instr), + Opcode::LoadArray => bytecode::format_load_array(instr), Opcode::LoadConstant => bytecode::format_load_constant(instr), Opcode::LoadInteger => bytecode::format_load_integer(instr), Opcode::LoadRegisterPointer => bytecode::format_load_register_ptr(instr), @@ -45,6 +47,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::NegateInteger => bytecode::format_negate_integer(instr), Opcode::Nop => bytecode::format_nop(instr), Opcode::Return => bytecode::format_return(instr), + Opcode::StoreArray => bytecode::format_store_array(instr), Opcode::Upcall => bytecode::format_upcall(instr), } } diff --git a/core2/src/mem.rs b/core2/src/mem.rs index 290525a2..3d2f048a 100644 --- a/core2/src/mem.rs +++ b/core2/src/mem.rs @@ -20,9 +20,47 @@ use crate::num::{U24, unchecked_u24_as_usize}; use std::convert::TryFrom; use std::hash::Hash; +/// Data for a multidimensional array stored on the heap. +#[derive(Clone, Debug)] +pub(crate) struct ArrayData { + /// Size of each dimension. + pub(crate) dimensions: Vec, + + /// Flattened row-major storage of element values as `u64` (matching register representation). + pub(crate) values: Vec, +} + +impl ArrayData { + /// Computes the flat index into `values` for the given `subscripts`, with bounds checking. + pub(crate) fn flat_index(&self, subscripts: &[i32]) -> Result { + debug_assert_eq!( + subscripts.len(), + self.dimensions.len(), + "Invalid number of subscripts; guaranteed valid by the compiler" + ); + + let mut offset = 0; + let mut multiplier = 1; + for (s, d) in subscripts.iter().zip(&self.dimensions) { + let Ok(s) = usize::try_from(*s) else { + return Err(format!("Subscript {} cannot be negative", s)); + }; + if s >= *d { + return Err(format!("Subscript {} exceeds limit of {}", s, d)); + } + offset += s * multiplier; + multiplier *= d; + } + Ok(offset) + } +} + /// A single value in the EndBASIC language. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug)] pub(crate) enum Datum { + /// An array value. + Array(ArrayData), + /// A boolean value. Boolean(bool), @@ -36,11 +74,25 @@ pub(crate) enum Datum { Text(String), } +impl PartialEq for Datum { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Boolean(a), Self::Boolean(b)) => a == b, + (Self::Double(a), Self::Double(b)) => a.to_bits() == b.to_bits(), + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Text(a), Self::Text(b)) => a == b, + _ => false, + } + } +} + impl Eq for Datum {} impl Hash for Datum { fn hash(&self, state: &mut H) { + std::mem::discriminant(self).hash(state); match self { + Self::Array(_) => panic!("Arrays cannot be hashed"), Self::Boolean(b) => b.hash(state), Self::Double(d) => d.to_bits().hash(state), Self::Integer(i) => i.hash(state), @@ -63,6 +115,7 @@ impl Datum { /// Returns the type of the datum. pub(crate) fn etype(&self) -> ExprType { match self { + Self::Array(..) => panic!("Arrays do not have a simple ExprType"), Self::Boolean(..) => ExprType::Boolean, Self::Double(..) => ExprType::Double, Self::Integer(..) => ExprType::Integer, @@ -114,4 +167,14 @@ impl DatumPtr { DatumPtr::Heap(index) => &heap[unchecked_u24_as_usize(*index)], } } + + /// Extracts the heap index from this pointer. + /// + /// Panics if this is not a heap pointer. + pub(crate) fn heap_index(&self) -> usize { + match self { + DatumPtr::Heap(index) => unchecked_u24_as_usize(*index), + DatumPtr::Constant(_) => panic!("Expected a heap pointer"), + } + } } diff --git a/core2/src/num.rs b/core2/src/num.rs index a88c8966..d8ea9dcb 100644 --- a/core2/src/num.rs +++ b/core2/src/num.rs @@ -80,6 +80,7 @@ impl_unchecked_cast!(unchecked_u32_as_u8, u32, u8, primitive); impl_unchecked_cast!(unchecked_u32_as_u16, u32, u16, primitive); impl_unchecked_cast!(unchecked_u32_as_usize, u32, usize, primitive); impl_unchecked_cast!(unchecked_u64_as_u8, u64, u8, primitive); +impl_unchecked_cast!(unchecked_usize_as_u8, usize, u8, primitive); impl_unchecked_cast!(unchecked_usize_as_u32, usize, u32, primitive); impl UncheckedFrom for usize { @@ -116,6 +117,11 @@ mod tests { assert_eq!(10u8, unchecked_u64_as_u8(10u64)); } + #[test] + fn test_unchecked_usize_as_u8() { + assert_eq!(10u8, unchecked_usize_as_u8(10_usize)); + } + #[test] fn test_unchecked_usize_as_u32() { assert_eq!(10u32, unchecked_usize_as_u32(10_usize)); diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 9875b408..75295a3b 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -15,10 +15,12 @@ //! Virtual processor for EndBASIC execution. +use crate::ExprType; use crate::Scope; use crate::bytecode::{self, Opcode, Register, TaggedRegisterRef, opcode_of}; use crate::image::Image; -use crate::mem::{Datum, DatumPtr}; +use crate::mem::{ArrayData, Datum, DatumPtr}; +use crate::num::unchecked_usize_as_u8; /// Alias for the type representing a program address. type Address = usize; @@ -113,6 +115,40 @@ impl Context { DatumPtr::from(raw_addr).resolve(constants, heap) } + /// Resolves array subscripts and computes the flat index for `arr_reg` with subscripts read + /// from registers starting at `first_sub_reg`. + /// + /// Returns `Some((heap_idx, flat_idx))` on success, or `None` if an exception was set. + fn resolve_array_index( + &mut self, + arr_reg: Register, + first_sub_reg: Register, + heap: &[Datum], + ) -> Option<(usize, usize)> { + let arr_ptr = DatumPtr::from(self.get_reg(arr_reg)); + let heap_idx = arr_ptr.heap_index(); + let array = match &heap[heap_idx] { + Datum::Array(a) => a, + _ => unreachable!("Register must point to an array"), + }; + + let ndims = array.dimensions.len(); + let (_, first_idx) = first_sub_reg.to_parts(); + let mut subscripts = Vec::with_capacity(ndims); + for i in 0..unchecked_usize_as_u8(ndims) { + let sub_reg = Register::local(first_idx + i).unwrap(); + subscripts.push(self.get_reg(sub_reg) as i32); + } + + match array.flat_index(&subscripts) { + Ok(flat_idx) => Some((heap_idx, flat_idx)), + Err(e) => { + self.set_exception(e); + None + } + } + } + /// Registers that the instruction being processed threw an exception `message`. /// /// It's the responsibility of the execution loop to check for the presence of exceptions and @@ -148,6 +184,7 @@ impl Context { Opcode::AddDouble => self.do_add_double(instr), Opcode::AddInteger => self.do_add_integer(instr), Opcode::Alloc => self.do_alloc(instr, heap), + Opcode::AllocArray => self.do_alloc_array(instr, heap), Opcode::Call => self.do_call(instr), Opcode::Concat => self.do_concat(instr, &image.constants, heap), Opcode::DoubleToInteger => self.do_double_to_integer(instr), @@ -156,6 +193,7 @@ impl Context { Opcode::Gosub => self.do_gosub(instr), Opcode::IntegerToDouble => self.do_integer_to_double(instr), Opcode::Jump => self.do_jump(instr), + Opcode::LoadArray => self.do_load_array(instr, heap), Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), Opcode::LoadInteger => self.do_load_integer(instr), Opcode::LoadRegisterPointer => self.do_load_register_ptr(instr), @@ -164,6 +202,7 @@ impl Context { Opcode::NegateInteger => self.do_negate_integer(instr), Opcode::Nop => self.do_nop(instr), Opcode::Return => self.do_return(instr), + Opcode::StoreArray => self.do_store_array(instr, heap), Opcode::Upcall => self.do_upcall(instr), } } @@ -206,6 +245,48 @@ impl Context { self.pc += 1; } + /// Implements the `AllocArray` opcode. + pub(super) fn do_alloc_array(&mut self, instr: u32, heap: &mut Vec) { + let (dest, packed, first_dim_reg) = bytecode::parse_alloc_array(instr); + let subtype = packed.subtype(); + let ndims = usize::from(packed.ndims()); + + let (_, first_idx) = first_dim_reg.to_parts(); + let mut dimensions = Vec::with_capacity(ndims); + let mut total: usize = 1; + for i in 0..ndims { + let dim_reg = Register::local(first_idx + i as u8).unwrap(); + let dim = match usize::try_from(self.get_reg(dim_reg) as i32) { + Ok(0) | Err(_) => { + self.set_exception(format!("Dimension {} must be positive", i)); + return; + } + Ok(n) => n, + }; + dimensions.push(dim); + total *= dim; + } + + let values = match subtype { + ExprType::Boolean | ExprType::Double | ExprType::Integer => { + vec![0; total] + } + ExprType::Text => { + let mut values = Vec::with_capacity(total); + for _ in 0..total { + heap.push(Datum::Text(String::new())); + values.push(DatumPtr::for_heap((heap.len() - 1) as u32)); + } + values + } + }; + let array = ArrayData { dimensions, values }; + heap.push(Datum::Array(array)); + let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); + self.set_reg(dest, ptr); + self.pc += 1; + } + /// Implements the `Call` opcode. pub(super) fn do_call(&mut self, instr: u32) { let (reg, offset) = bytecode::parse_call(instr); @@ -274,10 +355,25 @@ impl Context { self.pc = Address::from(offset); } + /// Implements the `LoadArray` opcode. + pub(super) fn do_load_array(&mut self, instr: u32, heap: &[Datum]) { + let (dest, arr_reg, first_sub_reg) = bytecode::parse_load_array(instr); + + if let Some((heap_idx, flat_idx)) = self.resolve_array_index(arr_reg, first_sub_reg, heap) { + let array = match &heap[heap_idx] { + Datum::Array(a) => a, + _ => unreachable!("Register must point to an array"), + }; + self.set_reg(dest, array.values[flat_idx]); + self.pc += 1; + } + } + /// Implements the `LoadConstant` opcode. pub(super) fn do_load_constant(&mut self, instr: u32, constants: &[Datum]) { let (register, i) = bytecode::parse_load_constant(instr); match &constants[usize::from(i)] { + Datum::Array(_) => unreachable!("Arrays cannot be constants"), Datum::Boolean(_) => unreachable!("Booleans are always immediates"), Datum::Double(d) => self.set_reg(register, d.to_bits()), Datum::Integer(i) => self.set_reg(register, *i as u64), @@ -359,6 +455,21 @@ impl Context { } } + /// Implements the `StoreArray` opcode. + pub(super) fn do_store_array(&mut self, instr: u32, heap: &mut [Datum]) { + let (arr_reg, val_reg, first_sub_reg) = bytecode::parse_store_array(instr); + + let value = self.get_reg(val_reg); + if let Some((heap_idx, flat_idx)) = self.resolve_array_index(arr_reg, first_sub_reg, heap) { + let array = match &mut heap[heap_idx] { + Datum::Array(a) => a, + _ => unreachable!("Register must point to an array"), + }; + array.values[flat_idx] = value; + self.pc += 1; + } + } + /// Implements the `Upcall` opcode. pub(super) fn do_upcall(&mut self, instr: u32) { let (index, first_reg) = bytecode::parse_upcall(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index dc9e8e7a..44ecfee3 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -34,6 +34,7 @@ macro_rules! one_test { one_test!(test_args); one_test!(test_arithmetic_add); one_test!(test_arithmetic_neg); +one_test!(test_arrays); one_test!(test_assignments); one_test!(test_empty); one_test!(test_end); diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md new file mode 100644 index 00000000..a3614776 --- /dev/null +++ b/core2/tests/test_arrays.md @@ -0,0 +1,819 @@ +# Test: 1D integer array + +## Source + +```basic +DIM a(3) AS INTEGER +a(0) = 10 +a(1) = 20 +a(2) = 30 +OUT a(0), a(1), a(2) +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 10 # 2:8 +0004: LOADI R66, 0 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R65, 20 # 3:8 +0007: LOADI R66, 1 # 3:3 +0008: STOREA R64, R65, R66 # 3:1 +0009: LOADI R65, 30 # 4:8 +0010: LOADI R66, 2 # 4:3 +0011: STOREA R64, R65, R66 # 4:1 +0012: LOADI R67, 0 # 5:7 +0013: LOADA R66, R64, R67 # 5:5 +0014: LOADI R65, 290 # 5:5 +0015: LOADI R69, 1 # 5:13 +0016: LOADA R68, R64, R69 # 5:11 +0017: LOADI R67, 290 # 5:11 +0018: LOADI R71, 2 # 5:19 +0019: LOADA R70, R64, R71 # 5:17 +0020: LOADI R69, 258 # 5:17 +0021: UPCALL 0, R65 # 5:1, OUT +0022: LOADI R65, 0 # 0:0 +0023: END R65 # 0:0 +``` + +## Output + +```plain +0=10% , 1=20% , 2=30% +``` + +# Test: 2D integer array + +## Source + +```basic +DIM m(2, 3) AS INTEGER +m(0, 0) = 1 +m(0, 1) = 2 +m(0, 2) = 3 +m(1, 0) = 4 +m(1, 1) = 5 +m(1, 2) = 6 +OUT m(0, 0), m(0, 2), m(1, 1), m(1, 2) +``` + +## Disassembly + +```asm +0000: ENTER 11 # 0:0 +0001: LOADI R65, 2 # 1:7 +0002: LOADI R66, 3 # 1:10 +0003: ALLOCA R64, [2]%, R65 # 1:5 +0004: LOADI R65, 1 # 2:11 +0005: LOADI R66, 0 # 2:3 +0006: LOADI R67, 0 # 2:6 +0007: STOREA R64, R65, R66 # 2:1 +0008: LOADI R65, 2 # 3:11 +0009: LOADI R66, 0 # 3:3 +0010: LOADI R67, 1 # 3:6 +0011: STOREA R64, R65, R66 # 3:1 +0012: LOADI R65, 3 # 4:11 +0013: LOADI R66, 0 # 4:3 +0014: LOADI R67, 2 # 4:6 +0015: STOREA R64, R65, R66 # 4:1 +0016: LOADI R65, 4 # 5:11 +0017: LOADI R66, 1 # 5:3 +0018: LOADI R67, 0 # 5:6 +0019: STOREA R64, R65, R66 # 5:1 +0020: LOADI R65, 5 # 6:11 +0021: LOADI R66, 1 # 6:3 +0022: LOADI R67, 1 # 6:6 +0023: STOREA R64, R65, R66 # 6:1 +0024: LOADI R65, 6 # 7:11 +0025: LOADI R66, 1 # 7:3 +0026: LOADI R67, 2 # 7:6 +0027: STOREA R64, R65, R66 # 7:1 +0028: LOADI R67, 0 # 8:7 +0029: LOADI R68, 0 # 8:10 +0030: LOADA R66, R64, R67 # 8:5 +0031: LOADI R65, 290 # 8:5 +0032: LOADI R69, 0 # 8:16 +0033: LOADI R70, 2 # 8:19 +0034: LOADA R68, R64, R69 # 8:14 +0035: LOADI R67, 290 # 8:14 +0036: LOADI R71, 1 # 8:25 +0037: LOADI R72, 1 # 8:28 +0038: LOADA R70, R64, R71 # 8:23 +0039: LOADI R69, 290 # 8:23 +0040: LOADI R73, 1 # 8:34 +0041: LOADI R74, 2 # 8:37 +0042: LOADA R72, R64, R73 # 8:32 +0043: LOADI R71, 258 # 8:32 +0044: UPCALL 0, R65 # 8:1, OUT +0045: LOADI R65, 0 # 0:0 +0046: END R65 # 0:0 +``` + +## Output + +```plain +0=1% , 1=3% , 2=5% , 3=6% +``` + +# Test: Boolean array + +## Source + +```basic +DIM flags(2) AS BOOLEAN +flags(0) = TRUE +OUT flags(0), flags(1) +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 2 # 1:11 +0002: ALLOCA R64, [1]?, R65 # 1:5 +0003: LOADI R65, 1 # 2:12 +0004: LOADI R66, 0 # 2:7 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R67, 0 # 3:11 +0007: LOADA R66, R64, R67 # 3:5 +0008: LOADI R65, 288 # 3:5 +0009: LOADI R69, 1 # 3:21 +0010: LOADA R68, R64, R69 # 3:15 +0011: LOADI R67, 256 # 3:15 +0012: UPCALL 0, R65 # 3:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=true? , 1=false? +``` + +# Test: Double array + +## Source + +```basic +DIM d(2) AS DOUBLE +d(0) = 3.14 +OUT d(0), d(1) +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 2 # 1:7 +0002: ALLOCA R64, [1]#, R65 # 1:5 +0003: LOADC R65, 0 # 2:8 +0004: LOADI R66, 0 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R67, 0 # 3:7 +0007: LOADA R66, R64, R67 # 3:5 +0008: LOADI R65, 289 # 3:5 +0009: LOADI R69, 1 # 3:13 +0010: LOADA R68, R64, R69 # 3:11 +0011: LOADI R67, 257 # 3:11 +0012: UPCALL 0, R65 # 3:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=3.14# , 1=0# +``` + +# Test: String array + +## Source + +```basic +DIM s(3) AS STRING +s(0) = "hello" +s(1) = "world" +OUT s(0), s(1), s(2) +``` + +## Disassembly + +```asm +0000: ENTER 8 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]$, R65 # 1:5 +0003: LOADI R65, 0 # 2:8 +0004: LOADI R66, 0 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R65, 1 # 3:8 +0007: LOADI R66, 1 # 3:3 +0008: STOREA R64, R65, R66 # 3:1 +0009: LOADI R67, 0 # 4:7 +0010: LOADA R66, R64, R67 # 4:5 +0011: LOADI R65, 291 # 4:5 +0012: LOADI R69, 1 # 4:13 +0013: LOADA R68, R64, R69 # 4:11 +0014: LOADI R67, 291 # 4:11 +0015: LOADI R71, 2 # 4:19 +0016: LOADA R70, R64, R71 # 4:17 +0017: LOADI R69, 259 # 4:17 +0018: UPCALL 0, R65 # 4:1, OUT +0019: LOADI R65, 0 # 0:0 +0020: END R65 # 0:0 +``` + +## Output + +```plain +0=hello$ , 1=world$ , 2=$ +``` + +# Test: DIM SHARED array + +## Source + +```basic +SUB fill_array + a(0) = 100 + a(1) = 200 +END SUB + +DIM SHARED a(2) AS INTEGER +fill_array +OUT a(0), a(1) +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 2 # 6:14 +0002: ALLOCA R0, [1]%, R64 # 6:12 +0003: CALL R64, 13 # 7:1, FILL_ARRAY +0004: LOADI R66, 0 # 8:7 +0005: LOADA R65, R0, R66 # 8:5 +0006: LOADI R64, 290 # 8:5 +0007: LOADI R68, 1 # 8:13 +0008: LOADA R67, R0, R68 # 8:11 +0009: LOADI R66, 258 # 8:11 +0010: UPCALL 0, R64 # 8:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 + +-- FILL_ARRAY +0013: ENTER 2 # 0:0 +0014: LOADI R64, 100 # 2:12 +0015: LOADI R65, 0 # 2:7 +0016: STOREA R0, R64, R65 # 2:5 +0017: LOADI R64, 200 # 3:12 +0018: LOADI R65, 1 # 3:7 +0019: STOREA R0, R64, R65 # 3:5 +0020: RETURN # 4:1 +``` + +## Output + +```plain +0=100% , 1=200% +``` + +# Test: Array reference with matching annotation + +## Source + +```basic +DIM a(3) AS INTEGER +OUT a%(1) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R67, 1 # 2:8 +0004: LOADA R66, R64, R67 # 2:5 +0005: LOADI R65, 258 # 2:5 +0006: UPCALL 0, R65 # 2:1, OUT +0007: LOADI R65, 0 # 0:0 +0008: END R65 # 0:0 +``` + +## Output + +```plain +0=0% +``` + +# Test: Array reference index is double and needs demotion + +## Source + +```basic +DIM a(4) AS INTEGER +a(2) = 99 +OUT a(1.9) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 4 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 99 # 2:8 +0004: LOADI R66, 2 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADC R67, 0 # 3:7 +0007: DTOI R67 # 3:7 +0008: LOADA R66, R64, R67 # 3:5 +0009: LOADI R65, 258 # 3:5 +0010: UPCALL 0, R65 # 3:1, OUT +0011: LOADI R65, 0 # 0:0 +0012: END R65 # 0:0 +``` + +## Output + +```plain +0=99% +``` + +# Test: Array reference index has bad type + +## Source + +```basic +DIM a(3) AS INTEGER +OUT a(FALSE) +``` + +## Compilation errors + +```plain +2:7: BOOLEAN is not a number +``` + +# Test: Array reference has wrong number of dimensions + +## Source + +```basic +DIM a(2, 3) AS INTEGER +OUT a(1) +``` + +## Compilation errors + +```plain +2:5: Array requires 2 subscripts but got 1 +``` + +# Test: Undefined symbol in array-style expression + +## Source + +```basic +OUT foo(1) +``` + +## Compilation errors + +```plain +1:5: Undefined global symbol foo +``` + +# Test: Scalar used as array-style expression + +## Source + +```basic +a = 3 +OUT a(1) +``` + +## Compilation errors + +```plain +2:5: Undefined global symbol a +``` + +# Test: Array dimension too large + +## Source + +```basic +DIM a(1000000000000000) +``` + +## Compilation errors + +```plain +1:7: Bad integer 1000000000000000: number too large to fit in target type +``` + +# Test: Array dimension is zero + +## Source + +```basic +DIM a(1, 0, 1) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 1 # 1:7 +0002: LOADI R66, 0 # 1:10 +0003: LOADI R67, 1 # 1:13 +0004: ALLOCA R64, [3]%, R65 # 1:5 +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:5: Dimension 1 must be positive +``` + +# Test: Array dimension is negative + +## Source + +```basic +DIM a(1, -5, 1) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 1 # 1:7 +0002: LOADI R66, 5 # 1:11 +0003: NEGI R66 # 1:10 +0004: LOADI R67, 1 # 1:14 +0005: ALLOCA R64, [3]%, R65 # 1:5 +0006: LOADI R65, 0 # 0:0 +0007: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:5: Dimension 1 must be positive +``` + +# Test: Array bounds error subscript too large + +## Source + +```basic +DIM a(3) AS INTEGER +a(5) = 10 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 10 # 2:8 +0004: LOADI R66, 5 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R65, 0 # 0:0 +0007: END R65 # 0:0 +``` + +## Runtime errors + +```plain +2:1: Subscript 5 exceeds limit of 3 +``` + +# Test: Array index is negative + +## Source + +```basic +DIM a(3) AS INTEGER +a(-5) = 10 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 10 # 2:9 +0004: LOADI R66, 5 # 2:4 +0005: NEGI R66 # 2:3 +0006: STOREA R64, R65, R66 # 2:1 +0007: LOADI R65, 0 # 0:0 +0008: END R65 # 0:0 +``` + +## Runtime errors + +```plain +2:1: Subscript -5 cannot be negative +``` + +# Test: Array bounds error subscript at limit + +## Source + +```basic +DIM a(3) AS INTEGER +OUT a(3) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R67, 3 # 2:7 +0004: LOADA R66, R64, R67 # 2:5 +0005: LOADI R65, 258 # 2:5 +0006: UPCALL 0, R65 # 2:1, OUT +0007: LOADI R65, 0 # 0:0 +0008: END R65 # 0:0 +``` + +## Runtime errors + +```plain +2:5: Subscript 3 exceeds limit of 3 +``` + +# Test: Multidimensional array bounds error + +## Source + +```basic +DIM a(5, 100) AS INTEGER +a(10, 50) = 123 +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R65, 5 # 1:7 +0002: LOADI R66, 100 # 1:10 +0003: ALLOCA R64, [2]%, R65 # 1:5 +0004: LOADI R65, 123 # 2:13 +0005: LOADI R66, 10 # 2:3 +0006: LOADI R67, 50 # 2:7 +0007: STOREA R64, R65, R66 # 2:1 +0008: LOADI R65, 0 # 0:0 +0009: END R65 # 0:0 +``` + +## Runtime errors + +```plain +2:1: Subscript 10 exceeds limit of 5 +``` + +# Test: Default values + +## Source + +```basic +DIM a(3) AS INTEGER +DIM b(2) AS BOOLEAN +DIM d(2) AS DOUBLE +DIM s(2) AS STRING +OUT a(0), a(1), a(2) +OUT b(0), b(1) +OUT d(0), d(1) +OUT s(0), s(1) +``` + +## Disassembly + +```asm +0000: ENTER 11 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R66, 2 # 2:7 +0004: ALLOCA R65, [1]?, R66 # 2:5 +0005: LOADI R67, 2 # 3:7 +0006: ALLOCA R66, [1]#, R67 # 3:5 +0007: LOADI R68, 2 # 4:7 +0008: ALLOCA R67, [1]$, R68 # 4:5 +0009: LOADI R70, 0 # 5:7 +0010: LOADA R69, R64, R70 # 5:5 +0011: LOADI R68, 290 # 5:5 +0012: LOADI R72, 1 # 5:13 +0013: LOADA R71, R64, R72 # 5:11 +0014: LOADI R70, 290 # 5:11 +0015: LOADI R74, 2 # 5:19 +0016: LOADA R73, R64, R74 # 5:17 +0017: LOADI R72, 258 # 5:17 +0018: UPCALL 0, R68 # 5:1, OUT +0019: LOADI R70, 0 # 6:7 +0020: LOADA R69, R65, R70 # 6:5 +0021: LOADI R68, 288 # 6:5 +0022: LOADI R72, 1 # 6:13 +0023: LOADA R71, R65, R72 # 6:11 +0024: LOADI R70, 256 # 6:11 +0025: UPCALL 0, R68 # 6:1, OUT +0026: LOADI R70, 0 # 7:7 +0027: LOADA R69, R66, R70 # 7:5 +0028: LOADI R68, 289 # 7:5 +0029: LOADI R72, 1 # 7:13 +0030: LOADA R71, R66, R72 # 7:11 +0031: LOADI R70, 257 # 7:11 +0032: UPCALL 0, R68 # 7:1, OUT +0033: LOADI R70, 0 # 8:7 +0034: LOADA R69, R67, R70 # 8:5 +0035: LOADI R68, 291 # 8:5 +0036: LOADI R72, 1 # 8:13 +0037: LOADA R71, R67, R72 # 8:11 +0038: LOADI R70, 259 # 8:11 +0039: UPCALL 0, R68 # 8:1, OUT +0040: LOADI R68, 0 # 0:0 +0041: END R68 # 0:0 +``` + +## Output + +```plain +0=0% , 1=0% , 2=0% +0=false? , 1=false? +0=0# , 1=0# +0=$ , 1=$ +``` + +# Test: Multiple arrays in same scope + +## Source + +```basic +DIM x(2) AS INTEGER +DIM y(2) AS INTEGER +x(0) = 1 +y(0) = 2 +OUT x(0), y(0) +``` + +## Disassembly + +```asm +0000: ENTER 7 # 0:0 +0001: LOADI R65, 2 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R66, 2 # 2:7 +0004: ALLOCA R65, [1]%, R66 # 2:5 +0005: LOADI R66, 1 # 3:8 +0006: LOADI R67, 0 # 3:3 +0007: STOREA R64, R66, R67 # 3:1 +0008: LOADI R66, 2 # 4:8 +0009: LOADI R67, 0 # 4:3 +0010: STOREA R65, R66, R67 # 4:1 +0011: LOADI R68, 0 # 5:7 +0012: LOADA R67, R64, R68 # 5:5 +0013: LOADI R66, 290 # 5:5 +0014: LOADI R70, 0 # 5:13 +0015: LOADA R69, R65, R70 # 5:11 +0016: LOADI R68, 258 # 5:11 +0017: UPCALL 0, R66 # 5:1, OUT +0018: LOADI R66, 0 # 0:0 +0019: END R66 # 0:0 +``` + +## Output + +```plain +0=1% , 1=2% +``` + +# Test: Array inside a function + +## Source + +```basic +FUNCTION sum%(n%) + DIM a(3) AS INTEGER + a(0) = 10 + a(1) = 20 + a(2) = 30 + sum = a(0) + a(1) + a(2) +END FUNCTION + +OUT sum(0) +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R66, 0 # 9:9 +0002: CALL R65, 7 # 9:5, SUM +0003: LOADI R64, 258 # 9:5 +0004: UPCALL 0, R64 # 9:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 + +-- SUM +0007: LOADI R64, 0 # 1:10 +0008: ENTER 7 # 0:0 +0009: LOADI R67, 3 # 2:11 +0010: ALLOCA R66, [1]%, R67 # 2:9 +0011: LOADI R67, 10 # 3:12 +0012: LOADI R68, 0 # 3:7 +0013: STOREA R66, R67, R68 # 3:5 +0014: LOADI R67, 20 # 4:12 +0015: LOADI R68, 1 # 4:7 +0016: STOREA R66, R67, R68 # 4:5 +0017: LOADI R67, 30 # 5:12 +0018: LOADI R68, 2 # 5:7 +0019: STOREA R66, R67, R68 # 5:5 +0020: LOADI R69, 0 # 6:13 +0021: LOADA R68, R66, R69 # 6:11 +0022: LOADI R70, 1 # 6:20 +0023: LOADA R69, R66, R70 # 6:18 +0024: ADDI R67, R68, R69 # 6:16 +0025: LOADI R69, 2 # 6:27 +0026: LOADA R68, R66, R69 # 6:25 +0027: ADDI R64, R67, R68 # 6:23 +0028: RETURN # 7:1 +``` + +## Output + +```plain +0=60% +``` + +# Test: Array used as scalar in expression + +## Source + +```basic +DIM a(3) AS INTEGER +OUT a +``` + +## Compilation errors + +```plain +2:5: a is an array and requires subscripts +``` + +# Test: Array used as scalar in assignment + +## Source + +```basic +DIM a(3) AS INTEGER +a = 5 +``` + +## Compilation errors + +```plain +2:1: a is an array and requires subscripts +``` + +# Test: Too many dimensions exceeding bytecode packing + +## Source + +```basic +DIM good(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15) +DIM bad(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16) +``` + +## Compilation errors + +```plain +2:5: Array cannot have 16 dimensions +``` + +# Test: Too many dimensions exceeding potential u8 integer + +## Source + +```basic +DIM bad(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260) +``` + +## Compilation errors + +```plain +1:5: Out of temp registers +``` From 6a2df0088d85b28cce0387a910e9d623ab7a4541 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Wed, 18 Feb 2026 14:45:36 -0800 Subject: [PATCH 10/53] core2: Unify variables and arrays under SymbolPrototype The previous commit added two separate HashMaps alongside the existing HashMapWithIds fields to track arrays, creating a dual-namespace kludge. This was done for efficiency of implementation, but now that we have integration tests in place, we can do better. This change introduces SymbolPrototype, an enum with Scalar(ExprType) and Array(ArrayInfo) variants, replacing the four separate maps with one unified field per scope, and exposes it to callers, thus eliminating the various _array methods. --- core2/src/compiler/args.rs | 16 +- core2/src/compiler/exprs.rs | 46 +++--- core2/src/compiler/syms.rs | 317 ++++++++++++++++++------------------ core2/src/compiler/top.rs | 46 ++++-- core2/tests/test_arrays.md | 15 ++ 5 files changed, 231 insertions(+), 209 deletions(-) diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index c8850d2e..f3e0853e 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -20,7 +20,7 @@ use crate::bytecode::{self, Register}; use crate::callable::{CallableMetadata, CallableSyntax}; use crate::compiler::codegen::Codegen; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; -use crate::compiler::syms::{self, TempSymtable}; +use crate::compiler::syms::{self, SymbolPrototype, TempSymtable}; use crate::compiler::{Error, Result}; use crate::reader::LineCol; use crate::{ArgSep, ArgSepSyntax, ExprType, RepeatedTypeSyntax, SingularArgSyntax}; @@ -151,7 +151,9 @@ fn define_new_arg( ) -> Result<()> { let key = SymbolKey::from(&vref.name); let vtype = vref.ref_type.unwrap_or(ExprType::Integer); - let reg = symtable.put_local(key, vtype).map_err(|e| Error::from_syms(e, pos))?; + let reg = symtable + .put_local(key, SymbolPrototype::Scalar(vtype)) + .map_err(|e| Error::from_syms(e, pos))?; codegen.emit_default(reg, vtype, pos); Ok(()) } @@ -271,7 +273,10 @@ pub(super) fn compile_args( None => return Err(Error::CallableSyntax(key_pos, md)), Some(Expr::Symbol(span)) => { let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { - Ok((reg, vtype)) => (reg, vtype), + Ok((reg, SymbolPrototype::Scalar(vtype))) => (reg, vtype), + Ok((_, SymbolPrototype::Array(_))) => { + return Err(Error::CallableSyntax(span.pos, md)); + } Err(e @ syms::Error::UndefinedSymbol(..)) => { if !details.define_undefined { return Err(Error::from_syms(e, span.pos)); @@ -393,7 +398,10 @@ pub(super) fn compile_args( }; let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { - Ok((reg, vtype)) => (reg, vtype), + Ok((reg, SymbolPrototype::Scalar(vtype))) => (reg, vtype), + Ok((_, SymbolPrototype::Array(_))) => { + return Err(Error::CallableSyntax(arg_pos, md)); + } Err(syms::Error::UndefinedSymbol(..)) => { unreachable!("Caller must use define_new_args first for commands"); } diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index dd3741b3..c52ad29f 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -19,24 +19,11 @@ use crate::ast::{BinaryOpSpan, Expr, ExprType}; use crate::bytecode::{self, Register, RegisterScope}; use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; -use crate::compiler::syms::{self, SymbolKey, TempScope, TempSymtable}; +use crate::compiler::syms::{self, SymbolKey, SymbolPrototype, TempScope, TempSymtable}; use crate::compiler::{Error, Result}; use crate::mem::Datum; use std::convert::TryFrom; -/// Checks that a variable reference does not refer to an array being used without subscripts. -fn check_not_array( - symtable: &TempSymtable<'_, '_, '_, '_, '_>, - vref: &crate::ast::VarRef, - pos: crate::reader::LineCol, -) -> Result<()> { - let key = SymbolKey::from(&vref.name); - if symtable.is_array(&key) { - return Err(Error::ArrayUsedAsScalar(pos, vref.clone())); - } - Ok(()) -} - /// Compiles `exprs` into consecutive integer registers allocated from `scope` and returns the /// first register. The caller must guarantee that `exprs` is non-empty. pub(super) fn compile_integer_exprs( @@ -163,17 +150,21 @@ pub(super) fn compile_expr( let key_pos = span.vref_pos; let Some(md) = symtable.get_callable(&key) else { - if let Some((arr_reg, info)) = symtable.get_array(&key) { - return compile_array_access( - codegen, symtable, reg, key_pos, arr_reg, &info, span.args, - ); + match symtable.get_local_or_global(&span.vref) { + Ok((arr_reg, SymbolPrototype::Array(info))) => { + return compile_array_access( + codegen, symtable, reg, key_pos, arr_reg, &info, span.args, + ); + } + Err(syms::Error::UndefinedSymbol(..)) | Ok((_, SymbolPrototype::Scalar(_))) => { + return Err(Error::UndefinedSymbol( + span.vref_pos, + span.vref, + RegisterScope::Global, + )); + } + Err(e) => return Err(Error::from_syms(e, key_pos)), } - - return Err(Error::UndefinedSymbol( - span.vref_pos, - span.vref, - RegisterScope::Global, - )); }; let Some(etype) = md.return_type() else { @@ -249,12 +240,15 @@ pub(super) fn compile_expr( } Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { - Ok((local, etype)) => { - check_not_array(symtable, &span.vref, span.pos)?; + Ok((local, SymbolPrototype::Scalar(etype))) => { codegen.emit(bytecode::make_move(reg, local), span.pos); Ok(etype) } + Ok((_, SymbolPrototype::Array(_))) => { + Err(Error::ArrayUsedAsScalar(span.pos, span.vref)) + } + Err(syms::Error::UndefinedSymbol(..)) => { let key = SymbolKey::from(&span.vref.name); diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 6955e883..291272cd 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -27,7 +27,7 @@ use std::fmt; use std::rc::Rc; /// Information about an array tracked in the symbol table. -#[derive(Clone, Debug)] +#[derive(Clone, Copy, Debug, PartialEq)] pub(super) struct ArrayInfo { /// Element type of the array. pub(super) subtype: ExprType, @@ -36,6 +36,16 @@ pub(super) struct ArrayInfo { pub(super) ndims: usize, } +/// Prototype for a variable-like symbol (scalar or array). +#[derive(Clone, Copy, Debug, PartialEq)] +pub(super) enum SymbolPrototype { + /// An array with the given element type and number of dimensions. + Array(ArrayInfo), + + /// A scalar variable of the given type. + Scalar(ExprType), +} + /// Errors related to symbols handling. #[derive(Debug, thiserror::Error)] #[allow(missing_docs)] // The error messages and names are good enough. @@ -74,49 +84,58 @@ impl fmt::Display for SymbolKey { } } -/// Gets the register and type of a local or global variable if it already exists. +/// Gets the register and prototype of a local or global variable if it already exists. fn get_var( vref: &VarRef, - table: &HashMapWithIds, + table: &HashMapWithIds, make_register: MKR, scope: RegisterScope, -) -> Result<(Register, ExprType)> +) -> Result<(Register, SymbolPrototype)> where MKR: FnOnce(u8) -> std::result::Result, { let key = SymbolKey::from(&vref.name); match table.get(&key) { - Some((etype, reg)) => { + Some((SymbolPrototype::Array(info), reg)) => { + if !vref.accepts(info.subtype) { + return Err(Error::IncompatibleTypeAnnotationInReference(vref.clone())); + } + + let reg = make_register(reg).map_err(|_| Error::OutOfRegisters(scope))?; + Ok((reg, SymbolPrototype::Array(*info))) + } + + Some((SymbolPrototype::Scalar(etype), reg)) => { if !vref.accepts(*etype) { return Err(Error::IncompatibleTypeAnnotationInReference(vref.clone())); } let reg = make_register(reg).map_err(|_| Error::OutOfRegisters(scope))?; - Ok((reg, *etype)) + Ok((reg, SymbolPrototype::Scalar(*etype))) } None => Err(Error::UndefinedSymbol(vref.clone(), scope)), } } -/// Defines a new local or global variable and assigns a register to it. +/// Defines a new local or global variable (or array) and assigns a register to it. /// -/// Panics if the variable already exists. +/// Panics if the symbol already exists. fn put_var( key: SymbolKey, - vtype: ExprType, - table: &mut HashMapWithIds, + proto: SymbolPrototype, + table: &mut HashMapWithIds, make_register: MKR, scope: RegisterScope, ) -> Result where MKR: FnOnce(u8) -> std::result::Result, { - match table.insert(key, vtype) { + match table.insert(key, proto) { Some((None, reg)) => Ok(make_register(reg).map_err(|_| Error::OutOfRegisters(scope))?), - Some((Some(_old_etype), _reg)) => { - unreachable!("Cannot redefine variable; caller must check for presence first"); + Some((Some(_old_proto), _reg)) => { + unreachable!("Cannot redefine symbol; caller must check for presence first"); } None => Err(Error::OutOfRegisters(scope)), @@ -127,11 +146,8 @@ where /// /// Globals are variables and callables that are visible from any scope. pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { - /// Map of global variable names to their types and assigned registers. - globals: HashMapWithIds, - - /// Map of global array names to their array info. - global_arrays: HashMap, + /// Map of global variable names to their prototypes and assigned registers. + globals: HashMapWithIds, /// Reference to the built-in callable metadata provided by the runtime. upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>, @@ -143,12 +159,7 @@ pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { /// Creates a new global symbol table that knows about the given `upcalls`. pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>) -> Self { - Self { - globals: HashMapWithIds::default(), - global_arrays: HashMap::default(), - upcalls, - user_callables: HashMap::default(), - } + Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } } /// Enters a new local scope. @@ -156,38 +167,18 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { LocalSymtable::new(self) } - /// Gets a global variable by its `vref`. - pub(crate) fn get_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + /// Gets a global symbol by its `vref`, returning its register and prototype. + pub(crate) fn get_global(&self, vref: &VarRef) -> Result<(Register, SymbolPrototype)> { get_var(vref, &self.globals, Register::global, RegisterScope::Global) } - /// Creates a new global variable `key` of `vtype`. - pub(crate) fn put_global(&mut self, key: SymbolKey, vtype: ExprType) -> Result { - put_var(key, vtype, &mut self.globals, Register::global, RegisterScope::Global) - } - - /// Creates a new global array `key` with `info` and assigns a register to it. - pub(crate) fn put_global_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { - let reg = put_var( - key.clone(), - info.subtype, - &mut self.globals, - Register::global, - RegisterScope::Global, - )?; - self.global_arrays.insert(key, info); - Ok(reg) - } - - /// Gets a global array by its `key`. - fn get_global_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { - if let Some(info) = self.global_arrays.get(key) - && let Some((_etype, reg)) = self.globals.get(key) - { - let reg = Register::global(reg).expect("Must be valid"); - return Some((reg, info.clone())); - } - None + /// Creates a new global symbol `key` with `proto`. + pub(crate) fn put_global( + &mut self, + key: SymbolKey, + proto: SymbolPrototype, + ) -> Result { + put_var(key, proto, &mut self.globals, Register::global, RegisterScope::Global) } /// Defines a new user-defined `vref` callable with `md` metadata. @@ -215,11 +206,8 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// Reference to the parent global symbol table. symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>, - /// Map of local variable names to their types and assigned registers. - locals: HashMapWithIds, - - /// Map of local array names to their array info. - local_arrays: HashMap, + /// Map of local variable names to their prototypes and assigned registers. + locals: HashMapWithIds, /// Maximum number of allocated temporary registers in all possible evaluation scopes created /// by this local symtable. This is used to determine the size of the scope for register @@ -230,12 +218,7 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// Creates a new local symbol table within the context of a global `symtable`. fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>) -> Self { - Self { - symtable, - locals: HashMapWithIds::default(), - local_arrays: HashMap::default(), - count_temps: 0, - } + Self { symtable, locals: HashMapWithIds::default(), count_temps: 0 } } /// Consumes the local scope and returns the number of local variables defined, which includes @@ -261,13 +244,17 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { TempSymtable::new(self) } - /// Creates a new global variable `key` of `vtype`. - pub(crate) fn put_global(&mut self, key: SymbolKey, vtype: ExprType) -> Result { - self.symtable.put_global(key, vtype) + /// Creates a new global symbol `key` with `proto` via the parent global symbol table. + pub(crate) fn put_global( + &mut self, + key: SymbolKey, + proto: SymbolPrototype, + ) -> Result { + self.symtable.put_global(key, proto) } - /// Gets a variable by its `vref`, looking for it in the local and global scopes. - pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + /// Gets a symbol by its `vref`, looking for it in the local and global scopes. + pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, SymbolPrototype)> { match get_var(vref, &self.locals, Register::local, RegisterScope::Local) { Ok(local) => Ok(local), Err(Error::UndefinedSymbol(..)) => self.symtable.get_global(vref), @@ -280,9 +267,9 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { self.symtable.get_callable(key) } - /// Creates a new local variable `key` of `vtype`. - pub(crate) fn put_local(&mut self, key: SymbolKey, vtype: ExprType) -> Result { - put_var(key, vtype, &mut self.locals, Register::local, RegisterScope::Local) + /// Creates a new local symbol `key` with `proto`. + pub(crate) fn put_local(&mut self, key: SymbolKey, proto: SymbolPrototype) -> Result { + put_var(key, proto, &mut self.locals, Register::local, RegisterScope::Local) } /// Changes the type of an existing local variable `vref` to `new_etype`. @@ -292,47 +279,16 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { let key = SymbolKey::from(&vref.name); // TODO: Verify reference type. match self.locals.get_mut(&key) { - Some((etype, _reg)) => { + Some((SymbolPrototype::Array(_), _)) | None => { + Err(Error::UndefinedSymbol(vref.clone(), RegisterScope::Local)) + } + + Some((SymbolPrototype::Scalar(etype), _reg)) => { *etype = new_etype; Ok(()) } - None => Err(Error::UndefinedSymbol(vref.clone(), RegisterScope::Local)), } } - - /// Creates a new local array `key` with `info` and assigns a register to it. - pub(crate) fn put_local_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { - let reg = put_var( - key.clone(), - info.subtype, - &mut self.locals, - Register::local, - RegisterScope::Local, - )?; - self.local_arrays.insert(key, info); - Ok(reg) - } - - /// Creates a new global array `key` with `info` via the parent global symbol table. - pub(crate) fn put_global_array(&mut self, key: SymbolKey, info: ArrayInfo) -> Result { - self.symtable.put_global_array(key, info) - } - - /// Gets an array by its `key`, looking in local arrays first, then global. - pub(crate) fn get_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { - if let Some(info) = self.local_arrays.get(key) - && let Some((_etype, reg)) = self.locals.get(key) - { - let reg = Register::local(reg).expect("Must be valid"); - return Some((reg, info.clone())); - } - self.symtable.get_global_array(key) - } - - /// Returns `true` if `key` refers to an array (local or global). - pub(crate) fn is_array(&self, key: &SymbolKey) -> bool { - self.local_arrays.contains_key(key) || self.symtable.global_arrays.contains_key(key) - } } /// A read-only view into a `SymTable` that allows allocating temporary registers. @@ -370,8 +326,8 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, } } - /// Gets a variable by its `vref`, looking for it in the local and global scopes. - pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, ExprType)> { + /// Gets a symbol by its `vref`, looking for it in the local and global scopes. + pub(crate) fn get_local_or_global(&self, vref: &VarRef) -> Result<(Register, SymbolPrototype)> { self.symtable.get_local_or_global(vref) } @@ -380,16 +336,6 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, self.symtable.get_callable(key) } - /// Gets an array by its `key`, looking in local arrays first, then global. - pub(crate) fn get_array(&self, key: &SymbolKey) -> Option<(Register, ArrayInfo)> { - self.symtable.get_array(key) - } - - /// Returns `true` if `key` refers to an array (local or global). - pub(crate) fn is_array(&self, key: &SymbolKey) -> bool { - self.symtable.is_array(key) - } - /// Enters a new temporary scope. pub(crate) fn temp_scope(&self) -> TempScope { let nlocals = u8::try_from(self.symtable.locals.len()) @@ -484,21 +430,23 @@ mod tests { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - let reg = global.put_global(SymbolKey::from("x"), ExprType::Integer)?; + let reg = + global.put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; assert_eq!(Register::global(0).unwrap(), reg); - let reg = global.put_global(SymbolKey::from("y"), ExprType::Text)?; + let reg = + global.put_global(SymbolKey::from("y"), SymbolPrototype::Scalar(ExprType::Text))?; assert_eq!(Register::global(1).unwrap(), reg); // Lookup with untyped ref succeeds. - let (reg, etype) = global.get_global(&VarRef::new("x", None))?; + let (reg, proto) = global.get_global(&VarRef::new("x", None))?; assert_eq!(Register::global(0).unwrap(), reg); - assert_eq!(ExprType::Integer, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Integer), proto); // Lookup with matching typed ref succeeds. - let (reg, etype) = global.get_global(&VarRef::new("y", Some(ExprType::Text)))?; + let (reg, proto) = global.get_global(&VarRef::new("y", Some(ExprType::Text)))?; assert_eq!(Register::global(1).unwrap(), reg); - assert_eq!(ExprType::Text, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Text), proto); Ok(()) } @@ -507,11 +455,11 @@ mod tests { fn test_global_get_case_insensitive() -> Result<()> { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - global.put_global(SymbolKey::from("MyVar"), ExprType::Double)?; + global.put_global(SymbolKey::from("MyVar"), SymbolPrototype::Scalar(ExprType::Double))?; - let (reg, etype) = global.get_global(&VarRef::new("myvar", None))?; + let (reg, proto) = global.get_global(&VarRef::new("myvar", None))?; assert_eq!(Register::global(0).unwrap(), reg); - assert_eq!(ExprType::Double, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Double), proto); let (reg2, _) = global.get_global(&VarRef::new("MYVAR", None))?; assert_eq!(reg, reg2); @@ -522,7 +470,9 @@ mod tests { fn test_global_get_incompatible_type() { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - global.put_global(SymbolKey::from("x"), ExprType::Integer).unwrap(); + global + .put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer)) + .unwrap(); let err = global.get_global(&VarRef::new("x", Some(ExprType::Text))).unwrap_err(); assert_eq!("Incompatible type annotation in x$ reference", err.to_string()); @@ -543,15 +493,17 @@ mod tests { let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - let reg = local.put_local(SymbolKey::from("a"), ExprType::Boolean)?; + let reg = + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Boolean))?; assert_eq!(Register::local(0).unwrap(), reg); - let reg = local.put_local(SymbolKey::from("b"), ExprType::Double)?; + let reg = + local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Double))?; assert_eq!(Register::local(1).unwrap(), reg); - let (reg, etype) = local.get_local_or_global(&VarRef::new("a", None))?; + let (reg, proto) = local.get_local_or_global(&VarRef::new("a", None))?; assert_eq!(Register::local(0).unwrap(), reg); - assert_eq!(ExprType::Boolean, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Boolean), proto); Ok(()) } @@ -560,14 +512,14 @@ mod tests { fn test_local_shadows_global() -> Result<()> { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - global.put_global(SymbolKey::from("x"), ExprType::Integer)?; + global.put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("x"), ExprType::Text)?; + local.put_local(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Text))?; - let (reg, etype) = local.get_local_or_global(&VarRef::new("x", None))?; + let (reg, proto) = local.get_local_or_global(&VarRef::new("x", None))?; assert_eq!(Register::local(0).unwrap(), reg); - assert_eq!(ExprType::Text, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Text), proto); Ok(()) } @@ -576,12 +528,12 @@ mod tests { fn test_local_falls_through_to_global() -> Result<()> { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - global.put_global(SymbolKey::from("g"), ExprType::Integer)?; + global.put_global(SymbolKey::from("g"), SymbolPrototype::Scalar(ExprType::Integer))?; let local = global.enter_scope(); - let (reg, etype) = local.get_local_or_global(&VarRef::new("g", None))?; + let (reg, proto) = local.get_local_or_global(&VarRef::new("g", None))?; assert_eq!(Register::global(0).unwrap(), reg); - assert_eq!(ExprType::Integer, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Integer), proto); Ok(()) } @@ -602,13 +554,14 @@ mod tests { let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - let reg = local.put_global(SymbolKey::from("g"), ExprType::Integer)?; + let reg = + local.put_global(SymbolKey::from("g"), SymbolPrototype::Scalar(ExprType::Integer))?; assert_eq!(Register::global(0).unwrap(), reg); // Should be visible from the local scope via fallthrough. - let (reg, etype) = local.get_local_or_global(&VarRef::new("g", None))?; + let (reg, proto) = local.get_local_or_global(&VarRef::new("g", None))?; assert_eq!(Register::global(0).unwrap(), reg); - assert_eq!(ExprType::Integer, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Integer), proto); Ok(()) } @@ -619,11 +572,11 @@ mod tests { let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("x"), ExprType::Integer)?; + local.put_local(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; local.fixup_local_type(&VarRef::new("x", None), ExprType::Double)?; - let (_, etype) = local.get_local_or_global(&VarRef::new("x", None))?; - assert_eq!(ExprType::Double, etype); + let (_, proto) = local.get_local_or_global(&VarRef::new("x", None))?; + assert_eq!(SymbolPrototype::Scalar(ExprType::Double), proto); Ok(()) } @@ -644,8 +597,8 @@ mod tests { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("a"), ExprType::Integer)?; - local.put_local(SymbolKey::from("b"), ExprType::Integer)?; + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; + local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; assert_eq!(2, local.leave_scope()?); Ok(()) } @@ -655,7 +608,7 @@ mod tests { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("a"), ExprType::Integer)?; + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; { let temp = local.frozen(); let mut scope = temp.temp_scope(); @@ -764,8 +717,8 @@ mod tests { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("a"), ExprType::Integer)?; - local.put_local(SymbolKey::from("b"), ExprType::Integer)?; + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; + local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; { let temp = local.frozen(); let mut scope = temp.temp_scope(); @@ -794,7 +747,7 @@ mod tests { let mut local = global.enter_scope(); assert_eq!( Register::local(0).unwrap(), - local.put_local(SymbolKey::from("foo"), ExprType::Integer)? + local.put_local(SymbolKey::from("foo"), SymbolPrototype::Scalar(ExprType::Integer))? ); { let temp = local.frozen(); @@ -830,20 +783,20 @@ mod tests { fn test_temp_scope_lookup_vars() -> Result<()> { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(&upcalls); - global.put_global(SymbolKey::from("g"), ExprType::Integer)?; + global.put_global(SymbolKey::from("g"), SymbolPrototype::Scalar(ExprType::Integer))?; let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("l"), ExprType::Text)?; + local.put_local(SymbolKey::from("l"), SymbolPrototype::Scalar(ExprType::Text))?; { let temp = local.frozen(); - let (reg, etype) = temp.get_local_or_global(&VarRef::new("l", None))?; + let (reg, proto) = temp.get_local_or_global(&VarRef::new("l", None))?; assert_eq!(Register::local(0).unwrap(), reg); - assert_eq!(ExprType::Text, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Text), proto); - let (reg, etype) = temp.get_local_or_global(&VarRef::new("g", None))?; + let (reg, proto) = temp.get_local_or_global(&VarRef::new("g", None))?; assert_eq!(Register::global(0).unwrap(), reg); - assert_eq!(ExprType::Integer, etype); + assert_eq!(SymbolPrototype::Scalar(ExprType::Integer), proto); } Ok(()) @@ -873,7 +826,7 @@ mod tests { { let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("x"), ExprType::Integer)?; + local.put_local(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; assert_eq!(1, local.leave_scope()?); } @@ -883,11 +836,53 @@ mod tests { let err = local.get_local_or_global(&VarRef::new("x", None)).unwrap_err(); assert_eq!("Undefined global symbol x", err.to_string()); - let reg = local.put_local(SymbolKey::from("y"), ExprType::Double)?; + let reg = + local.put_local(SymbolKey::from("y"), SymbolPrototype::Scalar(ExprType::Double))?; assert_eq!(Register::local(0).unwrap(), reg); assert_eq!(1, local.leave_scope()?); } Ok(()) } + + #[test] + fn test_global_put_and_get_array() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + + let reg = global.put_global( + SymbolKey::from("arr"), + SymbolPrototype::Array(ArrayInfo { subtype: ExprType::Integer, ndims: 2 }), + )?; + assert_eq!(Register::global(0).unwrap(), reg); + + let (got_reg, proto) = global.get_global(&VarRef::new("arr", None)).unwrap(); + assert_eq!(Register::global(0).unwrap(), got_reg); + let SymbolPrototype::Array(info) = proto else { panic!("Expected Array prototype") }; + assert_eq!(ExprType::Integer, info.subtype); + assert_eq!(2, info.ndims); + + Ok(()) + } + + #[test] + fn test_local_put_and_get_array() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let reg = local.put_local( + SymbolKey::from("arr"), + SymbolPrototype::Array(ArrayInfo { subtype: ExprType::Double, ndims: 1 }), + )?; + assert_eq!(Register::local(0).unwrap(), reg); + + let (got_reg, proto) = local.get_local_or_global(&VarRef::new("arr", None)).unwrap(); + assert_eq!(Register::local(0).unwrap(), got_reg); + let SymbolPrototype::Array(info) = proto else { panic!("Expected Array prototype") }; + assert_eq!(ExprType::Double, info.subtype); + assert_eq!(1, info.ndims); + + Ok(()) + } } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 3d28e6c1..1e4d2c54 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -21,7 +21,7 @@ use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, Singu use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer_exprs}; -use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey}; +use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype}; use crate::compiler::{Error, Result}; use crate::image::Image; use crate::mem::Datum; @@ -55,18 +55,19 @@ fn compile_assignment( let vref_pos = span.vref_pos; let (reg, etype) = match symtable.get_local_or_global(&span.vref) { - Ok((reg, etype)) => { - let key = SymbolKey::from(&span.vref.name); - if symtable.is_array(&key) { - return Err(Error::ArrayUsedAsScalar(vref_pos, span.vref)); - } - (reg, Some(etype)) + Ok((_, SymbolPrototype::Array(_))) => { + return Err(Error::ArrayUsedAsScalar(vref_pos, span.vref)); } + Ok((reg, SymbolPrototype::Scalar(etype))) => (reg, Some(etype)), + Err(syms::Error::UndefinedSymbol(..)) => { let key = SymbolKey::from(span.vref.name.clone()); let reg = symtable - .put_local(key, span.vref.ref_type.unwrap_or(ExprType::Integer)) + .put_local( + key, + SymbolPrototype::Scalar(span.vref.ref_type.unwrap_or(ExprType::Integer)), + ) .map_err(|e| Error::from_syms(e, span.vref_pos))?; match span.vref.ref_type { Some(etype) => (reg, Some(etype)), @@ -103,11 +104,16 @@ fn compile_stmt( ) -> Result<()> { match stmt { Statement::ArrayAssignment(span) => { - let key = SymbolKey::from(&span.vref.name); let key_pos = span.vref_pos; - let Some((arr_reg, info)) = symtable.get_array(&key) else { - return Err(Error::NotAnArray(key_pos, span.vref)); + let (arr_reg, info) = match symtable.get_local_or_global(&span.vref) { + Ok((reg, SymbolPrototype::Array(info))) => (reg, info), + + Ok((_, SymbolPrototype::Scalar(_))) | Err(syms::Error::UndefinedSymbol(..)) => { + return Err(Error::NotAnArray(key_pos, span.vref)); + } + + Err(e) => return Err(Error::from_syms(e, key_pos)), }; if span.subscripts.len() != info.ndims { @@ -206,9 +212,9 @@ fn compile_stmt( let key = SymbolKey::from(span.name); let name_pos = span.name_pos; let reg = if span.shared { - symtable.put_global(key, span.vtype) + symtable.put_global(key, SymbolPrototype::Scalar(span.vtype)) } else { - symtable.put_local(key, span.vtype) + symtable.put_local(key, SymbolPrototype::Scalar(span.vtype)) } .map_err(|e| Error::from_syms(e, name_pos))?; ctx.codegen.emit_default(reg, span.vtype, name_pos); @@ -221,9 +227,9 @@ fn compile_stmt( let info = syms::ArrayInfo { subtype: span.subtype, ndims }; let reg = if span.shared { - symtable.put_global_array(key, info) + symtable.put_global(key, SymbolPrototype::Array(info)) } else { - symtable.put_local_array(key, info) + symtable.put_local(key, SymbolPrototype::Array(info)) } .map_err(|e| Error::from_syms(e, name_pos))?; @@ -321,8 +327,9 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R // The call protocol expects the return value to be in the first local variable // so allocate it early, and then all arguments follow in order from left to right. if let Some(vtype) = callable.name.ref_type { - let ret_reg = - symtable.put_local(key.clone(), vtype).map_err(|e| Error::from_syms(e, key_pos))?; + let ret_reg = symtable + .put_local(key.clone(), SymbolPrototype::Scalar(vtype)) + .map_err(|e| Error::from_syms(e, key_pos))?; // Set the default value of the function result. We could instead try to do this // at runtime by clearning the return register... but the problem is that we need @@ -341,7 +348,10 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R for param in callable.params { let key = SymbolKey::from(param.name); symtable - .put_local(key.clone(), param.ref_type.unwrap_or(ExprType::Integer)) + .put_local( + key.clone(), + SymbolPrototype::Scalar(param.ref_type.unwrap_or(ExprType::Integer)), + ) .map_err(|e| Error::from_syms(e, key_pos))?; } diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index a3614776..d9734d3f 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -311,6 +311,21 @@ OUT a%(1) 0=0% ``` +# Test: Array reference with bad annotation + +## Source + +```basic +DIM a(3) AS INTEGER +OUT a#(1) +``` + +## Compilation errors + +```plain +2:5: Incompatible type annotation in a# reference +``` + # Test: Array reference index is double and needs demotion ## Source From 23fe8a836758ee3234b5c75e4751930c7e850ef9 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 17 Feb 2026 14:32:29 -0800 Subject: [PATCH 11/53] core2: Enforce single namespace for all symbols This change adds various integration tests to make sure there is a single namespace for all variables, arrays, and callables (no matter if they are user-defined or native). Consequently, this highlights various bugs, so fix those now too. The previous compiler implementation guaranteed this via the SymbolPrototype but the new implementation cannot do exactly the same due to the fact that not all of these symbol types require register allocations. --- core2/src/compiler/syms.rs | 18 +++++++ core2/src/compiler/top.rs | 51 +++++++++++++++++-- core2/tests/integration_test.rs | 1 + core2/tests/test_arrays.md | 90 +++++++++++++++++++++++++++++++++ core2/tests/test_assignments.md | 46 +++++++++++++++++ core2/tests/test_functions.md | 32 ++++++++++++ core2/tests/test_globals.md | 15 ++++++ core2/tests/test_locals.md | 59 +++++++++++++++++++++ core2/tests/test_subs.md | 32 ++++++++++++ 9 files changed, 341 insertions(+), 3 deletions(-) create mode 100644 core2/tests/test_locals.md diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 291272cd..cf756c4d 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -181,6 +181,11 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { put_var(key, proto, &mut self.globals, Register::global, RegisterScope::Global) } + /// Returns true if a global variable `key` is already defined. + pub(crate) fn contains_global(&self, key: &SymbolKey) -> bool { + self.globals.get(key).is_some() + } + /// Defines a new user-defined `vref` callable with `md` metadata. pub(crate) fn define_user_callable( &mut self, @@ -188,6 +193,9 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { md: CallableMetadata, ) -> Result<()> { let key = SymbolKey::from(&vref.name); + if self.globals.get(&key).is_some() { + return Err(Error::AlreadyDefined(vref.clone())); + } let previous = self.user_callables.insert(key, md); if previous.is_none() { Ok(()) } else { Err(Error::AlreadyDefined(vref.clone())) } } @@ -272,6 +280,16 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { put_var(key, proto, &mut self.locals, Register::local, RegisterScope::Local) } + /// Returns true if a local variable `key` is already defined. + pub(crate) fn contains_local(&self, key: &SymbolKey) -> bool { + self.locals.get(key).is_some() + } + + /// Returns true if a global variable `key` is already defined. + pub(crate) fn contains_global(&self, key: &SymbolKey) -> bool { + self.symtable.contains_global(key) + } + /// Changes the type of an existing local variable `vref` to `new_etype`. /// /// This is used for type inference on first assignment. diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 1e4d2c54..5e4d7e25 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -15,7 +15,7 @@ //! Entry point to the compilation, handling top-level definitions. -use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement}; +use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement, VarRef}; use crate::bytecode::{self, PackedArrayType, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; @@ -63,6 +63,12 @@ fn compile_assignment( Err(syms::Error::UndefinedSymbol(..)) => { let key = SymbolKey::from(span.vref.name.clone()); + if symtable.get_callable(&key).is_some() { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(span.vref.clone()), + span.vref_pos, + )); + } let reg = symtable .put_local( key, @@ -209,11 +215,31 @@ fn compile_stmt( } Statement::Dim(span) => { - let key = SymbolKey::from(span.name); let name_pos = span.name_pos; + let key = SymbolKey::from(&span.name); + + if symtable.get_callable(&key).is_some() { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } + let reg = if span.shared { + if symtable.contains_global(&key) { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } symtable.put_global(key, SymbolPrototype::Scalar(span.vtype)) } else { + if symtable.contains_local(&key) { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } symtable.put_local(key, SymbolPrototype::Scalar(span.vtype)) } .map_err(|e| Error::from_syms(e, name_pos))?; @@ -221,14 +247,33 @@ fn compile_stmt( } Statement::DimArray(span) => { - let key = SymbolKey::from(span.name); let name_pos = span.name_pos; + let key = SymbolKey::from(&span.name); let ndims = span.dimensions.len(); + if symtable.get_callable(&key).is_some() { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } + let info = syms::ArrayInfo { subtype: span.subtype, ndims }; let reg = if span.shared { + if symtable.contains_global(&key) { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } symtable.put_global(key, SymbolPrototype::Array(info)) } else { + if symtable.contains_local(&key) { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(VarRef::new(&span.name, None)), + name_pos, + )); + } symtable.put_local(key, SymbolPrototype::Array(info)) } .map_err(|e| Error::from_syms(e, name_pos))?; diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 44ecfee3..d4f1c57c 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -42,6 +42,7 @@ one_test!(test_functions); one_test!(test_globals); one_test!(test_gosub); one_test!(test_goto); +one_test!(test_locals); one_test!(test_out_of_registers); one_test!(test_strings); one_test!(test_subs); diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index d9734d3f..5d9e8dce 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -832,3 +832,93 @@ DIM bad(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 2 ```plain 1:5: Out of temp registers ``` + +# Test: DIM of name of a built-in callable + +## Source + +```basic +DIM out(3) AS INTEGER +``` + +## Compilation errors + +```plain +1:5: Cannot redefine out +``` + +# Test: DIM of name of a user-defined sub + +## Source + +```basic +SUB foo +END SUB +DIM foo(3) AS INTEGER +``` + +## Compilation errors + +```plain +3:5: Cannot redefine foo +``` + +# Test: Redefine existing array with DIM + +## Source + +```basic +DIM a(3) AS INTEGER +DIM a(3) AS INTEGER +``` + +## Compilation errors + +```plain +2:5: Cannot redefine a +``` + +# Test: Redefine existing variable as array with DIM + +## Source + +```basic +a = 5 +DIM a(3) AS INTEGER +``` + +## Compilation errors + +```plain +2:5: Cannot redefine a +``` + +# Test: Redefine existing shared array with DIM SHARED + +## Source + +```basic +DIM SHARED a(3) AS INTEGER +DIM SHARED a(3) AS INTEGER +``` + +## Compilation errors + +```plain +2:12: Cannot redefine a +``` + +# Test: Redefine existing array as scalar with DIM + +## Source + +```basic +DIM a(3) AS INTEGER +DIM a AS INTEGER +``` + +## Compilation errors + +```plain +2:5: Cannot redefine a +``` diff --git a/core2/tests/test_assignments.md b/core2/tests/test_assignments.md index 8d062a97..c8c99d69 100644 --- a/core2/tests/test_assignments.md +++ b/core2/tests/test_assignments.md @@ -208,3 +208,49 @@ OUT i1, i2 ```plain 0=3% , 1=4% ``` + +# Test: Assignment to name of a built-in callable + +## Source + +```basic +out = 5 +``` + +## Compilation errors + +```plain +1:1: Cannot redefine out +``` + +# Test: Assignment to name of a user-defined sub + +## Source + +```basic +SUB foo +END SUB +foo = 5 +``` + +## Compilation errors + +```plain +3:1: Cannot redefine foo +``` + +# Test: Assignment to name of a user-defined function + +## Source + +```basic +FUNCTION bar% +END FUNCTION +bar = 5 +``` + +## Compilation errors + +```plain +3:1: Cannot redefine bar +``` diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 3a3b3f42..bbd08654 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -788,3 +788,35 @@ x = SUM_DOUBLES ```plain 1:5: SUM_DOUBLES expected [arg1, .., argN] ``` + +# Test: Function name conflicts with existing global variable + +## Source + +```basic +DIM SHARED g AS INTEGER +FUNCTION g% +END FUNCTION +``` + +## Compilation errors + +```plain +2:10: Cannot redefine g% +``` + +# Test: Function name conflicts with existing global array + +## Source + +```basic +DIM SHARED g(3) AS INTEGER +FUNCTION g% +END FUNCTION +``` + +## Compilation errors + +```plain +2:10: Cannot redefine g% +``` diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index 3bbc0871..3a24ba77 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -293,3 +293,18 @@ d = "foo" ```plain 2:5: STRING is not a number ``` + +# Test: Redefine existing shared variable with DIM SHARED + +## Source + +```basic +DIM SHARED x AS INTEGER +DIM SHARED x AS INTEGER +``` + +## Compilation errors + +```plain +2:12: Cannot redefine x +``` diff --git a/core2/tests/test_locals.md b/core2/tests/test_locals.md new file mode 100644 index 00000000..4eab94a1 --- /dev/null +++ b/core2/tests/test_locals.md @@ -0,0 +1,59 @@ +# Test: DIM variable with name of a built-in callable + +## Source + +```basic +DIM out AS INTEGER +``` + +## Compilation errors + +```plain +1:5: Cannot redefine out +``` + +# Test: DIM variable with name of a user-defined sub + +## Source + +```basic +SUB foo +END SUB +DIM foo AS INTEGER +``` + +## Compilation errors + +```plain +3:5: Cannot redefine foo +``` + +# Test: Redefine existing scalar with DIM + +## Source + +```basic +DIM a AS INTEGER +DIM a AS INTEGER +``` + +## Compilation errors + +```plain +2:5: Cannot redefine a +``` + +# Test: Redefine existing variable as scalar with DIM + +## Source + +```basic +a = 5 +DIM a AS INTEGER +``` + +## Compilation errors + +```plain +2:5: Cannot redefine a +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index c128db20..595ccb73 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -235,3 +235,35 @@ x = OUT ```plain 1:5: Cannot call OUT (not a function) ``` + +# Test: Sub name conflicts with existing global variable + +## Source + +```basic +DIM SHARED g AS INTEGER +SUB g +END SUB +``` + +## Compilation errors + +```plain +2:5: Cannot redefine g +``` + +# Test: Sub name conflicts with existing global array + +## Source + +```basic +DIM SHARED g(3) AS INTEGER +SUB g +END SUB +``` + +## Compilation errors + +```plain +2:5: Cannot redefine g +``` From bf080aac6e1a3fc20b82fea7c2bcddebdfca0daa Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 26 Feb 2026 08:10:39 -0800 Subject: [PATCH 12/53] core2: Split Datum into ConstantDatum and HeapDatum The Datum enum was serving two distinct roles: a constant pool type (requiring Hash + Eq) and a heap storage type (holding runtime arrays). This caused Hash and etype() to panic on the Array variant because arrays are never constants, which was done to expedite adding support for arrays. Split Datum into two purpose-specific types so that impossible states are unrepresentable. ConstantDatum (Boolean, Double, Integer, Text) handles compile-time constants with proper Hash/Eq/PartialEq and etype(). HeapDatum (Array, Text) handles runtime heap-allocated values and carries no hashing requirement. DatumPtr::resolve is replaced by resolve_string, which extracts a string directly from either pool without returning a unified datum reference. All callers that needed text from a pointer now use this more specific API. --- core2/src/callable.rs | 24 +++++------- core2/src/compiler/codegen.rs | 6 +-- core2/src/compiler/exprs.rs | 9 +++-- core2/src/compiler/top.rs | 17 +++++---- core2/src/image.rs | 6 +-- core2/src/mem.rs | 70 ++++++++++++++++++++-------------- core2/src/vm/context.rs | 71 +++++++++++++++++++---------------- core2/src/vm/mod.rs | 4 +- 8 files changed, 112 insertions(+), 95 deletions(-) diff --git a/core2/src/callable.rs b/core2/src/callable.rs index a607a188..c59caf39 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -19,7 +19,7 @@ use crate::ast::ArgSep; use crate::ast::ExprType; use crate::bytecode::TaggedRegisterRef; use crate::bytecode::VarArgTag; -use crate::mem::{Datum, DatumPtr}; +use crate::mem::{ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u32; use async_trait::async_trait; use std::borrow::Cow; @@ -592,15 +592,12 @@ fn deref_string<'a>( regs: &[u64], index: usize, vtype: ExprType, - constants: &'a [Datum], - heap: &'a [Datum], + constants: &'a [ConstantDatum], + heap: &'a [HeapDatum], ) -> &'a str { assert_eq!(ExprType::Text, vtype); let ptr = DatumPtr::from(regs[index]); - match ptr.resolve(constants, heap) { - Datum::Text(s) => s, - _ => panic!("Mismatched constant type"), - } + ptr.resolve_string(constants, heap) } /// An immutable reference to a variable (register) in the register file, carrying @@ -700,7 +697,7 @@ impl<'a, 'vm> RegisterRefMut<'a, 'vm> { pub fn set_string>(&mut self, s: S) { assert_eq!(ExprType::Text, self.vtype); let index = self.scope.heap.len(); - self.scope.heap.push(Datum::Text(s.into())); + self.scope.heap.push(HeapDatum::Text(s.into())); self.scope.regs[self.index] = DatumPtr::for_heap(unchecked_usize_as_u32(index)); } } @@ -717,10 +714,10 @@ pub struct Scope<'a> { pub(crate) regs: &'a mut [u64], /// Reference to the constants pool for resolving constant pointers. - pub(crate) constants: &'a [Datum], + pub(crate) constants: &'a [ConstantDatum], /// Reference to the heap for resolving heap pointers. - pub(crate) heap: &'a mut Vec, + pub(crate) heap: &'a mut Vec, /// Start of the current frame (where the arguments to the upcall start). pub(crate) fp: usize, @@ -765,10 +762,7 @@ impl<'a> Scope<'a> { pub fn get_string(&self, arg: u8) -> &str { let index = self.regs[self.fp + (arg as usize)]; let ptr = DatumPtr::from(index); - match ptr.resolve(self.constants, self.heap) { - Datum::Text(s) => s, - _ => panic!("Mismatched constant type"), - } + ptr.resolve_string(self.constants, self.heap) } /// Sets the return value of the function to `b`. @@ -789,7 +783,7 @@ impl<'a> Scope<'a> { /// Sets the return value of the function to `s`. pub fn return_string>(self, s: S) { let index = self.heap.len(); - self.heap.push(Datum::Text(s.into())); + self.heap.push(HeapDatum::Text(s.into())); self.regs[self.fp] = DatumPtr::for_heap(unchecked_usize_as_u32(index)); } } diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 0d1d0379..ea5f4403 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -20,7 +20,7 @@ use crate::bytecode::{self, Register}; use crate::compiler::ids::HashMapWithIds; use crate::compiler::{Error, Result, SymbolKey}; use crate::image::{DebugInfo, Image}; -use crate::mem::Datum; +use crate::mem::ConstantDatum; use crate::reader::LineCol; use std::collections::HashMap; use std::convert::TryFrom; @@ -51,7 +51,7 @@ pub(super) struct Codegen { code: Vec, /// The constants pool for the image being generated. - constants: HashMapWithIds, + constants: HashMapWithIds, /// Collection of fixups to apply after code generation. fixups: HashMap, @@ -100,7 +100,7 @@ impl Codegen { } /// Gets the ID of a `constant`, adding it to the constants table if it isn't yet there. - pub(super) fn get_constant(&mut self, constant: Datum, pos: LineCol) -> Result { + pub(super) fn get_constant(&mut self, constant: ConstantDatum, pos: LineCol) -> Result { match self.constants.get(&constant) { Some((_etype, id)) => Ok(id), None => { diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index c52ad29f..3301ca17 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -21,7 +21,7 @@ use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::syms::{self, SymbolKey, SymbolPrototype, TempScope, TempSymtable}; use crate::compiler::{Error, Result}; -use crate::mem::Datum; +use crate::mem::ConstantDatum; use std::convert::TryFrom; /// Compiles `exprs` into consecutive integer registers allocated from `scope` and returns the @@ -204,7 +204,7 @@ pub(super) fn compile_expr( } Expr::Double(span) => { - let index = codegen.get_constant(Datum::Double(span.value), span.pos)?; + let index = codegen.get_constant(ConstantDatum::Double(span.value), span.pos)?; codegen.emit(bytecode::make_load_constant(reg, index), span.pos); Ok(ExprType::Double) } @@ -215,7 +215,8 @@ pub(super) fn compile_expr( codegen.emit(bytecode::make_load_integer(reg, i), span.pos); } Err(_) => { - let index = codegen.get_constant(Datum::Integer(span.value), span.pos)?; + let index = + codegen.get_constant(ConstantDatum::Integer(span.value), span.pos)?; codegen.emit(bytecode::make_load_constant(reg, index), span.pos); } } @@ -286,7 +287,7 @@ pub(super) fn compile_expr( }, Expr::Text(span) => { - let index = codegen.get_constant(Datum::Text(span.value), span.pos)?; + let index = codegen.get_constant(ConstantDatum::Text(span.value), span.pos)?; codegen.emit(bytecode::make_load_integer(reg, index), span.pos); Ok(ExprType::Text) } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 5e4d7e25..e0262e13 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -24,7 +24,7 @@ use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype}; use crate::compiler::{Error, Result}; use crate::image::Image; -use crate::mem::Datum; +use crate::mem::ConstantDatum; use crate::reader::LineCol; use crate::{Callable, CallableMetadataBuilder, parser}; use std::borrow::Cow; @@ -380,15 +380,16 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R // at runtime by clearning the return register... but the problem is that we need // to handle non-primitive types like strings and the runtime doesn't know the type // of the result to properly allocate it. - match vtype { - ExprType::Boolean | ExprType::Integer => { - ctx.codegen.emit(bytecode::make_load_integer(ret_reg, 0), key_pos); + let value = match vtype { + ExprType::Boolean | ExprType::Integer => 0, + ExprType::Double => { + ctx.codegen.get_constant(ConstantDatum::Double(0.0), key_pos)? } - ExprType::Double | ExprType::Text => { - let index = ctx.codegen.get_constant(Datum::new(vtype), key_pos)?; - ctx.codegen.emit(bytecode::make_load_integer(ret_reg, index), key_pos); + ExprType::Text => { + ctx.codegen.get_constant(ConstantDatum::Text(String::new()), key_pos)? } - } + }; + ctx.codegen.emit(bytecode::make_load_integer(ret_reg, value), key_pos); } for param in callable.params { let key = SymbolKey::from(param.name); diff --git a/core2/src/image.rs b/core2/src/image.rs index 620f34a9..4502c523 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -17,7 +17,7 @@ use crate::bytecode::{self, Opcode, Register, opcode_of}; use crate::compiler::SymbolKey; -use crate::mem::Datum; +use crate::mem::ConstantDatum; use crate::reader::LineCol; use std::collections::HashMap; @@ -74,7 +74,7 @@ pub struct Image { pub(crate) upcalls: Vec, /// Pool of constant values used by the program. - pub(crate) constants: Vec, + pub(crate) constants: Vec, /// Debugging information for error reporting and disassembly. pub(crate) debug_info: DebugInfo, @@ -106,7 +106,7 @@ impl Image { pub(crate) fn new( code: Vec, upcalls: Vec, - constants: Vec, + constants: Vec, debug_info: DebugInfo, ) -> Self { debug_assert!(!code.is_empty(), "Compiler must ensure the image is not empty"); diff --git a/core2/src/mem.rs b/core2/src/mem.rs index 3d2f048a..c00f2548 100644 --- a/core2/src/mem.rs +++ b/core2/src/mem.rs @@ -55,13 +55,14 @@ impl ArrayData { } } -/// A single value in the EndBASIC language. +/// A compile-time constant value stored in the constant pool. +/// +/// Only scalar types that can be hashed are included here. Arrays are never constants. #[derive(Clone, Debug)] -pub(crate) enum Datum { - /// An array value. - Array(ArrayData), - - /// A boolean value. +pub(crate) enum ConstantDatum { + /// A boolean value. Not currently produced by the compiler (booleans are always + /// immediate integers), but included for completeness and future use. + #[allow(dead_code)] Boolean(bool), /// A double-precision floating-point value. @@ -74,7 +75,7 @@ pub(crate) enum Datum { Text(String), } -impl PartialEq for Datum { +impl PartialEq for ConstantDatum { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Boolean(a), Self::Boolean(b)) => a == b, @@ -86,13 +87,12 @@ impl PartialEq for Datum { } } -impl Eq for Datum {} +impl Eq for ConstantDatum {} -impl Hash for Datum { +impl Hash for ConstantDatum { fn hash(&self, state: &mut H) { std::mem::discriminant(self).hash(state); match self { - Self::Array(_) => panic!("Arrays cannot be hashed"), Self::Boolean(b) => b.hash(state), Self::Double(d) => d.to_bits().hash(state), Self::Integer(i) => i.hash(state), @@ -101,21 +101,10 @@ impl Hash for Datum { } } -impl Datum { - /// Creates a new datum of `etype` with a default value. - pub(crate) fn new(etype: ExprType) -> Self { - match etype { - ExprType::Boolean => Datum::Boolean(false), - ExprType::Double => Datum::Double(0.0), - ExprType::Integer => Datum::Integer(0), - ExprType::Text => Datum::Text(String::new()), - } - } - - /// Returns the type of the datum. +impl ConstantDatum { + /// Returns the type of the constant datum. pub(crate) fn etype(&self) -> ExprType { match self { - Self::Array(..) => panic!("Arrays do not have a simple ExprType"), Self::Boolean(..) => ExprType::Boolean, Self::Double(..) => ExprType::Double, Self::Integer(..) => ExprType::Integer, @@ -124,9 +113,22 @@ impl Datum { } } +/// A heap-allocated value used at runtime. +/// +/// Only types that require heap allocation are included here. Scalars other than +/// `Text` live directly in registers and never appear on the heap. +#[derive(Clone, Debug)] +pub(crate) enum HeapDatum { + /// An array value. + Array(ArrayData), + + /// A string value. + Text(String), +} + /// Tagged pointers for constant and heap addresses. /// -/// A `DatumPtr` indexes into the constant pool or the heap, where `Datum` values live. +/// A `DatumPtr` indexes into the constant pool or the heap, where datum values live. /// The encoding uses the sign of the lower 32 bits of a `u64`: positive values are /// constant pool indices, and negative values (two's complement) are heap indices. /// @@ -160,11 +162,23 @@ impl DatumPtr { raw as u64 } - /// Gets the datum pointed to by this pointer from the `constants` and `heap`. - pub(crate) fn resolve<'b>(&self, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { + /// Resolves this pointer and returns the string it points to. + /// + /// Panics if the pointed-to datum is not a `Text` value. + pub(crate) fn resolve_string<'b>( + &self, + constants: &'b [ConstantDatum], + heap: &'b [HeapDatum], + ) -> &'b str { match self { - DatumPtr::Constant(index) => &constants[unchecked_u24_as_usize(*index)], - DatumPtr::Heap(index) => &heap[unchecked_u24_as_usize(*index)], + DatumPtr::Constant(index) => match &constants[unchecked_u24_as_usize(*index)] { + ConstantDatum::Text(s) => s, + _ => panic!("Constant pointer does not point to a Text value"), + }, + DatumPtr::Heap(index) => match &heap[unchecked_u24_as_usize(*index)] { + HeapDatum::Text(s) => s, + _ => panic!("Heap pointer does not point to a Text value"), + }, } } diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 75295a3b..49f5a50d 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -19,7 +19,7 @@ use crate::ExprType; use crate::Scope; use crate::bytecode::{self, Opcode, Register, TaggedRegisterRef, opcode_of}; use crate::image::Image; -use crate::mem::{ArrayData, Datum, DatumPtr}; +use crate::mem::{ArrayData, ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u8; /// Alias for the type representing a program address. @@ -109,10 +109,15 @@ impl Context { self.regs[index] = value; } - /// Dereferences a pointer. - fn deref_ptr<'b>(&self, reg: Register, constants: &'b [Datum], heap: &'b [Datum]) -> &'b Datum { + /// Dereferences a pointer register as a string. + fn deref_string<'b>( + &self, + reg: Register, + constants: &'b [ConstantDatum], + heap: &'b [HeapDatum], + ) -> &'b str { let raw_addr = self.get_reg(reg); - DatumPtr::from(raw_addr).resolve(constants, heap) + DatumPtr::from(raw_addr).resolve_string(constants, heap) } /// Resolves array subscripts and computes the flat index for `arr_reg` with subscripts read @@ -123,12 +128,12 @@ impl Context { &mut self, arr_reg: Register, first_sub_reg: Register, - heap: &[Datum], + heap: &[HeapDatum], ) -> Option<(usize, usize)> { let arr_ptr = DatumPtr::from(self.get_reg(arr_reg)); let heap_idx = arr_ptr.heap_index(); let array = match &heap[heap_idx] { - Datum::Array(a) => a, + HeapDatum::Array(a) => a, _ => unreachable!("Register must point to an array"), }; @@ -161,8 +166,8 @@ impl Context { pub(super) fn upcall_scope<'a>( &'a mut self, reg: Register, - constants: &'a [Datum], - heap: &'a mut Vec, + constants: &'a [ConstantDatum], + heap: &'a mut Vec, ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); @@ -176,7 +181,7 @@ impl Context { /// Panics if the processor state is out of sync with `image` or if the contents of `image` /// are invalid. We assume that the image comes from the result of an in-process compilation /// (not stored bytecode) and that the compiler guarantees that the image is valid. - pub(super) fn exec(&mut self, image: &Image, heap: &mut Vec) -> InternalStopReason { + pub(super) fn exec(&mut self, image: &Image, heap: &mut Vec) -> InternalStopReason { while self.stop.is_none() { let instr = image.code[self.pc]; @@ -237,16 +242,17 @@ impl Context { } /// Implements the `Alloc` opcode. - pub(super) fn do_alloc(&mut self, instr: u32, heap: &mut Vec) { + pub(super) fn do_alloc(&mut self, instr: u32, heap: &mut Vec) { let (dest, etype) = bytecode::parse_alloc(instr); - heap.push(Datum::new(etype)); + debug_assert_eq!(ExprType::Text, etype, "Alloc is only emitted for strings right now"); + heap.push(HeapDatum::Text(String::new())); let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); self.set_reg(dest, ptr); self.pc += 1; } /// Implements the `AllocArray` opcode. - pub(super) fn do_alloc_array(&mut self, instr: u32, heap: &mut Vec) { + pub(super) fn do_alloc_array(&mut self, instr: u32, heap: &mut Vec) { let (dest, packed, first_dim_reg) = bytecode::parse_alloc_array(instr); let subtype = packed.subtype(); let ndims = usize::from(packed.ndims()); @@ -274,14 +280,14 @@ impl Context { ExprType::Text => { let mut values = Vec::with_capacity(total); for _ in 0..total { - heap.push(Datum::Text(String::new())); + heap.push(HeapDatum::Text(String::new())); values.push(DatumPtr::for_heap((heap.len() - 1) as u32)); } values } }; let array = ArrayData { dimensions, values }; - heap.push(Datum::Array(array)); + heap.push(HeapDatum::Array(array)); let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); self.set_reg(dest, ptr); self.pc += 1; @@ -298,15 +304,17 @@ impl Context { } /// Implements the `Concat` opcode. - pub(super) fn do_concat(&mut self, instr: u32, constants: &[Datum], heap: &mut Vec) { + pub(super) fn do_concat( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &mut Vec, + ) { let (dest, src1, src2) = bytecode::parse_concat(instr); - let lhs = self.deref_ptr(src1, constants, heap); - let rhs = self.deref_ptr(src2, constants, heap); - let result = match (lhs, rhs) { - (Datum::Text(lhs), Datum::Text(rhs)) => format!("{}{}", lhs, rhs), - _ => unreachable!(), - }; - heap.push(Datum::Text(result)); + let lhs = self.deref_string(src1, constants, heap).to_owned(); + let rhs = self.deref_string(src2, constants, heap); + let result = lhs + rhs; + heap.push(HeapDatum::Text(result)); let ptr = DatumPtr::for_heap((heap.len() - 1) as u32); self.set_reg(dest, ptr); self.pc += 1; @@ -356,12 +364,12 @@ impl Context { } /// Implements the `LoadArray` opcode. - pub(super) fn do_load_array(&mut self, instr: u32, heap: &[Datum]) { + pub(super) fn do_load_array(&mut self, instr: u32, heap: &[HeapDatum]) { let (dest, arr_reg, first_sub_reg) = bytecode::parse_load_array(instr); if let Some((heap_idx, flat_idx)) = self.resolve_array_index(arr_reg, first_sub_reg, heap) { let array = match &heap[heap_idx] { - Datum::Array(a) => a, + HeapDatum::Array(a) => a, _ => unreachable!("Register must point to an array"), }; self.set_reg(dest, array.values[flat_idx]); @@ -370,14 +378,13 @@ impl Context { } /// Implements the `LoadConstant` opcode. - pub(super) fn do_load_constant(&mut self, instr: u32, constants: &[Datum]) { + pub(super) fn do_load_constant(&mut self, instr: u32, constants: &[ConstantDatum]) { let (register, i) = bytecode::parse_load_constant(instr); match &constants[usize::from(i)] { - Datum::Array(_) => unreachable!("Arrays cannot be constants"), - Datum::Boolean(_) => unreachable!("Booleans are always immediates"), - Datum::Double(d) => self.set_reg(register, d.to_bits()), - Datum::Integer(i) => self.set_reg(register, *i as u64), - Datum::Text(_) => unreachable!("Strings cannot be loaded into registers"), + ConstantDatum::Boolean(_) => unreachable!("Booleans are always immediates"), + ConstantDatum::Double(d) => self.set_reg(register, d.to_bits()), + ConstantDatum::Integer(i) => self.set_reg(register, *i as u64), + ConstantDatum::Text(_) => unreachable!("Strings cannot be loaded into registers"), } self.pc += 1; } @@ -456,13 +463,13 @@ impl Context { } /// Implements the `StoreArray` opcode. - pub(super) fn do_store_array(&mut self, instr: u32, heap: &mut [Datum]) { + pub(super) fn do_store_array(&mut self, instr: u32, heap: &mut [HeapDatum]) { let (arr_reg, val_reg, first_sub_reg) = bytecode::parse_store_array(instr); let value = self.get_reg(val_reg); if let Some((heap_idx, flat_idx)) = self.resolve_array_index(arr_reg, first_sub_reg, heap) { let array = match &mut heap[heap_idx] { - Datum::Array(a) => a, + HeapDatum::Array(a) => a, _ => unreachable!("Register must point to an array"), }; array.values[flat_idx] = value; diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index c0017a9e..62341d64 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -20,7 +20,7 @@ use crate::bytecode::Register; use crate::callable::{Callable, Scope}; use crate::compiler::SymbolKey; use crate::image::Image; -use crate::mem::Datum; +use crate::mem::HeapDatum; use crate::reader::LineCol; use std::collections::HashMap; use std::rc::Rc; @@ -68,7 +68,7 @@ pub struct Vm { upcalls: Vec>, /// Heap memory for dynamic allocations. - heap: Vec, + heap: Vec, /// Processor context for execution. context: Context, From 18be7287fc31749567e643cf9695eb0d7e03b63c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 19 Feb 2026 01:34:52 -0800 Subject: [PATCH 13/53] core2: Add global variable accessors by name Extend the core2 API so that a Rust host can both pre-define global variables before compilation and query global variable values by name after execution. This makes it practical to use EndBASIC as an embeddable configuration DSL. To make this possible, start by adding a new compile_with_globals function that accepts a slice of GlobalDef descriptors. Each descriptor names a variable (scalar or array) along with its type. Scalars are initialized to their default values via LOADI/ALLOC instructions emitted before the program's own ENTER. Arrays are allocated via a short-lived ENTER/DIMA/LEAVE preamble that borrows local-register slots for the dimension temporaries without consuming any of the 64 global register slots permanently. A new LEAVE bytecode instruction truncates the register file back to the FP, removing the preamble's temporary locals. All pre-defined globals are added to GlobalSymtable so the compiled program can read and write them directly. Then, to support querying the globals: DebugInfo now carries a global_vars map that records every global register assignment (from both compile_with_globals and DIM SHARED declarations in the user's program). After execution, the caller can query scalar globals via Vm::get_global and array elements via Vm::get_global_array. A new core2/examples/config.rs example (which mimics the old core/examples/config.rs, but uses a different binary name to disambiguate it in integration tests) demonstrates the full workflow: pre-defining several globals, running a script that reads and writes them, and extracting the results by name. --- core2/Cargo.toml | 5 ++ core2/examples/config.rs | 142 ++++++++++++++++++++++++++++++++++ core2/src/bytecode.rs | 11 +++ core2/src/compiler/codegen.rs | 9 ++- core2/src/compiler/ids.rs | 5 ++ core2/src/compiler/mod.rs | 2 +- core2/src/compiler/syms.rs | 7 ++ core2/src/compiler/top.rs | 137 ++++++++++++++++++++++++++++++-- core2/src/image.rs | 21 +++++ core2/src/lib.rs | 7 +- core2/src/mem.rs | 12 +-- core2/src/vm/context.rs | 15 ++++ core2/src/vm/mod.rs | 95 ++++++++++++++++++++++- core2/tests/config.out | 10 +++ core2/tests/examples_test.rs | 129 ++++++++++++++++++++++++++++++ 15 files changed, 589 insertions(+), 18 deletions(-) create mode 100644 core2/examples/config.rs create mode 100644 core2/tests/config.out create mode 100644 core2/tests/examples_test.rs diff --git a/core2/Cargo.toml b/core2/Cargo.toml index 1337932a..19192eb6 100644 --- a/core2/Cargo.toml +++ b/core2/Cargo.toml @@ -23,3 +23,8 @@ thiserror = "1.0" futures-lite = "2.2" tempfile = "3" tokio = { version = "1", features = ["full"] } + +[[example]] +name = "core2-config" +path = "examples/config.rs" +harness = false diff --git a/core2/examples/config.rs b/core2/examples/config.rs new file mode 100644 index 00000000..9867966b --- /dev/null +++ b/core2/examples/config.rs @@ -0,0 +1,142 @@ +// 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. + +//! Configuration file parser using an EndBASIC interpreter. +//! +//! This example sets up a minimal EndBASIC interpreter and uses it to parse what could be a +//! configuration file. Because the interpreter is configured without any commands or functions, +//! the scripted code cannot call back into Rust land, so the script's execution is guaranteed to +//! not have side-effects. + +use endbasic_core2::{ + ConstantDatum, ExprType, GlobalDef, GlobalDefKind, StopReason, Vm, compile_with_globals, + only_metadata, +}; +use std::collections::HashMap; + +/// Sample configuration file to parse. +const SCRIPT: &str = r#" +foo_value% = 123 +result_total% = foo_value% + 456 +status$ = "Processing complete" +results%(0) = 10 +results%(1) = 20 +results%(2) = 30 + +DIM SHARED defined_within +defined_within = 42 +"#; + +fn main() { + // Describe the global variables the script is expected to read and write. + // Note that "optional_flag" is declared so the script could set it, but the + // script does not assign it. Its value will be the zero default. + let global_defs = vec![ + GlobalDef { name: "foo_value".to_owned(), kind: GlobalDefKind::Scalar(ExprType::Integer) }, + GlobalDef { + name: "result_total".to_owned(), + kind: GlobalDefKind::Scalar(ExprType::Integer), + }, + GlobalDef { name: "status".to_owned(), kind: GlobalDefKind::Scalar(ExprType::Text) }, + GlobalDef { + name: "results".to_owned(), + kind: GlobalDefKind::Array { subtype: ExprType::Integer, dimensions: vec![3] }, + }, + GlobalDef { + name: "optional_flag".to_owned(), + kind: GlobalDefKind::Scalar(ExprType::Boolean), + }, + ]; + + // Compile the script, making the pre-defined globals visible to it. + let upcalls = HashMap::default(); + let image = + compile_with_globals(&mut SCRIPT.as_bytes(), &only_metadata(&upcalls), &global_defs) + .expect("Compilation failed"); + + // Load and execute the compiled image. + let mut vm = Vm::new(upcalls); + vm.load(image); + match vm.exec() { + StopReason::End(code) => { + if code != 0 { + eprintln!("Script exited with code {}", code); + } + } + StopReason::Exception(pos, msg) => { + eprintln!("Script raised an exception at {}: {}", pos, msg); + std::process::exit(1); + } + StopReason::Upcall(_) => { + eprintln!("Unexpected upcall (no upcalls are registered)"); + std::process::exit(1); + } + } + + // Query the global variables by name. + match vm.get_global("foo_value") { + Ok(Some(ConstantDatum::Integer(v))) => println!("foo_value% = {}", v), + Ok(Some(other)) => println!("foo_value% has unexpected type: {:?}", other), + Ok(None) => println!("foo_value% is not set"), + Err(e) => println!("foo_value%: error: {}", e), + } + match vm.get_global("result_total") { + Ok(Some(ConstantDatum::Integer(v))) => println!("result_total% = {}", v), + Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), + Ok(None) => println!("result_total% is not set"), + Err(e) => println!("result_total%: error: {}", e), + } + match vm.get_global("status") { + Ok(Some(ConstantDatum::Text(v))) => println!("status$ = {:?}", v), + Ok(Some(other)) => println!("status$ has unexpected type: {:?}", other), + Ok(None) => println!("status$ is not set"), + Err(e) => println!("status$: error: {}", e), + } + // defined_within was not provided upfront but was declared as a global within + // the script. We can query it here too. + match vm.get_global("defined_within") { + Ok(Some(ConstantDatum::Integer(v))) => println!("defined_within% = {}", v), + Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), + Ok(None) => println!("defined_within% is not set"), + Err(e) => println!("defined_within%: error: {}", e), + } + // optional_flag was declared but the script never assigned it, so it should + // receive its "zero value". + match vm.get_global("optional_flag") { + Ok(Some(ConstantDatum::Boolean(v))) => println!("optional_flag? = {}", v), + Ok(Some(other)) => println!("optional_flag? has unexpected type: {:?}", other), + Ok(None) => println!("optional_flag? is not declared"), + Err(e) => println!("optional_flag?: error: {}", e), + } + // "unknown" was never declared at all, so get_global returns Ok(None). + match vm.get_global("unknown") { + Ok(Some(v)) => println!("unknown = {:?}", v), + Ok(None) => println!("unknown is not declared"), + Err(e) => println!("unknown: error: {}", e), + } + for i in 0..3_i32 { + match vm.get_global_array("results", &[i]) { + Ok(Some(ConstantDatum::Integer(v))) => println!("results%({}) = {}", i, v), + Ok(Some(other)) => println!("results%({}) has unexpected type: {:?}", i, other), + Ok(None) => println!("results%({}) is not set", i), + Err(e) => println!("results%({}): error: {}", i, e), + } + } + // Demonstrate that querying a scalar as an array yields an error. + match vm.get_global_array("foo_value", &[0]) { + Ok(v) => println!("foo_value%(0) = {:?}", v), + Err(e) => println!("foo_value%(0): error: {}", e), + } +} diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 862e7a70..f8c2fb32 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -402,6 +402,9 @@ pub(crate) enum Opcode { /// Jumps to an address relative to the PC. Jump, + /// Deallocates the registers allocated by the preamble ENTER, unwinding to the FP. + Leave, + /// Loads an element from an array. LoadArray, @@ -534,6 +537,12 @@ instr!( u16, 0x0000ffff, 0, // Target address. ); +#[rustfmt::skip] +instr!( + Opcode::Leave, "LEAVE", + make_leave, parse_leave, format_leave, +); + #[rustfmt::skip] instr!( Opcode::LoadArray, "LOADA", @@ -804,6 +813,8 @@ mod tests { test_instr!(test_jump, make_jump, parse_jump, 12345); + test_instr!(test_leave, make_leave, parse_leave); + test_instr!( test_load_array, make_load_array, diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index ea5f4403..e20c8de7 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -19,7 +19,7 @@ use crate::ast::ExprType; use crate::bytecode::{self, Register}; use crate::compiler::ids::HashMapWithIds; use crate::compiler::{Error, Result, SymbolKey}; -use crate::image::{DebugInfo, Image}; +use crate::image::{DebugInfo, GlobalVarInfo, Image}; use crate::mem::ConstantDatum; use crate::reader::LineCol; use std::collections::HashMap; @@ -173,7 +173,10 @@ impl Codegen { } /// Consumes the code generator and builds a ready-to-use `Image`. - pub(super) fn build_image(mut self) -> Result { + pub(super) fn build_image( + mut self, + global_vars: HashMap, + ) -> Result { self.apply_fixups()?; let mut callables = HashMap::default(); @@ -186,7 +189,7 @@ impl Codegen { self.code, self.upcalls.keys_to_vec(), self.constants.keys_to_vec(), - DebugInfo { instr_linecols: self.instr_linecols, callables }, + DebugInfo { instr_linecols: self.instr_linecols, callables, global_vars }, )) } } diff --git a/core2/src/compiler/ids.rs b/core2/src/compiler/ids.rs index 149dc784..810a2a70 100644 --- a/core2/src/compiler/ids.rs +++ b/core2/src/compiler/ids.rs @@ -83,6 +83,11 @@ where self.map.len() } + /// Iterates over all entries, yielding `(key, value, id)` tuples in arbitrary order. + pub(super) fn iter(&self) -> impl Iterator + '_ { + self.map.iter().map(|(k, (v, i))| (k, v, *i)) + } + /// Returns the keys in insertion order. pub(super) fn keys_to_vec(self) -> Vec { let mut reverse = self.map.into_iter().collect::>(); diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 453c49c4..01eaa5f7 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -34,7 +34,7 @@ mod syms; pub use syms::SymbolKey; mod top; -pub use top::{compile, only_metadata}; +pub use top::{GlobalDef, GlobalDefKind, compile, compile_with_globals, only_metadata}; /// Errors that can occur during compilation. #[derive(Debug, thiserror::Error)] diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index cf756c4d..e0c431a4 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -186,6 +186,13 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { self.globals.get(key).is_some() } + /// Iterates over all global variables, yielding `(key, prototype, register_index)` tuples. + pub(crate) fn iter_globals( + &self, + ) -> impl Iterator + '_ { + self.globals.iter().map(|(k, v, i)| (k, *v, i)) + } + /// Defines a new user-defined `vref` callable with `md` metadata. pub(crate) fn define_user_callable( &mut self, diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index e0262e13..e8f243bc 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,18 +16,19 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement, VarRef}; -use crate::bytecode::{self, PackedArrayType, RegisterScope}; +use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer_exprs}; use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype}; use crate::compiler::{Error, Result}; -use crate::image::Image; +use crate::image::{GlobalVarInfo, Image}; use crate::mem::ConstantDatum; use crate::reader::LineCol; use crate::{Callable, CallableMetadataBuilder, parser}; use std::borrow::Cow; +use std::cmp::max; use std::collections::HashMap; use std::io; use std::iter::Iterator; @@ -424,21 +425,147 @@ pub fn only_metadata( upcalls } -/// Compiles the `input` into an `Image` that can be executed by the VM. +/// Descriptor for a single global variable to be pre-defined before compilation. +pub struct GlobalDef { + /// Name of the variable (case-insensitive, as in EndBASIC). + pub name: String, + + /// Kind and type information for the variable. + pub kind: GlobalDefKind, +} + +/// Kind of a pre-defined global variable. +pub enum GlobalDefKind { + /// A scalar (non-array) variable with the given element type. + /// + /// The variable is initialized to its default value: `0` for numeric types and an empty + /// string for `Text`. + Scalar(ExprType), + + /// A multidimensional array with the given element type and fixed dimension sizes. + /// + /// Each dimension size must be positive and must fit in a `u16`. + Array { + /// Element type of the array. + subtype: ExprType, + + /// Size of each dimension, in order from outermost to innermost. + dimensions: Vec, + }, +} + +/// Prepares global variables injected from outside of the compiled program. +/// +/// Pre-defined scalar globals are initialized to their default values (0 for numeric types, +/// empty string for text). Pre-defined array globals are allocated with all elements set to +/// their default values. The compiled program may read or write any of these globals. +/// +/// After execution, use `Vm::get_global*` methods to query the values of these globals (and +/// any globals declared via `DIM SHARED` in the program itself). +fn prepare_globals( + ctx: &mut Context, + symtable: &mut GlobalSymtable, + global_defs: &[GlobalDef], +) -> Result<()> { + let preamble_pos = LineCol { line: 0, col: 0 }; + + // Register all global defs in the symbol table and collect array globals for the preamble. + let mut max_ndims: u8 = 0; + let mut array_globals: Vec<(Register, &GlobalDef)> = vec![]; + for def in global_defs { + let key = SymbolKey::from(&def.name); + match &def.kind { + GlobalDefKind::Array { subtype, dimensions } => { + let ndims = + u8::try_from(dimensions.len()).expect("Array must have at most 255 dimensions"); + let info = syms::ArrayInfo { subtype: *subtype, ndims: usize::from(ndims) }; + let reg = symtable + .put_global(key, SymbolPrototype::Array(info)) + .map_err(|e| Error::from_syms(e, preamble_pos))?; + max_ndims = max(max_ndims, ndims); + array_globals.push((reg, def)); + } + + GlobalDefKind::Scalar(etype) => { + let reg = symtable + .put_global(key, SymbolPrototype::Scalar(*etype)) + .map_err(|e| Error::from_syms(e, preamble_pos))?; + ctx.codegen.emit_default(reg, *etype, preamble_pos); + } + } + } + + // Emit the array initialization preamble, but only if any arrays were defined. + // + // We use a short-lived `ENTER/LEAVE` scope to borrow local registers for the dimension + // temporaries without permanently consuming global register slots. + if array_globals.is_empty() { + return Ok(()); + } + ctx.codegen.emit(bytecode::make_enter(max_ndims), preamble_pos); + for (reg, def) in array_globals { + let GlobalDefKind::Array { subtype, dimensions } = &def.kind else { + unreachable!("array_globals only contains array defs per the loop above") + }; + + let ndims = u8::try_from(dimensions.len()).unwrap(); + for (i, &dim) in dimensions.iter().enumerate() { + let dim_u16 = + u16::try_from(dim).expect("Array dimension must fit in u16 for LOADI instruction"); + let local_reg = + Register::local(u8::try_from(i).unwrap()).expect("Dimension index fits in u8"); + ctx.codegen.emit(bytecode::make_load_integer(local_reg, dim_u16), preamble_pos); + } + + let first_dim_reg = Register::local(0).expect("Local register 0 is always valid"); + let packed = PackedArrayType::new(*subtype, usize::from(ndims)) + .map_err(|_| Error::TooManyArrayDimensions(preamble_pos, usize::from(ndims)))?; + ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), preamble_pos); + } + ctx.codegen.emit(bytecode::make_leave(), preamble_pos); + + Ok(()) +} + +/// Compiles the `input` into an `Image` that can be executed by the VM, with `global_defs` +/// pre-defined as global variables visible to the compiled program. /// /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. -pub fn compile( +pub fn compile_with_globals( input: &mut dyn io::Read, upcalls: &HashMap<&SymbolKey, &CallableMetadata>, + global_defs: &[GlobalDef], ) -> Result { let mut ctx = Context::default(); let mut symtable = GlobalSymtable::new(upcalls); + prepare_globals(&mut ctx, &mut symtable, global_defs)?; + let program_end = Statement::End(EndSpan { code: None, pos: LineCol { line: 0, col: 0 } }); compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input).chain([Ok(program_end)]))?; compile_user_callables(&mut ctx, &mut symtable)?; - ctx.codegen.build_image() + let global_vars = symtable + .iter_globals() + .map(|(key, proto, reg)| { + let (subtype, ndims) = match proto { + SymbolPrototype::Array(info) => (info.subtype, info.ndims), + SymbolPrototype::Scalar(etype) => (etype, 0), + }; + (key.clone(), GlobalVarInfo { reg, subtype, ndims }) + }) + .collect(); + ctx.codegen.build_image(global_vars) +} + +/// Compiles the `input` into an `Image` that can be executed by the VM. +/// +/// `upcalls` contains the metadata of all built-in callables that the compiled code can use. +pub fn compile( + input: &mut dyn io::Read, + upcalls: &HashMap<&SymbolKey, &CallableMetadata>, +) -> Result { + compile_with_globals(input, upcalls, &[]) } diff --git a/core2/src/image.rs b/core2/src/image.rs index 4502c523..902b31b0 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -15,6 +15,7 @@ //! Compiled program representation. +use crate::ast::ExprType; use crate::bytecode::{self, Opcode, Register, opcode_of}; use crate::compiler::SymbolKey; use crate::mem::ConstantDatum; @@ -38,6 +39,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::Gosub => bytecode::format_gosub(instr), Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), Opcode::Jump => bytecode::format_jump(instr), + Opcode::Leave => bytecode::format_leave(instr), Opcode::LoadArray => bytecode::format_load_array(instr), Opcode::LoadConstant => bytecode::format_load_constant(instr), Opcode::LoadInteger => bytecode::format_load_integer(instr), @@ -52,6 +54,18 @@ pub(crate) fn format_instr(instr: u32) -> String { } } +/// Information about a global variable tracked for post-execution querying. +pub(crate) struct GlobalVarInfo { + /// Global register index (0 to `Register::MAX_GLOBAL - 1`). + pub(crate) reg: u8, + + /// Element type (for arrays, the element type; for scalars, the scalar type). + pub(crate) subtype: ExprType, + + /// Number of dimensions: 0 for scalars, >=1 for arrays. + pub(crate) ndims: usize, +} + /// Debugging information for a compiled program. #[derive(Default)] pub struct DebugInfo { @@ -61,6 +75,12 @@ pub struct DebugInfo { /// Maps instruction addresses to the names of user-defined callables that start at those /// addresses. pub(crate) callables: HashMap, + + /// Maps global variable names to their register assignments and type information. + /// + /// This includes both host-pre-defined globals (from `compile_with_globals`) and + /// globals declared via `DIM SHARED` in the user's program. + pub(crate) global_vars: HashMap, } /// Representation of a compiled EndBASIC program. @@ -97,6 +117,7 @@ impl Default for Image { DebugInfo { instr_linecols: vec![LineCol { line: 0, col: 0 }], callables: HashMap::default(), + global_vars: HashMap::default(), }, ) } diff --git a/core2/src/lib.rs b/core2/src/lib.rs index 52efee99..772673c2 100644 --- a/core2/src/lib.rs +++ b/core2/src/lib.rs @@ -30,8 +30,11 @@ mod vm; pub use ast::{ArgSep, ExprType}; pub use bytecode::VarArgTag; pub use callable::*; -pub use compiler::{SymbolKey, compile, only_metadata}; -pub use vm::{StopReason, Vm}; +pub use compiler::{ + GlobalDef, GlobalDefKind, SymbolKey, compile, compile_with_globals, only_metadata, +}; +pub use mem::ConstantDatum; +pub use vm::{GetGlobalError, GetGlobalResult, StopReason, Vm}; #[cfg(test)] mod testutils; diff --git a/core2/src/mem.rs b/core2/src/mem.rs index c00f2548..711b8144 100644 --- a/core2/src/mem.rs +++ b/core2/src/mem.rs @@ -55,14 +55,14 @@ impl ArrayData { } } -/// A compile-time constant value stored in the constant pool. +/// A typed scalar value, used both in the compile-time constant pool and as a +/// return value when inspecting global variables after execution. /// -/// Only scalar types that can be hashed are included here. Arrays are never constants. +/// Only scalar types that can be hashed are included here. Arrays are never +/// stored as `ConstantDatum`. #[derive(Clone, Debug)] -pub(crate) enum ConstantDatum { - /// A boolean value. Not currently produced by the compiler (booleans are always - /// immediate integers), but included for completeness and future use. - #[allow(dead_code)] +pub enum ConstantDatum { + /// A boolean value. Boolean(bool), /// A double-precision floating-point value. diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 49f5a50d..628807bb 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -120,6 +120,13 @@ impl Context { DatumPtr::from(raw_addr).resolve_string(constants, heap) } + /// Returns the raw `u64` value stored in global register `index`. + /// + /// Used by the VM's `get_global_*` methods to read global variable values after execution. + pub(super) fn get_global_reg_raw(&self, index: u8) -> u64 { + self.regs[usize::from(index)] + } + /// Resolves array subscripts and computes the flat index for `arr_reg` with subscripts read /// from registers starting at `first_sub_reg`. /// @@ -198,6 +205,7 @@ impl Context { Opcode::Gosub => self.do_gosub(instr), Opcode::IntegerToDouble => self.do_integer_to_double(instr), Opcode::Jump => self.do_jump(instr), + Opcode::Leave => self.do_leave(instr), Opcode::LoadArray => self.do_load_array(instr, heap), Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), Opcode::LoadInteger => self.do_load_integer(instr), @@ -363,6 +371,13 @@ impl Context { self.pc = Address::from(offset); } + /// Implements the `Leave` opcode. + pub(super) fn do_leave(&mut self, instr: u32) { + bytecode::parse_leave(instr); + self.regs.truncate(self.fp); + self.pc += 1; + } + /// Implements the `LoadArray` opcode. pub(super) fn do_load_array(&mut self, instr: u32, heap: &[HeapDatum]) { let (dest, arr_reg, first_sub_reg) = bytecode::parse_load_array(instr); diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 62341d64..b3addcc2 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -16,11 +16,12 @@ //! Virtual machine for EndBASIC execution. use crate::CallResult; +use crate::ast::ExprType; use crate::bytecode::Register; use crate::callable::{Callable, Scope}; use crate::compiler::SymbolKey; use crate::image::Image; -use crate::mem::HeapDatum; +use crate::mem::{ConstantDatum, DatumPtr, HeapDatum}; use crate::reader::LineCol; use std::collections::HashMap; use std::rc::Rc; @@ -28,6 +29,28 @@ use std::rc::Rc; mod context; use context::{Context, InternalStopReason}; +/// Error returned when a global variable access encounters a type or shape mismatch. +/// +/// This is distinct from a missing variable, which is represented by `None` in the +/// return value of `get_global` and `get_global_array`. +#[derive(Debug, thiserror::Error)] +pub enum GetGlobalError { + /// The variable exists but is an array; use `get_global_array` instead. + #[error("'{0}' is an array variable; use get_global_array to access it")] + IsArray(String), + + /// The variable exists but is a scalar; use `get_global` instead. + #[error("'{0}' is a scalar variable; use get_global to access it")] + IsScalar(String), + + /// The array subscripts are out of bounds or invalid. + #[error("{0}")] + SubscriptOutOfBounds(String), +} + +/// Result type for global variable access operations. +pub type GetGlobalResult = Result; + /// Opaque handle to invoke a pending upcall. pub struct UpcallHandler<'a>(&'a mut Vm); @@ -116,6 +139,76 @@ impl Vm { self.context.upcall_scope(reg, constants, &mut self.heap) } + /// Returns the value of the global scalar variable `name` as a `ConstantDatum`. + /// + /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the + /// variable was not declared). Returns `Err` if the variable exists but is an + /// array; in that case, use `get_global_array` instead. + pub fn get_global(&self, name: &str) -> GetGlobalResult> { + let key = SymbolKey::from(name); + let Some(image) = self.image.as_ref() else { + return Ok(None); + }; + let Some(info) = image.debug_info.global_vars.get(&key) else { + return Ok(None); + }; + if info.ndims != 0 { + return Err(GetGlobalError::IsArray(name.to_owned())); + } + let raw = self.context.get_global_reg_raw(info.reg); + let datum = match info.subtype { + ExprType::Boolean => ConstantDatum::Boolean(raw != 0), + ExprType::Double => ConstantDatum::Double(f64::from_bits(raw)), + ExprType::Integer => ConstantDatum::Integer(raw as i32), + ExprType::Text => { + let ptr = DatumPtr::from(raw); + ConstantDatum::Text(ptr.resolve_string(&image.constants, &self.heap).to_owned()) + } + }; + Ok(Some(datum)) + } + + /// Returns the value of an element in the global array variable `name` at the given + /// `subscripts` as a `ConstantDatum`. + /// + /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the + /// variable was not declared). Returns `Err` if the variable exists but is a scalar + /// (use `get_global` instead), or if the subscripts are out of bounds. + pub fn get_global_array( + &self, + name: &str, + subscripts: &[i32], + ) -> GetGlobalResult> { + let key = SymbolKey::from(name); + let Some(image) = self.image.as_ref() else { + return Ok(None); + }; + let Some(info) = image.debug_info.global_vars.get(&key) else { + return Ok(None); + }; + if info.ndims == 0 { + return Err(GetGlobalError::IsScalar(name.to_owned())); + } + let raw = self.context.get_global_reg_raw(info.reg); + let ptr = DatumPtr::from(raw); + let heap_idx = ptr.heap_index(); + let HeapDatum::Array(a) = &self.heap[heap_idx] else { + panic!("Array variable does not point to an array on the heap"); + }; + let flat_idx = a.flat_index(subscripts).map_err(GetGlobalError::SubscriptOutOfBounds)?; + let v = a.values[flat_idx]; + let datum = match info.subtype { + ExprType::Boolean => ConstantDatum::Boolean(v != 0), + ExprType::Double => ConstantDatum::Double(f64::from_bits(v)), + ExprType::Integer => ConstantDatum::Integer(v as i32), + ExprType::Text => { + let ptr = DatumPtr::from(v); + ConstantDatum::Text(ptr.resolve_string(&image.constants, &self.heap).to_owned()) + } + }; + Ok(Some(datum)) + } + /// Starts or resumes execution of the loaded image. /// /// Returns a `StopReason` indicating why execution stopped, which may be due to program diff --git a/core2/tests/config.out b/core2/tests/config.out new file mode 100644 index 00000000..1c014c10 --- /dev/null +++ b/core2/tests/config.out @@ -0,0 +1,10 @@ +foo_value% = 123 +result_total% = 579 +status$ = "Processing complete" +defined_within% = 42 +optional_flag? = false +unknown is not declared +results%(0) = 10 +results%(1) = 20 +results%(2) = 30 +foo_value%(0): error: 'foo_value' is a scalar variable; use get_global to access it diff --git a/core2/tests/examples_test.rs b/core2/tests/examples_test.rs new file mode 100644 index 00000000..1ea0592b --- /dev/null +++ b/core2/tests/examples_test.rs @@ -0,0 +1,129 @@ +// EndBASIC +// Copyright 2020 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. + +//! Integration tests that use golden input and output files. + +use std::env; +use std::fs::File; +use std::io::Read; +use std::path::{Path, PathBuf}; +use std::process; + +/// Computes the path to the directory where this test's binary lives. +fn self_dir() -> PathBuf { + let self_exe = env::current_exe().expect("Cannot get self's executable path"); + let dir = self_exe.parent().expect("Cannot get self's directory"); + assert!(dir.ends_with("target/debug/deps") || dir.ends_with("target/release/deps")); + dir.to_owned() +} + +/// Computes the path to the built binary `name`. +fn bin_path>(name: P) -> PathBuf { + let test_dir = self_dir(); + let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory"); + debug_or_release_dir.join(name).with_extension(env::consts::EXE_EXTENSION) +} + +/// Computes the path to the source file `name`. +fn src_path(name: &str) -> PathBuf { + let test_dir = self_dir(); + let debug_or_release_dir = test_dir.parent().expect("Failed to get parent directory"); + let target_dir = debug_or_release_dir.parent().expect("Failed to get parent directory"); + let dir = target_dir.parent().expect("Failed to get parent directory"); + + // Sanity-check that we landed in the right location. + assert!(dir.join("Cargo.toml").exists()); + + dir.join(name) +} + +/// Describes the behavior for one of the three streams (stdin, stdout, stderr) connected to a +/// program. +enum Behavior { + /// Ensure the stream is silent. + Null, + + /// If stdin, feed the given path as the program's input. If stdout/stderr, expect the contents + /// of the stream to match this file. + File(PathBuf), +} + +/// Reads the contents of a golden data file. +fn read_golden(path: &Path) -> String { + let mut f = File::open(path).expect("Failed to open golden data file"); + let mut golden = vec![]; + f.read_to_end(&mut golden).expect("Failed to read golden data file"); + let raw = String::from_utf8(golden).expect("Golden data file is not valid UTF-8"); + if cfg!(target_os = "windows") { raw.replace("\r\n", "\n") } else { raw } +} + +/// Runs `bin` with arguments `args` and checks its behavior against expectations. +/// +/// `exp_code` is the expected error code from the program. `stdin_behavior` indicates what to feed +/// to the program's stdin. `stdout_behavior` and `stderr_behavior` indicate what to expect from +/// the program's textual output. +fn check>( + bin: P, + args: &[&str], + exp_code: i32, + stdin_behavior: Behavior, + stdout_behavior: Behavior, + stderr_behavior: Behavior, +) { + let golden_stdin = match stdin_behavior { + Behavior::Null => process::Stdio::null(), + Behavior::File(path) => File::open(path).unwrap().into(), + }; + + let exp_stdout = match stdout_behavior { + Behavior::Null => "".to_owned(), + Behavior::File(path) => read_golden(&path), + }; + + let exp_stderr = match stderr_behavior { + Behavior::Null => "".to_owned(), + Behavior::File(path) => read_golden(&path), + }; + + let result = process::Command::new(bin.as_ref()) + .args(args) + .stdin(golden_stdin) + .output() + .expect("Failed to execute subprocess"); + let code = result.status.code().expect("Subprocess didn't exit cleanly"); + let stdout = String::from_utf8(result.stdout).expect("Stdout not is not valid UTF-8"); + let stderr = String::from_utf8(result.stderr).expect("Stderr not is not valid UTF-8"); + + if exp_code != code || exp_stdout != stdout || exp_stderr != stderr { + eprintln!("Exit code: {}", code); + eprintln!("stdout:\n{}", stdout); + eprintln!("stderr:\n{}", stderr); + assert_eq!(exp_code, code); + assert_eq!(exp_stdout, stdout); + assert_eq!(exp_stderr, stderr); + } +} + +#[test] +fn test_config() { + check( + bin_path("examples/core2-config"), + &[], + 0, + Behavior::Null, + Behavior::File(src_path("core2/tests/config.out")), + Behavior::Null, + ); +} From 283bc5d5c6d2af257a87dd4d6a3ce5af689809b7 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 19 Feb 2026 06:18:24 -0800 Subject: [PATCH 14/53] core2: Expose arg locations in the Scope Built-in callables now have a way to query the source position of any argument at runtime via the new Scope::get_pos(n) method, enabling precise error messages that point to the specific argument that caused a problem. To support this, DebugInfo now stores an InstrMetadata struct per instruction instead of a bare LineCol. Each InstrMetadata carries the instruction's source position plus an optional arg_linecols vector that, for UPCALL and CALL instructions, records the source position of every compiled argument register slot. The compiler's compile_args function now returns these positions alongside the first-argument register, and all call emit sites attach them to the instruction via Codegen::set_arg_linecols. At runtime, upcall_scope extracts the arg_linecols slice for the UPCALL instruction (looked up by the saved PC) and passes it to Context::upcall_scope, which stores it directly in the Scope. The pending_upcall tuple now carries the UPCALL PC so the slice can be located after execution resumes. --- core2/src/callable.rs | 15 +++ core2/src/compiler/args.rs | 20 +++- core2/src/compiler/codegen.rs | 21 ++-- core2/src/compiler/exprs.rs | 6 +- core2/src/compiler/top.rs | 6 +- core2/src/image.rs | 32 ++++-- core2/src/vm/context.rs | 11 +- core2/src/vm/mod.rs | 183 ++++++++++++++++++++++++++++++++-- 8 files changed, 258 insertions(+), 36 deletions(-) diff --git a/core2/src/callable.rs b/core2/src/callable.rs index c59caf39..0cee430b 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -21,6 +21,7 @@ use crate::bytecode::TaggedRegisterRef; use crate::bytecode::VarArgTag; use crate::mem::{ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u32; +use crate::reader::LineCol; use async_trait::async_trait; use std::borrow::Cow; use std::fmt; @@ -721,9 +722,23 @@ pub struct Scope<'a> { /// Start of the current frame (where the arguments to the upcall start). pub(crate) fp: usize, + + /// Source locations of the call arguments, one per register slot in the argument area. + /// + /// Indexed in the same order as the argument registers: `arg_linecols[N]` is the source + /// position of the expression that was compiled into register slot `N`. May be shorter + /// than the actual argument register count if debug information is unavailable. + pub(crate) arg_linecols: &'a [LineCol], } impl<'a> Scope<'a> { + /// Returns the source position of the argument at `arg`, or `None` if unavailable. + /// + /// `arg` is the register-slot index of the argument, matching the `N` in `scope.get_*(N)`. + pub fn get_pos(&self, arg: u8) -> LineCol { + self.arg_linecols[usize::from(arg)] + } + /// Gets the type tag of the argument at `arg`. pub fn get_type(&self, arg: u8) -> VarArgTag { VarArgTag::parse_u64(self.regs[self.fp + (arg as usize)]).unwrap() diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index f3e0853e..83816871 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -237,13 +237,18 @@ pub(super) fn compile_args( md: CallableMetadata, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, codegen: &mut Codegen, -) -> Result { +) -> Result<(Register, Vec)> { let key_pos = span.vref_pos; let syntax = find_syntax(&md, key_pos, span.args.len())?; let mut scope = symtable.temp_scope(); + // Collects the source position for each register slot allocated below, in allocation order. + // This is used to populate the UPCALL instruction's arg_linecols metadata so that callables + // can query the source position of any argument via `Scope::get_pos`. + let mut arg_linecols: Vec = Vec::new(); + let input_nargs = span.args.len(); let mut arg_iter = span.args.into_iter().peekable(); @@ -258,6 +263,7 @@ pub(super) fn compile_args( None => return Err(Error::CallableSyntax(key_pos, md)), Some(expr) => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); compile_expr_as_type(codegen, symtable, temp_value, expr, details.vtype)?; validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; } @@ -286,6 +292,7 @@ pub(super) fn compile_args( Err(e) => return Err(Error::from_syms(e, span.pos)), }; let temp = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; } @@ -305,10 +312,12 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); let tag = match expr { None => bytecode::VarArgTag::Missing(sep), Some(expr) => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); compile_expr_as_type(codegen, symtable, temp_value, expr, details.vtype)?; bytecode::VarArgTag::Immediate(sep, details.vtype) } @@ -334,10 +343,12 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); let tag = match expr { None => bytecode::VarArgTag::Missing(sep), Some(expr) => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); let etype = compile_expr(codegen, symtable, temp_value, expr)?; bytecode::VarArgTag::Immediate(sep, etype) } @@ -370,6 +381,7 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); let tag = match expr { None => { @@ -382,12 +394,14 @@ pub(super) fn compile_args( Some(expr) => match syn.type_syn { RepeatedTypeSyntax::AnyValue => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); let etype = compile_expr(codegen, symtable, temp_value, expr)?; bytecode::VarArgTag::Immediate(sep, etype) } RepeatedTypeSyntax::TypedValue(vtype) => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); compile_expr_as_type(codegen, symtable, temp_value, expr, vtype)?; bytecode::VarArgTag::Immediate(sep, vtype) } @@ -408,6 +422,7 @@ pub(super) fn compile_args( Err(e) => return Err(Error::from_syms(e, span.pos)), }; let temp = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; + arg_linecols.push(arg_pos); codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); bytecode::VarArgTag::Pointer(sep) } @@ -422,5 +437,6 @@ pub(super) fn compile_args( return Err(Error::CallableSyntax(key_pos, md)); } - scope.first().map_err(|e| Error::from_syms(e, key_pos)) + let first_reg = scope.first().map_err(|e| Error::from_syms(e, key_pos))?; + Ok((first_reg, arg_linecols)) } diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index e20c8de7..7f5445a9 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -19,7 +19,7 @@ use crate::ast::ExprType; use crate::bytecode::{self, Register}; use crate::compiler::ids::HashMapWithIds; use crate::compiler::{Error, Result, SymbolKey}; -use crate::image::{DebugInfo, GlobalVarInfo, Image}; +use crate::image::{DebugInfo, GlobalVarInfo, Image, InstrMetadata}; use crate::mem::ConstantDatum; use crate::reader::LineCol; use std::collections::HashMap; @@ -56,8 +56,8 @@ pub(super) struct Codegen { /// Collection of fixups to apply after code generation. fixups: HashMap, - /// Line/column information for every instruction in `code`. - instr_linecols: Vec, + /// Per-instruction metadata for every instruction in `code`. + instrs: Vec, /// Map of label names to their target addresses. labels: HashMap, @@ -78,10 +78,19 @@ impl Codegen { /// Appends a new instruction `op` generated at `pos` to the code and returns its address. pub(super) fn emit(&mut self, op: u32, pos: LineCol) -> Address { self.code.push(op); - self.instr_linecols.push(pos); + self.instrs.push(InstrMetadata { linecol: pos, arg_linecols: vec![] }); self.code.len() - 1 } + /// Attaches argument source locations to the instruction at `addr`. + /// + /// `arg_linecols` has one entry per register slot in the argument area, in the same order + /// that `compile_args` allocates them. This should be called after emitting a UPCALL or + /// CALL instruction. + pub(super) fn set_arg_linecols(&mut self, addr: Address, arg_linecols: Vec) { + self.instrs[addr].arg_linecols = arg_linecols; + } + /// Emits code to set `reg` to the default value for `vtype`. pub(super) fn emit_default(&mut self, reg: Register, vtype: ExprType, pos: LineCol) { let instr = match vtype { @@ -134,7 +143,7 @@ impl Codegen { /// Applies all registered fixups to the generated code. fn apply_fixups(&mut self) -> Result<()> { for (addr, fixup) in self.fixups.drain() { - let pos = self.instr_linecols[addr]; + let pos = self.instrs[addr].linecol; let instr = match fixup { Fixup::Call(reg, key) => { let target = self.user_callables_addresses.get(&key).expect("Must be present"); @@ -189,7 +198,7 @@ impl Codegen { self.code, self.upcalls.keys_to_vec(), self.constants.keys_to_vec(), - DebugInfo { instr_linecols: self.instr_linecols, callables, global_vars }, + DebugInfo { instrs: self.instrs, callables, global_vars }, )) } } diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 3301ca17..09daf95b 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -187,14 +187,16 @@ pub(super) fn compile_expr( } else { reg }; - compile_args(span, md.clone(), symtable, codegen)?; + let (_first_temp, arg_linecols) = compile_args(span, md.clone(), symtable, codegen)?; if is_user_defined { let addr = codegen.emit(bytecode::make_nop(), key_pos); + codegen.set_arg_linecols(addr, arg_linecols); codegen.add_fixup(addr, Fixup::Call(ret_reg, key)); } else { let upcall = codegen.get_upcall(key, Some(etype), key_pos)?; - codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); + let addr = codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); + codegen.set_arg_linecols(addr, arg_linecols); } if is_global { codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index e8f243bc..edebb3e0 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -172,17 +172,19 @@ fn compile_stmt( let md = md.clone(); define_new_args(&span, &md, symtable, &mut ctx.codegen)?; - let first_temp = { + let (first_temp, arg_linecols) = { let mut symtable = symtable.frozen(); compile_args(span, md, &mut symtable, &mut ctx.codegen)? }; if is_user_defined { let addr = ctx.codegen.emit(bytecode::make_nop(), key_pos); + ctx.codegen.set_arg_linecols(addr, arg_linecols); ctx.codegen.add_fixup(addr, Fixup::Call(first_temp, key)); } else { let upcall = ctx.codegen.get_upcall(key, None, key_pos)?; - ctx.codegen.emit(bytecode::make_upcall(upcall, first_temp), key_pos); + let addr = ctx.codegen.emit(bytecode::make_upcall(upcall, first_temp), key_pos); + ctx.codegen.set_arg_linecols(addr, arg_linecols); } } diff --git a/core2/src/image.rs b/core2/src/image.rs index 902b31b0..ea274d07 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -66,11 +66,23 @@ pub(crate) struct GlobalVarInfo { pub(crate) ndims: usize, } +/// Per-instruction metadata stored in `DebugInfo`. +pub(crate) struct InstrMetadata { + /// Source location that generated this instruction. + pub(crate) linecol: LineCol, + + /// Source locations of the call arguments, if this is a UPCALL instruction. + /// + /// Each entry corresponds to one register slot in the argument area, in the same order + /// that `compile_args` allocates them. Empty for all other instruction types. + pub(crate) arg_linecols: Vec, +} + /// Debugging information for a compiled program. #[derive(Default)] pub struct DebugInfo { - /// Maps each instruction index to its source location (line and column). - pub(crate) instr_linecols: Vec, + /// Per-instruction metadata, one entry per instruction in the image's code. + pub(crate) instrs: Vec, /// Maps instruction addresses to the names of user-defined callables that start at those /// addresses. @@ -115,7 +127,10 @@ impl Default for Image { vec![], vec![], DebugInfo { - instr_linecols: vec![LineCol { line: 0, col: 0 }], + instrs: vec![InstrMetadata { + linecol: LineCol { line: 0, col: 0 }, + arg_linecols: vec![], + }], callables: HashMap::default(), global_vars: HashMap::default(), }, @@ -131,7 +146,7 @@ impl Image { debug_info: DebugInfo, ) -> Self { debug_assert!(!code.is_empty(), "Compiler must ensure the image is not empty"); - debug_assert_eq!(code.len(), debug_info.instr_linecols.len()); + debug_assert_eq!(code.len(), debug_info.instrs.len()); Self { code, upcalls, constants, debug_info, _internal: () } } @@ -139,13 +154,10 @@ impl Image { pub fn disasm(&self) -> Vec { let mut lines = Vec::with_capacity(self.code.len()); - for ((i, instr), pos) in self - .code - .iter() - .copied() - .enumerate() - .zip(self.debug_info.instr_linecols.iter().copied()) + for ((i, instr), meta) in + self.code.iter().copied().enumerate().zip(self.debug_info.instrs.iter()) { + let pos = meta.linecol; if let Some(key) = self.debug_info.callables.get(&i) { lines.push("".to_owned()); lines.push(format!("-- {} ", key)); diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 628807bb..0640adfa 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -21,6 +21,7 @@ use crate::bytecode::{self, Opcode, Register, TaggedRegisterRef, opcode_of}; use crate::image::Image; use crate::mem::{ArrayData, ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u8; +use crate::reader::LineCol; /// Alias for the type representing a program address. type Address = usize; @@ -34,7 +35,9 @@ pub(super) enum InternalStopReason { Exception(Address, String), /// Execution stopped due to an upcall that requires service from the caller. - Upcall(u16, Register), + /// + /// The fields are: upcall index, first argument register, and the PC of the UPCALL instruction. + Upcall(u16, Register, Address), } /// Represents a call frame in the stack. @@ -175,12 +178,13 @@ impl Context { reg: Register, constants: &'a [ConstantDatum], heap: &'a mut Vec, + arg_linecols: &'a [LineCol], ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); let index = usize::from(index); - Scope { regs: &mut self.regs, constants, heap, fp: self.fp + index } + Scope { regs: &mut self.regs, constants, heap, fp: self.fp + index, arg_linecols } } /// Starts or resumes execution of `image`. @@ -495,7 +499,8 @@ impl Context { /// Implements the `Upcall` opcode. pub(super) fn do_upcall(&mut self, instr: u32) { let (index, first_reg) = bytecode::parse_upcall(instr); - self.stop = Some(InternalStopReason::Upcall(index, first_reg)); + let upcall_pc = self.pc; self.pc += 1; + self.stop = Some(InternalStopReason::Upcall(index, first_reg, upcall_pc)); } } diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index b3addcc2..7b9078c9 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -58,12 +58,12 @@ impl<'a> UpcallHandler<'a> { /// Invokes the pending upcall. pub async fn invoke(self) -> CallResult<()> { let vm = self.0; - let (index, first_reg) = vm + let (index, first_reg, upcall_pc) = vm .pending_upcall .take() .expect("This is only reachable when the VM has a pending upcall"); let upcall = vm.upcalls[usize::from(index)].clone(); - upcall.exec(vm.upcall_scope(first_reg)).await + upcall.exec(vm.upcall_scope(first_reg, upcall_pc)).await } } @@ -97,7 +97,10 @@ pub struct Vm { context: Context, /// Details about the pending upcall that has to be handled by the caller. - pending_upcall: Option<(u16, Register)>, + /// + /// The tuple contains the upcall index, the first argument register, and the PC of the + /// UPCALL instruction (for arg position lookup in `DebugInfo`). + pending_upcall: Option<(u16, Register, usize)>, } impl Vm { @@ -131,12 +134,23 @@ impl Vm { } /// Constructs a `Scope` for an upcall with arguments starting at `reg`. - fn upcall_scope<'a>(&'a mut self, reg: Register) -> Scope<'a> { - let constants = match self.image.as_ref() { - Some(image) => image.constants.as_slice(), - None => &[], + /// + /// `upcall_pc` is the address of the UPCALL instruction in the image, used to look up + /// per-argument source locations from `DebugInfo`. + fn upcall_scope<'a>(&'a mut self, reg: Register, upcall_pc: usize) -> Scope<'a> { + let (constants, arg_linecols) = match self.image.as_ref() { + Some(image) => ( + image.constants.as_slice(), + image + .debug_info + .instrs + .get(upcall_pc) + .map(|m| m.arg_linecols.as_slice()) + .unwrap_or(&[]), + ), + None => (&[][..], &[][..]), }; - self.context.upcall_scope(reg, constants, &mut self.heap) + self.context.upcall_scope(reg, constants, &mut self.heap, arg_linecols) } /// Returns the value of the global scalar variable `name` as a `ConstantDatum`. @@ -225,11 +239,11 @@ impl Vm { match self.context.exec(image, &mut self.heap) { InternalStopReason::End(code) => StopReason::End(code), InternalStopReason::Exception(pc, e) => { - let pos = image.debug_info.instr_linecols[pc]; + let pos = image.debug_info.instrs[pc].linecol; StopReason::Exception(pos, e) } - InternalStopReason::Upcall(index, first_reg) => { - self.pending_upcall = Some((index, first_reg)); + InternalStopReason::Upcall(index, first_reg, upcall_pc) => { + self.pending_upcall = Some((index, first_reg, upcall_pc)); StopReason::Upcall(UpcallHandler(self)) } } @@ -239,13 +253,84 @@ impl Vm { #[cfg(test)] mod tests { use super::*; + use crate::ast::{ArgSep, ExprType}; + use crate::callable::{ + ArgSepSyntax, CallResult, CallableMetadata, CallableMetadataBuilder, RequiredValueSyntax, + SingularArgSyntax, + }; use crate::compiler::{SymbolKey, compile, only_metadata}; use crate::image::Image; + use crate::reader::LineCol; use crate::testutils::OutCommand; + use async_trait::async_trait; + use std::borrow::Cow; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; + /// A test callable that captures the source positions of argument register slots. + /// + /// On each invocation, records the result of `scope.get_pos(n)` for `0..nargs` into + /// `positions`. + struct PosCapture { + metadata: CallableMetadata, + nargs: u8, + positions: Rc>>, + } + + impl PosCapture { + /// Creates a new `PosCapture` callable named `POS_CAPTURE` that expects + /// `nargs` required integer arguments separated by commas. + fn new(nargs: u8, positions: Rc>>) -> Rc { + let singular: Vec = (0..nargs) + .map(|i| { + let sep = if i == nargs - 1 { + ArgSepSyntax::End + } else { + ArgSepSyntax::Exactly(ArgSep::Long) + }; + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Integer, + }, + sep, + ) + }) + .collect(); + let md = CallableMetadataBuilder::new("POS_CAPTURE") + .with_dynamic_syntax(vec![(singular, None)]) + .test_build(); + Rc::from(Self { metadata: md, nargs, positions }) + } + } + + #[async_trait(?Send)] + impl Callable for PosCapture { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let mut positions = self.positions.borrow_mut(); + for i in 0..self.nargs { + positions.push(scope.get_pos(i)); + } + Ok(()) + } + } + + /// Runs the VM to completion, invoking every upcall as it is encountered. + async fn run_to_end(vm: &mut Vm) { + loop { + match vm.exec() { + StopReason::End(_) => break, + StopReason::Exception(_, msg) => panic!("Unexpected exception: {}", msg), + StopReason::Upcall(handler) => handler.invoke().await.unwrap(), + } + } + } + #[test] fn test_exec_without_load_is_eof() { let mut vm = Vm::new(HashMap::default()); @@ -334,4 +419,80 @@ mod tests { _ => panic!("Unexpected stop reason"), } } + + #[tokio::test] + async fn test_scope_get_pos_no_args() { + let positions: Rc>> = Rc::default(); + let cmd = PosCapture::new(0, positions.clone()); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); + + let image = + compile(&mut b"POS_CAPTURE".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + run_to_end(&mut vm).await; + + let pos = positions.borrow(); + assert_eq!(&[] as &[LineCol], pos.as_slice()); + } + + #[tokio::test] + async fn test_scope_get_pos_single_arg() { + let positions: Rc>> = Rc::default(); + let cmd = PosCapture::new(1, positions.clone()); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); + + let image = + compile(&mut b"POS_CAPTURE 42".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + run_to_end(&mut vm).await; + + let pos = positions.borrow(); + assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice()); + } + + #[tokio::test] + async fn test_scope_get_pos_multiple_args() { + let positions: Rc>> = Rc::default(); + let cmd = PosCapture::new(3, positions.clone()); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); + + let image = + compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), &only_metadata(&upcalls_by_name)) + .unwrap(); + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + run_to_end(&mut vm).await; + + let pos = positions.borrow(); + assert_eq!( + &[ + LineCol { line: 1, col: 13 }, + LineCol { line: 1, col: 16 }, + LineCol { line: 1, col: 19 } + ], + pos.as_slice() + ); + } + + #[tokio::test] + async fn test_scope_get_pos_expression_arg() { + let positions: Rc>> = Rc::default(); + let cmd = PosCapture::new(1, positions.clone()); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); + + let image = compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), &only_metadata(&upcalls_by_name)) + .unwrap(); + let mut vm = Vm::new(upcalls_by_name); + vm.load(image); + run_to_end(&mut vm).await; + + let pos = positions.borrow(); + assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice()); + } } From 78e47cdd32f802606bb1117332f836d5af7732a8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 26 Feb 2026 16:12:05 -0800 Subject: [PATCH 15/53] core2: Allow initializing pre-declared globals Make GlobalDefKind::Scalar variant accept an optional initial value so callers can supply a non-default starting value for any pre-declared scalar global. When None, behaviour is identical to before (zero for numerics, empty string for text). To demonstrate this feature, update the config.rs example. --- core2/examples/config.rs | 33 +++++++-- core2/src/compiler/codegen.rs | 31 +++++++++ core2/src/compiler/exprs.rs | 21 ++---- core2/src/compiler/top.rs | 122 ++++++++++++++++++++++++++++++++-- core2/tests/config.out | 1 + 5 files changed, 180 insertions(+), 28 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 9867966b..3818f77a 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -37,26 +37,43 @@ results%(2) = 30 DIM SHARED defined_within defined_within = 42 + +injected_value% = injected_value% + 1 "#; fn main() { // Describe the global variables the script is expected to read and write. // Note that "optional_flag" is declared so the script could set it, but the // script does not assign it. Its value will be the zero default. + // "injected_value" is pre-initialized to a value that we expect the script + // to read and modify. let global_defs = vec![ - GlobalDef { name: "foo_value".to_owned(), kind: GlobalDefKind::Scalar(ExprType::Integer) }, + GlobalDef { + name: "foo_value".to_owned(), + kind: GlobalDefKind::Scalar { etype: ExprType::Integer, initial_value: None }, + }, GlobalDef { name: "result_total".to_owned(), - kind: GlobalDefKind::Scalar(ExprType::Integer), + kind: GlobalDefKind::Scalar { etype: ExprType::Integer, initial_value: None }, + }, + GlobalDef { + name: "status".to_owned(), + kind: GlobalDefKind::Scalar { etype: ExprType::Text, initial_value: None }, }, - GlobalDef { name: "status".to_owned(), kind: GlobalDefKind::Scalar(ExprType::Text) }, GlobalDef { name: "results".to_owned(), kind: GlobalDefKind::Array { subtype: ExprType::Integer, dimensions: vec![3] }, }, GlobalDef { name: "optional_flag".to_owned(), - kind: GlobalDefKind::Scalar(ExprType::Boolean), + kind: GlobalDefKind::Scalar { etype: ExprType::Boolean, initial_value: None }, + }, + GlobalDef { + name: "injected_value".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Integer, + initial_value: Some(ConstantDatum::Integer(5)), + }, }, ]; @@ -120,6 +137,14 @@ fn main() { Ok(None) => println!("optional_flag? is not declared"), Err(e) => println!("optional_flag?: error: {}", e), } + // injected_value was pre-initialized to 5 before compilation. The script incremented + // it by 1, so we expect 6 here. + match vm.get_global("injected_value") { + Ok(Some(ConstantDatum::Integer(v))) => println!("injected_value% = {}", v), + Ok(Some(other)) => println!("injected_value% has unexpected type: {:?}", other), + Ok(None) => println!("injected_value% is not set"), + Err(e) => println!("injected_value%: error: {}", e), + } // "unknown" was never declared at all, so get_global returns Ok(None). match vm.get_global("unknown") { Ok(Some(v)) => println!("unknown = {:?}", v), diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 7f5445a9..8e4e5670 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -102,6 +102,37 @@ impl Codegen { self.emit(instr, pos); } + /// Emits code to set `reg` to the specific `datum` value. + pub(super) fn emit_value( + &mut self, + reg: Register, + datum: ConstantDatum, + pos: LineCol, + ) -> Result<()> { + match datum { + ConstantDatum::Boolean(b) => { + self.emit(bytecode::make_load_integer(reg, if b { 1 } else { 0 }), pos); + } + ConstantDatum::Integer(i) => { + if let Ok(v) = u16::try_from(i) { + self.emit(bytecode::make_load_integer(reg, v), pos); + } else { + let idx = self.get_constant(ConstantDatum::Integer(i), pos)?; + self.emit(bytecode::make_load_constant(reg, idx), pos); + } + } + ConstantDatum::Double(d) => { + let idx = self.get_constant(ConstantDatum::Double(d), pos)?; + self.emit(bytecode::make_load_constant(reg, idx), pos); + } + ConstantDatum::Text(s) => { + let idx = self.get_constant(ConstantDatum::Text(s), pos)?; + self.emit(bytecode::make_load_integer(reg, idx), pos); + } + } + Ok(()) + } + /// Records a `fixup` that needs to be applied at `addr`. pub(super) fn add_fixup(&mut self, addr: usize, fixup: Fixup) { let previous = self.fixups.insert(addr, fixup); diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 09daf95b..125acb10 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -22,7 +22,6 @@ use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::syms::{self, SymbolKey, SymbolPrototype, TempScope, TempSymtable}; use crate::compiler::{Error, Result}; use crate::mem::ConstantDatum; -use std::convert::TryFrom; /// Compiles `exprs` into consecutive integer registers allocated from `scope` and returns the /// first register. The caller must guarantee that `exprs` is non-empty. @@ -140,8 +139,7 @@ pub(super) fn compile_expr( Expr::Add(span) => compile_arithmetic_binary_op(codegen, symtable, reg, *span, "+"), Expr::Boolean(span) => { - let value = if span.value { 1 } else { 0 }; - codegen.emit(bytecode::make_load_integer(reg, value), span.pos); + codegen.emit_value(reg, ConstantDatum::Boolean(span.value), span.pos)?; Ok(ExprType::Boolean) } @@ -206,22 +204,12 @@ pub(super) fn compile_expr( } Expr::Double(span) => { - let index = codegen.get_constant(ConstantDatum::Double(span.value), span.pos)?; - codegen.emit(bytecode::make_load_constant(reg, index), span.pos); + codegen.emit_value(reg, ConstantDatum::Double(span.value), span.pos)?; Ok(ExprType::Double) } Expr::Integer(span) => { - match u16::try_from(span.value) { - Ok(i) => { - codegen.emit(bytecode::make_load_integer(reg, i), span.pos); - } - Err(_) => { - let index = - codegen.get_constant(ConstantDatum::Integer(span.value), span.pos)?; - codegen.emit(bytecode::make_load_constant(reg, index), span.pos); - } - } + codegen.emit_value(reg, ConstantDatum::Integer(span.value), span.pos)?; Ok(ExprType::Integer) } @@ -289,8 +277,7 @@ pub(super) fn compile_expr( }, Expr::Text(span) => { - let index = codegen.get_constant(ConstantDatum::Text(span.value), span.pos)?; - codegen.emit(bytecode::make_load_integer(reg, index), span.pos); + codegen.emit_value(reg, ConstantDatum::Text(span.value), span.pos)?; Ok(ExprType::Text) } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index edebb3e0..d5757d67 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -438,11 +438,16 @@ pub struct GlobalDef { /// Kind of a pre-defined global variable. pub enum GlobalDefKind { - /// A scalar (non-array) variable with the given element type. - /// - /// The variable is initialized to its default value: `0` for numeric types and an empty - /// string for `Text`. - Scalar(ExprType), + /// A scalar (non-array) variable. + Scalar { + /// Type of the scalar variable. + etype: ExprType, + + /// Initial value for the variable. If `None`, the variable is initialized to its default + /// value: `0` for numeric types and an empty string for `Text`. The type of the datum + /// must match `etype`. + initial_value: Option, + }, /// A multidimensional array with the given element type and fixed dimension sizes. /// @@ -488,11 +493,19 @@ fn prepare_globals( array_globals.push((reg, def)); } - GlobalDefKind::Scalar(etype) => { + GlobalDefKind::Scalar { etype, initial_value } => { let reg = symtable .put_global(key, SymbolPrototype::Scalar(*etype)) .map_err(|e| Error::from_syms(e, preamble_pos))?; - ctx.codegen.emit_default(reg, *etype, preamble_pos); + match initial_value { + Some(datum) => { + if datum.etype() != *etype { + return Err(Error::TypeMismatch(preamble_pos, datum.etype(), *etype)); + } + ctx.codegen.emit_value(reg, datum.clone(), preamble_pos)?; + } + None => ctx.codegen.emit_default(reg, *etype, preamble_pos), + } } } } @@ -571,3 +584,98 @@ pub fn compile( ) -> Result { compile_with_globals(input, upcalls, &[]) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::ast::ExprType; + use crate::mem::ConstantDatum; + use crate::vm::{StopReason, Vm}; + + fn compile_and_get_global(defs: &[GlobalDef], name: &str) -> ConstantDatum { + let image = compile_with_globals(&mut "".as_bytes(), &HashMap::default(), defs) + .expect("compilation should succeed"); + let mut vm = Vm::new(HashMap::default()); + vm.load(image); + match vm.exec() { + StopReason::End(0) => {} + StopReason::End(code) => panic!("unexpected exit code: {code}"), + StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"), + StopReason::Upcall(_) => panic!("unexpected upcall"), + } + vm.get_global(name).expect("get_global failed").expect("global not found") + } + + #[test] + fn test_inject_boolean() { + let defs = vec![GlobalDef { + name: "b".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Boolean, + initial_value: Some(ConstantDatum::Boolean(true)), + }, + }]; + assert_eq!(ConstantDatum::Boolean(true), compile_and_get_global(&defs, "b")); + } + + #[test] + fn test_inject_integer() { + let defs = vec![GlobalDef { + name: "n".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Integer, + initial_value: Some(ConstantDatum::Integer(42)), + }, + }]; + assert_eq!(ConstantDatum::Integer(42), compile_and_get_global(&defs, "n")); + } + + #[test] + fn test_inject_integer_large() { + let defs = vec![GlobalDef { + name: "n".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Integer, + initial_value: Some(ConstantDatum::Integer(70000)), + }, + }]; + assert_eq!(ConstantDatum::Integer(70000), compile_and_get_global(&defs, "n")); + } + + #[test] + fn test_inject_double() { + let defs = vec![GlobalDef { + name: "d".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Double, + initial_value: Some(ConstantDatum::Double(1.5)), + }, + }]; + assert_eq!(ConstantDatum::Double(1.5), compile_and_get_global(&defs, "d")); + } + + #[test] + fn test_inject_text() { + let defs = vec![GlobalDef { + name: "s".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Text, + initial_value: Some(ConstantDatum::Text("hello".to_owned())), + }, + }]; + assert_eq!(ConstantDatum::Text("hello".to_owned()), compile_and_get_global(&defs, "s"),); + } + + #[test] + fn test_inject_type_mismatch() { + let defs = vec![GlobalDef { + name: "n".to_owned(), + kind: GlobalDefKind::Scalar { + etype: ExprType::Integer, + initial_value: Some(ConstantDatum::Double(1.5)), + }, + }]; + let result = compile_with_globals(&mut "".as_bytes(), &HashMap::default(), &defs); + assert!(matches!(result, Err(Error::TypeMismatch(..)))); + } +} diff --git a/core2/tests/config.out b/core2/tests/config.out index 1c014c10..c02d6f97 100644 --- a/core2/tests/config.out +++ b/core2/tests/config.out @@ -3,6 +3,7 @@ result_total% = 579 status$ = "Processing complete" defined_within% = 42 optional_flag? = false +injected_value% = 6 unknown is not declared results%(0) = 10 results%(1) = 20 From caa1f0535039a302e0c0b292114f47acbeefbdfa Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 27 Feb 2026 19:33:20 -0800 Subject: [PATCH 16/53] core2: Avoid deep recursion in long expressions The expression compiler used recursive descent, which meant that a long flat chain of arithmetic operators (e.g. a + b + c + ... + z) would overflow the call stack for chains of ~200 operations. Fix this by iteratively peeling left-recursive arithmetic binary op chains in compile_expr: instead of recursing on the lhs at each level, collect all the rhs operands into a Vec and then fold them left-to-right using the destination register as the accumulator. Do the same for Expr::start_pos, which was also recursive over binary op chains. As a side effect, this reduces temporary register pressure: a flat chain of N binary operations now uses a single temporary register rather than N, so the "Out of temp registers" tests that depended on that old behavior are removed. Replace them with a new test that still exhibits this behavior. --- core2/src/ast.rs | 65 ++++---- core2/src/compiler/exprs.rs | 234 ++++++++++++++++----------- core2/tests/test_arithmetic_add.md | 99 +++++++++--- core2/tests/test_arrays.md | 18 +-- core2/tests/test_functions.md | 56 +++---- core2/tests/test_out_of_registers.md | 20 +-- core2/tests/test_strings.md | 12 +- 7 files changed, 289 insertions(+), 215 deletions(-) diff --git a/core2/src/ast.rs b/core2/src/ast.rs index 39e64230..6c6154eb 100644 --- a/core2/src/ast.rs +++ b/core2/src/ast.rs @@ -156,38 +156,39 @@ pub enum Expr { impl Expr { /// Returns the start position of the expression. pub fn start_pos(&self) -> LineCol { - match self { - Expr::Boolean(span) => span.pos, - Expr::Double(span) => span.pos, - Expr::Integer(span) => span.pos, - Expr::Text(span) => span.pos, - - Expr::Symbol(span) => span.pos, - - Expr::And(span) => span.lhs.start_pos(), - Expr::Or(span) => span.lhs.start_pos(), - Expr::Xor(span) => span.lhs.start_pos(), - Expr::Not(span) => span.pos, - - Expr::ShiftLeft(span) => span.lhs.start_pos(), - Expr::ShiftRight(span) => span.lhs.start_pos(), - - Expr::Equal(span) => span.lhs.start_pos(), - Expr::NotEqual(span) => span.lhs.start_pos(), - Expr::Less(span) => span.lhs.start_pos(), - Expr::LessEqual(span) => span.lhs.start_pos(), - Expr::Greater(span) => span.lhs.start_pos(), - Expr::GreaterEqual(span) => span.lhs.start_pos(), - - Expr::Add(span) => span.lhs.start_pos(), - Expr::Subtract(span) => span.lhs.start_pos(), - Expr::Multiply(span) => span.lhs.start_pos(), - Expr::Divide(span) => span.lhs.start_pos(), - Expr::Modulo(span) => span.lhs.start_pos(), - Expr::Power(span) => span.lhs.start_pos(), - Expr::Negate(span) => span.pos, - - Expr::Call(span) => span.vref_pos, + let mut expr = self; + loop { + match expr { + Expr::Boolean(span) => return span.pos, + Expr::Double(span) => return span.pos, + Expr::Integer(span) => return span.pos, + Expr::Text(span) => return span.pos, + + Expr::Symbol(span) => return span.pos, + + Expr::Not(span) => return span.pos, + Expr::Negate(span) => return span.pos, + + Expr::Call(span) => return span.vref_pos, + + Expr::Add(span) + | Expr::And(span) + | Expr::Divide(span) + | Expr::Equal(span) + | Expr::Greater(span) + | Expr::GreaterEqual(span) + | Expr::Less(span) + | Expr::LessEqual(span) + | Expr::Modulo(span) + | Expr::Multiply(span) + | Expr::NotEqual(span) + | Expr::Or(span) + | Expr::Power(span) + | Expr::ShiftLeft(span) + | Expr::ShiftRight(span) + | Expr::Subtract(span) + | Expr::Xor(span) => expr = &span.lhs, + } } } } diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 125acb10..c28c13ee 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -15,13 +15,14 @@ //! Functions to convert expressions into bytecode. -use crate::ast::{BinaryOpSpan, Expr, ExprType}; +use crate::ast::{Expr, ExprType}; use crate::bytecode::{self, Register, RegisterScope}; use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::syms::{self, SymbolKey, SymbolPrototype, TempScope, TempSymtable}; use crate::compiler::{Error, Result}; use crate::mem::ConstantDatum; +use crate::reader::LineCol; /// Compiles `exprs` into consecutive integer registers allocated from `scope` and returns the /// first register. The caller must guarantee that `exprs` is non-empty. @@ -29,7 +30,7 @@ pub(super) fn compile_integer_exprs( codegen: &mut Codegen, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, scope: &mut TempScope, - pos: crate::reader::LineCol, + pos: LineCol, exprs: impl Iterator, ) -> Result { let mut first_reg = None; @@ -48,7 +49,7 @@ fn compile_array_access( codegen: &mut Codegen, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, reg: Register, - key_pos: crate::reader::LineCol, + key_pos: LineCol, arr_reg: Register, info: &syms::ArrayInfo, args: Vec, @@ -69,74 +70,112 @@ fn compile_array_access( Ok(info.subtype) } -/// Compiles an arithmetic binary operation `span` that returns its value into `reg`. -fn compile_arithmetic_binary_op( - codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, - reg: Register, - span: BinaryOpSpan, +/// A pending arithmetic binary operation waiting to be applied, used to flatten +/// expressions and avoid recursive calls during processing. +struct PendingBinaryOp { + pos: LineCol, + rhs: Expr, op_name: &'static str, -) -> Result { - let mut scope = symtable.temp_scope(); - - let lpos = span.lhs.start_pos(); - let ltemp = scope.alloc().map_err(|e| Error::from_syms(e, lpos))?; - let ltype = compile_expr(codegen, symtable, ltemp, span.lhs)?; - - let rpos = span.rhs.start_pos(); - let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; - let rtype = compile_expr(codegen, symtable, rtemp, span.rhs)?; - - let rtype = match (ltype, rtype) { - // Type-compatible operands. - (ExprType::Double, ExprType::Double) => ExprType::Double, - (ExprType::Integer, ExprType::Integer) => ExprType::Integer, - (ExprType::Text, ExprType::Text) if op_name == "+" => ExprType::Text, - - // Operands requiring type promotion. - (ExprType::Double, ExprType::Integer) => { - codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); - ExprType::Double - } - (ExprType::Integer, ExprType::Double) => { - codegen.emit(bytecode::make_integer_to_double(ltemp), lpos); - ExprType::Double - } + make_double: fn(Register, Register, Register) -> u32, + make_integer: fn(Register, Register, Register) -> u32, + make_text: Option u32>, +} - // Unsupported operand types. - _ => { - return Err(Error::BinaryOpType(span.pos, "+", ltype, rtype)); +/// Peels the left-recursive chain of arithmetic binary ops into a vector of pending +/// binary ops to avoid deep recursion. +/// +/// Returns the input `expr` holding the leftmost non-binary expression and the list +/// of pending ops. +fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { + let mut pending: Vec = vec![]; + #[allow(clippy::while_let_loop)] + loop { + match expr { + Expr::Add(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "+", + make_double: bytecode::make_add_double, + make_integer: bytecode::make_add_integer, + make_text: Some(bytecode::make_concat), + }); + expr = span.lhs; + } + _ => break, } - }; - - match rtype { - ExprType::Boolean => unreachable!(), + } + (expr, pending) +} - ExprType::Double => { - codegen.emit(bytecode::make_add_double(reg, ltemp, rtemp), span.pos); - } +/// Processes `pending` arithmetic binary ops from innermost to outermost, using +/// `reg` as the accumulator. +/// +/// This avoids the deep recursion that would arise if we compiled binary op chains +/// by recursing on the lhs. +fn compile_pending_ops( + codegen: &mut Codegen, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + reg: Register, + mut etype: ExprType, + mut pending: Vec, +) -> Result { + while let Some(op) = pending.pop() { + let rpos = op.rhs.start_pos(); + let mut scope = symtable.temp_scope(); + let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; + let rtype = compile_expr(codegen, symtable, rtemp, op.rhs)?; + + let result_type = match (etype, rtype) { + (ExprType::Double, ExprType::Double) => ExprType::Double, + (ExprType::Integer, ExprType::Integer) => ExprType::Integer, + (ExprType::Text, ExprType::Text) if op.make_text.is_some() => ExprType::Text, + (ExprType::Double, ExprType::Integer) => { + codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); + ExprType::Double + } + (ExprType::Integer, ExprType::Double) => { + codegen.emit(bytecode::make_integer_to_double(reg), op.pos); + ExprType::Double + } + _ => return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)), + }; - ExprType::Integer => { - codegen.emit(bytecode::make_add_integer(reg, ltemp, rtemp), span.pos); + match result_type { + ExprType::Boolean => unreachable!(), + ExprType::Double => { + codegen.emit((op.make_double)(reg, reg, rtemp), op.pos); + } + ExprType::Integer => { + codegen.emit((op.make_integer)(reg, reg, rtemp), op.pos); + } + ExprType::Text => { + codegen.emit(op.make_text.unwrap()(reg, reg, rtemp), op.pos); + } } - ExprType::Text => { - codegen.emit(bytecode::make_concat(reg, ltemp, rtemp), span.pos); - } + etype = result_type; } - Ok(rtype) + Ok(etype) } /// Compiles a single expression `expr` and leaves its value in `reg`. +/// +/// For left-recursive arithmetic binary operations (like `a + b + c`), this function +/// iterates rather than recurses so that very long expression chains do not overflow +/// the call stack. pub(super) fn compile_expr( codegen: &mut Codegen, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, reg: Register, expr: Expr, ) -> Result { - match expr { - Expr::Add(span) => compile_arithmetic_binary_op(codegen, symtable, reg, *span, "+"), + let (expr, pending) = peel_binary_ops(expr); + + let etype = match expr { + Expr::Add(..) => unreachable!("Peeled by peel_binary_ops"), Expr::Boolean(span) => { codegen.emit_value(reg, ConstantDatum::Boolean(span.value), span.pos)?; @@ -147,13 +186,49 @@ pub(super) fn compile_expr( let key = SymbolKey::from(&span.vref.name); let key_pos = span.vref_pos; - let Some(md) = symtable.get_callable(&key) else { + if let Some(md) = symtable.get_callable(&key) { + let Some(etype) = md.return_type() else { + return Err(Error::NotAFunction(span.vref_pos, span.vref)); + }; + + if md.is_argless() { + return Err(Error::CallableSyntax(span.vref_pos, md.clone())); + } + + let is_user_defined = md.is_user_defined(); + let (is_global, _index) = reg.to_parts(); + let mut alloc = symtable.temp_scope(); + let ret_reg = if is_global { + // The call instruction can only carry one register, and this register + // indicates where to store the result and where arguments start. So, + // if we are going to save the result to a global register, we must + // allocate a temp register first so that argument passing can work. + alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? + } else { + reg + }; + let (_first_temp, arg_linecols) = + compile_args(span, md.clone(), symtable, codegen)?; + + if is_user_defined { + let addr = codegen.emit(bytecode::make_nop(), key_pos); + codegen.set_arg_linecols(addr, arg_linecols); + codegen.add_fixup(addr, Fixup::Call(ret_reg, key)); + } else { + let upcall = codegen.get_upcall(key, Some(etype), key_pos)?; + let addr = codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); + codegen.set_arg_linecols(addr, arg_linecols); + } + if is_global { + codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); + } + + Ok(etype) + } else { match symtable.get_local_or_global(&span.vref) { - Ok((arr_reg, SymbolPrototype::Array(info))) => { - return compile_array_access( - codegen, symtable, reg, key_pos, arr_reg, &info, span.args, - ); - } + Ok((arr_reg, SymbolPrototype::Array(info))) => compile_array_access( + codegen, symtable, reg, key_pos, arr_reg, &info, span.args, + ), Err(syms::Error::UndefinedSymbol(..)) | Ok((_, SymbolPrototype::Scalar(_))) => { return Err(Error::UndefinedSymbol( span.vref_pos, @@ -163,44 +238,7 @@ pub(super) fn compile_expr( } Err(e) => return Err(Error::from_syms(e, key_pos)), } - }; - - let Some(etype) = md.return_type() else { - return Err(Error::NotAFunction(span.vref_pos, span.vref)); - }; - - if md.is_argless() { - return Err(Error::CallableSyntax(span.vref_pos, md.clone())); - } - - let is_user_defined = md.is_user_defined(); - let (is_global, _index) = reg.to_parts(); - let mut alloc = symtable.temp_scope(); - let ret_reg = if is_global { - // The call instruction can only carry one register, and this register - // indicates where to store the result and where arguments start. So, - // if we are going to save the result to a global register, we must - // allocate a temp register first so that argument passing can work. - alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? - } else { - reg - }; - let (_first_temp, arg_linecols) = compile_args(span, md.clone(), symtable, codegen)?; - - if is_user_defined { - let addr = codegen.emit(bytecode::make_nop(), key_pos); - codegen.set_arg_linecols(addr, arg_linecols); - codegen.add_fixup(addr, Fixup::Call(ret_reg, key)); - } else { - let upcall = codegen.get_upcall(key, Some(etype), key_pos)?; - let addr = codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); - codegen.set_arg_linecols(addr, arg_linecols); - } - if is_global { - codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); } - - Ok(etype) } Expr::Double(span) => { @@ -282,7 +320,9 @@ pub(super) fn compile_expr( } _ => todo!(), - } + }?; + + compile_pending_ops(codegen, symtable, reg, etype, pending) } /// Compiles a single expression, expecting it to be of a `target` type. Applies casts if diff --git a/core2/tests/test_arithmetic_add.md b/core2/tests/test_arithmetic_add.md index 102ec627..8c397f1f 100644 --- a/core2/tests/test_arithmetic_add.md +++ b/core2/tests/test_arithmetic_add.md @@ -9,10 +9,10 @@ OUT 4.5 + 2.3 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADC R66, 0 # 1:5 -0002: LOADC R67, 1 # 1:11 -0003: ADDD R65, R66, R67 # 1:9 +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: ADDD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT 0006: LOADI R64, 0 # 0:0 @@ -36,10 +36,10 @@ OUT 2 + 3 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R66, 2 # 1:5 -0002: LOADI R67, 3 # 1:9 -0003: ADDI R65, R66, R67 # 1:7 +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 3 # 1:9 +0003: ADDI R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT 0006: LOADI R64, 0 # 0:0 @@ -63,10 +63,10 @@ OUT "a" + "b" ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R66, 0 # 1:5 -0002: LOADI R67, 1 # 1:11 -0003: CONCAT R65, R66, R67 # 1:9 +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:11 +0003: CONCAT R65, R65, R66 # 1:9 0004: LOADI R64, 259 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT 0006: LOADI R64, 0 # 0:0 @@ -90,11 +90,11 @@ OUT 2 + 8.3 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R66, 2 # 1:5 -0002: LOADC R67, 0 # 1:9 -0003: ITOD R66 # 1:5 -0004: ADDD R65, R66, R67 # 1:7 +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: ADDD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT 0007: LOADI R64, 0 # 0:0 @@ -118,11 +118,11 @@ OUT 8.3 + 2 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADC R66, 0 # 1:5 -0002: LOADI R67, 2 # 1:11 -0003: ITOD R67 # 1:11 -0004: ADDD R65, R66, R67 # 1:9 +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 2 # 1:11 +0003: ITOD R66 # 1:11 +0004: ADDD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT 0007: LOADI R64, 0 # 0:0 @@ -146,10 +146,10 @@ a = 2147483640 + 20 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 20 # 1:18 -0003: ADDI R64, R65, R66 # 1:16 +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:5 +0002: LOADI R65, 20 # 1:18 +0003: ADDI R64, R64, R65 # 1:16 0004: LOADI R65, 0 # 0:0 0005: END R65 # 0:0 ``` @@ -159,3 +159,50 @@ a = 2147483640 + 20 ```plain 1:16: Integer overflow ``` + +# Test: Array subscripts in addition chain + +## Source + +```basic +DIM a(3) AS INTEGER +a(0) = 10 +a(1) = 20 +a(2) = 30 +OUT a(0) + a(1) + a(2) +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 10 # 2:8 +0004: LOADI R66, 0 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R65, 20 # 3:8 +0007: LOADI R66, 1 # 3:3 +0008: STOREA R64, R65, R66 # 3:1 +0009: LOADI R65, 30 # 4:8 +0010: LOADI R66, 2 # 4:3 +0011: STOREA R64, R65, R66 # 4:1 +0012: LOADI R67, 0 # 5:7 +0013: LOADA R66, R64, R67 # 5:5 +0014: LOADI R68, 1 # 5:14 +0015: LOADA R67, R64, R68 # 5:12 +0016: ADDI R66, R66, R67 # 5:10 +0017: LOADI R68, 2 # 5:21 +0018: LOADA R67, R64, R68 # 5:19 +0019: ADDI R66, R66, R67 # 5:17 +0020: LOADI R65, 258 # 5:5 +0021: UPCALL 0, R65 # 5:1, OUT +0022: LOADI R65, 0 # 0:0 +0023: END R65 # 0:0 +``` + +## Output + +```plain +0=60% +``` diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index 5d9e8dce..173b9107 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -745,7 +745,7 @@ OUT sum(0) -- SUM 0007: LOADI R64, 0 # 1:10 -0008: ENTER 7 # 0:0 +0008: ENTER 5 # 0:0 0009: LOADI R67, 3 # 2:11 0010: ALLOCA R66, [1]%, R67 # 2:9 0011: LOADI R67, 10 # 3:12 @@ -757,14 +757,14 @@ OUT sum(0) 0017: LOADI R67, 30 # 5:12 0018: LOADI R68, 2 # 5:7 0019: STOREA R66, R67, R68 # 5:5 -0020: LOADI R69, 0 # 6:13 -0021: LOADA R68, R66, R69 # 6:11 -0022: LOADI R70, 1 # 6:20 -0023: LOADA R69, R66, R70 # 6:18 -0024: ADDI R67, R68, R69 # 6:16 -0025: LOADI R69, 2 # 6:27 -0026: LOADA R68, R66, R69 # 6:25 -0027: ADDI R64, R67, R68 # 6:23 +0020: LOADI R67, 0 # 6:13 +0021: LOADA R64, R66, R67 # 6:11 +0022: LOADI R68, 1 # 6:20 +0023: LOADA R67, R66, R68 # 6:18 +0024: ADDI R64, R64, R67 # 6:16 +0025: LOADI R68, 2 # 6:27 +0026: LOADA R67, R66, R68 # 6:25 +0027: ADDI R64, R64, R67 # 6:23 0028: RETURN # 7:1 ``` diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index bbd08654..6cd4d366 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -366,14 +366,14 @@ OUT add(3, 5) + add(10, 20) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R67, 3 # 5:9 -0002: LOADI R68, 5 # 5:12 -0003: CALL R66, 12 # 5:5, ADD -0004: LOADI R68, 10 # 5:21 -0005: LOADI R69, 20 # 5:25 -0006: CALL R67, 12 # 5:17, ADD -0007: ADDI R65, R66, R67 # 5:15 +0000: ENTER 5 # 0:0 +0001: LOADI R66, 3 # 5:9 +0002: LOADI R67, 5 # 5:12 +0003: CALL R65, 12 # 5:5, ADD +0004: LOADI R67, 10 # 5:21 +0005: LOADI R68, 20 # 5:25 +0006: CALL R66, 12 # 5:17, ADD +0007: ADDI R65, R65, R66 # 5:15 0008: LOADI R64, 258 # 5:5 0009: UPCALL 0, R64 # 5:1, OUT 0010: LOADI R64, 0 # 0:0 @@ -381,10 +381,10 @@ OUT add(3, 5) + add(10, 20) -- ADD 0012: LOADI R64, 0 # 1:10 -0013: ENTER 5 # 0:0 -0014: MOVE R67, R65 # 2:11 -0015: MOVE R68, R66 # 2:15 -0016: ADDI R64, R67, R68 # 2:13 +0013: ENTER 4 # 0:0 +0014: MOVE R64, R65 # 2:11 +0015: MOVE R67, R66 # 2:15 +0016: ADDI R64, R64, R67 # 2:13 0017: RETURN # 3:1 ``` @@ -424,10 +424,10 @@ OUT ret -- FOO 0010: LOADI R64, 0 # 1:10 -0011: ENTER 4 # 0:0 -0012: LOADI R66, 42 # 2:11 -0013: MOVE R67, R65 # 2:16 -0014: ADDI R64, R66, R67 # 2:14 +0011: ENTER 3 # 0:0 +0012: LOADI R64, 42 # 2:11 +0013: MOVE R66, R65 # 2:16 +0014: ADDI R64, R64, R66 # 2:14 0015: RETURN # 3:1 ``` @@ -723,18 +723,18 @@ OUT SUM_DOUBLES(1.0, 2.0) + SUM_DOUBLES(3.0, 4.0) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADC R68, 0 # 1:17 -0002: LOADI R67, 289 # 1:17 -0003: LOADC R70, 1 # 1:22 -0004: LOADI R69, 257 # 1:22 -0005: UPCALL 0, R66 # 1:5, SUM_DOUBLES -0006: LOADC R69, 2 # 1:41 -0007: LOADI R68, 289 # 1:41 -0008: LOADC R71, 3 # 1:46 -0009: LOADI R70, 257 # 1:46 -0010: UPCALL 0, R67 # 1:29, SUM_DOUBLES -0011: ADDD R65, R66, R67 # 1:27 +0000: ENTER 7 # 0:0 +0001: LOADC R67, 0 # 1:17 +0002: LOADI R66, 289 # 1:17 +0003: LOADC R69, 1 # 1:22 +0004: LOADI R68, 257 # 1:22 +0005: UPCALL 0, R65 # 1:5, SUM_DOUBLES +0006: LOADC R68, 2 # 1:41 +0007: LOADI R67, 289 # 1:41 +0008: LOADC R70, 3 # 1:46 +0009: LOADI R69, 257 # 1:46 +0010: UPCALL 0, R66 # 1:29, SUM_DOUBLES +0011: ADDD R65, R65, R66 # 1:27 0012: LOADI R64, 257 # 1:5 0013: UPCALL 1, R64 # 1:1, OUT 0014: LOADI R64, 0 # 0:0 diff --git a/core2/tests/test_out_of_registers.md b/core2/tests/test_out_of_registers.md index 9fc42ebe..171c8212 100644 --- a/core2/tests/test_out_of_registers.md +++ b/core2/tests/test_out_of_registers.md @@ -330,30 +330,16 @@ l199 = 0 212:1: Out of local registers ``` -# Test: Out of temporary registers in final assignment +# Test: Out of temporary registers ## Source ```basic -result = 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 + 120 + 121 + 122 + 123 + 124 + 125 + 126 + 127 + 128 + 129 + 130 + 131 + 132 + 133 + 134 + 135 + 136 + 137 + 138 + 139 + 140 + 141 + 142 + 143 + 144 + 145 + 146 + 147 + 148 + 149 + 150 + 151 + 152 + 153 + 154 + 155 + 156 + 157 + 158 + 159 + 160 + 161 + 162 + 163 + 164 + 165 + 166 + 167 + 168 + 169 + 170 + 171 + 172 + 173 + 174 + 175 + 176 + 177 + 178 + 179 + 180 + 181 + 182 + 183 + 184 + 185 + 186 + 187 + 188 + 189 + 190 + 191 +a = SUM_INTEGERS(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96) ``` ## Compilation errors ```plain -1:14: Out of temp registers -``` - -# Test: Out of temporary registers in expression evaluation - -## Source - -```basic -result = 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + 10 + 11 + 12 + 13 + 14 + 15 + 16 + 17 + 18 + 19 + 20 + 21 + 22 + 23 + 24 + 25 + 26 + 27 + 28 + 29 + 30 + 31 + 32 + 33 + 34 + 35 + 36 + 37 + 38 + 39 + 40 + 41 + 42 + 43 + 44 + 45 + 46 + 47 + 48 + 49 + 50 + 51 + 52 + 53 + 54 + 55 + 56 + 57 + 58 + 59 + 60 + 61 + 62 + 63 + 64 + 65 + 66 + 67 + 68 + 69 + 70 + 71 + 72 + 73 + 74 + 75 + 76 + 77 + 78 + 79 + 80 + 81 + 82 + 83 + 84 + 85 + 86 + 87 + 88 + 89 + 90 + 91 + 92 + 93 + 94 + 95 + 96 + 97 + 98 + 99 + 100 + 101 + 102 + 103 + 104 + 105 + 106 + 107 + 108 + 109 + 110 + 111 + 112 + 113 + 114 + 115 + 116 + 117 + 118 + 119 + 120 + 121 + 122 + 123 + 124 + 125 + 126 + 127 + 128 + 129 + 130 + 131 + 132 + 133 + 134 + 135 + 136 + 137 + 138 + 139 + 140 + 141 + 142 + 143 + 144 + 145 + 146 + 147 + 148 + 149 + 150 + 151 + 152 + 153 + 154 + 155 + 156 + 157 + 158 + 159 + 160 + 161 + 162 + 163 + 164 + 165 + 166 + 167 + 168 + 169 + 170 + 171 + 172 + 173 + 174 + 175 + 176 + 177 + 178 + 179 + 180 + 181 + 182 + 183 + 184 + 185 + 186 + 187 + 188 + 189 + 190 + 191 + 192 -``` - -## Compilation errors - -```plain -1:10: Out of temp registers +1:389: Out of temp registers ``` diff --git a/core2/tests/test_strings.md b/core2/tests/test_strings.md index 2c450cc1..50d9d07e 100644 --- a/core2/tests/test_strings.md +++ b/core2/tests/test_strings.md @@ -21,12 +21,12 @@ OUT c4 0000: ENTER 6 # 0:0 0001: LOADI R64, 0 # 1:6 0002: LOADI R65, 1 # 2:6 -0003: MOVE R67, R64 # 4:6 -0004: MOVE R68, R65 # 4:11 -0005: CONCAT R66, R67, R68 # 4:9 -0006: MOVE R68, R66 # 5:6 -0007: LOADI R69, 2 # 5:11 -0008: CONCAT R67, R68, R69 # 5:9 +0003: MOVE R66, R64 # 4:6 +0004: MOVE R67, R65 # 4:11 +0005: CONCAT R66, R66, R67 # 4:9 +0006: MOVE R67, R66 # 5:6 +0007: LOADI R68, 2 # 5:11 +0008: CONCAT R67, R67, R68 # 5:9 0009: MOVE R69, R64 # 7:5 0010: LOADI R68, 259 # 7:5 0011: UPCALL 0, R68 # 7:1, OUT From 09325241dafbb2c8d0c19c6cb519af7c05d4d4b8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 26 Feb 2026 16:30:32 -0800 Subject: [PATCH 17/53] core2: Implement sub, mul, div, modulo, and power The expression compiler and VM previously only supported addition and negation. This change implements the five remaining binary arithmetic operations: subtraction, multiplication, division, modulo, and power. On the compiler side, the helper that emits a binary arithmetic op is refactored to take instruction-maker closures as parameters instead of hard-coding the add instructions. This eliminates any need for special-casing in the shared logic. On the VM side, two generic helpers (do_binary_double_op and do_binary_integer_op) are introduced to avoid repeating the register-decode/bit-cast/advance-PC pattern across every operation. --- core2/src/bytecode.rs | 221 ++++++++++++++++++++++++++++- core2/src/compiler/exprs.rs | 73 +++++++++- core2/src/image.rs | 10 ++ core2/src/vm/context.rs | 162 +++++++++++++++++++-- core2/tests/integration_test.rs | 5 + core2/tests/test_arithmetic_div.md | 163 +++++++++++++++++++++ core2/tests/test_arithmetic_mod.md | 163 +++++++++++++++++++++ core2/tests/test_arithmetic_mul.md | 134 +++++++++++++++++ core2/tests/test_arithmetic_pow.md | 160 +++++++++++++++++++++ core2/tests/test_arithmetic_sub.md | 135 ++++++++++++++++++ 10 files changed, 1210 insertions(+), 16 deletions(-) create mode 100644 core2/tests/test_arithmetic_div.md create mode 100644 core2/tests/test_arithmetic_mod.md create mode 100644 core2/tests/test_arithmetic_mul.md create mode 100644 core2/tests/test_arithmetic_pow.md create mode 100644 core2/tests/test_arithmetic_sub.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index f8c2fb32..6e879106 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -387,6 +387,12 @@ pub(crate) enum Opcode { /// Concatenates two strings and stores the pointer to the result into a third one. Concat, + /// Divides two doubles and stores the result into a third one. + DivideDouble, + + /// Divides two integers and stores the result into a third one. + DivideInteger, + /// Converts the double value in a register to an integer. DoubleToInteger, @@ -417,9 +423,21 @@ pub(crate) enum Opcode { /// Loads a register pointer into a register. LoadRegisterPointer, + /// Computes the modulo of two doubles and stores the result into a third one. + ModuloDouble, + + /// Computes the modulo of two integers and stores the result into a third one. + ModuloInteger, + /// Moves (copies) data between two registers. Move, + /// Multiplies two doubles and stores the result into a third one. + MultiplyDouble, + + /// Multiplies two integers and stores the result into a third one. + MultiplyInteger, + /// Negates a double value in place. NegateDouble, @@ -429,12 +447,24 @@ pub(crate) enum Opcode { /// The "null" instruction, used by the compiler to pad the code for fixups. Nop, + /// Computes the power of two doubles and stores the result into a third one. + PowerDouble, + + /// Computes the power of two integers and stores the result into a third one. + PowerInteger, + /// Returns from a previous `Call`. Return, /// Stores a value into an array element. StoreArray, + /// Subtracts two doubles and stores the result into a third one. + SubtractDouble, + + /// Subtracts two integers and stores the result into a third one. + SubtractInteger, + /// Requests the execution of an upcall, stopping VM execution. Upcall, @@ -495,6 +525,24 @@ instr!( Register, 0x000000ff, 0, // Right hand side value. ); +#[rustfmt::skip] +instr!( + Opcode::DivideDouble, "DIVD", + make_divide_double, parse_divide_double, format_divide_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::DivideInteger, "DIVI", + make_divide_integer, parse_divide_integer, format_divide_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::DoubleToInteger, "DTOI", @@ -577,6 +625,24 @@ instr!( Register, 0x000000ff, 0, // Register to load. ); +#[rustfmt::skip] +instr!( + Opcode::ModuloDouble, "MODD", + make_modulo_double, parse_modulo_double, format_modulo_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::ModuloInteger, "MODI", + make_modulo_integer, parse_modulo_integer, format_modulo_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::Move, "MOVE", @@ -587,9 +653,32 @@ instr!( #[rustfmt::skip] instr!( - Opcode::NegateDouble, "NEGD", - make_negate_double, parse_negate_double, format_negate_double, - Register, 0x000000ff, 0, // Register with the value to negate in place. + Opcode::MultiplyDouble, "MULD", + make_multiply_double, parse_multiply_double, format_multiply_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::MultiplyInteger, "MULI", + make_multiply_integer, parse_multiply_integer, format_multiply_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::NegateDouble, + "NEGD", + make_negate_double, + parse_negate_double, + format_negate_double, + Register, + 0x000000ff, + 0, // Register with the value to negate in place. ); #[rustfmt::skip] @@ -605,6 +694,24 @@ instr!( make_nop, parse_nop, format_nop, ); +#[rustfmt::skip] +instr!( + Opcode::PowerDouble, "POWD", + make_power_double, parse_power_double, format_power_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::PowerInteger, "POWI", + make_power_integer, parse_power_integer, format_power_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::Return, "RETURN", @@ -620,6 +727,24 @@ instr!( Register, 0x000000ff, 0, // First register containing subscript values. ); +#[rustfmt::skip] +instr!( + Opcode::SubtractDouble, "SUBD", + make_subtract_double, parse_subtract_double, format_subtract_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::SubtractInteger, "SUBI", + make_subtract_integer, parse_subtract_integer, format_subtract_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::Upcall, "UPCALL", @@ -791,6 +916,24 @@ mod tests { Register::local(3).unwrap() ); + test_instr!( + test_divide_double, + make_divide_double, + parse_divide_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_divide_integer, + make_divide_integer, + parse_divide_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_double_to_integer, make_double_to_integer, @@ -849,6 +992,24 @@ mod tests { Register::local(2).unwrap() ); + test_instr!( + test_modulo_double, + make_modulo_double, + parse_modulo_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_modulo_integer, + make_modulo_integer, + parse_modulo_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_move, make_move, @@ -857,6 +1018,24 @@ mod tests { Register::local(2).unwrap() ); + test_instr!( + test_multiply_double, + make_multiply_double, + parse_multiply_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_multiply_integer, + make_multiply_integer, + parse_multiply_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_negate_double, make_negate_double, @@ -873,6 +1052,24 @@ mod tests { test_instr!(test_nop, make_nop, parse_nop); + test_instr!( + test_power_double, + make_power_double, + parse_power_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_power_integer, + make_power_integer, + parse_power_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_return, make_return, parse_return); test_instr!( @@ -884,6 +1081,24 @@ mod tests { Register::local(3).unwrap() ); + test_instr!( + test_subtract_double, + make_subtract_double, + parse_subtract_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_subtract_integer, + make_subtract_integer, + parse_subtract_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_upcall, make_upcall, parse_upcall, 12345, Register::local(3).unwrap()); #[test] diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index c28c13ee..5a892902 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -88,7 +88,6 @@ struct PendingBinaryOp { /// of pending ops. fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { let mut pending: Vec = vec![]; - #[allow(clippy::while_let_loop)] loop { match expr { Expr::Add(span) => { @@ -103,6 +102,71 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { }); expr = span.lhs; } + + Expr::Divide(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "/", + make_double: bytecode::make_divide_double, + make_integer: bytecode::make_divide_integer, + make_text: None, + }); + expr = span.lhs; + } + + Expr::Modulo(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "MOD", + make_double: bytecode::make_modulo_double, + make_integer: bytecode::make_modulo_integer, + make_text: None, + }); + expr = span.lhs; + } + + Expr::Multiply(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "*", + make_double: bytecode::make_multiply_double, + make_integer: bytecode::make_multiply_integer, + make_text: None, + }); + expr = span.lhs; + } + + Expr::Power(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "^", + make_double: bytecode::make_power_double, + make_integer: bytecode::make_power_integer, + make_text: None, + }); + expr = span.lhs; + } + + Expr::Subtract(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "-", + make_double: bytecode::make_subtract_double, + make_integer: bytecode::make_subtract_integer, + make_text: None, + }); + expr = span.lhs; + } _ => break, } } @@ -175,7 +239,12 @@ pub(super) fn compile_expr( let (expr, pending) = peel_binary_ops(expr); let etype = match expr { - Expr::Add(..) => unreachable!("Peeled by peel_binary_ops"), + Expr::Add(..) + | Expr::Divide(..) + | Expr::Modulo(..) + | Expr::Multiply(..) + | Expr::Power(..) + | Expr::Subtract(..) => unreachable!("Peeled by peel_binary_ops"), Expr::Boolean(span) => { codegen.emit_value(reg, ConstantDatum::Boolean(span.value), span.pos)?; diff --git a/core2/src/image.rs b/core2/src/image.rs index ea274d07..b94d5578 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -33,6 +33,8 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::AllocArray => bytecode::format_alloc_array(instr), Opcode::Call => bytecode::format_call(instr), Opcode::Concat => bytecode::format_concat(instr), + Opcode::DivideDouble => bytecode::format_divide_double(instr), + Opcode::DivideInteger => bytecode::format_divide_integer(instr), Opcode::DoubleToInteger => bytecode::format_double_to_integer(instr), Opcode::End => bytecode::format_end(instr), Opcode::Enter => bytecode::format_enter(instr), @@ -44,12 +46,20 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::LoadConstant => bytecode::format_load_constant(instr), Opcode::LoadInteger => bytecode::format_load_integer(instr), Opcode::LoadRegisterPointer => bytecode::format_load_register_ptr(instr), + Opcode::ModuloDouble => bytecode::format_modulo_double(instr), + Opcode::ModuloInteger => bytecode::format_modulo_integer(instr), Opcode::Move => bytecode::format_move(instr), + Opcode::MultiplyDouble => bytecode::format_multiply_double(instr), + Opcode::MultiplyInteger => bytecode::format_multiply_integer(instr), Opcode::NegateDouble => bytecode::format_negate_double(instr), Opcode::NegateInteger => bytecode::format_negate_integer(instr), Opcode::Nop => bytecode::format_nop(instr), + Opcode::PowerDouble => bytecode::format_power_double(instr), + Opcode::PowerInteger => bytecode::format_power_integer(instr), Opcode::Return => bytecode::format_return(instr), Opcode::StoreArray => bytecode::format_store_array(instr), + Opcode::SubtractDouble => bytecode::format_subtract_double(instr), + Opcode::SubtractInteger => bytecode::format_subtract_integer(instr), Opcode::Upcall => bytecode::format_upcall(instr), } } diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 0640adfa..c399db64 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -52,6 +52,42 @@ struct Frame { ret_reg: Option, } +/// Custom implementation of checked integer additions for error reporting purposes. +#[inline(always)] +fn checked_add_integer(lhs: i32, rhs: i32) -> Result { + lhs.checked_add(rhs).ok_or("Integer overflow") +} + +/// Custom implementation of checked integer divisions for error reporting purposes. +#[inline(always)] +fn checked_div_integer(lhs: i32, rhs: i32) -> Result { + if rhs == 0 { Err("Division by zero") } else { lhs.checked_div(rhs).ok_or("Integer underflow") } +} + +/// Custom implementation of checked integer modulos for error reporting purposes. +#[inline(always)] +fn checked_mod_integer(lhs: i32, rhs: i32) -> Result { + if rhs == 0 { Err("Modulo by zero") } else { lhs.checked_rem(rhs).ok_or("Integer underflow") } +} + +/// Custom implementation of checked integer multiplications for error reporting purposes. +#[inline(always)] +fn checked_mul_integer(lhs: i32, rhs: i32) -> Result { + lhs.checked_mul(rhs).ok_or("Integer overflow") +} + +/// Custom implementation of checked integer powers for error reporting purposes. +#[inline(always)] +fn checked_pow_integer(lhs: i32, exp: u32) -> Result { + lhs.checked_pow(exp).ok_or("Integer overflow") +} + +/// Custom implementation of checked integer subtractions for error reporting purposes. +#[inline(always)] +fn checked_sub_integer(lhs: i32, rhs: i32) -> Result { + lhs.checked_sub(rhs).ok_or("Integer underflow") +} + /// Execution context for the virtual machine. /// /// This roughly corresponds to the concept of a "processor", making the VM the container of @@ -203,6 +239,8 @@ impl Context { Opcode::AllocArray => self.do_alloc_array(instr, heap), Opcode::Call => self.do_call(instr), Opcode::Concat => self.do_concat(instr, &image.constants, heap), + Opcode::DivideDouble => self.do_divide_double(instr), + Opcode::DivideInteger => self.do_divide_integer(instr), Opcode::DoubleToInteger => self.do_double_to_integer(instr), Opcode::End => self.do_end(instr), Opcode::Enter => self.do_enter(instr), @@ -214,12 +252,20 @@ impl Context { Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), Opcode::LoadInteger => self.do_load_integer(instr), Opcode::LoadRegisterPointer => self.do_load_register_ptr(instr), + Opcode::ModuloDouble => self.do_modulo_double(instr), + Opcode::ModuloInteger => self.do_modulo_integer(instr), Opcode::Move => self.do_move(instr), + Opcode::MultiplyDouble => self.do_multiply_double(instr), + Opcode::MultiplyInteger => self.do_multiply_integer(instr), Opcode::NegateDouble => self.do_negate_double(instr), Opcode::NegateInteger => self.do_negate_integer(instr), Opcode::Nop => self.do_nop(instr), + Opcode::PowerDouble => self.do_power_double(instr), + Opcode::PowerInteger => self.do_power_integer(instr), Opcode::Return => self.do_return(instr), Opcode::StoreArray => self.do_store_array(instr, heap), + Opcode::SubtractDouble => self.do_subtract_double(instr), + Opcode::SubtractInteger => self.do_subtract_integer(instr), Opcode::Upcall => self.do_upcall(instr), } } @@ -228,31 +274,57 @@ impl Context { } impl Context { - /// Implements the `AddDouble` opcode. - pub(super) fn do_add_double(&mut self, instr: u32) { - let (dest, src1, src2) = bytecode::parse_add_double(instr); + /// Applies a binary double operation using `parse` to decode the instruction and `op` to + /// compute the result. + fn do_binary_double_op( + &mut self, + instr: u32, + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(f64, f64) -> f64, + { + let (dest, src1, src2) = parse(instr); let lhs = f64::from_bits(self.get_reg(src1)); let rhs = f64::from_bits(self.get_reg(src2)); - self.set_reg(dest, (lhs + rhs).to_bits()); + self.set_reg(dest, op(lhs, rhs).to_bits()); self.pc += 1; } - /// Implements the `AddInteger` opcode. - pub(super) fn do_add_integer(&mut self, instr: u32) { - let (dest, src1, src2) = bytecode::parse_add_integer(instr); + /// Applies a binary integer operation using `parse` to decode the instruction and `op` to + /// compute the result. `op` returns `Err` with a message on failure. + fn do_binary_integer_op( + &mut self, + instr: u32, + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(i32, i32) -> Result, + { + let (dest, src1, src2) = parse(instr); let lhs = self.get_reg(src1) as i32; let rhs = self.get_reg(src2) as i32; - match lhs.checked_add(rhs) { - Some(result) => { + match op(lhs, rhs) { + Ok(result) => { self.set_reg(dest, result as u64); self.pc += 1; } - None => { - self.set_exception("Integer overflow"); + Err(msg) => { + self.set_exception(msg); } } } + /// Implements the `AddDouble` opcode. + pub(super) fn do_add_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_add_double, |l, r| l + r); + } + + /// Implements the `AddInteger` opcode. + pub(super) fn do_add_integer(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_add_integer, checked_add_integer); + } + /// Implements the `Alloc` opcode. pub(super) fn do_alloc(&mut self, instr: u32, heap: &mut Vec) { let (dest, etype) = bytecode::parse_alloc(instr); @@ -332,6 +404,16 @@ impl Context { self.pc += 1; } + /// Implements the `DivideDouble` opcode. + pub(super) fn do_divide_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_divide_double, |l, r| l / r); + } + + /// Implements the `DivideInteger` opcode. + pub(super) fn do_divide_integer(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_divide_integer, checked_div_integer); + } + /// Implements the `DoubleToInteger` opcode. pub(super) fn do_double_to_integer(&mut self, instr: u32) { let reg = bytecode::parse_double_to_integer(instr); @@ -423,6 +505,16 @@ impl Context { self.pc += 1; } + /// Implements the `ModuloDouble` opcode. + pub(super) fn do_modulo_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_modulo_double, |l, r| l % r); + } + + /// Implements the `ModuloInteger` opcode. + pub(super) fn do_modulo_integer(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_modulo_integer, checked_mod_integer); + } + /// Implements the `Move` opcode. pub(super) fn do_move(&mut self, instr: u32) { let (dest, src) = bytecode::parse_move(instr); @@ -431,6 +523,16 @@ impl Context { self.pc += 1; } + /// Implements the `MultiplyDouble` opcode. + pub(super) fn do_multiply_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_multiply_double, |l, r| l * r); + } + + /// Implements the `MultiplyInteger` opcode. + pub(super) fn do_multiply_integer(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_multiply_integer, checked_mul_integer); + } + /// Implements the `NegateDouble` opcode. pub(super) fn do_negate_double(&mut self, instr: u32) { let reg = bytecode::parse_negate_double(instr); @@ -460,6 +562,34 @@ impl Context { self.pc += 1; } + /// Implements the `PowerDouble` opcode. + pub(super) fn do_power_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_power_double, |l, r| l.powf(r)); + } + + /// Implements the `PowerInteger` opcode. + pub(super) fn do_power_integer(&mut self, instr: u32) { + let (dest, src1, src2) = bytecode::parse_power_integer(instr); + let lhs = self.get_reg(src1) as i32; + let rhs = self.get_reg(src2) as i32; + let exp = match u32::try_from(rhs) { + Ok(exp) => exp, + Err(_) => { + self.set_exception(format!("Exponent {} cannot be negative", rhs)); + return; + } + }; + match checked_pow_integer(lhs, exp) { + Ok(result) => { + self.set_reg(dest, result as u64); + self.pc += 1; + } + Err(msg) => { + self.set_exception(msg); + } + } + } + /// Implements the `Return` opcode. pub(super) fn do_return(&mut self, instr: u32) { bytecode::parse_return(instr); @@ -496,6 +626,16 @@ impl Context { } } + /// Implements the `SubtractDouble` opcode. + pub(super) fn do_subtract_double(&mut self, instr: u32) { + self.do_binary_double_op(instr, bytecode::parse_subtract_double, |l, r| l - r); + } + + /// Implements the `SubtractInteger` opcode. + pub(super) fn do_subtract_integer(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_subtract_integer, checked_sub_integer); + } + /// Implements the `Upcall` opcode. pub(super) fn do_upcall(&mut self, instr: u32) { let (index, first_reg) = bytecode::parse_upcall(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index d4f1c57c..eab928d0 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -33,7 +33,12 @@ macro_rules! one_test { one_test!(test_args); one_test!(test_arithmetic_add); +one_test!(test_arithmetic_div); +one_test!(test_arithmetic_mod); +one_test!(test_arithmetic_mul); one_test!(test_arithmetic_neg); +one_test!(test_arithmetic_pow); +one_test!(test_arithmetic_sub); one_test!(test_arrays); one_test!(test_assignments); one_test!(test_empty); diff --git a/core2/tests/test_arithmetic_div.md b/core2/tests/test_arithmetic_div.md new file mode 100644 index 00000000..a22de8e6 --- /dev/null +++ b/core2/tests/test_arithmetic_div.md @@ -0,0 +1,163 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 9.0 / 4.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: DIVD R65, R65, R66 # 1:9 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=2.25# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 10 / 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 10 # 1:5 +0002: LOADI R66, 3 # 1:10 +0003: DIVI R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=3% +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 3 / 1.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: DIVD R65, R65, R66 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=2# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 9.0 / 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 3 # 1:11 +0003: ITOD R66 # 1:11 +0004: DIVD R65, R65, R66 # 1:9 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=3# +``` + +# Test: Integer overflow + +## Source + +```basic +a = (-2147483647 - 1) / -1 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:7 +0002: NEGI R64 # 1:6 +0003: LOADI R65, 1 # 1:20 +0004: SUBI R64, R64, R65 # 1:18 +0005: LOADI R65, 1 # 1:26 +0006: NEGI R65 # 1:25 +0007: DIVI R64, R64, R65 # 1:23 +0008: LOADI R65, 0 # 0:0 +0009: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:23: Integer underflow +``` + +# Test: Division by zero + +## Source + +```basic +a = 5 / 0 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 5 # 1:5 +0002: LOADI R65, 0 # 1:9 +0003: DIVI R64, R64, R65 # 1:7 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:7: Division by zero +``` diff --git a/core2/tests/test_arithmetic_mod.md b/core2/tests/test_arithmetic_mod.md new file mode 100644 index 00000000..1409d9e4 --- /dev/null +++ b/core2/tests/test_arithmetic_mod.md @@ -0,0 +1,163 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 10.0 MOD 3.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:14 +0003: MODD R65, R65, R66 # 1:10 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=1# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 10 MOD 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 10 # 1:5 +0002: LOADI R66, 3 # 1:12 +0003: MODI R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 3 MOD 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADC R66, 0 # 1:11 +0003: ITOD R65 # 1:7 +0004: MODD R65, R65, R66 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=0.5# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 10.5 MOD 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 3 # 1:14 +0003: ITOD R66 # 1:14 +0004: MODD R65, R65, R66 # 1:10 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=1.5# +``` + +# Test: Integer overflow + +## Source + +```basic +a = (-2147483647 - 1) MOD -1 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:7 +0002: NEGI R64 # 1:6 +0003: LOADI R65, 1 # 1:20 +0004: SUBI R64, R64, R65 # 1:18 +0005: LOADI R65, 1 # 1:28 +0006: NEGI R65 # 1:27 +0007: MODI R64, R64, R65 # 1:23 +0008: LOADI R65, 0 # 0:0 +0009: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:23: Integer underflow +``` + +# Test: Modulo by zero + +## Source + +```basic +a = 5 MOD 0 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 5 # 1:5 +0002: LOADI R65, 0 # 1:11 +0003: MODI R64, R64, R65 # 1:7 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:7: Modulo by zero +``` diff --git a/core2/tests/test_arithmetic_mul.md b/core2/tests/test_arithmetic_mul.md new file mode 100644 index 00000000..f49d6b97 --- /dev/null +++ b/core2/tests/test_arithmetic_mul.md @@ -0,0 +1,134 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 4.0 * 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: MULD R65, R65, R66 # 1:9 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=10# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 6 * 7 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 6 # 1:5 +0002: LOADI R66, 7 # 1:9 +0003: MULI R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=42% +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 3 * 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: MULD R65, R65, R66 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=7.5# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 2.5 * 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 3 # 1:11 +0003: ITOD R66 # 1:11 +0004: MULD R65, R65, R66 # 1:9 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=7.5# +``` + +# Test: Integer overflow + +## Source + +```basic +a = 2147483640 * 10 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:5 +0002: LOADI R65, 10 # 1:18 +0003: MULI R64, R64, R65 # 1:16 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:16: Integer overflow +``` diff --git a/core2/tests/test_arithmetic_pow.md b/core2/tests/test_arithmetic_pow.md new file mode 100644 index 00000000..18b270ca --- /dev/null +++ b/core2/tests/test_arithmetic_pow.md @@ -0,0 +1,160 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.0 ^ 3.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: POWD R65, R65, R66 # 1:9 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=8# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 2 ^ 8 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 8 # 1:9 +0003: POWI R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=256% +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 4 ^ 0.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 4 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: POWD R65, R65, R66 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=2# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 2.5 ^ 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 3 # 1:11 +0003: ITOD R66 # 1:11 +0004: POWD R65, R65, R66 # 1:9 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=15.625# +``` + +# Test: Integer overflow + +## Source + +```basic +a = 46341 ^ 2 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 46341 # 1:5 +0002: LOADI R65, 2 # 1:13 +0003: POWI R64, R64, R65 # 1:11 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:11: Integer overflow +``` + +# Test: Negative exponent + +## Source + +```basic +a = 2 ^ -1 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: LOADI R65, 1 # 1:10 +0003: NEGI R65 # 1:9 +0004: POWI R64, R64, R65 # 1:7 +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:7: Exponent -1 cannot be negative +``` diff --git a/core2/tests/test_arithmetic_sub.md b/core2/tests/test_arithmetic_sub.md new file mode 100644 index 00000000..09142728 --- /dev/null +++ b/core2/tests/test_arithmetic_sub.md @@ -0,0 +1,135 @@ +# Test: Two immediate doubles + +## Source + +```basic +OUT 5.0 - 3.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: SUBD R65, R65, R66 # 1:9 +0004: LOADI R64, 257 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=2# +``` + +# Test: Two immediate integers + +## Source + +```basic +OUT 10 - 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 10 # 1:5 +0002: LOADI R66, 3 # 1:10 +0003: SUBI R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=7% +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 - 8.3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: SUBD R65, R65, R66 # 1:7 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=-6.300000000000001# +``` + +# Test: Right integer operand needs type promotion to double + +## Source + +```basic +OUT 8.3 - 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADI R66, 2 # 1:11 +0003: ITOD R66 # 1:11 +0004: SUBD R65, R65, R66 # 1:9 +0005: LOADI R64, 257 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=6.300000000000001# +``` + +# Test: Integer overflow + +## Source + +```basic +a = -2147483640 - 20 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADC R64, 0 # 1:6 +0002: NEGI R64 # 1:5 +0003: LOADI R65, 20 # 1:19 +0004: SUBI R64, R64, R65 # 1:17 +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 +``` + +## Runtime errors + +```plain +1:17: Integer underflow +``` From 1b5020f6086d198fc319ca203afe908f9c8056b8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 26 Feb 2026 16:38:08 -0800 Subject: [PATCH 18/53] core2: Implement bitwise and shift operations Adds support for the bitwise NOT, AND, OR, XOR, SHL (shift left), and SHR (shift right) operations in the expression compiler and VM. --- core2/src/bytecode.rs | 119 +++++++++++++++++++++ core2/src/compiler/exprs.rs | 148 ++++++++++++++++++++++---- core2/src/image.rs | 6 ++ core2/src/vm/context.rs | 85 ++++++++++++++- core2/tests/integration_test.rs | 6 ++ core2/tests/test_bitwise_and.md | 88 ++++++++++++++++ core2/tests/test_bitwise_not.md | 87 ++++++++++++++++ core2/tests/test_bitwise_or.md | 88 ++++++++++++++++ core2/tests/test_bitwise_shl.md | 122 ++++++++++++++++++++++ core2/tests/test_bitwise_shr.md | 178 ++++++++++++++++++++++++++++++++ core2/tests/test_bitwise_xor.md | 88 ++++++++++++++++ 11 files changed, 990 insertions(+), 25 deletions(-) create mode 100644 core2/tests/test_bitwise_and.md create mode 100644 core2/tests/test_bitwise_not.md create mode 100644 core2/tests/test_bitwise_or.md create mode 100644 core2/tests/test_bitwise_shl.md create mode 100644 core2/tests/test_bitwise_shr.md create mode 100644 core2/tests/test_bitwise_xor.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 6e879106..bdde36c6 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -381,6 +381,18 @@ pub(crate) enum Opcode { /// Allocates a multidimensional array on the heap. AllocArray, + /// Computes the bitwise AND of two integers and stores the result into a third one. + BitwiseAnd, + + /// Computes the bitwise NOT of an integer value in place. + BitwiseNot, + + /// Computes the bitwise OR of two integers and stores the result into a third one. + BitwiseOr, + + /// Computes the bitwise XOR of two integers and stores the result into a third one. + BitwiseXor, + /// Calls an address relative to the PC. Call, @@ -456,6 +468,14 @@ pub(crate) enum Opcode { /// Returns from a previous `Call`. Return, + /// Shifts an integer left by a number of bits without rotation, storing the result into + /// a third register. + ShiftLeft, + + /// Shifts an integer right by a number of bits without rotation, storing the result into + /// a third register. + ShiftRight, + /// Stores a value into an array element. StoreArray, @@ -508,6 +528,40 @@ instr!( Register, 0x000000ff, 0, // First register containing dimension sizes. ); +#[rustfmt::skip] +instr!( + Opcode::BitwiseAnd, "AND", + make_bitwise_and, parse_bitwise_and, format_bitwise_and, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::BitwiseNot, "NOT", + make_bitwise_not, parse_bitwise_not, format_bitwise_not, + Register, 0x000000ff, 0, // Register with the value to NOT in place. +); + +#[rustfmt::skip] +instr!( + Opcode::BitwiseOr, "OR", + make_bitwise_or, parse_bitwise_or, format_bitwise_or, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::BitwiseXor, "XOR", + make_bitwise_xor, parse_bitwise_xor, format_bitwise_xor, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::Call, "CALL", @@ -718,6 +772,24 @@ instr!( make_return, parse_return, format_return, ); +#[rustfmt::skip] +instr!( + Opcode::ShiftLeft, "SHL", + make_shift_left, parse_shift_left, format_shift_left, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Value to shift. + Register, 0x000000ff, 0, // Number of bits to shift by. +); + +#[rustfmt::skip] +instr!( + Opcode::ShiftRight, "SHR", + make_shift_right, parse_shift_right, format_shift_right, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Value to shift. + Register, 0x000000ff, 0, // Number of bits to shift by. +); + #[rustfmt::skip] instr!( Opcode::StoreArray, "STOREA", @@ -905,6 +977,35 @@ mod tests { Register::local(2).unwrap() ); + test_instr!( + test_bitwise_and, + make_bitwise_and, + parse_bitwise_and, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!(test_bitwise_not, make_bitwise_not, parse_bitwise_not, Register::local(1).unwrap()); + + test_instr!( + test_bitwise_or, + make_bitwise_or, + parse_bitwise_or, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_bitwise_xor, + make_bitwise_xor, + parse_bitwise_xor, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_call, make_call, parse_call, Register::local(3).unwrap(), 12345); test_instr!( @@ -1072,6 +1173,24 @@ mod tests { test_instr!(test_return, make_return, parse_return); + test_instr!( + test_shift_left, + make_shift_left, + parse_shift_left, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_shift_right, + make_shift_right, + parse_shift_right, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_store_array, make_store_array, diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 5a892902..c3fa854f 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -70,19 +70,20 @@ fn compile_array_access( Ok(info.subtype) } -/// A pending arithmetic binary operation waiting to be applied, used to flatten -/// expressions and avoid recursive calls during processing. +/// A pending binary operation waiting to be applied, used to flatten expressions and +/// avoid recursive calls during processing. struct PendingBinaryOp { pos: LineCol, rhs: Expr, op_name: &'static str, - make_double: fn(Register, Register, Register) -> u32, + make_boolean: Option u32>, + make_double: Option u32>, make_integer: fn(Register, Register, Register) -> u32, make_text: Option u32>, } -/// Peels the left-recursive chain of arithmetic binary ops into a vector of pending -/// binary ops to avoid deep recursion. +/// Peels the left-recursive chain of binary ops into a vector of pending binary ops to avoid +/// deep recursion. /// /// Returns the input `expr` holding the leftmost non-binary expression and the list /// of pending ops. @@ -96,20 +97,36 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { pos: span.pos, rhs: span.rhs, op_name: "+", - make_double: bytecode::make_add_double, + make_boolean: None, + make_double: Some(bytecode::make_add_double), make_integer: bytecode::make_add_integer, make_text: Some(bytecode::make_concat), }); expr = span.lhs; } + Expr::And(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "AND", + make_boolean: Some(bytecode::make_bitwise_and), + make_double: None, + make_integer: bytecode::make_bitwise_and, + make_text: None, + }); + expr = span.lhs; + } + Expr::Divide(span) => { let span = *span; pending.push(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "/", - make_double: bytecode::make_divide_double, + make_boolean: None, + make_double: Some(bytecode::make_divide_double), make_integer: bytecode::make_divide_integer, make_text: None, }); @@ -122,7 +139,8 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { pos: span.pos, rhs: span.rhs, op_name: "MOD", - make_double: bytecode::make_modulo_double, + make_boolean: None, + make_double: Some(bytecode::make_modulo_double), make_integer: bytecode::make_modulo_integer, make_text: None, }); @@ -135,46 +153,104 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { pos: span.pos, rhs: span.rhs, op_name: "*", - make_double: bytecode::make_multiply_double, + make_boolean: None, + make_double: Some(bytecode::make_multiply_double), make_integer: bytecode::make_multiply_integer, make_text: None, }); expr = span.lhs; } + Expr::Or(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "OR", + make_boolean: Some(bytecode::make_bitwise_or), + make_double: None, + make_integer: bytecode::make_bitwise_or, + make_text: None, + }); + expr = span.lhs; + } + Expr::Power(span) => { let span = *span; pending.push(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "^", - make_double: bytecode::make_power_double, + make_boolean: None, + make_double: Some(bytecode::make_power_double), make_integer: bytecode::make_power_integer, make_text: None, }); expr = span.lhs; } + Expr::ShiftLeft(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "<<", + make_boolean: None, + make_double: None, + make_integer: bytecode::make_shift_left, + make_text: None, + }); + expr = span.lhs; + } + + Expr::ShiftRight(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: ">>", + make_boolean: None, + make_double: None, + make_integer: bytecode::make_shift_right, + make_text: None, + }); + expr = span.lhs; + } Expr::Subtract(span) => { let span = *span; pending.push(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "-", - make_double: bytecode::make_subtract_double, + make_boolean: None, + make_double: Some(bytecode::make_subtract_double), make_integer: bytecode::make_subtract_integer, make_text: None, }); expr = span.lhs; } + + Expr::Xor(span) => { + let span = *span; + pending.push(PendingBinaryOp { + pos: span.pos, + rhs: span.rhs, + op_name: "XOR", + make_boolean: Some(bytecode::make_bitwise_xor), + make_double: None, + make_integer: bytecode::make_bitwise_xor, + make_text: None, + }); + expr = span.lhs; + } _ => break, } } (expr, pending) } -/// Processes `pending` arithmetic binary ops from innermost to outermost, using -/// `reg` as the accumulator. +/// Processes `pending` binary ops from innermost to outermost, using `reg` as the +/// accumulator. /// /// This avoids the deep recursion that would arise if we compiled binary op chains /// by recursing on the lhs. @@ -192,14 +268,17 @@ fn compile_pending_ops( let rtype = compile_expr(codegen, symtable, rtemp, op.rhs)?; let result_type = match (etype, rtype) { - (ExprType::Double, ExprType::Double) => ExprType::Double, + (ExprType::Double, ExprType::Double) if op.make_double.is_some() => ExprType::Double, + (ExprType::Boolean, ExprType::Boolean) if op.make_boolean.is_some() => { + ExprType::Boolean + } (ExprType::Integer, ExprType::Integer) => ExprType::Integer, (ExprType::Text, ExprType::Text) if op.make_text.is_some() => ExprType::Text, - (ExprType::Double, ExprType::Integer) => { + (ExprType::Double, ExprType::Integer) if op.make_double.is_some() => { codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); ExprType::Double } - (ExprType::Integer, ExprType::Double) => { + (ExprType::Integer, ExprType::Double) if op.make_double.is_some() => { codegen.emit(bytecode::make_integer_to_double(reg), op.pos); ExprType::Double } @@ -207,9 +286,11 @@ fn compile_pending_ops( }; match result_type { - ExprType::Boolean => unreachable!(), + ExprType::Boolean => { + codegen.emit(op.make_boolean.unwrap()(reg, reg, rtemp), op.pos); + } ExprType::Double => { - codegen.emit((op.make_double)(reg, reg, rtemp), op.pos); + codegen.emit(op.make_double.unwrap()(reg, reg, rtemp), op.pos); } ExprType::Integer => { codegen.emit((op.make_integer)(reg, reg, rtemp), op.pos); @@ -227,9 +308,8 @@ fn compile_pending_ops( /// Compiles a single expression `expr` and leaves its value in `reg`. /// -/// For left-recursive arithmetic binary operations (like `a + b + c`), this function -/// iterates rather than recurses so that very long expression chains do not overflow -/// the call stack. +/// For left-recursive binary operations (like `a + b + c`), this function iterates rather +/// than recurses so that very long expression chains do not overflow the call stack. pub(super) fn compile_expr( codegen: &mut Codegen, symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, @@ -240,11 +320,16 @@ pub(super) fn compile_expr( let etype = match expr { Expr::Add(..) + | Expr::And(..) | Expr::Divide(..) | Expr::Modulo(..) | Expr::Multiply(..) + | Expr::Or(..) | Expr::Power(..) - | Expr::Subtract(..) => unreachable!("Peeled by peel_binary_ops"), + | Expr::ShiftLeft(..) + | Expr::ShiftRight(..) + | Expr::Subtract(..) + | Expr::Xor(..) => unreachable!("Peeled by peel_binary_ops"), Expr::Boolean(span) => { codegen.emit_value(reg, ConstantDatum::Boolean(span.value), span.pos)?; @@ -337,6 +422,25 @@ pub(super) fn compile_expr( Ok(etype) } + Expr::Not(span) => { + let pos = span.pos; + let etype = compile_expr(codegen, symtable, reg, span.expr)?; + match etype { + ExprType::Boolean => { + let mut scope = symtable.temp_scope(); + let temp = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + codegen.emit_value(temp, ConstantDatum::Integer(1), pos)?; + codegen.emit(bytecode::make_bitwise_xor(reg, reg, temp), pos); + Ok(ExprType::Boolean) + } + ExprType::Integer => { + codegen.emit(bytecode::make_bitwise_not(reg), pos); + Ok(ExprType::Integer) + } + _ => Err(Error::TypeMismatch(pos, etype, ExprType::Integer)), + } + } + Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { Ok((local, SymbolPrototype::Scalar(etype))) => { codegen.emit(bytecode::make_move(reg, local), span.pos); diff --git a/core2/src/image.rs b/core2/src/image.rs index b94d5578..d63f59cf 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -31,6 +31,10 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::AddInteger => bytecode::format_add_integer(instr), Opcode::Alloc => bytecode::format_alloc(instr), Opcode::AllocArray => bytecode::format_alloc_array(instr), + Opcode::BitwiseAnd => bytecode::format_bitwise_and(instr), + Opcode::BitwiseNot => bytecode::format_bitwise_not(instr), + Opcode::BitwiseOr => bytecode::format_bitwise_or(instr), + Opcode::BitwiseXor => bytecode::format_bitwise_xor(instr), Opcode::Call => bytecode::format_call(instr), Opcode::Concat => bytecode::format_concat(instr), Opcode::DivideDouble => bytecode::format_divide_double(instr), @@ -57,6 +61,8 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::PowerDouble => bytecode::format_power_double(instr), Opcode::PowerInteger => bytecode::format_power_integer(instr), Opcode::Return => bytecode::format_return(instr), + Opcode::ShiftLeft => bytecode::format_shift_left(instr), + Opcode::ShiftRight => bytecode::format_shift_right(instr), Opcode::StoreArray => bytecode::format_store_array(instr), Opcode::SubtractDouble => bytecode::format_subtract_double(instr), Opcode::SubtractInteger => bytecode::format_subtract_integer(instr), diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index c399db64..6214641b 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -58,6 +58,12 @@ fn checked_add_integer(lhs: i32, rhs: i32) -> Result { lhs.checked_add(rhs).ok_or("Integer overflow") } +/// Custom implementation of checked bitwise AND for error reporting purposes. +#[inline(always)] +fn checked_and_integer(lhs: i32, rhs: i32) -> Result { + Ok(lhs & rhs) +} + /// Custom implementation of checked integer divisions for error reporting purposes. #[inline(always)] fn checked_div_integer(lhs: i32, rhs: i32) -> Result { @@ -76,18 +82,51 @@ fn checked_mul_integer(lhs: i32, rhs: i32) -> Result { lhs.checked_mul(rhs).ok_or("Integer overflow") } +/// Custom implementation of checked bitwise OR for error reporting purposes. +#[inline(always)] +fn checked_or_integer(lhs: i32, rhs: i32) -> Result { + Ok(lhs | rhs) +} + /// Custom implementation of checked integer powers for error reporting purposes. #[inline(always)] fn checked_pow_integer(lhs: i32, exp: u32) -> Result { lhs.checked_pow(exp).ok_or("Integer overflow") } +/// Custom implementation of checked left shift for error reporting purposes. +#[inline(always)] +fn checked_shl_integer(lhs: i32, rhs: i32) -> Result { + match u32::try_from(rhs) { + Err(_) => Err(format!("Number of bits to << ({}) must be positive", rhs)), + Ok(bits) => Ok(lhs.checked_shl(bits).unwrap_or(0)), + } +} + +/// Custom implementation of checked right shift for error reporting purposes. +#[inline(always)] +fn checked_shr_integer(lhs: i32, rhs: i32) -> Result { + match u32::try_from(rhs) { + Err(_) => Err(format!("Number of bits to >> ({}) must be positive", rhs)), + Ok(bits) => Ok(match lhs.checked_shr(bits) { + Some(i) => i, + None if lhs < 0 => -1, + None => 0, + }), + } +} /// Custom implementation of checked integer subtractions for error reporting purposes. #[inline(always)] fn checked_sub_integer(lhs: i32, rhs: i32) -> Result { lhs.checked_sub(rhs).ok_or("Integer underflow") } +/// Custom implementation of checked bitwise XOR for error reporting purposes. +#[inline(always)] +fn checked_xor_integer(lhs: i32, rhs: i32) -> Result { + Ok(lhs ^ rhs) +} + /// Execution context for the virtual machine. /// /// This roughly corresponds to the concept of a "processor", making the VM the container of @@ -237,6 +276,10 @@ impl Context { Opcode::AddInteger => self.do_add_integer(instr), Opcode::Alloc => self.do_alloc(instr, heap), Opcode::AllocArray => self.do_alloc_array(instr, heap), + Opcode::BitwiseAnd => self.do_bitwise_and(instr), + Opcode::BitwiseNot => self.do_bitwise_not(instr), + Opcode::BitwiseOr => self.do_bitwise_or(instr), + Opcode::BitwiseXor => self.do_bitwise_xor(instr), Opcode::Call => self.do_call(instr), Opcode::Concat => self.do_concat(instr, &image.constants, heap), Opcode::DivideDouble => self.do_divide_double(instr), @@ -263,6 +306,8 @@ impl Context { Opcode::PowerDouble => self.do_power_double(instr), Opcode::PowerInteger => self.do_power_integer(instr), Opcode::Return => self.do_return(instr), + Opcode::ShiftLeft => self.do_shift_left(instr), + Opcode::ShiftRight => self.do_shift_right(instr), Opcode::StoreArray => self.do_store_array(instr, heap), Opcode::SubtractDouble => self.do_subtract_double(instr), Opcode::SubtractInteger => self.do_subtract_integer(instr), @@ -293,13 +338,14 @@ impl Context { /// Applies a binary integer operation using `parse` to decode the instruction and `op` to /// compute the result. `op` returns `Err` with a message on failure. - fn do_binary_integer_op( + fn do_binary_integer_op( &mut self, instr: u32, parse: fn(u32) -> (Register, Register, Register), op: F, ) where - F: Fn(i32, i32) -> Result, + F: Fn(i32, i32) -> Result, + E: ToString, { let (dest, src1, src2) = parse(instr); let lhs = self.get_reg(src1) as i32; @@ -310,7 +356,7 @@ impl Context { self.pc += 1; } Err(msg) => { - self.set_exception(msg); + self.set_exception(msg.to_string()); } } } @@ -377,6 +423,29 @@ impl Context { self.pc += 1; } + /// Implements the `BitwiseAnd` opcode. + pub(super) fn do_bitwise_and(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_bitwise_and, checked_and_integer); + } + + /// Implements the `BitwiseNot` opcode. + pub(super) fn do_bitwise_not(&mut self, instr: u32) { + let reg = bytecode::parse_bitwise_not(instr); + let value = self.get_reg(reg) as i32; + self.set_reg(reg, (!value) as u64); + self.pc += 1; + } + + /// Implements the `BitwiseOr` opcode. + pub(super) fn do_bitwise_or(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_bitwise_or, checked_or_integer); + } + + /// Implements the `BitwiseXor` opcode. + pub(super) fn do_bitwise_xor(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_bitwise_xor, checked_xor_integer); + } + /// Implements the `Call` opcode. pub(super) fn do_call(&mut self, instr: u32) { let (reg, offset) = bytecode::parse_call(instr); @@ -611,6 +680,16 @@ impl Context { } } + /// Implements the `ShiftLeft` opcode. + pub(super) fn do_shift_left(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_shift_left, checked_shl_integer); + } + + /// Implements the `ShiftRight` opcode. + pub(super) fn do_shift_right(&mut self, instr: u32) { + self.do_binary_integer_op(instr, bytecode::parse_shift_right, checked_shr_integer); + } + /// Implements the `StoreArray` opcode. pub(super) fn do_store_array(&mut self, instr: u32, heap: &mut [HeapDatum]) { let (arr_reg, val_reg, first_sub_reg) = bytecode::parse_store_array(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index eab928d0..35a77a6f 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -41,6 +41,12 @@ one_test!(test_arithmetic_pow); one_test!(test_arithmetic_sub); one_test!(test_arrays); one_test!(test_assignments); +one_test!(test_bitwise_and); +one_test!(test_bitwise_not); +one_test!(test_bitwise_or); +one_test!(test_bitwise_shl); +one_test!(test_bitwise_shr); +one_test!(test_bitwise_xor); one_test!(test_empty); one_test!(test_end); one_test!(test_functions); diff --git a/core2/tests/test_bitwise_and.md b/core2/tests/test_bitwise_and.md new file mode 100644 index 00000000..37105552 --- /dev/null +++ b/core2/tests/test_bitwise_and.md @@ -0,0 +1,88 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 12 AND 10 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 12 # 1:5 +0002: LOADI R66, 10 # 1:12 +0003: AND R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=8% +``` + +# Test: Type error with double + +## Source + +```basic +OUT 1.0 AND 2.0 +``` + +## Compilation errors + +```plain +1:9: Cannot AND DOUBLE and DOUBLE +``` + +# Test: Type error with string + +## Source + +```basic +OUT "a" AND "b" +``` + +## Compilation errors + +```plain +1:9: Cannot AND STRING and STRING +``` + +# Test: Two immediate booleans + +## Source + +```basic +OUT TRUE AND FALSE +OUT TRUE AND TRUE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 0 # 1:14 +0003: AND R65, R65, R66 # 1:10 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R65, 1 # 2:5 +0007: LOADI R66, 1 # 2:14 +0008: AND R65, R65, R66 # 2:10 +0009: LOADI R64, 256 # 2:5 +0010: UPCALL 0, R64 # 2:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +0=true? +``` diff --git a/core2/tests/test_bitwise_not.md b/core2/tests/test_bitwise_not.md new file mode 100644 index 00000000..35103ab8 --- /dev/null +++ b/core2/tests/test_bitwise_not.md @@ -0,0 +1,87 @@ +# Test: Immediate integer + +## Source + +```basic +OUT NOT 12 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 12 # 1:9 +0002: NOT R65 # 1:5 +0003: LOADI R64, 258 # 1:5 +0004: UPCALL 0, R64 # 1:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=-13% +``` + +# Test: Type error with double + +## Source + +```basic +OUT NOT 1.0 +``` + +## Compilation errors + +```plain +1:5: Expected INTEGER but found DOUBLE +``` + +# Test: Type error with string + +## Source + +```basic +OUT NOT "a" +``` + +## Compilation errors + +```plain +1:5: Expected INTEGER but found STRING +``` + +# Test: Immediate boolean + +## Source + +```basic +OUT NOT TRUE +OUT NOT FALSE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:9 +0002: LOADI R66, 1 # 1:5 +0003: XOR R65, R65, R66 # 1:5 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R65, 0 # 2:9 +0007: LOADI R66, 1 # 2:5 +0008: XOR R65, R65, R66 # 2:5 +0009: LOADI R64, 256 # 2:5 +0010: UPCALL 0, R64 # 2:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +0=true? +``` diff --git a/core2/tests/test_bitwise_or.md b/core2/tests/test_bitwise_or.md new file mode 100644 index 00000000..8d21997a --- /dev/null +++ b/core2/tests/test_bitwise_or.md @@ -0,0 +1,88 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 12 OR 10 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 12 # 1:5 +0002: LOADI R66, 10 # 1:11 +0003: OR R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=14% +``` + +# Test: Type error with double + +## Source + +```basic +OUT 1.0 OR 2.0 +``` + +## Compilation errors + +```plain +1:9: Cannot OR DOUBLE and DOUBLE +``` + +# Test: Type error with string + +## Source + +```basic +OUT "a" OR "b" +``` + +## Compilation errors + +```plain +1:9: Cannot OR STRING and STRING +``` + +# Test: Two immediate booleans + +## Source + +```basic +OUT TRUE OR FALSE +OUT FALSE OR FALSE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 0 # 1:13 +0003: OR R65, R65, R66 # 1:10 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R65, 0 # 2:5 +0007: LOADI R66, 0 # 2:14 +0008: OR R65, R65, R66 # 2:11 +0009: LOADI R64, 256 # 2:5 +0010: UPCALL 0, R64 # 2:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +0=false? +``` diff --git a/core2/tests/test_bitwise_shl.md b/core2/tests/test_bitwise_shl.md new file mode 100644 index 00000000..f3ab85ef --- /dev/null +++ b/core2/tests/test_bitwise_shl.md @@ -0,0 +1,122 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 3 << 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADI R66, 2 # 1:10 +0003: SHL R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=12% +``` + +# Test: Shift by zero + +## Source + +```basic +OUT 7 << 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 7 # 1:5 +0002: LOADI R66, 0 # 1:10 +0003: SHL R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=7% +``` + +# Test: Shift amount larger than 31 + +## Source + +```basic +OUT 1 << 32 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 32 # 1:10 +0003: SHL R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=0% +``` + +# Test: Type error with double + +## Source + +```basic +OUT 1.0 << 2 +``` + +## Compilation errors + +```plain +1:9: Cannot << DOUBLE and INTEGER +``` + +# Test: Negative shift amount + +## Source + +```basic +OUT 1 << -1 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 1 # 1:11 +0003: NEGI R66 # 1:10 +0004: SHL R65, R65, R66 # 1:7 +0005: LOADI R64, 258 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Runtime errors + +```plain +1:7: Number of bits to << (-1) must be positive +``` diff --git a/core2/tests/test_bitwise_shr.md b/core2/tests/test_bitwise_shr.md new file mode 100644 index 00000000..0ff7f658 --- /dev/null +++ b/core2/tests/test_bitwise_shr.md @@ -0,0 +1,178 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 12 >> 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 12 # 1:5 +0002: LOADI R66, 2 # 1:11 +0003: SHR R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=3% +``` + +# Test: Shift by zero + +## Source + +```basic +OUT 7 >> 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 7 # 1:5 +0002: LOADI R66, 0 # 1:10 +0003: SHR R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=7% +``` + +# Test: Shift amount larger than 31 (positive value) + +## Source + +```basic +OUT 1 >> 32 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 32 # 1:10 +0003: SHR R65, R65, R66 # 1:7 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=0% +``` + +# Test: Shift amount larger than 31 (negative value) + +## Source + +```basic +OUT -1 >> 32 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:6 +0002: NEGI R65 # 1:5 +0003: LOADI R66, 32 # 1:11 +0004: SHR R65, R65, R66 # 1:8 +0005: LOADI R64, 258 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=-1% +``` + +# Test: Arithmetic right shift (sign extension) + +## Source + +```basic +OUT -8 >> 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 8 # 1:6 +0002: NEGI R65 # 1:5 +0003: LOADI R66, 2 # 1:11 +0004: SHR R65, R65, R66 # 1:8 +0005: LOADI R64, 258 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=-2% +``` + +# Test: Type error with double + +## Source + +```basic +OUT 1.0 >> 2 +``` + +## Compilation errors + +```plain +1:9: Cannot >> DOUBLE and INTEGER +``` + +# Test: Negative shift amount + +## Source + +```basic +OUT 1 >> -1 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 1 # 1:11 +0003: NEGI R66 # 1:10 +0004: SHR R65, R65, R66 # 1:7 +0005: LOADI R64, 258 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Runtime errors + +```plain +1:7: Number of bits to >> (-1) must be positive +``` diff --git a/core2/tests/test_bitwise_xor.md b/core2/tests/test_bitwise_xor.md new file mode 100644 index 00000000..3203ec77 --- /dev/null +++ b/core2/tests/test_bitwise_xor.md @@ -0,0 +1,88 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 12 XOR 10 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 12 # 1:5 +0002: LOADI R66, 10 # 1:12 +0003: XOR R65, R65, R66 # 1:8 +0004: LOADI R64, 258 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=6% +``` + +# Test: Type error with double + +## Source + +```basic +OUT 1.0 XOR 2.0 +``` + +## Compilation errors + +```plain +1:9: Cannot XOR DOUBLE and DOUBLE +``` + +# Test: Type error with string + +## Source + +```basic +OUT "a" XOR "b" +``` + +## Compilation errors + +```plain +1:9: Cannot XOR STRING and STRING +``` + +# Test: Two immediate booleans + +## Source + +```basic +OUT TRUE XOR FALSE +OUT TRUE XOR TRUE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 0 # 1:14 +0003: XOR R65, R65, R66 # 1:10 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R65, 1 # 2:5 +0007: LOADI R66, 1 # 2:14 +0008: XOR R65, R65, R66 # 2:10 +0009: LOADI R64, 256 # 2:5 +0010: UPCALL 0, R64 # 2:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +0=false? +``` From 7cb7bb65fb9d52af5e65ba89cc2a4f8391477150 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 28 Feb 2026 07:41:41 -0800 Subject: [PATCH 19/53] core2: Compile unary expression chains iteratively Flatten unary operators together with binary chains before code generation. Compile the pending operations in one iterative pass so deep unary expressions no longer recurse and overflow the stack. Add integration tests with very deep negation and NOT chains to exercise this path and document the previous failure mode. --- core2/src/compiler/exprs.rs | 253 ++++++++++++++++------------ core2/tests/integration_test.rs | 2 + core2/tests/test_unary_neg_depth.md | 13 ++ core2/tests/test_unary_not_depth.md | 13 ++ 4 files changed, 177 insertions(+), 104 deletions(-) create mode 100644 core2/tests/test_unary_neg_depth.md create mode 100644 core2/tests/test_unary_not_depth.md diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index c3fa854f..bc98ba5a 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -70,8 +70,22 @@ fn compile_array_access( Ok(info.subtype) } -/// A pending binary operation waiting to be applied, used to flatten expressions and -/// avoid recursive calls during processing. +/// The type of unary operation waiting to be applied. +enum PendingUnaryKind { + Negate, + Not, +} + +/// A pending unary operation waiting to be applied. +struct PendingUnaryOp { + pos: LineCol, + kind: PendingUnaryKind, + make_boolean: Option u32>, + make_integer: fn(Register) -> u32, + make_double: Option u32>, +} + +/// A pending binary operation waiting to be applied. struct PendingBinaryOp { pos: LineCol, rhs: Expr, @@ -82,18 +96,24 @@ struct PendingBinaryOp { make_text: Option u32>, } -/// Peels the left-recursive chain of binary ops into a vector of pending binary ops to avoid -/// deep recursion. +/// A pending expression operation waiting to be applied, used to flatten expression chains +/// and avoid recursive calls during processing. +enum PendingOp { + Unary(PendingUnaryOp), + Binary(PendingBinaryOp), +} + +/// Peels the expression chain into a vector of pending ops to avoid deep recursion. /// -/// Returns the input `expr` holding the leftmost non-binary expression and the list -/// of pending ops. -fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { - let mut pending: Vec = vec![]; +/// Returns the input `expr` holding the innermost non-op expression and the list of +/// pending ops. +fn peel_ops(mut expr: Expr) -> (Expr, Vec) { + let mut pending: Vec = vec![]; loop { match expr { Expr::Add(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "+", @@ -101,13 +121,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_add_double), make_integer: bytecode::make_add_integer, make_text: Some(bytecode::make_concat), - }); + })); expr = span.lhs; } Expr::And(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "AND", @@ -115,13 +135,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: None, make_integer: bytecode::make_bitwise_and, make_text: None, - }); + })); expr = span.lhs; } Expr::Divide(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "/", @@ -129,13 +149,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_divide_double), make_integer: bytecode::make_divide_integer, make_text: None, - }); + })); expr = span.lhs; } Expr::Modulo(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "MOD", @@ -143,13 +163,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_modulo_double), make_integer: bytecode::make_modulo_integer, make_text: None, - }); + })); expr = span.lhs; } Expr::Multiply(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "*", @@ -157,13 +177,37 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_multiply_double), make_integer: bytecode::make_multiply_integer, make_text: None, - }); + })); expr = span.lhs; } + Expr::Negate(span) => { + let span = *span; + pending.push(PendingOp::Unary(PendingUnaryOp { + pos: span.pos, + kind: PendingUnaryKind::Negate, + make_boolean: None, + make_integer: bytecode::make_negate_integer, + make_double: Some(bytecode::make_negate_double), + })); + expr = span.expr; + } + + Expr::Not(span) => { + let span = *span; + pending.push(PendingOp::Unary(PendingUnaryOp { + pos: span.pos, + kind: PendingUnaryKind::Not, + make_boolean: Some(bytecode::make_bitwise_xor), + make_integer: bytecode::make_bitwise_not, + make_double: None, + })); + expr = span.expr; + } + Expr::Or(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "OR", @@ -171,13 +215,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: None, make_integer: bytecode::make_bitwise_or, make_text: None, - }); + })); expr = span.lhs; } Expr::Power(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "^", @@ -185,13 +229,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_power_double), make_integer: bytecode::make_power_integer, make_text: None, - }); + })); expr = span.lhs; } Expr::ShiftLeft(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "<<", @@ -199,13 +243,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: None, make_integer: bytecode::make_shift_left, make_text: None, - }); + })); expr = span.lhs; } Expr::ShiftRight(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: ">>", @@ -213,12 +257,12 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: None, make_integer: bytecode::make_shift_right, make_text: None, - }); + })); expr = span.lhs; } Expr::Subtract(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "-", @@ -226,13 +270,13 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: Some(bytecode::make_subtract_double), make_integer: bytecode::make_subtract_integer, make_text: None, - }); + })); expr = span.lhs; } Expr::Xor(span) => { let span = *span; - pending.push(PendingBinaryOp { + pending.push(PendingOp::Binary(PendingBinaryOp { pos: span.pos, rhs: span.rhs, op_name: "XOR", @@ -240,9 +284,10 @@ fn peel_binary_ops(mut expr: Expr) -> (Expr, Vec) { make_double: None, make_integer: bytecode::make_bitwise_xor, make_text: None, - }); + })); expr = span.lhs; } + _ => break, } } @@ -259,48 +304,82 @@ fn compile_pending_ops( symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, reg: Register, mut etype: ExprType, - mut pending: Vec, + mut pending: Vec, ) -> Result { while let Some(op) = pending.pop() { - let rpos = op.rhs.start_pos(); - let mut scope = symtable.temp_scope(); - let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; - let rtype = compile_expr(codegen, symtable, rtemp, op.rhs)?; - - let result_type = match (etype, rtype) { - (ExprType::Double, ExprType::Double) if op.make_double.is_some() => ExprType::Double, - (ExprType::Boolean, ExprType::Boolean) if op.make_boolean.is_some() => { - ExprType::Boolean - } - (ExprType::Integer, ExprType::Integer) => ExprType::Integer, - (ExprType::Text, ExprType::Text) if op.make_text.is_some() => ExprType::Text, - (ExprType::Double, ExprType::Integer) if op.make_double.is_some() => { - codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); - ExprType::Double - } - (ExprType::Integer, ExprType::Double) if op.make_double.is_some() => { - codegen.emit(bytecode::make_integer_to_double(reg), op.pos); - ExprType::Double + match op { + PendingOp::Unary(op) => { + let result_type = match etype { + ExprType::Boolean if op.make_boolean.is_some() => ExprType::Boolean, + ExprType::Double if op.make_double.is_some() => ExprType::Double, + ExprType::Integer => ExprType::Integer, + _ => match op.kind { + PendingUnaryKind::Negate => return Err(Error::NotANumber(op.pos, etype)), + PendingUnaryKind::Not => { + return Err(Error::TypeMismatch(op.pos, etype, ExprType::Integer)); + } + }, + }; + match result_type { + ExprType::Boolean => { + let mut scope = symtable.temp_scope(); + let temp = scope.alloc().map_err(|e| Error::from_syms(e, op.pos))?; + codegen.emit_value(temp, ConstantDatum::Integer(1), op.pos)?; + codegen.emit(op.make_boolean.unwrap()(reg, reg, temp), op.pos); + } + ExprType::Double => { + codegen.emit(op.make_double.unwrap()(reg), op.pos); + } + ExprType::Integer => { + codegen.emit((op.make_integer)(reg), op.pos); + } + ExprType::Text => unreachable!(), + } + etype = result_type; } - _ => return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)), - }; + PendingOp::Binary(op) => { + let rpos = op.rhs.start_pos(); + let mut scope = symtable.temp_scope(); + let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; + let rtype = compile_expr(codegen, symtable, rtemp, op.rhs)?; + + let result_type = match (etype, rtype) { + (ExprType::Boolean, ExprType::Boolean) if op.make_boolean.is_some() => { + ExprType::Boolean + } + (ExprType::Double, ExprType::Double) if op.make_double.is_some() => { + ExprType::Double + } + (ExprType::Integer, ExprType::Integer) => ExprType::Integer, + (ExprType::Text, ExprType::Text) if op.make_text.is_some() => ExprType::Text, + (ExprType::Double, ExprType::Integer) if op.make_double.is_some() => { + codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); + ExprType::Double + } + (ExprType::Integer, ExprType::Double) if op.make_double.is_some() => { + codegen.emit(bytecode::make_integer_to_double(reg), op.pos); + ExprType::Double + } + _ => return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)), + }; - match result_type { - ExprType::Boolean => { - codegen.emit(op.make_boolean.unwrap()(reg, reg, rtemp), op.pos); - } - ExprType::Double => { - codegen.emit(op.make_double.unwrap()(reg, reg, rtemp), op.pos); - } - ExprType::Integer => { - codegen.emit((op.make_integer)(reg, reg, rtemp), op.pos); - } - ExprType::Text => { - codegen.emit(op.make_text.unwrap()(reg, reg, rtemp), op.pos); + match result_type { + ExprType::Boolean => { + codegen.emit(op.make_boolean.unwrap()(reg, reg, rtemp), op.pos); + } + ExprType::Double => { + codegen.emit(op.make_double.unwrap()(reg, reg, rtemp), op.pos); + } + ExprType::Integer => { + codegen.emit((op.make_integer)(reg, reg, rtemp), op.pos); + } + ExprType::Text => { + codegen.emit(op.make_text.unwrap()(reg, reg, rtemp), op.pos); + } + } + etype = result_type; } } - - etype = result_type; } Ok(etype) @@ -316,7 +395,7 @@ pub(super) fn compile_expr( reg: Register, expr: Expr, ) -> Result { - let (expr, pending) = peel_binary_ops(expr); + let (expr, pending) = peel_ops(expr); let etype = match expr { Expr::Add(..) @@ -324,12 +403,14 @@ pub(super) fn compile_expr( | Expr::Divide(..) | Expr::Modulo(..) | Expr::Multiply(..) + | Expr::Negate(..) + | Expr::Not(..) | Expr::Or(..) | Expr::Power(..) | Expr::ShiftLeft(..) | Expr::ShiftRight(..) | Expr::Subtract(..) - | Expr::Xor(..) => unreachable!("Peeled by peel_binary_ops"), + | Expr::Xor(..) => unreachable!("Peeled by peel_ops"), Expr::Boolean(span) => { codegen.emit_value(reg, ConstantDatum::Boolean(span.value), span.pos)?; @@ -405,42 +486,6 @@ pub(super) fn compile_expr( Ok(ExprType::Integer) } - Expr::Negate(span) => { - let pos = span.pos; - let etype = compile_expr(codegen, symtable, reg, span.expr)?; - match etype { - ExprType::Double => { - codegen.emit(bytecode::make_negate_double(reg), pos); - } - ExprType::Integer => { - codegen.emit(bytecode::make_negate_integer(reg), pos); - } - _ => { - return Err(Error::NotANumber(pos, etype)); - } - } - Ok(etype) - } - - Expr::Not(span) => { - let pos = span.pos; - let etype = compile_expr(codegen, symtable, reg, span.expr)?; - match etype { - ExprType::Boolean => { - let mut scope = symtable.temp_scope(); - let temp = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; - codegen.emit_value(temp, ConstantDatum::Integer(1), pos)?; - codegen.emit(bytecode::make_bitwise_xor(reg, reg, temp), pos); - Ok(ExprType::Boolean) - } - ExprType::Integer => { - codegen.emit(bytecode::make_bitwise_not(reg), pos); - Ok(ExprType::Integer) - } - _ => Err(Error::TypeMismatch(pos, etype, ExprType::Integer)), - } - } - Expr::Symbol(span) => match symtable.get_local_or_global(&span.vref) { Ok((local, SymbolPrototype::Scalar(etype))) => { codegen.emit(bytecode::make_move(reg, local), span.pos); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 35a77a6f..453db3f2 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -58,6 +58,8 @@ one_test!(test_out_of_registers); one_test!(test_strings); one_test!(test_subs); one_test!(test_types); +one_test!(test_unary_neg_depth); +one_test!(test_unary_not_depth); #[test] fn test_all_md_files_registered() -> io::Result<()> { diff --git a/core2/tests/test_unary_neg_depth.md b/core2/tests/test_unary_neg_depth.md new file mode 100644 index 00000000..c999e6eb --- /dev/null +++ b/core2/tests/test_unary_neg_depth.md @@ -0,0 +1,13 @@ +# Test: Long unary negation chain with type error + +## Source + +```basic +OUT - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - "a" +``` + +## Compilation errors + +```plain +1:40003: STRING is not a number +``` diff --git a/core2/tests/test_unary_not_depth.md b/core2/tests/test_unary_not_depth.md new file mode 100644 index 00000000..646a7bd3 --- /dev/null +++ b/core2/tests/test_unary_not_depth.md @@ -0,0 +1,13 @@ +# Test: Long unary NOT chain with type error + +## Source + +```basic +OUT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT NOT 1.0 +``` + +## Compilation errors + +```plain +1:80001: Expected INTEGER but found DOUBLE +``` From 8d6d9d6ee33bc42404ffe72cc5b8648468ec4d40 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 1 Mar 2026 07:21:59 -0800 Subject: [PATCH 20/53] core2: Implement relational operators Compile and execute relational operators in the core2 expression pipeline. Support equality and inequality across booleans, numbers, and strings, and support ordering comparisons across numbers and strings with numeric promotion. Add typed comparison bytecode instructions that follow the CMP<2-letter-op><1-letter-type> naming scheme, wire them into disassembly and VM execution, and add integration tests for =, <>, <, <=, >, and >=. This completes the implementation of the new expression compiler in core2 and brings feature parity with core. --- core2/src/bytecode.rs | 420 ++++++++++++++++++++++++++++++ core2/src/compiler/exprs.rs | 224 ++++++++++++++-- core2/src/image.rs | 20 ++ core2/src/vm/context.rs | 242 +++++++++++++++++ core2/tests/integration_test.rs | 6 + core2/tests/test_relational_eq.md | 149 +++++++++++ core2/tests/test_relational_ge.md | 122 +++++++++ core2/tests/test_relational_gt.md | 122 +++++++++ core2/tests/test_relational_le.md | 122 +++++++++ core2/tests/test_relational_lt.md | 122 +++++++++ core2/tests/test_relational_ne.md | 149 +++++++++++ 11 files changed, 1674 insertions(+), 24 deletions(-) create mode 100644 core2/tests/test_relational_eq.md create mode 100644 core2/tests/test_relational_ge.md create mode 100644 core2/tests/test_relational_gt.md create mode 100644 core2/tests/test_relational_le.md create mode 100644 core2/tests/test_relational_lt.md create mode 100644 core2/tests/test_relational_ne.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index bdde36c6..1dc3f7fa 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -408,12 +408,42 @@ pub(crate) enum Opcode { /// Converts the double value in a register to an integer. DoubleToInteger, + /// Compares two booleans for equality and stores the result into a third one. + EqualBoolean, + + /// Compares two doubles for equality and stores the result into a third one. + EqualDouble, + + /// Compares two integers for equality and stores the result into a third one. + EqualInteger, + + /// Compares two strings for equality and stores the result into a third one. + EqualText, + /// Allocates local registers (locals and temporaries) when entering a scope. Enter, /// Jumps to a subroutine at an address relative to the PC. Gosub, + /// Compares two doubles for greater-than and stores the result into a third one. + GreaterDouble, + + /// Compares two doubles for greater-than-or-equal and stores the result into a third one. + GreaterEqualDouble, + + /// Compares two integers for greater-than-or-equal and stores the result into a third one. + GreaterEqualInteger, + + /// Compares two strings for greater-than-or-equal and stores the result into a third one. + GreaterEqualText, + + /// Compares two integers for greater-than and stores the result into a third one. + GreaterInteger, + + /// Compares two strings for greater-than and stores the result into a third one. + GreaterText, + /// Converts the integer value in a register to a double. IntegerToDouble, @@ -423,6 +453,24 @@ pub(crate) enum Opcode { /// Deallocates the registers allocated by the preamble ENTER, unwinding to the FP. Leave, + /// Compares two doubles for less-than and stores the result into a third one. + LessDouble, + + /// Compares two doubles for less-than-or-equal and stores the result into a third one. + LessEqualDouble, + + /// Compares two integers for less-than-or-equal and stores the result into a third one. + LessEqualInteger, + + /// Compares two strings for less-than-or-equal and stores the result into a third one. + LessEqualText, + + /// Compares two integers for less-than and stores the result into a third one. + LessInteger, + + /// Compares two strings for less-than and stores the result into a third one. + LessText, + /// Loads an element from an array. LoadArray, @@ -456,6 +504,18 @@ pub(crate) enum Opcode { /// Negates an integer value in place. NegateInteger, + /// Compares two booleans for inequality and stores the result into a third one. + NotEqualBoolean, + + /// Compares two doubles for inequality and stores the result into a third one. + NotEqualDouble, + + /// Compares two integers for inequality and stores the result into a third one. + NotEqualInteger, + + /// Compares two strings for inequality and stores the result into a third one. + NotEqualText, + /// The "null" instruction, used by the compiler to pad the code for fixups. Nop, @@ -604,6 +664,42 @@ instr!( Register, 0x000000ff, 0, // Register with the value to convert. ); +#[rustfmt::skip] +instr!( + Opcode::EqualBoolean, "CMPEQB", + make_equal_boolean, parse_equal_boolean, format_equal_boolean, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::EqualDouble, "CMPEQD", + make_equal_double, parse_equal_double, format_equal_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::EqualInteger, "CMPEQI", + make_equal_integer, parse_equal_integer, format_equal_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::EqualText, "CMPEQS", + make_equal_text, parse_equal_text, format_equal_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::End, "END", @@ -625,6 +721,60 @@ instr!( u16, 0x0000ffff, 0, // Target address. ); +#[rustfmt::skip] +instr!( + Opcode::GreaterDouble, "CMPGTD", + make_greater_double, parse_greater_double, format_greater_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::GreaterEqualDouble, "CMPGED", + make_greater_equal_double, parse_greater_equal_double, format_greater_equal_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::GreaterEqualInteger, "CMPGEI", + make_greater_equal_integer, parse_greater_equal_integer, format_greater_equal_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::GreaterEqualText, "CMPGES", + make_greater_equal_text, parse_greater_equal_text, format_greater_equal_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::GreaterInteger, "CMPGTI", + make_greater_integer, parse_greater_integer, format_greater_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::GreaterText, "CMPGTS", + make_greater_text, parse_greater_text, format_greater_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::IntegerToDouble, "ITOD", @@ -645,6 +795,60 @@ instr!( make_leave, parse_leave, format_leave, ); +#[rustfmt::skip] +instr!( + Opcode::LessDouble, "CMPLTD", + make_less_double, parse_less_double, format_less_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::LessEqualDouble, "CMPLED", + make_less_equal_double, parse_less_equal_double, format_less_equal_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::LessEqualInteger, "CMPLEI", + make_less_equal_integer, parse_less_equal_integer, format_less_equal_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::LessEqualText, "CMPLES", + make_less_equal_text, parse_less_equal_text, format_less_equal_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::LessInteger, "CMPLTI", + make_less_integer, parse_less_integer, format_less_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::LessText, "CMPLTS", + make_less_text, parse_less_text, format_less_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::LoadArray, "LOADA", @@ -742,6 +946,42 @@ instr!( Register, 0x000000ff, 0, // Register with the value to negate in place. ); +#[rustfmt::skip] +instr!( + Opcode::NotEqualBoolean, "CMPNEB", + make_not_equal_boolean, parse_not_equal_boolean, format_not_equal_boolean, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::NotEqualDouble, "CMPNED", + make_not_equal_double, parse_not_equal_double, format_not_equal_double, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::NotEqualInteger, "CMPNEI", + make_not_equal_integer, parse_not_equal_integer, format_not_equal_integer, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + +#[rustfmt::skip] +instr!( + Opcode::NotEqualText, "CMPNES", + make_not_equal_text, parse_not_equal_text, format_not_equal_text, + Register, 0x000000ff, 16, // Destination register to store the result of the operation. + Register, 0x000000ff, 8, // Left hand side value. + Register, 0x000000ff, 0, // Right hand side value. +); + #[rustfmt::skip] instr!( Opcode::Nop, "NOP", @@ -1042,12 +1282,102 @@ mod tests { Register::local(1).unwrap() ); + test_instr!( + test_equal_boolean, + make_equal_boolean, + parse_equal_boolean, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_equal_double, + make_equal_double, + parse_equal_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_equal_integer, + make_equal_integer, + parse_equal_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_equal_text, + make_equal_text, + parse_equal_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_end, make_end, parse_end, Register::local(1).unwrap()); test_instr!(test_enter, make_enter, parse_enter, 10); test_instr!(test_gosub, make_gosub, parse_gosub, 12345); + test_instr!( + test_greater_double, + make_greater_double, + parse_greater_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_greater_equal_double, + make_greater_equal_double, + parse_greater_equal_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_greater_equal_integer, + make_greater_equal_integer, + parse_greater_equal_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_greater_equal_text, + make_greater_equal_text, + parse_greater_equal_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_greater_integer, + make_greater_integer, + parse_greater_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_greater_text, + make_greater_text, + parse_greater_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_integer_to_double, make_integer_to_double, @@ -1059,6 +1389,60 @@ mod tests { test_instr!(test_leave, make_leave, parse_leave); + test_instr!( + test_less_double, + make_less_double, + parse_less_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_less_equal_double, + make_less_equal_double, + parse_less_equal_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_less_equal_integer, + make_less_equal_integer, + parse_less_equal_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_less_equal_text, + make_less_equal_text, + parse_less_equal_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_less_integer, + make_less_integer, + parse_less_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_less_text, + make_less_text, + parse_less_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!( test_load_array, make_load_array, @@ -1151,6 +1535,42 @@ mod tests { Register::local(1).unwrap() ); + test_instr!( + test_not_equal_boolean, + make_not_equal_boolean, + parse_not_equal_boolean, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_not_equal_double, + make_not_equal_double, + parse_not_equal_double, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_not_equal_integer, + make_not_equal_integer, + parse_not_equal_integer, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + + test_instr!( + test_not_equal_text, + make_not_equal_text, + parse_not_equal_text, + Register::local(1).unwrap(), + Register::local(2).unwrap(), + Register::local(3).unwrap() + ); + test_instr!(test_nop, make_nop, parse_nop); test_instr!( diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index bc98ba5a..24439514 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -96,11 +96,23 @@ struct PendingBinaryOp { make_text: Option u32>, } +/// A pending relational operation waiting to be applied. +struct PendingRelationalOp { + pos: LineCol, + rhs: Expr, + op_name: &'static str, + make_boolean: Option u32>, + make_double: Option u32>, + make_integer: Option u32>, + make_text: Option u32>, +} + /// A pending expression operation waiting to be applied, used to flatten expression chains /// and avoid recursive calls during processing. enum PendingOp { Unary(PendingUnaryOp), Binary(PendingBinaryOp), + Relational(PendingRelationalOp), } /// Peels the expression chain into a vector of pending ops to avoid deep recursion. @@ -153,6 +165,76 @@ fn peel_ops(mut expr: Expr) -> (Expr, Vec) { expr = span.lhs; } + Expr::Equal(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: "=", + make_boolean: Some(bytecode::make_equal_boolean), + make_double: Some(bytecode::make_equal_double), + make_integer: Some(bytecode::make_equal_integer), + make_text: Some(bytecode::make_equal_text), + })); + expr = span.lhs; + } + + Expr::Greater(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: ">", + make_boolean: None, + make_double: Some(bytecode::make_greater_double), + make_integer: Some(bytecode::make_greater_integer), + make_text: Some(bytecode::make_greater_text), + })); + expr = span.lhs; + } + + Expr::GreaterEqual(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: ">=", + make_boolean: None, + make_double: Some(bytecode::make_greater_equal_double), + make_integer: Some(bytecode::make_greater_equal_integer), + make_text: Some(bytecode::make_greater_equal_text), + })); + expr = span.lhs; + } + + Expr::Less(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: "<", + make_boolean: None, + make_double: Some(bytecode::make_less_double), + make_integer: Some(bytecode::make_less_integer), + make_text: Some(bytecode::make_less_text), + })); + expr = span.lhs; + } + + Expr::LessEqual(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: "<=", + make_boolean: None, + make_double: Some(bytecode::make_less_equal_double), + make_integer: Some(bytecode::make_less_equal_integer), + make_text: Some(bytecode::make_less_equal_text), + })); + expr = span.lhs; + } + Expr::Modulo(span) => { let span = *span; pending.push(PendingOp::Binary(PendingBinaryOp { @@ -205,6 +287,20 @@ fn peel_ops(mut expr: Expr) -> (Expr, Vec) { expr = span.expr; } + Expr::NotEqual(span) => { + let span = *span; + pending.push(PendingOp::Relational(PendingRelationalOp { + pos: span.pos, + rhs: span.rhs, + op_name: "<>", + make_boolean: Some(bytecode::make_not_equal_boolean), + make_double: Some(bytecode::make_not_equal_double), + make_integer: Some(bytecode::make_not_equal_integer), + make_text: Some(bytecode::make_not_equal_text), + })); + expr = span.lhs; + } + Expr::Or(span) => { let span = *span; pending.push(PendingOp::Binary(PendingBinaryOp { @@ -294,6 +390,59 @@ fn peel_ops(mut expr: Expr) -> (Expr, Vec) { (expr, pending) } +/// Emits a cast in `reg` to convert from `from` to `to` if these are numerical types. +/// +/// Returns true if the conversion is valid, regardless of whether a cast was needed. +fn cast_numerical_type( + codegen: &mut Codegen, + reg: Register, + from: ExprType, + to: ExprType, + pos: LineCol, +) -> bool { + match (from, to) { + (ExprType::Double, ExprType::Integer) => { + codegen.emit(bytecode::make_double_to_integer(reg), pos); + true + } + (ExprType::Integer, ExprType::Double) => { + codegen.emit(bytecode::make_integer_to_double(reg), pos); + true + } + (ExprType::Double, ExprType::Double) | (ExprType::Integer, ExprType::Integer) => true, + _ => false, + } +} + +/// Resolves the numeric operand type for a binary operation and emits any required casts. +fn resolve_numeric_binary_type( + codegen: &mut Codegen, + reg: Register, + etype: ExprType, + rtemp: Register, + rtype: ExprType, + rpos: LineCol, + op_pos: LineCol, +) -> Option { + match (etype, rtype) { + (ExprType::Double, ExprType::Double) => Some(ExprType::Double), + (ExprType::Integer, ExprType::Integer) => Some(ExprType::Integer), + (ExprType::Double, ExprType::Integer) => { + let cast_ok = + cast_numerical_type(codegen, rtemp, ExprType::Integer, ExprType::Double, rpos); + debug_assert!(cast_ok); + Some(ExprType::Double) + } + (ExprType::Integer, ExprType::Double) => { + let cast_ok = + cast_numerical_type(codegen, reg, ExprType::Integer, ExprType::Double, op_pos); + debug_assert!(cast_ok); + Some(ExprType::Double) + } + _ => None, + } +} + /// Processes `pending` binary ops from innermost to outermost, using `reg` as the /// accumulator. /// @@ -347,19 +496,18 @@ fn compile_pending_ops( (ExprType::Boolean, ExprType::Boolean) if op.make_boolean.is_some() => { ExprType::Boolean } - (ExprType::Double, ExprType::Double) if op.make_double.is_some() => { - ExprType::Double - } - (ExprType::Integer, ExprType::Integer) => ExprType::Integer, (ExprType::Text, ExprType::Text) if op.make_text.is_some() => ExprType::Text, - (ExprType::Double, ExprType::Integer) if op.make_double.is_some() => { - codegen.emit(bytecode::make_integer_to_double(rtemp), rpos); - ExprType::Double - } - (ExprType::Integer, ExprType::Double) if op.make_double.is_some() => { - codegen.emit(bytecode::make_integer_to_double(reg), op.pos); - ExprType::Double + (_, _) if op.make_double.is_some() => { + match resolve_numeric_binary_type( + codegen, reg, etype, rtemp, rtype, rpos, op.pos, + ) { + Some(v) => v, + None => { + return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)); + } + } } + (ExprType::Integer, ExprType::Integer) => ExprType::Integer, _ => return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)), }; @@ -379,6 +527,40 @@ fn compile_pending_ops( } etype = result_type; } + + PendingOp::Relational(op) => { + let rpos = op.rhs.start_pos(); + let mut scope = symtable.temp_scope(); + let rtemp = scope.alloc().map_err(|e| Error::from_syms(e, rpos))?; + let rtype = compile_expr(codegen, symtable, rtemp, op.rhs)?; + + let make_opcode = match (etype, rtype) { + (ExprType::Boolean, ExprType::Boolean) if op.make_boolean.is_some() => { + op.make_boolean.unwrap() + } + + (ExprType::Text, ExprType::Text) if op.make_text.is_some() => { + op.make_text.unwrap() + } + + (_, _) if op.make_double.is_some() || op.make_integer.is_some() => { + match resolve_numeric_binary_type( + codegen, reg, etype, rtemp, rtype, rpos, op.pos, + ) { + Some(ExprType::Double) => op.make_double.expect("Must exist"), + Some(ExprType::Integer) => op.make_integer.expect("Must exist"), + _ => { + return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)); + } + } + } + + _ => return Err(Error::BinaryOpType(op.pos, op.op_name, etype, rtype)), + }; + + codegen.emit(make_opcode(reg, reg, rtemp), op.pos); + etype = ExprType::Boolean; + } } } @@ -401,10 +583,16 @@ pub(super) fn compile_expr( Expr::Add(..) | Expr::And(..) | Expr::Divide(..) + | Expr::Equal(..) + | Expr::Greater(..) + | Expr::GreaterEqual(..) + | Expr::Less(..) + | Expr::LessEqual(..) | Expr::Modulo(..) | Expr::Multiply(..) | Expr::Negate(..) | Expr::Not(..) + | Expr::NotEqual(..) | Expr::Or(..) | Expr::Power(..) | Expr::ShiftLeft(..) @@ -536,8 +724,6 @@ pub(super) fn compile_expr( codegen.emit_value(reg, ConstantDatum::Text(span.value), span.pos)?; Ok(ExprType::Text) } - - _ => todo!(), }?; compile_pending_ops(codegen, symtable, reg, etype, pending) @@ -554,17 +740,7 @@ pub(super) fn compile_expr_as_type( ) -> Result<()> { let epos = expr.start_pos(); let etype = compile_expr(codegen, symtable, reg, expr)?; - if etype == ExprType::Double && target.is_numerical() { - if target == ExprType::Integer { - codegen.emit(bytecode::make_double_to_integer(reg), epos); - } - Ok(()) - } else if etype == ExprType::Integer && target.is_numerical() { - if target == ExprType::Double { - codegen.emit(bytecode::make_integer_to_double(reg), epos); - } - Ok(()) - } else if etype == target { + if etype == target || cast_numerical_type(codegen, reg, etype, target, epos) { Ok(()) } else { if target.is_numerical() { diff --git a/core2/src/image.rs b/core2/src/image.rs index d63f59cf..9441b388 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -40,12 +40,28 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::DivideDouble => bytecode::format_divide_double(instr), Opcode::DivideInteger => bytecode::format_divide_integer(instr), Opcode::DoubleToInteger => bytecode::format_double_to_integer(instr), + Opcode::EqualBoolean => bytecode::format_equal_boolean(instr), + Opcode::EqualDouble => bytecode::format_equal_double(instr), + Opcode::EqualInteger => bytecode::format_equal_integer(instr), + Opcode::EqualText => bytecode::format_equal_text(instr), Opcode::End => bytecode::format_end(instr), Opcode::Enter => bytecode::format_enter(instr), Opcode::Gosub => bytecode::format_gosub(instr), + Opcode::GreaterDouble => bytecode::format_greater_double(instr), + Opcode::GreaterEqualDouble => bytecode::format_greater_equal_double(instr), + Opcode::GreaterEqualInteger => bytecode::format_greater_equal_integer(instr), + Opcode::GreaterEqualText => bytecode::format_greater_equal_text(instr), + Opcode::GreaterInteger => bytecode::format_greater_integer(instr), + Opcode::GreaterText => bytecode::format_greater_text(instr), Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), Opcode::Jump => bytecode::format_jump(instr), Opcode::Leave => bytecode::format_leave(instr), + Opcode::LessDouble => bytecode::format_less_double(instr), + Opcode::LessEqualDouble => bytecode::format_less_equal_double(instr), + Opcode::LessEqualInteger => bytecode::format_less_equal_integer(instr), + Opcode::LessEqualText => bytecode::format_less_equal_text(instr), + Opcode::LessInteger => bytecode::format_less_integer(instr), + Opcode::LessText => bytecode::format_less_text(instr), Opcode::LoadArray => bytecode::format_load_array(instr), Opcode::LoadConstant => bytecode::format_load_constant(instr), Opcode::LoadInteger => bytecode::format_load_integer(instr), @@ -57,6 +73,10 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::MultiplyInteger => bytecode::format_multiply_integer(instr), Opcode::NegateDouble => bytecode::format_negate_double(instr), Opcode::NegateInteger => bytecode::format_negate_integer(instr), + Opcode::NotEqualBoolean => bytecode::format_not_equal_boolean(instr), + Opcode::NotEqualDouble => bytecode::format_not_equal_double(instr), + Opcode::NotEqualInteger => bytecode::format_not_equal_integer(instr), + Opcode::NotEqualText => bytecode::format_not_equal_text(instr), Opcode::Nop => bytecode::format_nop(instr), Opcode::PowerDouble => bytecode::format_power_double(instr), Opcode::PowerInteger => bytecode::format_power_integer(instr), diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 6214641b..0f34229e 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -285,12 +285,30 @@ impl Context { Opcode::DivideDouble => self.do_divide_double(instr), Opcode::DivideInteger => self.do_divide_integer(instr), Opcode::DoubleToInteger => self.do_double_to_integer(instr), + Opcode::EqualBoolean => self.do_equal_boolean(instr), + Opcode::EqualDouble => self.do_equal_double(instr), + Opcode::EqualInteger => self.do_equal_integer(instr), + Opcode::EqualText => self.do_equal_text(instr, &image.constants, heap), Opcode::End => self.do_end(instr), Opcode::Enter => self.do_enter(instr), Opcode::Gosub => self.do_gosub(instr), + Opcode::GreaterDouble => self.do_greater_double(instr), + Opcode::GreaterEqualDouble => self.do_greater_equal_double(instr), + Opcode::GreaterEqualInteger => self.do_greater_equal_integer(instr), + Opcode::GreaterEqualText => { + self.do_greater_equal_text(instr, &image.constants, heap) + } + Opcode::GreaterInteger => self.do_greater_integer(instr), + Opcode::GreaterText => self.do_greater_text(instr, &image.constants, heap), Opcode::IntegerToDouble => self.do_integer_to_double(instr), Opcode::Jump => self.do_jump(instr), Opcode::Leave => self.do_leave(instr), + Opcode::LessDouble => self.do_less_double(instr), + Opcode::LessEqualDouble => self.do_less_equal_double(instr), + Opcode::LessEqualInteger => self.do_less_equal_integer(instr), + Opcode::LessEqualText => self.do_less_equal_text(instr, &image.constants, heap), + Opcode::LessInteger => self.do_less_integer(instr), + Opcode::LessText => self.do_less_text(instr, &image.constants, heap), Opcode::LoadArray => self.do_load_array(instr, heap), Opcode::LoadConstant => self.do_load_constant(instr, &image.constants), Opcode::LoadInteger => self.do_load_integer(instr), @@ -302,6 +320,10 @@ impl Context { Opcode::MultiplyInteger => self.do_multiply_integer(instr), Opcode::NegateDouble => self.do_negate_double(instr), Opcode::NegateInteger => self.do_negate_integer(instr), + Opcode::NotEqualBoolean => self.do_not_equal_boolean(instr), + Opcode::NotEqualDouble => self.do_not_equal_double(instr), + Opcode::NotEqualInteger => self.do_not_equal_integer(instr), + Opcode::NotEqualText => self.do_not_equal_text(instr, &image.constants, heap), Opcode::Nop => self.do_nop(instr), Opcode::PowerDouble => self.do_power_double(instr), Opcode::PowerInteger => self.do_power_integer(instr), @@ -336,6 +358,23 @@ impl Context { self.pc += 1; } + /// Applies a binary double predicate using `parse` to decode the instruction and `op` to + /// compute the result. + fn do_binary_double_predicate_op( + &mut self, + instr: u32, + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(f64, f64) -> bool, + { + let (dest, src1, src2) = parse(instr); + let lhs = f64::from_bits(self.get_reg(src1)); + let rhs = f64::from_bits(self.get_reg(src2)); + self.set_reg(dest, if op(lhs, rhs) { 1 } else { 0 }); + self.pc += 1; + } + /// Applies a binary integer operation using `parse` to decode the instruction and `op` to /// compute the result. `op` returns `Err` with a message on failure. fn do_binary_integer_op( @@ -361,6 +400,59 @@ impl Context { } } + /// Applies a binary integer predicate using `parse` to decode the instruction and `op` to + /// compute the result. + fn do_binary_integer_predicate_op( + &mut self, + instr: u32, + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(i32, i32) -> bool, + { + let (dest, src1, src2) = parse(instr); + let lhs = self.get_reg(src1) as i32; + let rhs = self.get_reg(src2) as i32; + self.set_reg(dest, if op(lhs, rhs) { 1 } else { 0 }); + self.pc += 1; + } + + /// Applies a binary boolean operation using `parse` to decode the instruction and `op` to + /// compute the result. + fn do_binary_boolean_op( + &mut self, + instr: u32, + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(bool, bool) -> bool, + { + let (dest, src1, src2) = parse(instr); + let lhs = self.get_reg(src1) != 0; + let rhs = self.get_reg(src2) != 0; + self.set_reg(dest, if op(lhs, rhs) { 1 } else { 0 }); + self.pc += 1; + } + + /// Applies a binary text operation using `parse` to decode the instruction and `op` to + /// compute the result. + fn do_binary_text_op( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + parse: fn(u32) -> (Register, Register, Register), + op: F, + ) where + F: Fn(&str, &str) -> bool, + { + let (dest, src1, src2) = parse(instr); + let lhs = self.deref_string(src1, constants, heap); + let rhs = self.deref_string(src2, constants, heap); + self.set_reg(dest, if op(lhs, rhs) { 1 } else { 0 }); + self.pc += 1; + } + /// Implements the `AddDouble` opcode. pub(super) fn do_add_double(&mut self, instr: u32) { self.do_binary_double_op(instr, bytecode::parse_add_double, |l, r| l + r); @@ -491,6 +583,31 @@ impl Context { self.pc += 1; } + /// Implements the `EqualBoolean` opcode. + pub(super) fn do_equal_boolean(&mut self, instr: u32) { + self.do_binary_boolean_op(instr, bytecode::parse_equal_boolean, |l, r| l == r); + } + + /// Implements the `EqualDouble` opcode. + pub(super) fn do_equal_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_equal_double, |l, r| l == r); + } + + /// Implements the `EqualInteger` opcode. + pub(super) fn do_equal_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op(instr, bytecode::parse_equal_integer, |l, r| l == r); + } + + /// Implements the `EqualText` opcode. + pub(super) fn do_equal_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op(instr, constants, heap, bytecode::parse_equal_text, |l, r| l == r); + } + /// Implements the `End` opcode. pub(super) fn do_end(&mut self, instr: u32) { let reg = bytecode::parse_end(instr); @@ -512,6 +629,58 @@ impl Context { self.pc = Address::from(offset); } + /// Implements the `GreaterDouble` opcode. + pub(super) fn do_greater_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_greater_double, |l, r| l > r); + } + + /// Implements the `GreaterEqualDouble` opcode. + pub(super) fn do_greater_equal_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_greater_equal_double, |l, r| { + l >= r + }); + } + + /// Implements the `GreaterEqualInteger` opcode. + pub(super) fn do_greater_equal_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op( + instr, + bytecode::parse_greater_equal_integer, + |l, r| l >= r, + ); + } + + /// Implements the `GreaterEqualText` opcode. + pub(super) fn do_greater_equal_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op( + instr, + constants, + heap, + bytecode::parse_greater_equal_text, + |l, r| l >= r, + ); + } + + /// Implements the `GreaterInteger` opcode. + pub(super) fn do_greater_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op(instr, bytecode::parse_greater_integer, |l, r| l > r); + } + + /// Implements the `GreaterText` opcode. + pub(super) fn do_greater_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op(instr, constants, heap, bytecode::parse_greater_text, |l, r| l > r); + } + /// Implements the `IntegerToDouble` opcode. pub(super) fn do_integer_to_double(&mut self, instr: u32) { let reg = bytecode::parse_integer_to_double(instr); @@ -533,6 +702,50 @@ impl Context { self.pc += 1; } + /// Implements the `LessDouble` opcode. + pub(super) fn do_less_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_less_double, |l, r| l < r); + } + + /// Implements the `LessEqualDouble` opcode. + pub(super) fn do_less_equal_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_less_equal_double, |l, r| l <= r); + } + + /// Implements the `LessEqualInteger` opcode. + pub(super) fn do_less_equal_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op(instr, bytecode::parse_less_equal_integer, |l, r| { + l <= r + }); + } + + /// Implements the `LessEqualText` opcode. + pub(super) fn do_less_equal_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op(instr, constants, heap, bytecode::parse_less_equal_text, |l, r| { + l <= r + }); + } + + /// Implements the `LessInteger` opcode. + pub(super) fn do_less_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op(instr, bytecode::parse_less_integer, |l, r| l < r); + } + + /// Implements the `LessText` opcode. + pub(super) fn do_less_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op(instr, constants, heap, bytecode::parse_less_text, |l, r| l < r); + } + /// Implements the `LoadArray` opcode. pub(super) fn do_load_array(&mut self, instr: u32, heap: &[HeapDatum]) { let (dest, arr_reg, first_sub_reg) = bytecode::parse_load_array(instr); @@ -625,6 +838,35 @@ impl Context { } } + /// Implements the `NotEqualBoolean` opcode. + pub(super) fn do_not_equal_boolean(&mut self, instr: u32) { + self.do_binary_boolean_op(instr, bytecode::parse_not_equal_boolean, |l, r| l != r); + } + + /// Implements the `NotEqualDouble` opcode. + pub(super) fn do_not_equal_double(&mut self, instr: u32) { + self.do_binary_double_predicate_op(instr, bytecode::parse_not_equal_double, |l, r| l != r); + } + + /// Implements the `NotEqualInteger` opcode. + pub(super) fn do_not_equal_integer(&mut self, instr: u32) { + self.do_binary_integer_predicate_op(instr, bytecode::parse_not_equal_integer, |l, r| { + l != r + }); + } + + /// Implements the `NotEqualText` opcode. + pub(super) fn do_not_equal_text( + &mut self, + instr: u32, + constants: &[ConstantDatum], + heap: &[HeapDatum], + ) { + self.do_binary_text_op(instr, constants, heap, bytecode::parse_not_equal_text, |l, r| { + l != r + }); + } + /// Implements the `Nop` opcode. pub(super) fn do_nop(&mut self, instr: u32) { bytecode::parse_nop(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 453db3f2..23ed9ce9 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -55,6 +55,12 @@ one_test!(test_gosub); one_test!(test_goto); one_test!(test_locals); one_test!(test_out_of_registers); +one_test!(test_relational_eq); +one_test!(test_relational_ge); +one_test!(test_relational_gt); +one_test!(test_relational_le); +one_test!(test_relational_lt); +one_test!(test_relational_ne); one_test!(test_strings); one_test!(test_subs); one_test!(test_types); diff --git a/core2/tests/test_relational_eq.md b/core2/tests/test_relational_eq.md new file mode 100644 index 00000000..5fd41af8 --- /dev/null +++ b/core2/tests/test_relational_eq.md @@ -0,0 +1,149 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 2 = 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 2 # 1:9 +0003: CMPEQI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 = 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: CMPEQD R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 = 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: CMPEQD R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" = "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:13 +0003: CMPEQS R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Two immediate booleans + +## Source + +```basic +OUT TRUE = FALSE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 0 # 1:12 +0003: CMPEQB R65, R65, R66 # 1:10 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Type error between integer and string + +## Source + +```basic +OUT 1 = "1" +``` + +## Compilation errors + +```plain +1:7: Cannot = INTEGER and STRING +``` diff --git a/core2/tests/test_relational_ge.md b/core2/tests/test_relational_ge.md new file mode 100644 index 00000000..355c06f4 --- /dev/null +++ b/core2/tests/test_relational_ge.md @@ -0,0 +1,122 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 3 >= 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADI R66, 2 # 1:10 +0003: CMPGEI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 >= 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:12 +0003: CMPGED R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 >= 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:10 +0003: ITOD R65 # 1:7 +0004: CMPGED R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" >= "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:14 +0003: CMPGES R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Type error with boolean + +## Source + +```basic +OUT TRUE >= FALSE +``` + +## Compilation errors + +```plain +1:10: Cannot >= BOOLEAN and BOOLEAN +``` diff --git a/core2/tests/test_relational_gt.md b/core2/tests/test_relational_gt.md new file mode 100644 index 00000000..e653bb93 --- /dev/null +++ b/core2/tests/test_relational_gt.md @@ -0,0 +1,122 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 3 > 2 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 3 # 1:5 +0002: LOADI R66, 2 # 1:9 +0003: CMPGTI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 > 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: CMPGTD R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 > 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: CMPGTD R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" > "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:13 +0003: CMPGTS R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Type error with boolean + +## Source + +```basic +OUT TRUE > FALSE +``` + +## Compilation errors + +```plain +1:10: Cannot > BOOLEAN and BOOLEAN +``` diff --git a/core2/tests/test_relational_le.md b/core2/tests/test_relational_le.md new file mode 100644 index 00000000..9f437eaf --- /dev/null +++ b/core2/tests/test_relational_le.md @@ -0,0 +1,122 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 2 <= 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 3 # 1:10 +0003: CMPLEI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 <= 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:12 +0003: CMPLED R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 <= 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:10 +0003: ITOD R65 # 1:7 +0004: CMPLED R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" <= "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:14 +0003: CMPLES R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Type error with boolean + +## Source + +```basic +OUT TRUE <= FALSE +``` + +## Compilation errors + +```plain +1:10: Cannot <= BOOLEAN and BOOLEAN +``` diff --git a/core2/tests/test_relational_lt.md b/core2/tests/test_relational_lt.md new file mode 100644 index 00000000..1cf3a112 --- /dev/null +++ b/core2/tests/test_relational_lt.md @@ -0,0 +1,122 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 2 < 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 3 # 1:9 +0003: CMPLTI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 < 2.0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 1 # 1:11 +0003: CMPLTD R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 < 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:9 +0003: ITOD R65 # 1:7 +0004: CMPLTD R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" < "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:13 +0003: CMPLTS R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Type error with boolean + +## Source + +```basic +OUT TRUE < FALSE +``` + +## Compilation errors + +```plain +1:10: Cannot < BOOLEAN and BOOLEAN +``` diff --git a/core2/tests/test_relational_ne.md b/core2/tests/test_relational_ne.md new file mode 100644 index 00000000..559b263e --- /dev/null +++ b/core2/tests/test_relational_ne.md @@ -0,0 +1,149 @@ +# Test: Two immediate integers + +## Source + +```basic +OUT 2 <> 3 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADI R66, 3 # 1:10 +0003: CMPNEI R65, R65, R66 # 1:7 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate doubles + +## Source + +```basic +OUT 2.5 <> 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADC R65, 0 # 1:5 +0002: LOADC R66, 0 # 1:12 +0003: CMPNED R65, R65, R66 # 1:9 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=false? +``` + +# Test: Left integer operand needs type promotion to double + +## Source + +```basic +OUT 2 <> 2.5 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 2 # 1:5 +0002: LOADC R66, 0 # 1:10 +0003: ITOD R65 # 1:7 +0004: CMPNED R65, R65, R66 # 1:7 +0005: LOADI R64, 256 # 1:5 +0006: UPCALL 0, R64 # 1:1, OUT +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate strings + +## Source + +```basic +OUT "foo" <> "bar" +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 0 # 1:5 +0002: LOADI R66, 1 # 1:14 +0003: CMPNES R65, R65, R66 # 1:11 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Two immediate booleans + +## Source + +```basic +OUT TRUE <> FALSE +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R65, 1 # 1:5 +0002: LOADI R66, 0 # 1:13 +0003: CMPNEB R65, R65, R66 # 1:10 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 0, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=true? +``` + +# Test: Type error between integer and string + +## Source + +```basic +OUT 1 <> "1" +``` + +## Compilation errors + +```plain +1:7: Cannot <> INTEGER and STRING +``` From 0370fffd9b4e9da078b27fd97a4378090cda873b Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 27 Feb 2026 21:50:08 -0800 Subject: [PATCH 21/53] core2: Implement support for IF statements Add IF/ELSEIF/ELSE statement compilation to the core2 compiler and VM. This required a new conditional branch instruction, a way to back-patch forward jumps within a short scope (which is simpler than the global fixup mechanism), and the compilation logic itself. The new `JumpIfFalse` bytecode instruction reads a condition register and jumps to the encoded target address when the register holds 0 (false), or falls through to the next instruction otherwise. This follows the same two-operand layout as the existing `Call` instruction. --- core2/src/bytecode.rs | 19 + core2/src/compiler/codegen.rs | 7 + core2/src/compiler/top.rs | 60 +++- core2/src/image.rs | 1 + core2/src/vm/context.rs | 11 + core2/tests/integration_test.rs | 1 + core2/tests/test_if.md | 591 ++++++++++++++++++++++++++++++++ 7 files changed, 689 insertions(+), 1 deletion(-) create mode 100644 core2/tests/test_if.md diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 1dc3f7fa..4a0f198a 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -450,6 +450,9 @@ pub(crate) enum Opcode { /// Jumps to an address relative to the PC. Jump, + /// Jumps to an address relative to the PC if the condition register is false (0). + JumpIfFalse, + /// Deallocates the registers allocated by the preamble ENTER, unwinding to the FP. Leave, @@ -789,6 +792,14 @@ instr!( u16, 0x0000ffff, 0, // Target address. ); +#[rustfmt::skip] +instr!( + Opcode::JumpIfFalse, "JMPF", + make_jump_if_false, parse_jump_if_false, format_jump_if_false, + Register, 0x000000ff, 16, // Condition register; if 0 (false), jump to target. + u16, 0x0000ffff, 0, // Target address. +); + #[rustfmt::skip] instr!( Opcode::Leave, "LEAVE", @@ -1387,6 +1398,14 @@ mod tests { test_instr!(test_jump, make_jump, parse_jump, 12345); + test_instr!( + test_jump_if_false, + make_jump_if_false, + parse_jump_if_false, + Register::local(1).unwrap(), + 12345 + ); + test_instr!(test_leave, make_leave, parse_leave); test_instr!( diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 8e4e5670..1562c341 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -133,6 +133,13 @@ impl Codegen { Ok(()) } + /// Overwrites the instruction at `addr` with `op`. + /// + /// Used by the compiler to back-patch placeholder instructions with resolved jump targets. + pub(super) fn patch(&mut self, addr: Address, op: u32) { + self.code[addr] = op; + } + /// Records a `fixup` that needs to be applied at `addr`. pub(super) fn add_fixup(&mut self, addr: usize, fixup: Fixup) { let previous = self.fixups.insert(addr, fixup); diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index d5757d67..fbc4c63d 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -15,7 +15,9 @@ //! Entry point to the compilation, handling top-level definitions. -use crate::ast::{ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, Statement, VarRef}; +use crate::ast::{ + ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, IfSpan, Statement, VarRef, +}; use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; @@ -103,6 +105,58 @@ fn compile_assignment( symtable.fixup_local_type(&span.vref, etype).map_err(|e| Error::from_syms(e, vref_pos)) } +/// Compiles an `IF` statement `span` into the `ctx`. +fn compile_if( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: IfSpan, +) -> Result<()> { + let mut end_pcs: Vec = vec![]; + let nbranches = span.branches.len(); + + for (i, branch) in span.branches.into_iter().enumerate() { + let is_last = i == nbranches - 1; + let guard_pos = branch.guard.start_pos(); + + let (jump_pc, cond_reg) = { + let mut frozen = symtable.frozen(); + let mut scope = frozen.temp_scope(); + let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?; + compile_expr_as_type( + &mut ctx.codegen, + &mut frozen, + reg, + branch.guard, + ExprType::Boolean, + )?; + (ctx.codegen.emit(bytecode::make_nop(), guard_pos), reg) + }; + + for stmt in branch.body { + compile_stmt(ctx, symtable, stmt)?; + } + + if !is_last { + let end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos); + end_pcs.push(end_pc); + } + + let next_addr = ctx.codegen.next_pc(); + let target = + u16::try_from(next_addr).map_err(|_| Error::TargetTooFar(guard_pos, next_addr))?; + ctx.codegen.patch(jump_pc, bytecode::make_jump_if_false(cond_reg, target)); + } + + let end_addr = ctx.codegen.next_pc(); + for end_pc in end_pcs { + let end_target = u16::try_from(end_addr) + .map_err(|_| Error::TargetTooFar(LineCol { line: 0, col: 0 }, end_addr))?; + ctx.codegen.patch(end_pc, bytecode::make_jump(end_target)); + } + + Ok(()) +} + /// Compiles a single `stmt` into the `ctx`. fn compile_stmt( ctx: &mut Context, @@ -335,6 +389,10 @@ fn compile_stmt( ctx.codegen.emit(bytecode::make_return(), span.pos); } + Statement::If(span) => { + compile_if(ctx, symtable, span)?; + } + _ => todo!(), } Ok(()) diff --git a/core2/src/image.rs b/core2/src/image.rs index 9441b388..a79784d8 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -55,6 +55,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::GreaterText => bytecode::format_greater_text(instr), Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), Opcode::Jump => bytecode::format_jump(instr), + Opcode::JumpIfFalse => bytecode::format_jump_if_false(instr), Opcode::Leave => bytecode::format_leave(instr), Opcode::LessDouble => bytecode::format_less_double(instr), Opcode::LessEqualDouble => bytecode::format_less_equal_double(instr), diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 0f34229e..a6e64ebc 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -302,6 +302,7 @@ impl Context { Opcode::GreaterText => self.do_greater_text(instr, &image.constants, heap), Opcode::IntegerToDouble => self.do_integer_to_double(instr), Opcode::Jump => self.do_jump(instr), + Opcode::JumpIfFalse => self.do_jump_if_false(instr), Opcode::Leave => self.do_leave(instr), Opcode::LessDouble => self.do_less_double(instr), Opcode::LessEqualDouble => self.do_less_equal_double(instr), @@ -695,6 +696,16 @@ impl Context { self.pc = Address::from(offset); } + /// Implements the `JumpIfFalse` opcode. + pub(super) fn do_jump_if_false(&mut self, instr: u32) { + let (cond_reg, target) = bytecode::parse_jump_if_false(instr); + if self.get_reg(cond_reg) != 0 { + self.pc += 1; + } else { + self.pc = Address::from(target); + } + } + /// Implements the `Leave` opcode. pub(super) fn do_leave(&mut self, instr: u32) { bytecode::parse_leave(instr); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 23ed9ce9..326ad0f9 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -53,6 +53,7 @@ one_test!(test_functions); one_test!(test_globals); one_test!(test_gosub); one_test!(test_goto); +one_test!(test_if); one_test!(test_locals); one_test!(test_out_of_registers); one_test!(test_relational_eq); diff --git a/core2/tests/test_if.md b/core2/tests/test_if.md new file mode 100644 index 00000000..ed5874df --- /dev/null +++ b/core2/tests/test_if.md @@ -0,0 +1,591 @@ +# Test: Execute IF branch when condition is true + +## Source + +```basic +IF TRUE THEN + OUT "yes" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 1 # 1:4 +0002: JMPF R64, 6 # 1:4 +0003: LOADI R65, 0 # 2:9 +0004: LOADI R64, 259 # 2:9 +0005: UPCALL 0, R64 # 2:5, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 +``` + +## Output + +```plain +0=yes$ +``` + +# Test: Skip IF branch when condition is false + +## Source + +```basic +IF FALSE THEN + OUT "yes" +END IF +OUT "after" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 0 # 1:4 +0002: JMPF R64, 6 # 1:4 +0003: LOADI R65, 0 # 2:9 +0004: LOADI R64, 259 # 2:9 +0005: UPCALL 0, R64 # 2:5, OUT +0006: LOADI R65, 1 # 4:5 +0007: LOADI R64, 259 # 4:5 +0008: UPCALL 0, R64 # 4:1, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=after$ +``` + +# Test: Take ELSEIF branch in IF ELSEIF ELSE chain + +## Source + +```basic +cond1 = FALSE +cond2 = TRUE +IF cond1 THEN + OUT "first" +ELSEIF cond2 THEN + OUT "second" +ELSE + OUT "other" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 0 # 1:9 +0002: LOADI R65, 1 # 2:9 +0003: MOVE R66, R64 # 3:4 +0004: JMPF R66, 9 # 3:4 +0005: LOADI R67, 0 # 4:9 +0006: LOADI R66, 259 # 4:9 +0007: UPCALL 0, R66 # 4:5, OUT +0008: JUMP 20 # 3:4 +0009: MOVE R66, R65 # 5:8 +0010: JMPF R66, 15 # 5:8 +0011: LOADI R67, 1 # 6:9 +0012: LOADI R66, 259 # 6:9 +0013: UPCALL 0, R66 # 6:5, OUT +0014: JUMP 20 # 5:8 +0015: LOADI R66, 1 # 7:1 +0016: JMPF R66, 20 # 7:1 +0017: LOADI R67, 2 # 8:9 +0018: LOADI R66, 259 # 8:9 +0019: UPCALL 0, R66 # 8:5, OUT +0020: LOADI R66, 0 # 0:0 +0021: END R66 # 0:0 +``` + +## Output + +```plain +0=second$ +``` + +# Test: Take IF branch in IF ELSEIF chain + +## Source + +```basic +cond1 = TRUE +cond2 = FALSE +IF cond1 THEN + OUT "first" +ELSEIF cond2 THEN + OUT "second" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 1 # 1:9 +0002: LOADI R65, 0 # 2:9 +0003: MOVE R66, R64 # 3:4 +0004: JMPF R66, 9 # 3:4 +0005: LOADI R67, 0 # 4:9 +0006: LOADI R66, 259 # 4:9 +0007: UPCALL 0, R66 # 4:5, OUT +0008: JUMP 14 # 3:4 +0009: MOVE R66, R65 # 5:8 +0010: JMPF R66, 14 # 5:8 +0011: LOADI R67, 1 # 6:9 +0012: LOADI R66, 259 # 6:9 +0013: UPCALL 0, R66 # 6:5, OUT +0014: LOADI R66, 0 # 0:0 +0015: END R66 # 0:0 +``` + +## Output + +```plain +0=first$ +``` + +# Test: Take ELSE branch in IF ELSE block + +## Source + +```basic +cond = FALSE +IF cond THEN + OUT "yes" +ELSE + OUT "no" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:8 +0002: MOVE R65, R64 # 2:4 +0003: JMPF R65, 8 # 2:4 +0004: LOADI R66, 0 # 3:9 +0005: LOADI R65, 259 # 3:9 +0006: UPCALL 0, R65 # 3:5, OUT +0007: JUMP 13 # 2:4 +0008: LOADI R65, 1 # 4:1 +0009: JMPF R65, 13 # 4:1 +0010: LOADI R66, 1 # 5:9 +0011: LOADI R65, 259 # 5:9 +0012: UPCALL 0, R65 # 5:5, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=no$ +``` + +# Test: Execute nested IF blocks + +## Source + +```basic +outer = TRUE +inner = TRUE +IF outer THEN + IF inner THEN + OUT "both" + END IF +END IF +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 1 # 1:9 +0002: LOADI R65, 1 # 2:9 +0003: MOVE R66, R64 # 3:4 +0004: JMPF R66, 10 # 3:4 +0005: MOVE R66, R65 # 4:8 +0006: JMPF R66, 10 # 4:8 +0007: LOADI R67, 0 # 5:13 +0008: LOADI R66, 259 # 5:13 +0009: UPCALL 0, R66 # 5:9, OUT +0010: LOADI R66, 0 # 0:0 +0011: END R66 # 0:0 +``` + +## Output + +```plain +0=both$ +``` + +# Test: Evaluate nested IF with variables and ELSE + +## Source + +```basic +a = TRUE +b = FALSE +IF a THEN + IF b THEN + OUT "a and b" + ELSE + OUT "only a" + END IF +END IF +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 1 # 1:5 +0002: LOADI R65, 0 # 2:5 +0003: MOVE R66, R64 # 3:4 +0004: JMPF R66, 16 # 3:4 +0005: MOVE R66, R65 # 4:8 +0006: JMPF R66, 11 # 4:8 +0007: LOADI R67, 0 # 5:13 +0008: LOADI R66, 259 # 5:13 +0009: UPCALL 0, R66 # 5:9, OUT +0010: JUMP 16 # 4:8 +0011: LOADI R66, 1 # 6:5 +0012: JMPF R66, 16 # 6:5 +0013: LOADI R67, 1 # 7:13 +0014: LOADI R66, 259 # 7:13 +0015: UPCALL 0, R66 # 7:9, OUT +0016: LOADI R66, 0 # 0:0 +0017: END R66 # 0:0 +``` + +## Output + +```plain +0=only a$ +``` + +# Test: Evaluate two IF guards when first guard matches + +## Source + +```basic +n = 3 +IF n = 3 THEN + OUT "match" +END IF +IF n <> 3 THEN + OUT "no match" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:4 +0003: LOADI R66, 3 # 2:8 +0004: CMPEQI R65, R65, R66 # 2:6 +0005: JMPF R65, 9 # 2:4 +0006: LOADI R66, 0 # 3:9 +0007: LOADI R65, 259 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R65, R64 # 5:4 +0010: LOADI R66, 3 # 5:9 +0011: CMPNEI R65, R65, R66 # 5:6 +0012: JMPF R65, 16 # 5:4 +0013: LOADI R66, 1 # 6:9 +0014: LOADI R65, 259 # 6:9 +0015: UPCALL 0, R65 # 6:5, OUT +0016: LOADI R65, 0 # 0:0 +0017: END R65 # 0:0 +``` + +## Output + +```plain +0=match$ +``` + +# Test: Evaluate two IF guards when second guard matches + +## Source + +```basic +n = 5 +IF n = 3 THEN + OUT "match" +END IF +IF n <> 3 THEN + OUT "no match" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 5 # 1:5 +0002: MOVE R65, R64 # 2:4 +0003: LOADI R66, 3 # 2:8 +0004: CMPEQI R65, R65, R66 # 2:6 +0005: JMPF R65, 9 # 2:4 +0006: LOADI R66, 0 # 3:9 +0007: LOADI R65, 259 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R65, R64 # 5:4 +0010: LOADI R66, 3 # 5:9 +0011: CMPNEI R65, R65, R66 # 5:6 +0012: JMPF R65, 16 # 5:4 +0013: LOADI R66, 1 # 6:9 +0014: LOADI R65, 259 # 6:9 +0015: UPCALL 0, R65 # 6:5, OUT +0016: LOADI R65, 0 # 0:0 +0017: END R65 # 0:0 +``` + +## Output + +```plain +0=no match$ +``` + +# Test: Take third ELSEIF branch in multi-branch IF chain + +## Source + +```basic +n = 3 +IF n = 1 THEN + OUT "first" +ELSEIF n = 2 THEN + OUT "second" +ELSEIF n = 3 THEN + OUT "third" +ELSE + OUT "fourth" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:4 +0003: LOADI R66, 1 # 2:8 +0004: CMPEQI R65, R65, R66 # 2:6 +0005: JMPF R65, 10 # 2:4 +0006: LOADI R66, 0 # 3:9 +0007: LOADI R65, 259 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: JUMP 31 # 2:4 +0010: MOVE R65, R64 # 4:8 +0011: LOADI R66, 2 # 4:12 +0012: CMPEQI R65, R65, R66 # 4:10 +0013: JMPF R65, 18 # 4:8 +0014: LOADI R66, 1 # 5:9 +0015: LOADI R65, 259 # 5:9 +0016: UPCALL 0, R65 # 5:5, OUT +0017: JUMP 31 # 4:8 +0018: MOVE R65, R64 # 6:8 +0019: LOADI R66, 3 # 6:12 +0020: CMPEQI R65, R65, R66 # 6:10 +0021: JMPF R65, 26 # 6:8 +0022: LOADI R66, 2 # 7:9 +0023: LOADI R65, 259 # 7:9 +0024: UPCALL 0, R65 # 7:5, OUT +0025: JUMP 31 # 6:8 +0026: LOADI R65, 1 # 8:1 +0027: JMPF R65, 31 # 8:1 +0028: LOADI R66, 3 # 9:9 +0029: LOADI R65, 259 # 9:9 +0030: UPCALL 0, R65 # 9:5, OUT +0031: LOADI R65, 0 # 0:0 +0032: END R65 # 0:0 +``` + +## Output + +```plain +0=third$ +``` + +# Test: Take ELSE branch in multi-branch IF chain + +## Source + +```basic +n = 4 +IF n = 1 THEN + OUT "first" +ELSEIF n = 2 THEN + OUT "second" +ELSEIF n = 3 THEN + OUT "third" +ELSE + OUT "fourth" +END IF +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 4 # 1:5 +0002: MOVE R65, R64 # 2:4 +0003: LOADI R66, 1 # 2:8 +0004: CMPEQI R65, R65, R66 # 2:6 +0005: JMPF R65, 10 # 2:4 +0006: LOADI R66, 0 # 3:9 +0007: LOADI R65, 259 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: JUMP 31 # 2:4 +0010: MOVE R65, R64 # 4:8 +0011: LOADI R66, 2 # 4:12 +0012: CMPEQI R65, R65, R66 # 4:10 +0013: JMPF R65, 18 # 4:8 +0014: LOADI R66, 1 # 5:9 +0015: LOADI R65, 259 # 5:9 +0016: UPCALL 0, R65 # 5:5, OUT +0017: JUMP 31 # 4:8 +0018: MOVE R65, R64 # 6:8 +0019: LOADI R66, 3 # 6:12 +0020: CMPEQI R65, R65, R66 # 6:10 +0021: JMPF R65, 26 # 6:8 +0022: LOADI R66, 2 # 7:9 +0023: LOADI R65, 259 # 7:9 +0024: UPCALL 0, R65 # 7:5, OUT +0025: JUMP 31 # 6:8 +0026: LOADI R65, 1 # 8:1 +0027: JMPF R65, 31 # 8:1 +0028: LOADI R66, 3 # 9:9 +0029: LOADI R65, 259 # 9:9 +0030: UPCALL 0, R65 # 9:5, OUT +0031: LOADI R65, 0 # 0:0 +0032: END R65 # 0:0 +``` + +## Output + +```plain +0=fourth$ +``` + +# Test: Fail when ELSEIF guard is not boolean + +## Source + +```basic +n = 5 +IF n = 3 THEN + OUT "match" +ELSEIF "foo" THEN + OUT "no match" +END IF +``` + +## Compilation errors + +```plain +4:8: Expected BOOLEAN but found STRING +``` + +# Test: Fail on invalid ELSE IF syntax + +## Source + +```basic +IF TRUE THEN +ELSE IF TRUE THEN +END IF +``` + +## Compilation errors + +```plain +2:6: Expecting newline after ELSE +``` + +# Test: Fail on END IF without matching IF + +## Source + +```basic +IF TRUE THEN END IF +``` + +## Compilation errors + +```plain +1:14: END IF without IF +``` + +# Test: Fail when IF condition lacks THEN + +## Source + +```basic +IF 2 +END IF +``` + +## Compilation errors + +```plain +1:5: No THEN in IF statement +``` + +# Test: Fail when TRUE condition lacks THEN + +## Source + +```basic +IF TRUE +END IF +OUT 3 +``` + +## Compilation errors + +```plain +1:8: No THEN in IF statement +``` + +# Test: Fail when IF condition is not boolean + +## Source + +```basic +IF 2 THEN +END IF +``` + +## Compilation errors + +```plain +1:4: Expected BOOLEAN but found INTEGER +``` + +# Test: Fail when ELSEIF condition is not boolean + +## Source + +```basic +IF FALSE THEN +ELSEIF 2 THEN +END IF +``` + +## Compilation errors + +```plain +2:8: Expected BOOLEAN but found INTEGER +``` From fc42e871cec94c80457b0d78b14d14c9a75b2bc2 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Mar 2026 10:10:53 -0800 Subject: [PATCH 22/53] core2: Implement support for DO loops Implement codegen for DO/LOOP variants and EXIT DO handling. Add integration coverage for DO loops and compiler errors. --- core2/src/compiler/mod.rs | 4 + core2/src/compiler/top.rs | 132 ++++- core2/tests/integration_test.rs | 1 + core2/tests/test_do.md | 873 ++++++++++++++++++++++++++++++++ 4 files changed, 1009 insertions(+), 1 deletion(-) create mode 100644 core2/tests/test_do.md diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 01eaa5f7..42432db0 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -75,6 +75,10 @@ pub enum Error { #[error("{0}: Cannot call {1} (not a function)")] NotAFunction(LineCol, VarRef), + /// `EXIT` statement found outside its expected block. + #[error("{0}: EXIT {1} outside of {1}")] + MisplacedExit(LineCol, &'static str), + /// Attempt to index something that is not an array. #[error("{0}: {1} is not an array")] NotAnArray(LineCol, VarRef), diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index fbc4c63d..ae52ce3c 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,7 +16,8 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ - ArgSep, AssignmentSpan, CallableSpan, EndSpan, ExprType, IfSpan, Statement, VarRef, + ArgSep, AssignmentSpan, CallableSpan, DoGuard, DoSpan, EndSpan, Expr, ExprType, IfSpan, + Statement, VarRef, }; use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; @@ -47,6 +48,9 @@ struct Context { /// Collection of user-defined callable definitions to be compiled after the main scope. user_callables: Vec, + + /// Stack of pending `EXIT DO` jumps for each nested `DO` loop. + do_exit_stack: Vec>, } /// Compiles an assignment statement `span` into the `codegen` block. @@ -105,6 +109,120 @@ fn compile_assignment( symtable.fixup_local_type(&span.vref, etype).map_err(|e| Error::from_syms(e, vref_pos)) } +/// Compiles a `DO` loop and emits bytecode into `ctx`. +fn compile_do( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: DoSpan, +) -> Result<()> { + fn compile_guard( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + guard: Expr, + ) -> Result<(Register, LineCol)> { + let guard_pos = guard.start_pos(); + let mut frozen = symtable.frozen(); + let mut scope = frozen.temp_scope(); + let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?; + compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, guard, ExprType::Boolean)?; + Ok((reg, guard_pos)) + } + + ctx.do_exit_stack.push(vec![]); + + let end_addr = match span.guard { + DoGuard::Infinite => { + let start_pc = ctx.codegen.next_pc(); + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + let end_pos = LineCol { line: 0, col: 0 }; + let target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(end_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(target), end_pos); + ctx.codegen.next_pc() + } + + DoGuard::PreUntil(guard) => { + let start_pc = ctx.codegen.next_pc(); + let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?; + let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos); + let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos); + let body_addr = ctx.codegen.next_pc(); + let body_target = + u16::try_from(body_addr).map_err(|_| Error::TargetTooFar(guard_pos, body_addr))?; + ctx.codegen.patch(jump_body_pc, bytecode::make_jump_if_false(cond_reg, body_target)); + + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos); + let end_addr = ctx.codegen.next_pc(); + let end_target = + u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?; + ctx.codegen.patch(jump_end_pc, bytecode::make_jump(end_target)); + end_addr + } + + DoGuard::PreWhile(guard) => { + let start_pc = ctx.codegen.next_pc(); + let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?; + let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos); + + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos); + let end_addr = ctx.codegen.next_pc(); + let end_target = + u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?; + ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target)); + end_addr + } + + DoGuard::PostUntil(guard) => { + let start_pc = ctx.codegen.next_pc(); + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?; + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump_if_false(cond_reg, start_target), guard_pos); + ctx.codegen.next_pc() + } + + DoGuard::PostWhile(guard) => { + let start_pc = ctx.codegen.next_pc(); + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + let (cond_reg, guard_pos) = compile_guard(ctx, symtable, guard)?; + let jump_end_pc = ctx.codegen.emit(bytecode::make_nop(), guard_pos); + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos); + let end_addr = ctx.codegen.next_pc(); + let end_target = + u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?; + ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target)); + end_addr + } + }; + + let exit_jumps = ctx.do_exit_stack.pop().expect("Must have a matching DO scope"); + for (addr, pos) in exit_jumps { + let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(pos, end_addr))?; + ctx.codegen.patch(addr, bytecode::make_jump(end_target)); + } + + Ok(()) +} + /// Compiles an `IF` statement `span` into the `ctx`. fn compile_if( ctx: &mut Context, @@ -350,6 +468,10 @@ fn compile_stmt( ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), name_pos); } + Statement::Do(span) => { + compile_do(ctx, symtable, span)?; + } + Statement::End(span) => { let mut symtable = symtable.frozen(); let mut scope = symtable.temp_scope(); @@ -371,6 +493,14 @@ fn compile_stmt( ctx.codegen.emit(bytecode::make_end(reg), span.pos); } + Statement::ExitDo(span) => { + let Some(exit_stack) = ctx.do_exit_stack.last_mut() else { + return Err(Error::MisplacedExit(span.pos, "DO")); + }; + let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos); + exit_stack.push((addr, span.pos)); + } + Statement::Gosub(span) => { let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); ctx.codegen.add_fixup(addr, Fixup::Gosub(SymbolKey::from(span.target))); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 326ad0f9..172a65e7 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -47,6 +47,7 @@ one_test!(test_bitwise_or); one_test!(test_bitwise_shl); one_test!(test_bitwise_shr); one_test!(test_bitwise_xor); +one_test!(test_do); one_test!(test_empty); one_test!(test_end); one_test!(test_functions); diff --git a/core2/tests/test_do.md b/core2/tests/test_do.md new file mode 100644 index 00000000..9b895768 --- /dev/null +++ b/core2/tests/test_do.md @@ -0,0 +1,873 @@ +# Test: Infinite DO with EXIT DO + +## Source + +```basic +DO + OUT "start" + EXIT DO + OUT "after" +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R65, 0 # 2:9 +0002: LOADI R64, 259 # 2:9 +0003: UPCALL 0, R64 # 2:5, OUT +0004: JUMP 9 # 3:5 +0005: LOADI R65, 1 # 4:9 +0006: LOADI R64, 259 # 4:9 +0007: UPCALL 0, R64 # 4:5, OUT +0008: JUMP 1 # 0:0 +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=start$ +``` + +# Test: Pre UNTIL DO loop with zero iterations + +## Source + +```basic +n = 0 +DO UNTIL n = 0 + OUT n + n = n - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPEQI R65, R65, R66 # 2:12 +0005: JMPF R65, 7 # 2:10 +0006: JUMP 14 # 2:10 +0007: MOVE R66, R64 # 3:9 +0008: LOADI R65, 258 # 3:9 +0009: UPCALL 0, R65 # 3:5, OUT +0010: MOVE R64, R64 # 4:9 +0011: LOADI R65, 1 # 4:13 +0012: SUBI R64, R64, R65 # 4:11 +0013: JUMP 2 # 2:10 +0014: LOADI R65, 0 # 0:0 +0015: END R65 # 0:0 +``` + +# Test: Pre UNTIL DO loop with iterations + +## Source + +```basic +n = 3 +DO UNTIL n = 0 + OUT n + n = n - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPEQI R65, R65, R66 # 2:12 +0005: JMPF R65, 7 # 2:10 +0006: JUMP 14 # 2:10 +0007: MOVE R66, R64 # 3:9 +0008: LOADI R65, 258 # 3:9 +0009: UPCALL 0, R65 # 3:5, OUT +0010: MOVE R64, R64 # 4:9 +0011: LOADI R65, 1 # 4:13 +0012: SUBI R64, R64, R65 # 4:11 +0013: JUMP 2 # 2:10 +0014: LOADI R65, 0 # 0:0 +0015: END R65 # 0:0 +``` + +## Output + +```plain +0=3% +0=2% +0=1% +``` + +# Test: Pre WHILE DO loop with zero iterations + +## Source + +```basic +n = 0 +DO WHILE n > 0 + OUT n + n = n - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 13 # 2:10 +0006: MOVE R66, R64 # 3:9 +0007: LOADI R65, 258 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R64, R64 # 4:9 +0010: LOADI R65, 1 # 4:13 +0011: SUBI R64, R64, R65 # 4:11 +0012: JUMP 2 # 2:10 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +# Test: Pre WHILE DO loop with iterations + +## Source + +```basic +n = 3 +DO WHILE n > 0 + OUT n + n = n - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 13 # 2:10 +0006: MOVE R66, R64 # 3:9 +0007: LOADI R65, 258 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R64, R64 # 4:9 +0010: LOADI R65, 1 # 4:13 +0011: SUBI R64, R64, R65 # 4:11 +0012: JUMP 2 # 2:10 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=3% +0=2% +0=1% +``` + +# Test: Post UNTIL DO loop with single iteration + +## Source + +```basic +n = 1 +DO + OUT n + n = n - 1 +LOOP UNTIL n = 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:5 +0002: MOVE R66, R64 # 3:9 +0003: LOADI R65, 258 # 3:9 +0004: UPCALL 0, R65 # 3:5, OUT +0005: MOVE R64, R64 # 4:9 +0006: LOADI R65, 1 # 4:13 +0007: SUBI R64, R64, R65 # 4:11 +0008: MOVE R65, R64 # 5:12 +0009: LOADI R66, 0 # 5:16 +0010: CMPEQI R65, R65, R66 # 5:14 +0011: JMPF R65, 2 # 5:12 +0012: LOADI R65, 0 # 0:0 +0013: END R65 # 0:0 +``` + +## Output + +```plain +0=1% +``` + +# Test: Post UNTIL DO loop with iterations + +## Source + +```basic +n = 3 +DO + OUT n + n = n - 1 +LOOP UNTIL n = 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R66, R64 # 3:9 +0003: LOADI R65, 258 # 3:9 +0004: UPCALL 0, R65 # 3:5, OUT +0005: MOVE R64, R64 # 4:9 +0006: LOADI R65, 1 # 4:13 +0007: SUBI R64, R64, R65 # 4:11 +0008: MOVE R65, R64 # 5:12 +0009: LOADI R66, 0 # 5:16 +0010: CMPEQI R65, R65, R66 # 5:14 +0011: JMPF R65, 2 # 5:12 +0012: LOADI R65, 0 # 0:0 +0013: END R65 # 0:0 +``` + +## Output + +```plain +0=3% +0=2% +0=1% +``` + +# Test: Post WHILE DO loop with single iteration + +## Source + +```basic +n = 1 +DO + OUT n + n = n - 1 +LOOP WHILE n > 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:5 +0002: MOVE R66, R64 # 3:9 +0003: LOADI R65, 258 # 3:9 +0004: UPCALL 0, R65 # 3:5, OUT +0005: MOVE R64, R64 # 4:9 +0006: LOADI R65, 1 # 4:13 +0007: SUBI R64, R64, R65 # 4:11 +0008: MOVE R65, R64 # 5:12 +0009: LOADI R66, 0 # 5:16 +0010: CMPGTI R65, R65, R66 # 5:14 +0011: JMPF R65, 13 # 5:12 +0012: JUMP 2 # 5:12 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=1% +``` + +# Test: Post WHILE DO loop with iterations + +## Source + +```basic +n = 3 +DO + OUT n + n = n - 1 +LOOP WHILE n > 0 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R66, R64 # 3:9 +0003: LOADI R65, 258 # 3:9 +0004: UPCALL 0, R65 # 3:5, OUT +0005: MOVE R64, R64 # 4:9 +0006: LOADI R65, 1 # 4:13 +0007: SUBI R64, R64, R65 # 4:11 +0008: MOVE R65, R64 # 5:12 +0009: LOADI R66, 0 # 5:16 +0010: CMPGTI R65, R65, R66 # 5:14 +0011: JMPF R65, 13 # 5:12 +0012: JUMP 2 # 5:12 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=3% +0=2% +0=1% +``` + +# Test: Nested DO loops with EXIT DO + +## Source + +```basic +i = 3 +DO WHILE i > 0 + j = 2 + DO UNTIL j = 0 + OUT i; j + IF i = 2 AND j = 2 THEN: EXIT DO: END IF + j = j - 1 + LOOP + IF i = 1 THEN: EXIT DO: END IF + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 39 # 2:10 +0006: LOADI R65, 2 # 3:9 +0007: MOVE R66, R65 # 4:14 +0008: LOADI R67, 0 # 4:18 +0009: CMPEQI R66, R66, R67 # 4:16 +0010: JMPF R66, 12 # 4:14 +0011: JUMP 30 # 4:14 +0012: MOVE R67, R64 # 5:13 +0013: LOADI R66, 274 # 5:13 +0014: MOVE R69, R65 # 5:16 +0015: LOADI R68, 258 # 5:16 +0016: UPCALL 0, R66 # 5:9, OUT +0017: MOVE R66, R64 # 6:12 +0018: LOADI R67, 2 # 6:16 +0019: CMPEQI R66, R66, R67 # 6:14 +0020: MOVE R67, R65 # 6:22 +0021: LOADI R68, 2 # 6:26 +0022: CMPEQI R67, R67, R68 # 6:24 +0023: AND R66, R66, R67 # 6:18 +0024: JMPF R66, 26 # 6:12 +0025: JUMP 30 # 6:34 +0026: MOVE R65, R65 # 7:13 +0027: LOADI R66, 1 # 7:17 +0028: SUBI R65, R65, R66 # 7:15 +0029: JUMP 7 # 4:14 +0030: MOVE R66, R64 # 9:8 +0031: LOADI R67, 1 # 9:12 +0032: CMPEQI R66, R66, R67 # 9:10 +0033: JMPF R66, 35 # 9:8 +0034: JUMP 39 # 9:20 +0035: MOVE R64, R64 # 10:9 +0036: LOADI R66, 1 # 10:13 +0037: SUBI R64, R64, R66 # 10:11 +0038: JUMP 2 # 2:10 +0039: LOADI R66, 0 # 0:0 +0040: END R66 # 0:0 +``` + +## Output + +```plain +0=3% ; 1=2% +0=3% ; 1=1% +0=2% ; 1=2% +0=1% ; 1=2% +0=1% ; 1=1% +``` + +# Test: Nested DO loop EXIT DO exits inner only + +## Source + +```basic +i = 2 +DO WHILE i > 0 + j = 3 + DO WHILE j > 0 + OUT i; j + EXIT DO + j = j - 1 + LOOP + OUT "after"; i + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 30 # 2:10 +0006: LOADI R65, 3 # 3:9 +0007: MOVE R66, R65 # 4:14 +0008: LOADI R67, 0 # 4:18 +0009: CMPGTI R66, R66, R67 # 4:16 +0010: JMPF R66, 21 # 4:14 +0011: MOVE R67, R64 # 5:13 +0012: LOADI R66, 274 # 5:13 +0013: MOVE R69, R65 # 5:16 +0014: LOADI R68, 258 # 5:16 +0015: UPCALL 0, R66 # 5:9, OUT +0016: JUMP 21 # 6:9 +0017: MOVE R65, R65 # 7:13 +0018: LOADI R66, 1 # 7:17 +0019: SUBI R65, R65, R66 # 7:15 +0020: JUMP 7 # 4:14 +0021: LOADI R67, 0 # 9:9 +0022: LOADI R66, 275 # 9:9 +0023: MOVE R69, R64 # 9:18 +0024: LOADI R68, 258 # 9:18 +0025: UPCALL 0, R66 # 9:5, OUT +0026: MOVE R64, R64 # 10:9 +0027: LOADI R66, 1 # 10:13 +0028: SUBI R64, R64, R66 # 10:11 +0029: JUMP 2 # 2:10 +0030: LOADI R66, 0 # 0:0 +0031: END R66 # 0:0 +``` + +## Output + +```plain +0=2% ; 1=3% +0=after$ ; 1=2% +0=1% ; 1=3% +0=after$ ; 1=1% +``` + +# Test: Nested DO loop with multiple EXIT DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + j = 2 + DO WHILE j > 0 + IF i = 2 THEN: EXIT DO: END IF + IF j = 1 THEN: EXIT DO: END IF + j = j - 1 + LOOP + OUT i + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 32 # 2:10 +0006: LOADI R65, 2 # 3:9 +0007: MOVE R66, R65 # 4:14 +0008: LOADI R67, 0 # 4:18 +0009: CMPGTI R66, R66, R67 # 4:16 +0010: JMPF R66, 25 # 4:14 +0011: MOVE R66, R64 # 5:12 +0012: LOADI R67, 2 # 5:16 +0013: CMPEQI R66, R66, R67 # 5:14 +0014: JMPF R66, 16 # 5:12 +0015: JUMP 25 # 5:24 +0016: MOVE R66, R65 # 6:12 +0017: LOADI R67, 1 # 6:16 +0018: CMPEQI R66, R66, R67 # 6:14 +0019: JMPF R66, 21 # 6:12 +0020: JUMP 25 # 6:24 +0021: MOVE R65, R65 # 7:13 +0022: LOADI R66, 1 # 7:17 +0023: SUBI R65, R65, R66 # 7:15 +0024: JUMP 7 # 4:14 +0025: MOVE R67, R64 # 9:9 +0026: LOADI R66, 258 # 9:9 +0027: UPCALL 0, R66 # 9:5, OUT +0028: MOVE R64, R64 # 10:9 +0029: LOADI R66, 1 # 10:13 +0030: SUBI R64, R64, R66 # 10:11 +0031: JUMP 2 # 2:10 +0032: LOADI R66, 0 # 0:0 +0033: END R66 # 0:0 +``` + +## Output + +```plain +0=2% +0=1% +``` + +# Test: Nested DO with inner post guard EXIT DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + j = 2 + DO + OUT i; j + EXIT DO + j = j - 1 + LOOP UNTIL j = 0 + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 24 # 2:10 +0006: LOADI R65, 2 # 3:9 +0007: MOVE R67, R64 # 5:13 +0008: LOADI R66, 274 # 5:13 +0009: MOVE R69, R65 # 5:16 +0010: LOADI R68, 258 # 5:16 +0011: UPCALL 0, R66 # 5:9, OUT +0012: JUMP 20 # 6:9 +0013: MOVE R65, R65 # 7:13 +0014: LOADI R66, 1 # 7:17 +0015: SUBI R65, R65, R66 # 7:15 +0016: MOVE R66, R65 # 8:16 +0017: LOADI R67, 0 # 8:20 +0018: CMPEQI R66, R66, R67 # 8:18 +0019: JMPF R66, 7 # 8:16 +0020: MOVE R64, R64 # 9:9 +0021: LOADI R66, 1 # 9:13 +0022: SUBI R64, R64, R66 # 9:11 +0023: JUMP 2 # 2:10 +0024: LOADI R66, 0 # 0:0 +0025: END R66 # 0:0 +``` + +## Output + +```plain +0=2% ; 1=2% +0=1% ; 1=2% +``` + +# Test: Nested DO with inner infinite EXIT DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + j = 1 + DO + OUT i; j + EXIT DO + LOOP + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 18 # 2:10 +0006: LOADI R65, 1 # 3:9 +0007: MOVE R67, R64 # 5:13 +0008: LOADI R66, 274 # 5:13 +0009: MOVE R69, R65 # 5:16 +0010: LOADI R68, 258 # 5:16 +0011: UPCALL 0, R66 # 5:9, OUT +0012: JUMP 14 # 6:9 +0013: JUMP 7 # 0:0 +0014: MOVE R64, R64 # 8:9 +0015: LOADI R66, 1 # 8:13 +0016: SUBI R64, R64, R66 # 8:11 +0017: JUMP 2 # 2:10 +0018: LOADI R66, 0 # 0:0 +0019: END R66 # 0:0 +``` + +## Output + +```plain +0=2% ; 1=1% +0=1% ; 1=1% +``` + +# Test: Nested DO with single-line EXIT DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + j = 2 + DO WHILE j > 0: OUT i; j: EXIT DO: j = j - 1: LOOP + OUT "after"; i + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 30 # 2:10 +0006: LOADI R65, 2 # 3:9 +0007: MOVE R66, R65 # 4:14 +0008: LOADI R67, 0 # 4:18 +0009: CMPGTI R66, R66, R67 # 4:16 +0010: JMPF R66, 21 # 4:14 +0011: MOVE R67, R64 # 4:25 +0012: LOADI R66, 274 # 4:25 +0013: MOVE R69, R65 # 4:28 +0014: LOADI R68, 258 # 4:28 +0015: UPCALL 0, R66 # 4:21, OUT +0016: JUMP 21 # 4:31 +0017: MOVE R65, R65 # 4:44 +0018: LOADI R66, 1 # 4:48 +0019: SUBI R65, R65, R66 # 4:46 +0020: JUMP 7 # 4:14 +0021: LOADI R67, 0 # 5:9 +0022: LOADI R66, 275 # 5:9 +0023: MOVE R69, R64 # 5:18 +0024: LOADI R68, 258 # 5:18 +0025: UPCALL 0, R66 # 5:5, OUT +0026: MOVE R64, R64 # 6:9 +0027: LOADI R66, 1 # 6:13 +0028: SUBI R64, R64, R66 # 6:11 +0029: JUMP 2 # 2:10 +0030: LOADI R66, 0 # 0:0 +0031: END R66 # 0:0 +``` + +## Output + +```plain +0=2% ; 1=2% +0=after$ ; 1=2% +0=1% ; 1=2% +0=after$ ; 1=1% +``` + +# Test: Sequential DO loops with EXIT DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + OUT "First"; i + i = i - 1 +LOOP + +i = 2 +DO WHILE i > 0 + OUT "Second"; i + i = i - 1 +LOOP +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 15 # 2:10 +0006: LOADI R66, 0 # 3:9 +0007: LOADI R65, 275 # 3:9 +0008: MOVE R68, R64 # 3:18 +0009: LOADI R67, 258 # 3:18 +0010: UPCALL 0, R65 # 3:5, OUT +0011: MOVE R64, R64 # 4:9 +0012: LOADI R65, 1 # 4:13 +0013: SUBI R64, R64, R65 # 4:11 +0014: JUMP 2 # 2:10 +0015: LOADI R64, 2 # 7:5 +0016: MOVE R65, R64 # 8:10 +0017: LOADI R66, 0 # 8:14 +0018: CMPGTI R65, R65, R66 # 8:12 +0019: JMPF R65, 29 # 8:10 +0020: LOADI R66, 1 # 9:9 +0021: LOADI R65, 275 # 9:9 +0022: MOVE R68, R64 # 9:19 +0023: LOADI R67, 258 # 9:19 +0024: UPCALL 0, R65 # 9:5, OUT +0025: MOVE R64, R64 # 10:9 +0026: LOADI R65, 1 # 10:13 +0027: SUBI R64, R64, R65 # 10:11 +0028: JUMP 16 # 8:10 +0029: LOADI R65, 0 # 0:0 +0030: END R65 # 0:0 +``` + +## Output + +```plain +0=First$ ; 1=2% +0=First$ ; 1=1% +0=Second$ ; 1=2% +0=Second$ ; 1=1% +``` + +# Test: EXIT DO from nested subroutine DO loop + +## Source + +```basic +i = 3 +DO WHILE i > 0 + GOSUB @another + IF i = 1 THEN: EXIT DO: END IF + i = i - 1 +LOOP +GOTO @end +@another +j = 2 +DO UNTIL j = 0 + OUT i; j + IF i = 2 AND j = 2 THEN: EXIT DO: END IF + j = j - 1 +LOOP +RETURN +@end +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 16 # 2:10 +0006: GOSUB 17 # 3:11 +0007: MOVE R65, R64 # 4:8 +0008: LOADI R66, 1 # 4:12 +0009: CMPEQI R65, R65, R66 # 4:10 +0010: JMPF R65, 12 # 4:8 +0011: JUMP 16 # 4:20 +0012: MOVE R64, R64 # 5:9 +0013: LOADI R65, 1 # 5:13 +0014: SUBI R64, R64, R65 # 5:11 +0015: JUMP 2 # 2:10 +0016: JUMP 42 # 7:6 +0017: LOADI R65, 2 # 9:5 +0018: MOVE R66, R65 # 10:10 +0019: LOADI R67, 0 # 10:14 +0020: CMPEQI R66, R66, R67 # 10:12 +0021: JMPF R66, 23 # 10:10 +0022: JUMP 41 # 10:10 +0023: MOVE R67, R64 # 11:9 +0024: LOADI R66, 274 # 11:9 +0025: MOVE R69, R65 # 11:12 +0026: LOADI R68, 258 # 11:12 +0027: UPCALL 0, R66 # 11:5, OUT +0028: MOVE R66, R64 # 12:8 +0029: LOADI R67, 2 # 12:12 +0030: CMPEQI R66, R66, R67 # 12:10 +0031: MOVE R67, R65 # 12:18 +0032: LOADI R68, 2 # 12:22 +0033: CMPEQI R67, R67, R68 # 12:20 +0034: AND R66, R66, R67 # 12:14 +0035: JMPF R66, 37 # 12:8 +0036: JUMP 41 # 12:30 +0037: MOVE R65, R65 # 13:9 +0038: LOADI R66, 1 # 13:13 +0039: SUBI R65, R65, R66 # 13:11 +0040: JUMP 18 # 10:10 +0041: RETURN # 15:1 +0042: LOADI R66, 0 # 0:0 +0043: END R66 # 0:0 +``` + +## Output + +```plain +0=3% ; 1=2% +0=3% ; 1=1% +0=2% ; 1=2% +0=1% ; 1=2% +0=1% ; 1=1% +``` + +# Test: DO guard must be boolean + +## Source + +```basic +DO WHILE 2 + OUT 1 +LOOP +``` + +## Compilation errors + +```plain +1:10: Expected BOOLEAN but found INTEGER +``` + +# Test: EXIT DO outside of DO + +## Source + +```basic +EXIT DO +``` + +## Compilation errors + +```plain +1:1: EXIT DO outside of DO +``` From f0b0ab1e94d05be234b2c276190f52275ff3542f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Mar 2026 11:02:09 -0800 Subject: [PATCH 23/53] core2: Implement support for WHILE loops Compile WHILE/WEND loops in core2 by emitting the same jump-based control-flow shape used by other guarded loops. Keep EXIT DO scoping behavior unchanged so EXIT DO remains tied to the nearest enclosing DO, even when it appears inside nested WHILE blocks. Add a new integration suite for WHILE coverage and register it in the test harness. Port parity-oriented cases for normal loop execution, zero-iteration behavior, type-checking of guards, parser-level WHILE errors, and EXIT DO interactions across DO and WHILE nesting. --- core2/src/compiler/top.rs | 39 ++++++- core2/tests/integration_test.rs | 1 + core2/tests/test_while.md | 174 ++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 core2/tests/test_while.md diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index ae52ce3c..70ae4269 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -17,7 +17,7 @@ use crate::ast::{ ArgSep, AssignmentSpan, CallableSpan, DoGuard, DoSpan, EndSpan, Expr, ExprType, IfSpan, - Statement, VarRef, + Statement, VarRef, WhileSpan, }; use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; @@ -275,6 +275,39 @@ fn compile_if( Ok(()) } +/// Compiles a `WHILE` loop and emits bytecode into `ctx`. +fn compile_while( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: WhileSpan, +) -> Result<()> { + let start_pc = ctx.codegen.next_pc(); + + let (jump_end_pc, cond_reg, guard_pos) = { + let guard_pos = span.expr.start_pos(); + let mut frozen = symtable.frozen(); + let mut scope = frozen.temp_scope(); + let reg = scope.alloc().map_err(|e| Error::from_syms(e, guard_pos))?; + compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, span.expr, ExprType::Boolean)?; + (ctx.codegen.emit(bytecode::make_nop(), guard_pos), reg, guard_pos) + }; + + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(guard_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(start_target), guard_pos); + + let end_addr = ctx.codegen.next_pc(); + let end_target = + u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(guard_pos, end_addr))?; + ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target)); + + Ok(()) +} + /// Compiles a single `stmt` into the `ctx`. fn compile_stmt( ctx: &mut Context, @@ -523,6 +556,10 @@ fn compile_stmt( compile_if(ctx, symtable, span)?; } + Statement::While(span) => { + compile_while(ctx, symtable, span)?; + } + _ => todo!(), } Ok(()) diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 172a65e7..c06cd991 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -68,6 +68,7 @@ one_test!(test_subs); one_test!(test_types); one_test!(test_unary_neg_depth); one_test!(test_unary_not_depth); +one_test!(test_while); #[test] fn test_all_md_files_registered() -> io::Result<()> { diff --git a/core2/tests/test_while.md b/core2/tests/test_while.md new file mode 100644 index 00000000..6b5093bb --- /dev/null +++ b/core2/tests/test_while.md @@ -0,0 +1,174 @@ +# Test: WHILE loop with iterations + +## Source + +```basic +n = 3 +WHILE n > 0 + OUT n + n = n - 1 +WEND +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:7 +0003: LOADI R66, 0 # 2:11 +0004: CMPGTI R65, R65, R66 # 2:9 +0005: JMPF R65, 13 # 2:7 +0006: MOVE R66, R64 # 3:9 +0007: LOADI R65, 258 # 3:9 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R64, R64 # 4:9 +0010: LOADI R65, 1 # 4:13 +0011: SUBI R64, R64, R65 # 4:11 +0012: JUMP 2 # 2:7 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=3% +0=2% +0=1% +``` + +# Test: WHILE loop with zero iterations + +## Source + +```basic +WHILE FALSE + OUT 1 +WEND +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: LOADI R64, 0 # 1:7 +0002: JMPF R64, 7 # 1:7 +0003: LOADI R65, 1 # 2:9 +0004: LOADI R64, 258 # 2:9 +0005: UPCALL 0, R64 # 2:5, OUT +0006: JUMP 1 # 1:7 +0007: LOADI R64, 0 # 0:0 +0008: END R64 # 0:0 +``` + +# Test: WHILE guard must be boolean + +## Source + +```basic +WHILE 2 + OUT 1 +WEND +``` + +## Compilation errors + +```plain +1:7: Expected BOOLEAN but found INTEGER +``` + +# Test: WHILE requires an expression + +## Source + +```basic +WHILE +WEND +``` + +## Compilation errors + +```plain +1:6: No expression in WHILE statement +``` + +# Test: WHILE requires matching WEND + +## Source + +```basic +WHILE TRUE +END +``` + +## Compilation errors + +```plain +1:1: WHILE without WEND +``` + +# Test: EXIT DO outside DO in WHILE + +## Source + +```basic +WHILE TRUE + EXIT DO +WEND +``` + +## Compilation errors + +```plain +2:5: EXIT DO outside of DO +``` + +# Test: EXIT DO in WHILE nested in DO exits DO + +## Source + +```basic +i = 2 +DO WHILE i > 0 + WHILE TRUE + EXIT DO + WEND + OUT i + i = i - 1 +LOOP +OUT 9 +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 2 # 1:5 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 0 # 2:14 +0004: CMPGTI R65, R65, R66 # 2:12 +0005: JMPF R65, 17 # 2:10 +0006: LOADI R65, 1 # 3:11 +0007: JMPF R65, 10 # 3:11 +0008: JUMP 17 # 4:9 +0009: JUMP 6 # 3:11 +0010: MOVE R66, R64 # 6:9 +0011: LOADI R65, 258 # 6:9 +0012: UPCALL 0, R65 # 6:5, OUT +0013: MOVE R64, R64 # 7:9 +0014: LOADI R65, 1 # 7:13 +0015: SUBI R64, R64, R65 # 7:11 +0016: JUMP 2 # 2:10 +0017: LOADI R66, 9 # 9:5 +0018: LOADI R65, 258 # 9:5 +0019: UPCALL 0, R65 # 9:1, OUT +0020: LOADI R65, 0 # 0:0 +0021: END R65 # 0:0 +``` + +## Output + +```plain +0=9% +``` From 243ca23189ae84a9ac71c2cfa40870dcf0d2d0f1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Mar 2026 11:12:08 -0800 Subject: [PATCH 24/53] core2: Implement support for FOR loops and EXIT FOR Compile FOR loops by lowering iterator initialization, loop guards, and updates to existing bytecode control-flow operations. Track nested EXIT FOR jumps similarly to EXIT DO and patch them to the loop epilogue. Add integration coverage for FOR semantics in core2/tests/test_for.md and register the new test in the integration test driver. --- core2/src/compiler/top.rs | 95 ++++- core2/tests/integration_test.rs | 1 + core2/tests/test_for.md | 590 ++++++++++++++++++++++++++++++++ 3 files changed, 684 insertions(+), 2 deletions(-) create mode 100644 core2/tests/test_for.md diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 70ae4269..fa70f635 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,8 +16,8 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ - ArgSep, AssignmentSpan, CallableSpan, DoGuard, DoSpan, EndSpan, Expr, ExprType, IfSpan, - Statement, VarRef, WhileSpan, + ArgSep, AssignmentSpan, CallableSpan, DoGuard, DoSpan, EndSpan, Expr, ExprType, ForSpan, + IfSpan, Statement, VarRef, WhileSpan, }; use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; @@ -51,6 +51,9 @@ struct Context { /// Stack of pending `EXIT DO` jumps for each nested `DO` loop. do_exit_stack: Vec>, + + /// Stack of pending `EXIT FOR` jumps for each nested `FOR` loop. + for_exit_stack: Vec>, } /// Compiles an assignment statement `span` into the `codegen` block. @@ -223,6 +226,82 @@ fn compile_do( Ok(()) } +/// Compiles a `FOR` loop and emits bytecode into `ctx`. +fn compile_for( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: ForSpan, +) -> Result<()> { + if span.iter_double && span.iter.ref_type.is_none() { + match symtable.get_local_or_global(&span.iter) { + Ok(..) => { + // Keep existing iterators as-is. This mirrors core behavior where implicit + // widening to DOUBLE only happens when the iterator does not exist yet. + } + + Err(syms::Error::UndefinedSymbol(..)) => { + let key = SymbolKey::from(&span.iter.name); + if symtable.get_callable(&key).is_some() { + return Err(Error::from_syms( + syms::Error::AlreadyDefined(span.iter.clone()), + span.iter_pos, + )); + } + symtable + .put_local(key, SymbolPrototype::Scalar(ExprType::Double)) + .map_err(|e| Error::from_syms(e, span.iter_pos))?; + } + + Err(e) => return Err(Error::from_syms(e, span.iter_pos)), + } + } + + compile_assignment( + &mut ctx.codegen, + symtable, + AssignmentSpan { vref: span.iter.clone(), vref_pos: span.iter_pos, expr: span.start }, + )?; + + let start_pc = ctx.codegen.next_pc(); + let (jump_end_pc, cond_reg, cond_pos) = { + let cond_pos = span.end.start_pos(); + let mut frozen = symtable.frozen(); + let mut scope = frozen.temp_scope(); + let reg = scope.alloc().map_err(|e| Error::from_syms(e, cond_pos))?; + compile_expr_as_type(&mut ctx.codegen, &mut frozen, reg, span.end, ExprType::Boolean)?; + (ctx.codegen.emit(bytecode::make_nop(), cond_pos), reg, cond_pos) + }; + + ctx.for_exit_stack.push(vec![]); + + for stmt in span.body { + compile_stmt(ctx, symtable, stmt)?; + } + + compile_assignment( + &mut ctx.codegen, + symtable, + AssignmentSpan { vref: span.iter, vref_pos: span.iter_pos, expr: span.next }, + )?; + + let start_target = + u16::try_from(start_pc).map_err(|_| Error::TargetTooFar(cond_pos, start_pc))?; + ctx.codegen.emit(bytecode::make_jump(start_target), cond_pos); + + let end_addr = ctx.codegen.next_pc(); + let end_target = + u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(cond_pos, end_addr))?; + ctx.codegen.patch(jump_end_pc, bytecode::make_jump_if_false(cond_reg, end_target)); + + let exit_jumps = ctx.for_exit_stack.pop().expect("Must have a matching FOR scope"); + for (addr, pos) in exit_jumps { + let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(pos, end_addr))?; + ctx.codegen.patch(addr, bytecode::make_jump(end_target)); + } + + Ok(()) +} + /// Compiles an `IF` statement `span` into the `ctx`. fn compile_if( ctx: &mut Context, @@ -534,6 +613,18 @@ fn compile_stmt( exit_stack.push((addr, span.pos)); } + Statement::ExitFor(span) => { + let Some(exit_stack) = ctx.for_exit_stack.last_mut() else { + return Err(Error::MisplacedExit(span.pos, "FOR")); + }; + let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos); + exit_stack.push((addr, span.pos)); + } + + Statement::For(span) => { + compile_for(ctx, symtable, span)?; + } + Statement::Gosub(span) => { let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); ctx.codegen.add_fixup(addr, Fixup::Gosub(SymbolKey::from(span.target))); diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index c06cd991..60b550e7 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -50,6 +50,7 @@ one_test!(test_bitwise_xor); one_test!(test_do); one_test!(test_empty); one_test!(test_end); +one_test!(test_for); one_test!(test_functions); one_test!(test_globals); one_test!(test_gosub); diff --git a/core2/tests/test_for.md b/core2/tests/test_for.md new file mode 100644 index 00000000..730b9d95 --- /dev/null +++ b/core2/tests/test_for.md @@ -0,0 +1,590 @@ +# Test: Basic FOR incrementing loop + +## Source + +```basic +FOR a = 0 TO 3 + OUT a +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 3 # 1:14 +0004: CMPLEI R65, R65, R66 # 1:11 +0005: JMPF R65, 13 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 1:5 +0010: LOADI R65, 1 # 1:15 +0011: ADDI R64, R64, R65 # 1:11 +0012: JUMP 2 # 1:5 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=0% +0=1% +0=2% +0=3% +``` + +# Test: FOR incrementing loop with STEP + +## Source + +```basic +FOR a = 0 TO 10 STEP 3 + OUT a +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:14 +0004: CMPLEI R65, R65, R66 # 1:11 +0005: JMPF R65, 13 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 1:5 +0010: LOADI R65, 3 # 1:22 +0011: ADDI R64, R64, R65 # 1:11 +0012: JUMP 2 # 1:5 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=0% +0=3% +0=6% +0=9% +``` + +# Test: FOR decrementing loop with negative STEP + +## Source + +```basic +FOR a = 10 TO 1 STEP -2 + OUT a +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 10 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 1 # 1:15 +0004: CMPGEI R65, R65, R66 # 1:12 +0005: JMPF R65, 13 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 1:5 +0010: LOADC R65, 0 # 1:23 +0011: ADDI R64, R64, R65 # 1:12 +0012: JUMP 2 # 1:5 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=10% +0=8% +0=6% +0=4% +0=2% +``` + +# Test: FOR loop can have zero iterations + +## Source + +```basic +FOR i = 10 TO 9 + OUT i +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 10 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 9 # 1:15 +0004: CMPLEI R65, R65, R66 # 1:12 +0005: JMPF R65, 13 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 1:5 +0010: LOADI R65, 1 # 1:16 +0011: ADDI R64, R64, R65 # 1:12 +0012: JUMP 2 # 1:5 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +# Test: FOR loop with invalid direction has zero iterations + +## Source + +```basic +FOR i = 9 TO 10 STEP -1 + OUT i +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 9 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:14 +0004: CMPGEI R65, R65, R66 # 1:11 +0005: JMPF R65, 13 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 1:5 +0010: LOADC R65, 0 # 1:23 +0011: ADDI R64, R64, R65 # 1:11 +0012: JUMP 2 # 1:5 +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +# Test: FOR iterator is visible after NEXT + +## Source + +```basic +FOR something = 1 TO 10 STEP 8 +NEXT +OUT something +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:17 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:22 +0004: CMPLEI R65, R65, R66 # 1:19 +0005: JMPF R65, 10 # 1:5 +0006: MOVE R64, R64 # 1:5 +0007: LOADI R65, 8 # 1:30 +0008: ADDI R64, R64, R65 # 1:19 +0009: JUMP 2 # 1:5 +0010: MOVE R66, R64 # 3:5 +0011: LOADI R65, 258 # 3:5 +0012: UPCALL 0, R65 # 3:1, OUT +0013: LOADI R65, 0 # 0:0 +0014: END R65 # 0:0 +``` + +## Output + +```plain +0=17% +``` + +# Test: FOR iterator can be modified in body + +## Source + +```basic +FOR something = 1 TO 5 + OUT something + something = something + 1 +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:17 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 5 # 1:22 +0004: CMPLEI R65, R65, R66 # 1:19 +0005: JMPF R65, 16 # 1:5 +0006: MOVE R66, R64 # 2:9 +0007: LOADI R65, 258 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: MOVE R64, R64 # 3:17 +0010: LOADI R65, 1 # 3:29 +0011: ADDI R64, R64, R65 # 3:27 +0012: MOVE R64, R64 # 1:5 +0013: LOADI R65, 1 # 1:23 +0014: ADDI R64, R64, R65 # 1:19 +0015: JUMP 2 # 1:5 +0016: LOADI R65, 0 # 0:0 +0017: END R65 # 0:0 +``` + +## Output + +```plain +0=1% +0=3% +0=5% +``` + +# Test: FOR with floating point bounds and integer sink + +## Source + +```basic +FOR a = 1.1 TO 2.1 + b% = (a * 10) + OUT b +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADC R64, 0 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADC R66, 1 # 1:16 +0004: CMPLED R65, R65, R66 # 1:13 +0005: JMPF R65, 19 # 1:5 +0006: MOVE R65, R64 # 2:11 +0007: LOADI R66, 10 # 2:15 +0008: ITOD R66 # 2:15 +0009: MULD R65, R65, R66 # 2:13 +0010: DTOI R65 # 2:11 +0011: MOVE R67, R65 # 3:9 +0012: LOADI R66, 258 # 3:9 +0013: UPCALL 0, R66 # 3:5, OUT +0014: MOVE R64, R64 # 1:5 +0015: LOADI R66, 1 # 1:19 +0016: ITOD R66 # 1:19 +0017: ADDD R64, R64, R66 # 1:13 +0018: JUMP 2 # 1:5 +0019: LOADI R66, 0 # 0:0 +0020: END R66 # 0:0 +``` + +## Output + +```plain +0=11% +0=21% +``` + +# Test: FOR with untyped iterator and floating STEP uses double arithmetic + +## Source + +```basic +i = 0 +FOR iter = 0 TO 2 STEP 0.1 + i = i + 1 + IF i = 5 THEN EXIT FOR +NEXT +b% = (iter * 10) +OUT i; b +``` + +## Disassembly + +```asm +0000: ENTER 7 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: LOADI R65, 0 # 2:12 +0003: ITOD R65 # 2:12 +0004: MOVE R66, R65 # 2:5 +0005: LOADI R67, 2 # 2:17 +0006: ITOD R67 # 2:17 +0007: CMPLED R66, R66, R67 # 2:14 +0008: JMPF R66, 21 # 2:5 +0009: MOVE R64, R64 # 3:9 +0010: LOADI R66, 1 # 3:13 +0011: ADDI R64, R64, R66 # 3:11 +0012: MOVE R66, R64 # 4:8 +0013: LOADI R67, 5 # 4:12 +0014: CMPEQI R66, R66, R67 # 4:10 +0015: JMPF R66, 17 # 4:8 +0016: JUMP 21 # 4:19 +0017: MOVE R65, R65 # 2:5 +0018: LOADC R66, 0 # 2:24 +0019: ADDD R65, R65, R66 # 2:14 +0020: JUMP 4 # 2:5 +0021: MOVE R66, R65 # 6:7 +0022: LOADI R67, 10 # 6:14 +0023: ITOD R67 # 6:14 +0024: MULD R66, R66, R67 # 6:12 +0025: DTOI R66 # 6:7 +0026: MOVE R68, R64 # 7:5 +0027: LOADI R67, 274 # 7:5 +0028: MOVE R70, R66 # 7:8 +0029: LOADI R69, 258 # 7:8 +0030: UPCALL 0, R67 # 7:1, OUT +0031: LOADI R67, 0 # 0:0 +0032: END R67 # 0:0 +``` + +## Output + +```plain +0=5% ; 1=4% +``` + +# Test: FOR with integer iterator and floating STEP can get stuck + +## Source + +```basic +i = 0 +DIM a AS INTEGER +FOR a = 1.0 TO 2.0 STEP 0.4 + i = i + 1 + IF i = 100 THEN + GOTO @out + END IF +NEXT +@out: +OUT i +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: LOADI R65, 0 # 2:5 +0003: LOADC R65, 0 # 3:9 +0004: DTOI R65 # 3:9 +0005: MOVE R66, R65 # 3:5 +0006: LOADC R67, 1 # 3:16 +0007: ITOD R66 # 3:13 +0008: CMPLED R66, R66, R67 # 3:13 +0009: JMPF R66, 24 # 3:5 +0010: MOVE R64, R64 # 4:9 +0011: LOADI R66, 1 # 4:13 +0012: ADDI R64, R64, R66 # 4:11 +0013: MOVE R66, R64 # 5:8 +0014: LOADI R67, 100 # 5:12 +0015: CMPEQI R66, R66, R67 # 5:10 +0016: JMPF R66, 18 # 5:8 +0017: JUMP 24 # 6:14 +0018: MOVE R65, R65 # 3:5 +0019: LOADC R66, 2 # 3:25 +0020: ITOD R65 # 3:13 +0021: ADDD R65, R65, R66 # 3:13 +0022: DTOI R65 # 3:5 +0023: JUMP 5 # 3:5 +0024: MOVE R67, R64 # 10:5 +0025: LOADI R66, 258 # 10:5 +0026: UPCALL 0, R66 # 10:1, OUT +0027: LOADI R66, 0 # 0:0 +0028: END R66 # 0:0 +``` + +## Output + +```plain +0=100% +``` + +# Test: EXIT FOR exits innermost FOR + +## Source + +```basic +FOR i = 1 TO 10 + IF i = 5 THEN EXIT FOR + OUT i +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:14 +0004: CMPLEI R65, R65, R66 # 1:11 +0005: JMPF R65, 18 # 1:5 +0006: MOVE R65, R64 # 2:8 +0007: LOADI R66, 5 # 2:12 +0008: CMPEQI R65, R65, R66 # 2:10 +0009: JMPF R65, 11 # 2:8 +0010: JUMP 18 # 2:19 +0011: MOVE R66, R64 # 3:9 +0012: LOADI R65, 258 # 3:9 +0013: UPCALL 0, R65 # 3:5, OUT +0014: MOVE R64, R64 # 1:5 +0015: LOADI R65, 1 # 1:16 +0016: ADDI R64, R64, R65 # 1:11 +0017: JUMP 2 # 1:5 +0018: LOADI R65, 0 # 0:0 +0019: END R65 # 0:0 +``` + +## Output + +```plain +0=1% +0=2% +0=3% +0=4% +``` + +# Test: EXIT DO and EXIT FOR in nested loops + +## Source + +```basic +FOR i = 1 TO 10 + j = 5 + DO WHILE j > 0 + IF j = 2 THEN EXIT DO + IF i = 4 THEN EXIT FOR + OUT i; j + j = j - 1 + LOOP +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R64, 1 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:14 +0004: CMPLEI R65, R65, R66 # 1:11 +0005: JMPF R65, 34 # 1:5 +0006: LOADI R65, 5 # 2:9 +0007: MOVE R66, R65 # 3:14 +0008: LOADI R67, 0 # 3:18 +0009: CMPGTI R66, R66, R67 # 3:16 +0010: JMPF R66, 30 # 3:14 +0011: MOVE R66, R65 # 4:12 +0012: LOADI R67, 2 # 4:16 +0013: CMPEQI R66, R66, R67 # 4:14 +0014: JMPF R66, 16 # 4:12 +0015: JUMP 30 # 4:23 +0016: MOVE R66, R64 # 5:12 +0017: LOADI R67, 4 # 5:16 +0018: CMPEQI R66, R66, R67 # 5:14 +0019: JMPF R66, 21 # 5:12 +0020: JUMP 34 # 5:23 +0021: MOVE R67, R64 # 6:13 +0022: LOADI R66, 274 # 6:13 +0023: MOVE R69, R65 # 6:16 +0024: LOADI R68, 258 # 6:16 +0025: UPCALL 0, R66 # 6:9, OUT +0026: MOVE R65, R65 # 7:13 +0027: LOADI R66, 1 # 7:17 +0028: SUBI R65, R65, R66 # 7:15 +0029: JUMP 7 # 3:14 +0030: MOVE R64, R64 # 1:5 +0031: LOADI R66, 1 # 1:16 +0032: ADDI R64, R64, R66 # 1:11 +0033: JUMP 2 # 1:5 +0034: LOADI R66, 0 # 0:0 +0035: END R66 # 0:0 +``` + +## Output + +```plain +0=1% ; 1=5% +0=1% ; 1=4% +0=1% ; 1=3% +0=2% ; 1=5% +0=2% ; 1=4% +0=2% ; 1=3% +0=3% ; 1=5% +0=3% ; 1=4% +0=3% ; 1=3% +``` + +# Test: EXIT FOR outside FOR is an error + +## Source + +```basic +EXIT FOR +``` + +## Compilation errors + +```plain +1:1: EXIT FOR outside of FOR +``` + +# Test: EXIT FOR in WHILE is an error + +## Source + +```basic +WHILE TRUE + EXIT FOR +WEND +``` + +## Compilation errors + +```plain +2:5: EXIT FOR outside of FOR +``` + +# Test: FOR guard errors with incompatible types and positive STEP + +## Source + +```basic +FOR i = "a" TO 3 +NEXT +``` + +## Compilation errors + +```plain +1:13: Cannot <= STRING and INTEGER +``` + +# Test: FOR guard errors with incompatible types and negative STEP + +## Source + +```basic +FOR i = 1 TO "b" STEP -8 +NEXT +``` + +## Compilation errors + +```plain +1:11: Cannot >= INTEGER and STRING +``` From 58ccbe6734a8dea6e0ac18702f05cc2494c6f670 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Mar 2026 11:24:01 -0800 Subject: [PATCH 25/53] core2: Implement support for SELECT CASE statements Compile SELECT CASE statements and lower CASE guards to relational checks plus jump chains. Add an integration test suite for SELECT behavior and register it in the integration harness. To make this possible, we need to compile SELECT CASE test expressions into reserved temporary registers _without_ entering a TempSymtable. This is difficult due to lifetimes and yields a pretty complex with_reserved_temp function; however, given that we only seem to need this for SELECT, I'm not bothering to making this better for now. --- core2/src/compiler/syms.rs | 132 ++++++- core2/src/compiler/top.rs | 279 ++++++++++++++- core2/tests/integration_test.rs | 1 + core2/tests/test_select.md | 595 ++++++++++++++++++++++++++++++++ 4 files changed, 1000 insertions(+), 7 deletions(-) create mode 100644 core2/tests/test_select.md diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index e0c431a4..386c79c5 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -19,6 +19,7 @@ use crate::ast::{ExprType, VarRef}; use crate::bytecode::{Register, RegisterScope}; use crate::compiler::ids::HashMapWithIds; use crate::{CallableMetadata, bytecode}; +use std::cell::Cell; use std::cell::RefCell; use std::cmp::max; use std::collections::HashMap; @@ -228,12 +229,20 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// by this local symtable. This is used to determine the size of the scope for register /// allocation purposes at runtime. count_temps: u8, + + /// Number of reserved temporary registers that are active outside of `TempScope`. + active_temps: Rc>, } impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// Creates a new local symbol table within the context of a global `symtable`. fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>) -> Self { - Self { symtable, locals: HashMapWithIds::default(), count_temps: 0 } + Self { + symtable, + locals: HashMapWithIds::default(), + count_temps: 0, + active_temps: Rc::from(Cell::new(0)), + } } /// Consumes the local scope and returns the number of local variables defined, which includes @@ -259,6 +268,50 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { TempSymtable::new(self) } + /// Reserves one temporary register for the duration of `f`. + pub(crate) fn with_reserved_temp( + &mut self, + map_error: ME, + f: F, + ) -> std::result::Result + where + ME: Fn(Error) -> E, + F: FnOnce( + Register, + &mut TempSymtable<'uref, 'ukey, 'umd, '_, 'a>, + ) -> std::result::Result, + { + struct TempReservationGuard { + active_temps: Rc>, + } + + impl Drop for TempReservationGuard { + fn drop(&mut self) { + let active_temps = self.active_temps.get(); + debug_assert!(active_temps > 0); + self.active_temps.set(active_temps - 1); + } + } + + let nlocals = u8::try_from(self.locals.len()) + .map_err(|_| map_error(Error::OutOfRegisters(RegisterScope::Local)))?; + let first_temp = self.active_temps.get(); + let new_active_temps = first_temp + .checked_add(1) + .ok_or(map_error(Error::OutOfRegisters(RegisterScope::Temp)))?; + self.active_temps.set(new_active_temps); + self.count_temps = max(self.count_temps, new_active_temps); + let _guard = TempReservationGuard { active_temps: self.active_temps.clone() }; + + let reg_idx = u8::try_from(usize::from(nlocals) + usize::from(first_temp)) + .map_err(|_| map_error(Error::OutOfRegisters(RegisterScope::Temp)))?; + let reg = Register::local(reg_idx) + .map_err(|_| map_error(Error::OutOfRegisters(RegisterScope::Temp)))?; + + let mut temp = self.frozen(); + f(reg, &mut temp) + } + /// Creates a new global symbol `key` with `proto` via the parent global symbol table. pub(crate) fn put_global( &mut self, @@ -327,6 +380,9 @@ pub(crate) struct TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { /// Reference to the underlying local symbol table. symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>, + /// Number of temporary registers that were already reserved on creation. + base_temp: u8, + /// Index of the next temporary register to allocate. next_temp: Rc>, @@ -336,7 +392,7 @@ pub(crate) struct TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { impl<'uref, 'ukey, 'umd, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { fn drop(&mut self) { - debug_assert_eq!(0, *self.next_temp.borrow(), "Unbalanced temp drops"); + debug_assert_eq!(self.base_temp, *self.next_temp.borrow(), "Unbalanced temp drops"); self.symtable.count_temps = max(self.symtable.count_temps, *self.count_temps.borrow()); } } @@ -344,10 +400,12 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'umd impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { /// Creates a new temporary symbol table from a `local` table. fn new(symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>) -> Self { + let base_temp = symtable.active_temps.get(); Self { symtable, - next_temp: Rc::from(RefCell::from(0)), - count_temps: Rc::from(RefCell::from(0)), + base_temp, + next_temp: Rc::from(RefCell::from(base_temp)), + count_temps: Rc::from(RefCell::from(base_temp)), } } @@ -804,6 +862,72 @@ mod tests { Ok(()) } + #[test] + fn test_with_reserved_temp_register_index() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; + local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; + + local.with_reserved_temp( + |e| e, + |reg, _| { + assert_eq!(Register::local(2).unwrap(), reg); + Ok(()) + }, + )?; + + assert_eq!(3, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_with_reserved_temp_shifts_temp_scope_base() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; + + local.with_reserved_temp( + |e| e, + |reserved, temp| { + assert_eq!(Register::local(1).unwrap(), reserved); + let mut scope = temp.temp_scope(); + assert_eq!(Register::local(2).unwrap(), scope.alloc()?); + Ok(()) + }, + )?; + + assert_eq!(3, local.leave_scope()?); + Ok(()) + } + + #[test] + fn test_with_reserved_temp_released_after_error() { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + + let err = local + .with_reserved_temp( + |e| e, + |_, _| Err::<(), Error>(Error::OutOfRegisters(RegisterScope::Temp)), + ) + .unwrap_err(); + assert_eq!("Out of temp registers", err.to_string()); + + local + .with_reserved_temp( + |e| e, + |reg, _| { + assert_eq!(Register::local(0).unwrap(), reg); + Ok(()) + }, + ) + .unwrap(); + } + #[test] fn test_temp_scope_lookup_vars() -> Result<()> { let upcalls = HashMap::default(); diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index fa70f635..d07c31b2 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,15 +16,17 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ - ArgSep, AssignmentSpan, CallableSpan, DoGuard, DoSpan, EndSpan, Expr, ExprType, ForSpan, - IfSpan, Statement, VarRef, WhileSpan, + ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, EndSpan, Expr, + ExprType, ForSpan, IfSpan, SelectSpan, Statement, VarRef, WhileSpan, }; use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::exprs::{compile_expr, compile_expr_as_type, compile_integer_exprs}; -use crate::compiler::syms::{self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype}; +use crate::compiler::syms::{ + self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype, TempSymtable, +}; use crate::compiler::{Error, Result}; use crate::image::{GlobalVarInfo, Image}; use crate::mem::ConstantDatum; @@ -112,6 +114,273 @@ fn compile_assignment( symtable.fixup_local_type(&span.vref, etype).map_err(|e| Error::from_syms(e, vref_pos)) } +/// Returns the textual name of `relop` for diagnostics. +fn case_relop_name(relop: &CaseRelOp) -> &'static str { + match relop { + CaseRelOp::Equal => "=", + CaseRelOp::NotEqual => "<>", + CaseRelOp::Less => "<", + CaseRelOp::LessEqual => "<=", + CaseRelOp::Greater => ">", + CaseRelOp::GreaterEqual => ">=", + } +} + +/// Returns the bytecode opcode constructor for `relop` and operands of type `etype`. +fn case_relop_instr( + relop: &CaseRelOp, + etype: ExprType, +) -> Option u32> { + match etype { + ExprType::Boolean => match relop { + CaseRelOp::Equal => Some(bytecode::make_equal_boolean), + CaseRelOp::NotEqual => Some(bytecode::make_not_equal_boolean), + CaseRelOp::Less + | CaseRelOp::LessEqual + | CaseRelOp::Greater + | CaseRelOp::GreaterEqual => None, + }, + + ExprType::Double => match relop { + CaseRelOp::Equal => Some(bytecode::make_equal_double), + CaseRelOp::NotEqual => Some(bytecode::make_not_equal_double), + CaseRelOp::Less => Some(bytecode::make_less_double), + CaseRelOp::LessEqual => Some(bytecode::make_less_equal_double), + CaseRelOp::Greater => Some(bytecode::make_greater_double), + CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_double), + }, + + ExprType::Integer => match relop { + CaseRelOp::Equal => Some(bytecode::make_equal_integer), + CaseRelOp::NotEqual => Some(bytecode::make_not_equal_integer), + CaseRelOp::Less => Some(bytecode::make_less_integer), + CaseRelOp::LessEqual => Some(bytecode::make_less_equal_integer), + CaseRelOp::Greater => Some(bytecode::make_greater_integer), + CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_integer), + }, + + ExprType::Text => match relop { + CaseRelOp::Equal => Some(bytecode::make_equal_text), + CaseRelOp::NotEqual => Some(bytecode::make_not_equal_text), + CaseRelOp::Less => Some(bytecode::make_less_text), + CaseRelOp::LessEqual => Some(bytecode::make_less_equal_text), + CaseRelOp::Greater => Some(bytecode::make_greater_text), + CaseRelOp::GreaterEqual => Some(bytecode::make_greater_equal_text), + }, + } +} + +/// Compiles a comparison between `test` and `rhs`, leaving the boolean result in `dest`. +fn compile_case_relop( + ctx: &mut Context, + pos: LineCol, + test: (Register, ExprType), + rhs: (Register, ExprType), + relop: CaseRelOp, + dest: Register, +) -> Result<()> { + let (test_reg, test_type) = test; + let (rhs_reg, rhs_type) = rhs; + let op_name = case_relop_name(&relop); + let opcode = match (test_type, rhs_type) { + (ExprType::Double, ExprType::Integer) => { + ctx.codegen.emit(bytecode::make_integer_to_double(rhs_reg), pos); + case_relop_instr(&relop, ExprType::Double) + } + + (ExprType::Integer, ExprType::Double) => { + ctx.codegen.emit(bytecode::make_integer_to_double(test_reg), pos); + case_relop_instr(&relop, ExprType::Double) + } + + (lhs, rhs) if lhs == rhs => case_relop_instr(&relop, lhs), + _ => None, + }; + + match opcode { + Some(opcode) => { + ctx.codegen.emit(opcode(dest, test_reg, rhs_reg), pos); + Ok(()) + } + None => Err(Error::BinaryOpType(pos, op_name, test_type, rhs_type)), + } +} + +/// Compiles one `CASE` guard and returns the register and source position of its boolean result. +fn compile_case_guard( + ctx: &mut Context, + symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + test_reg: Register, + test_type: ExprType, + guard: CaseGuardSpan, +) -> Result<(Register, LineCol)> { + match guard { + CaseGuardSpan::Is(relop, expr) => { + let pos = expr.start_pos(); + + let mut scope = symtable.temp_scope(); + let lhs_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let rhs_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let cond_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + + ctx.codegen.emit(bytecode::make_move(lhs_reg, test_reg), pos); + let rhs_type = compile_expr(&mut ctx.codegen, symtable, rhs_reg, expr)?; + compile_case_relop( + ctx, + pos, + (lhs_reg, test_type), + (rhs_reg, rhs_type), + relop, + cond_reg, + )?; + + Ok((cond_reg, pos)) + } + + CaseGuardSpan::To(from_expr, to_expr) => { + let pos = from_expr.start_pos(); + + let mut scope = symtable.temp_scope(); + let lhs_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let rhs_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let cond_from_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + + let lhs_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let rhs_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + let cond_to_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + + let cond_reg = scope.alloc().map_err(|e| Error::from_syms(e, pos))?; + + ctx.codegen.emit(bytecode::make_move(lhs_from_reg, test_reg), pos); + let rhs_from_type = compile_expr(&mut ctx.codegen, symtable, rhs_from_reg, from_expr)?; + compile_case_relop( + ctx, + pos, + (lhs_from_reg, test_type), + (rhs_from_reg, rhs_from_type), + CaseRelOp::GreaterEqual, + cond_from_reg, + )?; + + ctx.codegen.emit(bytecode::make_move(lhs_to_reg, test_reg), pos); + let rhs_to_type = compile_expr(&mut ctx.codegen, symtable, rhs_to_reg, to_expr)?; + compile_case_relop( + ctx, + pos, + (lhs_to_reg, test_type), + (rhs_to_reg, rhs_to_type), + CaseRelOp::LessEqual, + cond_to_reg, + )?; + + ctx.codegen.emit(bytecode::make_bitwise_and(cond_reg, cond_from_reg, cond_to_reg), pos); + Ok((cond_reg, pos)) + } + } +} + +/// Compiles a `SELECT` statement and emits bytecode into `ctx`. +fn compile_select( + ctx: &mut Context, + symtable: &mut LocalSymtable<'_, '_, '_, '_>, + span: SelectSpan, +) -> Result<()> { + let end_pos = span.end_pos; + let ncases = span.cases.len(); + let select_cases = span.cases; + let select_expr = span.expr; + let select_expr_pos = select_expr.start_pos(); + + struct PendingCase { + body: Vec, + body_jump_pcs: Vec<(usize, LineCol)>, + has_next_case: bool, + } + + let mut pending_cases = Vec::with_capacity(ncases); + let mut pending_next_case_jump: Option<(usize, LineCol)> = None; + + symtable.with_reserved_temp( + |e| Error::from_syms(e, select_expr_pos), + |test_reg, frozen| { + let test_type = compile_expr(&mut ctx.codegen, frozen, test_reg, select_expr)?; + + for (i, case) in select_cases.into_iter().enumerate() { + let has_next_case = i < ncases - 1; + let mut body_jump_pcs = vec![]; + let case_dispatch_addr = ctx.codegen.next_pc(); + if let Some((jump_pc, pos)) = pending_next_case_jump.take() { + let target = u16::try_from(case_dispatch_addr) + .map_err(|_| Error::TargetTooFar(pos, case_dispatch_addr))?; + ctx.codegen.patch(jump_pc, bytecode::make_jump(target)); + } + + if case.guards.is_empty() { + let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos); + body_jump_pcs.push((jump_body_pc, end_pos)); + } else { + for guard in case.guards { + let (cond_reg, pos) = + compile_case_guard(ctx, frozen, test_reg, test_type, guard)?; + let jump_next_guard_pc = ctx.codegen.emit(bytecode::make_nop(), pos); + let jump_body_pc = ctx.codegen.emit(bytecode::make_nop(), pos); + body_jump_pcs.push((jump_body_pc, pos)); + + let next_addr = ctx.codegen.next_pc(); + let target = u16::try_from(next_addr) + .map_err(|_| Error::TargetTooFar(pos, next_addr))?; + ctx.codegen.patch( + jump_next_guard_pc, + bytecode::make_jump_if_false(cond_reg, target), + ); + } + + let jump_next_case_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos); + pending_next_case_jump = Some((jump_next_case_pc, end_pos)); + } + + pending_cases.push(PendingCase { body: case.body, body_jump_pcs, has_next_case }); + } + + Ok(()) + }, + )?; + + let dispatch_end_jump_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos); + if let Some((jump_pc, pos)) = pending_next_case_jump { + let target = + u16::try_from(dispatch_end_jump_pc).map_err(|_| Error::TargetTooFar(pos, dispatch_end_jump_pc))?; + ctx.codegen.patch(jump_pc, bytecode::make_jump(target)); + } + + let mut end_jumps = vec![]; + for case in pending_cases { + let body_addr = ctx.codegen.next_pc(); + for (jump_body_pc, pos) in case.body_jump_pcs { + let target = + u16::try_from(body_addr).map_err(|_| Error::TargetTooFar(pos, body_addr))?; + ctx.codegen.patch(jump_body_pc, bytecode::make_jump(target)); + } + + for stmt in case.body { + compile_stmt(ctx, symtable, stmt)?; + } + + if case.has_next_case { + end_jumps.push(ctx.codegen.emit(bytecode::make_nop(), end_pos)); + } + } + + let end_addr = ctx.codegen.next_pc(); + let end_target = u16::try_from(end_addr).map_err(|_| Error::TargetTooFar(end_pos, end_addr))?; + ctx.codegen.patch(dispatch_end_jump_pc, bytecode::make_jump(end_target)); + for end_jump in end_jumps { + ctx.codegen.patch(end_jump, bytecode::make_jump(end_target)); + } + + Ok(()) +} + /// Compiles a `DO` loop and emits bytecode into `ctx`. fn compile_do( ctx: &mut Context, @@ -647,6 +916,10 @@ fn compile_stmt( compile_if(ctx, symtable, span)?; } + Statement::Select(span) => { + compile_select(ctx, symtable, span)?; + } + Statement::While(span) => { compile_while(ctx, symtable, span)?; } diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 60b550e7..d6740318 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -64,6 +64,7 @@ one_test!(test_relational_gt); one_test!(test_relational_le); one_test!(test_relational_lt); one_test!(test_relational_ne); +one_test!(test_select); one_test!(test_strings); one_test!(test_subs); one_test!(test_types); diff --git a/core2/tests/test_select.md b/core2/tests/test_select.md new file mode 100644 index 00000000..bccd46cb --- /dev/null +++ b/core2/tests/test_select.md @@ -0,0 +1,595 @@ +# Test: Basic SELECT CASE matching + +## Source + +```basic +n = 3 +SELECT CASE n + CASE 1, 3, 5, 7, 9 + OUT "odd" + CASE 0, 2, 4, 6, 8 + OUT "even" + CASE 10 TO 20 + OUT "large" + CASE IS < 0 + OUT "negative" + CASE ELSE + OUT "too large" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 9 # 0:0 +0001: LOADI R64, 3 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 1 # 3:10 +0005: CMPEQI R68, R66, R67 # 3:10 +0006: JMPF R68, 8 # 3:10 +0007: JUMP 73 # 3:10 +0008: MOVE R66, R65 # 3:13 +0009: LOADI R67, 3 # 3:13 +0010: CMPEQI R68, R66, R67 # 3:13 +0011: JMPF R68, 13 # 3:13 +0012: JUMP 73 # 3:13 +0013: MOVE R66, R65 # 3:16 +0014: LOADI R67, 5 # 3:16 +0015: CMPEQI R68, R66, R67 # 3:16 +0016: JMPF R68, 18 # 3:16 +0017: JUMP 73 # 3:16 +0018: MOVE R66, R65 # 3:19 +0019: LOADI R67, 7 # 3:19 +0020: CMPEQI R68, R66, R67 # 3:19 +0021: JMPF R68, 23 # 3:19 +0022: JUMP 73 # 3:19 +0023: MOVE R66, R65 # 3:22 +0024: LOADI R67, 9 # 3:22 +0025: CMPEQI R68, R66, R67 # 3:22 +0026: JMPF R68, 28 # 3:22 +0027: JUMP 73 # 3:22 +0028: JUMP 29 # 13:1 +0029: MOVE R66, R65 # 5:10 +0030: LOADI R67, 0 # 5:10 +0031: CMPEQI R68, R66, R67 # 5:10 +0032: JMPF R68, 34 # 5:10 +0033: JUMP 77 # 5:10 +0034: MOVE R66, R65 # 5:13 +0035: LOADI R67, 2 # 5:13 +0036: CMPEQI R68, R66, R67 # 5:13 +0037: JMPF R68, 39 # 5:13 +0038: JUMP 77 # 5:13 +0039: MOVE R66, R65 # 5:16 +0040: LOADI R67, 4 # 5:16 +0041: CMPEQI R68, R66, R67 # 5:16 +0042: JMPF R68, 44 # 5:16 +0043: JUMP 77 # 5:16 +0044: MOVE R66, R65 # 5:19 +0045: LOADI R67, 6 # 5:19 +0046: CMPEQI R68, R66, R67 # 5:19 +0047: JMPF R68, 49 # 5:19 +0048: JUMP 77 # 5:19 +0049: MOVE R66, R65 # 5:22 +0050: LOADI R67, 8 # 5:22 +0051: CMPEQI R68, R66, R67 # 5:22 +0052: JMPF R68, 54 # 5:22 +0053: JUMP 77 # 5:22 +0054: JUMP 55 # 13:1 +0055: MOVE R66, R65 # 7:10 +0056: LOADI R67, 10 # 7:10 +0057: CMPGEI R68, R66, R67 # 7:10 +0058: MOVE R69, R65 # 7:10 +0059: LOADI R70, 20 # 7:16 +0060: CMPLEI R71, R69, R70 # 7:10 +0061: AND R72, R68, R71 # 7:10 +0062: JMPF R72, 64 # 7:10 +0063: JUMP 81 # 7:10 +0064: JUMP 65 # 13:1 +0065: MOVE R66, R65 # 9:15 +0066: LOADI R67, 0 # 9:15 +0067: CMPLTI R68, R66, R67 # 9:15 +0068: JMPF R68, 70 # 9:15 +0069: JUMP 85 # 9:15 +0070: JUMP 71 # 13:1 +0071: JUMP 89 # 13:1 +0072: JUMP 92 # 13:1 +0073: LOADI R66, 0 # 4:13 +0074: LOADI R65, 259 # 4:13 +0075: UPCALL 0, R65 # 4:9, OUT +0076: JUMP 92 # 13:1 +0077: LOADI R66, 1 # 6:13 +0078: LOADI R65, 259 # 6:13 +0079: UPCALL 0, R65 # 6:9, OUT +0080: JUMP 92 # 13:1 +0081: LOADI R66, 2 # 8:13 +0082: LOADI R65, 259 # 8:13 +0083: UPCALL 0, R65 # 8:9, OUT +0084: JUMP 92 # 13:1 +0085: LOADI R66, 3 # 10:13 +0086: LOADI R65, 259 # 10:13 +0087: UPCALL 0, R65 # 10:9, OUT +0088: JUMP 92 # 13:1 +0089: LOADI R66, 4 # 12:13 +0090: LOADI R65, 259 # 12:13 +0091: UPCALL 0, R65 # 12:9, OUT +0092: LOADI R65, 0 # 0:0 +0093: END R65 # 0:0 +``` + +## Output + +```plain +0=odd$ +``` + +# Test: CASE ELSE fallback + +## Source + +```basic +n = 21 +SELECT CASE n + CASE 1 + OUT "one" + CASE ELSE + OUT "fallback" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 21 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 1 # 3:10 +0005: CMPEQI R68, R66, R67 # 3:10 +0006: JMPF R68, 8 # 3:10 +0007: JUMP 11 # 3:10 +0008: JUMP 9 # 7:1 +0009: JUMP 15 # 7:1 +0010: JUMP 18 # 7:1 +0011: LOADI R66, 0 # 4:13 +0012: LOADI R65, 259 # 4:13 +0013: UPCALL 0, R65 # 4:9, OUT +0014: JUMP 18 # 7:1 +0015: LOADI R66, 1 # 6:13 +0016: LOADI R65, 259 # 6:13 +0017: UPCALL 0, R65 # 6:9, OUT +0018: LOADI R65, 0 # 0:0 +0019: END R65 # 0:0 +``` + +## Output + +```plain +0=fallback$ +``` + +# Test: SELECT test expression evaluated once + +## Source + +```basic +SELECT CASE MEANING_OF_LIFE + 1 + CASE 43 + OUT "ok" + CASE 100 + OUT "nope" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: UPCALL 0, R64 # 1:13, MEANING_OF_LIFE +0002: LOADI R65, 1 # 1:31 +0003: ADDI R64, R64, R65 # 1:29 +0004: MOVE R65, R64 # 2:10 +0005: LOADI R66, 43 # 2:10 +0006: CMPEQI R67, R65, R66 # 2:10 +0007: JMPF R67, 9 # 2:10 +0008: JUMP 17 # 2:10 +0009: JUMP 10 # 6:1 +0010: MOVE R65, R64 # 4:10 +0011: LOADI R66, 100 # 4:10 +0012: CMPEQI R67, R65, R66 # 4:10 +0013: JMPF R67, 15 # 4:10 +0014: JUMP 21 # 4:10 +0015: JUMP 16 # 6:1 +0016: JUMP 24 # 6:1 +0017: LOADI R65, 0 # 3:13 +0018: LOADI R64, 259 # 3:13 +0019: UPCALL 1, R64 # 3:9, OUT +0020: JUMP 24 # 6:1 +0021: LOADI R65, 1 # 5:13 +0022: LOADI R64, 259 # 5:13 +0023: UPCALL 1, R64 # 5:9, OUT +0024: LOADI R64, 0 # 0:0 +0025: END R64 # 0:0 +``` + +## Output + +```plain +0=ok$ +``` + +# Test: SELECT with no cases still evaluates expression once + +## Source + +```basic +SELECT CASE MEANING_OF_LIFE + 1 +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: UPCALL 0, R64 # 1:13, MEANING_OF_LIFE +0002: LOADI R65, 1 # 1:31 +0003: ADDI R64, R64, R65 # 1:29 +0004: JUMP 5 # 2:1 +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +# Test: CASE IS and TO with strings + +## Source + +```basic +s = "M" +SELECT CASE s + CASE "exact" + OUT "exact" + CASE IS > "ZZZ" + OUT "is" + CASE "B" TO "Y" + OUT "to" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 9 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 1 # 3:10 +0005: CMPEQS R68, R66, R67 # 3:10 +0006: JMPF R68, 8 # 3:10 +0007: JUMP 26 # 3:10 +0008: JUMP 9 # 9:1 +0009: MOVE R66, R65 # 5:15 +0010: LOADI R67, 2 # 5:15 +0011: CMPGTS R68, R66, R67 # 5:15 +0012: JMPF R68, 14 # 5:15 +0013: JUMP 30 # 5:15 +0014: JUMP 15 # 9:1 +0015: MOVE R66, R65 # 7:10 +0016: LOADI R67, 3 # 7:10 +0017: CMPGES R68, R66, R67 # 7:10 +0018: MOVE R69, R65 # 7:10 +0019: LOADI R70, 4 # 7:17 +0020: CMPLES R71, R69, R70 # 7:10 +0021: AND R72, R68, R71 # 7:10 +0022: JMPF R72, 24 # 7:10 +0023: JUMP 34 # 7:10 +0024: JUMP 25 # 9:1 +0025: JUMP 37 # 9:1 +0026: LOADI R66, 1 # 4:13 +0027: LOADI R65, 259 # 4:13 +0028: UPCALL 0, R65 # 4:9, OUT +0029: JUMP 37 # 9:1 +0030: LOADI R66, 5 # 6:13 +0031: LOADI R65, 259 # 6:13 +0032: UPCALL 0, R65 # 6:9, OUT +0033: JUMP 37 # 9:1 +0034: LOADI R66, 6 # 8:13 +0035: LOADI R65, 259 # 8:13 +0036: UPCALL 0, R65 # 8:9, OUT +0037: LOADI R65, 0 # 0:0 +0038: END R65 # 0:0 +``` + +## Output + +```plain +0=to$ +``` + +# Test: DOUBLE test expression compared against INTEGER guards + +## Source + +```basic +n# = 5.1 +SELECT CASE n# + CASE 2 + OUT "bad" + CASE IS > 5 + OUT "ok" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADC R64, 0 # 1:6 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 2 # 3:10 +0005: ITOD R67 # 3:10 +0006: CMPEQD R68, R66, R67 # 3:10 +0007: JMPF R68, 9 # 3:10 +0008: JUMP 18 # 3:10 +0009: JUMP 10 # 7:1 +0010: MOVE R66, R65 # 5:15 +0011: LOADI R67, 5 # 5:15 +0012: ITOD R67 # 5:15 +0013: CMPGTD R68, R66, R67 # 5:15 +0014: JMPF R68, 16 # 5:15 +0015: JUMP 22 # 5:15 +0016: JUMP 17 # 7:1 +0017: JUMP 25 # 7:1 +0018: LOADI R66, 1 # 4:13 +0019: LOADI R65, 259 # 4:13 +0020: UPCALL 0, R65 # 4:9, OUT +0021: JUMP 25 # 7:1 +0022: LOADI R66, 2 # 6:13 +0023: LOADI R65, 259 # 6:13 +0024: UPCALL 0, R65 # 6:9, OUT +0025: LOADI R65, 0 # 0:0 +0026: END R65 # 0:0 +``` + +## Output + +```plain +0=ok$ +``` + +# Test: INTEGER test expression compared against DOUBLE guards + +## Source + +```basic +n = 11 +SELECT CASE n + CASE 2.0 + OUT "bad" + CASE 5.0, -1.0 + OUT "bad" + CASE 10.2 TO 11.8 + OUT "ok" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 9 # 0:0 +0001: LOADI R64, 11 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADC R67, 0 # 3:10 +0005: ITOD R66 # 3:10 +0006: CMPEQD R68, R66, R67 # 3:10 +0007: JMPF R68, 9 # 3:10 +0008: JUMP 37 # 3:10 +0009: JUMP 10 # 9:1 +0010: MOVE R66, R65 # 5:10 +0011: LOADC R67, 1 # 5:10 +0012: ITOD R66 # 5:10 +0013: CMPEQD R68, R66, R67 # 5:10 +0014: JMPF R68, 16 # 5:10 +0015: JUMP 41 # 5:10 +0016: MOVE R66, R65 # 5:15 +0017: LOADC R67, 2 # 5:16 +0018: NEGD R67 # 5:15 +0019: ITOD R66 # 5:15 +0020: CMPEQD R68, R66, R67 # 5:15 +0021: JMPF R68, 23 # 5:15 +0022: JUMP 41 # 5:15 +0023: JUMP 24 # 9:1 +0024: MOVE R66, R65 # 7:10 +0025: LOADC R67, 3 # 7:10 +0026: ITOD R66 # 7:10 +0027: CMPGED R68, R66, R67 # 7:10 +0028: MOVE R69, R65 # 7:10 +0029: LOADC R70, 4 # 7:18 +0030: ITOD R69 # 7:10 +0031: CMPLED R71, R69, R70 # 7:10 +0032: AND R72, R68, R71 # 7:10 +0033: JMPF R72, 35 # 7:10 +0034: JUMP 45 # 7:10 +0035: JUMP 36 # 9:1 +0036: JUMP 48 # 9:1 +0037: LOADI R66, 5 # 4:13 +0038: LOADI R65, 259 # 4:13 +0039: UPCALL 0, R65 # 4:9, OUT +0040: JUMP 48 # 9:1 +0041: LOADI R66, 5 # 6:13 +0042: LOADI R65, 259 # 6:13 +0043: UPCALL 0, R65 # 6:9, OUT +0044: JUMP 48 # 9:1 +0045: LOADI R66, 6 # 8:13 +0046: LOADI R65, 259 # 8:13 +0047: UPCALL 0, R65 # 8:9, OUT +0048: LOADI R65, 0 # 0:0 +0049: END R65 # 0:0 +``` + +## Output + +```plain +0=ok$ +``` + +# Test: Nested SELECT blocks + +## Source + +```basic +i = 5 +SELECT CASE i + CASE 5 + OUT "ok 1" + i = 6 + SELECT CASE i + CASE 6 + OUT "ok 2" + END SELECT + CASE 6 + OUT "not ok" +END SELECT +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 5 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 5 # 3:10 +0005: CMPEQI R68, R66, R67 # 3:10 +0006: JMPF R68, 8 # 3:10 +0007: JUMP 16 # 3:10 +0008: JUMP 9 # 12:1 +0009: MOVE R66, R65 # 10:10 +0010: LOADI R67, 6 # 10:10 +0011: CMPEQI R68, R66, R67 # 10:10 +0012: JMPF R68, 14 # 10:10 +0013: JUMP 32 # 10:10 +0014: JUMP 15 # 12:1 +0015: JUMP 35 # 12:1 +0016: LOADI R66, 0 # 4:13 +0017: LOADI R65, 259 # 4:13 +0018: UPCALL 0, R65 # 4:9, OUT +0019: LOADI R64, 6 # 5:13 +0020: MOVE R65, R64 # 6:21 +0021: MOVE R66, R65 # 7:18 +0022: LOADI R67, 6 # 7:18 +0023: CMPEQI R68, R66, R67 # 7:18 +0024: JMPF R68, 26 # 7:18 +0025: JUMP 28 # 7:18 +0026: JUMP 27 # 9:9 +0027: JUMP 31 # 9:9 +0028: LOADI R66, 1 # 8:21 +0029: LOADI R65, 259 # 8:21 +0030: UPCALL 0, R65 # 8:17, OUT +0031: JUMP 35 # 12:1 +0032: LOADI R66, 2 # 11:13 +0033: LOADI R65, 259 # 11:13 +0034: UPCALL 0, R65 # 11:9, OUT +0035: LOADI R65, 0 # 0:0 +0036: END R65 # 0:0 +``` + +## Output + +```plain +0=ok 1$ +0=ok 2$ +``` + +# Test: SELECT nested indirectly through GOSUB + +## Source + +```basic +i = 5 +SELECT CASE i + CASE 5 + OUT "ok 1" + GOSUB @another + CASE 6 + OUT "not ok" +END SELECT +GOTO @end +@another +i = 6 +SELECT CASE i + CASE 6 + OUT "ok 2" +END SELECT +RETURN +@end +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R64, 5 # 1:5 +0002: MOVE R65, R64 # 2:13 +0003: MOVE R66, R65 # 3:10 +0004: LOADI R67, 5 # 3:10 +0005: CMPEQI R68, R66, R67 # 3:10 +0006: JMPF R68, 8 # 3:10 +0007: JUMP 16 # 3:10 +0008: JUMP 9 # 8:1 +0009: MOVE R66, R65 # 6:10 +0010: LOADI R67, 6 # 6:10 +0011: CMPEQI R68, R66, R67 # 6:10 +0012: JMPF R68, 14 # 6:10 +0013: JUMP 21 # 6:10 +0014: JUMP 15 # 8:1 +0015: JUMP 24 # 8:1 +0016: LOADI R66, 0 # 4:13 +0017: LOADI R65, 259 # 4:13 +0018: UPCALL 0, R65 # 4:9, OUT +0019: GOSUB 25 # 5:15 +0020: JUMP 24 # 8:1 +0021: LOADI R66, 1 # 7:13 +0022: LOADI R65, 259 # 7:13 +0023: UPCALL 0, R65 # 7:9, OUT +0024: JUMP 38 # 9:6 +0025: LOADI R64, 6 # 11:5 +0026: MOVE R65, R64 # 12:13 +0027: MOVE R66, R65 # 13:10 +0028: LOADI R67, 6 # 13:10 +0029: CMPEQI R68, R66, R67 # 13:10 +0030: JMPF R68, 32 # 13:10 +0031: JUMP 34 # 13:10 +0032: JUMP 33 # 15:1 +0033: JUMP 37 # 15:1 +0034: LOADI R66, 2 # 14:13 +0035: LOADI R65, 259 # 14:13 +0036: UPCALL 0, R65 # 14:9, OUT +0037: RETURN # 16:1 +0038: LOADI R65, 0 # 0:0 +0039: END R65 # 0:0 +``` + +## Output + +```plain +0=ok 1$ +0=ok 2$ +``` + +# Test: CASE guard type mismatch + +## Source + +```basic +SELECT CASE 2 + CASE FALSE +END SELECT +``` + +## Compilation errors + +```plain +2:10: Cannot = INTEGER and BOOLEAN +``` From 423f80a47b60358668f7ad79cb1e50639fa84164 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Mon, 2 Mar 2026 11:36:06 -0800 Subject: [PATCH 26/53] core2: Implement support for EXIT FUNCTION/SUB Compile EXIT FUNCTION and EXIT SUB into jumps to callable return sites, and report misplaced exits for invalid contexts. Add integration coverage for early exits and mismatched FUNCTION/SUB exit statements. --- core2/src/compiler/top.rs | 45 ++++++++++++++++ core2/tests/test_functions.md | 92 +++++++++++++++++++++++++++++++++ core2/tests/test_subs.md | 96 +++++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+) diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index d07c31b2..22090252 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -39,6 +39,16 @@ use std::io; use std::iter::Iterator; use std::rc::Rc; +/// Kind of a user-defined callable. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum CallableKind { + /// A function definition. + Function, + + /// A subroutine definition. + Sub, +} + /// Bag of state required by various top-level compilation functions. /// /// This type exists to minimize the number of complex arguments passed across functions. @@ -56,6 +66,12 @@ struct Context { /// Stack of pending `EXIT FOR` jumps for each nested `FOR` loop. for_exit_stack: Vec>, + + /// Kind of the callable currently being compiled, if any. + current_callable: Option, + + /// List of pending `EXIT FUNCTION` or `EXIT SUB` jumps in the current callable. + callable_exit_jumps: Vec<(usize, LineCol)>, } /// Compiles an assignment statement `span` into the `codegen` block. @@ -890,6 +906,22 @@ fn compile_stmt( exit_stack.push((addr, span.pos)); } + Statement::ExitFunction(span) => { + if ctx.current_callable != Some(CallableKind::Function) { + return Err(Error::MisplacedExit(span.pos, "FUNCTION")); + } + let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos); + ctx.callable_exit_jumps.push((addr, span.pos)); + } + + Statement::ExitSub(span) => { + if ctx.current_callable != Some(CallableKind::Sub) { + return Err(Error::MisplacedExit(span.pos, "SUB")); + } + let addr = ctx.codegen.emit(bytecode::make_nop(), span.pos); + ctx.callable_exit_jumps.push((addr, span.pos)); + } + Statement::For(span) => { compile_for(ctx, symtable, span)?; } @@ -958,6 +990,12 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R let key_pos = callable.name_pos; let key = SymbolKey::from(callable.name.name); + ctx.current_callable = Some(if callable.name.ref_type.is_some() { + CallableKind::Function + } else { + CallableKind::Sub + }); + debug_assert!(ctx.callable_exit_jumps.is_empty()); let mut symtable = symtable.enter_scope(); @@ -998,7 +1036,14 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R return Err(Error::CannotNestUserCallables(span.name_pos)); } + let return_addr = ctx.codegen.next_pc(); ctx.codegen.emit(bytecode::make_return(), callable.end_pos); + for (addr, pos) in ctx.callable_exit_jumps.drain(..) { + let target = + u16::try_from(return_addr).map_err(|_| Error::TargetTooFar(pos, return_addr))?; + ctx.codegen.patch(addr, bytecode::make_jump(target)); + } + ctx.current_callable = None; ctx.codegen.define_user_callable(key, start_pc); } diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 6cd4d366..982cf7bd 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -820,3 +820,95 @@ END FUNCTION ```plain 2:10: Cannot redefine g% ``` + +# Test: Early function exit + +## Source + +```basic +FUNCTION maybe_exit(i%) + maybe_exit = 1 + IF i > 2 THEN EXIT FUNCTION + maybe_exit = 2 +END FUNCTION + +FOR i = 0 TO 5 + OUT maybe_exit(i) +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 0 # 7:9 +0002: MOVE R65, R64 # 7:5 +0003: LOADI R66, 5 # 7:14 +0004: CMPLEI R65, R65, R66 # 7:11 +0005: JMPF R65, 14 # 7:5 +0006: MOVE R67, R64 # 8:20 +0007: CALL R66, 16 # 8:9, MAYBE_EXIT +0008: LOADI R65, 258 # 8:9 +0009: UPCALL 0, R65 # 8:5, OUT +0010: MOVE R64, R64 # 7:5 +0011: LOADI R65, 1 # 7:15 +0012: ADDI R64, R64, R65 # 7:11 +0013: JUMP 2 # 7:5 +0014: LOADI R65, 0 # 0:0 +0015: END R65 # 0:0 + +-- MAYBE_EXIT +0016: LOADI R64, 0 # 1:10 +0017: ENTER 4 # 0:0 +0018: LOADI R64, 1 # 2:18 +0019: MOVE R66, R65 # 3:8 +0020: LOADI R67, 2 # 3:12 +0021: CMPGTI R66, R66, R67 # 3:10 +0022: JMPF R66, 24 # 3:8 +0023: JUMP 25 # 3:19 +0024: LOADI R64, 2 # 4:18 +0025: RETURN # 5:1 +``` + +## Output + +```plain +0=2% +0=2% +0=2% +0=1% +0=1% +0=1% +``` + +# Test: EXIT FUNCTION outside FUNCTION + +## Source + +```basic +FUNCTION a +END FUNCTION +EXIT FUNCTION +``` + +## Compilation errors + +```plain +3:1: EXIT FUNCTION outside of FUNCTION +``` + +# Test: EXIT SUB in FUNCTION + +## Source + +```basic +FUNCTION a + EXIT SUB +END FUNCTION +``` + +## Compilation errors + +```plain +2:5: EXIT SUB outside of SUB +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 595ccb73..a1466682 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -267,3 +267,99 @@ END SUB ```plain 2:5: Cannot redefine g ``` + +# Test: Early sub exit + +## Source + +```basic +SUB maybe_exit(i%) + OUT 1 + IF i > 2 THEN EXIT SUB + OUT 2 +END SUB + +FOR i = 0 TO 5 + maybe_exit(i) +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 7:9 +0002: MOVE R65, R64 # 7:5 +0003: LOADI R66, 5 # 7:14 +0004: CMPLEI R65, R65, R66 # 7:11 +0005: JMPF R65, 12 # 7:5 +0006: MOVE R65, R64 # 8:16 +0007: CALL R65, 14 # 8:5, MAYBE_EXIT +0008: MOVE R64, R64 # 7:5 +0009: LOADI R65, 1 # 7:15 +0010: ADDI R64, R64, R65 # 7:11 +0011: JUMP 2 # 7:5 +0012: LOADI R65, 0 # 0:0 +0013: END R65 # 0:0 + +-- MAYBE_EXIT +0014: ENTER 3 # 0:0 +0015: LOADI R66, 1 # 2:9 +0016: LOADI R65, 258 # 2:9 +0017: UPCALL 0, R65 # 2:5, OUT +0018: MOVE R65, R64 # 3:8 +0019: LOADI R66, 2 # 3:12 +0020: CMPGTI R65, R65, R66 # 3:10 +0021: JMPF R65, 23 # 3:8 +0022: JUMP 26 # 3:19 +0023: LOADI R66, 2 # 4:9 +0024: LOADI R65, 258 # 4:9 +0025: UPCALL 0, R65 # 4:5, OUT +0026: RETURN # 5:1 +``` + +## Output + +```plain +0=1% +0=2% +0=1% +0=2% +0=1% +0=2% +0=1% +0=1% +0=1% +``` + +# Test: EXIT SUB outside SUB + +## Source + +```basic +SUB a +END SUB +EXIT SUB +``` + +## Compilation errors + +```plain +3:1: EXIT SUB outside of SUB +``` + +# Test: EXIT FUNCTION in SUB + +## Source + +```basic +SUB a + EXIT FUNCTION +END SUB +``` + +## Compilation errors + +```plain +2:5: EXIT FUNCTION outside of FUNCTION +``` From bcaadd8079d9e463ca6362ed70748a189faf5a4e Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 07:15:00 -0800 Subject: [PATCH 27/53] core2: Implement support for ON ERROR Implement SETEH and runtime error handlers. Track statement starts in debug metadata for RESUME NEXT. Add integration tests and test callables for error paths. --- core2/src/ast.rs | 6 +- core2/src/bytecode.rs | 49 +++ core2/src/callable.rs | 8 + core2/src/compiler/codegen.rs | 44 +- core2/src/compiler/mod.rs | 4 + core2/src/compiler/top.rs | 38 +- core2/src/image.rs | 5 + core2/src/parser.rs | 51 +-- core2/src/vm/context.rs | 51 ++- core2/src/vm/mod.rs | 104 ++++- core2/tests/integration_test.rs | 1 + core2/tests/test_on_error.md | 407 ++++++++++++++++++ .../testutils/callables/last_error_fn.rs | 49 +++ core2/tests/testutils/callables/mod.rs | 12 + core2/tests/testutils/callables/raise_cmd.rs | 61 +++ core2/tests/testutils/callables/raisef_fn.rs | 62 +++ 16 files changed, 892 insertions(+), 60 deletions(-) create mode 100644 core2/tests/test_on_error.md create mode 100644 core2/tests/testutils/callables/last_error_fn.rs create mode 100644 core2/tests/testutils/callables/raise_cmd.rs create mode 100644 core2/tests/testutils/callables/raisef_fn.rs diff --git a/core2/src/ast.rs b/core2/src/ast.rs index 6c6154eb..c8a3a66e 100644 --- a/core2/src/ast.rs +++ b/core2/src/ast.rs @@ -622,13 +622,13 @@ pub struct LabelSpan { #[derive(Debug, Eq, PartialEq)] pub enum OnErrorSpan { /// Components of an `ON ERROR GOTO @label` statement. - Goto(GotoSpan), + Goto(GotoSpan, LineCol), /// Components of an `ON ERROR GOTO 0` statement. - Reset, + Reset(LineCol), /// Components of an `ON ERROR RESUME NEXT` statement. - ResumeNext, + ResumeNext(LineCol), } /// Components of a `RETURN` statement. diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 4a0f198a..dcfe0c71 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -269,6 +269,44 @@ impl RawValue for ExprType { } } +/// Modes for the error handler configured by `ON ERROR`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ErrorHandlerMode { + /// Disable error handling. + None, + + /// Resume execution at the next statement after an error. + ResumeNext, + + /// Jump to a specific handler address after an error. + Jump, +} + +impl RawValue for ErrorHandlerMode { + fn from_u32(v: u32) -> Self { + #[allow(unsafe_code)] + unsafe { + let v = unchecked_u32_as_u8(v); + assert!(v <= ErrorHandlerMode::Jump as u8); + std::mem::transmute(v) + } + } + + fn to_u32(self) -> u32 { + u32::from(self as u8) + } +} + +impl fmt::Display for ErrorHandlerMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::None => write!(f, "NONE"), + Self::ResumeNext => write!(f, "RESUME_NEXT"), + Self::Jump => write!(f, "JUMP"), + } + } +} + /// Generates functions to construct an instruction's bytecode representation for the compiler's /// benefit, to parse it for the VM's benefit, and to format it for debugging purposes. macro_rules! instr { @@ -531,6 +569,9 @@ pub(crate) enum Opcode { /// Returns from a previous `Call`. Return, + /// Sets the error handler mode and target address. + SetErrorHandler, + /// Shifts an integer left by a number of bits without rotation, storing the result into /// a third register. ShiftLeft, @@ -1023,6 +1064,14 @@ instr!( make_return, parse_return, format_return, ); +#[rustfmt::skip] +instr!( + Opcode::SetErrorHandler, "SETEH", + make_set_error_handler, parse_set_error_handler, format_set_error_handler, + ErrorHandlerMode, 0x000000ff, 16, // Error handler mode. + u16, 0x0000ffff, 0, // Target address for Jump mode. +); + #[rustfmt::skip] instr!( Opcode::ShiftLeft, "SHL", diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 0cee430b..3af8cb98 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -729,6 +729,9 @@ pub struct Scope<'a> { /// position of the expression that was compiled into register slot `N`. May be shorter /// than the actual argument register count if debug information is unavailable. pub(crate) arg_linecols: &'a [LineCol], + + /// Last error raised in the VM, if any. + pub(crate) last_error: &'a Option, } impl<'a> Scope<'a> { @@ -780,6 +783,11 @@ impl<'a> Scope<'a> { ptr.resolve_string(self.constants, self.heap) } + /// Returns the last error stored in the VM, if any. + pub fn last_error(&self) -> Option<&str> { + self.last_error.as_deref() + } + /// Sets the return value of the function to `b`. pub fn return_boolean(self, b: bool) { self.regs[self.fp] = if b { 1 } else { 0 }; diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 1562c341..6666d96e 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -16,7 +16,7 @@ //! Code generation for the EndBASIC compiler. use crate::ast::ExprType; -use crate::bytecode::{self, Register}; +use crate::bytecode::{self, ErrorHandlerMode, Register}; use crate::compiler::ids::HashMapWithIds; use crate::compiler::{Error, Result, SymbolKey}; use crate::image::{DebugInfo, GlobalVarInfo, Image, InstrMetadata}; @@ -38,10 +38,13 @@ pub(super) enum Fixup { Enter(u8), /// Fixup to resolve a label target address into a `GOSUB` instruction. - Gosub(SymbolKey), + Gosub(String), /// Fixup to resolve a label target address into a `GOTO` (jump) instruction. - Goto(SymbolKey), + Goto(String), + + /// Fixup to resolve a label target address into a `SET_ERROR_HANDLER` instruction. + OnErrorGoto(String), } /// The code generator. @@ -78,10 +81,19 @@ impl Codegen { /// Appends a new instruction `op` generated at `pos` to the code and returns its address. pub(super) fn emit(&mut self, op: u32, pos: LineCol) -> Address { self.code.push(op); - self.instrs.push(InstrMetadata { linecol: pos, arg_linecols: vec![] }); + self.instrs.push(InstrMetadata { + linecol: pos, + is_stmt_start: false, + arg_linecols: vec![], + }); self.code.len() - 1 } + /// Marks the instruction at `addr` as the start of a statement. + pub(super) fn mark_statement_start(&mut self, addr: Address) { + self.instrs[addr].is_stmt_start = true; + } + /// Attaches argument source locations to the instruction at `addr`. /// /// `arg_linecols` has one entry per register slot in the argument area, in the same order @@ -188,14 +200,30 @@ impl Codegen { bytecode::make_call(reg, Self::make_target(*target, pos)?) } Fixup::Enter(nargs) => bytecode::make_enter(nargs), - Fixup::Gosub(key) => { - let target = self.labels.get(&key).expect("Must be present"); + Fixup::Gosub(label) => { + let key = SymbolKey::from(&label); + let Some(target) = self.labels.get(&key) else { + return Err(Error::UnknownLabel(pos, label)); + }; bytecode::make_gosub(Self::make_target(*target, pos)?) } - Fixup::Goto(key) => { - let target = self.labels.get(&key).expect("Must be present"); + Fixup::Goto(label) => { + let key = SymbolKey::from(&label); + let Some(target) = self.labels.get(&key) else { + return Err(Error::UnknownLabel(pos, label)); + }; bytecode::make_jump(Self::make_target(*target, pos)?) } + Fixup::OnErrorGoto(label) => { + let key = SymbolKey::from(&label); + let Some(target) = self.labels.get(&key) else { + return Err(Error::UnknownLabel(pos, label)); + }; + bytecode::make_set_error_handler( + ErrorHandlerMode::Jump, + Self::make_target(*target, pos)?, + ) + } }; self.code[addr] = instr; } diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 42432db0..81bc896d 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -119,6 +119,10 @@ pub enum Error { #[error("{0}: Undefined {2} symbol {1}")] UndefinedSymbol(LineCol, VarRef, RegisterScope), + /// Reference to an unknown label. + #[error("{0}: Unknown label {1}")] + UnknownLabel(LineCol, String), + /// Wrong number of subscripts for an array access. #[error("{0}: Array requires {1} subscripts but got {2}")] WrongNumberOfSubscripts(LineCol, usize, usize), diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 22090252..c05fca6b 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -17,9 +17,9 @@ use crate::ast::{ ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, EndSpan, Expr, - ExprType, ForSpan, IfSpan, SelectSpan, Statement, VarRef, WhileSpan, + ExprType, ForSpan, IfSpan, OnErrorSpan, SelectSpan, Statement, VarRef, WhileSpan, }; -use crate::bytecode::{self, PackedArrayType, Register, RegisterScope}; +use crate::bytecode::{self, ErrorHandlerMode, PackedArrayType, Register, RegisterScope}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; @@ -364,8 +364,8 @@ fn compile_select( let dispatch_end_jump_pc = ctx.codegen.emit(bytecode::make_nop(), end_pos); if let Some((jump_pc, pos)) = pending_next_case_jump { - let target = - u16::try_from(dispatch_end_jump_pc).map_err(|_| Error::TargetTooFar(pos, dispatch_end_jump_pc))?; + let target = u16::try_from(dispatch_end_jump_pc) + .map_err(|_| Error::TargetTooFar(pos, dispatch_end_jump_pc))?; ctx.codegen.patch(jump_pc, bytecode::make_jump(target)); } @@ -678,6 +678,8 @@ fn compile_stmt( symtable: &mut LocalSymtable<'_, '_, '_, '_>, stmt: Statement, ) -> Result<()> { + let start_pc = ctx.codegen.next_pc(); + let mut mark_start = true; match stmt { Statement::ArrayAssignment(span) => { let key_pos = span.vref_pos; @@ -758,6 +760,7 @@ fn compile_stmt( } Statement::Callable(span) => { + mark_start = false; let mut syntax = vec![]; for (i, param) in span.params.iter().enumerate() { let sep = if i == span.params.len() - 1 { @@ -928,15 +931,16 @@ fn compile_stmt( Statement::Gosub(span) => { let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); - ctx.codegen.add_fixup(addr, Fixup::Gosub(SymbolKey::from(span.target))); + ctx.codegen.add_fixup(addr, Fixup::Gosub(span.target)); } Statement::Goto(span) => { let addr = ctx.codegen.emit(bytecode::make_nop(), span.target_pos); - ctx.codegen.add_fixup(addr, Fixup::Goto(SymbolKey::from(span.target))); + ctx.codegen.add_fixup(addr, Fixup::Goto(span.target)); } Statement::Label(span) => { + mark_start = false; ctx.codegen.define_label(SymbolKey::from(span.name), ctx.codegen.next_pc()); } @@ -948,6 +952,25 @@ fn compile_stmt( compile_if(ctx, symtable, span)?; } + Statement::OnError(span) => { + match span { + OnErrorSpan::Goto(span, pos) => { + let addr = ctx.codegen.emit(bytecode::make_nop(), pos); + ctx.codegen.add_fixup(addr, Fixup::OnErrorGoto(span.target)); + } + OnErrorSpan::Reset(pos) => { + ctx.codegen + .emit(bytecode::make_set_error_handler(ErrorHandlerMode::None, 0), pos); + } + OnErrorSpan::ResumeNext(pos) => { + ctx.codegen.emit( + bytecode::make_set_error_handler(ErrorHandlerMode::ResumeNext, 0), + pos, + ); + } + }; + } + Statement::Select(span) => { compile_select(ctx, symtable, span)?; } @@ -958,6 +981,9 @@ fn compile_stmt( _ => todo!(), } + if mark_start && start_pc != ctx.codegen.next_pc() { + ctx.codegen.mark_statement_start(start_pc); + } Ok(()) } diff --git a/core2/src/image.rs b/core2/src/image.rs index a79784d8..e3436711 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -82,6 +82,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::PowerDouble => bytecode::format_power_double(instr), Opcode::PowerInteger => bytecode::format_power_integer(instr), Opcode::Return => bytecode::format_return(instr), + Opcode::SetErrorHandler => bytecode::format_set_error_handler(instr), Opcode::ShiftLeft => bytecode::format_shift_left(instr), Opcode::ShiftRight => bytecode::format_shift_right(instr), Opcode::StoreArray => bytecode::format_store_array(instr), @@ -108,6 +109,9 @@ pub(crate) struct InstrMetadata { /// Source location that generated this instruction. pub(crate) linecol: LineCol, + /// True if this instruction is the start of a statement. + pub(crate) is_stmt_start: bool, + /// Source locations of the call arguments, if this is a UPCALL instruction. /// /// Each entry corresponds to one register slot in the argument area, in the same order @@ -166,6 +170,7 @@ impl Default for Image { DebugInfo { instrs: vec![InstrMetadata { linecol: LineCol { line: 0, col: 0 }, + is_stmt_start: true, arg_linecols: vec![], }], callables: HashMap::default(), diff --git a/core2/src/parser.rs b/core2/src/parser.rs index 3b299e5f..9ff623be 100644 --- a/core2/src/parser.rs +++ b/core2/src/parser.rs @@ -1530,7 +1530,7 @@ impl<'a> Parser<'a> { } /// Parses an `ON ERROR` statement. Only `ON` has been consumed so far. - fn parse_on(&mut self) -> Result { + fn parse_on(&mut self, pos: LineCol) -> Result { self.expect_and_consume(Token::Error, "Expected ERROR after ON")?; let token_span = self.lexer.read()?; @@ -1538,15 +1538,15 @@ impl<'a> Parser<'a> { Token::Goto => { let token_span = self.lexer.read()?; match token_span.token { - Token::Integer(0) => Ok(Statement::OnError(OnErrorSpan::Reset)), - Token::Integer(i) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { - target: format!("{}", i), - target_pos: token_span.pos, - }))), - Token::Label(target) => Ok(Statement::OnError(OnErrorSpan::Goto(GotoSpan { - target, - target_pos: token_span.pos, - }))), + Token::Integer(0) => Ok(Statement::OnError(OnErrorSpan::Reset(pos))), + Token::Integer(i) => Ok(Statement::OnError(OnErrorSpan::Goto( + GotoSpan { target: format!("{}", i), target_pos: token_span.pos }, + pos, + ))), + Token::Label(target) => Ok(Statement::OnError(OnErrorSpan::Goto( + GotoSpan { target, target_pos: token_span.pos }, + pos, + ))), _ => Err(Error::Bad( token_span.pos, "Expected label name or 0 after ON ERROR GOTO".to_owned(), @@ -1555,7 +1555,7 @@ impl<'a> Parser<'a> { } Token::Resume => { self.expect_and_consume(Token::Next, "Expected NEXT after ON ERROR RESUME")?; - Ok(Statement::OnError(OnErrorSpan::ResumeNext)) + Ok(Statement::OnError(OnErrorSpan::ResumeNext(pos))) } _ => { Err(Error::Bad(token_span.pos, "Expected GOTO or RESUME after ON ERROR".to_owned())) @@ -1829,7 +1829,7 @@ impl<'a> Parser<'a> { Token::Exit => Ok(Some(self.parse_exit(token_span.pos)?)), Token::Gosub => Ok(Some(self.parse_gosub()?)), Token::Goto => Ok(Some(self.parse_goto()?)), - Token::On => Ok(Some(self.parse_on()?)), + Token::On => Ok(Some(self.parse_on(token_span.pos)?)), Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), Token::Symbol(vref) => { let peeked = self.lexer.peek()?; @@ -1914,7 +1914,7 @@ impl<'a> Parser<'a> { // ending given that the next statement may start after the label we found. return Ok(Some(Statement::Label(LabelSpan { name, name_pos: token_span.pos }))); } - Token::On => Ok(Some(self.parse_on()?)), + Token::On => Ok(Some(self.parse_on(token_span.pos)?)), Token::Return => Ok(Some(Statement::Return(ReturnSpan { pos: token_span.pos }))), Token::Select => { let result = self.parse_select(token_span.pos); @@ -3929,7 +3929,7 @@ mod tests { fn test_if_uniline_allowed_on_error() { do_if_uniline_allowed_test( "ON ERROR RESUME NEXT", - Statement::OnError(OnErrorSpan::ResumeNext), + Statement::OnError(OnErrorSpan::ResumeNext(lc(1, 11))), ); do_error_test("IF 1 THEN ON", "1:13: Expected ERROR after ON"); @@ -4376,25 +4376,28 @@ mod tests { #[test] fn test_parse_on_error_ok() { - do_ok_test("ON ERROR GOTO 0", &[Statement::OnError(OnErrorSpan::Reset)]); + do_ok_test("ON ERROR GOTO 0", &[Statement::OnError(OnErrorSpan::Reset(lc(1, 1)))]); do_ok_test( "ON ERROR GOTO 10", - &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { - target: "10".to_owned(), - target_pos: lc(1, 15), - }))], + &[Statement::OnError(OnErrorSpan::Goto( + GotoSpan { target: "10".to_owned(), target_pos: lc(1, 15) }, + lc(1, 1), + ))], ); do_ok_test( "ON ERROR GOTO @foo", - &[Statement::OnError(OnErrorSpan::Goto(GotoSpan { - target: "foo".to_owned(), - target_pos: lc(1, 15), - }))], + &[Statement::OnError(OnErrorSpan::Goto( + GotoSpan { target: "foo".to_owned(), target_pos: lc(1, 15) }, + lc(1, 1), + ))], ); - do_ok_test("ON ERROR RESUME NEXT", &[Statement::OnError(OnErrorSpan::ResumeNext)]); + do_ok_test( + "ON ERROR RESUME NEXT", + &[Statement::OnError(OnErrorSpan::ResumeNext(lc(1, 1)))], + ); } #[test] diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index a6e64ebc..6c923b4d 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -17,7 +17,7 @@ use crate::ExprType; use crate::Scope; -use crate::bytecode::{self, Opcode, Register, TaggedRegisterRef, opcode_of}; +use crate::bytecode::{self, ErrorHandlerMode, Opcode, Register, TaggedRegisterRef, opcode_of}; use crate::image::Image; use crate::mem::{ArrayData, ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u8; @@ -40,6 +40,19 @@ pub(super) enum InternalStopReason { Upcall(u16, Register, Address), } +/// Error handler configuration set by `ON ERROR`. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(super) enum ErrorHandler { + /// Errors are not handled. + None, + + /// Errors resume execution at the next statement. + ResumeNext, + + /// Errors jump to a handler address. + Jump(Address), +} + /// Represents a call frame in the stack. struct Frame { /// Program counter of the instruction that caused the call. @@ -142,6 +155,9 @@ pub(super) struct Context { /// Stop signal. If set, indicates why the execution stopped during instruction processing. stop: Option, + /// Current error handler configuration. + err_handler: ErrorHandler, + /// Register values. The first N registers hold global variables. After those, we find /// the registers for all local variables and for all scopes. regs: Vec, @@ -156,6 +172,7 @@ impl Default for Context { pc: 0, fp: usize::from(Register::MAX_GLOBAL), stop: None, + err_handler: ErrorHandler::None, regs: vec![0; usize::from(Register::MAX_GLOBAL)], call_stack: vec![], } @@ -187,6 +204,16 @@ impl Context { self.regs[index] = value; } + /// Sets the program counter to `pc`. + pub(super) fn set_pc(&mut self, pc: Address) { + self.pc = pc; + } + + /// Returns the current error handler configuration. + pub(super) fn error_handler(&self) -> ErrorHandler { + self.err_handler + } + /// Dereferences a pointer register as a string. fn deref_string<'b>( &self, @@ -254,12 +281,20 @@ impl Context { constants: &'a [ConstantDatum], heap: &'a mut Vec, arg_linecols: &'a [LineCol], + last_error: &'a Option, ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); let index = usize::from(index); - Scope { regs: &mut self.regs, constants, heap, fp: self.fp + index, arg_linecols } + Scope { + regs: &mut self.regs, + constants, + heap, + fp: self.fp + index, + arg_linecols, + last_error, + } } /// Starts or resumes execution of `image`. @@ -329,6 +364,7 @@ impl Context { Opcode::PowerDouble => self.do_power_double(instr), Opcode::PowerInteger => self.do_power_integer(instr), Opcode::Return => self.do_return(instr), + Opcode::SetErrorHandler => self.do_set_error_handler(instr), Opcode::ShiftLeft => self.do_shift_left(instr), Opcode::ShiftRight => self.do_shift_right(instr), Opcode::StoreArray => self.do_store_array(instr, heap), @@ -933,6 +969,17 @@ impl Context { } } + /// Implements the `SetErrorHandler` opcode. + pub(super) fn do_set_error_handler(&mut self, instr: u32) { + let (mode, target) = bytecode::parse_set_error_handler(instr); + self.err_handler = match mode { + ErrorHandlerMode::None => ErrorHandler::None, + ErrorHandlerMode::ResumeNext => ErrorHandler::ResumeNext, + ErrorHandlerMode::Jump => ErrorHandler::Jump(usize::from(target)), + }; + self.pc += 1; + } + /// Implements the `ShiftLeft` opcode. pub(super) fn do_shift_left(&mut self, instr: u32) { self.do_binary_integer_op(instr, bytecode::parse_shift_left, checked_shl_integer); diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 7b9078c9..d3e0b344 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -27,7 +27,7 @@ use std::collections::HashMap; use std::rc::Rc; mod context; -use context::{Context, InternalStopReason}; +use context::{Context, ErrorHandler, InternalStopReason}; /// Error returned when a global variable access encounters a type or shape mismatch. /// @@ -63,7 +63,16 @@ impl<'a> UpcallHandler<'a> { .take() .expect("This is only reachable when the VM has a pending upcall"); let upcall = vm.upcalls[usize::from(index)].clone(); - upcall.exec(vm.upcall_scope(first_reg, upcall_pc)).await + match upcall.exec(vm.upcall_scope(first_reg, upcall_pc)).await { + Ok(()) => Ok(()), + Err(e) => { + let pos_override = vm.image.as_ref().and_then(|image| { + image.debug_info.instrs[upcall_pc].arg_linecols.first().copied() + }); + vm.handle_exception(upcall_pc, e.to_string(), pos_override); + Ok(()) + } + } } } @@ -96,6 +105,12 @@ pub struct Vm { /// Processor context for execution. context: Context, + /// Last error seen by the VM, if any. + last_error: Option, + + /// Pending exception to report to the caller. + pending_exception: Option<(LineCol, String)>, + /// Details about the pending upcall that has to be handled by the caller. /// /// The tuple contains the upcall index, the first argument register, and the PC of the @@ -112,6 +127,8 @@ impl Vm { upcalls: vec![], heap: vec![], context: Context::default(), + last_error: None, + pending_exception: None, pending_upcall: None, } } @@ -131,6 +148,10 @@ impl Vm { self.image = Some(image); self.heap.clear(); + self.context = Context::default(); + self.last_error = None; + self.pending_exception = None; + self.pending_upcall = None; } /// Constructs a `Scope` for an upcall with arguments starting at `reg`. @@ -150,7 +171,47 @@ impl Vm { ), None => (&[][..], &[][..]), }; - self.context.upcall_scope(reg, constants, &mut self.heap, arg_linecols) + self.context.upcall_scope(reg, constants, &mut self.heap, arg_linecols, &self.last_error) + } + + /// Handles an exception raised at `pc` with `message`. Returns true if the error was handled. + fn handle_exception( + &mut self, + pc: usize, + message: String, + pos_override: Option, + ) -> bool { + let Some(image) = self.image.as_ref() else { + let pos = pos_override.unwrap_or(LineCol { line: 0, col: 0 }); + self.pending_exception = Some((pos, message)); + return false; + }; + + let pos = pos_override.unwrap_or(image.debug_info.instrs[pc].linecol); + self.last_error = Some(format!("{}: {}", pos, message)); + self.pending_exception = None; + + match self.context.error_handler() { + ErrorHandler::None => { + self.pending_exception = Some((pos, message)); + false + } + ErrorHandler::Jump(addr) => { + self.context.set_pc(addr); + true + } + ErrorHandler::ResumeNext => { + let mut next_pc = image.code.len(); + for (idx, meta) in image.debug_info.instrs.iter().enumerate().skip(pc + 1) { + if meta.is_stmt_start { + next_pc = idx; + break; + } + } + self.context.set_pc(next_pc); + true + } + } } /// Returns the value of the global scalar variable `name` as a `ConstantDatum`. @@ -228,23 +289,32 @@ impl Vm { /// Returns a `StopReason` indicating why execution stopped, which may be due to program /// termination, an exception, or a pending upcall that requires caller handling. pub fn exec(&mut self) -> StopReason<'_> { - let Some(image) = self.image.as_ref() else { - return StopReason::End(0); - }; + loop { + if let Some((pos, message)) = self.pending_exception.take() { + return StopReason::Exception(pos, message); + } - if self.pending_upcall.is_some() { - return StopReason::Upcall(UpcallHandler(self)); - }; + let Some(image) = self.image.as_ref() else { + return StopReason::End(0); + }; - match self.context.exec(image, &mut self.heap) { - InternalStopReason::End(code) => StopReason::End(code), - InternalStopReason::Exception(pc, e) => { - let pos = image.debug_info.instrs[pc].linecol; - StopReason::Exception(pos, e) + if self.pending_upcall.is_some() { + return StopReason::Upcall(UpcallHandler(self)); } - InternalStopReason::Upcall(index, first_reg, upcall_pc) => { - self.pending_upcall = Some((index, first_reg, upcall_pc)); - StopReason::Upcall(UpcallHandler(self)) + + match self.context.exec(image, &mut self.heap) { + InternalStopReason::End(code) => return StopReason::End(code), + InternalStopReason::Exception(pc, e) => { + if !self.handle_exception(pc, e, None) + && let Some((pos, message)) = self.pending_exception.take() + { + return StopReason::Exception(pos, message); + } + } + InternalStopReason::Upcall(index, first_reg, upcall_pc) => { + self.pending_upcall = Some((index, first_reg, upcall_pc)); + return StopReason::Upcall(UpcallHandler(self)); + } } } } diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index d6740318..5a70ec70 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -57,6 +57,7 @@ one_test!(test_gosub); one_test!(test_goto); one_test!(test_if); one_test!(test_locals); +one_test!(test_on_error); one_test!(test_out_of_registers); one_test!(test_relational_eq); one_test!(test_relational_ge); diff --git a/core2/tests/test_on_error.md b/core2/tests/test_on_error.md new file mode 100644 index 00000000..1c9ff548 --- /dev/null +++ b/core2/tests/test_on_error.md @@ -0,0 +1,407 @@ +# Test: ON ERROR GOTO line + +## Source + +```basic +ON ERROR GOTO 100 +OUT 1 +OUT RAISEF("internal") +OUT 2 +100 OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH JUMP, 12 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R66, 0 # 3:12 +0006: UPCALL 1, R65 # 3:5, RAISEF +0007: LOADI R64, 256 # 3:5 +0008: UPCALL 0, R64 # 3:1, OUT +0009: LOADI R65, 2 # 4:5 +0010: LOADI R64, 258 # 4:5 +0011: UPCALL 0, R64 # 4:1, OUT +0012: UPCALL 2, R65 # 5:9, LAST_ERROR +0013: LOADI R64, 259 # 5:9 +0014: UPCALL 0, R64 # 5:5, OUT +0015: LOADI R64, 0 # 0:0 +0016: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=3:12: Some internal error$ +``` + +# Test: ON ERROR GOTO label + +## Source + +```basic +ON ERROR GOTO @foo +OUT 1 +OUT RAISEF("internal") +OUT 2 +@foo +OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH JUMP, 12 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R66, 0 # 3:12 +0006: UPCALL 1, R65 # 3:5, RAISEF +0007: LOADI R64, 256 # 3:5 +0008: UPCALL 0, R64 # 3:1, OUT +0009: LOADI R65, 2 # 4:5 +0010: LOADI R64, 258 # 4:5 +0011: UPCALL 0, R64 # 4:1, OUT +0012: UPCALL 2, R65 # 6:5, LAST_ERROR +0013: LOADI R64, 259 # 6:5 +0014: UPCALL 0, R64 # 6:1, OUT +0015: LOADI R64, 0 # 0:0 +0016: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=3:12: Some internal error$ +``` + +# Test: ON ERROR reset + +## Source + +```basic +ON ERROR GOTO @foo +OUT 1 +OUT RAISEF("internal") +@foo +ON ERROR GOTO 0 +OUT 2 +OUT RAISEF("internal") +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH JUMP, 9 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R66, 0 # 3:12 +0006: UPCALL 1, R65 # 3:5, RAISEF +0007: LOADI R64, 256 # 3:5 +0008: UPCALL 0, R64 # 3:1, OUT +0009: SETEH NONE, 0 # 5:1 +0010: LOADI R65, 2 # 6:5 +0011: LOADI R64, 258 # 6:5 +0012: UPCALL 0, R64 # 6:1, OUT +0013: LOADI R66, 0 # 7:12 +0014: UPCALL 1, R65 # 7:5, RAISEF +0015: LOADI R64, 256 # 7:5 +0016: UPCALL 0, R64 # 7:1, OUT +0017: LOADI R64, 0 # 0:0 +0018: END R64 # 0:0 +``` + +## Runtime errors + +```plain +7:12: Some internal error +``` + +## Output + +```plain +0=1% +0=2% +``` + +# Test: ON ERROR RESUME NEXT function failure + +## Source + +```basic +ON ERROR RESUME NEXT +OUT 1 +OUT RAISEF("internal") +OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R66, 0 # 3:12 +0006: UPCALL 1, R65 # 3:5, RAISEF +0007: LOADI R64, 256 # 3:5 +0008: UPCALL 0, R64 # 3:1, OUT +0009: UPCALL 2, R65 # 4:5, LAST_ERROR +0010: LOADI R64, 259 # 4:5 +0011: UPCALL 0, R64 # 4:1, OUT +0012: LOADI R64, 0 # 0:0 +0013: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=3:12: Some internal error$ +``` + +# Test: ON ERROR RESUME NEXT command failure + +## Source + +```basic +ON ERROR RESUME NEXT +OUT 1 +RAISE "internal" +OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R64, 0 # 3:7 +0006: UPCALL 1, R64 # 3:1, RAISE +0007: UPCALL 2, R65 # 4:5, LAST_ERROR +0008: LOADI R64, 259 # 4:5 +0009: UPCALL 0, R64 # 4:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=3:7: Some internal error$ +``` + +# Test: ON ERROR RESUME NEXT function failure in statement + +## Source + +```basic +ON ERROR RESUME NEXT +OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R66, 0 # 2:19 +0006: UPCALL 1, R65 # 2:12, RAISEF +0007: LOADI R64, 256 # 2:12 +0008: UPCALL 0, R64 # 2:8, OUT +0009: UPCALL 2, R65 # 2:36, LAST_ERROR +0010: LOADI R64, 259 # 2:36 +0011: UPCALL 0, R64 # 2:32, OUT +0012: LOADI R64, 0 # 0:0 +0013: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=2:19: Some internal error$ +``` + +# Test: ON ERROR RESUME NEXT command failure in statement + +## Source + +```basic +ON ERROR RESUME NEXT +OUT 1: RAISE "internal": OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R65, 1 # 2:5 +0003: LOADI R64, 258 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R64, 0 # 2:14 +0006: UPCALL 1, R64 # 2:8, RAISE +0007: UPCALL 2, R65 # 2:30, LAST_ERROR +0008: LOADI R64, 259 # 2:30 +0009: UPCALL 0, R64 # 2:26, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +0=2:14: Some internal error$ +``` + +# Test: ON ERROR RESUME NEXT argument error + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R66, 0 # 1:34 +0003: UPCALL 0, R65 # 1:27, RAISEF +0004: LOADI R64, 256 # 1:27 +0005: UPCALL 1, R64 # 1:23, OUT +0006: UPCALL 2, R65 # 1:51, LAST_ERROR +0007: LOADI R64, 259 # 1:51 +0008: UPCALL 1, R64 # 1:47, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=1:34: Bad argument$ +``` + +# Test: ON ERROR RESUME NEXT eval error + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R66, 0 # 1:34 +0003: UPCALL 0, R65 # 1:27, RAISEF +0004: LOADI R64, 256 # 1:27 +0005: UPCALL 1, R64 # 1:23, OUT +0006: UPCALL 2, R65 # 1:47, LAST_ERROR +0007: LOADI R64, 259 # 1:47 +0008: UPCALL 1, R64 # 1:43, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=1:34: Some eval error$ +``` + +# Test: ON ERROR RESUME NEXT internal error + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R66, 0 # 1:34 +0003: UPCALL 0, R65 # 1:27, RAISEF +0004: LOADI R64, 256 # 1:27 +0005: UPCALL 1, R64 # 1:23, OUT +0006: UPCALL 2, R65 # 1:51, LAST_ERROR +0007: LOADI R64, 259 # 1:51 +0008: UPCALL 1, R64 # 1:47, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=1:34: Some internal error$ +``` + +# Test: ON ERROR RESUME NEXT I/O error + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: SETEH RESUME_NEXT, 0 # 1:1 +0002: LOADI R66, 0 # 1:34 +0003: UPCALL 0, R65 # 1:27, RAISEF +0004: LOADI R64, 256 # 1:27 +0005: UPCALL 1, R64 # 1:23, OUT +0006: UPCALL 2, R65 # 1:45, LAST_ERROR +0007: LOADI R64, 259 # 1:45 +0008: UPCALL 1, R64 # 1:41, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=1:34: Some I/O error$ +``` + +# Test: ON ERROR GOTO unknown label + +## Source + +```basic +ON ERROR GOTO @foo +``` + +## Compilation errors + +```plain +1:1: Unknown label foo +``` diff --git a/core2/tests/testutils/callables/last_error_fn.rs b/core2/tests/testutils/callables/last_error_fn.rs new file mode 100644 index 00000000..a7d66830 --- /dev/null +++ b/core2/tests/testutils/callables/last_error_fn.rs @@ -0,0 +1,49 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::rc::Rc; + +/// A function that returns the last error recorded by the VM. +pub(super) struct LastErrorFunction { + metadata: CallableMetadata, +} + +impl LastErrorFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("LAST_ERROR") + .with_return_type(ExprType::Text) + .with_syntax(&[(&[], None)]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for LastErrorFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let last_error = scope.last_error().map(str::to_owned).unwrap_or_default(); + scope.return_string(last_error); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index e49b9d97..0e9cb603 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -35,6 +35,9 @@ use increment_required_int_cmd::IncrementRequiredIntCommand; mod is_positive_fn; use is_positive_fn::IsPositiveFunction; +mod last_error_fn; +use last_error_fn::LastErrorFunction; + mod meaning_of_life_fn; use meaning_of_life_fn::MeaningOfLifeFunction; @@ -56,6 +59,12 @@ use out_positional_cmd::OutPositionalCommand; mod out_required_value_cmd; use out_required_value_cmd::OutRequiredValueCommand; +mod raise_cmd; +use raise_cmd::RaiseCommand; + +mod raisef_fn; +use raisef_fn::RaisefFunction; + mod sum_doubles_fn; use sum_doubles_fn::SumDoublesFunction; @@ -99,6 +108,7 @@ pub(super) fn register_all( DefineArgCommand::new() as Rc, IncrementRequiredIntCommand::new() as Rc, IsPositiveFunction::new() as Rc, + LastErrorFunction::new() as Rc, MeaningOfLifeFunction::new() as Rc, OutAnyValueCommand::new(console.clone()) as Rc, OutAnyValueOptionalCommand::new(console.clone()) as Rc, @@ -106,6 +116,8 @@ pub(super) fn register_all( OutOptionalCommand::new(console.clone()) as Rc, OutPositionalCommand::new(console.clone()) as Rc, OutRequiredValueCommand::new(console) as Rc, + RaiseCommand::new() as Rc, + RaisefFunction::new() as Rc, SumDoublesFunction::new() as Rc, SumIntegersFunction::new() as Rc, ]; diff --git a/core2/tests/testutils/callables/raise_cmd.rs b/core2/tests/testutils/callables/raise_cmd.rs new file mode 100644 index 00000000..54099331 --- /dev/null +++ b/core2/tests/testutils/callables/raise_cmd.rs @@ -0,0 +1,61 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A command that raises an error based on a string argument. +pub(super) struct RaiseCommand { + metadata: CallableMetadata, +} + +impl RaiseCommand { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("RAISE") + .with_syntax(&[( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { name: Cow::Borrowed("arg"), vtype: ExprType::Text }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for RaiseCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let arg = scope.get_string(0); + let message = match arg { + "argument" => "Bad argument", + "eval" => "Some eval error", + "internal" => "Some internal error", + "io" => "Some I/O error", + _ => "Invalid arguments", + }; + Err(CallError::Other(message)) + } +} diff --git a/core2/tests/testutils/callables/raisef_fn.rs b/core2/tests/testutils/callables/raisef_fn.rs new file mode 100644 index 00000000..e4639f5a --- /dev/null +++ b/core2/tests/testutils/callables/raisef_fn.rs @@ -0,0 +1,62 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::borrow::Cow; +use std::rc::Rc; + +/// A function that raises an error based on a string argument. +pub(super) struct RaisefFunction { + metadata: CallableMetadata, +} + +impl RaisefFunction { + pub(super) fn new() -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("RAISEF") + .with_return_type(ExprType::Boolean) + .with_syntax(&[( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { name: Cow::Borrowed("arg"), vtype: ExprType::Text }, + ArgSepSyntax::End, + )], + None, + )]) + .test_build(), + }) + } +} + +#[async_trait(?Send)] +impl Callable for RaisefFunction { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let arg = scope.get_string(1); + let message = match arg { + "argument" => "Bad argument", + "eval" => "Some eval error", + "internal" => "Some internal error", + "io" => "Some I/O error", + _ => "Invalid arguments", + }; + Err(CallError::Other(message)) + } +} From 5774b13c7bce984ef1bf4f59db66715b49378d13 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 08:24:14 -0800 Subject: [PATCH 28/53] core2: Implement support for DATA Compile DATA literals into image metadata and expose them to upcalls via Scope so integration callables can inspect the full sequence. Add test_data integration coverage for collection order, nested statements, upcall visibility, and compile-time rejection of non-literal DATA values. --- core2/src/callable.rs | 8 ++ core2/src/compiler/codegen.rs | 2 + core2/src/compiler/top.rs | 22 +++- core2/src/image.rs | 7 +- core2/src/vm/context.rs | 2 + core2/src/vm/mod.rs | 14 ++- core2/tests/integration_test.rs | 1 + core2/tests/test_data.md | 117 ++++++++++++++++++ .../tests/testutils/callables/get_data_cmd.rs | 69 +++++++++++ core2/tests/testutils/callables/mod.rs | 4 + 10 files changed, 239 insertions(+), 7 deletions(-) create mode 100644 core2/tests/test_data.md create mode 100644 core2/tests/testutils/callables/get_data_cmd.rs diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 3af8cb98..ae4d22a8 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -732,9 +732,17 @@ pub struct Scope<'a> { /// Last error raised in the VM, if any. pub(crate) last_error: &'a Option, + + /// `DATA` values captured from the compiled source. + pub(crate) data: &'a [Option], } impl<'a> Scope<'a> { + /// Returns `DATA` values captured from the compiled source in encounter order. + pub fn data(&self) -> &[Option] { + self.data + } + /// Returns the source position of the argument at `arg`, or `None` if unavailable. /// /// `arg` is the register-slot index of the argument, matching the `N` in `scope.get_*(N)`. diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 6666d96e..c9cbbea9 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -251,6 +251,7 @@ impl Codegen { pub(super) fn build_image( mut self, global_vars: HashMap, + data: Vec>, ) -> Result { self.apply_fixups()?; @@ -264,6 +265,7 @@ impl Codegen { self.code, self.upcalls.keys_to_vec(), self.constants.keys_to_vec(), + data, DebugInfo { instrs: self.instrs, callables, global_vars }, )) } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index c05fca6b..5420c03b 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -61,6 +61,9 @@ struct Context { /// Collection of user-defined callable definitions to be compiled after the main scope. user_callables: Vec, + /// Collection of `DATA` values captured while compiling all statements. + data: Vec>, + /// Stack of pending `EXIT DO` jumps for each nested `DO` loop. do_exit_stack: Vec>, @@ -74,6 +77,17 @@ struct Context { callable_exit_jumps: Vec<(usize, LineCol)>, } +/// Converts parser-validated `DATA` expressions into image data constants. +fn data_expr_to_constant(expr: Expr) -> ConstantDatum { + match expr { + Expr::Boolean(span) => ConstantDatum::Boolean(span.value), + Expr::Double(span) => ConstantDatum::Double(span.value), + Expr::Integer(span) => ConstantDatum::Integer(span.value), + Expr::Text(span) => ConstantDatum::Text(span.value), + _ => unreachable!("Parser guarantees DATA only contains literal values"), + } +} + /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, @@ -789,6 +803,10 @@ fn compile_stmt( ctx.user_callables.push(span); } + Statement::Data(span) => { + ctx.data.extend(span.values.into_iter().map(|expr| expr.map(data_expr_to_constant))); + } + Statement::Dim(span) => { let name_pos = span.name_pos; let key = SymbolKey::from(&span.name); @@ -978,8 +996,6 @@ fn compile_stmt( Statement::While(span) => { compile_while(ctx, symtable, span)?; } - - _ => todo!(), } if mark_start && start_pc != ctx.codegen.next_pc() { ctx.codegen.mark_statement_start(start_pc); @@ -1232,7 +1248,7 @@ pub fn compile_with_globals( (key.clone(), GlobalVarInfo { reg, subtype, ndims }) }) .collect(); - ctx.codegen.build_image(global_vars) + ctx.codegen.build_image(global_vars, ctx.data) } /// Compiles the `input` into an `Image` that can be executed by the VM. diff --git a/core2/src/image.rs b/core2/src/image.rs index e3436711..831cd401 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -149,6 +149,9 @@ pub struct Image { /// Pool of constant values used by the program. pub(crate) constants: Vec, + /// Values captured from all `DATA` statements in source order. + pub(crate) data: Vec>, + /// Debugging information for error reporting and disassembly. pub(crate) debug_info: DebugInfo, @@ -167,6 +170,7 @@ impl Default for Image { ], vec![], vec![], + vec![], DebugInfo { instrs: vec![InstrMetadata { linecol: LineCol { line: 0, col: 0 }, @@ -185,11 +189,12 @@ impl Image { code: Vec, upcalls: Vec, constants: Vec, + data: Vec>, debug_info: DebugInfo, ) -> Self { debug_assert!(!code.is_empty(), "Compiler must ensure the image is not empty"); debug_assert_eq!(code.len(), debug_info.instrs.len()); - Self { code, upcalls, constants, debug_info, _internal: () } + Self { code, upcalls, constants, data, debug_info, _internal: () } } /// Disassembles the image into a textual representation for debugging. diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 6c923b4d..65a527cb 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -282,6 +282,7 @@ impl Context { heap: &'a mut Vec, arg_linecols: &'a [LineCol], last_error: &'a Option, + data: &'a [Option], ) -> Scope<'a> { let (is_global, index) = reg.to_parts(); assert!(!is_global); @@ -294,6 +295,7 @@ impl Context { fp: self.fp + index, arg_linecols, last_error, + data, } } diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index d3e0b344..3570bc50 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -159,7 +159,7 @@ impl Vm { /// `upcall_pc` is the address of the UPCALL instruction in the image, used to look up /// per-argument source locations from `DebugInfo`. fn upcall_scope<'a>(&'a mut self, reg: Register, upcall_pc: usize) -> Scope<'a> { - let (constants, arg_linecols) = match self.image.as_ref() { + let (constants, arg_linecols, data) = match self.image.as_ref() { Some(image) => ( image.constants.as_slice(), image @@ -168,10 +168,18 @@ impl Vm { .get(upcall_pc) .map(|m| m.arg_linecols.as_slice()) .unwrap_or(&[]), + image.data.as_slice(), ), - None => (&[][..], &[][..]), + None => (&[][..], &[][..], &[][..]), }; - self.context.upcall_scope(reg, constants, &mut self.heap, arg_linecols, &self.last_error) + self.context.upcall_scope( + reg, + constants, + &mut self.heap, + arg_linecols, + &self.last_error, + data, + ) } /// Handles an exception raised at `pc` with `message`. Returns true if the error was handled. diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 5a70ec70..4712d633 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -47,6 +47,7 @@ one_test!(test_bitwise_or); one_test!(test_bitwise_shl); one_test!(test_bitwise_shr); one_test!(test_bitwise_xor); +one_test!(test_data); one_test!(test_do); one_test!(test_empty); one_test!(test_end); diff --git a/core2/tests/test_data.md b/core2/tests/test_data.md new file mode 100644 index 00000000..01fc5a1d --- /dev/null +++ b/core2/tests/test_data.md @@ -0,0 +1,117 @@ +# Test: DATA values are collected in source order + +## Source + +```basic +DATA TRUE, 3 +DATA , "hello" +GETDATA +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: UPCALL 0, R64 # 3:1, GETDATA +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 +``` + +## Output + +```plain +0=true? 1=3% 2=() 3=hello$ +``` + +# Test: DATA values are collected in nested statements + +## Source + +```basic +IF FALSE THEN + DATA 5 +ELSE + DATA 6 +END IF +WHILE FALSE + DATA 1 +WEND +FOR i = 0 TO 0 + DATA 0 +NEXT +GETDATA +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:4 +0002: JMPF R64, 4 # 1:4 +0003: JUMP 6 # 1:4 +0004: LOADI R64, 1 # 3:1 +0005: JMPF R64, 6 # 3:1 +0006: LOADI R64, 0 # 6:7 +0007: JMPF R64, 9 # 6:7 +0008: JUMP 6 # 6:7 +0009: LOADI R64, 0 # 9:9 +0010: MOVE R65, R64 # 9:5 +0011: LOADI R66, 0 # 9:14 +0012: CMPLEI R65, R65, R66 # 9:11 +0013: JMPF R65, 18 # 9:5 +0014: MOVE R64, R64 # 9:5 +0015: LOADI R65, 1 # 9:15 +0016: ADDI R64, R64, R65 # 9:11 +0017: JUMP 10 # 9:5 +0018: UPCALL 0, R65 # 12:1, GETDATA +0019: LOADI R65, 0 # 0:0 +0020: END R65 # 0:0 +``` + +## Output + +```plain +0=5% 1=6% 2=1% 3=0% +``` + +# Test: GETDATA sees all DATA values even before execution + +## Source + +```basic +GETDATA +DATA 1 +DATA 2 +GETDATA +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: UPCALL 0, R64 # 1:1, GETDATA +0002: UPCALL 0, R64 # 4:1, GETDATA +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Output + +```plain +0=1% 1=2% +0=1% 1=2% +``` + +# Test: DATA rejects non-literal values at compile time + +## Source + +```basic +DATA 5 + 1 +``` + +## Compilation errors + +```plain +1:8: Expected comma after datum but found + +``` diff --git a/core2/tests/testutils/callables/get_data_cmd.rs b/core2/tests/testutils/callables/get_data_cmd.rs new file mode 100644 index 00000000..e9fe591b --- /dev/null +++ b/core2/tests/testutils/callables/get_data_cmd.rs @@ -0,0 +1,69 @@ +// 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. + +//! A callable exposed to integration tests. + +use async_trait::async_trait; +use endbasic_core2::*; +use std::cell::RefCell; +use std::rc::Rc; + +/// A command that dumps all DATA values visible to the upcall. +pub(super) struct GetDataCommand { + metadata: CallableMetadata, + output: Rc>, +} + +impl GetDataCommand { + pub(super) fn new(output: Rc>) -> Rc { + Rc::from(Self { + metadata: CallableMetadataBuilder::new("GETDATA") + .with_syntax(&[(&[], None)]) + .test_build(), + output, + }) + } +} + +fn format_datum(datum: &Option) -> String { + match datum { + None => "()".to_owned(), + Some(ConstantDatum::Boolean(b)) => format!("{b}?"), + Some(ConstantDatum::Double(d)) => format!("{d}#"), + Some(ConstantDatum::Integer(i)) => format!("{i}%"), + Some(ConstantDatum::Text(s)) => format!("{s}$"), + } +} + +#[async_trait(?Send)] +impl Callable for GetDataCommand { + fn metadata(&self) -> &CallableMetadata { + &self.metadata + } + + async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { + let text = scope + .data() + .iter() + .enumerate() + .map(|(i, datum)| format!("{i}={}", format_datum(datum))) + .collect::>() + .join(" "); + let mut output = self.output.borrow_mut(); + output.push_str(&text); + output.push('\n'); + Ok(()) + } +} diff --git a/core2/tests/testutils/callables/mod.rs b/core2/tests/testutils/callables/mod.rs index 0e9cb603..ebcf4a17 100644 --- a/core2/tests/testutils/callables/mod.rs +++ b/core2/tests/testutils/callables/mod.rs @@ -29,6 +29,9 @@ use define_arg_cmd::DefineArgCommand; mod define_and_change_args_cmd; use define_and_change_args_cmd::DefineAndChangeArgsCommand; +mod get_data_cmd; +use get_data_cmd::GetDataCommand; + mod increment_required_int_cmd; use increment_required_int_cmd::IncrementRequiredIntCommand; @@ -106,6 +109,7 @@ pub(super) fn register_all( ConcatFunction::new() as Rc, DefineAndChangeArgsCommand::new() as Rc, DefineArgCommand::new() as Rc, + GetDataCommand::new(console.clone()) as Rc, IncrementRequiredIntCommand::new() as Rc, IsPositiveFunction::new() as Rc, LastErrorFunction::new() as Rc, From 83159eafd05fd00e9d670949aa6be70485d59930 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 13:05:54 -0800 Subject: [PATCH 29/53] core2: Audit test gaps against core Compare language-visible scenarios between the legacy core tests and core2 integration tests. Add missing core2 scenarios for SELECT, GOTO, GOSUB, END, FUNCTION, and SUB behavior. This does NOT address pre-existing issues I discovered that yield divergent behavior. Those will be addressed next. --- core2/tests/integration_test.rs | 1 + core2/tests/test_case_insensitivity.md | 308 +++++++++++++++++++++++++ core2/tests/test_end.md | 88 +++++++ core2/tests/test_functions.md | 126 ++++++++++ core2/tests/test_globals.md | 42 ++++ core2/tests/test_gosub.md | 84 +++++++ core2/tests/test_goto.md | 143 ++++++++++++ core2/tests/test_select.md | 48 ++++ core2/tests/test_subs.md | 107 +++++++++ 9 files changed, 947 insertions(+) create mode 100644 core2/tests/test_case_insensitivity.md diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index 4712d633..f578eb8e 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -47,6 +47,7 @@ one_test!(test_bitwise_or); one_test!(test_bitwise_shl); one_test!(test_bitwise_shr); one_test!(test_bitwise_xor); +one_test!(test_case_insensitivity); one_test!(test_data); one_test!(test_do); one_test!(test_empty); diff --git a/core2/tests/test_case_insensitivity.md b/core2/tests/test_case_insensitivity.md new file mode 100644 index 00000000..862aaaf4 --- /dev/null +++ b/core2/tests/test_case_insensitivity.md @@ -0,0 +1,308 @@ +# Test: Variable names are case insensitive + +## Source + +```basic +A = 1 +OUT a +a = 2 +OUT A +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:5 +0002: MOVE R66, R64 # 2:5 +0003: LOADI R65, 258 # 2:5 +0004: UPCALL 0, R65 # 2:1, OUT +0005: LOADI R64, 2 # 3:5 +0006: MOVE R66, R64 # 4:5 +0007: LOADI R65, 258 # 4:5 +0008: UPCALL 0, R65 # 4:1, OUT +0009: LOADI R65, 0 # 0:0 +0010: END R65 # 0:0 +``` + +## Output + +```plain +0=1% +0=2% +``` + +# Test: Array names are case insensitive + +## Source + +```basic +DIM A(3) +a(0) = 10 +A(1) = 20 +OUT A(0), a(1) +``` + +## Disassembly + +```asm +0000: ENTER 6 # 0:0 +0001: LOADI R65, 3 # 1:7 +0002: ALLOCA R64, [1]%, R65 # 1:5 +0003: LOADI R65, 10 # 2:8 +0004: LOADI R66, 0 # 2:3 +0005: STOREA R64, R65, R66 # 2:1 +0006: LOADI R65, 20 # 3:8 +0007: LOADI R66, 1 # 3:3 +0008: STOREA R64, R65, R66 # 3:1 +0009: LOADI R67, 0 # 4:7 +0010: LOADA R66, R64, R67 # 4:5 +0011: LOADI R65, 290 # 4:5 +0012: LOADI R69, 1 # 4:13 +0013: LOADA R68, R64, R69 # 4:11 +0014: LOADI R67, 258 # 4:11 +0015: UPCALL 0, R65 # 4:1, OUT +0016: LOADI R65, 0 # 0:0 +0017: END R65 # 0:0 +``` + +## Output + +```plain +0=10% , 1=20% +``` + +# Test: DIM conflicts with existing variable of different case + +## Source + +```basic +a = 5 +DIM A +``` + +## Compilation errors + +```plain +2:5: Cannot redefine A +``` + +# Test: DIM SHARED conflicts with existing global of different case + +## Source + +```basic +DIM SHARED a +DIM SHARED A +``` + +## Compilation errors + +```plain +2:12: Cannot redefine A +``` + +# Test: Global variable name is case insensitive + +## Source + +```basic +DIM SHARED A +A = 1 +a = 2 +OUT A, a +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R0, 1 # 2:5 +0003: LOADI R0, 2 # 3:5 +0004: MOVE R65, R0 # 4:5 +0005: LOADI R64, 290 # 4:5 +0006: MOVE R67, R0 # 4:8 +0007: LOADI R66, 258 # 4:8 +0008: UPCALL 0, R64 # 4:1, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 +``` + +## Output + +```plain +0=2% , 1=2% +``` + +# Test: Function name is case insensitive + +## Source + +```basic +FUNCTION Foo + foo = 42 +END FUNCTION + +OUT FOO +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: CALL R65, 6 # 5:5, FOO +0002: LOADI R64, 258 # 5:5 +0003: UPCALL 0, R64 # 5:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 + +-- FOO +0006: LOADI R64, 0 # 1:10 +0007: ENTER 1 # 0:0 +0008: LOADI R64, 42 # 2:11 +0009: RETURN # 3:1 +``` + +## Output + +```plain +0=42% +``` + +# Test: Sub name is case insensitive + +## Source + +```basic +SUB Foo + OUT "hello" +END SUB + +FOO +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: CALL R64, 4 # 5:1, FOO +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 + +-- FOO +0004: ENTER 2 # 0:0 +0005: LOADI R65, 0 # 2:9 +0006: LOADI R64, 259 # 2:9 +0007: UPCALL 0, R64 # 2:5, OUT +0008: RETURN # 3:1 +``` + +## Output + +```plain +0=hello$ +``` + +# Test: Label name is case insensitive + +## Source + +```basic +GOTO @FOO +OUT "skipped" +@foo: +OUT "done" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 5 # 1:6 +0002: LOADI R65, 0 # 2:5 +0003: LOADI R64, 259 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R65, 1 # 4:5 +0006: LOADI R64, 259 # 4:5 +0007: UPCALL 0, R64 # 4:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=done$ +``` + +# Test: GOSUB target is case insensitive + +## Source + +```basic +GOSUB @FOO +END +@foo: +OUT "in gosub" +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: GOSUB 4 # 1:7 +0002: LOADI R64, 0 # 2:1 +0003: END R64 # 2:1 +0004: LOADI R65, 0 # 4:5 +0005: LOADI R64, 259 # 4:5 +0006: UPCALL 0, R64 # 4:1, OUT +0007: RETURN # 5:1 +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=in gosub$ +``` + +# Test: Parameter names are case insensitive + +## Source + +```basic +FUNCTION foo(A) + foo = a + 1 +END FUNCTION + +OUT foo(5) +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R66, 5 # 5:9 +0002: CALL R65, 7 # 5:5, FOO +0003: LOADI R64, 258 # 5:5 +0004: UPCALL 0, R64 # 5:1, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 + +-- FOO +0007: LOADI R64, 0 # 1:10 +0008: ENTER 3 # 0:0 +0009: MOVE R64, R65 # 2:11 +0010: LOADI R66, 1 # 2:15 +0011: ADDI R64, R64, R66 # 2:13 +0012: RETURN # 3:1 +``` + +## Output + +```plain +0=6% +``` diff --git a/core2/tests/test_end.md b/core2/tests/test_end.md index 15b11d78..375d8420 100644 --- a/core2/tests/test_end.md +++ b/core2/tests/test_end.md @@ -132,3 +132,91 @@ END "foo" ```plain 1:5: STRING is not a number ``` + +# Test: Exit code cannot be negative + +## Source + +```basic +END -3 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 3 # 1:6 +0002: NEGI R64 # 1:5 +0003: END R64 # 1:1 +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 +``` + +## Exit code + +```plain +-3 +``` + +# Test: Exit code cannot be larger than 127 + +## Source + +```basic +END 128 +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 128 # 1:5 +0002: END R64 # 1:1 +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Exit code + +```plain +128 +``` + +# Test: END exits from inside FOR loop + +## Source + +```basic +FOR i = 1 TO 10 + IF i = 3 THEN END 42 +NEXT +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 1 # 1:9 +0002: MOVE R65, R64 # 1:5 +0003: LOADI R66, 10 # 1:14 +0004: CMPLEI R65, R65, R66 # 1:11 +0005: JMPF R65, 16 # 1:5 +0006: MOVE R65, R64 # 2:8 +0007: LOADI R66, 3 # 2:12 +0008: CMPEQI R65, R65, R66 # 2:10 +0009: JMPF R65, 12 # 2:8 +0010: LOADI R65, 42 # 2:23 +0011: END R65 # 2:19 +0012: MOVE R64, R64 # 1:5 +0013: LOADI R65, 1 # 1:16 +0014: ADDI R64, R64, R65 # 1:11 +0015: JUMP 2 # 1:5 +0016: LOADI R65, 0 # 0:0 +0017: END R65 # 0:0 +``` + +## Exit code + +```plain +42 +``` diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 982cf7bd..8c94e8bc 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -912,3 +912,129 @@ END FUNCTION ```plain 2:5: EXIT SUB outside of SUB ``` + +# Test: Recursive function + +## Source + +```basic +DIM SHARED calls AS INTEGER +FUNCTION factorial(n%) + IF n = 1 THEN factorial = 1 ELSE factorial = n * factorial(n - 1) + calls = calls + 1 +END FUNCTION +OUT calls; factorial(5) +``` + +## Disassembly + +```asm +0000: ENTER 5 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: MOVE R65, R0 # 6:5 +0003: LOADI R64, 274 # 6:5 +0004: LOADI R68, 5 # 6:22 +0005: CALL R67, 10 # 6:12, FACTORIAL +0006: LOADI R66, 258 # 6:12 +0007: UPCALL 0, R64 # 6:1, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 + +-- FACTORIAL +0010: LOADI R64, 0 # 2:10 +0011: ENTER 5 # 0:0 +0012: MOVE R66, R65 # 3:8 +0013: LOADI R67, 1 # 3:12 +0014: CMPEQI R66, R66, R67 # 3:10 +0015: JMPF R66, 18 # 3:8 +0016: LOADI R64, 1 # 3:31 +0017: JUMP 26 # 3:8 +0018: LOADI R66, 1 # 3:33 +0019: JMPF R66, 26 # 3:33 +0020: MOVE R64, R65 # 3:50 +0021: MOVE R67, R65 # 3:64 +0022: LOADI R68, 1 # 3:68 +0023: SUBI R67, R67, R68 # 3:66 +0024: CALL R66, 10 # 3:54, FACTORIAL +0025: MULI R64, R64, R66 # 3:52 +0026: MOVE R0, R0 # 4:13 +0027: LOADI R66, 1 # 4:21 +0028: ADDI R0, R0, R66 # 4:19 +0029: RETURN # 5:1 +``` + +## Output + +```plain +0=0% ; 1=120% +``` + +# Test: Calling a function as a command is an error + +## Source + +```basic +FUNCTION f + OUT "foo" +END FUNCTION +f +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: CALL R64, 4 # 4:1, F +0002: LOADI R64, 0 # 0:0 +0003: END R64 # 0:0 + +-- F +0004: LOADI R64, 0 # 1:10 +0005: ENTER 3 # 0:0 +0006: LOADI R66, 0 # 2:9 +0007: LOADI R65, 259 # 2:9 +0008: UPCALL 0, R65 # 2:5, OUT +0009: RETURN # 3:1 +``` + +## Output + +```plain +0=foo$ +``` + +# Test: Function redefines existing function + +## Source + +```basic +FUNCTION foo +END FUNCTION + +FUNCTION foo +END FUNCTION +``` + +## Compilation errors + +```plain +4:10: Cannot redefine foo% +``` + +# Test: Function redefines existing sub + +## Source + +```basic +SUB foo +END SUB + +FUNCTION foo +END FUNCTION +``` + +## Compilation errors + +```plain +4:10: Cannot redefine foo% +``` diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index 3a24ba77..ae13d834 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -308,3 +308,45 @@ DIM SHARED x AS INTEGER ```plain 2:12: Cannot redefine x ``` + +# Test: DIM SHARED when local variable of same name exists + +## Source + +```basic +FUNCTION foo + x = 5 + DIM SHARED x + OUT x +END FUNCTION + +OUT foo +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: CALL R65, 6 # 7:5, FOO +0002: LOADI R64, 258 # 7:5 +0003: UPCALL 0, R64 # 7:1, OUT +0004: LOADI R64, 0 # 0:0 +0005: END R64 # 0:0 + +-- FOO +0006: LOADI R64, 0 # 1:10 +0007: ENTER 4 # 0:0 +0008: LOADI R65, 5 # 2:9 +0009: LOADI R0, 0 # 3:16 +0010: MOVE R67, R65 # 4:9 +0011: LOADI R66, 258 # 4:9 +0012: UPCALL 0, R66 # 4:5, OUT +0013: RETURN # 5:1 +``` + +## Output + +```plain +0=5% +0=0% +``` diff --git a/core2/tests/test_gosub.md b/core2/tests/test_gosub.md index 2cf8d1c8..2d9350ab 100644 --- a/core2/tests/test_gosub.md +++ b/core2/tests/test_gosub.md @@ -42,6 +42,38 @@ RETURN 0=c$ ``` +# Test: GOSUB with numeric label + +## Source + +```basic +GOSUB 100 +END +100 OUT "in gosub" +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: GOSUB 4 # 1:7 +0002: LOADI R64, 0 # 2:1 +0003: END R64 # 2:1 +0004: LOADI R65, 0 # 3:9 +0005: LOADI R64, 259 # 3:9 +0006: UPCALL 0, R64 # 3:5, OUT +0007: RETURN # 4:1 +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=in gosub$ +``` + # Test: Nested GOSUB calls and returns ## Source @@ -175,3 +207,55 @@ GOSUB @s ```plain 0=In target$ ``` + +# Test: GOSUB without RETURN still runs target + +## Source + +```basic +GOSUB @sub: @sub: OUT 1 +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: GOSUB 2 # 1:7 +0002: LOADI R65, 1 # 1:23 +0003: LOADI R64, 258 # 1:23 +0004: UPCALL 0, R64 # 1:19, OUT +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 +``` + +## Output + +```plain +0=1% +``` + +# Test: RETURN reached by GOTO fails + +## Source + +```basic +GOTO @foo +@foo: +RETURN +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: JUMP 2 # 1:6 +0002: RETURN # 3:1 +0003: LOADI R64, 0 # 0:0 +0004: END R64 # 0:0 +``` + +## Runtime errors + +```plain +3:1: RETURN without GOSUB or FUNCTION call +``` diff --git a/core2/tests/test_goto.md b/core2/tests/test_goto.md index 61232983..f9fe4531 100644 --- a/core2/tests/test_goto.md +++ b/core2/tests/test_goto.md @@ -94,3 +94,146 @@ GOTO @print_it ```plain 0=Print something$ ``` + +# Test: GOTO with numeric label + +## Source + +```basic +GOTO 20 +OUT "skipped" +20 OUT "target" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 5 # 1:6 +0002: LOADI R65, 0 # 2:5 +0003: LOADI R64, 259 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R65, 1 # 3:8 +0006: LOADI R64, 259 # 3:8 +0007: UPCALL 0, R64 # 3:4, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=target$ +``` + +# Test: GOTO to numeric label in middle of line + +## Source + +```basic +GOTO 20 +OUT "skipped": 20 OUT "target" +``` + +## Disassembly + +```asm +0000: ENTER 2 # 0:0 +0001: JUMP 5 # 1:6 +0002: LOADI R65, 0 # 2:5 +0003: LOADI R64, 259 # 2:5 +0004: UPCALL 0, R64 # 2:1, OUT +0005: LOADI R65, 1 # 2:23 +0006: LOADI R64, 259 # 2:23 +0007: UPCALL 0, R64 # 2:19, OUT +0008: LOADI R64, 0 # 0:0 +0009: END R64 # 0:0 +``` + +## Output + +```plain +0=target$ +``` + +# Test: GOTO unknown label + +## Source + +```basic +GOTO @missing +``` + +## Compilation errors + +```plain +1:6: Unknown label missing +``` + +# Test: GOTO unknown numeric label + +## Source + +```basic +GOTO 10 +``` + +## Compilation errors + +```plain +1:6: Unknown label 10 +``` + +# Test: Duplicate label + +## Source + +```basic +@foo: +@foo: +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R64, 0 # 0:0 +0002: END R64 # 0:0 +``` + +# Test: GOTO as the last statement in a loop + +## Source + +```basic +i = 0 +@again: +IF i = 5 THEN END i +i = i + 1 +GOTO @again +``` + +## Disassembly + +```asm +0000: ENTER 3 # 0:0 +0001: LOADI R64, 0 # 1:5 +0002: MOVE R65, R64 # 3:4 +0003: LOADI R66, 5 # 3:8 +0004: CMPEQI R65, R65, R66 # 3:6 +0005: JMPF R65, 8 # 3:4 +0006: MOVE R65, R64 # 3:19 +0007: END R65 # 3:15 +0008: MOVE R64, R64 # 4:5 +0009: LOADI R65, 1 # 4:9 +0010: ADDI R64, R64, R65 # 4:7 +0011: JUMP 2 # 5:6 +0012: LOADI R65, 0 # 0:0 +0013: END R65 # 0:0 +``` + +## Exit code + +```plain +5 +``` diff --git a/core2/tests/test_select.md b/core2/tests/test_select.md index bccd46cb..b72de09e 100644 --- a/core2/tests/test_select.md +++ b/core2/tests/test_select.md @@ -593,3 +593,51 @@ END SELECT ```plain 2:10: Cannot = INTEGER and BOOLEAN ``` + +# Test: SELECT with no matching CASE + +## Source + +```basic +SELECT CASE 42 + CASE 1 + OUT "unexpected" + CASE 2 +END SELECT +OUT "done" +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R64, 42 # 1:13 +0002: MOVE R65, R64 # 2:10 +0003: LOADI R66, 1 # 2:10 +0004: CMPEQI R67, R65, R66 # 2:10 +0005: JMPF R67, 7 # 2:10 +0006: JUMP 15 # 2:10 +0007: JUMP 8 # 5:1 +0008: MOVE R65, R64 # 4:10 +0009: LOADI R66, 2 # 4:10 +0010: CMPEQI R67, R65, R66 # 4:10 +0011: JMPF R67, 13 # 4:10 +0012: JUMP 19 # 4:10 +0013: JUMP 14 # 5:1 +0014: JUMP 19 # 5:1 +0015: LOADI R65, 0 # 3:13 +0016: LOADI R64, 259 # 3:13 +0017: UPCALL 0, R64 # 3:9, OUT +0018: JUMP 19 # 5:1 +0019: LOADI R65, 1 # 6:5 +0020: LOADI R64, 259 # 6:5 +0021: UPCALL 0, R64 # 6:1, OUT +0022: LOADI R64, 0 # 0:0 +0023: END R64 # 0:0 +``` + +## Output + +```plain +0=done$ +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index a1466682..47a20774 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -363,3 +363,110 @@ END SUB ```plain 2:5: EXIT FUNCTION outside of FUNCTION ``` + +# Test: Recursive subroutine + +## Source + +```basic +DIM SHARED counter AS INTEGER +SUB count_down(prefix$) + OUT prefix; counter + IF counter > 1 THEN + counter = counter - 1 + count_down prefix + END IF +END SUB +counter = 3 +count_down "counter is" +``` + +## Disassembly + +```asm +0000: ENTER 1 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R0, 3 # 9:11 +0003: LOADI R64, 0 # 10:12 +0004: CALL R64, 7 # 10:1, COUNT_DOWN +0005: LOADI R64, 0 # 0:0 +0006: END R64 # 0:0 + +-- COUNT_DOWN +0007: ENTER 5 # 0:0 +0008: MOVE R66, R64 # 3:9 +0009: LOADI R65, 275 # 3:9 +0010: MOVE R68, R0 # 3:17 +0011: LOADI R67, 258 # 3:17 +0012: UPCALL 0, R65 # 3:5, OUT +0013: MOVE R65, R0 # 4:8 +0014: LOADI R66, 1 # 4:18 +0015: CMPGTI R65, R65, R66 # 4:16 +0016: JMPF R65, 22 # 4:8 +0017: MOVE R0, R0 # 5:19 +0018: LOADI R65, 1 # 5:29 +0019: SUBI R0, R0, R65 # 5:27 +0020: MOVE R65, R64 # 6:20 +0021: CALL R65, 7 # 6:9, COUNT_DOWN +0022: RETURN # 8:1 +``` + +## Output + +```plain +0=counter is$ ; 1=3% +0=counter is$ ; 1=2% +0=counter is$ ; 1=1% +``` + +# Test: Calling a subroutine as a function is an error + +## Source + +```basic +SUB f +END SUB +OUT f +``` + +## Compilation errors + +```plain +3:5: Cannot call f (not a function) +``` + +# Test: Sub redefines existing function + +## Source + +```basic +FUNCTION foo +END FUNCTION + +SUB foo +END SUB +``` + +## Compilation errors + +```plain +4:5: Cannot redefine foo +``` + +# Test: Sub redefines existing sub + +## Source + +```basic +SUB foo +END SUB + +SUB foo +END SUB +``` + +## Compilation errors + +```plain +4:5: Cannot redefine foo +``` From 89c5d6f959fb670ebc0b6aecd1e699de986c2d70 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 17:08:51 -0800 Subject: [PATCH 30/53] core2: Reject function calls as commands Reject statement-form invocation of functions during compilation. Treat callable symbols with return types as invalid command targets and raise NotAFunction before compiling arguments. Update function integration coverage to assert compilation errors for user-defined functions and builtin function upcalls used as commands. --- core2/src/compiler/top.rs | 3 +++ core2/tests/test_functions.md | 43 ++++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 5420c03b..ecafe7b0 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -753,6 +753,9 @@ fn compile_stmt( RegisterScope::Global, )); }; + if md.return_type().is_some() { + return Err(Error::NotAFunction(span.vref_pos, span.vref)); + } let is_user_defined = md.is_user_defined(); let md = md.clone(); diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 8c94e8bc..64f83abe 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -980,27 +980,38 @@ END FUNCTION f ``` -## Disassembly +## Compilation errors -```asm -0000: ENTER 1 # 0:0 -0001: CALL R64, 4 # 4:1, F -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 - --- F -0004: LOADI R64, 0 # 1:10 -0005: ENTER 3 # 0:0 -0006: LOADI R66, 0 # 2:9 -0007: LOADI R65, 259 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: RETURN # 3:1 +```plain +4:1: Cannot call F (not a function) ``` -## Output +# Test: Calling an argless function upcall as a command is an error + +## Source + +```basic +MEANING_OF_LIFE +``` + +## Compilation errors + +```plain +1:1: Cannot call MEANING_OF_LIFE (not a function) +``` + +# Test: Calling a function upcall with arguments as a command is an error + +## Source + +```basic +SUM_DOUBLES 1.0, 2.0 +``` + +## Compilation errors ```plain -0=foo$ +1:1: Cannot call SUM_DOUBLES (not a function) ``` # Test: Function redefines existing function From 483cb7efc5c7fae419d18435df2eb625453ba21d Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 18:09:34 -0800 Subject: [PATCH 31/53] core2: Reject duplicate labels in compiler Report duplicate labels as a compilation error instead of silently accepting redefinitions. Keep parity with legacy core semantics by rejecting duplicates in nested control-flow contexts and update GOTO integration tests to validate the new behavior. --- core2/src/compiler/codegen.rs | 6 +++--- core2/src/compiler/mod.rs | 4 ++++ core2/src/compiler/top.rs | 4 +++- core2/tests/test_goto.md | 31 ++++++++++++++++++++++++++----- 4 files changed, 36 insertions(+), 9 deletions(-) diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index c9cbbea9..72962304 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -177,9 +177,9 @@ impl Codegen { self.user_callables_addresses.insert(key, address); } - /// Records the location of a label. - pub(super) fn define_label(&mut self, key: SymbolKey, address: Address) { - self.labels.insert(key, address); + /// Records the location of a label. Returns false on failure (if the label already existed). + pub(super) fn define_label(&mut self, key: SymbolKey, address: Address) -> bool { + self.labels.insert(key, address).is_none() } /// Converts a symbolic `target` address into a 16-bit relative address from `pos`. diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 81bc896d..06a6c112 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -59,6 +59,10 @@ pub enum Error { #[error("{0}: Cannot nest FUNCTION or SUB definitions")] CannotNestUserCallables(LineCol), + /// Attempt to redefine an already-defined label. + #[error("{0}: Duplicate label {1}")] + DuplicateLabel(LineCol, String), + /// Type annotation in a reference doesn't match the variable's type. #[error("{0}: Incompatible type annotation in {1} reference")] IncompatibleTypeAnnotationInReference(LineCol, VarRef), diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index ecafe7b0..4875f561 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -962,7 +962,9 @@ fn compile_stmt( Statement::Label(span) => { mark_start = false; - ctx.codegen.define_label(SymbolKey::from(span.name), ctx.codegen.next_pc()); + if !ctx.codegen.define_label(SymbolKey::from(&span.name), ctx.codegen.next_pc()) { + return Err(Error::DuplicateLabel(span.name_pos, span.name)); + } } Statement::Return(span) => { diff --git a/core2/tests/test_goto.md b/core2/tests/test_goto.md index f9fe4531..41c4ccdc 100644 --- a/core2/tests/test_goto.md +++ b/core2/tests/test_goto.md @@ -193,12 +193,33 @@ GOTO 10 @foo: ``` -## Disassembly +## Compilation errors -```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 0:0 -0002: END R64 # 0:0 +```plain +2:1: Duplicate label foo +``` + +# Test: Duplicate label in nested control flow + +## Source + +```basic +i = 0 +@a + @b + @c + i = i + 1 + IF i = 1 THEN: GOTO @b: END IF + @a + IF i = 2 THEN: GOTO @c: END IF + IF i = 3 THEN: GOTO @out: END IF +@out +``` + +## Compilation errors + +```plain +7:13: Duplicate label a ``` # Test: GOTO as the last statement in a loop From 60035768c502b189d9e5b4f3acdf783b1ba4484f Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 3 Mar 2026 18:37:07 -0800 Subject: [PATCH 32/53] core2: Enforce END exit-code bounds Reject out-of-range literal END codes during compilation to match legacy semantics. Validate dynamic END codes in the VM and raise the same errors when values are negative or larger than 127. Update END integration coverage to expect compilation errors for invalid literals and runtime errors for invalid dynamic values. --- core2/examples/config.rs | 4 +-- core2/src/bytecode.rs | 52 +++++++++++++++++++++++++++++++ core2/src/compiler/mod.rs | 11 ++++++- core2/src/compiler/top.rs | 25 +++++++++++++-- core2/src/lib.rs | 2 +- core2/src/vm/context.rs | 13 ++++++-- core2/src/vm/mod.rs | 20 ++++++------ core2/tests/test_end.md | 60 +++++++++++++++++++++++++++--------- core2/tests/testutils/mod.rs | 2 +- 9 files changed, 157 insertions(+), 32 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 3818f77a..1f8cb780 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -88,8 +88,8 @@ fn main() { vm.load(image); match vm.exec() { StopReason::End(code) => { - if code != 0 { - eprintln!("Script exited with code {}", code); + if !code.is_success() { + eprintln!("Script exited with code {}", code.to_i32()); } } StopReason::Exception(pos, msg) => { diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index dcfe0c71..11059319 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -45,6 +45,11 @@ impl fmt::Display for RegisterScope { } } +/// Error to indicate an invalid `END` exit code. +#[derive(Debug, thiserror::Error)] +#[error("Exit code must be in the 0..127 range")] +pub struct InvalidExitCodeError(()); + /// Error to indicate that we have run out of registers. #[derive(Debug, thiserror::Error)] #[error("Out of registers")] @@ -66,6 +71,39 @@ pub(crate) enum ParseError { /// Result type for bytecode parsing operations. pub(crate) type ParseResult = Result; +/// Program exit code carried by the `END` instruction. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ExitCode(u8); + +impl ExitCode { + /// Returns true if this code represents successful execution. + pub fn is_success(self) -> bool { + self.0 == 0 + } + + /// Creates an `ExitCode` from an integer. + pub fn try_new(value: i32) -> Result { + if (0..128).contains(&value) { + Ok(Self(value as u8)) + } else { + Err(InvalidExitCodeError(())) + } + } + + /// Converts this exit code to an integer. + pub fn to_i32(self) -> i32 { + i32::from(self.0) + } +} + +impl TryFrom for ExitCode { + type Error = InvalidExitCodeError; + + fn try_from(value: i32) -> Result { + Self::try_new(value) + } +} + /// Conversions between a primitive type and a `u32` for insertion into an instruction. trait RawValue: Sized { /// Converts a `u32` to the primitive type `Self`. @@ -1708,6 +1746,20 @@ mod tests { test_instr!(test_upcall, make_upcall, parse_upcall, 12345, Register::local(3).unwrap()); + #[test] + fn test_exit_code_try_ok() { + assert_eq!(ExitCode(0), ExitCode::try_from(0).unwrap()); + assert_eq!(ExitCode(127), ExitCode::try_from(127).unwrap()); + assert!(ExitCode::try_from(0).unwrap().is_success()); + assert!(!ExitCode::try_from(127).unwrap().is_success()); + } + + #[test] + fn test_exit_code_try_errors() { + assert!(ExitCode::try_from(-1).is_err()); + assert!(ExitCode::try_from(128).is_err()); + } + #[test] fn test_packed_array_type_round_trip() { for subtype in [ExprType::Boolean, ExprType::Double, ExprType::Integer, ExprType::Text] { diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 06a6c112..c5f6d3a6 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -16,7 +16,7 @@ //! Compiler for the EndBASIC language into bytecode. use crate::ast::{ExprType, VarRef}; -use crate::bytecode::RegisterScope; +use crate::bytecode::{InvalidExitCodeError, RegisterScope}; use crate::callable::CallableMetadata; use crate::parser; use crate::reader::LineCol; @@ -71,6 +71,10 @@ pub enum Error { #[error("{0}: Cannot assign value of type {1} to variable of type {2}")] IncompatibleTypesInAssignment(LineCol, ExprType, ExprType), + /// `END` code is out of range. + #[error("{0}: {1}")] + InvalidEndCode(LineCol, String), + /// I/O error while reading the source. #[error("{0}: I/O error during compilation: {1}")] Io(LineCol, io::Error), @@ -133,6 +137,11 @@ pub enum Error { } impl Error { + /// Annotates an invalid `END` exit code error with a source position. + fn from_bytecode_invalid_exit_code(value: InvalidExitCodeError, pos: LineCol) -> Self { + Self::InvalidEndCode(pos, value.to_string()) + } + /// Annotates an error from the symbol table with the position it arised from. fn from_syms(value: syms::Error, pos: LineCol) -> Self { match value { diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 4875f561..a5bd80c3 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -88,6 +88,20 @@ fn data_expr_to_constant(expr: Expr) -> ConstantDatum { } } +/// Returns a statically-known `END` exit code and its source position, if any. +fn static_end_code(expr: &Expr) -> Option<(i32, LineCol)> { + match expr { + Expr::Integer(span) => Some((span.value, span.pos)), + Expr::Negate(span) => { + let Expr::Integer(inner) = &span.expr else { + return None; + }; + inner.value.checked_neg().map(|value| (value, span.pos)) + } + _ => None, + } +} + /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, @@ -894,6 +908,13 @@ fn compile_stmt( } Statement::End(span) => { + if let Some(expr) = span.code.as_ref() + && let Some((code, code_pos)) = static_end_code(expr) + && let Err(e) = bytecode::ExitCode::try_from(code) + { + return Err(Error::from_bytecode_invalid_exit_code(e, code_pos)); + } + let mut symtable = symtable.frozen(); let mut scope = symtable.temp_scope(); let reg = scope.alloc().map_err(|e| Error::from_syms(e, span.pos))?; @@ -1279,8 +1300,8 @@ mod tests { let mut vm = Vm::new(HashMap::default()); vm.load(image); match vm.exec() { - StopReason::End(0) => {} - StopReason::End(code) => panic!("unexpected exit code: {code}"), + StopReason::End(code) if code.is_success() => {} + StopReason::End(code) => panic!("unexpected exit code: {}", code.to_i32()), StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"), StopReason::Upcall(_) => panic!("unexpected upcall"), } diff --git a/core2/src/lib.rs b/core2/src/lib.rs index 772673c2..af5bae38 100644 --- a/core2/src/lib.rs +++ b/core2/src/lib.rs @@ -28,7 +28,7 @@ mod reader; mod vm; pub use ast::{ArgSep, ExprType}; -pub use bytecode::VarArgTag; +pub use bytecode::{ExitCode, InvalidExitCodeError, VarArgTag}; pub use callable::*; pub use compiler::{ GlobalDef, GlobalDefKind, SymbolKey, compile, compile_with_globals, only_metadata, diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 65a527cb..d76b8d05 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -17,7 +17,9 @@ use crate::ExprType; use crate::Scope; -use crate::bytecode::{self, ErrorHandlerMode, Opcode, Register, TaggedRegisterRef, opcode_of}; +use crate::bytecode::{ + self, ErrorHandlerMode, ExitCode, Opcode, Register, TaggedRegisterRef, opcode_of, +}; use crate::image::Image; use crate::mem::{ArrayData, ConstantDatum, DatumPtr, HeapDatum}; use crate::num::unchecked_usize_as_u8; @@ -29,7 +31,7 @@ type Address = usize; /// Internal representation of a `StopReason` that requires further annotation by the caller. pub(super) enum InternalStopReason { /// Execution terminated due to an `END` instruction. - End(i32), + End(ExitCode), /// Execution stopped due to an instruction-level exception. Exception(Address, String), @@ -651,6 +653,13 @@ impl Context { pub(super) fn do_end(&mut self, instr: u32) { let reg = bytecode::parse_end(instr); let code = self.get_reg(reg) as i32; + let code = match ExitCode::try_from(code) { + Ok(code) => code, + Err(e) => { + self.set_exception(e.to_string()); + return; + } + }; self.stop = Some(InternalStopReason::End(code)); } diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 3570bc50..3e4db466 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -17,7 +17,7 @@ use crate::CallResult; use crate::ast::ExprType; -use crate::bytecode::Register; +use crate::bytecode::{ExitCode, Register}; use crate::callable::{Callable, Scope}; use crate::compiler::SymbolKey; use crate::image::Image; @@ -79,7 +79,7 @@ impl<'a> UpcallHandler<'a> { /// Representation of termination states from program execution. pub enum StopReason<'a> { /// Execution terminated due to an `END` instruction. - End(i32), + End(ExitCode), /// Execution stopped due to an instruction-level exception. Exception(LineCol, String), @@ -303,7 +303,9 @@ impl Vm { } let Some(image) = self.image.as_ref() else { - return StopReason::End(0); + return StopReason::End( + ExitCode::try_from(0).expect("Zero must be a valid exit code"), + ); }; if self.pending_upcall.is_some() { @@ -413,7 +415,7 @@ mod tests { fn test_exec_without_load_is_eof() { let mut vm = Vm::new(HashMap::default()); match vm.exec() { - StopReason::End(0) => (), + StopReason::End(code) if code.is_success() => (), _ => panic!("Unexpected stop reason"), } } @@ -423,7 +425,7 @@ mod tests { let mut vm = Vm::new(HashMap::default()); vm.load(Image::default()); match vm.exec() { - StopReason::End(0) => (), + StopReason::End(code) if code.is_success() => (), _ => panic!("Unexpected stop reason"), } } @@ -434,7 +436,7 @@ mod tests { let image = compile(&mut b"".as_slice(), &HashMap::default()).unwrap(); vm.load(image); match vm.exec() { - StopReason::End(0) => (), + StopReason::End(code) if code.is_success() => (), _ => panic!("Unexpected stop reason"), } } @@ -470,7 +472,7 @@ mod tests { assert_eq!(["30", "20"], *data.borrow().as_slice()); match vm.exec() { - StopReason::End(0) => (), + StopReason::End(code) if code.is_success() => (), _ => panic!("Fourth exec should stop at EOF"), } assert_eq!(["30", "20"], *data.borrow().as_slice()); @@ -482,7 +484,7 @@ mod tests { let image = compile(&mut b"END".as_slice(), &HashMap::default()).unwrap(); vm.load(image); match vm.exec() { - StopReason::End(0) => (), + StopReason::End(code) if code.is_success() => (), _ => panic!("Unexpected stop reason"), } } @@ -493,7 +495,7 @@ mod tests { let image = compile(&mut b"END 3".as_slice(), &HashMap::default()).unwrap(); vm.load(image); match vm.exec() { - StopReason::End(3) => (), + StopReason::End(code) if code.to_i32() == 3 => (), _ => panic!("Unexpected stop reason"), } } diff --git a/core2/tests/test_end.md b/core2/tests/test_end.md index 375d8420..1b2d9c22 100644 --- a/core2/tests/test_end.md +++ b/core2/tests/test_end.md @@ -141,45 +141,77 @@ END "foo" END -3 ``` +## Compilation errors + +```plain +1:5: Exit code must be in the 0..127 range +``` + +# Test: Exit code cannot be larger than 127 + +## Source + +```basic +END 128 +``` + +## Compilation errors + +```plain +1:5: Exit code must be in the 0..127 range +``` + +# Test: Dynamic exit code cannot be negative + +## Source + +```basic +i = -3 +END i +``` + ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 2 # 0:0 0001: LOADI R64, 3 # 1:6 0002: NEGI R64 # 1:5 -0003: END R64 # 1:1 -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0003: MOVE R65, R64 # 2:5 +0004: END R65 # 2:1 +0005: LOADI R65, 0 # 0:0 +0006: END R65 # 0:0 ``` -## Exit code +## Runtime errors ```plain --3 +2:1: Exit code must be in the 0..127 range ``` -# Test: Exit code cannot be larger than 127 +# Test: Dynamic exit code cannot be larger than 127 ## Source ```basic -END 128 +i = 128 +END i ``` ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 2 # 0:0 0001: LOADI R64, 128 # 1:5 -0002: END R64 # 1:1 -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0002: MOVE R65, R64 # 2:5 +0003: END R65 # 2:1 +0004: LOADI R65, 0 # 0:0 +0005: END R65 # 0:0 ``` -## Exit code +## Runtime errors ```plain -128 +2:1: Exit code must be in the 0..127 range ``` # Test: END exits from inside FOR loop diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index b5bcd298..498ea638 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -346,7 +346,7 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let mut stop: Option> = None; while stop.is_none() { match vm.exec() { - StopReason::End(code) => stop = Some(Ok(code)), + StopReason::End(code) => stop = Some(Ok(code.to_i32())), StopReason::Upcall(handle) => { if let Err(e) = handle.invoke().await { stop = Some(Err(e.to_string())); From c9c281a07e3068af96ace674982d7d34cb849638 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 5 Mar 2026 19:36:14 -0800 Subject: [PATCH 33/53] core2: Fix function call frame setup for recursion Reserve a temporary result slot before compiling function call arguments so the CALL base register always precedes the argument area. Add regression coverage for mutual recursion and mixed FUNCTION/SUB recursion, and regenerate the affected golden test files. --- core2/src/compiler/exprs.rs | 19 +- core2/src/compiler/syms.rs | 25 +- core2/tests/test_arrays.md | 59 ++-- core2/tests/test_case_insensitivity.md | 27 +- core2/tests/test_functions.md | 416 ++++++++++++++++--------- core2/tests/test_on_error.md | 218 ++++++------- core2/tests/test_subs.md | 81 +++++ 7 files changed, 533 insertions(+), 312 deletions(-) diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 24439514..c40b4535 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -610,26 +610,19 @@ pub(super) fn compile_expr( let key_pos = span.vref_pos; if let Some(md) = symtable.get_callable(&key) { + let md = md.clone(); + let Some(etype) = md.return_type() else { return Err(Error::NotAFunction(span.vref_pos, span.vref)); }; if md.is_argless() { - return Err(Error::CallableSyntax(span.vref_pos, md.clone())); + return Err(Error::CallableSyntax(span.vref_pos, md)); } let is_user_defined = md.is_user_defined(); - let (is_global, _index) = reg.to_parts(); - let mut alloc = symtable.temp_scope(); - let ret_reg = if is_global { - // The call instruction can only carry one register, and this register - // indicates where to store the result and where arguments start. So, - // if we are going to save the result to a global register, we must - // allocate a temp register first so that argument passing can work. - alloc.alloc().map_err(|e| Error::from_syms(e, key_pos))? - } else { - reg - }; + let mut call_scope = symtable.temp_scope(); + let ret_reg = call_scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?; let (_first_temp, arg_linecols) = compile_args(span, md.clone(), symtable, codegen)?; @@ -642,7 +635,7 @@ pub(super) fn compile_expr( let addr = codegen.emit(bytecode::make_upcall(upcall, ret_reg), key_pos); codegen.set_arg_linecols(addr, arg_linecols); } - if is_global { + if reg != ret_reg { codegen.emit(bytecode::make_move(reg, ret_reg), key_pos); } diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 386c79c5..2a8da242 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -424,6 +424,7 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, let nlocals = u8::try_from(self.symtable.locals.len()) .expect("Cannot have allocated more locals than u8"); TempScope { + base_temp: *self.next_temp.borrow(), nlocals, ntemps: 0, next_temp: self.next_temp.clone(), @@ -436,6 +437,9 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, /// /// Temporaries are allocated on demand and are cleaned up when the scope is dropped. pub(crate) struct TempScope { + /// Number of temporary registers that were already active on scope creation. + base_temp: u8, + /// Number of local variables in the enclosing scope, used as the base for temporary registers. nlocals: u8, @@ -460,7 +464,9 @@ impl Drop for TempScope { impl TempScope { /// Returns the first register available for this scope. pub(crate) fn first(&mut self) -> Result { - Register::local(self.nlocals).map_err(|_| Error::OutOfRegisters(RegisterScope::Temp)) + let reg = u8::try_from(usize::from(self.nlocals) + usize::from(self.base_temp)) + .map_err(|_| Error::OutOfRegisters(RegisterScope::Temp))?; + Register::local(reg).map_err(|_| Error::OutOfRegisters(RegisterScope::Temp)) } /// Allocates a new temporary register. @@ -823,6 +829,23 @@ mod tests { Ok(()) } + #[test] + fn test_temp_scope_first_with_outer_allocation() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; + { + let temp = local.frozen(); + let mut outer = temp.temp_scope(); + assert_eq!(Register::local(1).unwrap(), outer.alloc()?); + + let mut inner = temp.temp_scope(); + assert_eq!(Register::local(2).unwrap(), inner.first()?); + } + Ok(()) + } + #[test] fn test_temp_scope() -> Result<()> { let upcalls = HashMap::default(); diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index 173b9107..10397c46 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -735,37 +735,38 @@ OUT sum(0) ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R66, 0 # 9:9 -0002: CALL R65, 7 # 9:5, SUM -0003: LOADI R64, 258 # 9:5 -0004: UPCALL 0, R64 # 9:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0000: ENTER 4 # 0:0 +0001: LOADI R67, 0 # 9:9 +0002: CALL R66, 8 # 9:5, SUM +0003: MOVE R65, R66 # 9:5 +0004: LOADI R64, 258 # 9:5 +0005: UPCALL 0, R64 # 9:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 -- SUM -0007: LOADI R64, 0 # 1:10 -0008: ENTER 5 # 0:0 -0009: LOADI R67, 3 # 2:11 -0010: ALLOCA R66, [1]%, R67 # 2:9 -0011: LOADI R67, 10 # 3:12 -0012: LOADI R68, 0 # 3:7 -0013: STOREA R66, R67, R68 # 3:5 -0014: LOADI R67, 20 # 4:12 -0015: LOADI R68, 1 # 4:7 -0016: STOREA R66, R67, R68 # 4:5 -0017: LOADI R67, 30 # 5:12 -0018: LOADI R68, 2 # 5:7 -0019: STOREA R66, R67, R68 # 5:5 -0020: LOADI R67, 0 # 6:13 -0021: LOADA R64, R66, R67 # 6:11 -0022: LOADI R68, 1 # 6:20 -0023: LOADA R67, R66, R68 # 6:18 -0024: ADDI R64, R64, R67 # 6:16 -0025: LOADI R68, 2 # 6:27 -0026: LOADA R67, R66, R68 # 6:25 -0027: ADDI R64, R64, R67 # 6:23 -0028: RETURN # 7:1 +0008: LOADI R64, 0 # 1:10 +0009: ENTER 5 # 0:0 +0010: LOADI R67, 3 # 2:11 +0011: ALLOCA R66, [1]%, R67 # 2:9 +0012: LOADI R67, 10 # 3:12 +0013: LOADI R68, 0 # 3:7 +0014: STOREA R66, R67, R68 # 3:5 +0015: LOADI R67, 20 # 4:12 +0016: LOADI R68, 1 # 4:7 +0017: STOREA R66, R67, R68 # 4:5 +0018: LOADI R67, 30 # 5:12 +0019: LOADI R68, 2 # 5:7 +0020: STOREA R66, R67, R68 # 5:5 +0021: LOADI R67, 0 # 6:13 +0022: LOADA R64, R66, R67 # 6:11 +0023: LOADI R68, 1 # 6:20 +0024: LOADA R67, R66, R68 # 6:18 +0025: ADDI R64, R64, R67 # 6:16 +0026: LOADI R68, 2 # 6:27 +0027: LOADA R67, R66, R68 # 6:25 +0028: ADDI R64, R64, R67 # 6:23 +0029: RETURN # 7:1 ``` ## Output diff --git a/core2/tests/test_case_insensitivity.md b/core2/tests/test_case_insensitivity.md index 862aaaf4..d3103410 100644 --- a/core2/tests/test_case_insensitivity.md +++ b/core2/tests/test_case_insensitivity.md @@ -284,21 +284,22 @@ OUT foo(5) ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R66, 5 # 5:9 -0002: CALL R65, 7 # 5:5, FOO -0003: LOADI R64, 258 # 5:5 -0004: UPCALL 0, R64 # 5:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0000: ENTER 4 # 0:0 +0001: LOADI R67, 5 # 5:9 +0002: CALL R66, 8 # 5:5, FOO +0003: MOVE R65, R66 # 5:5 +0004: LOADI R64, 258 # 5:5 +0005: UPCALL 0, R64 # 5:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 -- FOO -0007: LOADI R64, 0 # 1:10 -0008: ENTER 3 # 0:0 -0009: MOVE R64, R65 # 2:11 -0010: LOADI R66, 1 # 2:15 -0011: ADDI R64, R64, R66 # 2:13 -0012: RETURN # 3:1 +0008: LOADI R64, 0 # 1:10 +0009: ENTER 3 # 0:0 +0010: MOVE R64, R65 # 2:11 +0011: LOADI R66, 1 # 2:15 +0012: ADDI R64, R64, R66 # 2:13 +0013: RETURN # 3:1 ``` ## Output diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 64f83abe..09fb9ee4 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -366,26 +366,28 @@ OUT add(3, 5) + add(10, 20) ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R66, 3 # 5:9 -0002: LOADI R67, 5 # 5:12 -0003: CALL R65, 12 # 5:5, ADD -0004: LOADI R67, 10 # 5:21 -0005: LOADI R68, 20 # 5:25 -0006: CALL R66, 12 # 5:17, ADD -0007: ADDI R65, R65, R66 # 5:15 -0008: LOADI R64, 258 # 5:5 -0009: UPCALL 0, R64 # 5:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0000: ENTER 6 # 0:0 +0001: LOADI R67, 3 # 5:9 +0002: LOADI R68, 5 # 5:12 +0003: CALL R66, 14 # 5:5, ADD +0004: MOVE R65, R66 # 5:5 +0005: LOADI R68, 10 # 5:21 +0006: LOADI R69, 20 # 5:25 +0007: CALL R67, 14 # 5:17, ADD +0008: MOVE R66, R67 # 5:17 +0009: ADDI R65, R65, R66 # 5:15 +0010: LOADI R64, 258 # 5:5 +0011: UPCALL 0, R64 # 5:1, OUT +0012: LOADI R64, 0 # 0:0 +0013: END R64 # 0:0 -- ADD -0012: LOADI R64, 0 # 1:10 -0013: ENTER 4 # 0:0 -0014: MOVE R64, R65 # 2:11 -0015: MOVE R67, R66 # 2:15 -0016: ADDI R64, R64, R67 # 2:13 -0017: RETURN # 3:1 +0014: LOADI R64, 0 # 1:10 +0015: ENTER 4 # 0:0 +0016: MOVE R64, R65 # 2:11 +0017: MOVE R67, R66 # 2:15 +0018: ADDI R64, R64, R67 # 2:13 +0019: RETURN # 3:1 ``` ## Output @@ -462,37 +464,39 @@ OUT s ## Disassembly ```asm -0000: ENTER 5 # 0:0 +0000: ENTER 6 # 0:0 0001: LOADI R64, 5 # 9:5 -0002: MOVE R67, R64 # 10:20 -0003: CALL R66, 19 # 10:5, CHANGE_INTEGER -0004: LOADI R65, 258 # 10:5 -0005: UPCALL 0, R65 # 10:1, OUT -0006: MOVE R66, R64 # 11:5 -0007: LOADI R65, 258 # 11:5 -0008: UPCALL 0, R65 # 11:1, OUT -0009: LOADI R65, 0 # 13:5 -0010: MOVE R68, R65 # 14:19 -0011: CALL R67, 23 # 14:5, CHANGE_STRING -0012: LOADI R66, 258 # 14:5 -0013: UPCALL 0, R66 # 14:1, OUT -0014: MOVE R67, R65 # 15:5 -0015: LOADI R66, 259 # 15:5 -0016: UPCALL 0, R66 # 15:1, OUT -0017: LOADI R66, 0 # 0:0 -0018: END R66 # 0:0 +0002: MOVE R68, R64 # 10:20 +0003: CALL R67, 21 # 10:5, CHANGE_INTEGER +0004: MOVE R66, R67 # 10:5 +0005: LOADI R65, 258 # 10:5 +0006: UPCALL 0, R65 # 10:1, OUT +0007: MOVE R66, R64 # 11:5 +0008: LOADI R65, 258 # 11:5 +0009: UPCALL 0, R65 # 11:1, OUT +0010: LOADI R65, 0 # 13:5 +0011: MOVE R69, R65 # 14:19 +0012: CALL R68, 25 # 14:5, CHANGE_STRING +0013: MOVE R67, R68 # 14:5 +0014: LOADI R66, 258 # 14:5 +0015: UPCALL 0, R66 # 14:1, OUT +0016: MOVE R67, R65 # 15:5 +0017: LOADI R66, 259 # 15:5 +0018: UPCALL 0, R66 # 15:1, OUT +0019: LOADI R66, 0 # 0:0 +0020: END R66 # 0:0 -- CHANGE_INTEGER -0019: LOADI R64, 0 # 1:10 -0020: ENTER 2 # 0:0 -0021: LOADI R65, 3 # 2:9 -0022: RETURN # 3:1 +0021: LOADI R64, 0 # 1:10 +0022: ENTER 2 # 0:0 +0023: LOADI R65, 3 # 2:9 +0024: RETURN # 3:1 -- CHANGE_STRING -0023: LOADI R64, 0 # 5:10 -0024: ENTER 2 # 0:0 -0025: LOADI R65, 1 # 6:9 -0026: RETURN # 7:1 +0025: LOADI R64, 0 # 5:10 +0026: ENTER 2 # 0:0 +0027: LOADI R65, 1 # 6:9 +0028: RETURN # 7:1 ``` ## Output @@ -515,18 +519,19 @@ OUT SUM_DOUBLES(3.4, 2, 7.1) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADC R67, 0 # 1:17 -0002: LOADI R66, 289 # 1:17 -0003: LOADI R69, 2 # 1:22 -0004: LOADI R68, 290 # 1:22 -0005: LOADC R71, 1 # 1:25 -0006: LOADI R70, 257 # 1:25 -0007: UPCALL 0, R65 # 1:5, SUM_DOUBLES -0008: LOADI R64, 257 # 1:5 -0009: UPCALL 1, R64 # 1:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0000: ENTER 9 # 0:0 +0001: LOADC R68, 0 # 1:17 +0002: LOADI R67, 289 # 1:17 +0003: LOADI R70, 2 # 1:22 +0004: LOADI R69, 290 # 1:22 +0005: LOADC R72, 1 # 1:25 +0006: LOADI R71, 257 # 1:25 +0007: UPCALL 0, R66 # 1:5, SUM_DOUBLES +0008: MOVE R65, R66 # 1:5 +0009: LOADI R64, 257 # 1:5 +0010: UPCALL 1, R64 # 1:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 ``` ## Output @@ -546,18 +551,19 @@ OUT SUM_INTEGERS(3, 2, 7) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADI R67, 3 # 1:18 -0002: LOADI R66, 290 # 1:18 -0003: LOADI R69, 2 # 1:21 -0004: LOADI R68, 290 # 1:21 -0005: LOADI R71, 7 # 1:24 -0006: LOADI R70, 258 # 1:24 -0007: UPCALL 0, R65 # 1:5, SUM_INTEGERS -0008: LOADI R64, 258 # 1:5 -0009: UPCALL 1, R64 # 1:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0000: ENTER 9 # 0:0 +0001: LOADI R68, 3 # 1:18 +0002: LOADI R67, 290 # 1:18 +0003: LOADI R70, 2 # 1:21 +0004: LOADI R69, 290 # 1:21 +0005: LOADI R72, 7 # 1:24 +0006: LOADI R71, 258 # 1:24 +0007: UPCALL 0, R66 # 1:5, SUM_INTEGERS +0008: MOVE R65, R66 # 1:5 +0009: LOADI R64, 258 # 1:5 +0010: UPCALL 1, R64 # 1:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 ``` ## Output @@ -577,18 +583,19 @@ OUT CONCAT$("hello", " ", "world") ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADI R67, 0 # 1:13 -0002: LOADI R66, 291 # 1:13 -0003: LOADI R69, 1 # 1:22 -0004: LOADI R68, 291 # 1:22 -0005: LOADI R71, 2 # 1:27 -0006: LOADI R70, 259 # 1:27 -0007: UPCALL 0, R65 # 1:5, CONCAT -0008: LOADI R64, 259 # 1:5 -0009: UPCALL 1, R64 # 1:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0000: ENTER 9 # 0:0 +0001: LOADI R68, 0 # 1:13 +0002: LOADI R67, 291 # 1:13 +0003: LOADI R70, 1 # 1:22 +0004: LOADI R69, 291 # 1:22 +0005: LOADI R72, 2 # 1:27 +0006: LOADI R71, 259 # 1:27 +0007: UPCALL 0, R66 # 1:5, CONCAT +0008: MOVE R65, R66 # 1:5 +0009: LOADI R64, 259 # 1:5 +0010: UPCALL 1, R64 # 1:1, OUT +0011: LOADI R64, 0 # 0:0 +0012: END R64 # 0:0 ``` ## Output @@ -608,13 +615,14 @@ OUT IS_POSITIVE?(42) ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R66, 42 # 1:18 -0002: UPCALL 0, R65 # 1:5, IS_POSITIVE -0003: LOADI R64, 256 # 1:5 -0004: UPCALL 1, R64 # 1:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0000: ENTER 4 # 0:0 +0001: LOADI R67, 42 # 1:18 +0002: UPCALL 0, R66 # 1:5, IS_POSITIVE +0003: MOVE R65, R66 # 1:5 +0004: LOADI R64, 256 # 1:5 +0005: UPCALL 1, R64 # 1:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 ``` ## Output @@ -723,22 +731,24 @@ OUT SUM_DOUBLES(1.0, 2.0) + SUM_DOUBLES(3.0, 4.0) ## Disassembly ```asm -0000: ENTER 7 # 0:0 -0001: LOADC R67, 0 # 1:17 -0002: LOADI R66, 289 # 1:17 -0003: LOADC R69, 1 # 1:22 -0004: LOADI R68, 257 # 1:22 -0005: UPCALL 0, R65 # 1:5, SUM_DOUBLES -0006: LOADC R68, 2 # 1:41 -0007: LOADI R67, 289 # 1:41 -0008: LOADC R70, 3 # 1:46 -0009: LOADI R69, 257 # 1:46 -0010: UPCALL 0, R66 # 1:29, SUM_DOUBLES -0011: ADDD R65, R65, R66 # 1:27 -0012: LOADI R64, 257 # 1:5 -0013: UPCALL 1, R64 # 1:1, OUT -0014: LOADI R64, 0 # 0:0 -0015: END R64 # 0:0 +0000: ENTER 8 # 0:0 +0001: LOADC R68, 0 # 1:17 +0002: LOADI R67, 289 # 1:17 +0003: LOADC R70, 1 # 1:22 +0004: LOADI R69, 257 # 1:22 +0005: UPCALL 0, R66 # 1:5, SUM_DOUBLES +0006: MOVE R65, R66 # 1:5 +0007: LOADC R69, 2 # 1:41 +0008: LOADI R68, 289 # 1:41 +0009: LOADC R71, 3 # 1:46 +0010: LOADI R70, 257 # 1:46 +0011: UPCALL 0, R67 # 1:29, SUM_DOUBLES +0012: MOVE R66, R67 # 1:29 +0013: ADDD R65, R65, R66 # 1:27 +0014: LOADI R64, 257 # 1:5 +0015: UPCALL 1, R64 # 1:1, OUT +0016: LOADI R64, 0 # 0:0 +0017: END R64 # 0:0 ``` ## Output @@ -840,34 +850,35 @@ NEXT ## Disassembly ```asm -0000: ENTER 4 # 0:0 +0000: ENTER 5 # 0:0 0001: LOADI R64, 0 # 7:9 0002: MOVE R65, R64 # 7:5 0003: LOADI R66, 5 # 7:14 0004: CMPLEI R65, R65, R66 # 7:11 -0005: JMPF R65, 14 # 7:5 -0006: MOVE R67, R64 # 8:20 -0007: CALL R66, 16 # 8:9, MAYBE_EXIT -0008: LOADI R65, 258 # 8:9 -0009: UPCALL 0, R65 # 8:5, OUT -0010: MOVE R64, R64 # 7:5 -0011: LOADI R65, 1 # 7:15 -0012: ADDI R64, R64, R65 # 7:11 -0013: JUMP 2 # 7:5 -0014: LOADI R65, 0 # 0:0 -0015: END R65 # 0:0 +0005: JMPF R65, 15 # 7:5 +0006: MOVE R68, R64 # 8:20 +0007: CALL R67, 17 # 8:9, MAYBE_EXIT +0008: MOVE R66, R67 # 8:9 +0009: LOADI R65, 258 # 8:9 +0010: UPCALL 0, R65 # 8:5, OUT +0011: MOVE R64, R64 # 7:5 +0012: LOADI R65, 1 # 7:15 +0013: ADDI R64, R64, R65 # 7:11 +0014: JUMP 2 # 7:5 +0015: LOADI R65, 0 # 0:0 +0016: END R65 # 0:0 -- MAYBE_EXIT -0016: LOADI R64, 0 # 1:10 -0017: ENTER 4 # 0:0 -0018: LOADI R64, 1 # 2:18 -0019: MOVE R66, R65 # 3:8 -0020: LOADI R67, 2 # 3:12 -0021: CMPGTI R66, R66, R67 # 3:10 -0022: JMPF R66, 24 # 3:8 -0023: JUMP 25 # 3:19 -0024: LOADI R64, 2 # 4:18 -0025: RETURN # 5:1 +0017: LOADI R64, 0 # 1:10 +0018: ENTER 4 # 0:0 +0019: LOADI R64, 1 # 2:18 +0020: MOVE R66, R65 # 3:8 +0021: LOADI R67, 2 # 3:12 +0022: CMPGTI R66, R66, R67 # 3:10 +0023: JMPF R66, 25 # 3:8 +0024: JUMP 26 # 3:19 +0025: LOADI R64, 2 # 4:18 +0026: RETURN # 5:1 ``` ## Output @@ -929,38 +940,40 @@ OUT calls; factorial(5) ## Disassembly ```asm -0000: ENTER 5 # 0:0 +0000: ENTER 6 # 0:0 0001: LOADI R0, 0 # 1:12 0002: MOVE R65, R0 # 6:5 0003: LOADI R64, 274 # 6:5 -0004: LOADI R68, 5 # 6:22 -0005: CALL R67, 10 # 6:12, FACTORIAL -0006: LOADI R66, 258 # 6:12 -0007: UPCALL 0, R64 # 6:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0004: LOADI R69, 5 # 6:22 +0005: CALL R68, 11 # 6:12, FACTORIAL +0006: MOVE R67, R68 # 6:12 +0007: LOADI R66, 258 # 6:12 +0008: UPCALL 0, R64 # 6:1, OUT +0009: LOADI R64, 0 # 0:0 +0010: END R64 # 0:0 -- FACTORIAL -0010: LOADI R64, 0 # 2:10 -0011: ENTER 5 # 0:0 -0012: MOVE R66, R65 # 3:8 -0013: LOADI R67, 1 # 3:12 -0014: CMPEQI R66, R66, R67 # 3:10 -0015: JMPF R66, 18 # 3:8 -0016: LOADI R64, 1 # 3:31 -0017: JUMP 26 # 3:8 -0018: LOADI R66, 1 # 3:33 -0019: JMPF R66, 26 # 3:33 -0020: MOVE R64, R65 # 3:50 -0021: MOVE R67, R65 # 3:64 -0022: LOADI R68, 1 # 3:68 -0023: SUBI R67, R67, R68 # 3:66 -0024: CALL R66, 10 # 3:54, FACTORIAL -0025: MULI R64, R64, R66 # 3:52 -0026: MOVE R0, R0 # 4:13 -0027: LOADI R66, 1 # 4:21 -0028: ADDI R0, R0, R66 # 4:19 -0029: RETURN # 5:1 +0011: LOADI R64, 0 # 2:10 +0012: ENTER 6 # 0:0 +0013: MOVE R66, R65 # 3:8 +0014: LOADI R67, 1 # 3:12 +0015: CMPEQI R66, R66, R67 # 3:10 +0016: JMPF R66, 19 # 3:8 +0017: LOADI R64, 1 # 3:31 +0018: JUMP 28 # 3:8 +0019: LOADI R66, 1 # 3:33 +0020: JMPF R66, 28 # 3:33 +0021: MOVE R64, R65 # 3:50 +0022: MOVE R68, R65 # 3:64 +0023: LOADI R69, 1 # 3:68 +0024: SUBI R68, R68, R69 # 3:66 +0025: CALL R67, 11 # 3:54, FACTORIAL +0026: MOVE R66, R67 # 3:54 +0027: MULI R64, R64, R66 # 3:52 +0028: MOVE R0, R0 # 4:13 +0029: LOADI R66, 1 # 4:21 +0030: ADDI R0, R0, R66 # 4:19 +0031: RETURN # 5:1 ``` ## Output @@ -969,6 +982,105 @@ OUT calls; factorial(5) 0=0% ; 1=120% ``` +# Test: Mutually recursive functions + +## Source + +```basic +FUNCTION ping(n%) + OUT "ping"; n + IF n = 0 THEN + ping = 100 + ELSE + ping = pong(n - 1) + 1 + END IF +END FUNCTION + +FUNCTION pong(n%) + OUT "pong"; n + IF n = 0 THEN + pong = 200 + ELSE + pong = ping(n - 1) + 10 + END IF +END FUNCTION + +OUT ping(3) +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R67, 3 # 19:10 +0002: CALL R66, 8 # 19:5, PING +0003: MOVE R65, R66 # 19:5 +0004: LOADI R64, 258 # 19:5 +0005: UPCALL 0, R64 # 19:1, OUT +0006: LOADI R64, 0 # 0:0 +0007: END R64 # 0:0 + +-- PING +0008: LOADI R64, 0 # 1:10 +0009: ENTER 6 # 0:0 +0010: LOADI R67, 0 # 2:9 +0011: LOADI R66, 275 # 2:9 +0012: MOVE R69, R65 # 2:17 +0013: LOADI R68, 258 # 2:17 +0014: UPCALL 0, R66 # 2:5, OUT +0015: MOVE R66, R65 # 3:8 +0016: LOADI R67, 0 # 3:12 +0017: CMPEQI R66, R66, R67 # 3:10 +0018: JMPF R66, 21 # 3:8 +0019: LOADI R64, 100 # 4:16 +0020: JUMP 30 # 3:8 +0021: LOADI R66, 1 # 5:5 +0022: JMPF R66, 30 # 5:5 +0023: MOVE R67, R65 # 6:21 +0024: LOADI R68, 1 # 6:25 +0025: SUBI R67, R67, R68 # 6:23 +0026: CALL R66, 31 # 6:16, PONG +0027: MOVE R64, R66 # 6:16 +0028: LOADI R66, 1 # 6:30 +0029: ADDI R64, R64, R66 # 6:28 +0030: RETURN # 8:1 + +-- PONG +0031: LOADI R64, 0 # 10:10 +0032: ENTER 6 # 0:0 +0033: LOADI R67, 1 # 11:9 +0034: LOADI R66, 275 # 11:9 +0035: MOVE R69, R65 # 11:17 +0036: LOADI R68, 258 # 11:17 +0037: UPCALL 0, R66 # 11:5, OUT +0038: MOVE R66, R65 # 12:8 +0039: LOADI R67, 0 # 12:12 +0040: CMPEQI R66, R66, R67 # 12:10 +0041: JMPF R66, 44 # 12:8 +0042: LOADI R64, 200 # 13:16 +0043: JUMP 53 # 12:8 +0044: LOADI R66, 1 # 14:5 +0045: JMPF R66, 53 # 14:5 +0046: MOVE R67, R65 # 15:21 +0047: LOADI R68, 1 # 15:25 +0048: SUBI R67, R67, R68 # 15:23 +0049: CALL R66, 8 # 15:16, PING +0050: MOVE R64, R66 # 15:16 +0051: LOADI R66, 10 # 15:30 +0052: ADDI R64, R64, R66 # 15:28 +0053: RETURN # 17:1 +``` + +## Output + +```plain +0=ping$ ; 1=3% +0=pong$ ; 1=2% +0=ping$ ; 1=1% +0=pong$ ; 1=0% +0=212% +``` + # Test: Calling a function as a command is an error ## Source diff --git a/core2/tests/test_on_error.md b/core2/tests/test_on_error.md index 1c9ff548..28161973 100644 --- a/core2/tests/test_on_error.md +++ b/core2/tests/test_on_error.md @@ -13,23 +13,24 @@ OUT 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: SETEH JUMP, 12 # 1:1 +0000: ENTER 4 # 0:0 +0001: SETEH JUMP, 13 # 1:1 0002: LOADI R65, 1 # 2:5 0003: LOADI R64, 258 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R66, 0 # 3:12 -0006: UPCALL 1, R65 # 3:5, RAISEF -0007: LOADI R64, 256 # 3:5 -0008: UPCALL 0, R64 # 3:1, OUT -0009: LOADI R65, 2 # 4:5 -0010: LOADI R64, 258 # 4:5 -0011: UPCALL 0, R64 # 4:1, OUT -0012: UPCALL 2, R65 # 5:9, LAST_ERROR -0013: LOADI R64, 259 # 5:9 -0014: UPCALL 0, R64 # 5:5, OUT -0015: LOADI R64, 0 # 0:0 -0016: END R64 # 0:0 +0005: LOADI R67, 0 # 3:12 +0006: UPCALL 1, R66 # 3:5, RAISEF +0007: MOVE R65, R66 # 3:5 +0008: LOADI R64, 256 # 3:5 +0009: UPCALL 0, R64 # 3:1, OUT +0010: LOADI R65, 2 # 4:5 +0011: LOADI R64, 258 # 4:5 +0012: UPCALL 0, R64 # 4:1, OUT +0013: UPCALL 2, R65 # 5:9, LAST_ERROR +0014: LOADI R64, 259 # 5:9 +0015: UPCALL 0, R64 # 5:5, OUT +0016: LOADI R64, 0 # 0:0 +0017: END R64 # 0:0 ``` ## Output @@ -55,23 +56,24 @@ OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: SETEH JUMP, 12 # 1:1 +0000: ENTER 4 # 0:0 +0001: SETEH JUMP, 13 # 1:1 0002: LOADI R65, 1 # 2:5 0003: LOADI R64, 258 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R66, 0 # 3:12 -0006: UPCALL 1, R65 # 3:5, RAISEF -0007: LOADI R64, 256 # 3:5 -0008: UPCALL 0, R64 # 3:1, OUT -0009: LOADI R65, 2 # 4:5 -0010: LOADI R64, 258 # 4:5 -0011: UPCALL 0, R64 # 4:1, OUT -0012: UPCALL 2, R65 # 6:5, LAST_ERROR -0013: LOADI R64, 259 # 6:5 -0014: UPCALL 0, R64 # 6:1, OUT -0015: LOADI R64, 0 # 0:0 -0016: END R64 # 0:0 +0005: LOADI R67, 0 # 3:12 +0006: UPCALL 1, R66 # 3:5, RAISEF +0007: MOVE R65, R66 # 3:5 +0008: LOADI R64, 256 # 3:5 +0009: UPCALL 0, R64 # 3:1, OUT +0010: LOADI R65, 2 # 4:5 +0011: LOADI R64, 258 # 4:5 +0012: UPCALL 0, R64 # 4:1, OUT +0013: UPCALL 2, R65 # 6:5, LAST_ERROR +0014: LOADI R64, 259 # 6:5 +0015: UPCALL 0, R64 # 6:1, OUT +0016: LOADI R64, 0 # 0:0 +0017: END R64 # 0:0 ``` ## Output @@ -98,25 +100,27 @@ OUT RAISEF("internal") ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: SETEH JUMP, 9 # 1:1 +0000: ENTER 4 # 0:0 +0001: SETEH JUMP, 10 # 1:1 0002: LOADI R65, 1 # 2:5 0003: LOADI R64, 258 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R66, 0 # 3:12 -0006: UPCALL 1, R65 # 3:5, RAISEF -0007: LOADI R64, 256 # 3:5 -0008: UPCALL 0, R64 # 3:1, OUT -0009: SETEH NONE, 0 # 5:1 -0010: LOADI R65, 2 # 6:5 -0011: LOADI R64, 258 # 6:5 -0012: UPCALL 0, R64 # 6:1, OUT -0013: LOADI R66, 0 # 7:12 -0014: UPCALL 1, R65 # 7:5, RAISEF -0015: LOADI R64, 256 # 7:5 -0016: UPCALL 0, R64 # 7:1, OUT -0017: LOADI R64, 0 # 0:0 -0018: END R64 # 0:0 +0005: LOADI R67, 0 # 3:12 +0006: UPCALL 1, R66 # 3:5, RAISEF +0007: MOVE R65, R66 # 3:5 +0008: LOADI R64, 256 # 3:5 +0009: UPCALL 0, R64 # 3:1, OUT +0010: SETEH NONE, 0 # 5:1 +0011: LOADI R65, 2 # 6:5 +0012: LOADI R64, 258 # 6:5 +0013: UPCALL 0, R64 # 6:1, OUT +0014: LOADI R67, 0 # 7:12 +0015: UPCALL 1, R66 # 7:5, RAISEF +0016: MOVE R65, R66 # 7:5 +0017: LOADI R64, 256 # 7:5 +0018: UPCALL 0, R64 # 7:1, OUT +0019: LOADI R64, 0 # 0:0 +0020: END R64 # 0:0 ``` ## Runtime errors @@ -146,20 +150,21 @@ OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 0002: LOADI R65, 1 # 2:5 0003: LOADI R64, 258 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R66, 0 # 3:12 -0006: UPCALL 1, R65 # 3:5, RAISEF -0007: LOADI R64, 256 # 3:5 -0008: UPCALL 0, R64 # 3:1, OUT -0009: UPCALL 2, R65 # 4:5, LAST_ERROR -0010: LOADI R64, 259 # 4:5 -0011: UPCALL 0, R64 # 4:1, OUT -0012: LOADI R64, 0 # 0:0 -0013: END R64 # 0:0 +0005: LOADI R67, 0 # 3:12 +0006: UPCALL 1, R66 # 3:5, RAISEF +0007: MOVE R65, R66 # 3:5 +0008: LOADI R64, 256 # 3:5 +0009: UPCALL 0, R64 # 3:1, OUT +0010: UPCALL 2, R65 # 4:5, LAST_ERROR +0011: LOADI R64, 259 # 4:5 +0012: UPCALL 0, R64 # 4:1, OUT +0013: LOADI R64, 0 # 0:0 +0014: END R64 # 0:0 ``` ## Output @@ -216,20 +221,21 @@ OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 0002: LOADI R65, 1 # 2:5 0003: LOADI R64, 258 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R66, 0 # 2:19 -0006: UPCALL 1, R65 # 2:12, RAISEF -0007: LOADI R64, 256 # 2:12 -0008: UPCALL 0, R64 # 2:8, OUT -0009: UPCALL 2, R65 # 2:36, LAST_ERROR -0010: LOADI R64, 259 # 2:36 -0011: UPCALL 0, R64 # 2:32, OUT -0012: LOADI R64, 0 # 0:0 -0013: END R64 # 0:0 +0005: LOADI R67, 0 # 2:19 +0006: UPCALL 1, R66 # 2:12, RAISEF +0007: MOVE R65, R66 # 2:12 +0008: LOADI R64, 256 # 2:12 +0009: UPCALL 0, R64 # 2:8, OUT +0010: UPCALL 2, R65 # 2:36, LAST_ERROR +0011: LOADI R64, 259 # 2:36 +0012: UPCALL 0, R64 # 2:32, OUT +0013: LOADI R64, 0 # 0:0 +0014: END R64 # 0:0 ``` ## Output @@ -283,17 +289,18 @@ ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R66, 0 # 1:34 -0003: UPCALL 0, R65 # 1:27, RAISEF -0004: LOADI R64, 256 # 1:27 -0005: UPCALL 1, R64 # 1:23, OUT -0006: UPCALL 2, R65 # 1:51, LAST_ERROR -0007: LOADI R64, 259 # 1:51 -0008: UPCALL 1, R64 # 1:47, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0002: LOADI R67, 0 # 1:34 +0003: UPCALL 0, R66 # 1:27, RAISEF +0004: MOVE R65, R66 # 1:27 +0005: LOADI R64, 256 # 1:27 +0006: UPCALL 1, R64 # 1:23, OUT +0007: UPCALL 2, R65 # 1:51, LAST_ERROR +0008: LOADI R64, 259 # 1:51 +0009: UPCALL 1, R64 # 1:47, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 ``` ## Output @@ -313,17 +320,18 @@ ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R66, 0 # 1:34 -0003: UPCALL 0, R65 # 1:27, RAISEF -0004: LOADI R64, 256 # 1:27 -0005: UPCALL 1, R64 # 1:23, OUT -0006: UPCALL 2, R65 # 1:47, LAST_ERROR -0007: LOADI R64, 259 # 1:47 -0008: UPCALL 1, R64 # 1:43, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0002: LOADI R67, 0 # 1:34 +0003: UPCALL 0, R66 # 1:27, RAISEF +0004: MOVE R65, R66 # 1:27 +0005: LOADI R64, 256 # 1:27 +0006: UPCALL 1, R64 # 1:23, OUT +0007: UPCALL 2, R65 # 1:47, LAST_ERROR +0008: LOADI R64, 259 # 1:47 +0009: UPCALL 1, R64 # 1:43, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 ``` ## Output @@ -343,17 +351,18 @@ ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R66, 0 # 1:34 -0003: UPCALL 0, R65 # 1:27, RAISEF -0004: LOADI R64, 256 # 1:27 -0005: UPCALL 1, R64 # 1:23, OUT -0006: UPCALL 2, R65 # 1:51, LAST_ERROR -0007: LOADI R64, 259 # 1:51 -0008: UPCALL 1, R64 # 1:47, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0002: LOADI R67, 0 # 1:34 +0003: UPCALL 0, R66 # 1:27, RAISEF +0004: MOVE R65, R66 # 1:27 +0005: LOADI R64, 256 # 1:27 +0006: UPCALL 1, R64 # 1:23, OUT +0007: UPCALL 2, R65 # 1:51, LAST_ERROR +0008: LOADI R64, 259 # 1:51 +0009: UPCALL 1, R64 # 1:47, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 ``` ## Output @@ -373,17 +382,18 @@ ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 3 # 0:0 +0000: ENTER 4 # 0:0 0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R66, 0 # 1:34 -0003: UPCALL 0, R65 # 1:27, RAISEF -0004: LOADI R64, 256 # 1:27 -0005: UPCALL 1, R64 # 1:23, OUT -0006: UPCALL 2, R65 # 1:45, LAST_ERROR -0007: LOADI R64, 259 # 1:45 -0008: UPCALL 1, R64 # 1:41, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0002: LOADI R67, 0 # 1:34 +0003: UPCALL 0, R66 # 1:27, RAISEF +0004: MOVE R65, R66 # 1:27 +0005: LOADI R64, 256 # 1:27 +0006: UPCALL 1, R64 # 1:23, OUT +0007: UPCALL 2, R65 # 1:45, LAST_ERROR +0008: LOADI R64, 259 # 1:45 +0009: UPCALL 1, R64 # 1:41, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 ``` ## Output diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 47a20774..d76deac9 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -419,6 +419,87 @@ count_down "counter is" 0=counter is$ ; 1=1% ``` +# Test: Function and subroutine call one another + +## Source + +```basic +DIM SHARED value AS INTEGER + +FUNCTION count_value(n%) + value = value + 1 + IF n = 0 THEN + count_value = value + ELSE + bump_value(n - 1) + count_value = value + END IF +END FUNCTION + +SUB bump_value(n%) + value = value + 10 + value = count_value(n) +END SUB + +OUT count_value(2) +OUT value +``` + +## Disassembly + +```asm +0000: ENTER 4 # 0:0 +0001: LOADI R0, 0 # 1:12 +0002: LOADI R67, 2 # 18:17 +0003: CALL R66, 12 # 18:5, COUNT_VALUE +0004: MOVE R65, R66 # 18:5 +0005: LOADI R64, 258 # 18:5 +0006: UPCALL 0, R64 # 18:1, OUT +0007: MOVE R65, R0 # 19:5 +0008: LOADI R64, 258 # 19:5 +0009: UPCALL 0, R64 # 19:1, OUT +0010: LOADI R64, 0 # 0:0 +0011: END R64 # 0:0 + +-- COUNT_VALUE +0012: LOADI R64, 0 # 3:10 +0013: ENTER 4 # 0:0 +0014: MOVE R0, R0 # 4:13 +0015: LOADI R66, 1 # 4:21 +0016: ADDI R0, R0, R66 # 4:19 +0017: MOVE R66, R65 # 5:8 +0018: LOADI R67, 0 # 5:12 +0019: CMPEQI R66, R66, R67 # 5:10 +0020: JMPF R66, 23 # 5:8 +0021: MOVE R64, R0 # 6:23 +0022: JUMP 30 # 5:8 +0023: LOADI R66, 1 # 7:5 +0024: JMPF R66, 30 # 7:5 +0025: MOVE R66, R65 # 8:20 +0026: LOADI R67, 1 # 8:24 +0027: SUBI R66, R66, R67 # 8:22 +0028: CALL R66, 31 # 8:9, BUMP_VALUE +0029: MOVE R64, R0 # 9:23 +0030: RETURN # 11:1 + +-- BUMP_VALUE +0031: ENTER 3 # 0:0 +0032: MOVE R0, R0 # 14:13 +0033: LOADI R65, 10 # 14:21 +0034: ADDI R0, R0, R65 # 14:19 +0035: MOVE R66, R64 # 15:25 +0036: CALL R65, 12 # 15:13, COUNT_VALUE +0037: MOVE R0, R65 # 15:13 +0038: RETURN # 16:1 +``` + +## Output + +```plain +0=23% +0=23% +``` + # Test: Calling a subroutine as a function is an error ## Source From a1b7346b23956907a878e9878ad618d7fd7b7c69 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 19 Mar 2026 21:54:44 +0100 Subject: [PATCH 34/53] core2: Distinguish EOF from explicit END Add an operand-less EOF opcode and stop reason so the VM can tell natural fallthrough apart from explicit END. Emit EOF as the compiler-generated terminator and update tests and helpers to recognize it as successful completion. As part of this, we need to apply an unrelated fix to compile missing repeated-argument tags explicitly so argless upcalls no longer depend on the synthetic END 0 footer. --- core2/examples/config.rs | 1 + core2/src/bytecode.rs | 17 +- core2/src/compiler/args.rs | 73 +++++ core2/src/compiler/top.rs | 7 +- core2/src/image.rs | 10 +- core2/src/vm/context.rs | 10 + core2/src/vm/mod.rs | 17 +- core2/tests/test_args.md | 57 ++-- core2/tests/test_arithmetic_add.md | 21 +- core2/tests/test_arithmetic_div.md | 18 +- core2/tests/test_arithmetic_mod.md | 18 +- core2/tests/test_arithmetic_mul.md | 15 +- core2/tests/test_arithmetic_neg.md | 14 +- core2/tests/test_arithmetic_pow.md | 18 +- core2/tests/test_arithmetic_sub.md | 15 +- core2/tests/test_arrays.md | 115 +++---- core2/tests/test_assignments.md | 15 +- core2/tests/test_bitwise_and.md | 6 +- core2/tests/test_bitwise_not.md | 6 +- core2/tests/test_bitwise_or.md | 6 +- core2/tests/test_bitwise_shl.md | 12 +- core2/tests/test_bitwise_shr.md | 18 +- core2/tests/test_bitwise_xor.md | 6 +- core2/tests/test_case_insensitivity.md | 62 ++-- core2/tests/test_data.md | 13 +- core2/tests/test_do.md | 51 +-- core2/tests/test_empty.md | 5 +- core2/tests/test_end.md | 24 +- core2/tests/test_for.md | 36 +-- core2/tests/test_functions.md | 431 ++++++++++++------------- core2/tests/test_globals.md | 77 ++--- core2/tests/test_gosub.md | 25 +- core2/tests/test_goto.md | 18 +- core2/tests/test_if.md | 33 +- core2/tests/test_on_error.md | 33 +- core2/tests/test_relational_eq.md | 15 +- core2/tests/test_relational_ge.md | 12 +- core2/tests/test_relational_gt.md | 12 +- core2/tests/test_relational_le.md | 12 +- core2/tests/test_relational_lt.md | 12 +- core2/tests/test_relational_ne.md | 15 +- core2/tests/test_select.md | 30 +- core2/tests/test_strings.md | 3 +- core2/tests/test_subs.md | 220 ++++++------- core2/tests/test_types.md | 15 +- core2/tests/test_while.md | 9 +- core2/tests/testutils/mod.rs | 1 + 47 files changed, 751 insertions(+), 908 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 1f8cb780..43f3e93c 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -92,6 +92,7 @@ fn main() { eprintln!("Script exited with code {}", code.to_i32()); } } + StopReason::Eof => (), StopReason::Exception(pos, msg) => { eprintln!("Script raised an exception at {}: {}", pos, msg); std::process::exit(1); diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index 11059319..fc392aa0 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -627,12 +627,15 @@ pub(crate) enum Opcode { /// Subtracts two integers and stores the result into a third one. SubtractInteger, + /// Terminates execution with an explicit exit code. + End, + /// Requests the execution of an upcall, stopping VM execution. Upcall, - /// Terminates execution. + /// Terminates execution due to natural fallthrough. // KEEP THIS LAST. - End, + Eof, } #[rustfmt::skip] @@ -796,6 +799,12 @@ instr!( u8, 0x000000ff, 0, // Number of local registers to allocate. ); +#[rustfmt::skip] +instr!( + Opcode::Eof, "EOF", + make_eof, parse_eof, format_eof, +); + #[rustfmt::skip] instr!( Opcode::Gosub, "GOSUB", @@ -1168,7 +1177,7 @@ pub(crate) fn opcode_of(instr: u32) -> Opcode { #[allow(unsafe_code)] unsafe { let num = unchecked_u32_as_u8(instr >> 24); - debug_assert!(num <= Opcode::End as u8); + debug_assert!(num <= Opcode::Eof as u8); std::mem::transmute::(num) } } @@ -1420,6 +1429,8 @@ mod tests { test_instr!(test_enter, make_enter, parse_enter, 10); + test_instr!(test_eof, make_eof, parse_eof); + test_instr!(test_gosub, make_gosub, parse_gosub, 12345); test_instr!( diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 83816871..9af234d1 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -375,6 +375,13 @@ pub(super) fn compile_args( return Err(Error::CallableSyntax(key_pos, md)); } + if arg_iter.peek().is_none() { + let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, key_pos))?; + arg_linecols.push(key_pos); + let tag = bytecode::VarArgTag::Missing(ArgSep::End); + codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), key_pos); + } + while arg_iter.peek().is_some() { let ArgSpan { expr, sep, sep_pos } = arg_iter.next().expect("Args and their syntax must advance in unison"); @@ -440,3 +447,69 @@ pub(super) fn compile_args( let first_reg = scope.first().map_err(|e| Error::from_syms(e, key_pos))?; Ok((first_reg, arg_linecols)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::CallableMetadataBuilder; + use crate::callable::RepeatedSyntax; + use crate::compiler::codegen::Fixup; + use crate::compiler::syms::GlobalSymtable; + use std::borrow::Cow; + use std::collections::HashMap; + + #[test] + fn test_compile_args_materializes_missing_repeated_tag() -> Result<()> { + let pos = LineCol { line: 1, col: 1 }; + let md = CallableMetadataBuilder::new("OUT") + .with_syntax(&[( + &[], + Some(&RepeatedSyntax { + name: Cow::Borrowed("arg"), + type_syn: RepeatedTypeSyntax::AnyValue, + sep: ArgSepSyntax::Exactly(ArgSep::Short), + require_one: false, + allow_missing: false, + }), + )]) + .test_build(); + + let mut codegen = Codegen::default(); + let enter = codegen.emit(bytecode::make_nop(), LineCol { line: 0, col: 0 }); + + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(&upcalls); + let mut local = global.enter_scope(); + let (first_reg, arg_linecols) = { + let mut symtable = local.frozen(); + compile_args( + CallSpan { vref: VarRef::new("OUT", None), vref_pos: pos, args: vec![] }, + md, + &mut symtable, + &mut codegen, + )? + }; + assert_eq!(Register::local(0).unwrap(), first_reg); + assert_eq!(vec![pos], arg_linecols); + + let upcall = codegen.get_upcall(SymbolKey::from("OUT"), None, pos)?; + let addr = codegen.emit(bytecode::make_upcall(upcall, first_reg), pos); + codegen.set_arg_linecols(addr, arg_linecols); + codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); + + let nlocals = local.leave_scope().map_err(|e| Error::from_syms(e, pos))?; + codegen.add_fixup(enter, Fixup::Enter(nlocals)); + + let image = codegen.build_image(HashMap::default(), vec![])?; + assert_eq!( + vec![ + "0000: ENTER 1 # 0:0".to_owned(), + "0001: LOADI R64, 0 # 1:1".to_owned(), + "0002: UPCALL 0, R64 # 1:1, OUT".to_owned(), + "0003: EOF # 0:0".to_owned(), + ], + image.disasm() + ); + Ok(()) + } +} diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index a5bd80c3..1977851d 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -16,7 +16,7 @@ //! Entry point to the compilation, handling top-level definitions. use crate::ast::{ - ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, EndSpan, Expr, + ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, Expr, ExprType, ForSpan, IfSpan, OnErrorSpan, SelectSpan, Statement, VarRef, WhileSpan, }; use crate::bytecode::{self, ErrorHandlerMode, PackedArrayType, Register, RegisterScope}; @@ -1259,8 +1259,8 @@ pub fn compile_with_globals( prepare_globals(&mut ctx, &mut symtable, global_defs)?; - let program_end = Statement::End(EndSpan { code: None, pos: LineCol { line: 0, col: 0 } }); - compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input).chain([Ok(program_end)]))?; + compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input))?; + ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); compile_user_callables(&mut ctx, &mut symtable)?; @@ -1302,6 +1302,7 @@ mod tests { match vm.exec() { StopReason::End(code) if code.is_success() => {} StopReason::End(code) => panic!("unexpected exit code: {}", code.to_i32()), + StopReason::Eof => {} StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"), StopReason::Upcall(_) => panic!("unexpected upcall"), } diff --git a/core2/src/image.rs b/core2/src/image.rs index 831cd401..558ef04f 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -16,7 +16,7 @@ //! Compiled program representation. use crate::ast::ExprType; -use crate::bytecode::{self, Opcode, Register, opcode_of}; +use crate::bytecode::{self, Opcode, opcode_of}; use crate::compiler::SymbolKey; use crate::mem::ConstantDatum; use crate::reader::LineCol; @@ -44,6 +44,7 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::EqualDouble => bytecode::format_equal_double(instr), Opcode::EqualInteger => bytecode::format_equal_integer(instr), Opcode::EqualText => bytecode::format_equal_text(instr), + Opcode::Eof => bytecode::format_eof(instr), Opcode::End => bytecode::format_end(instr), Opcode::Enter => bytecode::format_enter(instr), Opcode::Gosub => bytecode::format_gosub(instr), @@ -163,10 +164,9 @@ impl Default for Image { fn default() -> Self { Self::new( vec![ - // The minimum valid program requires an explicit `END` so that the VM knows to - // exit. We can directly reference register 0 because all registers would have - // been cleared and accessing them would result in their default values. - bytecode::make_end(Register::global(0).expect("Global 0 register be valid")), + // The minimum valid program requires an explicit terminator so that the VM knows + // to exit. + bytecode::make_eof(), ], vec![], vec![], diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index d76b8d05..415061d6 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -33,6 +33,9 @@ pub(super) enum InternalStopReason { /// Execution terminated due to an `END` instruction. End(ExitCode), + /// Execution terminated due to natural fallthrough. + Eof, + /// Execution stopped due to an instruction-level exception. Exception(Address, String), @@ -330,6 +333,7 @@ impl Context { Opcode::EqualText => self.do_equal_text(instr, &image.constants, heap), Opcode::End => self.do_end(instr), Opcode::Enter => self.do_enter(instr), + Opcode::Eof => self.do_eof(instr), Opcode::Gosub => self.do_gosub(instr), Opcode::GreaterDouble => self.do_greater_double(instr), Opcode::GreaterEqualDouble => self.do_greater_equal_double(instr), @@ -670,6 +674,12 @@ impl Context { self.pc += 1; } + /// Implements the `Eof` opcode. + pub(super) fn do_eof(&mut self, instr: u32) { + bytecode::parse_eof(instr); + self.stop = Some(InternalStopReason::Eof); + } + /// Implements the `Gosub` opcode. pub(super) fn do_gosub(&mut self, instr: u32) { let offset = bytecode::parse_gosub(instr); diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 3e4db466..80ebed5f 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -81,6 +81,9 @@ pub enum StopReason<'a> { /// Execution terminated due to an `END` instruction. End(ExitCode), + /// Execution terminated due to natural fallthrough. + Eof, + /// Execution stopped due to an instruction-level exception. Exception(LineCol, String), @@ -303,9 +306,7 @@ impl Vm { } let Some(image) = self.image.as_ref() else { - return StopReason::End( - ExitCode::try_from(0).expect("Zero must be a valid exit code"), - ); + return StopReason::Eof; }; if self.pending_upcall.is_some() { @@ -314,6 +315,7 @@ impl Vm { match self.context.exec(image, &mut self.heap) { InternalStopReason::End(code) => return StopReason::End(code), + InternalStopReason::Eof => return StopReason::Eof, InternalStopReason::Exception(pc, e) => { if !self.handle_exception(pc, e, None) && let Some((pos, message)) = self.pending_exception.take() @@ -405,6 +407,7 @@ mod tests { loop { match vm.exec() { StopReason::End(_) => break, + StopReason::Eof => break, StopReason::Exception(_, msg) => panic!("Unexpected exception: {}", msg), StopReason::Upcall(handler) => handler.invoke().await.unwrap(), } @@ -415,7 +418,7 @@ mod tests { fn test_exec_without_load_is_eof() { let mut vm = Vm::new(HashMap::default()); match vm.exec() { - StopReason::End(code) if code.is_success() => (), + StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } } @@ -425,7 +428,7 @@ mod tests { let mut vm = Vm::new(HashMap::default()); vm.load(Image::default()); match vm.exec() { - StopReason::End(code) if code.is_success() => (), + StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } } @@ -436,7 +439,7 @@ mod tests { let image = compile(&mut b"".as_slice(), &HashMap::default()).unwrap(); vm.load(image); match vm.exec() { - StopReason::End(code) if code.is_success() => (), + StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } } @@ -472,7 +475,7 @@ mod tests { assert_eq!(["30", "20"], *data.borrow().as_slice()); match vm.exec() { - StopReason::End(code) if code.is_success() => (), + StopReason::Eof => (), _ => panic!("Fourth exec should stop at EOF"), } assert_eq!(["30", "20"], *data.borrow().as_slice()); diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index 3630ffe2..46dc5ba2 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -40,8 +40,7 @@ OUT_REQUIRED_VALUE 4 0000: ENTER 1 # 0:0 0001: LOADI R64, 4 # 1:20 0002: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` ## Output @@ -65,8 +64,7 @@ OUT_REQUIRED_VALUE 7.8 0001: LOADC R64, 0 # 1:20 0002: DTOI R64 # 1:20 0003: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 ``` ## Output @@ -89,8 +87,7 @@ OUT_OPTIONAL 0000: ENTER 1 # 0:0 0001: LOADI R64, 0 # 1:1 0002: UPCALL 0, R64 # 1:1, OUT_OPTIONAL -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` ## Output @@ -114,8 +111,7 @@ OUT_OPTIONAL "Foo" 0001: LOADI R65, 0 # 1:14 0002: LOADI R64, 259 # 1:14 0003: UPCALL 0, R64 # 1:1, OUT_OPTIONAL -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 ``` ## Output @@ -157,8 +153,7 @@ OUT_ANY_VALUE "Text" 0004: LOADI R65, 0 # 2:15 0005: LOADI R64, 259 # 2:15 0006: UPCALL 0, R64 # 2:1, OUT_ANY_VALUE -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -204,8 +199,7 @@ OUT_ANY_VALUE_OPTIONAL "Text" 0006: LOADI R65, 0 # 3:24 0007: LOADI R64, 259 # 3:24 0008: UPCALL 0, R64 # 3:1, OUT_ANY_VALUE_OPTIONAL -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -262,8 +256,7 @@ OUT_POSITIONAL ; 0 AS 8.2 0016: LOADC R67, 3 # 3:23 0017: LOADI R66, 257 # 3:23 0018: UPCALL 0, R64 # 3:1, OUT_POSITIONAL -0019: LOADI R64, 0 # 0:0 -0020: END R64 # 0:0 +0019: EOF # 0:0 ``` ## Output @@ -306,9 +299,9 @@ OUT ```asm 0000: ENTER 1 # 0:0 -0001: UPCALL 0, R64 # 1:1, OUT -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 +0001: LOADI R64, 0 # 1:1 +0002: UPCALL 0, R64 # 1:1, OUT +0003: EOF # 0:0 ``` ## Output @@ -336,8 +329,7 @@ OUT 100, 200, 300 0005: LOADI R69, 300 # 1:15 0006: LOADI R68, 258 # 1:15 0007: UPCALL 0, R64 # 1:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -365,8 +357,7 @@ OUT 100, , 300, 0005: LOADI R67, 290 # 1:12 0006: LOADI R69, 0 # 1:16 0007: UPCALL 0, R64 # 1:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -396,8 +387,7 @@ OUT 100; 200 AS 300; 400 0007: LOADI R71, 400 # 1:22 0008: LOADI R70, 258 # 1:22 0009: UPCALL 0, R64 # 1:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -423,8 +413,7 @@ OUT 100, "Foo" 0003: LOADI R67, 0 # 1:10 0004: LOADI R66, 259 # 1:10 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -483,8 +472,7 @@ OUT i 0005: MOVE R65, R0 # 4:5 0006: LOADI R64, 258 # 4:5 0007: UPCALL 1, R64 # 4:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -513,8 +501,7 @@ OUT i 0004: MOVE R66, R64 # 3:5 0005: LOADI R65, 258 # 3:5 0006: UPCALL 1, R65 # 3:1, OUT -0007: LOADI R65, 0 # 0:0 -0008: END R65 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -566,8 +553,7 @@ OUT i 0010: MOVE R66, R64 # 5:5 0011: LOADI R65, 258 # 5:5 0012: UPCALL 1, R65 # 5:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -605,8 +591,7 @@ OUT t 0010: MOVE R66, R64 # 5:5 0011: LOADI R65, 259 # 5:5 0012: UPCALL 1, R65 # 5:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -672,8 +657,7 @@ OUT b, d, i, s 0038: MOVE R75, R67 # 5:14 0039: LOADI R74, 259 # 5:14 0040: UPCALL 1, R68 # 5:1, OUT -0041: LOADI R68, 0 # 0:0 -0042: END R68 # 0:0 +0041: EOF # 0:0 ``` ## Output @@ -776,8 +760,7 @@ OUT b 0005: MOVE R66, R64 # 3:5 0006: LOADI R65, 256 # 3:5 0007: UPCALL 1, R65 # 3:1, OUT -0008: LOADI R65, 0 # 0:0 -0009: END R65 # 0:0 +0008: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_arithmetic_add.md b/core2/tests/test_arithmetic_add.md index 8c397f1f..5a421ee6 100644 --- a/core2/tests/test_arithmetic_add.md +++ b/core2/tests/test_arithmetic_add.md @@ -15,8 +15,7 @@ OUT 4.5 + 2.3 0003: ADDD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2 + 3 0003: ADDI R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -69,8 +67,7 @@ OUT "a" + "b" 0003: CONCAT R65, R65, R66 # 1:9 0004: LOADI R64, 259 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT 2 + 8.3 0004: ADDD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -125,8 +121,7 @@ OUT 8.3 + 2 0004: ADDD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -150,8 +145,7 @@ a = 2147483640 + 20 0001: LOADC R64, 0 # 1:5 0002: LOADI R65, 20 # 1:18 0003: ADDI R64, R64, R65 # 1:16 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors @@ -197,8 +191,7 @@ OUT a(0) + a(1) + a(2) 0019: ADDI R66, R66, R67 # 5:17 0020: LOADI R65, 258 # 5:5 0021: UPCALL 0, R65 # 5:1, OUT -0022: LOADI R65, 0 # 0:0 -0023: END R65 # 0:0 +0022: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_arithmetic_div.md b/core2/tests/test_arithmetic_div.md index a22de8e6..109863a1 100644 --- a/core2/tests/test_arithmetic_div.md +++ b/core2/tests/test_arithmetic_div.md @@ -15,8 +15,7 @@ OUT 9.0 / 4.0 0003: DIVD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 10 / 3 0003: DIVI R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 3 / 1.5 0004: DIVD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -98,8 +95,7 @@ OUT 9.0 / 3 0004: DIVD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -127,8 +123,7 @@ a = (-2147483647 - 1) / -1 0005: LOADI R65, 1 # 1:26 0006: NEGI R65 # 1:25 0007: DIVI R64, R64, R65 # 1:23 -0008: LOADI R65, 0 # 0:0 -0009: END R65 # 0:0 +0008: EOF # 0:0 ``` ## Runtime errors @@ -152,8 +147,7 @@ a = 5 / 0 0001: LOADI R64, 5 # 1:5 0002: LOADI R65, 0 # 1:9 0003: DIVI R64, R64, R65 # 1:7 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_mod.md b/core2/tests/test_arithmetic_mod.md index 1409d9e4..2d21aa7e 100644 --- a/core2/tests/test_arithmetic_mod.md +++ b/core2/tests/test_arithmetic_mod.md @@ -15,8 +15,7 @@ OUT 10.0 MOD 3.0 0003: MODD R65, R65, R66 # 1:10 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 10 MOD 3 0003: MODI R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 3 MOD 2.5 0004: MODD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -98,8 +95,7 @@ OUT 10.5 MOD 3 0004: MODD R65, R65, R66 # 1:10 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -127,8 +123,7 @@ a = (-2147483647 - 1) MOD -1 0005: LOADI R65, 1 # 1:28 0006: NEGI R65 # 1:27 0007: MODI R64, R64, R65 # 1:23 -0008: LOADI R65, 0 # 0:0 -0009: END R65 # 0:0 +0008: EOF # 0:0 ``` ## Runtime errors @@ -152,8 +147,7 @@ a = 5 MOD 0 0001: LOADI R64, 5 # 1:5 0002: LOADI R65, 0 # 1:11 0003: MODI R64, R64, R65 # 1:7 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_mul.md b/core2/tests/test_arithmetic_mul.md index f49d6b97..a900f9c7 100644 --- a/core2/tests/test_arithmetic_mul.md +++ b/core2/tests/test_arithmetic_mul.md @@ -15,8 +15,7 @@ OUT 4.0 * 2.5 0003: MULD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 6 * 7 0003: MULI R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 3 * 2.5 0004: MULD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -98,8 +95,7 @@ OUT 2.5 * 3 0004: MULD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -123,8 +119,7 @@ a = 2147483640 * 10 0001: LOADC R64, 0 # 1:5 0002: LOADI R65, 10 # 1:18 0003: MULI R64, R64, R65 # 1:16 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_neg.md b/core2/tests/test_arithmetic_neg.md index 328ca3bb..db22ad02 100644 --- a/core2/tests/test_arithmetic_neg.md +++ b/core2/tests/test_arithmetic_neg.md @@ -14,8 +14,7 @@ OUT -3.5 0002: NEGD R65 # 1:5 0003: LOADI R64, 257 # 1:5 0004: UPCALL 0, R64 # 1:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -40,8 +39,7 @@ OUT -7 0002: NEGI R65 # 1:5 0003: LOADI R64, 258 # 1:5 0004: UPCALL 0, R64 # 1:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -66,8 +64,7 @@ OUT -0 0002: NEGI R65 # 1:5 0003: LOADI R64, 258 # 1:5 0004: UPCALL 0, R64 # 1:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -101,11 +98,10 @@ a = -(&x80000000) ## Disassembly ```asm -0000: ENTER 2 # 0:0 +0000: ENTER 1 # 0:0 0001: LOADC R64, 0 # 1:7 0002: NEGI R64 # 1:5 -0003: LOADI R65, 0 # 0:0 -0004: END R65 # 0:0 +0003: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_pow.md b/core2/tests/test_arithmetic_pow.md index 18b270ca..e6d7f05d 100644 --- a/core2/tests/test_arithmetic_pow.md +++ b/core2/tests/test_arithmetic_pow.md @@ -15,8 +15,7 @@ OUT 2.0 ^ 3.0 0003: POWD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2 ^ 8 0003: POWI R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 4 ^ 0.5 0004: POWD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -98,8 +95,7 @@ OUT 2.5 ^ 3 0004: POWD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -123,8 +119,7 @@ a = 46341 ^ 2 0001: LOADI R64, 46341 # 1:5 0002: LOADI R65, 2 # 1:13 0003: POWI R64, R64, R65 # 1:11 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors @@ -149,8 +144,7 @@ a = 2 ^ -1 0002: LOADI R65, 1 # 1:10 0003: NEGI R65 # 1:9 0004: POWI R64, R64, R65 # 1:7 -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_sub.md b/core2/tests/test_arithmetic_sub.md index 09142728..c8c12be4 100644 --- a/core2/tests/test_arithmetic_sub.md +++ b/core2/tests/test_arithmetic_sub.md @@ -15,8 +15,7 @@ OUT 5.0 - 3.0 0003: SUBD R65, R65, R66 # 1:9 0004: LOADI R64, 257 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 10 - 3 0003: SUBI R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 - 8.3 0004: SUBD R65, R65, R66 # 1:7 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -98,8 +95,7 @@ OUT 8.3 - 2 0004: SUBD R65, R65, R66 # 1:9 0005: LOADI R64, 257 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -124,8 +120,7 @@ a = -2147483640 - 20 0002: NEGI R64 # 1:5 0003: LOADI R65, 20 # 1:19 0004: SUBI R64, R64, R65 # 1:17 -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index 10397c46..e5001b0f 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -35,8 +35,7 @@ OUT a(0), a(1), a(2) 0019: LOADA R70, R64, R71 # 5:17 0020: LOADI R69, 258 # 5:17 0021: UPCALL 0, R65 # 5:1, OUT -0022: LOADI R65, 0 # 0:0 -0023: END R65 # 0:0 +0022: EOF # 0:0 ``` ## Output @@ -108,8 +107,7 @@ OUT m(0, 0), m(0, 2), m(1, 1), m(1, 2) 0042: LOADA R72, R64, R73 # 8:32 0043: LOADI R71, 258 # 8:32 0044: UPCALL 0, R65 # 8:1, OUT -0045: LOADI R65, 0 # 0:0 -0046: END R65 # 0:0 +0045: EOF # 0:0 ``` ## Output @@ -144,8 +142,7 @@ OUT flags(0), flags(1) 0010: LOADA R68, R64, R69 # 3:15 0011: LOADI R67, 256 # 3:15 0012: UPCALL 0, R65 # 3:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -180,8 +177,7 @@ OUT d(0), d(1) 0010: LOADA R68, R64, R69 # 3:11 0011: LOADI R67, 257 # 3:11 0012: UPCALL 0, R65 # 3:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -223,8 +219,7 @@ OUT s(0), s(1), s(2) 0016: LOADA R70, R64, R71 # 4:17 0017: LOADI R69, 259 # 4:17 0018: UPCALL 0, R65 # 4:1, OUT -0019: LOADI R65, 0 # 0:0 -0020: END R65 # 0:0 +0019: EOF # 0:0 ``` ## Output @@ -254,7 +249,7 @@ OUT a(0), a(1) 0000: ENTER 5 # 0:0 0001: LOADI R64, 2 # 6:14 0002: ALLOCA R0, [1]%, R64 # 6:12 -0003: CALL R64, 13 # 7:1, FILL_ARRAY +0003: CALL R64, 12 # 7:1, FILL_ARRAY 0004: LOADI R66, 0 # 8:7 0005: LOADA R65, R0, R66 # 8:5 0006: LOADI R64, 290 # 8:5 @@ -262,18 +257,17 @@ OUT a(0), a(1) 0008: LOADA R67, R0, R68 # 8:11 0009: LOADI R66, 258 # 8:11 0010: UPCALL 0, R64 # 8:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 -- FILL_ARRAY -0013: ENTER 2 # 0:0 -0014: LOADI R64, 100 # 2:12 -0015: LOADI R65, 0 # 2:7 -0016: STOREA R0, R64, R65 # 2:5 -0017: LOADI R64, 200 # 3:12 -0018: LOADI R65, 1 # 3:7 -0019: STOREA R0, R64, R65 # 3:5 -0020: RETURN # 4:1 +0012: ENTER 2 # 0:0 +0013: LOADI R64, 100 # 2:12 +0014: LOADI R65, 0 # 2:7 +0015: STOREA R0, R64, R65 # 2:5 +0016: LOADI R64, 200 # 3:12 +0017: LOADI R65, 1 # 3:7 +0018: STOREA R0, R64, R65 # 3:5 +0019: RETURN # 4:1 ``` ## Output @@ -301,8 +295,7 @@ OUT a%(1) 0004: LOADA R66, R64, R67 # 2:5 0005: LOADI R65, 258 # 2:5 0006: UPCALL 0, R65 # 2:1, OUT -0007: LOADI R65, 0 # 0:0 -0008: END R65 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -350,8 +343,7 @@ OUT a(1.9) 0008: LOADA R66, R64, R67 # 3:5 0009: LOADI R65, 258 # 3:5 0010: UPCALL 0, R65 # 3:1, OUT -0011: LOADI R65, 0 # 0:0 -0012: END R65 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -449,8 +441,7 @@ DIM a(1, 0, 1) 0002: LOADI R66, 0 # 1:10 0003: LOADI R67, 1 # 1:13 0004: ALLOCA R64, [3]%, R65 # 1:5 -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Runtime errors @@ -476,8 +467,7 @@ DIM a(1, -5, 1) 0003: NEGI R66 # 1:10 0004: LOADI R67, 1 # 1:14 0005: ALLOCA R64, [3]%, R65 # 1:5 -0006: LOADI R65, 0 # 0:0 -0007: END R65 # 0:0 +0006: EOF # 0:0 ``` ## Runtime errors @@ -504,8 +494,7 @@ a(5) = 10 0003: LOADI R65, 10 # 2:8 0004: LOADI R66, 5 # 2:3 0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R65, 0 # 0:0 -0007: END R65 # 0:0 +0006: EOF # 0:0 ``` ## Runtime errors @@ -533,8 +522,7 @@ a(-5) = 10 0004: LOADI R66, 5 # 2:4 0005: NEGI R66 # 2:3 0006: STOREA R64, R65, R66 # 2:1 -0007: LOADI R65, 0 # 0:0 -0008: END R65 # 0:0 +0007: EOF # 0:0 ``` ## Runtime errors @@ -562,8 +550,7 @@ OUT a(3) 0004: LOADA R66, R64, R67 # 2:5 0005: LOADI R65, 258 # 2:5 0006: UPCALL 0, R65 # 2:1, OUT -0007: LOADI R65, 0 # 0:0 -0008: END R65 # 0:0 +0007: EOF # 0:0 ``` ## Runtime errors @@ -592,8 +579,7 @@ a(10, 50) = 123 0005: LOADI R66, 10 # 2:3 0006: LOADI R67, 50 # 2:7 0007: STOREA R64, R65, R66 # 2:1 -0008: LOADI R65, 0 # 0:0 -0009: END R65 # 0:0 +0008: EOF # 0:0 ``` ## Runtime errors @@ -660,8 +646,7 @@ OUT s(0), s(1) 0037: LOADA R71, R67, R72 # 8:11 0038: LOADI R70, 259 # 8:11 0039: UPCALL 0, R68 # 8:1, OUT -0040: LOADI R68, 0 # 0:0 -0041: END R68 # 0:0 +0040: EOF # 0:0 ``` ## Output @@ -706,8 +691,7 @@ OUT x(0), y(0) 0015: LOADA R69, R65, R70 # 5:11 0016: LOADI R68, 258 # 5:11 0017: UPCALL 0, R66 # 5:1, OUT -0018: LOADI R66, 0 # 0:0 -0019: END R66 # 0:0 +0018: EOF # 0:0 ``` ## Output @@ -737,36 +721,35 @@ OUT sum(0) ```asm 0000: ENTER 4 # 0:0 0001: LOADI R67, 0 # 9:9 -0002: CALL R66, 8 # 9:5, SUM +0002: CALL R66, 7 # 9:5, SUM 0003: MOVE R65, R66 # 9:5 0004: LOADI R64, 258 # 9:5 0005: UPCALL 0, R64 # 9:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 -- SUM -0008: LOADI R64, 0 # 1:10 -0009: ENTER 5 # 0:0 -0010: LOADI R67, 3 # 2:11 -0011: ALLOCA R66, [1]%, R67 # 2:9 -0012: LOADI R67, 10 # 3:12 -0013: LOADI R68, 0 # 3:7 -0014: STOREA R66, R67, R68 # 3:5 -0015: LOADI R67, 20 # 4:12 -0016: LOADI R68, 1 # 4:7 -0017: STOREA R66, R67, R68 # 4:5 -0018: LOADI R67, 30 # 5:12 -0019: LOADI R68, 2 # 5:7 -0020: STOREA R66, R67, R68 # 5:5 -0021: LOADI R67, 0 # 6:13 -0022: LOADA R64, R66, R67 # 6:11 -0023: LOADI R68, 1 # 6:20 -0024: LOADA R67, R66, R68 # 6:18 -0025: ADDI R64, R64, R67 # 6:16 -0026: LOADI R68, 2 # 6:27 -0027: LOADA R67, R66, R68 # 6:25 -0028: ADDI R64, R64, R67 # 6:23 -0029: RETURN # 7:1 +0007: LOADI R64, 0 # 1:10 +0008: ENTER 5 # 0:0 +0009: LOADI R67, 3 # 2:11 +0010: ALLOCA R66, [1]%, R67 # 2:9 +0011: LOADI R67, 10 # 3:12 +0012: LOADI R68, 0 # 3:7 +0013: STOREA R66, R67, R68 # 3:5 +0014: LOADI R67, 20 # 4:12 +0015: LOADI R68, 1 # 4:7 +0016: STOREA R66, R67, R68 # 4:5 +0017: LOADI R67, 30 # 5:12 +0018: LOADI R68, 2 # 5:7 +0019: STOREA R66, R67, R68 # 5:5 +0020: LOADI R67, 0 # 6:13 +0021: LOADA R64, R66, R67 # 6:11 +0022: LOADI R68, 1 # 6:20 +0023: LOADA R67, R66, R68 # 6:18 +0024: ADDI R64, R64, R67 # 6:16 +0025: LOADI R68, 2 # 6:27 +0026: LOADA R67, R66, R68 # 6:25 +0027: ADDI R64, R64, R67 # 6:23 +0028: RETURN # 7:1 ``` ## Output diff --git a/core2/tests/test_assignments.md b/core2/tests/test_assignments.md index c8c99d69..46b0d970 100644 --- a/core2/tests/test_assignments.md +++ b/core2/tests/test_assignments.md @@ -18,8 +18,7 @@ OUT a 0003: MOVE R66, R64 # 4:5 0004: LOADI R65, 258 # 4:5 0005: UPCALL 0, R65 # 4:1, OUT -0006: LOADI R65, 0 # 0:0 -0007: END R65 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -52,8 +51,7 @@ OUT a 0006: MOVE R66, R64 # 5:5 0007: LOADI R65, 259 # 5:5 0008: UPCALL 0, R65 # 5:1, OUT -0009: LOADI R65, 0 # 0:0 -0010: END R65 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -87,8 +85,7 @@ OUT a 0006: MOVE R66, R64 # 5:5 0007: LOADI R65, 259 # 5:5 0008: UPCALL 0, R65 # 5:1, OUT -0009: LOADI R65, 0 # 0:0 -0010: END R65 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -160,8 +157,7 @@ OUT d 0003: MOVE R66, R64 # 2:5 0004: LOADI R65, 257 # 2:5 0005: UPCALL 0, R65 # 2:1, OUT -0006: LOADI R65, 0 # 0:0 -0007: END R65 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -199,8 +195,7 @@ OUT i1, i2 0009: MOVE R69, R65 # 7:9 0010: LOADI R68, 258 # 7:9 0011: UPCALL 0, R66 # 7:1, OUT -0012: LOADI R66, 0 # 0:0 -0013: END R66 # 0:0 +0012: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_and.md b/core2/tests/test_bitwise_and.md index 37105552..a70e5c5f 100644 --- a/core2/tests/test_bitwise_and.md +++ b/core2/tests/test_bitwise_and.md @@ -15,8 +15,7 @@ OUT 12 AND 10 0003: AND R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -76,8 +75,7 @@ OUT TRUE AND TRUE 0008: AND R65, R65, R66 # 2:10 0009: LOADI R64, 256 # 2:5 0010: UPCALL 0, R64 # 2:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_not.md b/core2/tests/test_bitwise_not.md index 35103ab8..59aca816 100644 --- a/core2/tests/test_bitwise_not.md +++ b/core2/tests/test_bitwise_not.md @@ -14,8 +14,7 @@ OUT NOT 12 0002: NOT R65 # 1:5 0003: LOADI R64, 258 # 1:5 0004: UPCALL 0, R64 # 1:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -75,8 +74,7 @@ OUT NOT FALSE 0008: XOR R65, R65, R66 # 2:5 0009: LOADI R64, 256 # 2:5 0010: UPCALL 0, R64 # 2:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_or.md b/core2/tests/test_bitwise_or.md index 8d21997a..15ef2ce6 100644 --- a/core2/tests/test_bitwise_or.md +++ b/core2/tests/test_bitwise_or.md @@ -15,8 +15,7 @@ OUT 12 OR 10 0003: OR R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -76,8 +75,7 @@ OUT FALSE OR FALSE 0008: OR R65, R65, R66 # 2:11 0009: LOADI R64, 256 # 2:5 0010: UPCALL 0, R64 # 2:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_shl.md b/core2/tests/test_bitwise_shl.md index f3ab85ef..86f8c262 100644 --- a/core2/tests/test_bitwise_shl.md +++ b/core2/tests/test_bitwise_shl.md @@ -15,8 +15,7 @@ OUT 3 << 2 0003: SHL R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 7 << 0 0003: SHL R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -69,8 +67,7 @@ OUT 1 << 32 0003: SHL R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -111,8 +108,7 @@ OUT 1 << -1 0004: SHL R65, R65, R66 # 1:7 0005: LOADI R64, 258 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_bitwise_shr.md b/core2/tests/test_bitwise_shr.md index 0ff7f658..7de7d9bf 100644 --- a/core2/tests/test_bitwise_shr.md +++ b/core2/tests/test_bitwise_shr.md @@ -15,8 +15,7 @@ OUT 12 >> 2 0003: SHR R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 7 >> 0 0003: SHR R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -69,8 +67,7 @@ OUT 1 >> 32 0003: SHR R65, R65, R66 # 1:7 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT -1 >> 32 0004: SHR R65, R65, R66 # 1:8 0005: LOADI R64, 258 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -125,8 +121,7 @@ OUT -8 >> 2 0004: SHR R65, R65, R66 # 1:8 0005: LOADI R64, 258 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -167,8 +162,7 @@ OUT 1 >> -1 0004: SHR R65, R65, R66 # 1:7 0005: LOADI R64, 258 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_bitwise_xor.md b/core2/tests/test_bitwise_xor.md index 3203ec77..9eec36ae 100644 --- a/core2/tests/test_bitwise_xor.md +++ b/core2/tests/test_bitwise_xor.md @@ -15,8 +15,7 @@ OUT 12 XOR 10 0003: XOR R65, R65, R66 # 1:8 0004: LOADI R64, 258 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -76,8 +75,7 @@ OUT TRUE XOR TRUE 0008: XOR R65, R65, R66 # 2:10 0009: LOADI R64, 256 # 2:5 0010: UPCALL 0, R64 # 2:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_case_insensitivity.md b/core2/tests/test_case_insensitivity.md index d3103410..616a335d 100644 --- a/core2/tests/test_case_insensitivity.md +++ b/core2/tests/test_case_insensitivity.md @@ -21,8 +21,7 @@ OUT A 0006: MOVE R66, R64 # 4:5 0007: LOADI R65, 258 # 4:5 0008: UPCALL 0, R65 # 4:1, OUT -0009: LOADI R65, 0 # 0:0 -0010: END R65 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -62,8 +61,7 @@ OUT A(0), a(1) 0013: LOADA R68, R64, R69 # 4:11 0014: LOADI R67, 258 # 4:11 0015: UPCALL 0, R65 # 4:1, OUT -0016: LOADI R65, 0 # 0:0 -0017: END R65 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -125,8 +123,7 @@ OUT A, a 0006: MOVE R67, R0 # 4:8 0007: LOADI R66, 258 # 4:8 0008: UPCALL 0, R64 # 4:1, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -151,17 +148,16 @@ OUT FOO ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 6 # 5:5, FOO +0001: CALL R65, 5 # 5:5, FOO 0002: LOADI R64, 258 # 5:5 0003: UPCALL 0, R64 # 5:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 -- FOO -0006: LOADI R64, 0 # 1:10 -0007: ENTER 1 # 0:0 -0008: LOADI R64, 42 # 2:11 -0009: RETURN # 3:1 +0005: LOADI R64, 0 # 1:10 +0006: ENTER 1 # 0:0 +0007: LOADI R64, 42 # 2:11 +0008: RETURN # 3:1 ``` ## Output @@ -185,17 +181,16 @@ FOO ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: CALL R64, 4 # 5:1, FOO -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 +0000: ENTER 0 # 0:0 +0001: CALL R64, 3 # 5:1, FOO +0002: EOF # 0:0 -- FOO -0004: ENTER 2 # 0:0 -0005: LOADI R65, 0 # 2:9 -0006: LOADI R64, 259 # 2:9 -0007: UPCALL 0, R64 # 2:5, OUT -0008: RETURN # 3:1 +0003: ENTER 2 # 0:0 +0004: LOADI R65, 0 # 2:9 +0005: LOADI R64, 259 # 2:9 +0006: UPCALL 0, R64 # 2:5, OUT +0007: RETURN # 3:1 ``` ## Output @@ -226,8 +221,7 @@ OUT "done" 0005: LOADI R65, 1 # 4:5 0006: LOADI R64, 259 # 4:5 0007: UPCALL 0, R64 # 4:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -259,8 +253,7 @@ RETURN 0005: LOADI R64, 259 # 4:5 0006: UPCALL 0, R64 # 4:1, OUT 0007: RETURN # 5:1 -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -286,20 +279,19 @@ OUT foo(5) ```asm 0000: ENTER 4 # 0:0 0001: LOADI R67, 5 # 5:9 -0002: CALL R66, 8 # 5:5, FOO +0002: CALL R66, 7 # 5:5, FOO 0003: MOVE R65, R66 # 5:5 0004: LOADI R64, 258 # 5:5 0005: UPCALL 0, R64 # 5:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 -- FOO -0008: LOADI R64, 0 # 1:10 -0009: ENTER 3 # 0:0 -0010: MOVE R64, R65 # 2:11 -0011: LOADI R66, 1 # 2:15 -0012: ADDI R64, R64, R66 # 2:13 -0013: RETURN # 3:1 +0007: LOADI R64, 0 # 1:10 +0008: ENTER 3 # 0:0 +0009: MOVE R64, R65 # 2:11 +0010: LOADI R66, 1 # 2:15 +0011: ADDI R64, R64, R66 # 2:13 +0012: RETURN # 3:1 ``` ## Output diff --git a/core2/tests/test_data.md b/core2/tests/test_data.md index 01fc5a1d..3d1fa289 100644 --- a/core2/tests/test_data.md +++ b/core2/tests/test_data.md @@ -11,10 +11,9 @@ GETDATA ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 0 # 0:0 0001: UPCALL 0, R64 # 3:1, GETDATA -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 +0002: EOF # 0:0 ``` ## Output @@ -64,8 +63,7 @@ GETDATA 0016: ADDI R64, R64, R65 # 9:11 0017: JUMP 10 # 9:5 0018: UPCALL 0, R65 # 12:1, GETDATA -0019: LOADI R65, 0 # 0:0 -0020: END R65 # 0:0 +0019: EOF # 0:0 ``` ## Output @@ -88,11 +86,10 @@ GETDATA ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 0 # 0:0 0001: UPCALL 0, R64 # 1:1, GETDATA 0002: UPCALL 0, R64 # 4:1, GETDATA -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_do.md b/core2/tests/test_do.md index 9b895768..2e95c664 100644 --- a/core2/tests/test_do.md +++ b/core2/tests/test_do.md @@ -22,8 +22,7 @@ LOOP 0006: LOADI R64, 259 # 4:9 0007: UPCALL 0, R64 # 4:5, OUT 0008: JUMP 1 # 0:0 -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -61,8 +60,7 @@ LOOP 0011: LOADI R65, 1 # 4:13 0012: SUBI R64, R64, R65 # 4:11 0013: JUMP 2 # 2:10 -0014: LOADI R65, 0 # 0:0 -0015: END R65 # 0:0 +0014: EOF # 0:0 ``` # Test: Pre UNTIL DO loop with iterations @@ -94,8 +92,7 @@ LOOP 0011: LOADI R65, 1 # 4:13 0012: SUBI R64, R64, R65 # 4:11 0013: JUMP 2 # 2:10 -0014: LOADI R65, 0 # 0:0 -0015: END R65 # 0:0 +0014: EOF # 0:0 ``` ## Output @@ -134,8 +131,7 @@ LOOP 0010: LOADI R65, 1 # 4:13 0011: SUBI R64, R64, R65 # 4:11 0012: JUMP 2 # 2:10 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` # Test: Pre WHILE DO loop with iterations @@ -166,8 +162,7 @@ LOOP 0010: LOADI R65, 1 # 4:13 0011: SUBI R64, R64, R65 # 4:11 0012: JUMP 2 # 2:10 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -205,8 +200,7 @@ LOOP UNTIL n = 0 0009: LOADI R66, 0 # 5:16 0010: CMPEQI R65, R65, R66 # 5:14 0011: JMPF R65, 2 # 5:12 -0012: LOADI R65, 0 # 0:0 -0013: END R65 # 0:0 +0012: EOF # 0:0 ``` ## Output @@ -242,8 +236,7 @@ LOOP UNTIL n = 0 0009: LOADI R66, 0 # 5:16 0010: CMPEQI R65, R65, R66 # 5:14 0011: JMPF R65, 2 # 5:12 -0012: LOADI R65, 0 # 0:0 -0013: END R65 # 0:0 +0012: EOF # 0:0 ``` ## Output @@ -282,8 +275,7 @@ LOOP WHILE n > 0 0010: CMPGTI R65, R65, R66 # 5:14 0011: JMPF R65, 13 # 5:12 0012: JUMP 2 # 5:12 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -320,8 +312,7 @@ LOOP WHILE n > 0 0010: CMPGTI R65, R65, R66 # 5:14 0011: JMPF R65, 13 # 5:12 0012: JUMP 2 # 5:12 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -392,8 +383,7 @@ LOOP 0036: LOADI R66, 1 # 10:13 0037: SUBI R64, R64, R66 # 10:11 0038: JUMP 2 # 2:10 -0039: LOADI R66, 0 # 0:0 -0040: END R66 # 0:0 +0039: EOF # 0:0 ``` ## Output @@ -457,8 +447,7 @@ LOOP 0027: LOADI R66, 1 # 10:13 0028: SUBI R64, R64, R66 # 10:11 0029: JUMP 2 # 2:10 -0030: LOADI R66, 0 # 0:0 -0031: END R66 # 0:0 +0030: EOF # 0:0 ``` ## Output @@ -523,8 +512,7 @@ LOOP 0029: LOADI R66, 1 # 10:13 0030: SUBI R64, R64, R66 # 10:11 0031: JUMP 2 # 2:10 -0032: LOADI R66, 0 # 0:0 -0033: END R66 # 0:0 +0032: EOF # 0:0 ``` ## Output @@ -578,8 +566,7 @@ LOOP 0021: LOADI R66, 1 # 9:13 0022: SUBI R64, R64, R66 # 9:11 0023: JUMP 2 # 2:10 -0024: LOADI R66, 0 # 0:0 -0025: END R66 # 0:0 +0024: EOF # 0:0 ``` ## Output @@ -626,8 +613,7 @@ LOOP 0015: LOADI R66, 1 # 8:13 0016: SUBI R64, R64, R66 # 8:11 0017: JUMP 2 # 2:10 -0018: LOADI R66, 0 # 0:0 -0019: END R66 # 0:0 +0018: EOF # 0:0 ``` ## Output @@ -684,8 +670,7 @@ LOOP 0027: LOADI R66, 1 # 6:13 0028: SUBI R64, R64, R66 # 6:11 0029: JUMP 2 # 2:10 -0030: LOADI R66, 0 # 0:0 -0031: END R66 # 0:0 +0030: EOF # 0:0 ``` ## Output @@ -747,8 +732,7 @@ LOOP 0026: LOADI R65, 1 # 10:13 0027: SUBI R64, R64, R65 # 10:11 0028: JUMP 16 # 8:10 -0029: LOADI R65, 0 # 0:0 -0030: END R65 # 0:0 +0029: EOF # 0:0 ``` ## Output @@ -828,8 +812,7 @@ RETURN 0039: SUBI R65, R65, R66 # 13:11 0040: JUMP 18 # 10:10 0041: RETURN # 15:1 -0042: LOADI R66, 0 # 0:0 -0043: END R66 # 0:0 +0042: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_empty.md b/core2/tests/test_empty.md index b785b73c..6c5918c4 100644 --- a/core2/tests/test_empty.md +++ b/core2/tests/test_empty.md @@ -8,7 +8,6 @@ ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 0:0 -0002: END R64 # 0:0 +0000: ENTER 0 # 0:0 +0001: EOF # 0:0 ``` diff --git a/core2/tests/test_end.md b/core2/tests/test_end.md index 1b2d9c22..8a9b9f8e 100644 --- a/core2/tests/test_end.md +++ b/core2/tests/test_end.md @@ -12,8 +12,7 @@ END 0000: ENTER 1 # 0:0 0001: LOADI R64, 0 # 1:1 0002: END R64 # 1:1 -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` # Test: Exit code is an integer immediate @@ -30,8 +29,7 @@ END 42 0000: ENTER 1 # 0:0 0001: LOADI R64, 42 # 1:5 0002: END R64 # 1:1 -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` ## Exit code @@ -55,8 +53,7 @@ END 43.98 0001: LOADC R64, 0 # 1:5 0002: DTOI R64 # 1:5 0003: END R64 # 1:1 -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 ``` ## Exit code @@ -83,8 +80,7 @@ END i 0002: LOADI R0, 5 # 2:5 0003: MOVE R64, R0 # 3:5 0004: END R64 # 3:1 -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Exit code @@ -109,8 +105,7 @@ END i 0001: LOADI R64, 3 # 1:5 0002: MOVE R65, R64 # 2:5 0003: END R65 # 2:1 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Exit code @@ -178,8 +173,7 @@ END i 0002: NEGI R64 # 1:5 0003: MOVE R65, R64 # 2:5 0004: END R65 # 2:1 -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Runtime errors @@ -204,8 +198,7 @@ END i 0001: LOADI R64, 128 # 1:5 0002: MOVE R65, R64 # 2:5 0003: END R65 # 2:1 -0004: LOADI R65, 0 # 0:0 -0005: END R65 # 0:0 +0004: EOF # 0:0 ``` ## Runtime errors @@ -243,8 +236,7 @@ NEXT 0013: LOADI R65, 1 # 1:16 0014: ADDI R64, R64, R65 # 1:11 0015: JUMP 2 # 1:5 -0016: LOADI R65, 0 # 0:0 -0017: END R65 # 0:0 +0016: EOF # 0:0 ``` ## Exit code diff --git a/core2/tests/test_for.md b/core2/tests/test_for.md index 730b9d95..4d0439a8 100644 --- a/core2/tests/test_for.md +++ b/core2/tests/test_for.md @@ -24,8 +24,7 @@ NEXT 0010: LOADI R65, 1 # 1:15 0011: ADDI R64, R64, R65 # 1:11 0012: JUMP 2 # 1:5 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -63,8 +62,7 @@ NEXT 0010: LOADI R65, 3 # 1:22 0011: ADDI R64, R64, R65 # 1:11 0012: JUMP 2 # 1:5 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -102,8 +100,7 @@ NEXT 0010: LOADC R65, 0 # 1:23 0011: ADDI R64, R64, R65 # 1:12 0012: JUMP 2 # 1:5 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -142,8 +139,7 @@ NEXT 0010: LOADI R65, 1 # 1:16 0011: ADDI R64, R64, R65 # 1:12 0012: JUMP 2 # 1:5 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` # Test: FOR loop with invalid direction has zero iterations @@ -172,8 +168,7 @@ NEXT 0010: LOADC R65, 0 # 1:23 0011: ADDI R64, R64, R65 # 1:11 0012: JUMP 2 # 1:5 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` # Test: FOR iterator is visible after NEXT @@ -202,8 +197,7 @@ OUT something 0010: MOVE R66, R64 # 3:5 0011: LOADI R65, 258 # 3:5 0012: UPCALL 0, R65 # 3:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -242,8 +236,7 @@ NEXT 0013: LOADI R65, 1 # 1:23 0014: ADDI R64, R64, R65 # 1:19 0015: JUMP 2 # 1:5 -0016: LOADI R65, 0 # 0:0 -0017: END R65 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -287,8 +280,7 @@ NEXT 0016: ITOD R66 # 1:19 0017: ADDD R64, R64, R66 # 1:13 0018: JUMP 2 # 1:5 -0019: LOADI R66, 0 # 0:0 -0020: END R66 # 0:0 +0019: EOF # 0:0 ``` ## Output @@ -346,8 +338,7 @@ OUT i; b 0028: MOVE R70, R66 # 7:8 0029: LOADI R69, 258 # 7:8 0030: UPCALL 0, R67 # 7:1, OUT -0031: LOADI R67, 0 # 0:0 -0032: END R67 # 0:0 +0031: EOF # 0:0 ``` ## Output @@ -403,8 +394,7 @@ OUT i 0024: MOVE R67, R64 # 10:5 0025: LOADI R66, 258 # 10:5 0026: UPCALL 0, R66 # 10:1, OUT -0027: LOADI R66, 0 # 0:0 -0028: END R66 # 0:0 +0027: EOF # 0:0 ``` ## Output @@ -445,8 +435,7 @@ NEXT 0015: LOADI R65, 1 # 1:16 0016: ADDI R64, R64, R65 # 1:11 0017: JUMP 2 # 1:5 -0018: LOADI R65, 0 # 0:0 -0019: END R65 # 0:0 +0018: EOF # 0:0 ``` ## Output @@ -511,8 +500,7 @@ NEXT 0031: LOADI R66, 1 # 1:16 0032: ADDI R64, R64, R66 # 1:11 0033: JUMP 2 # 1:5 -0034: LOADI R66, 0 # 0:0 -0035: END R66 # 0:0 +0034: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 09fb9ee4..48d138cf 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -14,17 +14,16 @@ OUT foo$ ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 6 # 5:5, FOO +0001: CALL R65, 5 # 5:5, FOO 0002: LOADI R64, 259 # 5:5 0003: UPCALL 0, R64 # 5:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 -- FOO -0006: LOADI R64, 0 # 1:10 -0007: ENTER 1 # 0:0 -0008: LOADI R64, 1 # 2:11 -0009: RETURN # 3:1 +0005: LOADI R64, 0 # 1:10 +0006: ENTER 1 # 0:0 +0007: LOADI R64, 1 # 2:11 +0008: RETURN # 3:1 ``` ## Output @@ -81,7 +80,7 @@ OUT "After", a 0006: UPCALL 0, R65 # 9:1, OUT 0007: LOADI R66, 1 # 10:5 0008: LOADI R65, 291 # 10:5 -0009: CALL R68, 19 # 10:21, FOO +0009: CALL R68, 18 # 10:21, FOO 0010: LOADI R67, 258 # 10:21 0011: UPCALL 0, R65 # 10:1, OUT 0012: LOADI R66, 2 # 11:5 @@ -89,20 +88,19 @@ OUT "After", a 0014: MOVE R68, R64 # 11:14 0015: LOADI R67, 258 # 11:14 0016: UPCALL 0, R65 # 11:1, OUT -0017: LOADI R65, 0 # 0:0 -0018: END R65 # 0:0 +0017: EOF # 0:0 -- FOO -0019: LOADI R64, 0 # 3:10 -0020: ENTER 6 # 0:0 -0021: LOADI R65, 20 # 4:9 -0022: LOADI R67, 3 # 5:9 -0023: LOADI R66, 291 # 5:9 -0024: MOVE R69, R65 # 5:19 -0025: LOADI R68, 258 # 5:19 -0026: UPCALL 0, R66 # 5:5, OUT -0027: LOADI R64, 30 # 6:11 -0028: RETURN # 7:1 +0018: LOADI R64, 0 # 3:10 +0019: ENTER 6 # 0:0 +0020: LOADI R65, 20 # 4:9 +0021: LOADI R67, 3 # 5:9 +0022: LOADI R66, 291 # 5:9 +0023: MOVE R69, R65 # 5:19 +0024: LOADI R68, 258 # 5:19 +0025: UPCALL 0, R66 # 5:5, OUT +0026: LOADI R64, 30 # 6:11 +0027: RETURN # 7:1 ``` ## Output @@ -135,26 +133,25 @@ OUT second ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 13 # 10:5, SECOND +0001: CALL R65, 12 # 10:5, SECOND 0002: LOADI R64, 258 # 10:5 0003: UPCALL 0, R64 # 10:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 -- FIRST -0006: LOADI R64, 0 # 1:10 -0007: ENTER 3 # 0:0 -0008: LOADI R66, 0 # 2:9 -0009: LOADI R65, 259 # 2:9 -0010: UPCALL 0, R65 # 2:5, OUT -0011: LOADI R64, 123 # 3:13 -0012: RETURN # 4:1 +0005: LOADI R64, 0 # 1:10 +0006: ENTER 3 # 0:0 +0007: LOADI R66, 0 # 2:9 +0008: LOADI R65, 259 # 2:9 +0009: UPCALL 0, R65 # 2:5, OUT +0010: LOADI R64, 123 # 3:13 +0011: RETURN # 4:1 -- SECOND -0013: LOADI R64, 0 # 6:10 -0014: ENTER 1 # 0:0 -0015: CALL R64, 6 # 7:14, FIRST -0016: RETURN # 8:1 +0012: LOADI R64, 0 # 6:10 +0013: ENTER 1 # 0:0 +0014: CALL R64, 5 # 7:14, FIRST +0015: RETURN # 8:1 ``` ## Output @@ -192,43 +189,42 @@ OUT do_call ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 15 # 17:5, DO_CALL +0001: CALL R65, 14 # 17:5, DO_CALL 0002: LOADI R64, 258 # 17:5 0003: UPCALL 0, R64 # 17:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 -- DEFAULT_DOUBLE -0006: LOADI R64, 0 # 1:10 -0007: ENTER 1 # 0:0 -0008: RETURN # 2:1 +0005: LOADI R64, 0 # 1:10 +0006: ENTER 1 # 0:0 +0007: RETURN # 2:1 -- DEFAULT_INTEGER -0009: LOADI R64, 0 # 4:10 -0010: ENTER 1 # 0:0 -0011: RETURN # 5:1 +0008: LOADI R64, 0 # 4:10 +0009: ENTER 1 # 0:0 +0010: RETURN # 5:1 -- DEFAULT_STRING -0012: LOADI R64, 1 # 7:10 -0013: ENTER 1 # 0:0 -0014: RETURN # 8:1 +0011: LOADI R64, 1 # 7:10 +0012: ENTER 1 # 0:0 +0013: RETURN # 8:1 -- DO_CALL -0015: LOADI R64, 0 # 10:10 -0016: ENTER 3 # 0:0 -0017: LOADI R66, 300 # 11:9 -0018: LOADI R65, 258 # 11:9 -0019: UPCALL 0, R65 # 11:5, OUT -0020: CALL R66, 6 # 12:9, DEFAULT_DOUBLE -0021: LOADI R65, 257 # 12:9 -0022: UPCALL 0, R65 # 12:5, OUT -0023: CALL R66, 9 # 13:9, DEFAULT_INTEGER -0024: LOADI R65, 258 # 13:9 -0025: UPCALL 0, R65 # 13:5, OUT -0026: CALL R66, 12 # 14:9, DEFAULT_STRING -0027: LOADI R65, 259 # 14:9 -0028: UPCALL 0, R65 # 14:5, OUT -0029: RETURN # 15:1 +0014: LOADI R64, 0 # 10:10 +0015: ENTER 3 # 0:0 +0016: LOADI R66, 300 # 11:9 +0017: LOADI R65, 258 # 11:9 +0018: UPCALL 0, R65 # 11:5, OUT +0019: CALL R66, 5 # 12:9, DEFAULT_DOUBLE +0020: LOADI R65, 257 # 12:9 +0021: UPCALL 0, R65 # 12:5, OUT +0022: CALL R66, 8 # 13:9, DEFAULT_INTEGER +0023: LOADI R65, 258 # 13:9 +0024: UPCALL 0, R65 # 13:5, OUT +0025: CALL R66, 11 # 14:9, DEFAULT_STRING +0026: LOADI R65, 259 # 14:9 +0027: UPCALL 0, R65 # 14:5, OUT +0028: RETURN # 15:1 ``` ## Output @@ -276,7 +272,7 @@ OUT "After modify_1", var 0004: MOVE R68, R64 # 16:24 0005: LOADI R67, 258 # 16:24 0006: UPCALL 0, R65 # 16:1, OUT -0007: CALL R66, 27 # 17:5, MODIFY_1 +0007: CALL R66, 26 # 17:5, MODIFY_1 0008: LOADI R65, 258 # 17:5 0009: UPCALL 0, R65 # 17:1, OUT 0010: LOADI R66, 1 # 18:5 @@ -284,40 +280,39 @@ OUT "After modify_1", var 0012: MOVE R68, R64 # 18:23 0013: LOADI R67, 258 # 18:23 0014: UPCALL 0, R65 # 18:1, OUT -0015: LOADI R65, 0 # 0:0 -0016: END R65 # 0:0 +0015: EOF # 0:0 -- MODIFY_2 -0017: LOADI R64, 0 # 1:10 -0018: ENTER 6 # 0:0 -0019: LOADI R65, 300 # 2:11 -0020: LOADI R64, 2000 # 3:16 -0021: LOADI R67, 2 # 4:9 -0022: LOADI R66, 291 # 4:9 -0023: MOVE R69, R65 # 4:28 -0024: LOADI R68, 258 # 4:28 -0025: UPCALL 0, R66 # 4:5, OUT -0026: RETURN # 5:1 +0016: LOADI R64, 0 # 1:10 +0017: ENTER 6 # 0:0 +0018: LOADI R65, 300 # 2:11 +0019: LOADI R64, 2000 # 3:16 +0020: LOADI R67, 2 # 4:9 +0021: LOADI R66, 291 # 4:9 +0022: MOVE R69, R65 # 4:28 +0023: LOADI R68, 258 # 4:28 +0024: UPCALL 0, R66 # 4:5, OUT +0025: RETURN # 5:1 -- MODIFY_1 -0027: LOADI R64, 0 # 7:10 -0028: ENTER 6 # 0:0 -0029: LOADI R65, 200 # 8:11 -0030: LOADI R67, 3 # 9:9 -0031: LOADI R66, 291 # 9:9 -0032: MOVE R69, R65 # 9:28 -0033: LOADI R68, 258 # 9:28 -0034: UPCALL 0, R66 # 9:5, OUT -0035: CALL R67, 17 # 10:9, MODIFY_2 -0036: LOADI R66, 258 # 10:9 -0037: UPCALL 0, R66 # 10:5, OUT -0038: LOADI R67, 4 # 11:9 -0039: LOADI R66, 291 # 11:9 -0040: MOVE R69, R65 # 11:27 -0041: LOADI R68, 258 # 11:27 -0042: UPCALL 0, R66 # 11:5, OUT -0043: LOADI R64, 1000 # 12:16 -0044: RETURN # 13:1 +0026: LOADI R64, 0 # 7:10 +0027: ENTER 6 # 0:0 +0028: LOADI R65, 200 # 8:11 +0029: LOADI R67, 3 # 9:9 +0030: LOADI R66, 291 # 9:9 +0031: MOVE R69, R65 # 9:28 +0032: LOADI R68, 258 # 9:28 +0033: UPCALL 0, R66 # 9:5, OUT +0034: CALL R67, 16 # 10:9, MODIFY_2 +0035: LOADI R66, 258 # 10:9 +0036: UPCALL 0, R66 # 10:5, OUT +0037: LOADI R67, 4 # 11:9 +0038: LOADI R66, 291 # 11:9 +0039: MOVE R69, R65 # 11:27 +0040: LOADI R68, 258 # 11:27 +0041: UPCALL 0, R66 # 11:5, OUT +0042: LOADI R64, 1000 # 12:16 +0043: RETURN # 13:1 ``` ## Output @@ -369,25 +364,24 @@ OUT add(3, 5) + add(10, 20) 0000: ENTER 6 # 0:0 0001: LOADI R67, 3 # 5:9 0002: LOADI R68, 5 # 5:12 -0003: CALL R66, 14 # 5:5, ADD +0003: CALL R66, 13 # 5:5, ADD 0004: MOVE R65, R66 # 5:5 0005: LOADI R68, 10 # 5:21 0006: LOADI R69, 20 # 5:25 -0007: CALL R67, 14 # 5:17, ADD +0007: CALL R67, 13 # 5:17, ADD 0008: MOVE R66, R67 # 5:17 0009: ADDI R65, R65, R66 # 5:15 0010: LOADI R64, 258 # 5:5 0011: UPCALL 0, R64 # 5:1, OUT -0012: LOADI R64, 0 # 0:0 -0013: END R64 # 0:0 +0012: EOF # 0:0 -- ADD -0014: LOADI R64, 0 # 1:10 -0015: ENTER 4 # 0:0 -0016: MOVE R64, R65 # 2:11 -0017: MOVE R67, R66 # 2:15 -0018: ADDI R64, R64, R67 # 2:13 -0019: RETURN # 3:1 +0013: LOADI R64, 0 # 1:10 +0014: ENTER 4 # 0:0 +0015: MOVE R64, R65 # 2:11 +0016: MOVE R67, R66 # 2:15 +0017: ADDI R64, R64, R67 # 2:13 +0018: RETURN # 3:1 ``` ## Output @@ -416,21 +410,20 @@ OUT ret 0000: ENTER 2 # 0:0 0001: LOADI R0, 0 # 5:12 0002: LOADI R65, 3 # 6:11 -0003: CALL R64, 10 # 6:7, FOO +0003: CALL R64, 9 # 6:7, FOO 0004: MOVE R0, R64 # 6:7 0005: MOVE R65, R0 # 7:5 0006: LOADI R64, 258 # 7:5 0007: UPCALL 0, R64 # 7:1, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 -- FOO -0010: LOADI R64, 0 # 1:10 -0011: ENTER 3 # 0:0 -0012: LOADI R64, 42 # 2:11 -0013: MOVE R66, R65 # 2:16 -0014: ADDI R64, R64, R66 # 2:14 -0015: RETURN # 3:1 +0009: LOADI R64, 0 # 1:10 +0010: ENTER 3 # 0:0 +0011: LOADI R64, 42 # 2:11 +0012: MOVE R66, R65 # 2:16 +0013: ADDI R64, R64, R66 # 2:14 +0014: RETURN # 3:1 ``` ## Output @@ -467,7 +460,7 @@ OUT s 0000: ENTER 6 # 0:0 0001: LOADI R64, 5 # 9:5 0002: MOVE R68, R64 # 10:20 -0003: CALL R67, 21 # 10:5, CHANGE_INTEGER +0003: CALL R67, 20 # 10:5, CHANGE_INTEGER 0004: MOVE R66, R67 # 10:5 0005: LOADI R65, 258 # 10:5 0006: UPCALL 0, R65 # 10:1, OUT @@ -476,27 +469,26 @@ OUT s 0009: UPCALL 0, R65 # 11:1, OUT 0010: LOADI R65, 0 # 13:5 0011: MOVE R69, R65 # 14:19 -0012: CALL R68, 25 # 14:5, CHANGE_STRING +0012: CALL R68, 24 # 14:5, CHANGE_STRING 0013: MOVE R67, R68 # 14:5 0014: LOADI R66, 258 # 14:5 0015: UPCALL 0, R66 # 14:1, OUT 0016: MOVE R67, R65 # 15:5 0017: LOADI R66, 259 # 15:5 0018: UPCALL 0, R66 # 15:1, OUT -0019: LOADI R66, 0 # 0:0 -0020: END R66 # 0:0 +0019: EOF # 0:0 -- CHANGE_INTEGER -0021: LOADI R64, 0 # 1:10 -0022: ENTER 2 # 0:0 -0023: LOADI R65, 3 # 2:9 -0024: RETURN # 3:1 +0020: LOADI R64, 0 # 1:10 +0021: ENTER 2 # 0:0 +0022: LOADI R65, 3 # 2:9 +0023: RETURN # 3:1 -- CHANGE_STRING -0025: LOADI R64, 0 # 5:10 -0026: ENTER 2 # 0:0 -0027: LOADI R65, 1 # 6:9 -0028: RETURN # 7:1 +0024: LOADI R64, 0 # 5:10 +0025: ENTER 2 # 0:0 +0026: LOADI R65, 1 # 6:9 +0027: RETURN # 7:1 ``` ## Output @@ -530,8 +522,7 @@ OUT SUM_DOUBLES(3.4, 2, 7.1) 0008: MOVE R65, R66 # 1:5 0009: LOADI R64, 257 # 1:5 0010: UPCALL 1, R64 # 1:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -562,8 +553,7 @@ OUT SUM_INTEGERS(3, 2, 7) 0008: MOVE R65, R66 # 1:5 0009: LOADI R64, 258 # 1:5 0010: UPCALL 1, R64 # 1:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -594,8 +584,7 @@ OUT CONCAT$("hello", " ", "world") 0008: MOVE R65, R66 # 1:5 0009: LOADI R64, 259 # 1:5 0010: UPCALL 1, R64 # 1:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -621,8 +610,7 @@ OUT IS_POSITIVE?(42) 0003: MOVE R65, R66 # 1:5 0004: LOADI R64, 256 # 1:5 0005: UPCALL 1, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -646,8 +634,7 @@ OUT MEANING_OF_LIFE 0001: UPCALL 0, R65 # 1:5, MEANING_OF_LIFE 0002: LOADI R64, 258 # 1:5 0003: UPCALL 1, R64 # 1:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 ``` ## Output @@ -680,8 +667,7 @@ OUT x 0008: MOVE R65, R0 # 3:5 0009: LOADI R64, 257 # 3:5 0010: UPCALL 1, R64 # 3:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -710,8 +696,7 @@ OUT x 0004: MOVE R65, R0 # 3:5 0005: LOADI R64, 258 # 3:5 0006: UPCALL 1, R64 # 3:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -747,8 +732,7 @@ OUT SUM_DOUBLES(1.0, 2.0) + SUM_DOUBLES(3.0, 4.0) 0013: ADDD R65, R65, R66 # 1:27 0014: LOADI R64, 257 # 1:5 0015: UPCALL 1, R64 # 1:1, OUT -0016: LOADI R64, 0 # 0:0 -0017: END R64 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -857,7 +841,7 @@ NEXT 0004: CMPLEI R65, R65, R66 # 7:11 0005: JMPF R65, 15 # 7:5 0006: MOVE R68, R64 # 8:20 -0007: CALL R67, 17 # 8:9, MAYBE_EXIT +0007: CALL R67, 16 # 8:9, MAYBE_EXIT 0008: MOVE R66, R67 # 8:9 0009: LOADI R65, 258 # 8:9 0010: UPCALL 0, R65 # 8:5, OUT @@ -865,20 +849,19 @@ NEXT 0012: LOADI R65, 1 # 7:15 0013: ADDI R64, R64, R65 # 7:11 0014: JUMP 2 # 7:5 -0015: LOADI R65, 0 # 0:0 -0016: END R65 # 0:0 +0015: EOF # 0:0 -- MAYBE_EXIT -0017: LOADI R64, 0 # 1:10 -0018: ENTER 4 # 0:0 -0019: LOADI R64, 1 # 2:18 -0020: MOVE R66, R65 # 3:8 -0021: LOADI R67, 2 # 3:12 -0022: CMPGTI R66, R66, R67 # 3:10 -0023: JMPF R66, 25 # 3:8 -0024: JUMP 26 # 3:19 -0025: LOADI R64, 2 # 4:18 -0026: RETURN # 5:1 +0016: LOADI R64, 0 # 1:10 +0017: ENTER 4 # 0:0 +0018: LOADI R64, 1 # 2:18 +0019: MOVE R66, R65 # 3:8 +0020: LOADI R67, 2 # 3:12 +0021: CMPGTI R66, R66, R67 # 3:10 +0022: JMPF R66, 24 # 3:8 +0023: JUMP 25 # 3:19 +0024: LOADI R64, 2 # 4:18 +0025: RETURN # 5:1 ``` ## Output @@ -945,35 +928,34 @@ OUT calls; factorial(5) 0002: MOVE R65, R0 # 6:5 0003: LOADI R64, 274 # 6:5 0004: LOADI R69, 5 # 6:22 -0005: CALL R68, 11 # 6:12, FACTORIAL +0005: CALL R68, 10 # 6:12, FACTORIAL 0006: MOVE R67, R68 # 6:12 0007: LOADI R66, 258 # 6:12 0008: UPCALL 0, R64 # 6:1, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0009: EOF # 0:0 -- FACTORIAL -0011: LOADI R64, 0 # 2:10 -0012: ENTER 6 # 0:0 -0013: MOVE R66, R65 # 3:8 -0014: LOADI R67, 1 # 3:12 -0015: CMPEQI R66, R66, R67 # 3:10 -0016: JMPF R66, 19 # 3:8 -0017: LOADI R64, 1 # 3:31 -0018: JUMP 28 # 3:8 -0019: LOADI R66, 1 # 3:33 -0020: JMPF R66, 28 # 3:33 -0021: MOVE R64, R65 # 3:50 -0022: MOVE R68, R65 # 3:64 -0023: LOADI R69, 1 # 3:68 -0024: SUBI R68, R68, R69 # 3:66 -0025: CALL R67, 11 # 3:54, FACTORIAL -0026: MOVE R66, R67 # 3:54 -0027: MULI R64, R64, R66 # 3:52 -0028: MOVE R0, R0 # 4:13 -0029: LOADI R66, 1 # 4:21 -0030: ADDI R0, R0, R66 # 4:19 -0031: RETURN # 5:1 +0010: LOADI R64, 0 # 2:10 +0011: ENTER 6 # 0:0 +0012: MOVE R66, R65 # 3:8 +0013: LOADI R67, 1 # 3:12 +0014: CMPEQI R66, R66, R67 # 3:10 +0015: JMPF R66, 18 # 3:8 +0016: LOADI R64, 1 # 3:31 +0017: JUMP 27 # 3:8 +0018: LOADI R66, 1 # 3:33 +0019: JMPF R66, 27 # 3:33 +0020: MOVE R64, R65 # 3:50 +0021: MOVE R68, R65 # 3:64 +0022: LOADI R69, 1 # 3:68 +0023: SUBI R68, R68, R69 # 3:66 +0024: CALL R67, 10 # 3:54, FACTORIAL +0025: MOVE R66, R67 # 3:54 +0026: MULI R64, R64, R66 # 3:52 +0027: MOVE R0, R0 # 4:13 +0028: LOADI R66, 1 # 4:21 +0029: ADDI R0, R0, R66 # 4:19 +0030: RETURN # 5:1 ``` ## Output @@ -1013,62 +995,61 @@ OUT ping(3) ```asm 0000: ENTER 4 # 0:0 0001: LOADI R67, 3 # 19:10 -0002: CALL R66, 8 # 19:5, PING +0002: CALL R66, 7 # 19:5, PING 0003: MOVE R65, R66 # 19:5 0004: LOADI R64, 258 # 19:5 0005: UPCALL 0, R64 # 19:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 -- PING -0008: LOADI R64, 0 # 1:10 -0009: ENTER 6 # 0:0 -0010: LOADI R67, 0 # 2:9 -0011: LOADI R66, 275 # 2:9 -0012: MOVE R69, R65 # 2:17 -0013: LOADI R68, 258 # 2:17 -0014: UPCALL 0, R66 # 2:5, OUT -0015: MOVE R66, R65 # 3:8 -0016: LOADI R67, 0 # 3:12 -0017: CMPEQI R66, R66, R67 # 3:10 -0018: JMPF R66, 21 # 3:8 -0019: LOADI R64, 100 # 4:16 -0020: JUMP 30 # 3:8 -0021: LOADI R66, 1 # 5:5 -0022: JMPF R66, 30 # 5:5 -0023: MOVE R67, R65 # 6:21 -0024: LOADI R68, 1 # 6:25 -0025: SUBI R67, R67, R68 # 6:23 -0026: CALL R66, 31 # 6:16, PONG -0027: MOVE R64, R66 # 6:16 -0028: LOADI R66, 1 # 6:30 -0029: ADDI R64, R64, R66 # 6:28 -0030: RETURN # 8:1 +0007: LOADI R64, 0 # 1:10 +0008: ENTER 6 # 0:0 +0009: LOADI R67, 0 # 2:9 +0010: LOADI R66, 275 # 2:9 +0011: MOVE R69, R65 # 2:17 +0012: LOADI R68, 258 # 2:17 +0013: UPCALL 0, R66 # 2:5, OUT +0014: MOVE R66, R65 # 3:8 +0015: LOADI R67, 0 # 3:12 +0016: CMPEQI R66, R66, R67 # 3:10 +0017: JMPF R66, 20 # 3:8 +0018: LOADI R64, 100 # 4:16 +0019: JUMP 29 # 3:8 +0020: LOADI R66, 1 # 5:5 +0021: JMPF R66, 29 # 5:5 +0022: MOVE R67, R65 # 6:21 +0023: LOADI R68, 1 # 6:25 +0024: SUBI R67, R67, R68 # 6:23 +0025: CALL R66, 30 # 6:16, PONG +0026: MOVE R64, R66 # 6:16 +0027: LOADI R66, 1 # 6:30 +0028: ADDI R64, R64, R66 # 6:28 +0029: RETURN # 8:1 -- PONG -0031: LOADI R64, 0 # 10:10 -0032: ENTER 6 # 0:0 -0033: LOADI R67, 1 # 11:9 -0034: LOADI R66, 275 # 11:9 -0035: MOVE R69, R65 # 11:17 -0036: LOADI R68, 258 # 11:17 -0037: UPCALL 0, R66 # 11:5, OUT -0038: MOVE R66, R65 # 12:8 -0039: LOADI R67, 0 # 12:12 -0040: CMPEQI R66, R66, R67 # 12:10 -0041: JMPF R66, 44 # 12:8 -0042: LOADI R64, 200 # 13:16 -0043: JUMP 53 # 12:8 -0044: LOADI R66, 1 # 14:5 -0045: JMPF R66, 53 # 14:5 -0046: MOVE R67, R65 # 15:21 -0047: LOADI R68, 1 # 15:25 -0048: SUBI R67, R67, R68 # 15:23 -0049: CALL R66, 8 # 15:16, PING -0050: MOVE R64, R66 # 15:16 -0051: LOADI R66, 10 # 15:30 -0052: ADDI R64, R64, R66 # 15:28 -0053: RETURN # 17:1 +0030: LOADI R64, 0 # 10:10 +0031: ENTER 6 # 0:0 +0032: LOADI R67, 1 # 11:9 +0033: LOADI R66, 275 # 11:9 +0034: MOVE R69, R65 # 11:17 +0035: LOADI R68, 258 # 11:17 +0036: UPCALL 0, R66 # 11:5, OUT +0037: MOVE R66, R65 # 12:8 +0038: LOADI R67, 0 # 12:12 +0039: CMPEQI R66, R66, R67 # 12:10 +0040: JMPF R66, 43 # 12:8 +0041: LOADI R64, 200 # 13:16 +0042: JUMP 52 # 12:8 +0043: LOADI R66, 1 # 14:5 +0044: JMPF R66, 52 # 14:5 +0045: MOVE R67, R65 # 15:21 +0046: LOADI R68, 1 # 15:25 +0047: SUBI R67, R67, R68 # 15:23 +0048: CALL R66, 7 # 15:16, PING +0049: MOVE R64, R66 # 15:16 +0050: LOADI R66, 10 # 15:30 +0051: ADDI R64, R64, R66 # 15:28 +0052: RETURN # 17:1 ``` ## Output diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index ae13d834..85edd6f9 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -32,8 +32,7 @@ OUT b1, d1, i1, i2, s1 0014: MOVE R73, R4 # 7:21 0015: LOADI R72, 259 # 7:21 0016: UPCALL 0, R64 # 7:1, OUT -0017: LOADI R64, 0 # 0:0 -0018: END R64 # 0:0 +0017: EOF # 0:0 ``` ## Output @@ -86,8 +85,7 @@ OUT b1, d1, i1, i2, s1 0019: MOVE R73, R4 # 12:21 0020: LOADI R72, 259 # 12:21 0021: UPCALL 0, R64 # 12:1, OUT -0022: LOADI R64, 0 # 0:0 -0023: END R64 # 0:0 +0022: EOF # 0:0 ``` ## Output @@ -124,7 +122,7 @@ OUT "After", i1 0005: MOVE R67, R0 # 8:15 0006: LOADI R66, 258 # 8:15 0007: UPCALL 0, R64 # 8:1, OUT -0008: CALL R65, 18 # 9:5, MODIFY_GLOBAL +0008: CALL R65, 17 # 9:5, MODIFY_GLOBAL 0009: LOADI R64, 258 # 9:5 0010: UPCALL 0, R64 # 9:1, OUT 0011: LOADI R65, 1 # 10:5 @@ -132,19 +130,18 @@ OUT "After", i1 0013: MOVE R67, R0 # 10:14 0014: LOADI R66, 258 # 10:14 0015: UPCALL 0, R64 # 10:1, OUT -0016: LOADI R64, 0 # 0:0 -0017: END R64 # 0:0 +0016: EOF # 0:0 -- MODIFY_GLOBAL -0018: LOADI R64, 0 # 1:10 -0019: ENTER 5 # 0:0 -0020: LOADI R0, 3 # 2:10 -0021: LOADI R66, 2 # 3:9 -0022: LOADI R65, 291 # 3:9 -0023: MOVE R68, R0 # 3:25 -0024: LOADI R67, 258 # 3:25 -0025: UPCALL 0, R65 # 3:5, OUT -0026: RETURN # 4:1 +0017: LOADI R64, 0 # 1:10 +0018: ENTER 5 # 0:0 +0019: LOADI R0, 3 # 2:10 +0020: LOADI R66, 2 # 3:9 +0021: LOADI R65, 291 # 3:9 +0022: MOVE R68, R0 # 3:25 +0023: LOADI R67, 258 # 3:25 +0024: UPCALL 0, R65 # 3:5, OUT +0025: RETURN # 4:1 ``` ## Output @@ -184,24 +181,23 @@ OUT "After", i1 0005: MOVE R67, R0 # 8:15 0006: LOADI R66, 258 # 8:15 0007: UPCALL 0, R64 # 8:1, OUT -0008: CALL R64, 16 # 9:1, MODIFY_GLOBAL +0008: CALL R64, 15 # 9:1, MODIFY_GLOBAL 0009: LOADI R65, 1 # 10:5 0010: LOADI R64, 291 # 10:5 0011: MOVE R67, R0 # 10:14 0012: LOADI R66, 258 # 10:14 0013: UPCALL 0, R64 # 10:1, OUT -0014: LOADI R64, 0 # 0:0 -0015: END R64 # 0:0 +0014: EOF # 0:0 -- MODIFY_GLOBAL -0016: ENTER 4 # 0:0 -0017: LOADI R0, 3 # 2:10 -0018: LOADI R65, 2 # 3:9 -0019: LOADI R64, 291 # 3:9 -0020: MOVE R67, R0 # 3:25 -0021: LOADI R66, 258 # 3:25 -0022: UPCALL 0, R64 # 3:5, OUT -0023: RETURN # 4:1 +0015: ENTER 4 # 0:0 +0016: LOADI R0, 3 # 2:10 +0017: LOADI R65, 2 # 3:9 +0018: LOADI R64, 291 # 3:9 +0019: MOVE R67, R0 # 3:25 +0020: LOADI R66, 258 # 3:25 +0021: UPCALL 0, R64 # 3:5, OUT +0022: RETURN # 4:1 ``` ## Output @@ -232,8 +228,7 @@ OUT d 0004: MOVE R65, R0 # 3:5 0005: LOADI R64, 257 # 3:5 0006: UPCALL 0, R64 # 3:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -269,8 +264,7 @@ OUT i1, i2 0009: MOVE R67, R1 # 5:9 0010: LOADI R66, 258 # 5:9 0011: UPCALL 0, R64 # 5:1, OUT -0012: LOADI R64, 0 # 0:0 -0013: END R64 # 0:0 +0012: EOF # 0:0 ``` ## Output @@ -327,21 +321,20 @@ OUT foo ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 6 # 7:5, FOO +0001: CALL R65, 5 # 7:5, FOO 0002: LOADI R64, 258 # 7:5 0003: UPCALL 0, R64 # 7:1, OUT -0004: LOADI R64, 0 # 0:0 -0005: END R64 # 0:0 +0004: EOF # 0:0 -- FOO -0006: LOADI R64, 0 # 1:10 -0007: ENTER 4 # 0:0 -0008: LOADI R65, 5 # 2:9 -0009: LOADI R0, 0 # 3:16 -0010: MOVE R67, R65 # 4:9 -0011: LOADI R66, 258 # 4:9 -0012: UPCALL 0, R66 # 4:5, OUT -0013: RETURN # 5:1 +0005: LOADI R64, 0 # 1:10 +0006: ENTER 4 # 0:0 +0007: LOADI R65, 5 # 2:9 +0008: LOADI R0, 0 # 3:16 +0009: MOVE R67, R65 # 4:9 +0010: LOADI R66, 258 # 4:9 +0011: UPCALL 0, R66 # 4:5, OUT +0012: RETURN # 5:1 ``` ## Output diff --git a/core2/tests/test_gosub.md b/core2/tests/test_gosub.md index 2d9350ab..de518c0f 100644 --- a/core2/tests/test_gosub.md +++ b/core2/tests/test_gosub.md @@ -30,8 +30,7 @@ RETURN 0011: LOADI R64, 259 # 7:5 0012: UPCALL 0, R64 # 7:1, OUT 0013: RETURN # 8:1 -0014: LOADI R64, 0 # 0:0 -0015: END R64 # 0:0 +0014: EOF # 0:0 ``` ## Output @@ -64,8 +63,7 @@ RETURN 0005: LOADI R64, 259 # 3:9 0006: UPCALL 0, R64 # 3:5, OUT 0007: RETURN # 4:1 -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -134,8 +132,7 @@ RETURN 0027: LOADI R64, 259 # 19:5 0028: UPCALL 0, R64 # 19:1, OUT 0029: RETURN # 20:1 -0030: LOADI R64, 0 # 0:0 -0031: END R64 # 0:0 +0030: EOF # 0:0 ``` ## Output @@ -161,10 +158,9 @@ RETURN ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 0 # 0:0 0001: RETURN # 1:1 -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 +0002: EOF # 0:0 ``` ## Runtime errors @@ -198,8 +194,7 @@ GOSUB @s 0004: UPCALL 0, R64 # 4:1, OUT 0005: RETURN # 5:1 0006: GOSUB 2 # 8:7 -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -224,8 +219,7 @@ GOSUB @sub: @sub: OUT 1 0002: LOADI R65, 1 # 1:23 0003: LOADI R64, 258 # 1:23 0004: UPCALL 0, R64 # 1:19, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -247,11 +241,10 @@ RETURN ## Disassembly ```asm -0000: ENTER 1 # 0:0 +0000: ENTER 0 # 0:0 0001: JUMP 2 # 1:6 0002: RETURN # 3:1 -0003: LOADI R64, 0 # 0:0 -0004: END R64 # 0:0 +0003: EOF # 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_goto.md b/core2/tests/test_goto.md index 41c4ccdc..4c8b1ccf 100644 --- a/core2/tests/test_goto.md +++ b/core2/tests/test_goto.md @@ -24,8 +24,7 @@ OUT "c" 0008: LOADI R65, 2 # 5:5 0009: LOADI R64, 259 # 5:5 0010: UPCALL 0, R64 # 5:1, OUT -0011: LOADI R64, 0 # 0:0 -0012: END R64 # 0:0 +0011: EOF # 0:0 ``` ## Output @@ -53,8 +52,7 @@ OUT "a" 0002: LOADI R65, 0 # 2:5 0003: LOADI R64, 259 # 2:5 0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` # Test: GOTO target requires backwards jump @@ -85,8 +83,7 @@ GOTO @print_it 0007: UPCALL 0, R64 # 4:1, OUT 0008: JUMP 10 # 5:6 0009: JUMP 5 # 7:6 -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -116,8 +113,7 @@ OUT "skipped" 0005: LOADI R65, 1 # 3:8 0006: LOADI R64, 259 # 3:8 0007: UPCALL 0, R64 # 3:4, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -146,8 +142,7 @@ OUT "skipped": 20 OUT "target" 0005: LOADI R65, 1 # 2:23 0006: LOADI R64, 259 # 2:23 0007: UPCALL 0, R64 # 2:19, OUT -0008: LOADI R64, 0 # 0:0 -0009: END R64 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -249,8 +244,7 @@ GOTO @again 0009: LOADI R65, 1 # 4:9 0010: ADDI R64, R64, R65 # 4:7 0011: JUMP 2 # 5:6 -0012: LOADI R65, 0 # 0:0 -0013: END R65 # 0:0 +0012: EOF # 0:0 ``` ## Exit code diff --git a/core2/tests/test_if.md b/core2/tests/test_if.md index ed5874df..9ad34241 100644 --- a/core2/tests/test_if.md +++ b/core2/tests/test_if.md @@ -17,8 +17,7 @@ END IF 0003: LOADI R65, 0 # 2:9 0004: LOADI R64, 259 # 2:9 0005: UPCALL 0, R64 # 2:5, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -50,8 +49,7 @@ OUT "after" 0006: LOADI R65, 1 # 4:5 0007: LOADI R64, 259 # 4:5 0008: UPCALL 0, R64 # 4:1, OUT -0009: LOADI R64, 0 # 0:0 -0010: END R64 # 0:0 +0009: EOF # 0:0 ``` ## Output @@ -99,8 +97,7 @@ END IF 0017: LOADI R67, 2 # 8:9 0018: LOADI R66, 259 # 8:9 0019: UPCALL 0, R66 # 8:5, OUT -0020: LOADI R66, 0 # 0:0 -0021: END R66 # 0:0 +0020: EOF # 0:0 ``` ## Output @@ -140,8 +137,7 @@ END IF 0011: LOADI R67, 1 # 6:9 0012: LOADI R66, 259 # 6:9 0013: UPCALL 0, R66 # 6:5, OUT -0014: LOADI R66, 0 # 0:0 -0015: END R66 # 0:0 +0014: EOF # 0:0 ``` ## Output @@ -179,8 +175,7 @@ END IF 0010: LOADI R66, 1 # 5:9 0011: LOADI R65, 259 # 5:9 0012: UPCALL 0, R65 # 5:5, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -216,8 +211,7 @@ END IF 0007: LOADI R67, 0 # 5:13 0008: LOADI R66, 259 # 5:13 0009: UPCALL 0, R66 # 5:9, OUT -0010: LOADI R66, 0 # 0:0 -0011: END R66 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -261,8 +255,7 @@ END IF 0013: LOADI R67, 1 # 7:13 0014: LOADI R66, 259 # 7:13 0015: UPCALL 0, R66 # 7:9, OUT -0016: LOADI R66, 0 # 0:0 -0017: END R66 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -304,8 +297,7 @@ END IF 0013: LOADI R66, 1 # 6:9 0014: LOADI R65, 259 # 6:9 0015: UPCALL 0, R65 # 6:5, OUT -0016: LOADI R65, 0 # 0:0 -0017: END R65 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -347,8 +339,7 @@ END IF 0013: LOADI R66, 1 # 6:9 0014: LOADI R65, 259 # 6:9 0015: UPCALL 0, R65 # 6:5, OUT -0016: LOADI R65, 0 # 0:0 -0017: END R65 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -408,8 +399,7 @@ END IF 0028: LOADI R66, 3 # 9:9 0029: LOADI R65, 259 # 9:9 0030: UPCALL 0, R65 # 9:5, OUT -0031: LOADI R65, 0 # 0:0 -0032: END R65 # 0:0 +0031: EOF # 0:0 ``` ## Output @@ -469,8 +459,7 @@ END IF 0028: LOADI R66, 3 # 9:9 0029: LOADI R65, 259 # 9:9 0030: UPCALL 0, R65 # 9:5, OUT -0031: LOADI R65, 0 # 0:0 -0032: END R65 # 0:0 +0031: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_on_error.md b/core2/tests/test_on_error.md index 28161973..11a6f969 100644 --- a/core2/tests/test_on_error.md +++ b/core2/tests/test_on_error.md @@ -29,8 +29,7 @@ OUT 2 0013: UPCALL 2, R65 # 5:9, LAST_ERROR 0014: LOADI R64, 259 # 5:9 0015: UPCALL 0, R64 # 5:5, OUT -0016: LOADI R64, 0 # 0:0 -0017: END R64 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -72,8 +71,7 @@ OUT LAST_ERROR 0013: UPCALL 2, R65 # 6:5, LAST_ERROR 0014: LOADI R64, 259 # 6:5 0015: UPCALL 0, R64 # 6:1, OUT -0016: LOADI R64, 0 # 0:0 -0017: END R64 # 0:0 +0016: EOF # 0:0 ``` ## Output @@ -119,8 +117,7 @@ OUT RAISEF("internal") 0016: MOVE R65, R66 # 7:5 0017: LOADI R64, 256 # 7:5 0018: UPCALL 0, R64 # 7:1, OUT -0019: LOADI R64, 0 # 0:0 -0020: END R64 # 0:0 +0019: EOF # 0:0 ``` ## Runtime errors @@ -163,8 +160,7 @@ OUT LAST_ERROR 0010: UPCALL 2, R65 # 4:5, LAST_ERROR 0011: LOADI R64, 259 # 4:5 0012: UPCALL 0, R64 # 4:1, OUT -0013: LOADI R64, 0 # 0:0 -0014: END R64 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -198,8 +194,7 @@ OUT LAST_ERROR 0007: UPCALL 2, R65 # 4:5, LAST_ERROR 0008: LOADI R64, 259 # 4:5 0009: UPCALL 0, R64 # 4:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -234,8 +229,7 @@ OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR 0010: UPCALL 2, R65 # 2:36, LAST_ERROR 0011: LOADI R64, 259 # 2:36 0012: UPCALL 0, R64 # 2:32, OUT -0013: LOADI R64, 0 # 0:0 -0014: END R64 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -267,8 +261,7 @@ OUT 1: RAISE "internal": OUT LAST_ERROR 0007: UPCALL 2, R65 # 2:30, LAST_ERROR 0008: LOADI R64, 259 # 2:30 0009: UPCALL 0, R64 # 2:26, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -299,8 +292,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR 0007: UPCALL 2, R65 # 1:51, LAST_ERROR 0008: LOADI R64, 259 # 1:51 0009: UPCALL 1, R64 # 1:47, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -330,8 +322,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR 0007: UPCALL 2, R65 # 1:47, LAST_ERROR 0008: LOADI R64, 259 # 1:47 0009: UPCALL 1, R64 # 1:43, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -361,8 +352,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR 0007: UPCALL 2, R65 # 1:51, LAST_ERROR 0008: LOADI R64, 259 # 1:51 0009: UPCALL 1, R64 # 1:47, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output @@ -392,8 +382,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR 0007: UPCALL 2, R65 # 1:45, LAST_ERROR 0008: LOADI R64, 259 # 1:45 0009: UPCALL 1, R64 # 1:41, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_eq.md b/core2/tests/test_relational_eq.md index 5fd41af8..bfa3221b 100644 --- a/core2/tests/test_relational_eq.md +++ b/core2/tests/test_relational_eq.md @@ -15,8 +15,7 @@ OUT 2 = 2 0003: CMPEQI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 = 2.0 0003: CMPEQD R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 = 2.0 0004: CMPEQD R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" = "bar" 0003: CMPEQS R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -124,8 +120,7 @@ OUT TRUE = FALSE 0003: CMPEQB R65, R65, R66 # 1:10 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_ge.md b/core2/tests/test_relational_ge.md index 355c06f4..63e74c9f 100644 --- a/core2/tests/test_relational_ge.md +++ b/core2/tests/test_relational_ge.md @@ -15,8 +15,7 @@ OUT 3 >= 2 0003: CMPGEI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 >= 2.0 0003: CMPGED R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 >= 2.5 0004: CMPGED R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" >= "bar" 0003: CMPGES R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_gt.md b/core2/tests/test_relational_gt.md index e653bb93..f8519b7f 100644 --- a/core2/tests/test_relational_gt.md +++ b/core2/tests/test_relational_gt.md @@ -15,8 +15,7 @@ OUT 3 > 2 0003: CMPGTI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 > 2.0 0003: CMPGTD R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 > 2.5 0004: CMPGTD R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" > "bar" 0003: CMPGTS R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_le.md b/core2/tests/test_relational_le.md index 9f437eaf..b911dc5f 100644 --- a/core2/tests/test_relational_le.md +++ b/core2/tests/test_relational_le.md @@ -15,8 +15,7 @@ OUT 2 <= 3 0003: CMPLEI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 <= 2.0 0003: CMPLED R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 <= 2.5 0004: CMPLED R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" <= "bar" 0003: CMPLES R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_lt.md b/core2/tests/test_relational_lt.md index 1cf3a112..f2d4c8b0 100644 --- a/core2/tests/test_relational_lt.md +++ b/core2/tests/test_relational_lt.md @@ -15,8 +15,7 @@ OUT 2 < 3 0003: CMPLTI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 < 2.0 0003: CMPLTD R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 < 2.5 0004: CMPLTD R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" < "bar" 0003: CMPLTS R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_relational_ne.md b/core2/tests/test_relational_ne.md index 559b263e..2ea917f3 100644 --- a/core2/tests/test_relational_ne.md +++ b/core2/tests/test_relational_ne.md @@ -15,8 +15,7 @@ OUT 2 <> 3 0003: CMPNEI R65, R65, R66 # 1:7 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -42,8 +41,7 @@ OUT 2.5 <> 2.5 0003: CMPNED R65, R65, R66 # 1:9 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -70,8 +68,7 @@ OUT 2 <> 2.5 0004: CMPNED R65, R65, R66 # 1:7 0005: LOADI R64, 256 # 1:5 0006: UPCALL 0, R64 # 1:1, OUT -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` ## Output @@ -97,8 +94,7 @@ OUT "foo" <> "bar" 0003: CMPNES R65, R65, R66 # 1:11 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output @@ -124,8 +120,7 @@ OUT TRUE <> FALSE 0003: CMPNEB R65, R65, R66 # 1:10 0004: LOADI R64, 256 # 1:5 0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R64, 0 # 0:0 -0007: END R64 # 0:0 +0006: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_select.md b/core2/tests/test_select.md index b72de09e..14bcb926 100644 --- a/core2/tests/test_select.md +++ b/core2/tests/test_select.md @@ -113,8 +113,7 @@ END SELECT 0089: LOADI R66, 4 # 12:13 0090: LOADI R65, 259 # 12:13 0091: UPCALL 0, R65 # 12:9, OUT -0092: LOADI R65, 0 # 0:0 -0093: END R65 # 0:0 +0092: EOF # 0:0 ``` ## Output @@ -158,8 +157,7 @@ END SELECT 0015: LOADI R66, 1 # 6:13 0016: LOADI R65, 259 # 6:13 0017: UPCALL 0, R65 # 6:9, OUT -0018: LOADI R65, 0 # 0:0 -0019: END R65 # 0:0 +0018: EOF # 0:0 ``` ## Output @@ -208,8 +206,7 @@ END SELECT 0021: LOADI R65, 1 # 5:13 0022: LOADI R64, 259 # 5:13 0023: UPCALL 1, R64 # 5:9, OUT -0024: LOADI R64, 0 # 0:0 -0025: END R64 # 0:0 +0024: EOF # 0:0 ``` ## Output @@ -235,8 +232,7 @@ END SELECT 0002: LOADI R65, 1 # 1:31 0003: ADDI R64, R64, R65 # 1:29 0004: JUMP 5 # 2:1 -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0005: EOF # 0:0 ``` # Test: CASE IS and TO with strings @@ -295,8 +291,7 @@ END SELECT 0034: LOADI R66, 6 # 8:13 0035: LOADI R65, 259 # 8:13 0036: UPCALL 0, R65 # 8:9, OUT -0037: LOADI R65, 0 # 0:0 -0038: END R65 # 0:0 +0037: EOF # 0:0 ``` ## Output @@ -347,8 +342,7 @@ END SELECT 0022: LOADI R66, 2 # 6:13 0023: LOADI R65, 259 # 6:13 0024: UPCALL 0, R65 # 6:9, OUT -0025: LOADI R65, 0 # 0:0 -0026: END R65 # 0:0 +0025: EOF # 0:0 ``` ## Output @@ -424,8 +418,7 @@ END SELECT 0045: LOADI R66, 6 # 8:13 0046: LOADI R65, 259 # 8:13 0047: UPCALL 0, R65 # 8:9, OUT -0048: LOADI R65, 0 # 0:0 -0049: END R65 # 0:0 +0048: EOF # 0:0 ``` ## Output @@ -491,8 +484,7 @@ END SELECT 0032: LOADI R66, 2 # 11:13 0033: LOADI R65, 259 # 11:13 0034: UPCALL 0, R65 # 11:9, OUT -0035: LOADI R65, 0 # 0:0 -0036: END R65 # 0:0 +0035: EOF # 0:0 ``` ## Output @@ -567,8 +559,7 @@ RETURN 0035: LOADI R65, 259 # 14:13 0036: UPCALL 0, R65 # 14:9, OUT 0037: RETURN # 16:1 -0038: LOADI R65, 0 # 0:0 -0039: END R65 # 0:0 +0038: EOF # 0:0 ``` ## Output @@ -632,8 +623,7 @@ OUT "done" 0019: LOADI R65, 1 # 6:5 0020: LOADI R64, 259 # 6:5 0021: UPCALL 0, R64 # 6:1, OUT -0022: LOADI R64, 0 # 0:0 -0023: END R64 # 0:0 +0022: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_strings.md b/core2/tests/test_strings.md index 50d9d07e..dd533e9d 100644 --- a/core2/tests/test_strings.md +++ b/core2/tests/test_strings.md @@ -39,8 +39,7 @@ OUT c4 0018: MOVE R69, R67 # 10:5 0019: LOADI R68, 259 # 10:5 0020: UPCALL 0, R68 # 10:1, OUT -0021: LOADI R68, 0 # 0:0 -0022: END R68 # 0:0 +0021: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index d76deac9..a9958745 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -25,24 +25,23 @@ OUT "After", a 0004: MOVE R68, R64 # 8:15 0005: LOADI R67, 258 # 8:15 0006: UPCALL 0, R65 # 8:1, OUT -0007: CALL R65, 15 # 9:1, FOO +0007: CALL R65, 14 # 9:1, FOO 0008: LOADI R66, 1 # 10:5 0009: LOADI R65, 291 # 10:5 0010: MOVE R68, R64 # 10:14 0011: LOADI R67, 258 # 10:14 0012: UPCALL 0, R65 # 10:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 -- FOO -0015: ENTER 5 # 0:0 -0016: LOADI R64, 20 # 4:9 -0017: LOADI R66, 2 # 5:9 -0018: LOADI R65, 291 # 5:9 -0019: MOVE R68, R64 # 5:19 -0020: LOADI R67, 258 # 5:19 -0021: UPCALL 0, R65 # 5:5, OUT -0022: RETURN # 6:1 +0014: ENTER 5 # 0:0 +0015: LOADI R64, 20 # 4:9 +0016: LOADI R66, 2 # 5:9 +0017: LOADI R65, 291 # 5:9 +0018: MOVE R68, R64 # 5:19 +0019: LOADI R67, 258 # 5:19 +0020: UPCALL 0, R65 # 5:5, OUT +0021: RETURN # 6:1 ``` ## Output @@ -72,22 +71,21 @@ second ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: CALL R64, 9 # 9:1, SECOND -0002: LOADI R64, 0 # 0:0 -0003: END R64 # 0:0 +0000: ENTER 0 # 0:0 +0001: CALL R64, 8 # 9:1, SECOND +0002: EOF # 0:0 -- FIRST -0004: ENTER 2 # 0:0 -0005: LOADI R65, 0 # 2:9 -0006: LOADI R64, 259 # 2:9 -0007: UPCALL 0, R64 # 2:5, OUT -0008: RETURN # 3:1 +0003: ENTER 2 # 0:0 +0004: LOADI R65, 0 # 2:9 +0005: LOADI R64, 259 # 2:9 +0006: UPCALL 0, R64 # 2:5, OUT +0007: RETURN # 3:1 -- SECOND -0009: ENTER 0 # 0:0 -0010: CALL R64, 4 # 6:5, FIRST -0011: RETURN # 7:1 +0008: ENTER 0 # 0:0 +0009: CALL R64, 3 # 6:5, FIRST +0010: RETURN # 7:1 ``` ## Output @@ -143,40 +141,39 @@ OUT "After modify_1", var 0004: MOVE R68, R64 # 14:24 0005: LOADI R67, 258 # 14:24 0006: UPCALL 0, R65 # 14:1, OUT -0007: CALL R65, 23 # 15:1, MODIFY_1 +0007: CALL R65, 22 # 15:1, MODIFY_1 0008: LOADI R66, 1 # 16:5 0009: LOADI R65, 291 # 16:5 0010: MOVE R68, R64 # 16:23 0011: LOADI R67, 258 # 16:23 0012: UPCALL 0, R65 # 16:1, OUT -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 -- MODIFY_2 -0015: ENTER 5 # 0:0 -0016: LOADI R64, 2 # 2:11 -0017: LOADI R66, 2 # 3:9 -0018: LOADI R65, 291 # 3:9 -0019: MOVE R68, R64 # 3:28 -0020: LOADI R67, 258 # 3:28 -0021: UPCALL 0, R65 # 3:5, OUT -0022: RETURN # 4:1 +0014: ENTER 5 # 0:0 +0015: LOADI R64, 2 # 2:11 +0016: LOADI R66, 2 # 3:9 +0017: LOADI R65, 291 # 3:9 +0018: MOVE R68, R64 # 3:28 +0019: LOADI R67, 258 # 3:28 +0020: UPCALL 0, R65 # 3:5, OUT +0021: RETURN # 4:1 -- MODIFY_1 -0023: ENTER 5 # 0:0 -0024: LOADI R64, 1 # 7:11 -0025: LOADI R66, 3 # 8:9 -0026: LOADI R65, 291 # 8:9 -0027: MOVE R68, R64 # 8:28 -0028: LOADI R67, 258 # 8:28 -0029: UPCALL 0, R65 # 8:5, OUT -0030: CALL R65, 15 # 9:5, MODIFY_2 -0031: LOADI R66, 4 # 10:9 -0032: LOADI R65, 291 # 10:9 -0033: MOVE R68, R64 # 10:27 -0034: LOADI R67, 258 # 10:27 -0035: UPCALL 0, R65 # 10:5, OUT -0036: RETURN # 11:1 +0022: ENTER 5 # 0:0 +0023: LOADI R64, 1 # 7:11 +0024: LOADI R66, 3 # 8:9 +0025: LOADI R65, 291 # 8:9 +0026: MOVE R68, R64 # 8:28 +0027: LOADI R67, 258 # 8:28 +0028: UPCALL 0, R65 # 8:5, OUT +0029: CALL R65, 14 # 9:5, MODIFY_2 +0030: LOADI R66, 4 # 10:9 +0031: LOADI R65, 291 # 10:9 +0032: MOVE R68, R64 # 10:27 +0033: LOADI R67, 258 # 10:27 +0034: UPCALL 0, R65 # 10:5, OUT +0035: RETURN # 11:1 ``` ## Output @@ -294,28 +291,27 @@ NEXT 0004: CMPLEI R65, R65, R66 # 7:11 0005: JMPF R65, 12 # 7:5 0006: MOVE R65, R64 # 8:16 -0007: CALL R65, 14 # 8:5, MAYBE_EXIT +0007: CALL R65, 13 # 8:5, MAYBE_EXIT 0008: MOVE R64, R64 # 7:5 0009: LOADI R65, 1 # 7:15 0010: ADDI R64, R64, R65 # 7:11 0011: JUMP 2 # 7:5 -0012: LOADI R65, 0 # 0:0 -0013: END R65 # 0:0 +0012: EOF # 0:0 -- MAYBE_EXIT -0014: ENTER 3 # 0:0 -0015: LOADI R66, 1 # 2:9 -0016: LOADI R65, 258 # 2:9 -0017: UPCALL 0, R65 # 2:5, OUT -0018: MOVE R65, R64 # 3:8 -0019: LOADI R66, 2 # 3:12 -0020: CMPGTI R65, R65, R66 # 3:10 -0021: JMPF R65, 23 # 3:8 -0022: JUMP 26 # 3:19 -0023: LOADI R66, 2 # 4:9 -0024: LOADI R65, 258 # 4:9 -0025: UPCALL 0, R65 # 4:5, OUT -0026: RETURN # 5:1 +0013: ENTER 3 # 0:0 +0014: LOADI R66, 1 # 2:9 +0015: LOADI R65, 258 # 2:9 +0016: UPCALL 0, R65 # 2:5, OUT +0017: MOVE R65, R64 # 3:8 +0018: LOADI R66, 2 # 3:12 +0019: CMPGTI R65, R65, R66 # 3:10 +0020: JMPF R65, 22 # 3:8 +0021: JUMP 25 # 3:19 +0022: LOADI R66, 2 # 4:9 +0023: LOADI R65, 258 # 4:9 +0024: UPCALL 0, R65 # 4:5, OUT +0025: RETURN # 5:1 ``` ## Output @@ -388,27 +384,26 @@ count_down "counter is" 0001: LOADI R0, 0 # 1:12 0002: LOADI R0, 3 # 9:11 0003: LOADI R64, 0 # 10:12 -0004: CALL R64, 7 # 10:1, COUNT_DOWN -0005: LOADI R64, 0 # 0:0 -0006: END R64 # 0:0 +0004: CALL R64, 6 # 10:1, COUNT_DOWN +0005: EOF # 0:0 -- COUNT_DOWN -0007: ENTER 5 # 0:0 -0008: MOVE R66, R64 # 3:9 -0009: LOADI R65, 275 # 3:9 -0010: MOVE R68, R0 # 3:17 -0011: LOADI R67, 258 # 3:17 -0012: UPCALL 0, R65 # 3:5, OUT -0013: MOVE R65, R0 # 4:8 -0014: LOADI R66, 1 # 4:18 -0015: CMPGTI R65, R65, R66 # 4:16 -0016: JMPF R65, 22 # 4:8 -0017: MOVE R0, R0 # 5:19 -0018: LOADI R65, 1 # 5:29 -0019: SUBI R0, R0, R65 # 5:27 -0020: MOVE R65, R64 # 6:20 -0021: CALL R65, 7 # 6:9, COUNT_DOWN -0022: RETURN # 8:1 +0006: ENTER 5 # 0:0 +0007: MOVE R66, R64 # 3:9 +0008: LOADI R65, 275 # 3:9 +0009: MOVE R68, R0 # 3:17 +0010: LOADI R67, 258 # 3:17 +0011: UPCALL 0, R65 # 3:5, OUT +0012: MOVE R65, R0 # 4:8 +0013: LOADI R66, 1 # 4:18 +0014: CMPGTI R65, R65, R66 # 4:16 +0015: JMPF R65, 21 # 4:8 +0016: MOVE R0, R0 # 5:19 +0017: LOADI R65, 1 # 5:29 +0018: SUBI R0, R0, R65 # 5:27 +0019: MOVE R65, R64 # 6:20 +0020: CALL R65, 6 # 6:9, COUNT_DOWN +0021: RETURN # 8:1 ``` ## Output @@ -451,46 +446,45 @@ OUT value 0000: ENTER 4 # 0:0 0001: LOADI R0, 0 # 1:12 0002: LOADI R67, 2 # 18:17 -0003: CALL R66, 12 # 18:5, COUNT_VALUE +0003: CALL R66, 11 # 18:5, COUNT_VALUE 0004: MOVE R65, R66 # 18:5 0005: LOADI R64, 258 # 18:5 0006: UPCALL 0, R64 # 18:1, OUT 0007: MOVE R65, R0 # 19:5 0008: LOADI R64, 258 # 19:5 0009: UPCALL 0, R64 # 19:1, OUT -0010: LOADI R64, 0 # 0:0 -0011: END R64 # 0:0 +0010: EOF # 0:0 -- COUNT_VALUE -0012: LOADI R64, 0 # 3:10 -0013: ENTER 4 # 0:0 -0014: MOVE R0, R0 # 4:13 -0015: LOADI R66, 1 # 4:21 -0016: ADDI R0, R0, R66 # 4:19 -0017: MOVE R66, R65 # 5:8 -0018: LOADI R67, 0 # 5:12 -0019: CMPEQI R66, R66, R67 # 5:10 -0020: JMPF R66, 23 # 5:8 -0021: MOVE R64, R0 # 6:23 -0022: JUMP 30 # 5:8 -0023: LOADI R66, 1 # 7:5 -0024: JMPF R66, 30 # 7:5 -0025: MOVE R66, R65 # 8:20 -0026: LOADI R67, 1 # 8:24 -0027: SUBI R66, R66, R67 # 8:22 -0028: CALL R66, 31 # 8:9, BUMP_VALUE -0029: MOVE R64, R0 # 9:23 -0030: RETURN # 11:1 +0011: LOADI R64, 0 # 3:10 +0012: ENTER 4 # 0:0 +0013: MOVE R0, R0 # 4:13 +0014: LOADI R66, 1 # 4:21 +0015: ADDI R0, R0, R66 # 4:19 +0016: MOVE R66, R65 # 5:8 +0017: LOADI R67, 0 # 5:12 +0018: CMPEQI R66, R66, R67 # 5:10 +0019: JMPF R66, 22 # 5:8 +0020: MOVE R64, R0 # 6:23 +0021: JUMP 29 # 5:8 +0022: LOADI R66, 1 # 7:5 +0023: JMPF R66, 29 # 7:5 +0024: MOVE R66, R65 # 8:20 +0025: LOADI R67, 1 # 8:24 +0026: SUBI R66, R66, R67 # 8:22 +0027: CALL R66, 30 # 8:9, BUMP_VALUE +0028: MOVE R64, R0 # 9:23 +0029: RETURN # 11:1 -- BUMP_VALUE -0031: ENTER 3 # 0:0 -0032: MOVE R0, R0 # 14:13 -0033: LOADI R65, 10 # 14:21 -0034: ADDI R0, R0, R65 # 14:19 -0035: MOVE R66, R64 # 15:25 -0036: CALL R65, 12 # 15:13, COUNT_VALUE -0037: MOVE R0, R65 # 15:13 -0038: RETURN # 16:1 +0030: ENTER 3 # 0:0 +0031: MOVE R0, R0 # 14:13 +0032: LOADI R65, 10 # 14:21 +0033: ADDI R0, R0, R65 # 14:19 +0034: MOVE R66, R64 # 15:25 +0035: CALL R65, 11 # 15:13, COUNT_VALUE +0036: MOVE R0, R65 # 15:13 +0037: RETURN # 16:1 ``` ## Output diff --git a/core2/tests/test_types.md b/core2/tests/test_types.md index cda0ebff..88b8daca 100644 --- a/core2/tests/test_types.md +++ b/core2/tests/test_types.md @@ -19,8 +19,7 @@ OUT bool_1, bool_2 0005: MOVE R69, R65 # 3:13 0006: LOADI R68, 256 # 3:13 0007: UPCALL 0, R66 # 3:1, OUT -0008: LOADI R66, 0 # 0:0 -0009: END R66 # 0:0 +0008: EOF # 0:0 ``` ## Output @@ -58,8 +57,7 @@ OUT zero_double, small_double, large_double, tiny_double 0011: MOVE R75, R67 # 5:46 0012: LOADI R74, 257 # 5:46 0013: UPCALL 0, R68 # 5:1, OUT -0014: LOADI R68, 0 # 0:0 -0015: END R68 # 0:0 +0014: EOF # 0:0 ``` ## Output @@ -85,8 +83,7 @@ OUT small_int 0002: MOVE R66, R64 # 2:5 0003: LOADI R65, 258 # 2:5 0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -112,8 +109,7 @@ OUT large_int 0002: MOVE R66, R64 # 2:5 0003: LOADI R65, 258 # 2:5 0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Output @@ -139,8 +135,7 @@ OUT text 0002: MOVE R66, R64 # 2:5 0003: LOADI R65, 259 # 2:5 0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R65, 0 # 0:0 -0006: END R65 # 0:0 +0005: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_while.md b/core2/tests/test_while.md index 6b5093bb..a93b2f47 100644 --- a/core2/tests/test_while.md +++ b/core2/tests/test_while.md @@ -26,8 +26,7 @@ WEND 0010: LOADI R65, 1 # 4:13 0011: SUBI R64, R64, R65 # 4:11 0012: JUMP 2 # 2:7 -0013: LOADI R65, 0 # 0:0 -0014: END R65 # 0:0 +0013: EOF # 0:0 ``` ## Output @@ -58,8 +57,7 @@ WEND 0004: LOADI R64, 258 # 2:9 0005: UPCALL 0, R64 # 2:5, OUT 0006: JUMP 1 # 1:7 -0007: LOADI R64, 0 # 0:0 -0008: END R64 # 0:0 +0007: EOF # 0:0 ``` # Test: WHILE guard must be boolean @@ -163,8 +161,7 @@ OUT 9 0017: LOADI R66, 9 # 9:5 0018: LOADI R65, 258 # 9:5 0019: UPCALL 0, R65 # 9:1, OUT -0020: LOADI R65, 0 # 0:0 -0021: END R65 # 0:0 +0020: EOF # 0:0 ``` ## Output diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index 498ea638..7347e83f 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -347,6 +347,7 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() while stop.is_none() { match vm.exec() { StopReason::End(code) => stop = Some(Ok(code.to_i32())), + StopReason::Eof => stop = Some(Ok(0)), StopReason::Upcall(handle) => { if let Err(e) = handle.invoke().await { stop = Some(Err(e.to_string())); From 64e957365fd8ad6954ea7d020b531ef05305d781 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 20 Mar 2026 16:06:49 +0100 Subject: [PATCH 35/53] core2: Wrap CallableMetadata in Rc Up until now, we have been able to track lifetimes for CallableMetadata objects. Cloning was only necessary when producing errors, which was a good compromise. However, as I try to make the compiler incremental, these lifetimes are getting in the way. The real problem stems from the Callable trait abstraction, which mixes metadata needed by the compiler and runtime behavior needed by the VM. Untangling that is a bigger lift than what core2 is trying to achieve so, for now, rely on Rc to make CallableMetadata cheap to copy. --- core2/src/callable.rs | 74 ++++++++++-- core2/src/compiler/args.rs | 112 +++++------------- core2/src/compiler/exprs.rs | 14 +-- core2/src/compiler/mod.rs | 5 + core2/src/compiler/syms.rs | 53 ++++----- core2/src/compiler/top.rs | 26 ++-- core2/src/testutils.rs | 6 +- core2/src/vm/mod.rs | 6 +- core2/tests/testutils/callables/concat_fn.rs | 6 +- .../callables/define_and_change_args_cmd.rs | 6 +- .../testutils/callables/define_arg_cmd.rs | 6 +- .../tests/testutils/callables/get_data_cmd.rs | 6 +- .../callables/increment_required_int_cmd.rs | 6 +- .../testutils/callables/is_positive_fn.rs | 6 +- .../testutils/callables/last_error_fn.rs | 6 +- .../testutils/callables/meaning_of_life_fn.rs | 6 +- .../testutils/callables/out_any_value_cmd.rs | 6 +- .../callables/out_any_value_optional_cmd.rs | 6 +- core2/tests/testutils/callables/out_cmd.rs | 6 +- .../testutils/callables/out_optional_cmd.rs | 6 +- .../testutils/callables/out_positional_cmd.rs | 6 +- .../callables/out_required_value_cmd.rs | 6 +- core2/tests/testutils/callables/raise_cmd.rs | 6 +- core2/tests/testutils/callables/raisef_fn.rs | 6 +- .../testutils/callables/sum_doubles_fn.rs | 6 +- .../testutils/callables/sum_integers_fn.rs | 6 +- 26 files changed, 204 insertions(+), 200 deletions(-) diff --git a/core2/src/callable.rs b/core2/src/callable.rs index ae4d22a8..2cb02875 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -26,6 +26,7 @@ use async_trait::async_trait; use std::borrow::Cow; use std::fmt; use std::ops::RangeInclusive; +use std::rc::Rc; use std::str::Lines; /// Error types for callable execution. @@ -452,30 +453,30 @@ impl CallableMetadataBuilder { } /// Generates the final `CallableMetadata` object, ensuring all values are present. - pub fn build(self) -> CallableMetadata { + pub fn build(self) -> Rc { assert!(!self.syntaxes.is_empty(), "All callables must specify a syntax"); - CallableMetadata { + Rc::from(CallableMetadata { name: self.name, return_type: self.return_type, syntaxes: self.syntaxes, category: self.category.expect("All callables must specify a category"), description: self.description.expect("All callables must specify a description"), - } + }) } /// Generates the final `CallableMetadata` object, ensuring the minimal set of values are /// present. Only useful for testing. - pub fn test_build(mut self) -> CallableMetadata { + pub fn test_build(mut self) -> Rc { if self.syntaxes.is_empty() { self.syntaxes.push(CallableSyntax::new_static(&[], None)); } - CallableMetadata { + Rc::from(CallableMetadata { name: self.name, return_type: self.return_type, syntaxes: self.syntaxes, category: self.category.unwrap_or(""), description: self.description.unwrap_or(""), - } + }) } } @@ -533,9 +534,62 @@ impl CallableMetadata { } } - /// Returns the callable's syntax definitions. - pub(crate) fn syntaxes(&self) -> &[CallableSyntax] { - &self.syntaxes + /// Returns true if `sep` is valid for a function call (only `Long` and `End` are allowed because + /// the parser only produces comma separators for function arguments). + fn is_function_sep(sep: &ArgSepSyntax) -> bool { + match sep { + ArgSepSyntax::Exactly(ArgSep::Long) | ArgSepSyntax::End => true, + ArgSepSyntax::OneOf(seps) => seps.iter().all(|s| *s == ArgSep::Long), + _ => false, + } + } + + /// Checks that the syntax of a callable that returns a value only uses separators that can appear + /// in a function call (i.e. the comma separator). The parser only produces `ArgSep::Long` for + /// function arguments, so any other separator in the metadata would be dead/untestable. + fn debug_assert_function_seps(&self, syntax: &CallableSyntax) { + if self.return_type().is_none() { + return; + } + for syn in syntax.singular.iter() { + let sep = match syn { + SingularArgSyntax::RequiredValue(_, sep) => sep, + SingularArgSyntax::RequiredRef(_, sep) => sep, + SingularArgSyntax::OptionalValue(_, sep) => sep, + SingularArgSyntax::AnyValue(_, sep) => sep, + }; + debug_assert!( + Self::is_function_sep(sep), + "Function {} has a non-comma separator in its singular args syntax", + self.name() + ); + } + if let Some(repeated) = syntax.repeated.as_ref() { + debug_assert!( + Self::is_function_sep(&repeated.sep), + "Function {} has a non-comma separator in its repeated args syntax", + self.name() + ); + } + } + + /// Finds the syntax definition that matches the given argument count. + /// + /// Returns an error if no syntax matches, and panics if multiple syntaxes match (which would + /// indicate an ambiguous callable definition). + pub(crate) fn find_syntax(&self, nargs: usize) -> Option<&CallableSyntax> { + let mut matches = self.syntaxes.iter().filter(|s| s.expected_nargs().contains(&nargs)); + let syntax = matches.next(); + match syntax { + Some(syntax) => { + debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); + if cfg!(debug_assertions) { + self.debug_assert_function_seps(syntax); + } + Some(syntax) + } + None => None, + } } /// Gets the callable's category as a collection of lines. The first line is the title of the @@ -833,7 +887,7 @@ pub trait Callable { /// /// The return value takes the form of a reference to force the callable to store the metadata /// as a struct field so that calls to this function are guaranteed to be cheap. - fn metadata(&self) -> &CallableMetadata; + fn metadata(&self) -> Rc; /// Executes the function. /// diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 9af234d1..bd42f1e5 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -15,76 +15,18 @@ //! Common compilers for callable arguments. +use super::SymbolKey; +use super::syms::LocalSymtable; use crate::ast::{ArgSpan, CallSpan, Expr, VarRef}; use crate::bytecode::{self, Register}; -use crate::callable::{CallableMetadata, CallableSyntax}; +use crate::callable::CallableMetadata; use crate::compiler::codegen::Codegen; use crate::compiler::exprs::{compile_expr, compile_expr_as_type}; use crate::compiler::syms::{self, SymbolPrototype, TempSymtable}; use crate::compiler::{Error, Result}; use crate::reader::LineCol; use crate::{ArgSep, ArgSepSyntax, ExprType, RepeatedTypeSyntax, SingularArgSyntax}; - -use super::SymbolKey; -use super::syms::LocalSymtable; - -/// Returns true if `sep` is valid for a function call (only `Long` and `End` are allowed because -/// the parser only produces comma separators for function arguments). -fn is_function_sep(sep: &ArgSepSyntax) -> bool { - match sep { - ArgSepSyntax::Exactly(ArgSep::Long) | ArgSepSyntax::End => true, - ArgSepSyntax::OneOf(seps) => seps.iter().all(|s| *s == ArgSep::Long), - _ => false, - } -} - -/// Checks that the syntax of a callable that returns a value only uses separators that can appear -/// in a function call (i.e. the comma separator). The parser only produces `ArgSep::Long` for -/// function arguments, so any other separator in the metadata would be dead/untestable. -fn debug_assert_function_seps(md: &CallableMetadata, syntax: &CallableSyntax) { - if md.return_type().is_none() { - return; - } - for syn in syntax.singular.iter() { - let sep = match syn { - SingularArgSyntax::RequiredValue(_, sep) => sep, - SingularArgSyntax::RequiredRef(_, sep) => sep, - SingularArgSyntax::OptionalValue(_, sep) => sep, - SingularArgSyntax::AnyValue(_, sep) => sep, - }; - debug_assert!( - is_function_sep(sep), - "Function {} has a non-comma separator in its singular args syntax", - md.name() - ); - } - if let Some(repeated) = syntax.repeated.as_ref() { - debug_assert!( - is_function_sep(&repeated.sep), - "Function {} has a non-comma separator in its repeated args syntax", - md.name() - ); - } -} - -/// Finds the syntax definition that matches the given argument count. -/// -/// Returns an error if no syntax matches, and panics if multiple syntaxes match (which would -/// indicate an ambiguous callable definition). -fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&CallableSyntax> { - let mut matches = md.syntaxes().iter().filter(|s| s.expected_nargs().contains(&nargs)); - let syntax = matches.next(); - match syntax { - Some(syntax) => { - debug_assert!(matches.next().is_none(), "Ambiguous syntax definitions"); - if cfg!(debug_assertions) { - debug_assert_function_seps(md, syntax); - } - Ok(syntax) - } - None => Err(Error::CallableSyntax(pos, md.clone())), - } -} +use std::rc::Rc; /// Compiles an argument separator with any necessary tagging. /// @@ -99,7 +41,7 @@ fn find_syntax(md: &CallableMetadata, pos: LineCol, nargs: usize) -> Result<&Cal /// only for diagnostics purposes. #[allow(clippy::too_many_arguments)] fn validate_syn_argsep( - md: &CallableMetadata, + md: &Rc, pos: LineCol, syn: &ArgSepSyntax, is_last: bool, @@ -114,7 +56,7 @@ fn validate_syn_argsep( ArgSepSyntax::Exactly(exp_sep) => { debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); if sep != ArgSep::End && sep != *exp_sep { - return Err(Error::CallableSyntax(pos, md.clone())); + return Err(Error::CallableSyntax(pos, md.as_ref().clone())); } Ok(()) } @@ -129,7 +71,7 @@ fn validate_syn_argsep( } } if !found { - return Err(Error::CallableSyntax(pos, md.clone())); + return Err(Error::CallableSyntax(pos, md.as_ref().clone())); } Ok(()) } @@ -144,7 +86,7 @@ fn validate_syn_argsep( /// Pre-allocates one local variable for a command output argument, setting its to its default /// value. fn define_new_arg( - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, vref: &VarRef, pos: LineCol, codegen: &mut Codegen, @@ -161,11 +103,13 @@ fn define_new_arg( /// Pre-allocates local variables for command output arguments. pub(super) fn define_new_args( span: &CallSpan, - md: &CallableMetadata, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + md: &Rc, + symtable: &mut LocalSymtable<'_, '_, '_>, codegen: &mut Codegen, ) -> Result<()> { - let syntax = find_syntax(md, span.vref_pos, span.args.len())?; + let Some(syntax) = md.find_syntax(span.args.len()) else { + return Err(Error::CallableSyntax(span.vref_pos, md.as_ref().clone())); + }; let mut arg_iter = span.args.iter(); @@ -234,13 +178,15 @@ pub(super) fn define_new_args( /// eliminating the `MetadataBuilder` and pass a static reference here. pub(super) fn compile_args( span: CallSpan, - md: CallableMetadata, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + md: Rc, + symtable: &mut TempSymtable<'_, '_, '_, '_>, codegen: &mut Codegen, ) -> Result<(Register, Vec)> { let key_pos = span.vref_pos; - let syntax = find_syntax(&md, key_pos, span.args.len())?; + let Some(syntax) = md.find_syntax(span.args.len()) else { + return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())); + }; let mut scope = symtable.temp_scope(); @@ -260,7 +206,7 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); match expr { - None => return Err(Error::CallableSyntax(key_pos, md)), + None => return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())), Some(expr) => { let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; arg_linecols.push(arg_pos); @@ -276,12 +222,12 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); match expr { - None => return Err(Error::CallableSyntax(key_pos, md)), + None => return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())), Some(Expr::Symbol(span)) => { let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { Ok((reg, SymbolPrototype::Scalar(vtype))) => (reg, vtype), Ok((_, SymbolPrototype::Array(_))) => { - return Err(Error::CallableSyntax(span.pos, md)); + return Err(Error::CallableSyntax(span.pos, md.as_ref().clone())); } Err(e @ syms::Error::UndefinedSymbol(..)) => { if !details.define_undefined { @@ -296,7 +242,9 @@ pub(super) fn compile_args( codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; } - Some(expr) => return Err(Error::CallableSyntax(expr.start_pos(), md)), + Some(expr) => { + return Err(Error::CallableSyntax(expr.start_pos(), md.as_ref().clone())); + } } } @@ -335,7 +283,7 @@ pub(super) fn compile_args( Some(ArgSpan { expr, sep, sep_pos }) => (expr, sep, sep_pos), None => { if !details.allow_missing { - return Err(Error::CallableSyntax(key_pos, md)); + return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())); } (None, ArgSep::End, key_pos) } @@ -372,7 +320,7 @@ pub(super) fn compile_args( min_nargs += 1; } if input_nargs < min_nargs { - return Err(Error::CallableSyntax(key_pos, md)); + return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())); } if arg_iter.peek().is_none() { @@ -393,7 +341,7 @@ pub(super) fn compile_args( let tag = match expr { None => { if !syn.allow_missing { - return Err(Error::CallableSyntax(arg_pos, md)); + return Err(Error::CallableSyntax(arg_pos, md.as_ref().clone())); } bytecode::VarArgTag::Missing(sep) } @@ -415,13 +363,13 @@ pub(super) fn compile_args( RepeatedTypeSyntax::VariableRef => { let Expr::Symbol(span) = expr else { - return Err(Error::CallableSyntax(arg_pos, md)); + return Err(Error::CallableSyntax(arg_pos, md.as_ref().clone())); }; let (reg, vtype) = match symtable.get_local_or_global(&span.vref) { Ok((reg, SymbolPrototype::Scalar(vtype))) => (reg, vtype), Ok((_, SymbolPrototype::Array(_))) => { - return Err(Error::CallableSyntax(arg_pos, md)); + return Err(Error::CallableSyntax(arg_pos, md.as_ref().clone())); } Err(syms::Error::UndefinedSymbol(..)) => { unreachable!("Caller must use define_new_args first for commands"); @@ -441,7 +389,7 @@ pub(super) fn compile_args( if arg_iter.peek().is_some() { debug_assert!(arg_iter.next().is_some(), "Args and their syntax must advance in unison"); - return Err(Error::CallableSyntax(key_pos, md)); + return Err(Error::CallableSyntax(key_pos, md.as_ref().clone())); } let first_reg = scope.first().map_err(|e| Error::from_syms(e, key_pos))?; diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index c40b4535..d7d29424 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -28,7 +28,7 @@ use crate::reader::LineCol; /// first register. The caller must guarantee that `exprs` is non-empty. pub(super) fn compile_integer_exprs( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, scope: &mut TempScope, pos: LineCol, exprs: impl Iterator, @@ -47,7 +47,7 @@ pub(super) fn compile_integer_exprs( /// Compiles an array element access expression into `reg`. fn compile_array_access( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, reg: Register, key_pos: LineCol, arr_reg: Register, @@ -450,7 +450,7 @@ fn resolve_numeric_binary_type( /// by recursing on the lhs. fn compile_pending_ops( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, reg: Register, mut etype: ExprType, mut pending: Vec, @@ -573,7 +573,7 @@ fn compile_pending_ops( /// than recurses so that very long expression chains do not overflow the call stack. pub(super) fn compile_expr( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, reg: Register, expr: Expr, ) -> Result { @@ -617,7 +617,7 @@ pub(super) fn compile_expr( }; if md.is_argless() { - return Err(Error::CallableSyntax(span.vref_pos, md)); + return Err(Error::CallableSyntax(span.vref_pos, md.as_ref().clone())); } let is_user_defined = md.is_user_defined(); @@ -689,7 +689,7 @@ pub(super) fn compile_expr( }; if !md.is_argless() { - return Err(Error::CallableSyntax(span.pos, md.clone())); + return Err(Error::CallableSyntax(span.pos, md.as_ref().clone())); } if md.is_user_defined() { @@ -726,7 +726,7 @@ pub(super) fn compile_expr( /// possible. pub(super) fn compile_expr_as_type( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, reg: Register, expr: Expr, target: ExprType, diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index c5f6d3a6..e7cb0d24 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -52,6 +52,11 @@ pub enum Error { BinaryOpType(LineCol, &'static str, ExprType, ExprType), /// Callable invoked with incorrect syntax. + // TODO(jmmv): It'd be nice if we could carry an Rc here to avoid copying + // but... because of async in consumers, we would need an `Arc` instead just for this single + // error type. Given that performance during error propagation is not important, the copy + // is just fine. If we ever have to pollute everything with `Arc`s in the future, then we + // could do this. #[error("{0}: {} expected {}", .1.name(), .1.syntax())] CallableSyntax(LineCol, CallableMetadata), diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 2a8da242..7c0a319d 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -146,25 +146,25 @@ where /// Representation of the symbol table for global symbols. /// /// Globals are variables and callables that are visible from any scope. -pub(crate) struct GlobalSymtable<'uref, 'ukey, 'umd> { +pub(crate) struct GlobalSymtable<'uref, 'ukey> { /// Map of global variable names to their prototypes and assigned registers. globals: HashMapWithIds, /// Reference to the built-in callable metadata provided by the runtime. - upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>, + upcalls: &'uref HashMap<&'ukey SymbolKey, Rc>, /// Map of user-defined callable names to their metadata. - user_callables: HashMap, + user_callables: HashMap>, } -impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { +impl<'uref, 'ukey> GlobalSymtable<'uref, 'ukey> { /// Creates a new global symbol table that knows about the given `upcalls`. - pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, &'umd CallableMetadata>) -> Self { + pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, Rc>) -> Self { Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } } /// Enters a new local scope. - pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, 'ukey, 'umd, '_> { + pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, 'ukey, '_> { LocalSymtable::new(self) } @@ -198,7 +198,7 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { pub(crate) fn define_user_callable( &mut self, vref: &VarRef, - md: CallableMetadata, + md: Rc, ) -> Result<()> { let key = SymbolKey::from(&vref.name); if self.globals.get(&key).is_some() { @@ -209,8 +209,8 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { } /// Gets a callable by its name `key`. - pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { - self.user_callables.get(key).or(self.upcalls.get(key).copied()) + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option> { + self.user_callables.get(key).or(self.upcalls.get(key)).cloned() } } @@ -218,9 +218,9 @@ impl<'uref, 'ukey, 'umd> GlobalSymtable<'uref, 'ukey, 'umd> { /// /// A local scope can see all global symbols and defines its own symbols, which can shadow the /// global ones. -pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { +pub(crate) struct LocalSymtable<'uref, 'ukey, 'a> { /// Reference to the parent global symbol table. - symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>, + symtable: &'a mut GlobalSymtable<'uref, 'ukey>, /// Map of local variable names to their prototypes and assigned registers. locals: HashMapWithIds, @@ -234,9 +234,9 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'umd, 'a> { active_temps: Rc>, } -impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { +impl<'uref, 'ukey, 'a> LocalSymtable<'uref, 'ukey, 'a> { /// Creates a new local symbol table within the context of a global `symtable`. - fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey, 'umd>) -> Self { + fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey>) -> Self { Self { symtable, locals: HashMapWithIds::default(), @@ -258,13 +258,13 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { pub(crate) fn define_user_callable( &mut self, vref: &VarRef, - md: CallableMetadata, + md: Rc, ) -> Result<()> { self.symtable.define_user_callable(vref, md) } /// Freezes this table to get a `TempSymtable` that can be used to compile expressions. - pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, 'ukey, 'umd, '_, 'a> { + pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, 'ukey, '_, 'a> { TempSymtable::new(self) } @@ -276,10 +276,7 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { ) -> std::result::Result where ME: Fn(Error) -> E, - F: FnOnce( - Register, - &mut TempSymtable<'uref, 'ukey, 'umd, '_, 'a>, - ) -> std::result::Result, + F: FnOnce(Register, &mut TempSymtable<'uref, 'ukey, '_, 'a>) -> std::result::Result, { struct TempReservationGuard { active_temps: Rc>, @@ -331,7 +328,7 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { } /// Gets a callable by its name `key`. - pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option> { self.symtable.get_callable(key) } @@ -376,9 +373,9 @@ impl<'uref, 'ukey, 'umd, 'a> LocalSymtable<'uref, 'ukey, 'umd, 'a> { /// to forbid mutations to local variables. We need to be able to pass a `TempSymtable` /// across recursive function calls (for expression evaluation), but at the same time we /// need each call site to have its own `TempScope` for temporary register cleanup. -pub(crate) struct TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { +pub(crate) struct TempSymtable<'uref, 'ukey, 'temp, 'local> { /// Reference to the underlying local symbol table. - symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>, + symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'local>, /// Number of temporary registers that were already reserved on creation. base_temp: u8, @@ -390,16 +387,16 @@ pub(crate) struct TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { count_temps: Rc>, } -impl<'uref, 'ukey, 'umd, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { +impl<'uref, 'ukey, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'temp, 'local> { fn drop(&mut self) { debug_assert_eq!(self.base_temp, *self.next_temp.borrow(), "Unbalanced temp drops"); self.symtable.count_temps = max(self.symtable.count_temps, *self.count_temps.borrow()); } } -impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, 'local> { +impl<'uref, 'ukey, 'temp, 'local> TempSymtable<'uref, 'ukey, 'temp, 'local> { /// Creates a new temporary symbol table from a `local` table. - fn new(symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'umd, 'local>) -> Self { + fn new(symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'local>) -> Self { let base_temp = symtable.active_temps.get(); Self { symtable, @@ -415,7 +412,7 @@ impl<'uref, 'ukey, 'umd, 'temp, 'local> TempSymtable<'uref, 'ukey, 'umd, 'temp, } /// Gets a callable by its name `key`. - pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option<&CallableMetadata> { + pub(crate) fn get_callable(&self, key: &SymbolKey) -> Option> { self.symtable.get_callable(key) } @@ -769,7 +766,7 @@ mod tests { let key = SymbolKey::from("BUILTIN"); let md = CallableMetadataBuilder::new("BUILTIN").test_build(); let mut upcalls_map = HashMap::new(); - upcalls_map.insert(&key, &md); + upcalls_map.insert(&key, md); let global = GlobalSymtable::new(&upcalls_map); let found = global.get_callable(&SymbolKey::from("builtin")); @@ -783,7 +780,7 @@ mod tests { let builtin_md = CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Boolean).test_build(); let mut upcalls_map = HashMap::new(); - upcalls_map.insert(&key, &builtin_md); + upcalls_map.insert(&key, builtin_md); let mut global = GlobalSymtable::new(&upcalls_map); let user_md = diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 1977851d..32f0dc85 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -105,7 +105,7 @@ fn static_end_code(expr: &Expr) -> Option<(i32, LineCol)> { /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: AssignmentSpan, ) -> Result<()> { let vref_pos = span.vref_pos; @@ -253,7 +253,7 @@ fn compile_case_relop( /// Compiles one `CASE` guard and returns the register and source position of its boolean result. fn compile_case_guard( ctx: &mut Context, - symtable: &mut TempSymtable<'_, '_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_, '_>, test_reg: Register, test_type: ExprType, guard: CaseGuardSpan, @@ -326,7 +326,7 @@ fn compile_case_guard( /// Compiles a `SELECT` statement and emits bytecode into `ctx`. fn compile_select( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: SelectSpan, ) -> Result<()> { let end_pos = span.end_pos; @@ -428,12 +428,12 @@ fn compile_select( /// Compiles a `DO` loop and emits bytecode into `ctx`. fn compile_do( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: DoSpan, ) -> Result<()> { fn compile_guard( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, guard: Expr, ) -> Result<(Register, LineCol)> { let guard_pos = guard.start_pos(); @@ -542,7 +542,7 @@ fn compile_do( /// Compiles a `FOR` loop and emits bytecode into `ctx`. fn compile_for( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: ForSpan, ) -> Result<()> { if span.iter_double && span.iter.ref_type.is_none() { @@ -618,7 +618,7 @@ fn compile_for( /// Compiles an `IF` statement `span` into the `ctx`. fn compile_if( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: IfSpan, ) -> Result<()> { let mut end_pcs: Vec = vec![]; @@ -670,7 +670,7 @@ fn compile_if( /// Compiles a `WHILE` loop and emits bytecode into `ctx`. fn compile_while( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, span: WhileSpan, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -703,7 +703,7 @@ fn compile_while( /// Compiles a single `stmt` into the `ctx`. fn compile_stmt( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_, '_>, stmt: Statement, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -1032,7 +1032,7 @@ fn compile_stmt( /// Compiles a sequence of `stmts` that all live in the same `symtable` scope. fn compile_scope( ctx: &mut Context, - mut symtable: LocalSymtable<'_, '_, '_, '_>, + mut symtable: LocalSymtable<'_, '_, '_>, stmts: I, ) -> Result<()> where @@ -1121,7 +1121,7 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R /// Extracts the metadata of all provided `upcalls`. pub fn only_metadata( upcalls_by_name: &HashMap>, -) -> HashMap<&SymbolKey, &CallableMetadata> { +) -> HashMap<&SymbolKey, Rc> { let mut upcalls = HashMap::with_capacity(upcalls_by_name.len()); for (name, callable) in upcalls_by_name { upcalls.insert(name, callable.metadata()); @@ -1250,7 +1250,7 @@ fn prepare_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile_with_globals( input: &mut dyn io::Read, - upcalls: &HashMap<&SymbolKey, &CallableMetadata>, + upcalls: &HashMap<&SymbolKey, Rc>, global_defs: &[GlobalDef], ) -> Result { let mut ctx = Context::default(); @@ -1282,7 +1282,7 @@ pub fn compile_with_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile( input: &mut dyn io::Read, - upcalls: &HashMap<&SymbolKey, &CallableMetadata>, + upcalls: &HashMap<&SymbolKey, Rc>, ) -> Result { compile_with_globals(input, upcalls, &[]) } diff --git a/core2/src/testutils.rs b/core2/src/testutils.rs index d05573ce..f908a32d 100644 --- a/core2/src/testutils.rs +++ b/core2/src/testutils.rs @@ -32,7 +32,7 @@ use std::rc::Rc; /// them with a single space. pub struct OutCommand { /// Metadata describing the command's name and syntax. - metadata: CallableMetadata, + metadata: Rc, /// Shared storage for captured output strings. data: Rc>>, @@ -61,8 +61,8 @@ impl OutCommand { #[async_trait(?Send)] impl Callable for OutCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 80ebed5f..841240c6 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -355,7 +355,7 @@ mod tests { /// On each invocation, records the result of `scope.get_pos(n)` for `0..nargs` into /// `positions`. struct PosCapture { - metadata: CallableMetadata, + metadata: Rc, nargs: u8, positions: Rc>>, } @@ -389,8 +389,8 @@ mod tests { #[async_trait(?Send)] impl Callable for PosCapture { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/concat_fn.rs b/core2/tests/testutils/callables/concat_fn.rs index f290a2f0..bb5c82cb 100644 --- a/core2/tests/testutils/callables/concat_fn.rs +++ b/core2/tests/testutils/callables/concat_fn.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A function that concatenates all of its string arguments. pub(super) struct ConcatFunction { - metadata: CallableMetadata, + metadata: Rc, } impl ConcatFunction { @@ -47,8 +47,8 @@ impl ConcatFunction { #[async_trait(?Send)] impl Callable for ConcatFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/define_and_change_args_cmd.rs b/core2/tests/testutils/callables/define_and_change_args_cmd.rs index c8ce9458..3fa3d019 100644 --- a/core2/tests/testutils/callables/define_and_change_args_cmd.rs +++ b/core2/tests/testutils/callables/define_and_change_args_cmd.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A command that defines the arguments passed in as a reference. pub(super) struct DefineAndChangeArgsCommand { - metadata: CallableMetadata, + metadata: Rc, } impl DefineAndChangeArgsCommand { @@ -46,8 +46,8 @@ impl DefineAndChangeArgsCommand { #[async_trait(?Send)] impl Callable for DefineAndChangeArgsCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/define_arg_cmd.rs b/core2/tests/testutils/callables/define_arg_cmd.rs index 9167eaad..c4d587e8 100644 --- a/core2/tests/testutils/callables/define_arg_cmd.rs +++ b/core2/tests/testutils/callables/define_arg_cmd.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A command that defines the argument passed in as a reference. pub(super) struct DefineArgCommand { - metadata: CallableMetadata, + metadata: Rc, } impl DefineArgCommand { @@ -47,8 +47,8 @@ impl DefineArgCommand { #[async_trait(?Send)] impl Callable for DefineArgCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, _scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/get_data_cmd.rs b/core2/tests/testutils/callables/get_data_cmd.rs index e9fe591b..8df60394 100644 --- a/core2/tests/testutils/callables/get_data_cmd.rs +++ b/core2/tests/testutils/callables/get_data_cmd.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A command that dumps all DATA values visible to the upcall. pub(super) struct GetDataCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -49,8 +49,8 @@ fn format_datum(datum: &Option) -> String { #[async_trait(?Send)] impl Callable for GetDataCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/increment_required_int_cmd.rs b/core2/tests/testutils/callables/increment_required_int_cmd.rs index a9d77af3..8f8864bf 100644 --- a/core2/tests/testutils/callables/increment_required_int_cmd.rs +++ b/core2/tests/testutils/callables/increment_required_int_cmd.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A command that increments the argument passed in as a reference. pub(super) struct IncrementRequiredIntCommand { - metadata: CallableMetadata, + metadata: Rc, } impl IncrementRequiredIntCommand { @@ -47,8 +47,8 @@ impl IncrementRequiredIntCommand { #[async_trait(?Send)] impl Callable for IncrementRequiredIntCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, mut scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/is_positive_fn.rs b/core2/tests/testutils/callables/is_positive_fn.rs index b0e67c1a..f5365941 100644 --- a/core2/tests/testutils/callables/is_positive_fn.rs +++ b/core2/tests/testutils/callables/is_positive_fn.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A function that returns whether its integer argument is positive. pub(super) struct IsPositiveFunction { - metadata: CallableMetadata, + metadata: Rc, } impl IsPositiveFunction { @@ -44,8 +44,8 @@ impl IsPositiveFunction { #[async_trait(?Send)] impl Callable for IsPositiveFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/last_error_fn.rs b/core2/tests/testutils/callables/last_error_fn.rs index a7d66830..40616747 100644 --- a/core2/tests/testutils/callables/last_error_fn.rs +++ b/core2/tests/testutils/callables/last_error_fn.rs @@ -21,7 +21,7 @@ use std::rc::Rc; /// A function that returns the last error recorded by the VM. pub(super) struct LastErrorFunction { - metadata: CallableMetadata, + metadata: Rc, } impl LastErrorFunction { @@ -37,8 +37,8 @@ impl LastErrorFunction { #[async_trait(?Send)] impl Callable for LastErrorFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/meaning_of_life_fn.rs b/core2/tests/testutils/callables/meaning_of_life_fn.rs index ef2a2b70..b1cf05ab 100644 --- a/core2/tests/testutils/callables/meaning_of_life_fn.rs +++ b/core2/tests/testutils/callables/meaning_of_life_fn.rs @@ -21,7 +21,7 @@ use std::rc::Rc; /// An argless function that returns the meaning of life (42). pub(super) struct MeaningOfLifeFunction { - metadata: CallableMetadata, + metadata: Rc, } impl MeaningOfLifeFunction { @@ -37,8 +37,8 @@ impl MeaningOfLifeFunction { #[async_trait(?Send)] impl Callable for MeaningOfLifeFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_any_value_cmd.rs b/core2/tests/testutils/callables/out_any_value_cmd.rs index 042852d7..d2c8924a 100644 --- a/core2/tests/testutils/callables/out_any_value_cmd.rs +++ b/core2/tests/testutils/callables/out_any_value_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints an argument of any type. pub(super) struct OutAnyValueCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -47,8 +47,8 @@ impl OutAnyValueCommand { #[async_trait(?Send)] impl Callable for OutAnyValueCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_any_value_optional_cmd.rs b/core2/tests/testutils/callables/out_any_value_optional_cmd.rs index ffc383d2..f3565f55 100644 --- a/core2/tests/testutils/callables/out_any_value_optional_cmd.rs +++ b/core2/tests/testutils/callables/out_any_value_optional_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints an argument of any type. pub(super) struct OutAnyValueOptionalCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -47,8 +47,8 @@ impl OutAnyValueOptionalCommand { #[async_trait(?Send)] impl Callable for OutAnyValueOptionalCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_cmd.rs b/core2/tests/testutils/callables/out_cmd.rs index f09b5626..cb25c5e4 100644 --- a/core2/tests/testutils/callables/out_cmd.rs +++ b/core2/tests/testutils/callables/out_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints its arguments to a virtual console. pub(super) struct OutCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -50,8 +50,8 @@ impl OutCommand { #[async_trait(?Send)] impl Callable for OutCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_optional_cmd.rs b/core2/tests/testutils/callables/out_optional_cmd.rs index 8e415506..a8846d42 100644 --- a/core2/tests/testutils/callables/out_optional_cmd.rs +++ b/core2/tests/testutils/callables/out_optional_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints its single optional argument. pub(super) struct OutOptionalCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -47,8 +47,8 @@ impl OutOptionalCommand { #[async_trait(?Send)] impl Callable for OutOptionalCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_positional_cmd.rs b/core2/tests/testutils/callables/out_positional_cmd.rs index cfddba0d..e54044a2 100644 --- a/core2/tests/testutils/callables/out_positional_cmd.rs +++ b/core2/tests/testutils/callables/out_positional_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints various positional arguments of different types. pub(super) struct OutPositionalCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -60,8 +60,8 @@ impl OutPositionalCommand { #[async_trait(?Send)] impl Callable for OutPositionalCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/out_required_value_cmd.rs b/core2/tests/testutils/callables/out_required_value_cmd.rs index ff6c6241..87f2bec1 100644 --- a/core2/tests/testutils/callables/out_required_value_cmd.rs +++ b/core2/tests/testutils/callables/out_required_value_cmd.rs @@ -24,7 +24,7 @@ use std::rc::Rc; /// A command that prints an argument of a specific type. pub(super) struct OutRequiredValueCommand { - metadata: CallableMetadata, + metadata: Rc, output: Rc>, } @@ -50,8 +50,8 @@ impl OutRequiredValueCommand { #[async_trait(?Send)] impl Callable for OutRequiredValueCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/raise_cmd.rs b/core2/tests/testutils/callables/raise_cmd.rs index 54099331..a83a4946 100644 --- a/core2/tests/testutils/callables/raise_cmd.rs +++ b/core2/tests/testutils/callables/raise_cmd.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A command that raises an error based on a string argument. pub(super) struct RaiseCommand { - metadata: CallableMetadata, + metadata: Rc, } impl RaiseCommand { @@ -43,8 +43,8 @@ impl RaiseCommand { #[async_trait(?Send)] impl Callable for RaiseCommand { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/raisef_fn.rs b/core2/tests/testutils/callables/raisef_fn.rs index e4639f5a..efda4261 100644 --- a/core2/tests/testutils/callables/raisef_fn.rs +++ b/core2/tests/testutils/callables/raisef_fn.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A function that raises an error based on a string argument. pub(super) struct RaisefFunction { - metadata: CallableMetadata, + metadata: Rc, } impl RaisefFunction { @@ -44,8 +44,8 @@ impl RaisefFunction { #[async_trait(?Send)] impl Callable for RaisefFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/sum_doubles_fn.rs b/core2/tests/testutils/callables/sum_doubles_fn.rs index 55023147..1a6b1ac8 100644 --- a/core2/tests/testutils/callables/sum_doubles_fn.rs +++ b/core2/tests/testutils/callables/sum_doubles_fn.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A function that adds all of its arguments. pub(super) struct SumDoublesFunction { - metadata: CallableMetadata, + metadata: Rc, } impl SumDoublesFunction { @@ -47,8 +47,8 @@ impl SumDoublesFunction { #[async_trait(?Send)] impl Callable for SumDoublesFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { diff --git a/core2/tests/testutils/callables/sum_integers_fn.rs b/core2/tests/testutils/callables/sum_integers_fn.rs index 36698634..489d5a26 100644 --- a/core2/tests/testutils/callables/sum_integers_fn.rs +++ b/core2/tests/testutils/callables/sum_integers_fn.rs @@ -22,7 +22,7 @@ use std::rc::Rc; /// A function that adds all of its integer arguments. pub(super) struct SumIntegersFunction { - metadata: CallableMetadata, + metadata: Rc, } impl SumIntegersFunction { @@ -47,8 +47,8 @@ impl SumIntegersFunction { #[async_trait(?Send)] impl Callable for SumIntegersFunction { - fn metadata(&self) -> &CallableMetadata { - &self.metadata + fn metadata(&self) -> Rc { + self.metadata.clone() } async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { From 4ffb8b6bef3802148ff55b52e8600d55955888f0 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 20 Mar 2026 16:30:30 +0100 Subject: [PATCH 36/53] core2: Drop SymbolKey refs in compiler upcalls Similar to the previous change, it is theoretically possible to hold references to the SymbolKeys for the upcalls injected into the compiler. However, that becomes very problematic with incremental compilation. Given that upcall names are not going to change throughout the duration of a compilation / execution and that they are just a bunch of strings, clone them. --- core2/src/compiler/args.rs | 6 +++--- core2/src/compiler/exprs.rs | 10 +++++----- core2/src/compiler/syms.rs | 36 ++++++++++++++++----------------- core2/src/compiler/top.rs | 40 +++++++++++++------------------------ 4 files changed, 40 insertions(+), 52 deletions(-) diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index bd42f1e5..009d2846 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -86,7 +86,7 @@ fn validate_syn_argsep( /// Pre-allocates one local variable for a command output argument, setting its to its default /// value. fn define_new_arg( - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, vref: &VarRef, pos: LineCol, codegen: &mut Codegen, @@ -104,7 +104,7 @@ fn define_new_arg( pub(super) fn define_new_args( span: &CallSpan, md: &Rc, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, codegen: &mut Codegen, ) -> Result<()> { let Some(syntax) = md.find_syntax(span.args.len()) else { @@ -179,7 +179,7 @@ pub(super) fn define_new_args( pub(super) fn compile_args( span: CallSpan, md: Rc, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, codegen: &mut Codegen, ) -> Result<(Register, Vec)> { let key_pos = span.vref_pos; diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index d7d29424..9d9f7fee 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -28,7 +28,7 @@ use crate::reader::LineCol; /// first register. The caller must guarantee that `exprs` is non-empty. pub(super) fn compile_integer_exprs( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, scope: &mut TempScope, pos: LineCol, exprs: impl Iterator, @@ -47,7 +47,7 @@ pub(super) fn compile_integer_exprs( /// Compiles an array element access expression into `reg`. fn compile_array_access( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, reg: Register, key_pos: LineCol, arr_reg: Register, @@ -450,7 +450,7 @@ fn resolve_numeric_binary_type( /// by recursing on the lhs. fn compile_pending_ops( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, reg: Register, mut etype: ExprType, mut pending: Vec, @@ -573,7 +573,7 @@ fn compile_pending_ops( /// than recurses so that very long expression chains do not overflow the call stack. pub(super) fn compile_expr( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, reg: Register, expr: Expr, ) -> Result { @@ -726,7 +726,7 @@ pub(super) fn compile_expr( /// possible. pub(super) fn compile_expr_as_type( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, reg: Register, expr: Expr, target: ExprType, diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 7c0a319d..fe733fb7 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -146,25 +146,25 @@ where /// Representation of the symbol table for global symbols. /// /// Globals are variables and callables that are visible from any scope. -pub(crate) struct GlobalSymtable<'uref, 'ukey> { +pub(crate) struct GlobalSymtable<'uref> { /// Map of global variable names to their prototypes and assigned registers. globals: HashMapWithIds, /// Reference to the built-in callable metadata provided by the runtime. - upcalls: &'uref HashMap<&'ukey SymbolKey, Rc>, + upcalls: &'uref HashMap>, /// Map of user-defined callable names to their metadata. user_callables: HashMap>, } -impl<'uref, 'ukey> GlobalSymtable<'uref, 'ukey> { +impl<'uref> GlobalSymtable<'uref> { /// Creates a new global symbol table that knows about the given `upcalls`. - pub(crate) fn new(upcalls: &'uref HashMap<&'ukey SymbolKey, Rc>) -> Self { + pub(crate) fn new(upcalls: &'uref HashMap>) -> Self { Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } } /// Enters a new local scope. - pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, 'ukey, '_> { + pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, '_> { LocalSymtable::new(self) } @@ -218,9 +218,9 @@ impl<'uref, 'ukey> GlobalSymtable<'uref, 'ukey> { /// /// A local scope can see all global symbols and defines its own symbols, which can shadow the /// global ones. -pub(crate) struct LocalSymtable<'uref, 'ukey, 'a> { +pub(crate) struct LocalSymtable<'uref, 'a> { /// Reference to the parent global symbol table. - symtable: &'a mut GlobalSymtable<'uref, 'ukey>, + symtable: &'a mut GlobalSymtable<'uref>, /// Map of local variable names to their prototypes and assigned registers. locals: HashMapWithIds, @@ -234,9 +234,9 @@ pub(crate) struct LocalSymtable<'uref, 'ukey, 'a> { active_temps: Rc>, } -impl<'uref, 'ukey, 'a> LocalSymtable<'uref, 'ukey, 'a> { +impl<'uref, 'a> LocalSymtable<'uref, 'a> { /// Creates a new local symbol table within the context of a global `symtable`. - fn new(symtable: &'a mut GlobalSymtable<'uref, 'ukey>) -> Self { + fn new(symtable: &'a mut GlobalSymtable<'uref>) -> Self { Self { symtable, locals: HashMapWithIds::default(), @@ -264,7 +264,7 @@ impl<'uref, 'ukey, 'a> LocalSymtable<'uref, 'ukey, 'a> { } /// Freezes this table to get a `TempSymtable` that can be used to compile expressions. - pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, 'ukey, '_, 'a> { + pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, '_, 'a> { TempSymtable::new(self) } @@ -276,7 +276,7 @@ impl<'uref, 'ukey, 'a> LocalSymtable<'uref, 'ukey, 'a> { ) -> std::result::Result where ME: Fn(Error) -> E, - F: FnOnce(Register, &mut TempSymtable<'uref, 'ukey, '_, 'a>) -> std::result::Result, + F: FnOnce(Register, &mut TempSymtable<'uref, '_, 'a>) -> std::result::Result, { struct TempReservationGuard { active_temps: Rc>, @@ -373,9 +373,9 @@ impl<'uref, 'ukey, 'a> LocalSymtable<'uref, 'ukey, 'a> { /// to forbid mutations to local variables. We need to be able to pass a `TempSymtable` /// across recursive function calls (for expression evaluation), but at the same time we /// need each call site to have its own `TempScope` for temporary register cleanup. -pub(crate) struct TempSymtable<'uref, 'ukey, 'temp, 'local> { +pub(crate) struct TempSymtable<'uref, 'temp, 'local> { /// Reference to the underlying local symbol table. - symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'local>, + symtable: &'temp mut LocalSymtable<'uref, 'local>, /// Number of temporary registers that were already reserved on creation. base_temp: u8, @@ -387,16 +387,16 @@ pub(crate) struct TempSymtable<'uref, 'ukey, 'temp, 'local> { count_temps: Rc>, } -impl<'uref, 'ukey, 'temp, 'local> Drop for TempSymtable<'uref, 'ukey, 'temp, 'local> { +impl<'uref, 'temp, 'local> Drop for TempSymtable<'uref, 'temp, 'local> { fn drop(&mut self) { debug_assert_eq!(self.base_temp, *self.next_temp.borrow(), "Unbalanced temp drops"); self.symtable.count_temps = max(self.symtable.count_temps, *self.count_temps.borrow()); } } -impl<'uref, 'ukey, 'temp, 'local> TempSymtable<'uref, 'ukey, 'temp, 'local> { +impl<'uref, 'temp, 'local> TempSymtable<'uref, 'temp, 'local> { /// Creates a new temporary symbol table from a `local` table. - fn new(symtable: &'temp mut LocalSymtable<'uref, 'ukey, 'local>) -> Self { + fn new(symtable: &'temp mut LocalSymtable<'uref, 'local>) -> Self { let base_temp = symtable.active_temps.get(); Self { symtable, @@ -766,7 +766,7 @@ mod tests { let key = SymbolKey::from("BUILTIN"); let md = CallableMetadataBuilder::new("BUILTIN").test_build(); let mut upcalls_map = HashMap::new(); - upcalls_map.insert(&key, md); + upcalls_map.insert(key, md); let global = GlobalSymtable::new(&upcalls_map); let found = global.get_callable(&SymbolKey::from("builtin")); @@ -780,7 +780,7 @@ mod tests { let builtin_md = CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Boolean).test_build(); let mut upcalls_map = HashMap::new(); - upcalls_map.insert(&key, builtin_md); + upcalls_map.insert(key, builtin_md); let mut global = GlobalSymtable::new(&upcalls_map); let user_md = diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 32f0dc85..ce54bd4f 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -105,7 +105,7 @@ fn static_end_code(expr: &Expr) -> Option<(i32, LineCol)> { /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, span: AssignmentSpan, ) -> Result<()> { let vref_pos = span.vref_pos; @@ -253,7 +253,7 @@ fn compile_case_relop( /// Compiles one `CASE` guard and returns the register and source position of its boolean result. fn compile_case_guard( ctx: &mut Context, - symtable: &mut TempSymtable<'_, '_, '_, '_>, + symtable: &mut TempSymtable<'_, '_, '_>, test_reg: Register, test_type: ExprType, guard: CaseGuardSpan, @@ -326,7 +326,7 @@ fn compile_case_guard( /// Compiles a `SELECT` statement and emits bytecode into `ctx`. fn compile_select( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, span: SelectSpan, ) -> Result<()> { let end_pos = span.end_pos; @@ -426,14 +426,10 @@ fn compile_select( } /// Compiles a `DO` loop and emits bytecode into `ctx`. -fn compile_do( - ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, - span: DoSpan, -) -> Result<()> { +fn compile_do(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: DoSpan) -> Result<()> { fn compile_guard( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, guard: Expr, ) -> Result<(Register, LineCol)> { let guard_pos = guard.start_pos(); @@ -542,7 +538,7 @@ fn compile_do( /// Compiles a `FOR` loop and emits bytecode into `ctx`. fn compile_for( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, span: ForSpan, ) -> Result<()> { if span.iter_double && span.iter.ref_type.is_none() { @@ -616,11 +612,7 @@ fn compile_for( } /// Compiles an `IF` statement `span` into the `ctx`. -fn compile_if( - ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, - span: IfSpan, -) -> Result<()> { +fn compile_if(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: IfSpan) -> Result<()> { let mut end_pcs: Vec = vec![]; let nbranches = span.branches.len(); @@ -670,7 +662,7 @@ fn compile_if( /// Compiles a `WHILE` loop and emits bytecode into `ctx`. fn compile_while( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, span: WhileSpan, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -703,7 +695,7 @@ fn compile_while( /// Compiles a single `stmt` into the `ctx`. fn compile_stmt( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_, '_>, + symtable: &mut LocalSymtable<'_, '_>, stmt: Statement, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -1030,11 +1022,7 @@ fn compile_stmt( } /// Compiles a sequence of `stmts` that all live in the same `symtable` scope. -fn compile_scope( - ctx: &mut Context, - mut symtable: LocalSymtable<'_, '_, '_>, - stmts: I, -) -> Result<()> +fn compile_scope(ctx: &mut Context, mut symtable: LocalSymtable<'_, '_>, stmts: I) -> Result<()> where I: Iterator>, { @@ -1121,10 +1109,10 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R /// Extracts the metadata of all provided `upcalls`. pub fn only_metadata( upcalls_by_name: &HashMap>, -) -> HashMap<&SymbolKey, Rc> { +) -> HashMap> { let mut upcalls = HashMap::with_capacity(upcalls_by_name.len()); for (name, callable) in upcalls_by_name { - upcalls.insert(name, callable.metadata()); + upcalls.insert(name.clone(), callable.metadata()); } upcalls } @@ -1250,7 +1238,7 @@ fn prepare_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile_with_globals( input: &mut dyn io::Read, - upcalls: &HashMap<&SymbolKey, Rc>, + upcalls: &HashMap>, global_defs: &[GlobalDef], ) -> Result { let mut ctx = Context::default(); @@ -1282,7 +1270,7 @@ pub fn compile_with_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile( input: &mut dyn io::Read, - upcalls: &HashMap<&SymbolKey, Rc>, + upcalls: &HashMap>, ) -> Result { compile_with_globals(input, upcalls, &[]) } From 7f3fdd5a8ab17c9fa9c6a88f55cff424f0eedc99 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 20 Mar 2026 17:00:30 +0100 Subject: [PATCH 37/53] core2: Make GlobalSymtable own the upcalls metadata Continuing with the previous simplification, remove the reference to the upcalls metadata from the GlobalSymtable and instead own the map. Same reasons as before. --- core2/examples/config.rs | 5 +- core2/src/compiler/args.rs | 8 +-- core2/src/compiler/exprs.rs | 10 ++-- core2/src/compiler/syms.rs | 96 ++++++++++++++++++------------------ core2/src/compiler/top.rs | 32 ++++++------ core2/src/vm/mod.rs | 18 +++---- core2/tests/testutils/mod.rs | 2 +- 7 files changed, 83 insertions(+), 88 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 43f3e93c..ee00a4a4 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -79,9 +79,8 @@ fn main() { // Compile the script, making the pre-defined globals visible to it. let upcalls = HashMap::default(); - let image = - compile_with_globals(&mut SCRIPT.as_bytes(), &only_metadata(&upcalls), &global_defs) - .expect("Compilation failed"); + let image = compile_with_globals(&mut SCRIPT.as_bytes(), only_metadata(&upcalls), &global_defs) + .expect("Compilation failed"); // Load and execute the compiled image. let mut vm = Vm::new(upcalls); diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 009d2846..fe1f276b 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -86,7 +86,7 @@ fn validate_syn_argsep( /// Pre-allocates one local variable for a command output argument, setting its to its default /// value. fn define_new_arg( - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, vref: &VarRef, pos: LineCol, codegen: &mut Codegen, @@ -104,7 +104,7 @@ fn define_new_arg( pub(super) fn define_new_args( span: &CallSpan, md: &Rc, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, codegen: &mut Codegen, ) -> Result<()> { let Some(syntax) = md.find_syntax(span.args.len()) else { @@ -179,7 +179,7 @@ pub(super) fn define_new_args( pub(super) fn compile_args( span: CallSpan, md: Rc, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, codegen: &mut Codegen, ) -> Result<(Register, Vec)> { let key_pos = span.vref_pos; @@ -426,7 +426,7 @@ mod tests { let enter = codegen.emit(bytecode::make_nop(), LineCol { line: 0, col: 0 }); let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let (first_reg, arg_linecols) = { let mut symtable = local.frozen(); diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index 9d9f7fee..ef1f8121 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -28,7 +28,7 @@ use crate::reader::LineCol; /// first register. The caller must guarantee that `exprs` is non-empty. pub(super) fn compile_integer_exprs( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, scope: &mut TempScope, pos: LineCol, exprs: impl Iterator, @@ -47,7 +47,7 @@ pub(super) fn compile_integer_exprs( /// Compiles an array element access expression into `reg`. fn compile_array_access( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, reg: Register, key_pos: LineCol, arr_reg: Register, @@ -450,7 +450,7 @@ fn resolve_numeric_binary_type( /// by recursing on the lhs. fn compile_pending_ops( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, reg: Register, mut etype: ExprType, mut pending: Vec, @@ -573,7 +573,7 @@ fn compile_pending_ops( /// than recurses so that very long expression chains do not overflow the call stack. pub(super) fn compile_expr( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, reg: Register, expr: Expr, ) -> Result { @@ -726,7 +726,7 @@ pub(super) fn compile_expr( /// possible. pub(super) fn compile_expr_as_type( codegen: &mut Codegen, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, reg: Register, expr: Expr, target: ExprType, diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index fe733fb7..acefbbe8 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -146,25 +146,25 @@ where /// Representation of the symbol table for global symbols. /// /// Globals are variables and callables that are visible from any scope. -pub(crate) struct GlobalSymtable<'uref> { +pub(crate) struct GlobalSymtable { /// Map of global variable names to their prototypes and assigned registers. globals: HashMapWithIds, /// Reference to the built-in callable metadata provided by the runtime. - upcalls: &'uref HashMap>, + upcalls: HashMap>, /// Map of user-defined callable names to their metadata. user_callables: HashMap>, } -impl<'uref> GlobalSymtable<'uref> { +impl GlobalSymtable { /// Creates a new global symbol table that knows about the given `upcalls`. - pub(crate) fn new(upcalls: &'uref HashMap>) -> Self { + pub(crate) fn new(upcalls: HashMap>) -> Self { Self { globals: HashMapWithIds::default(), upcalls, user_callables: HashMap::default() } } /// Enters a new local scope. - pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'uref, '_> { + pub(crate) fn enter_scope(&mut self) -> LocalSymtable<'_> { LocalSymtable::new(self) } @@ -218,9 +218,9 @@ impl<'uref> GlobalSymtable<'uref> { /// /// A local scope can see all global symbols and defines its own symbols, which can shadow the /// global ones. -pub(crate) struct LocalSymtable<'uref, 'a> { +pub(crate) struct LocalSymtable<'a> { /// Reference to the parent global symbol table. - symtable: &'a mut GlobalSymtable<'uref>, + symtable: &'a mut GlobalSymtable, /// Map of local variable names to their prototypes and assigned registers. locals: HashMapWithIds, @@ -234,9 +234,9 @@ pub(crate) struct LocalSymtable<'uref, 'a> { active_temps: Rc>, } -impl<'uref, 'a> LocalSymtable<'uref, 'a> { +impl<'a> LocalSymtable<'a> { /// Creates a new local symbol table within the context of a global `symtable`. - fn new(symtable: &'a mut GlobalSymtable<'uref>) -> Self { + fn new(symtable: &'a mut GlobalSymtable) -> Self { Self { symtable, locals: HashMapWithIds::default(), @@ -264,7 +264,7 @@ impl<'uref, 'a> LocalSymtable<'uref, 'a> { } /// Freezes this table to get a `TempSymtable` that can be used to compile expressions. - pub(crate) fn frozen(&mut self) -> TempSymtable<'uref, '_, 'a> { + pub(crate) fn frozen(&mut self) -> TempSymtable<'_, 'a> { TempSymtable::new(self) } @@ -276,7 +276,7 @@ impl<'uref, 'a> LocalSymtable<'uref, 'a> { ) -> std::result::Result where ME: Fn(Error) -> E, - F: FnOnce(Register, &mut TempSymtable<'uref, '_, 'a>) -> std::result::Result, + F: FnOnce(Register, &mut TempSymtable<'_, 'a>) -> std::result::Result, { struct TempReservationGuard { active_temps: Rc>, @@ -373,9 +373,9 @@ impl<'uref, 'a> LocalSymtable<'uref, 'a> { /// to forbid mutations to local variables. We need to be able to pass a `TempSymtable` /// across recursive function calls (for expression evaluation), but at the same time we /// need each call site to have its own `TempScope` for temporary register cleanup. -pub(crate) struct TempSymtable<'uref, 'temp, 'local> { +pub(crate) struct TempSymtable<'temp, 'local> { /// Reference to the underlying local symbol table. - symtable: &'temp mut LocalSymtable<'uref, 'local>, + symtable: &'temp mut LocalSymtable<'local>, /// Number of temporary registers that were already reserved on creation. base_temp: u8, @@ -387,16 +387,16 @@ pub(crate) struct TempSymtable<'uref, 'temp, 'local> { count_temps: Rc>, } -impl<'uref, 'temp, 'local> Drop for TempSymtable<'uref, 'temp, 'local> { +impl<'temp, 'local> Drop for TempSymtable<'temp, 'local> { fn drop(&mut self) { debug_assert_eq!(self.base_temp, *self.next_temp.borrow(), "Unbalanced temp drops"); self.symtable.count_temps = max(self.symtable.count_temps, *self.count_temps.borrow()); } } -impl<'uref, 'temp, 'local> TempSymtable<'uref, 'temp, 'local> { +impl<'temp, 'local> TempSymtable<'temp, 'local> { /// Creates a new temporary symbol table from a `local` table. - fn new(symtable: &'temp mut LocalSymtable<'uref, 'local>) -> Self { + fn new(symtable: &'temp mut LocalSymtable<'local>) -> Self { let base_temp = symtable.active_temps.get(); Self { symtable, @@ -514,7 +514,7 @@ mod tests { #[test] fn test_global_put_and_get() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let reg = global.put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -540,7 +540,7 @@ mod tests { #[test] fn test_global_get_case_insensitive() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); global.put_global(SymbolKey::from("MyVar"), SymbolPrototype::Scalar(ExprType::Double))?; let (reg, proto) = global.get_global(&VarRef::new("myvar", None))?; @@ -555,7 +555,7 @@ mod tests { #[test] fn test_global_get_incompatible_type() { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); global .put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer)) .unwrap(); @@ -567,7 +567,7 @@ mod tests { #[test] fn test_global_get_undefined() { let upcalls = HashMap::default(); - let global = GlobalSymtable::new(&upcalls); + let global = GlobalSymtable::new(upcalls); let err = global.get_global(&VarRef::new("x", None)).unwrap_err(); assert_eq!("Undefined global symbol x", err.to_string()); @@ -576,7 +576,7 @@ mod tests { #[test] fn test_local_put_and_get() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let reg = @@ -597,7 +597,7 @@ mod tests { #[test] fn test_local_shadows_global() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); global.put_global(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; let mut local = global.enter_scope(); @@ -613,7 +613,7 @@ mod tests { #[test] fn test_local_falls_through_to_global() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); global.put_global(SymbolKey::from("g"), SymbolPrototype::Scalar(ExprType::Integer))?; let local = global.enter_scope(); @@ -627,7 +627,7 @@ mod tests { #[test] fn test_local_get_undefined() { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let local = global.enter_scope(); let err = local.get_local_or_global(&VarRef::new("nope", None)).unwrap_err(); @@ -637,7 +637,7 @@ mod tests { #[test] fn test_local_put_global_through_local() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let reg = @@ -655,7 +655,7 @@ mod tests { #[test] fn test_fixup_local_type() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -670,7 +670,7 @@ mod tests { #[test] fn test_fixup_local_type_undefined() { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let err = @@ -681,7 +681,7 @@ mod tests { #[test] fn test_leave_scope_counts_locals_only() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -692,7 +692,7 @@ mod tests { #[test] fn test_leave_scope_counts_locals_and_temps() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; { @@ -708,7 +708,7 @@ mod tests { #[test] fn test_leave_scope_empty() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let local = global.enter_scope(); assert_eq!(0, local.leave_scope()?); Ok(()) @@ -717,7 +717,7 @@ mod tests { #[test] fn test_define_and_get_user_callable() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let md = CallableMetadataBuilder::new("MY_FUNC") .with_return_type(ExprType::Integer) @@ -734,7 +734,7 @@ mod tests { #[test] fn test_define_user_callable_already_defined() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let md = CallableMetadataBuilder::new("DUP").test_build(); global.define_user_callable(&VarRef::new("dup", None), md)?; @@ -749,7 +749,7 @@ mod tests { #[test] fn test_define_user_callable_via_local() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let md = CallableMetadataBuilder::new("SUB1").test_build(); @@ -768,7 +768,7 @@ mod tests { let mut upcalls_map = HashMap::new(); upcalls_map.insert(key, md); - let global = GlobalSymtable::new(&upcalls_map); + let global = GlobalSymtable::new(upcalls_map); let found = global.get_callable(&SymbolKey::from("builtin")); assert!(found.is_some()); assert_eq!("BUILTIN", found.unwrap().name()); @@ -782,7 +782,7 @@ mod tests { let mut upcalls_map = HashMap::new(); upcalls_map.insert(key, builtin_md); - let mut global = GlobalSymtable::new(&upcalls_map); + let mut global = GlobalSymtable::new(upcalls_map); let user_md = CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Integer).test_build(); global.define_user_callable(&VarRef::new("shared", None), user_md).unwrap(); @@ -794,14 +794,14 @@ mod tests { #[test] fn test_get_callable_not_found() { let upcalls = HashMap::default(); - let global = GlobalSymtable::new(&upcalls); + let global = GlobalSymtable::new(upcalls); assert!(global.get_callable(&SymbolKey::from("nope")).is_none()); } #[test] fn test_temp_scope_first() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -816,7 +816,7 @@ mod tests { #[test] fn test_temp_scope_first_no_locals() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); { let temp = local.frozen(); @@ -829,7 +829,7 @@ mod tests { #[test] fn test_temp_scope_first_with_outer_allocation() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; { @@ -846,7 +846,7 @@ mod tests { #[test] fn test_temp_scope() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); assert_eq!( Register::local(0).unwrap(), @@ -885,7 +885,7 @@ mod tests { #[test] fn test_with_reserved_temp_register_index() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -905,7 +905,7 @@ mod tests { #[test] fn test_with_reserved_temp_shifts_temp_scope_base() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; @@ -926,7 +926,7 @@ mod tests { #[test] fn test_with_reserved_temp_released_after_error() { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let err = local @@ -951,7 +951,7 @@ mod tests { #[test] fn test_temp_scope_lookup_vars() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); global.put_global(SymbolKey::from("g"), SymbolPrototype::Scalar(ExprType::Integer))?; let mut local = global.enter_scope(); local.put_local(SymbolKey::from("l"), SymbolPrototype::Scalar(ExprType::Text))?; @@ -974,7 +974,7 @@ mod tests { #[test] fn test_temp_scope_lookup_callable() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let md = CallableMetadataBuilder::new("FOO").test_build(); global.define_user_callable(&VarRef::new("foo", None), md)?; @@ -991,7 +991,7 @@ mod tests { #[test] fn test_multiple_scopes_independent_locals() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); { let mut local = global.enter_scope(); @@ -1017,7 +1017,7 @@ mod tests { #[test] fn test_global_put_and_get_array() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let reg = global.put_global( SymbolKey::from("arr"), @@ -1037,7 +1037,7 @@ mod tests { #[test] fn test_local_put_and_get_array() -> Result<()> { let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(&upcalls); + let mut global = GlobalSymtable::new(upcalls); let mut local = global.enter_scope(); let reg = local.put_local( diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index ce54bd4f..3e602cbd 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -105,7 +105,7 @@ fn static_end_code(expr: &Expr) -> Option<(i32, LineCol)> { /// Compiles an assignment statement `span` into the `codegen` block. fn compile_assignment( codegen: &mut Codegen, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, span: AssignmentSpan, ) -> Result<()> { let vref_pos = span.vref_pos; @@ -253,7 +253,7 @@ fn compile_case_relop( /// Compiles one `CASE` guard and returns the register and source position of its boolean result. fn compile_case_guard( ctx: &mut Context, - symtable: &mut TempSymtable<'_, '_, '_>, + symtable: &mut TempSymtable<'_, '_>, test_reg: Register, test_type: ExprType, guard: CaseGuardSpan, @@ -326,7 +326,7 @@ fn compile_case_guard( /// Compiles a `SELECT` statement and emits bytecode into `ctx`. fn compile_select( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, span: SelectSpan, ) -> Result<()> { let end_pos = span.end_pos; @@ -426,10 +426,10 @@ fn compile_select( } /// Compiles a `DO` loop and emits bytecode into `ctx`. -fn compile_do(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: DoSpan) -> Result<()> { +fn compile_do(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: DoSpan) -> Result<()> { fn compile_guard( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, guard: Expr, ) -> Result<(Register, LineCol)> { let guard_pos = guard.start_pos(); @@ -536,11 +536,7 @@ fn compile_do(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: DoS } /// Compiles a `FOR` loop and emits bytecode into `ctx`. -fn compile_for( - ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_>, - span: ForSpan, -) -> Result<()> { +fn compile_for(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: ForSpan) -> Result<()> { if span.iter_double && span.iter.ref_type.is_none() { match symtable.get_local_or_global(&span.iter) { Ok(..) => { @@ -612,7 +608,7 @@ fn compile_for( } /// Compiles an `IF` statement `span` into the `ctx`. -fn compile_if(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: IfSpan) -> Result<()> { +fn compile_if(ctx: &mut Context, symtable: &mut LocalSymtable<'_>, span: IfSpan) -> Result<()> { let mut end_pcs: Vec = vec![]; let nbranches = span.branches.len(); @@ -662,7 +658,7 @@ fn compile_if(ctx: &mut Context, symtable: &mut LocalSymtable<'_, '_>, span: IfS /// Compiles a `WHILE` loop and emits bytecode into `ctx`. fn compile_while( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, span: WhileSpan, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -695,7 +691,7 @@ fn compile_while( /// Compiles a single `stmt` into the `ctx`. fn compile_stmt( ctx: &mut Context, - symtable: &mut LocalSymtable<'_, '_>, + symtable: &mut LocalSymtable<'_>, stmt: Statement, ) -> Result<()> { let start_pc = ctx.codegen.next_pc(); @@ -1022,7 +1018,7 @@ fn compile_stmt( } /// Compiles a sequence of `stmts` that all live in the same `symtable` scope. -fn compile_scope(ctx: &mut Context, mut symtable: LocalSymtable<'_, '_>, stmts: I) -> Result<()> +fn compile_scope(ctx: &mut Context, mut symtable: LocalSymtable<'_>, stmts: I) -> Result<()> where I: Iterator>, { @@ -1238,7 +1234,7 @@ fn prepare_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile_with_globals( input: &mut dyn io::Read, - upcalls: &HashMap>, + upcalls: HashMap>, global_defs: &[GlobalDef], ) -> Result { let mut ctx = Context::default(); @@ -1270,7 +1266,7 @@ pub fn compile_with_globals( /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. pub fn compile( input: &mut dyn io::Read, - upcalls: &HashMap>, + upcalls: HashMap>, ) -> Result { compile_with_globals(input, upcalls, &[]) } @@ -1283,7 +1279,7 @@ mod tests { use crate::vm::{StopReason, Vm}; fn compile_and_get_global(defs: &[GlobalDef], name: &str) -> ConstantDatum { - let image = compile_with_globals(&mut "".as_bytes(), &HashMap::default(), defs) + let image = compile_with_globals(&mut "".as_bytes(), HashMap::default(), defs) .expect("compilation should succeed"); let mut vm = Vm::new(HashMap::default()); vm.load(image); @@ -1366,7 +1362,7 @@ mod tests { initial_value: Some(ConstantDatum::Double(1.5)), }, }]; - let result = compile_with_globals(&mut "".as_bytes(), &HashMap::default(), &defs); + let result = compile_with_globals(&mut "".as_bytes(), HashMap::default(), &defs); assert!(matches!(result, Err(Error::TypeMismatch(..)))); } } diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 841240c6..47179739 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -436,7 +436,7 @@ mod tests { #[test] fn test_exec_empty_compilation_is_eof() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"".as_slice(), &HashMap::default()).unwrap(); + let image = compile(&mut b"".as_slice(), HashMap::default()).unwrap(); vm.load(image); match vm.exec() { StopReason::Eof => (), @@ -451,7 +451,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); let image = - compile(&mut b"OUT 30: OUT 20".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"OUT 30: OUT 20".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); @@ -484,7 +484,7 @@ mod tests { #[tokio::test] async fn test_exec_end_code_default() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END".as_slice(), &HashMap::default()).unwrap(); + let image = compile(&mut b"END".as_slice(), HashMap::default()).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.is_success() => (), @@ -495,7 +495,7 @@ mod tests { #[tokio::test] async fn test_exec_end_code_explicit() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END 3".as_slice(), &HashMap::default()).unwrap(); + let image = compile(&mut b"END 3".as_slice(), HashMap::default()).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.to_i32() == 3 => (), @@ -511,7 +511,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"POS_CAPTURE".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -528,7 +528,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE 42".as_slice(), &only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"POS_CAPTURE 42".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -545,7 +545,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), &only_metadata(&upcalls_by_name)) + compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), only_metadata(&upcalls_by_name)) .unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); @@ -569,8 +569,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); - let image = compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), &only_metadata(&upcalls_by_name)) - .unwrap(); + let image = + compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index 7347e83f..ed6c1318 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -321,7 +321,7 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let console = Rc::from(RefCell::from(String::new())); let mut upcalls_by_name: HashMap> = HashMap::default(); callables::register_all(&mut upcalls_by_name, console.clone()); - let image = { compile(&mut source.as_bytes(), &only_metadata(&upcalls_by_name)) }; + let image = { compile(&mut source.as_bytes(), only_metadata(&upcalls_by_name)) }; let image = match image { Ok(image) => image, From 90cd7efb37ee4b65e1f48bd9843e01a4d485c6d8 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 07:18:32 +0100 Subject: [PATCH 38/53] core2: Unify compile_with_globals and compile There is no need for the compile_with_globals and compile duality, so unify both under compile_with_globals after renaming it to compile. --- core2/examples/config.rs | 5 ++--- core2/src/compiler/mod.rs | 2 +- core2/src/compiler/top.rs | 16 +++------------- core2/src/lib.rs | 4 +--- core2/src/vm/mod.rs | 19 +++++++++++-------- core2/tests/testutils/mod.rs | 2 +- 6 files changed, 19 insertions(+), 29 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index ee00a4a4..a6a6269e 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -21,8 +21,7 @@ //! not have side-effects. use endbasic_core2::{ - ConstantDatum, ExprType, GlobalDef, GlobalDefKind, StopReason, Vm, compile_with_globals, - only_metadata, + ConstantDatum, ExprType, GlobalDef, GlobalDefKind, StopReason, Vm, compile, only_metadata, }; use std::collections::HashMap; @@ -79,7 +78,7 @@ fn main() { // Compile the script, making the pre-defined globals visible to it. let upcalls = HashMap::default(); - let image = compile_with_globals(&mut SCRIPT.as_bytes(), only_metadata(&upcalls), &global_defs) + let image = compile(&mut SCRIPT.as_bytes(), only_metadata(&upcalls), &global_defs) .expect("Compilation failed"); // Load and execute the compiled image. diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index e7cb0d24..1d7200c8 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -34,7 +34,7 @@ mod syms; pub use syms::SymbolKey; mod top; -pub use top::{GlobalDef, GlobalDefKind, compile, compile_with_globals, only_metadata}; +pub use top::{GlobalDef, GlobalDefKind, compile, only_metadata}; /// Errors that can occur during compilation. #[derive(Debug, thiserror::Error)] diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 3e602cbd..7da27550 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -1232,7 +1232,7 @@ fn prepare_globals( /// pre-defined as global variables visible to the compiled program. /// /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. -pub fn compile_with_globals( +pub fn compile( input: &mut dyn io::Read, upcalls: HashMap>, global_defs: &[GlobalDef], @@ -1261,16 +1261,6 @@ pub fn compile_with_globals( ctx.codegen.build_image(global_vars, ctx.data) } -/// Compiles the `input` into an `Image` that can be executed by the VM. -/// -/// `upcalls` contains the metadata of all built-in callables that the compiled code can use. -pub fn compile( - input: &mut dyn io::Read, - upcalls: HashMap>, -) -> Result { - compile_with_globals(input, upcalls, &[]) -} - #[cfg(test)] mod tests { use super::*; @@ -1279,7 +1269,7 @@ mod tests { use crate::vm::{StopReason, Vm}; fn compile_and_get_global(defs: &[GlobalDef], name: &str) -> ConstantDatum { - let image = compile_with_globals(&mut "".as_bytes(), HashMap::default(), defs) + let image = compile(&mut "".as_bytes(), HashMap::default(), defs) .expect("compilation should succeed"); let mut vm = Vm::new(HashMap::default()); vm.load(image); @@ -1362,7 +1352,7 @@ mod tests { initial_value: Some(ConstantDatum::Double(1.5)), }, }]; - let result = compile_with_globals(&mut "".as_bytes(), HashMap::default(), &defs); + let result = compile(&mut "".as_bytes(), HashMap::default(), &defs); assert!(matches!(result, Err(Error::TypeMismatch(..)))); } } diff --git a/core2/src/lib.rs b/core2/src/lib.rs index af5bae38..00d96c22 100644 --- a/core2/src/lib.rs +++ b/core2/src/lib.rs @@ -30,9 +30,7 @@ mod vm; pub use ast::{ArgSep, ExprType}; pub use bytecode::{ExitCode, InvalidExitCodeError, VarArgTag}; pub use callable::*; -pub use compiler::{ - GlobalDef, GlobalDefKind, SymbolKey, compile, compile_with_globals, only_metadata, -}; +pub use compiler::{GlobalDef, GlobalDefKind, SymbolKey, compile, only_metadata}; pub use mem::ConstantDatum; pub use vm::{GetGlobalError, GetGlobalResult, StopReason, Vm}; diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 47179739..796f6492 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -436,7 +436,7 @@ mod tests { #[test] fn test_exec_empty_compilation_is_eof() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"".as_slice(), HashMap::default()).unwrap(); + let image = compile(&mut b"".as_slice(), HashMap::default(), &[]).unwrap(); vm.load(image); match vm.exec() { StopReason::Eof => (), @@ -451,7 +451,8 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); let image = - compile(&mut b"OUT 30: OUT 20".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"OUT 30: OUT 20".as_slice(), only_metadata(&upcalls_by_name), &[]) + .unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); @@ -484,7 +485,7 @@ mod tests { #[tokio::test] async fn test_exec_end_code_default() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END".as_slice(), HashMap::default()).unwrap(); + let image = compile(&mut b"END".as_slice(), HashMap::default(), &[]).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.is_success() => (), @@ -495,7 +496,7 @@ mod tests { #[tokio::test] async fn test_exec_end_code_explicit() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END 3".as_slice(), HashMap::default()).unwrap(); + let image = compile(&mut b"END 3".as_slice(), HashMap::default(), &[]).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.to_i32() == 3 => (), @@ -511,7 +512,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"POS_CAPTURE".as_slice(), only_metadata(&upcalls_by_name), &[]).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -528,7 +529,8 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE 42".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"POS_CAPTURE 42".as_slice(), only_metadata(&upcalls_by_name), &[]) + .unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -545,7 +547,7 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), only_metadata(&upcalls_by_name)) + compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), only_metadata(&upcalls_by_name), &[]) .unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); @@ -570,7 +572,8 @@ mod tests { upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); let image = - compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), only_metadata(&upcalls_by_name)).unwrap(); + compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), only_metadata(&upcalls_by_name), &[]) + .unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index ed6c1318..f38fe1a7 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -321,7 +321,7 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let console = Rc::from(RefCell::from(String::new())); let mut upcalls_by_name: HashMap> = HashMap::default(); callables::register_all(&mut upcalls_by_name, console.clone()); - let image = { compile(&mut source.as_bytes(), only_metadata(&upcalls_by_name)) }; + let image = { compile(&mut source.as_bytes(), only_metadata(&upcalls_by_name), &[]) }; let image = match image { Ok(image) => image, From 57c64e28ed6844ccc692b6fa1925f51285dff9e1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 10:48:08 +0100 Subject: [PATCH 39/53] core2: Add missing tests for callable nesting Spotted while working on the upcoming change to compile callables differently and noticing that there was a dead code path. --- core2/tests/test_functions.md | 34 ++++++++++++++++++++++++++++++++++ core2/tests/test_subs.md | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 48d138cf..c0734fcb 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -1142,3 +1142,37 @@ END FUNCTION ```plain 4:10: Cannot redefine foo% ``` + +# Test: Function nesting within a function + +## Source + +```basic +FUNCTION foo + FUNCTION bar + END FUNCTION +END FUNCTION +``` + +## Compilation errors + +```plain +2:5: Cannot nest FUNCTION or SUB definitions +``` + +# Test: Function nesting within a sub + +## Source + +```basic +SUB foo + FUNCTION bar + END FUNCTION +END SUB +``` + +## Compilation errors + +```plain +2:5: Cannot nest FUNCTION or SUB definitions +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index a9958745..26f9db97 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -545,3 +545,37 @@ END SUB ```plain 4:5: Cannot redefine foo ``` + +# Test: Sub nesting within a sub + +## Source + +```basic +SUB foo + SUB bar + END SUB +END SUB +``` + +## Compilation errors + +```plain +2:5: Cannot nest FUNCTION or SUB definitions +``` + +# Test: Sub nesting within a function + +## Source + +```basic +FUNCTION foo + SUB bar + END SUB +END FUNCTION +``` + +## Compilation errors + +```plain +2:5: Cannot nest FUNCTION or SUB definitions +``` From a6e7b4b49bf3ff419ca8a96c8188a8ec5a619ca9 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 07:40:19 +0100 Subject: [PATCH 40/53] core2: Add support for DECLARE This is a brand new statement that wasn't supported in the old core but should have been. We need to add it now to allow compiling callables as soon as they are found instead of at the end of the program, which is a prerequisite for allowing REPL-style compilation. Note that, in this commit, we still do not require predeclaration of callables in mutually-called functions or subroutines. This is difficult to do right now but will become easy in the subsequent change, so we delay this part of the feature until then. --- core2/README.md | 1 + core2/src/ast.rs | 17 +++ core2/src/callable.rs | 18 ++-- core2/src/compiler/mod.rs | 2 +- core2/src/compiler/syms.rs | 45 +++++--- core2/src/compiler/top.rs | 81 ++++++++++----- core2/src/lexer.rs | 10 ++ core2/src/parser.rs | 189 ++++++++++++++++++++++++++++------ core2/tests/test_functions.md | 123 ++++++++++++++++++++++ core2/tests/test_subs.md | 121 ++++++++++++++++++++++ 10 files changed, 527 insertions(+), 80 deletions(-) diff --git a/core2/README.md b/core2/README.md index c0c68c99..5d371610 100644 --- a/core2/README.md +++ b/core2/README.md @@ -38,6 +38,7 @@ currently supports: * Strong typing with optional variable type annotations. * `DATA` statements for literal primitive values. Booleans, numbers, and strings are supported, but strings must be double-quoted. +* `DECLARE FUNCTION` / `DECLARE SUB`. * `DO` / `LOOP` statements with optional `UNTIL` / `WHILE` pre- and post-guards and optional `EXIT DO` early terminations. * `DIM SHARED` for global variables. diff --git a/core2/src/ast.rs b/core2/src/ast.rs index c8a3a66e..1f9cc6fe 100644 --- a/core2/src/ast.rs +++ b/core2/src/ast.rs @@ -446,6 +446,20 @@ pub struct DataSpan { pub values: Vec>, } +/// Components of a `DECLARE` statement. +#[derive(Debug, PartialEq)] +pub struct DeclareSpan { + /// Name of the callable, expressed as a variable reference. For functions, this contains + /// a type, and for subroutines, it does not. + pub name: VarRef, + + /// Position of the name of the callable. + pub name_pos: LineCol, + + /// Definition of the callable parameters. + pub params: Vec, +} + /// Components of a variable definition. /// /// Given that a definition causes the variable to be initialized to a default value, it is @@ -723,6 +737,9 @@ pub enum Statement { /// Represents a `DATA` statement. Data(DataSpan), + /// Represents a `DECLARE` statement. + Declare(DeclareSpan), + /// Represents a variable definition. Dim(DimSpan), diff --git a/core2/src/callable.rs b/core2/src/callable.rs index 2cb02875..cdd2943b 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -41,7 +41,7 @@ pub enum CallError { pub type CallResult = Result; /// Syntax specification for a required scalar parameter. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct RequiredValueSyntax { /// The name of the parameter for help purposes. pub name: Cow<'static, str>, @@ -51,7 +51,7 @@ pub struct RequiredValueSyntax { } /// Syntax specification for a required reference parameter. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct RequiredRefSyntax { /// The name of the parameter for help purposes. pub name: Cow<'static, str>, @@ -68,7 +68,7 @@ pub struct RequiredRefSyntax { /// Syntax specification for an optional scalar parameter. /// /// Optional parameters are only supported in commands. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct OptionalValueSyntax { /// The name of the parameter for help purposes. pub name: Cow<'static, str>, @@ -78,7 +78,7 @@ pub struct OptionalValueSyntax { } /// Specifies the type constraints for a repeated parameter. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum RepeatedTypeSyntax { /// Allows any value type, including empty arguments. The values pushed onto the stack have /// the same semantics as those pushed by `AnyValueSyntax`. @@ -94,7 +94,7 @@ pub enum RepeatedTypeSyntax { /// Syntax specification for a repeated parameter. /// /// The repeated parameter must appear after all singular positional parameters. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct RepeatedSyntax { /// The name of the parameter for help purposes. pub name: Cow<'static, str>, @@ -152,7 +152,7 @@ impl RepeatedSyntax { } /// Syntax specification for a parameter that accepts any scalar type. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct AnyValueSyntax { /// The name of the parameter for help purposes. pub name: Cow<'static, str>, @@ -211,7 +211,7 @@ impl ArgSepSyntax { /// /// Every item in this enum is composed of a struct that provides the details on the parameter and /// a struct that provides the details on how this parameter is separated from the next. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum SingularArgSyntax { /// A required scalar value with the syntax details and the separator that follows. RequiredValue(RequiredValueSyntax, ArgSepSyntax), @@ -233,7 +233,7 @@ pub enum SingularArgSyntax { /// builtin functions must never be ill-defined. // TODO(jmmv): It might be nice to try to express these restrictions in the type system, but // things are already too verbose as they are... -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct CallableSyntax { /// Ordered list of singular arguments that appear before repeated arguments. pub(crate) singular: Cow<'static, [SingularArgSyntax]>, @@ -484,7 +484,7 @@ impl CallableMetadataBuilder { /// /// The callable is expected to hold onto an instance of this object within its struct to make /// queries fast. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct CallableMetadata { /// Name of the callable, stored in uppercase. name: Cow<'static, str>, diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 1d7200c8..73093f89 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -61,7 +61,7 @@ pub enum Error { CallableSyntax(LineCol, CallableMetadata), /// Attempt to nest FUNCTION or SUB definitions. - #[error("{0}: Cannot nest FUNCTION or SUB definitions")] + #[error("{0}: Cannot nest FUNCTION or SUB declarations nor definitions")] CannotNestUserCallables(LineCol), /// Attempt to redefine an already-defined label. diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index acefbbe8..e42ea179 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -195,7 +195,7 @@ impl GlobalSymtable { } /// Defines a new user-defined `vref` callable with `md` metadata. - pub(crate) fn define_user_callable( + pub(crate) fn declare_user_callable( &mut self, vref: &VarRef, md: Rc, @@ -204,8 +204,12 @@ impl GlobalSymtable { if self.globals.get(&key).is_some() { return Err(Error::AlreadyDefined(vref.clone())); } - let previous = self.user_callables.insert(key, md); - if previous.is_none() { Ok(()) } else { Err(Error::AlreadyDefined(vref.clone())) } + if let Some(previous_md) = self.user_callables.insert(key.clone(), md.clone()) + && previous_md != md + { + return Err(Error::AlreadyDefined(vref.clone())); + } + Ok(()) } /// Gets a callable by its name `key`. @@ -254,13 +258,13 @@ impl<'a> LocalSymtable<'a> { } } - /// Defines a new user-defined `vref` callable with `md` metadata. - pub(crate) fn define_user_callable( + /// Declares a new user-defined `vref` callable with `md` metadata. + pub(crate) fn declare_user_callable( &mut self, vref: &VarRef, md: Rc, ) -> Result<()> { - self.symtable.define_user_callable(vref, md) + self.symtable.declare_user_callable(vref, md) } /// Freezes this table to get a `TempSymtable` that can be used to compile expressions. @@ -722,7 +726,7 @@ mod tests { let md = CallableMetadataBuilder::new("MY_FUNC") .with_return_type(ExprType::Integer) .test_build(); - global.define_user_callable(&VarRef::new("my_func", None), md)?; + global.declare_user_callable(&VarRef::new("my_func", None), md)?; let found = global.get_callable(&SymbolKey::from("my_func")); assert!(found.is_some()); @@ -732,15 +736,30 @@ mod tests { } #[test] - fn test_define_user_callable_already_defined() -> Result<()> { + fn test_define_user_callable_already_defined_but_is_compatible() -> Result<()> { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(upcalls); let md = CallableMetadataBuilder::new("DUP").test_build(); - global.define_user_callable(&VarRef::new("dup", None), md)?; + global.declare_user_callable(&VarRef::new("dup", None), md)?; let md2 = CallableMetadataBuilder::new("DUP").test_build(); - let err = global.define_user_callable(&VarRef::new("dup", None), md2).unwrap_err(); + global.declare_user_callable(&VarRef::new("dup", None), md2)?; + + Ok(()) + } + + #[test] + fn test_define_user_callable_already_defined_but_is_incompatible() -> Result<()> { + let upcalls = HashMap::default(); + let mut global = GlobalSymtable::new(upcalls); + + let md = CallableMetadataBuilder::new("DUP").test_build(); + global.declare_user_callable(&VarRef::new("dup", None), md)?; + + let md2 = + CallableMetadataBuilder::new("DUP").with_return_type(ExprType::Integer).test_build(); + let err = global.declare_user_callable(&VarRef::new("dup", None), md2).unwrap_err(); assert_eq!("Cannot redefine dup", err.to_string()); Ok(()) @@ -753,7 +772,7 @@ mod tests { let mut local = global.enter_scope(); let md = CallableMetadataBuilder::new("SUB1").test_build(); - local.define_user_callable(&VarRef::new("sub1", None), md)?; + local.declare_user_callable(&VarRef::new("sub1", None), md)?; let found = local.get_callable(&SymbolKey::from("sub1")); assert!(found.is_some()); @@ -785,7 +804,7 @@ mod tests { let mut global = GlobalSymtable::new(upcalls_map); let user_md = CallableMetadataBuilder::new("SHARED").with_return_type(ExprType::Integer).test_build(); - global.define_user_callable(&VarRef::new("shared", None), user_md).unwrap(); + global.declare_user_callable(&VarRef::new("shared", None), user_md).unwrap(); let found = global.get_callable(&SymbolKey::from("shared")).unwrap(); assert_eq!(Some(ExprType::Integer), found.return_type()); @@ -976,7 +995,7 @@ mod tests { let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(upcalls); let md = CallableMetadataBuilder::new("FOO").test_build(); - global.define_user_callable(&VarRef::new("foo", None), md)?; + global.declare_user_callable(&VarRef::new("foo", None), md)?; let mut local = global.enter_scope(); { diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 7da27550..ad86523c 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -780,31 +780,14 @@ fn compile_stmt( Statement::Callable(span) => { mark_start = false; - let mut syntax = vec![]; - for (i, param) in span.params.iter().enumerate() { - let sep = if i == span.params.len() - 1 { - ArgSepSyntax::End - } else { - ArgSepSyntax::Exactly(ArgSep::Long) - }; - syntax.push(SingularArgSyntax::RequiredValue( - RequiredValueSyntax { - name: Cow::Owned(param.name.to_owned()), - vtype: param.ref_type.unwrap_or(ExprType::Integer), - }, - sep, - )); - } - - let mut builder = CallableMetadataBuilder::new_dynamic(span.name.name.to_owned()) - .with_dynamic_syntax(vec![(syntax, None)]); - if let Some(ctype) = span.name.ref_type { - builder = builder.with_return_type(ctype); + declare_callable(symtable, &span.name, span.name_pos, &span.params)?; + // If declaration succeeds, we still have to check for callable redefinition. + // This linear scan is not the most efficient, but it's fine for now. + for callable in &ctx.user_callables { + if span.name == callable.name { + return Err(Error::AlreadyDefined(span.name_pos, span.name)); + } } - - symtable - .define_user_callable(&span.name, builder.build()) - .map_err(|e| Error::from_syms(e, span.name_pos))?; ctx.user_callables.push(span); } @@ -812,6 +795,14 @@ fn compile_stmt( ctx.data.extend(span.values.into_iter().map(|expr| expr.map(data_expr_to_constant))); } + Statement::Declare(span) => { + mark_start = false; + if ctx.current_callable.is_some() { + return Err(Error::CannotNestUserCallables(span.name_pos)); + } + declare_callable(symtable, &span.name, span.name_pos, &span.params)?; + } + Statement::Dim(span) => { let name_pos = span.name_pos; let key = SymbolKey::from(&span.name); @@ -1032,12 +1023,51 @@ where Ok(()) } +/// Declares a callable. +/// +/// If the callable is already defined, this ensures the new declaration matches the previous one +/// and raises an error if not. +fn declare_callable( + symtable: &mut LocalSymtable, + name: &VarRef, + name_pos: LineCol, + params: &[VarRef], +) -> Result<()> { + let mut syntax = vec![]; + for (i, param) in params.iter().enumerate() { + let sep = if i == params.len() - 1 { + ArgSepSyntax::End + } else { + ArgSepSyntax::Exactly(ArgSep::Long) + }; + syntax.push(SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Owned(param.name.to_owned()), + vtype: param.ref_type.unwrap_or(ExprType::Integer), + }, + sep, + )); + } + + let mut builder = CallableMetadataBuilder::new_dynamic(name.name.to_owned()) + .with_dynamic_syntax(vec![(syntax, None)]); + if let Some(ctype) = name.ref_type { + builder = builder.with_return_type(ctype); + } + + symtable.declare_user_callable(name, builder.build()).map_err(|e| Error::from_syms(e, name_pos)) +} + /// Compiles all user-defined callables that have been captured in `ctx`. fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> Result<()> { let user_callables: Vec = ctx.user_callables.drain(..).collect(); debug_assert!(ctx.user_callables.is_empty()); for callable in user_callables { + if ctx.current_callable.is_some() { + return Err(Error::CannotNestUserCallables(callable.name_pos)); + } + let start_pc = ctx.codegen.next_pc(); let key_pos = callable.name_pos; @@ -1084,9 +1114,6 @@ fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> R } compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; - if let Some(span) = ctx.user_callables.first() { - return Err(Error::CannotNestUserCallables(span.name_pos)); - } let return_addr = ctx.codegen.next_pc(); ctx.codegen.emit(bytecode::make_return(), callable.end_pos); diff --git a/core2/src/lexer.rs b/core2/src/lexer.rs index ab95ee5d..42508927 100644 --- a/core2/src/lexer.rs +++ b/core2/src/lexer.rs @@ -72,6 +72,7 @@ pub enum Token { Case, Data, + Declare, Do, Else, Elseif, @@ -156,6 +157,7 @@ impl fmt::Display for Token { Token::Case => write!(f, "CASE"), Token::Data => write!(f, "DATA"), + Token::Declare => write!(f, "DECLARE"), Token::Do => write!(f, "DO"), Token::Else => write!(f, "ELSE"), Token::Elseif => write!(f, "ELSEIF"), @@ -516,6 +518,7 @@ impl<'a> Lexer<'a> { "BOOLEAN" => Token::BooleanName, "CASE" => Token::Case, "DATA" => Token::Data, + "DECLARE" => Token::Declare, "DIM" => Token::Dim, "DO" => Token::Do, "DOUBLE" => Token::DoubleName, @@ -1083,6 +1086,13 @@ mod tests { ); } + #[test] + fn test_declare() { + do_ok_test("DECLARE", &[ts(Token::Declare, 1, 1, 7), ts(Token::Eof, 1, 8, 0)]); + + do_ok_test("declare", &[ts(Token::Declare, 1, 1, 7), ts(Token::Eof, 1, 8, 0)]); + } + #[test] fn test_dim() { do_ok_test( diff --git a/core2/src/parser.rs b/core2/src/parser.rs index 9ff623be..5c57c123 100644 --- a/core2/src/parser.rs +++ b/core2/src/parser.rs @@ -962,6 +962,7 @@ impl<'a> Parser<'a> { Token::BooleanName | Token::Case | Token::Data + | Token::Declare | Token::Do | Token::Dim | Token::DoubleName @@ -1450,61 +1451,81 @@ impl<'a> Parser<'a> { Ok((body, end_pos)) } - /// Parses a `FUNCTION` definition. - fn parse_function(&mut self, function_pos: LineCol) -> Result { + /// Parses a `DECLARE` statement. + fn parse_declare(&mut self) -> Result { let token_span = self.lexer.read()?; - let name = match token_span.token { - Token::Symbol(name) => { - if name.ref_type.is_none() { - VarRef::new(name.name, Some(ExprType::Integer)) - } else { - name - } - } + let (name, name_pos, params) = match token_span.token { + Token::Function => self.parse_callable_signature(true, true)?, + + Token::Sub => self.parse_callable_signature(true, false)?, + _ => { return Err(Error::Bad( token_span.pos, - "Expected a function name after FUNCTION".to_owned(), + "Expected FUNCTION or SUB after DECLARE".to_owned(), )); } }; - let name_pos = token_span.pos; - - let params = self.parse_callable_args()?; - self.expect_and_consume(Token::Eol, "Expected newline after FUNCTION name")?; - - let (body, end_pos) = self.parse_callable_body(function_pos, Token::Function)?; - - Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + Ok(Statement::Declare(DeclareSpan { name, name_pos, params })) } - /// Parses a `SUB` definition. - fn parse_sub(&mut self, sub_pos: LineCol) -> Result { + /// Parses the signature of a callable declaration or definition. + fn parse_callable_signature( + &mut self, + is_declare: bool, + is_function: bool, + ) -> Result<(VarRef, LineCol, Vec)> { + let kw_name = if is_function { "FUNCTION" } else { "SUB" }; let token_span = self.lexer.read()?; let name = match token_span.token { - Token::Symbol(name) => { - if name.ref_type.is_some() { + Token::Symbol(name) => match (name.ref_type, is_function) { + (None, true) => VarRef::new(name.name, Some(ExprType::Integer)), + (Some(..), true) => name, + (None, false) => name, + (Some(..), false) => { return Err(Error::Bad( token_span.pos, "SUBs cannot return a value so type annotations are not allowed".to_owned(), )); } - name - } + }, _ => { return Err(Error::Bad( token_span.pos, - "Expected a function name after SUB".to_owned(), + format!("Expected a name after {}", kw_name), )); } }; let name_pos = token_span.pos; let params = self.parse_callable_args()?; - self.expect_and_consume(Token::Eol, "Expected newline after SUB name")?; - let (body, end_pos) = self.parse_callable_body(sub_pos, Token::Sub)?; + let peeked = self.lexer.peek()?; + match peeked.token { + Token::Eol => (), + Token::Eof if is_declare => (), + _ => { + return Err(Error::Bad( + peeked.pos, + format!("Expected newline after {} name", kw_name), + )); + } + } + + Ok((name, name_pos, params)) + } + + /// Parses a `FUNCTION` definition. + fn parse_function(&mut self, function_pos: LineCol) -> Result { + let (name, name_pos, params) = self.parse_callable_signature(false, true)?; + let (body, end_pos) = self.parse_callable_body(function_pos, Token::Function)?; + Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) + } + /// Parses a `SUB` definition. + fn parse_sub(&mut self, sub_pos: LineCol) -> Result { + let (name, name_pos, params) = self.parse_callable_signature(false, false)?; + let (body, end_pos) = self.parse_callable_body(sub_pos, Token::Sub)?; Ok(Statement::Callable(CallableSpan { name, name_pos, params, body, end_pos })) } @@ -1862,6 +1883,7 @@ impl<'a> Parser<'a> { let token_span = self.lexer.read()?; let res = match token_span.token { Token::Data => Ok(Some(self.parse_data()?)), + Token::Declare => Ok(Some(self.parse_declare()?)), Token::Dim => Ok(Some(self.parse_dim()?)), Token::Do => { let result = self.parse_do(token_span.pos); @@ -2468,6 +2490,113 @@ mod tests { do_error_test("DATA -foo", "1:6: Expected number after -"); } + #[test] + fn test_declare_callable_no_args_eof() { + do_ok_test( + "DECLARE FUNCTION foo$", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 18), + params: vec![], + })], + ); + + do_ok_test( + "DECLARE SUB foo", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 13), + params: vec![], + })], + ); + } + + #[test] + fn test_declare_callable_no_args_not_eof() { + do_ok_test( + "DECLARE FUNCTION foo$\nREM A comment", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 18), + params: vec![], + })], + ); + + do_ok_test( + "DECLARE SUB foo\nREM A comment", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 13), + params: vec![], + })], + ); + } + + #[test] + fn test_declare_callable_consecutive() { + do_ok_test( + "DECLARE FUNCTION foo$\nDECLARE SUB bar", + &[ + Statement::Declare(DeclareSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 18), + params: vec![], + }), + Statement::Declare(DeclareSpan { + name: VarRef::new("bar", None), + name_pos: lc(2, 13), + params: vec![], + }), + ], + ); + } + + #[test] + fn test_declare_callable_multiple_params() { + do_ok_test( + "DECLARE FUNCTION foo$(x$, y, z AS BOOLEAN)", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", Some(ExprType::Text)), + name_pos: lc(1, 18), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + })], + ); + + do_ok_test( + "DECLARE SUB foo(x$, y, z AS BOOLEAN)", + &[Statement::Declare(DeclareSpan { + name: VarRef::new("foo", None), + name_pos: lc(1, 13), + params: vec![ + VarRef::new("x", Some(ExprType::Text)), + VarRef::new("y", None), + VarRef::new("z", Some(ExprType::Boolean)), + ], + })], + ); + } + + #[test] + fn test_declare_callable_errors() { + do_error_test("DECLARE", "1:8: Expected FUNCTION or SUB after DECLARE"); + do_error_test("DECLARE foo", "1:9: Expected FUNCTION or SUB after DECLARE"); + + do_error_test("DECLARE FUNCTION", "1:17: Expected a name after FUNCTION"); + do_error_test("DECLARE SUB", "1:12: Expected a name after SUB"); + + do_error_test("DECLARE FUNCTION foo()", "1:22: Expected a parameter name"); + do_error_test("DECLARE SUB foo()", "1:17: Expected a parameter name"); + + do_error_test( + "DECLARE SUB foo%", + "1:13: SUBs cannot return a value so type annotations are not allowed", + ); + } + #[test] fn test_dim_default_type() { do_ok_test( @@ -4265,7 +4394,7 @@ mod tests { #[test] fn test_function_errors() { - do_error_test("FUNCTION", "1:9: Expected a function name after FUNCTION"); + do_error_test("FUNCTION", "1:9: Expected a name after FUNCTION"); do_error_test("FUNCTION foo", "1:13: Expected newline after FUNCTION name"); do_error_test("FUNCTION foo 3", "1:14: Expected newline after FUNCTION name"); do_error_test("FUNCTION foo\nEND", "1:1: FUNCTION without END FUNCTION"); @@ -4775,7 +4904,7 @@ mod tests { #[test] fn test_sub_errors() { - do_error_test("SUB", "1:4: Expected a function name after SUB"); + do_error_test("SUB", "1:4: Expected a name after SUB"); do_error_test("SUB foo", "1:8: Expected newline after SUB name"); do_error_test("SUB foo 3", "1:9: Expected newline after SUB name"); do_error_test("SUB foo\nEND", "1:1: SUB without END SUB"); diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index c0734fcb..cebe66ab 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -1176,3 +1176,126 @@ END SUB ```plain 2:5: Cannot nest FUNCTION or SUB definitions ``` + +# Test: Function declarations + +## Source + +```basic +DECLARE FUNCTION foo +DECLARE FUNCTION foo% +DECLARE FUNCTION bar(a AS STRING) +``` + +## Disassembly + +```asm +0000: ENTER 0 # 0:0 +0001: EOF # 0:0 +``` + +# Test: Function declarations match definition + +## Source + +```basic +DECLARE FUNCTION foo + +FUNCTION foo +END FUNCTION + +DECLARE FUNCTION foo +``` + +## Disassembly + +```asm +0000: ENTER 0 # 0:0 +0001: EOF # 0:0 + +-- FOO +0002: LOADI R64, 0 # 3:10 +0003: ENTER 1 # 0:0 +0004: RETURN # 4:1 +``` + +# Test: Function declarations must be top-level + +## Source + +```basic + +FUNCTION foo + DECLARE FUNCTION bar +END FUNCTION +``` + +## Compilation errors + +```plain +3:22: Cannot nest FUNCTION or SUB declarations nor definitions +``` + +# Test: Function pre-declaration does not match pre-definition + +## Source + +```basic +DECLARE FUNCTION foo + +FUNCTION foo# +END FUNCTION +``` + +## Compilation errors + +```plain +3:10: Cannot redefine foo# +``` + +# Test: Function post-declaration does not match definition + +## Source + +```basic +FUNCTION foo# +END FUNCTION + +DECLARE FUNCTION foo +``` + +## Compilation errors + +```plain +4:18: Cannot redefine foo% +``` + +# Test: Function declarations do not match + +## Source + +```basic +DECLARE FUNCTION foo +DECLARE FUNCTION foo# +``` + +## Compilation errors + +```plain +2:18: Cannot redefine foo# +``` + +# Test: Function redeclared as sub + +## Source + +```basic +DECLARE FUNCTION foo +DECLARE SUB foo +``` + +## Compilation errors + +```plain +2:13: Cannot redefine foo +``` diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 26f9db97..322de114 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -579,3 +579,124 @@ END FUNCTION ```plain 2:5: Cannot nest FUNCTION or SUB definitions ``` + +# Test: Sub declarations + +## Source + +```basic +DECLARE SUB foo +DECLARE SUB bar(a AS STRING) +``` + +## Disassembly + +```asm +0000: ENTER 0 # 0:0 +0001: EOF # 0:0 +``` + +# Test: Sub declarations match definition + +## Source + +```basic +DECLARE SUB foo + +SUB foo +END SUB + +DECLARE SUB foo +``` + +## Disassembly + +```asm +0000: ENTER 0 # 0:0 +0001: EOF # 0:0 + +-- FOO +0002: ENTER 0 # 0:0 +0003: RETURN # 4:1 +``` + +# Test: Sub declarations must be top-level + +## Source + +```basic + +SUB foo + SUB FUNCTION bar +END SUB +``` + +## Compilation errors + +```plain +3:5: Cannot nest FUNCTION or SUB definitions +``` + +# Test: Sub pre-declaration does not match pre-definition + +## Source + +```basic +DECLARE SUB foo + +SUB foo(a AS STRING) +END SUB +``` + +## Compilation errors + +```plain +3:5: Cannot redefine foo +``` + +# Test: Sub post-declaration does not match definition + +## Source + +```basic +SUB foo +END SUB + +DECLARE SUB foo(a AS STRING) +``` + +## Compilation errors + +```plain +4:13: Cannot redefine foo +``` + +# Test: Sub declarations do not match + +## Source + +```basic +DECLARE SUB foo +DECLARE SUB foo(a as STRING) +``` + +## Compilation errors + +```plain +2:13: Cannot redefine foo +``` + +# Test: Sub redeclared as function + +## Source + +```basic +DECLARE SUB foo +DECLARE FUNCTION foo +``` + +## Compilation errors + +```plain +2:18: Cannot redefine foo% +``` From f02c5002f12156612babea4ef5180995d8b9f7db Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 07:29:11 +0100 Subject: [PATCH 41/53] core2: Compile callables right where they are defined Instead of "buffering" all callables to compile them at the end of the image, emit code as we encounter them. This makes it easier to support the REPL scenario and should help fix a long-standing deficiency where it was not possible to define callables and use them as separate lines in the REPL. --- core2/src/compiler/codegen.rs | 18 +- core2/src/compiler/syms.rs | 5 + core2/src/compiler/top.rs | 146 +++--- core2/src/image.rs | 19 +- core2/tests/test_arrays.md | 111 ++-- core2/tests/test_case_insensitivity.md | 75 +-- core2/tests/test_functions.md | 667 ++++++++++++++----------- core2/tests/test_globals.md | 145 +++--- core2/tests/test_subs.md | 356 +++++++------ 9 files changed, 838 insertions(+), 704 deletions(-) diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 72962304..4a224a5c 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -65,8 +65,8 @@ pub(super) struct Codegen { /// Map of label names to their target addresses. labels: HashMap, - /// Map of user callable names to their target addresses. - user_callables_addresses: HashMap, + /// Map of user callable names to their target start and end addresses. + user_callables_addresses: HashMap, /// Map of built-in callable names to their return types and assigned upcall IDs. upcalls: HashMapWithIds, u16>, @@ -173,8 +173,8 @@ impl Codegen { } /// Records the location of a user-defined callable. - pub(super) fn define_user_callable(&mut self, key: SymbolKey, address: Address) { - self.user_callables_addresses.insert(key, address); + pub(super) fn define_user_callable(&mut self, key: SymbolKey, start: Address, end: Address) { + self.user_callables_addresses.insert(key, (start, end)); } /// Records the location of a label. Returns false on failure (if the label already existed). @@ -196,7 +196,8 @@ impl Codegen { let pos = self.instrs[addr].linecol; let instr = match fixup { Fixup::Call(reg, key) => { - let target = self.user_callables_addresses.get(&key).expect("Must be present"); + let (target, _end) = + self.user_callables_addresses.get(&key).expect("Must be present"); bytecode::make_call(reg, Self::make_target(*target, pos)?) } Fixup::Enter(nargs) => bytecode::make_enter(nargs), @@ -256,8 +257,11 @@ impl Codegen { self.apply_fixups()?; let mut callables = HashMap::default(); - for (key, pc) in self.user_callables_addresses { - let previous = callables.insert(pc, key); + for (key, (start_pc, end_pc)) in self.user_callables_addresses { + let previous = callables.insert(start_pc, (key.clone(), true)); + debug_assert!(previous.is_none(), "An address can only start one callable"); + + let previous = callables.insert(end_pc, (key, false)); debug_assert!(previous.is_none(), "An address can only start one callable"); } diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index e42ea179..0469c99d 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -249,6 +249,11 @@ impl<'a> LocalSymtable<'a> { } } + /// Obtains mutable access to the parent global symtable. + pub(crate) fn global(&mut self) -> &mut GlobalSymtable { + self.symtable + } + /// Consumes the local scope and returns the number of local variables defined, which includes /// the locals themselves and any temporaries used by the scope. pub(crate) fn leave_scope(self) -> Result { diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index ad86523c..1b50014b 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -34,7 +34,7 @@ use crate::reader::LineCol; use crate::{Callable, CallableMetadataBuilder, parser}; use std::borrow::Cow; use std::cmp::max; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::io; use std::iter::Iterator; use std::rc::Rc; @@ -58,9 +58,6 @@ struct Context { /// The code generator accumulating bytecode instructions. codegen: Codegen, - /// Collection of user-defined callable definitions to be compiled after the main scope. - user_callables: Vec, - /// Collection of `DATA` values captured while compiling all statements. data: Vec>, @@ -73,6 +70,9 @@ struct Context { /// Kind of the callable currently being compiled, if any. current_callable: Option, + /// Callables defined (not just declared) so far. + defined_callables: HashSet, + /// List of pending `EXIT FUNCTION` or `EXIT SUB` jumps in the current callable. callable_exit_jumps: Vec<(usize, LineCol)>, } @@ -781,14 +781,15 @@ fn compile_stmt( Statement::Callable(span) => { mark_start = false; declare_callable(symtable, &span.name, span.name_pos, &span.params)?; + + let key = SymbolKey::from(&span.name.name); // If declaration succeeds, we still have to check for callable redefinition. // This linear scan is not the most efficient, but it's fine for now. - for callable in &ctx.user_callables { - if span.name == callable.name { - return Err(Error::AlreadyDefined(span.name_pos, span.name)); - } + if ctx.defined_callables.contains(&key) { + return Err(Error::AlreadyDefined(span.name_pos, span.name)); } - ctx.user_callables.push(span); + compile_user_callable(ctx, symtable.global(), span)?; + ctx.defined_callables.insert(key); } Statement::Data(span) => { @@ -1058,73 +1059,76 @@ fn declare_callable( symtable.declare_user_callable(name, builder.build()).map_err(|e| Error::from_syms(e, name_pos)) } -/// Compiles all user-defined callables that have been captured in `ctx`. -fn compile_user_callables(ctx: &mut Context, symtable: &mut GlobalSymtable) -> Result<()> { - let user_callables: Vec = ctx.user_callables.drain(..).collect(); - debug_assert!(ctx.user_callables.is_empty()); +/// Compiles a single user-defined callable. +fn compile_user_callable( + ctx: &mut Context, + symtable: &mut GlobalSymtable, + callable: CallableSpan, +) -> Result<()> { + if ctx.current_callable.is_some() { + return Err(Error::CannotNestUserCallables(callable.name_pos)); + } - for callable in user_callables { - if ctx.current_callable.is_some() { - return Err(Error::CannotNestUserCallables(callable.name_pos)); - } + let skip_pc = ctx.codegen.emit(bytecode::make_nop(), callable.name_pos); - let start_pc = ctx.codegen.next_pc(); + let start_pc = ctx.codegen.next_pc(); - let key_pos = callable.name_pos; - let key = SymbolKey::from(callable.name.name); - ctx.current_callable = Some(if callable.name.ref_type.is_some() { - CallableKind::Function - } else { - CallableKind::Sub - }); - debug_assert!(ctx.callable_exit_jumps.is_empty()); - - let mut symtable = symtable.enter_scope(); - - // The call protocol expects the return value to be in the first local variable - // so allocate it early, and then all arguments follow in order from left to right. - if let Some(vtype) = callable.name.ref_type { - let ret_reg = symtable - .put_local(key.clone(), SymbolPrototype::Scalar(vtype)) - .map_err(|e| Error::from_syms(e, key_pos))?; - - // Set the default value of the function result. We could instead try to do this - // at runtime by clearning the return register... but the problem is that we need - // to handle non-primitive types like strings and the runtime doesn't know the type - // of the result to properly allocate it. - let value = match vtype { - ExprType::Boolean | ExprType::Integer => 0, - ExprType::Double => { - ctx.codegen.get_constant(ConstantDatum::Double(0.0), key_pos)? - } - ExprType::Text => { - ctx.codegen.get_constant(ConstantDatum::Text(String::new()), key_pos)? - } - }; - ctx.codegen.emit(bytecode::make_load_integer(ret_reg, value), key_pos); - } - for param in callable.params { - let key = SymbolKey::from(param.name); - symtable - .put_local( - key.clone(), - SymbolPrototype::Scalar(param.ref_type.unwrap_or(ExprType::Integer)), - ) - .map_err(|e| Error::from_syms(e, key_pos))?; - } + let key_pos = callable.name_pos; + let key = SymbolKey::from(callable.name.name); + ctx.current_callable = Some(if callable.name.ref_type.is_some() { + CallableKind::Function + } else { + CallableKind::Sub + }); + debug_assert!(ctx.callable_exit_jumps.is_empty()); + + let mut symtable = symtable.enter_scope(); + + // The call protocol expects the return value to be in the first local variable + // so allocate it early, and then all arguments follow in order from left to right. + if let Some(vtype) = callable.name.ref_type { + let ret_reg = symtable + .put_local(key.clone(), SymbolPrototype::Scalar(vtype)) + .map_err(|e| Error::from_syms(e, key_pos))?; + + // Set the default value of the function result. We could instead try to do this + // at runtime by clearning the return register... but the problem is that we need + // to handle non-primitive types like strings and the runtime doesn't know the type + // of the result to properly allocate it. + let value = match vtype { + ExprType::Boolean | ExprType::Integer => 0, + ExprType::Double => ctx.codegen.get_constant(ConstantDatum::Double(0.0), key_pos)?, + ExprType::Text => { + ctx.codegen.get_constant(ConstantDatum::Text(String::new()), key_pos)? + } + }; + ctx.codegen.emit(bytecode::make_load_integer(ret_reg, value), key_pos); + } + for param in callable.params { + let key = SymbolKey::from(param.name); + symtable + .put_local( + key.clone(), + SymbolPrototype::Scalar(param.ref_type.unwrap_or(ExprType::Integer)), + ) + .map_err(|e| Error::from_syms(e, key_pos))?; + } - compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; + compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; - let return_addr = ctx.codegen.next_pc(); - ctx.codegen.emit(bytecode::make_return(), callable.end_pos); - for (addr, pos) in ctx.callable_exit_jumps.drain(..) { - let target = - u16::try_from(return_addr).map_err(|_| Error::TargetTooFar(pos, return_addr))?; - ctx.codegen.patch(addr, bytecode::make_jump(target)); - } - ctx.current_callable = None; - ctx.codegen.define_user_callable(key, start_pc); + let return_addr = ctx.codegen.emit(bytecode::make_return(), callable.end_pos); + for (addr, pos) in ctx.callable_exit_jumps.drain(..) { + let target = + u16::try_from(return_addr).map_err(|_| Error::TargetTooFar(pos, return_addr))?; + ctx.codegen.patch(addr, bytecode::make_jump(target)); } + ctx.current_callable = None; + let skip_addr = ctx.codegen.next_pc(); + ctx.codegen.define_user_callable(key, start_pc, skip_addr); + + let target = + u16::try_from(skip_addr).map_err(|_| Error::TargetTooFar(callable.end_pos, skip_addr))?; + ctx.codegen.patch(skip_pc, bytecode::make_jump(target)); Ok(()) } @@ -1273,8 +1277,6 @@ pub fn compile( compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input))?; ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); - compile_user_callables(&mut ctx, &mut symtable)?; - let global_vars = symtable .iter_globals() .map(|(key, proto, reg)| { diff --git a/core2/src/image.rs b/core2/src/image.rs index 558ef04f..66e9d078 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -126,9 +126,9 @@ pub struct DebugInfo { /// Per-instruction metadata, one entry per instruction in the image's code. pub(crate) instrs: Vec, - /// Maps instruction addresses to the names of user-defined callables that start at those - /// addresses. - pub(crate) callables: HashMap, + /// Maps instruction addresses to the names of user-defined callables that start or end + /// at those addresses. If the boolean is true, the position is a callable start. + pub(crate) callables: HashMap, /// Maps global variable names to their register assignments and type information. /// @@ -205,9 +205,14 @@ impl Image { self.code.iter().copied().enumerate().zip(self.debug_info.instrs.iter()) { let pos = meta.linecol; - if let Some(key) = self.debug_info.callables.get(&i) { - lines.push("".to_owned()); - lines.push(format!("-- {} ", key)); + if let Some((key, is_start)) = self.debug_info.callables.get(&i) { + if *is_start { + lines.push("".to_owned()); + lines.push(format!("-- {} (BEGIN)", key)); + } else { + lines.push(format!("-- {} (END)", key)); + lines.push("".to_owned()); + } } let mut line = format!("{:04}: {}", i, format_instr(instr)); @@ -221,7 +226,7 @@ impl Image { Opcode::Call => { let (_reg, target) = bytecode::parse_call(instr); let target = usize::from(target); - let name = self + let (name, _is_start) = self .debug_info .callables .get(&target) diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index e5001b0f..6a0f42b7 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -233,12 +233,13 @@ OUT s(0), s(1), s(2) ## Source ```basic +DIM SHARED a(2) AS INTEGER + SUB fill_array a(0) = 100 a(1) = 200 END SUB -DIM SHARED a(2) AS INTEGER fill_array OUT a(0), a(1) ``` @@ -247,27 +248,30 @@ OUT a(0), a(1) ```asm 0000: ENTER 5 # 0:0 -0001: LOADI R64, 2 # 6:14 -0002: ALLOCA R0, [1]%, R64 # 6:12 -0003: CALL R64, 12 # 7:1, FILL_ARRAY -0004: LOADI R66, 0 # 8:7 -0005: LOADA R65, R0, R66 # 8:5 -0006: LOADI R64, 290 # 8:5 -0007: LOADI R68, 1 # 8:13 -0008: LOADA R67, R0, R68 # 8:11 -0009: LOADI R66, 258 # 8:11 -0010: UPCALL 0, R64 # 8:1, OUT -0011: EOF # 0:0 - --- FILL_ARRAY -0012: ENTER 2 # 0:0 -0013: LOADI R64, 100 # 2:12 -0014: LOADI R65, 0 # 2:7 -0015: STOREA R0, R64, R65 # 2:5 -0016: LOADI R64, 200 # 3:12 -0017: LOADI R65, 1 # 3:7 -0018: STOREA R0, R64, R65 # 3:5 -0019: RETURN # 4:1 +0001: LOADI R64, 2 # 1:14 +0002: ALLOCA R0, [1]%, R64 # 1:12 +0003: JUMP 12 # 3:5 + +-- FILL_ARRAY (BEGIN) +0004: ENTER 2 # 0:0 +0005: LOADI R64, 100 # 4:12 +0006: LOADI R65, 0 # 4:7 +0007: STOREA R0, R64, R65 # 4:5 +0008: LOADI R64, 200 # 5:12 +0009: LOADI R65, 1 # 5:7 +0010: STOREA R0, R64, R65 # 5:5 +0011: RETURN # 6:1 +-- FILL_ARRAY (END) + +0012: CALL R64, 4 # 8:1, FILL_ARRAY +0013: LOADI R66, 0 # 9:7 +0014: LOADA R65, R0, R66 # 9:5 +0015: LOADI R64, 290 # 9:5 +0016: LOADI R68, 1 # 9:13 +0017: LOADA R67, R0, R68 # 9:11 +0018: LOADI R66, 258 # 9:11 +0019: UPCALL 0, R64 # 9:1, OUT +0020: EOF # 0:0 ``` ## Output @@ -720,36 +724,39 @@ OUT sum(0) ```asm 0000: ENTER 4 # 0:0 -0001: LOADI R67, 0 # 9:9 -0002: CALL R66, 7 # 9:5, SUM -0003: MOVE R65, R66 # 9:5 -0004: LOADI R64, 258 # 9:5 -0005: UPCALL 0, R64 # 9:1, OUT -0006: EOF # 0:0 - --- SUM -0007: LOADI R64, 0 # 1:10 -0008: ENTER 5 # 0:0 -0009: LOADI R67, 3 # 2:11 -0010: ALLOCA R66, [1]%, R67 # 2:9 -0011: LOADI R67, 10 # 3:12 -0012: LOADI R68, 0 # 3:7 -0013: STOREA R66, R67, R68 # 3:5 -0014: LOADI R67, 20 # 4:12 -0015: LOADI R68, 1 # 4:7 -0016: STOREA R66, R67, R68 # 4:5 -0017: LOADI R67, 30 # 5:12 -0018: LOADI R68, 2 # 5:7 -0019: STOREA R66, R67, R68 # 5:5 -0020: LOADI R67, 0 # 6:13 -0021: LOADA R64, R66, R67 # 6:11 -0022: LOADI R68, 1 # 6:20 -0023: LOADA R67, R66, R68 # 6:18 -0024: ADDI R64, R64, R67 # 6:16 -0025: LOADI R68, 2 # 6:27 -0026: LOADA R67, R66, R68 # 6:25 -0027: ADDI R64, R64, R67 # 6:23 -0028: RETURN # 7:1 +0001: JUMP 24 # 1:10 + +-- SUM (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 5 # 0:0 +0004: LOADI R67, 3 # 2:11 +0005: ALLOCA R66, [1]%, R67 # 2:9 +0006: LOADI R67, 10 # 3:12 +0007: LOADI R68, 0 # 3:7 +0008: STOREA R66, R67, R68 # 3:5 +0009: LOADI R67, 20 # 4:12 +0010: LOADI R68, 1 # 4:7 +0011: STOREA R66, R67, R68 # 4:5 +0012: LOADI R67, 30 # 5:12 +0013: LOADI R68, 2 # 5:7 +0014: STOREA R66, R67, R68 # 5:5 +0015: LOADI R67, 0 # 6:13 +0016: LOADA R64, R66, R67 # 6:11 +0017: LOADI R68, 1 # 6:20 +0018: LOADA R67, R66, R68 # 6:18 +0019: ADDI R64, R64, R67 # 6:16 +0020: LOADI R68, 2 # 6:27 +0021: LOADA R67, R66, R68 # 6:25 +0022: ADDI R64, R64, R67 # 6:23 +0023: RETURN # 7:1 +-- SUM (END) + +0024: LOADI R67, 0 # 9:9 +0025: CALL R66, 2 # 9:5, SUM +0026: MOVE R65, R66 # 9:5 +0027: LOADI R64, 258 # 9:5 +0028: UPCALL 0, R64 # 9:1, OUT +0029: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_case_insensitivity.md b/core2/tests/test_case_insensitivity.md index 616a335d..11a06dea 100644 --- a/core2/tests/test_case_insensitivity.md +++ b/core2/tests/test_case_insensitivity.md @@ -148,16 +148,19 @@ OUT FOO ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 5 # 5:5, FOO -0002: LOADI R64, 258 # 5:5 -0003: UPCALL 0, R64 # 5:1, OUT -0004: EOF # 0:0 - --- FOO -0005: LOADI R64, 0 # 1:10 -0006: ENTER 1 # 0:0 -0007: LOADI R64, 42 # 2:11 -0008: RETURN # 3:1 +0001: JUMP 6 # 1:10 + +-- FOO (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 1 # 0:0 +0004: LOADI R64, 42 # 2:11 +0005: RETURN # 3:1 +-- FOO (END) + +0006: CALL R65, 2 # 5:5, FOO +0007: LOADI R64, 258 # 5:5 +0008: UPCALL 0, R64 # 5:1, OUT +0009: EOF # 0:0 ``` ## Output @@ -182,15 +185,18 @@ FOO ```asm 0000: ENTER 0 # 0:0 -0001: CALL R64, 3 # 5:1, FOO -0002: EOF # 0:0 - --- FOO -0003: ENTER 2 # 0:0 -0004: LOADI R65, 0 # 2:9 -0005: LOADI R64, 259 # 2:9 -0006: UPCALL 0, R64 # 2:5, OUT -0007: RETURN # 3:1 +0001: JUMP 7 # 1:5 + +-- FOO (BEGIN) +0002: ENTER 2 # 0:0 +0003: LOADI R65, 0 # 2:9 +0004: LOADI R64, 259 # 2:9 +0005: UPCALL 0, R64 # 2:5, OUT +0006: RETURN # 3:1 +-- FOO (END) + +0007: CALL R64, 2 # 5:1, FOO +0008: EOF # 0:0 ``` ## Output @@ -278,20 +284,23 @@ OUT foo(5) ```asm 0000: ENTER 4 # 0:0 -0001: LOADI R67, 5 # 5:9 -0002: CALL R66, 7 # 5:5, FOO -0003: MOVE R65, R66 # 5:5 -0004: LOADI R64, 258 # 5:5 -0005: UPCALL 0, R64 # 5:1, OUT -0006: EOF # 0:0 - --- FOO -0007: LOADI R64, 0 # 1:10 -0008: ENTER 3 # 0:0 -0009: MOVE R64, R65 # 2:11 -0010: LOADI R66, 1 # 2:15 -0011: ADDI R64, R64, R66 # 2:13 -0012: RETURN # 3:1 +0001: JUMP 8 # 1:10 + +-- FOO (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 3 # 0:0 +0004: MOVE R64, R65 # 2:11 +0005: LOADI R66, 1 # 2:15 +0006: ADDI R64, R64, R66 # 2:13 +0007: RETURN # 3:1 +-- FOO (END) + +0008: LOADI R67, 5 # 5:9 +0009: CALL R66, 2 # 5:5, FOO +0010: MOVE R65, R66 # 5:5 +0011: LOADI R64, 258 # 5:5 +0012: UPCALL 0, R64 # 5:1, OUT +0013: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index cebe66ab..9802bcbf 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -14,16 +14,19 @@ OUT foo$ ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 5 # 5:5, FOO -0002: LOADI R64, 259 # 5:5 -0003: UPCALL 0, R64 # 5:1, OUT -0004: EOF # 0:0 +0001: JUMP 6 # 1:10 + +-- FOO (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 1 # 0:0 +0004: LOADI R64, 1 # 2:11 +0005: RETURN # 3:1 +-- FOO (END) --- FOO -0005: LOADI R64, 0 # 1:10 -0006: ENTER 1 # 0:0 -0007: LOADI R64, 1 # 2:11 -0008: RETURN # 3:1 +0006: CALL R65, 2 # 5:5, FOO +0007: LOADI R64, 259 # 5:5 +0008: UPCALL 0, R64 # 5:1, OUT +0009: EOF # 0:0 ``` ## Output @@ -73,34 +76,37 @@ OUT "After", a ```asm 0000: ENTER 5 # 0:0 0001: LOADI R64, 10 # 1:5 -0002: LOADI R66, 0 # 9:5 -0003: LOADI R65, 291 # 9:5 -0004: MOVE R68, R64 # 9:15 -0005: LOADI R67, 258 # 9:15 -0006: UPCALL 0, R65 # 9:1, OUT -0007: LOADI R66, 1 # 10:5 -0008: LOADI R65, 291 # 10:5 -0009: CALL R68, 18 # 10:21, FOO -0010: LOADI R67, 258 # 10:21 -0011: UPCALL 0, R65 # 10:1, OUT -0012: LOADI R66, 2 # 11:5 -0013: LOADI R65, 291 # 11:5 -0014: MOVE R68, R64 # 11:14 -0015: LOADI R67, 258 # 11:14 -0016: UPCALL 0, R65 # 11:1, OUT -0017: EOF # 0:0 - --- FOO -0018: LOADI R64, 0 # 3:10 -0019: ENTER 6 # 0:0 -0020: LOADI R65, 20 # 4:9 -0021: LOADI R67, 3 # 5:9 -0022: LOADI R66, 291 # 5:9 -0023: MOVE R69, R65 # 5:19 -0024: LOADI R68, 258 # 5:19 -0025: UPCALL 0, R66 # 5:5, OUT -0026: LOADI R64, 30 # 6:11 -0027: RETURN # 7:1 +0002: JUMP 13 # 3:10 + +-- FOO (BEGIN) +0003: LOADI R64, 0 # 3:10 +0004: ENTER 6 # 0:0 +0005: LOADI R65, 20 # 4:9 +0006: LOADI R67, 0 # 5:9 +0007: LOADI R66, 291 # 5:9 +0008: MOVE R69, R65 # 5:19 +0009: LOADI R68, 258 # 5:19 +0010: UPCALL 0, R66 # 5:5, OUT +0011: LOADI R64, 30 # 6:11 +0012: RETURN # 7:1 +-- FOO (END) + +0013: LOADI R66, 1 # 9:5 +0014: LOADI R65, 291 # 9:5 +0015: MOVE R68, R64 # 9:15 +0016: LOADI R67, 258 # 9:15 +0017: UPCALL 0, R65 # 9:1, OUT +0018: LOADI R66, 2 # 10:5 +0019: LOADI R65, 291 # 10:5 +0020: CALL R68, 3 # 10:21, FOO +0021: LOADI R67, 258 # 10:21 +0022: UPCALL 0, R65 # 10:1, OUT +0023: LOADI R66, 3 # 11:5 +0024: LOADI R65, 291 # 11:5 +0025: MOVE R68, R64 # 11:14 +0026: LOADI R67, 258 # 11:14 +0027: UPCALL 0, R65 # 11:1, OUT +0028: EOF # 0:0 ``` ## Output @@ -133,25 +139,31 @@ OUT second ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 12 # 10:5, SECOND -0002: LOADI R64, 258 # 10:5 -0003: UPCALL 0, R64 # 10:1, OUT -0004: EOF # 0:0 - --- FIRST -0005: LOADI R64, 0 # 1:10 -0006: ENTER 3 # 0:0 -0007: LOADI R66, 0 # 2:9 -0008: LOADI R65, 259 # 2:9 -0009: UPCALL 0, R65 # 2:5, OUT -0010: LOADI R64, 123 # 3:13 -0011: RETURN # 4:1 +0001: JUMP 9 # 1:10 + +-- FIRST (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 3 # 0:0 +0004: LOADI R66, 0 # 2:9 +0005: LOADI R65, 259 # 2:9 +0006: UPCALL 0, R65 # 2:5, OUT +0007: LOADI R64, 123 # 3:13 +0008: RETURN # 4:1 +-- FIRST (END) + +0009: JUMP 14 # 6:10 + +-- SECOND (BEGIN) +0010: LOADI R64, 0 # 6:10 +0011: ENTER 1 # 0:0 +0012: CALL R64, 2 # 7:14, FIRST +0013: RETURN # 8:1 +-- SECOND (END) --- SECOND -0012: LOADI R64, 0 # 6:10 -0013: ENTER 1 # 0:0 -0014: CALL R64, 5 # 7:14, FIRST -0015: RETURN # 8:1 +0014: CALL R65, 10 # 10:5, SECOND +0015: LOADI R64, 258 # 10:5 +0016: UPCALL 0, R64 # 10:1, OUT +0017: EOF # 0:0 ``` ## Output @@ -189,42 +201,54 @@ OUT do_call ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 14 # 17:5, DO_CALL -0002: LOADI R64, 258 # 17:5 -0003: UPCALL 0, R64 # 17:1, OUT -0004: EOF # 0:0 +0001: JUMP 5 # 1:10 --- DEFAULT_DOUBLE -0005: LOADI R64, 0 # 1:10 -0006: ENTER 1 # 0:0 -0007: RETURN # 2:1 +-- DEFAULT_DOUBLE (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 1 # 0:0 +0004: RETURN # 2:1 +-- DEFAULT_DOUBLE (END) --- DEFAULT_INTEGER -0008: LOADI R64, 0 # 4:10 -0009: ENTER 1 # 0:0 -0010: RETURN # 5:1 +0005: JUMP 9 # 4:10 --- DEFAULT_STRING -0011: LOADI R64, 1 # 7:10 -0012: ENTER 1 # 0:0 -0013: RETURN # 8:1 +-- DEFAULT_INTEGER (BEGIN) +0006: LOADI R64, 0 # 4:10 +0007: ENTER 1 # 0:0 +0008: RETURN # 5:1 +-- DEFAULT_INTEGER (END) + +0009: JUMP 13 # 7:10 --- DO_CALL +-- DEFAULT_STRING (BEGIN) +0010: LOADI R64, 1 # 7:10 +0011: ENTER 1 # 0:0 +0012: RETURN # 8:1 +-- DEFAULT_STRING (END) + +0013: JUMP 29 # 10:10 + +-- DO_CALL (BEGIN) 0014: LOADI R64, 0 # 10:10 0015: ENTER 3 # 0:0 0016: LOADI R66, 300 # 11:9 0017: LOADI R65, 258 # 11:9 0018: UPCALL 0, R65 # 11:5, OUT -0019: CALL R66, 5 # 12:9, DEFAULT_DOUBLE +0019: CALL R66, 2 # 12:9, DEFAULT_DOUBLE 0020: LOADI R65, 257 # 12:9 0021: UPCALL 0, R65 # 12:5, OUT -0022: CALL R66, 8 # 13:9, DEFAULT_INTEGER +0022: CALL R66, 6 # 13:9, DEFAULT_INTEGER 0023: LOADI R65, 258 # 13:9 0024: UPCALL 0, R65 # 13:5, OUT -0025: CALL R66, 11 # 14:9, DEFAULT_STRING +0025: CALL R66, 10 # 14:9, DEFAULT_STRING 0026: LOADI R65, 259 # 14:9 0027: UPCALL 0, R65 # 14:5, OUT 0028: RETURN # 15:1 +-- DO_CALL (END) + +0029: CALL R65, 14 # 17:5, DO_CALL +0030: LOADI R64, 258 # 17:5 +0031: UPCALL 0, R64 # 17:1, OUT +0032: EOF # 0:0 ``` ## Output @@ -266,53 +290,59 @@ OUT "After modify_1", var ```asm 0000: ENTER 5 # 0:0 -0001: LOADI R64, 100 # 15:7 -0002: LOADI R66, 0 # 16:5 -0003: LOADI R65, 291 # 16:5 -0004: MOVE R68, R64 # 16:24 -0005: LOADI R67, 258 # 16:24 -0006: UPCALL 0, R65 # 16:1, OUT -0007: CALL R66, 26 # 17:5, MODIFY_1 -0008: LOADI R65, 258 # 17:5 -0009: UPCALL 0, R65 # 17:1, OUT -0010: LOADI R66, 1 # 18:5 -0011: LOADI R65, 291 # 18:5 -0012: MOVE R68, R64 # 18:23 -0013: LOADI R67, 258 # 18:23 -0014: UPCALL 0, R65 # 18:1, OUT -0015: EOF # 0:0 - --- MODIFY_2 -0016: LOADI R64, 0 # 1:10 -0017: ENTER 6 # 0:0 -0018: LOADI R65, 300 # 2:11 -0019: LOADI R64, 2000 # 3:16 -0020: LOADI R67, 2 # 4:9 -0021: LOADI R66, 291 # 4:9 -0022: MOVE R69, R65 # 4:28 -0023: LOADI R68, 258 # 4:28 -0024: UPCALL 0, R66 # 4:5, OUT -0025: RETURN # 5:1 - --- MODIFY_1 -0026: LOADI R64, 0 # 7:10 -0027: ENTER 6 # 0:0 -0028: LOADI R65, 200 # 8:11 -0029: LOADI R67, 3 # 9:9 -0030: LOADI R66, 291 # 9:9 -0031: MOVE R69, R65 # 9:28 -0032: LOADI R68, 258 # 9:28 -0033: UPCALL 0, R66 # 9:5, OUT -0034: CALL R67, 16 # 10:9, MODIFY_2 -0035: LOADI R66, 258 # 10:9 -0036: UPCALL 0, R66 # 10:5, OUT -0037: LOADI R67, 4 # 11:9 -0038: LOADI R66, 291 # 11:9 -0039: MOVE R69, R65 # 11:27 -0040: LOADI R68, 258 # 11:27 -0041: UPCALL 0, R66 # 11:5, OUT -0042: LOADI R64, 1000 # 12:16 -0043: RETURN # 13:1 +0001: JUMP 12 # 1:10 + +-- MODIFY_2 (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 6 # 0:0 +0004: LOADI R65, 300 # 2:11 +0005: LOADI R64, 2000 # 3:16 +0006: LOADI R67, 0 # 4:9 +0007: LOADI R66, 291 # 4:9 +0008: MOVE R69, R65 # 4:28 +0009: LOADI R68, 258 # 4:28 +0010: UPCALL 0, R66 # 4:5, OUT +0011: RETURN # 5:1 +-- MODIFY_2 (END) + +0012: JUMP 31 # 7:10 + +-- MODIFY_1 (BEGIN) +0013: LOADI R64, 0 # 7:10 +0014: ENTER 6 # 0:0 +0015: LOADI R65, 200 # 8:11 +0016: LOADI R67, 1 # 9:9 +0017: LOADI R66, 291 # 9:9 +0018: MOVE R69, R65 # 9:28 +0019: LOADI R68, 258 # 9:28 +0020: UPCALL 0, R66 # 9:5, OUT +0021: CALL R67, 2 # 10:9, MODIFY_2 +0022: LOADI R66, 258 # 10:9 +0023: UPCALL 0, R66 # 10:5, OUT +0024: LOADI R67, 2 # 11:9 +0025: LOADI R66, 291 # 11:9 +0026: MOVE R69, R65 # 11:27 +0027: LOADI R68, 258 # 11:27 +0028: UPCALL 0, R66 # 11:5, OUT +0029: LOADI R64, 1000 # 12:16 +0030: RETURN # 13:1 +-- MODIFY_1 (END) + +0031: LOADI R64, 100 # 15:7 +0032: LOADI R66, 3 # 16:5 +0033: LOADI R65, 291 # 16:5 +0034: MOVE R68, R64 # 16:24 +0035: LOADI R67, 258 # 16:24 +0036: UPCALL 0, R65 # 16:1, OUT +0037: CALL R66, 13 # 17:5, MODIFY_1 +0038: LOADI R65, 258 # 17:5 +0039: UPCALL 0, R65 # 17:1, OUT +0040: LOADI R66, 4 # 18:5 +0041: LOADI R65, 291 # 18:5 +0042: MOVE R68, R64 # 18:23 +0043: LOADI R67, 258 # 18:23 +0044: UPCALL 0, R65 # 18:1, OUT +0045: EOF # 0:0 ``` ## Output @@ -362,26 +392,29 @@ OUT add(3, 5) + add(10, 20) ```asm 0000: ENTER 6 # 0:0 -0001: LOADI R67, 3 # 5:9 -0002: LOADI R68, 5 # 5:12 -0003: CALL R66, 13 # 5:5, ADD -0004: MOVE R65, R66 # 5:5 -0005: LOADI R68, 10 # 5:21 -0006: LOADI R69, 20 # 5:25 -0007: CALL R67, 13 # 5:17, ADD -0008: MOVE R66, R67 # 5:17 -0009: ADDI R65, R65, R66 # 5:15 -0010: LOADI R64, 258 # 5:5 -0011: UPCALL 0, R64 # 5:1, OUT -0012: EOF # 0:0 - --- ADD -0013: LOADI R64, 0 # 1:10 -0014: ENTER 4 # 0:0 -0015: MOVE R64, R65 # 2:11 -0016: MOVE R67, R66 # 2:15 -0017: ADDI R64, R64, R67 # 2:13 -0018: RETURN # 3:1 +0001: JUMP 8 # 1:10 + +-- ADD (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 4 # 0:0 +0004: MOVE R64, R65 # 2:11 +0005: MOVE R67, R66 # 2:15 +0006: ADDI R64, R64, R67 # 2:13 +0007: RETURN # 3:1 +-- ADD (END) + +0008: LOADI R67, 3 # 5:9 +0009: LOADI R68, 5 # 5:12 +0010: CALL R66, 2 # 5:5, ADD +0011: MOVE R65, R66 # 5:5 +0012: LOADI R68, 10 # 5:21 +0013: LOADI R69, 20 # 5:25 +0014: CALL R67, 2 # 5:17, ADD +0015: MOVE R66, R67 # 5:17 +0016: ADDI R65, R65, R66 # 5:15 +0017: LOADI R64, 258 # 5:5 +0018: UPCALL 0, R64 # 5:1, OUT +0019: EOF # 0:0 ``` ## Output @@ -408,22 +441,25 @@ OUT ret ```asm 0000: ENTER 2 # 0:0 -0001: LOADI R0, 0 # 5:12 -0002: LOADI R65, 3 # 6:11 -0003: CALL R64, 9 # 6:7, FOO -0004: MOVE R0, R64 # 6:7 -0005: MOVE R65, R0 # 7:5 -0006: LOADI R64, 258 # 7:5 -0007: UPCALL 0, R64 # 7:1, OUT -0008: EOF # 0:0 - --- FOO -0009: LOADI R64, 0 # 1:10 -0010: ENTER 3 # 0:0 -0011: LOADI R64, 42 # 2:11 -0012: MOVE R66, R65 # 2:16 -0013: ADDI R64, R64, R66 # 2:14 -0014: RETURN # 3:1 +0001: JUMP 8 # 1:10 + +-- FOO (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 3 # 0:0 +0004: LOADI R64, 42 # 2:11 +0005: MOVE R66, R65 # 2:16 +0006: ADDI R64, R64, R66 # 2:14 +0007: RETURN # 3:1 +-- FOO (END) + +0008: LOADI R0, 0 # 5:12 +0009: LOADI R65, 3 # 6:11 +0010: CALL R64, 2 # 6:7, FOO +0011: MOVE R0, R64 # 6:7 +0012: MOVE R65, R0 # 7:5 +0013: LOADI R64, 258 # 7:5 +0014: UPCALL 0, R64 # 7:1, OUT +0015: EOF # 0:0 ``` ## Output @@ -458,37 +494,43 @@ OUT s ```asm 0000: ENTER 6 # 0:0 -0001: LOADI R64, 5 # 9:5 -0002: MOVE R68, R64 # 10:20 -0003: CALL R67, 20 # 10:5, CHANGE_INTEGER -0004: MOVE R66, R67 # 10:5 -0005: LOADI R65, 258 # 10:5 -0006: UPCALL 0, R65 # 10:1, OUT -0007: MOVE R66, R64 # 11:5 -0008: LOADI R65, 258 # 11:5 -0009: UPCALL 0, R65 # 11:1, OUT -0010: LOADI R65, 0 # 13:5 -0011: MOVE R69, R65 # 14:19 -0012: CALL R68, 24 # 14:5, CHANGE_STRING -0013: MOVE R67, R68 # 14:5 -0014: LOADI R66, 258 # 14:5 -0015: UPCALL 0, R66 # 14:1, OUT -0016: MOVE R67, R65 # 15:5 -0017: LOADI R66, 259 # 15:5 -0018: UPCALL 0, R66 # 15:1, OUT -0019: EOF # 0:0 - --- CHANGE_INTEGER -0020: LOADI R64, 0 # 1:10 -0021: ENTER 2 # 0:0 -0022: LOADI R65, 3 # 2:9 -0023: RETURN # 3:1 - --- CHANGE_STRING -0024: LOADI R64, 0 # 5:10 -0025: ENTER 2 # 0:0 -0026: LOADI R65, 1 # 6:9 -0027: RETURN # 7:1 +0001: JUMP 6 # 1:10 + +-- CHANGE_INTEGER (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 2 # 0:0 +0004: LOADI R65, 3 # 2:9 +0005: RETURN # 3:1 +-- CHANGE_INTEGER (END) + +0006: JUMP 11 # 5:10 + +-- CHANGE_STRING (BEGIN) +0007: LOADI R64, 0 # 5:10 +0008: ENTER 2 # 0:0 +0009: LOADI R65, 0 # 6:9 +0010: RETURN # 7:1 +-- CHANGE_STRING (END) + +0011: LOADI R64, 5 # 9:5 +0012: MOVE R68, R64 # 10:20 +0013: CALL R67, 2 # 10:5, CHANGE_INTEGER +0014: MOVE R66, R67 # 10:5 +0015: LOADI R65, 258 # 10:5 +0016: UPCALL 0, R65 # 10:1, OUT +0017: MOVE R66, R64 # 11:5 +0018: LOADI R65, 258 # 11:5 +0019: UPCALL 0, R65 # 11:1, OUT +0020: LOADI R65, 1 # 13:5 +0021: MOVE R69, R65 # 14:19 +0022: CALL R68, 7 # 14:5, CHANGE_STRING +0023: MOVE R67, R68 # 14:5 +0024: LOADI R66, 258 # 14:5 +0025: UPCALL 0, R66 # 14:1, OUT +0026: MOVE R67, R65 # 15:5 +0027: LOADI R66, 259 # 15:5 +0028: UPCALL 0, R66 # 15:1, OUT +0029: EOF # 0:0 ``` ## Output @@ -835,33 +877,36 @@ NEXT ```asm 0000: ENTER 5 # 0:0 -0001: LOADI R64, 0 # 7:9 -0002: MOVE R65, R64 # 7:5 -0003: LOADI R66, 5 # 7:14 -0004: CMPLEI R65, R65, R66 # 7:11 -0005: JMPF R65, 15 # 7:5 -0006: MOVE R68, R64 # 8:20 -0007: CALL R67, 16 # 8:9, MAYBE_EXIT -0008: MOVE R66, R67 # 8:9 -0009: LOADI R65, 258 # 8:9 -0010: UPCALL 0, R65 # 8:5, OUT -0011: MOVE R64, R64 # 7:5 -0012: LOADI R65, 1 # 7:15 -0013: ADDI R64, R64, R65 # 7:11 -0014: JUMP 2 # 7:5 -0015: EOF # 0:0 - --- MAYBE_EXIT -0016: LOADI R64, 0 # 1:10 -0017: ENTER 4 # 0:0 -0018: LOADI R64, 1 # 2:18 -0019: MOVE R66, R65 # 3:8 -0020: LOADI R67, 2 # 3:12 -0021: CMPGTI R66, R66, R67 # 3:10 -0022: JMPF R66, 24 # 3:8 -0023: JUMP 25 # 3:19 -0024: LOADI R64, 2 # 4:18 -0025: RETURN # 5:1 +0001: JUMP 12 # 1:10 + +-- MAYBE_EXIT (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 4 # 0:0 +0004: LOADI R64, 1 # 2:18 +0005: MOVE R66, R65 # 3:8 +0006: LOADI R67, 2 # 3:12 +0007: CMPGTI R66, R66, R67 # 3:10 +0008: JMPF R66, 10 # 3:8 +0009: JUMP 11 # 3:19 +0010: LOADI R64, 2 # 4:18 +0011: RETURN # 5:1 +-- MAYBE_EXIT (END) + +0012: LOADI R64, 0 # 7:9 +0013: MOVE R65, R64 # 7:5 +0014: LOADI R66, 5 # 7:14 +0015: CMPLEI R65, R65, R66 # 7:11 +0016: JMPF R65, 26 # 7:5 +0017: MOVE R68, R64 # 8:20 +0018: CALL R67, 2 # 8:9, MAYBE_EXIT +0019: MOVE R66, R67 # 8:9 +0020: LOADI R65, 258 # 8:9 +0021: UPCALL 0, R65 # 8:5, OUT +0022: MOVE R64, R64 # 7:5 +0023: LOADI R65, 1 # 7:15 +0024: ADDI R64, R64, R65 # 7:11 +0025: JUMP 13 # 7:5 +0026: EOF # 0:0 ``` ## Output @@ -925,37 +970,40 @@ OUT calls; factorial(5) ```asm 0000: ENTER 6 # 0:0 0001: LOADI R0, 0 # 1:12 -0002: MOVE R65, R0 # 6:5 -0003: LOADI R64, 274 # 6:5 -0004: LOADI R69, 5 # 6:22 -0005: CALL R68, 10 # 6:12, FACTORIAL -0006: MOVE R67, R68 # 6:12 -0007: LOADI R66, 258 # 6:12 -0008: UPCALL 0, R64 # 6:1, OUT -0009: EOF # 0:0 - --- FACTORIAL -0010: LOADI R64, 0 # 2:10 -0011: ENTER 6 # 0:0 -0012: MOVE R66, R65 # 3:8 -0013: LOADI R67, 1 # 3:12 -0014: CMPEQI R66, R66, R67 # 3:10 -0015: JMPF R66, 18 # 3:8 -0016: LOADI R64, 1 # 3:31 -0017: JUMP 27 # 3:8 -0018: LOADI R66, 1 # 3:33 -0019: JMPF R66, 27 # 3:33 -0020: MOVE R64, R65 # 3:50 -0021: MOVE R68, R65 # 3:64 -0022: LOADI R69, 1 # 3:68 -0023: SUBI R68, R68, R69 # 3:66 -0024: CALL R67, 10 # 3:54, FACTORIAL -0025: MOVE R66, R67 # 3:54 -0026: MULI R64, R64, R66 # 3:52 -0027: MOVE R0, R0 # 4:13 -0028: LOADI R66, 1 # 4:21 -0029: ADDI R0, R0, R66 # 4:19 -0030: RETURN # 5:1 +0002: JUMP 24 # 2:10 + +-- FACTORIAL (BEGIN) +0003: LOADI R64, 0 # 2:10 +0004: ENTER 6 # 0:0 +0005: MOVE R66, R65 # 3:8 +0006: LOADI R67, 1 # 3:12 +0007: CMPEQI R66, R66, R67 # 3:10 +0008: JMPF R66, 11 # 3:8 +0009: LOADI R64, 1 # 3:31 +0010: JUMP 20 # 3:8 +0011: LOADI R66, 1 # 3:33 +0012: JMPF R66, 20 # 3:33 +0013: MOVE R64, R65 # 3:50 +0014: MOVE R68, R65 # 3:64 +0015: LOADI R69, 1 # 3:68 +0016: SUBI R68, R68, R69 # 3:66 +0017: CALL R67, 3 # 3:54, FACTORIAL +0018: MOVE R66, R67 # 3:54 +0019: MULI R64, R64, R66 # 3:52 +0020: MOVE R0, R0 # 4:13 +0021: LOADI R66, 1 # 4:21 +0022: ADDI R0, R0, R66 # 4:19 +0023: RETURN # 5:1 +-- FACTORIAL (END) + +0024: MOVE R65, R0 # 6:5 +0025: LOADI R64, 274 # 6:5 +0026: LOADI R69, 5 # 6:22 +0027: CALL R68, 3 # 6:12, FACTORIAL +0028: MOVE R67, R68 # 6:12 +0029: LOADI R66, 258 # 6:12 +0030: UPCALL 0, R64 # 6:1, OUT +0031: EOF # 0:0 ``` ## Output @@ -969,6 +1017,8 @@ OUT calls; factorial(5) ## Source ```basic +DECLARE FUNCTION pong(n%) + FUNCTION ping(n%) OUT "ping"; n IF n = 0 THEN @@ -994,62 +1044,68 @@ OUT ping(3) ```asm 0000: ENTER 4 # 0:0 -0001: LOADI R67, 3 # 19:10 -0002: CALL R66, 7 # 19:5, PING -0003: MOVE R65, R66 # 19:5 -0004: LOADI R64, 258 # 19:5 -0005: UPCALL 0, R64 # 19:1, OUT -0006: EOF # 0:0 +0001: JUMP 25 # 3:10 --- PING -0007: LOADI R64, 0 # 1:10 -0008: ENTER 6 # 0:0 -0009: LOADI R67, 0 # 2:9 -0010: LOADI R66, 275 # 2:9 -0011: MOVE R69, R65 # 2:17 -0012: LOADI R68, 258 # 2:17 -0013: UPCALL 0, R66 # 2:5, OUT -0014: MOVE R66, R65 # 3:8 -0015: LOADI R67, 0 # 3:12 -0016: CMPEQI R66, R66, R67 # 3:10 -0017: JMPF R66, 20 # 3:8 -0018: LOADI R64, 100 # 4:16 -0019: JUMP 29 # 3:8 -0020: LOADI R66, 1 # 5:5 -0021: JMPF R66, 29 # 5:5 -0022: MOVE R67, R65 # 6:21 -0023: LOADI R68, 1 # 6:25 -0024: SUBI R67, R67, R68 # 6:23 -0025: CALL R66, 30 # 6:16, PONG -0026: MOVE R64, R66 # 6:16 -0027: LOADI R66, 1 # 6:30 -0028: ADDI R64, R64, R66 # 6:28 -0029: RETURN # 8:1 - --- PONG -0030: LOADI R64, 0 # 10:10 -0031: ENTER 6 # 0:0 -0032: LOADI R67, 1 # 11:9 -0033: LOADI R66, 275 # 11:9 -0034: MOVE R69, R65 # 11:17 -0035: LOADI R68, 258 # 11:17 -0036: UPCALL 0, R66 # 11:5, OUT -0037: MOVE R66, R65 # 12:8 -0038: LOADI R67, 0 # 12:12 -0039: CMPEQI R66, R66, R67 # 12:10 -0040: JMPF R66, 43 # 12:8 -0041: LOADI R64, 200 # 13:16 -0042: JUMP 52 # 12:8 -0043: LOADI R66, 1 # 14:5 -0044: JMPF R66, 52 # 14:5 -0045: MOVE R67, R65 # 15:21 -0046: LOADI R68, 1 # 15:25 -0047: SUBI R67, R67, R68 # 15:23 -0048: CALL R66, 7 # 15:16, PING -0049: MOVE R64, R66 # 15:16 -0050: LOADI R66, 10 # 15:30 -0051: ADDI R64, R64, R66 # 15:28 -0052: RETURN # 17:1 +-- PING (BEGIN) +0002: LOADI R64, 0 # 3:10 +0003: ENTER 6 # 0:0 +0004: LOADI R67, 0 # 4:9 +0005: LOADI R66, 275 # 4:9 +0006: MOVE R69, R65 # 4:17 +0007: LOADI R68, 258 # 4:17 +0008: UPCALL 0, R66 # 4:5, OUT +0009: MOVE R66, R65 # 5:8 +0010: LOADI R67, 0 # 5:12 +0011: CMPEQI R66, R66, R67 # 5:10 +0012: JMPF R66, 15 # 5:8 +0013: LOADI R64, 100 # 6:16 +0014: JUMP 24 # 5:8 +0015: LOADI R66, 1 # 7:5 +0016: JMPF R66, 24 # 7:5 +0017: MOVE R67, R65 # 8:21 +0018: LOADI R68, 1 # 8:25 +0019: SUBI R67, R67, R68 # 8:23 +0020: CALL R66, 26 # 8:16, PONG +0021: MOVE R64, R66 # 8:16 +0022: LOADI R66, 1 # 8:30 +0023: ADDI R64, R64, R66 # 8:28 +0024: RETURN # 10:1 +-- PING (END) + +0025: JUMP 49 # 12:10 + +-- PONG (BEGIN) +0026: LOADI R64, 0 # 12:10 +0027: ENTER 6 # 0:0 +0028: LOADI R67, 1 # 13:9 +0029: LOADI R66, 275 # 13:9 +0030: MOVE R69, R65 # 13:17 +0031: LOADI R68, 258 # 13:17 +0032: UPCALL 0, R66 # 13:5, OUT +0033: MOVE R66, R65 # 14:8 +0034: LOADI R67, 0 # 14:12 +0035: CMPEQI R66, R66, R67 # 14:10 +0036: JMPF R66, 39 # 14:8 +0037: LOADI R64, 200 # 15:16 +0038: JUMP 48 # 14:8 +0039: LOADI R66, 1 # 16:5 +0040: JMPF R66, 48 # 16:5 +0041: MOVE R67, R65 # 17:21 +0042: LOADI R68, 1 # 17:25 +0043: SUBI R67, R67, R68 # 17:23 +0044: CALL R66, 2 # 17:16, PING +0045: MOVE R64, R66 # 17:16 +0046: LOADI R66, 10 # 17:30 +0047: ADDI R64, R64, R66 # 17:28 +0048: RETURN # 19:1 +-- PONG (END) + +0049: LOADI R67, 3 # 21:10 +0050: CALL R66, 2 # 21:5, PING +0051: MOVE R65, R66 # 21:5 +0052: LOADI R64, 258 # 21:5 +0053: UPCALL 0, R64 # 21:1, OUT +0054: EOF # 0:0 ``` ## Output @@ -1211,12 +1267,15 @@ DECLARE FUNCTION foo ```asm 0000: ENTER 0 # 0:0 -0001: EOF # 0:0 +0001: JUMP 5 # 3:10 --- FOO +-- FOO (BEGIN) 0002: LOADI R64, 0 # 3:10 0003: ENTER 1 # 0:0 0004: RETURN # 4:1 +-- FOO (END) + +0005: EOF # 0:0 ``` # Test: Function declarations must be top-level diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index 85edd6f9..3503dd67 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -99,12 +99,13 @@ OUT b1, d1, i1, i2, s1 ## Source ```basic +DIM SHARED i1 + FUNCTION modify_global i1 = 3 OUT "Inside after", i1 END FUNCTION -DIM SHARED i1 i1 = 2 OUT "Before", i1 OUT modify_global @@ -115,33 +116,36 @@ OUT "After", i1 ```asm 0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 6:12 -0002: LOADI R0, 2 # 7:6 -0003: LOADI R65, 0 # 8:5 -0004: LOADI R64, 291 # 8:5 -0005: MOVE R67, R0 # 8:15 -0006: LOADI R66, 258 # 8:15 -0007: UPCALL 0, R64 # 8:1, OUT -0008: CALL R65, 17 # 9:5, MODIFY_GLOBAL -0009: LOADI R64, 258 # 9:5 -0010: UPCALL 0, R64 # 9:1, OUT -0011: LOADI R65, 1 # 10:5 -0012: LOADI R64, 291 # 10:5 -0013: MOVE R67, R0 # 10:14 -0014: LOADI R66, 258 # 10:14 -0015: UPCALL 0, R64 # 10:1, OUT -0016: EOF # 0:0 - --- MODIFY_GLOBAL -0017: LOADI R64, 0 # 1:10 -0018: ENTER 5 # 0:0 -0019: LOADI R0, 3 # 2:10 -0020: LOADI R66, 2 # 3:9 -0021: LOADI R65, 291 # 3:9 -0022: MOVE R68, R0 # 3:25 -0023: LOADI R67, 258 # 3:25 -0024: UPCALL 0, R65 # 3:5, OUT -0025: RETURN # 4:1 +0001: LOADI R0, 0 # 1:12 +0002: JUMP 12 # 3:10 + +-- MODIFY_GLOBAL (BEGIN) +0003: LOADI R64, 0 # 3:10 +0004: ENTER 5 # 0:0 +0005: LOADI R0, 3 # 4:10 +0006: LOADI R66, 0 # 5:9 +0007: LOADI R65, 291 # 5:9 +0008: MOVE R68, R0 # 5:25 +0009: LOADI R67, 258 # 5:25 +0010: UPCALL 0, R65 # 5:5, OUT +0011: RETURN # 6:1 +-- MODIFY_GLOBAL (END) + +0012: LOADI R0, 2 # 8:6 +0013: LOADI R65, 1 # 9:5 +0014: LOADI R64, 291 # 9:5 +0015: MOVE R67, R0 # 9:15 +0016: LOADI R66, 258 # 9:15 +0017: UPCALL 0, R64 # 9:1, OUT +0018: CALL R65, 3 # 10:5, MODIFY_GLOBAL +0019: LOADI R64, 258 # 10:5 +0020: UPCALL 0, R64 # 10:1, OUT +0021: LOADI R65, 2 # 11:5 +0022: LOADI R64, 291 # 11:5 +0023: MOVE R67, R0 # 11:14 +0024: LOADI R66, 258 # 11:14 +0025: UPCALL 0, R64 # 11:1, OUT +0026: EOF # 0:0 ``` ## Output @@ -158,12 +162,13 @@ OUT "After", i1 ## Source ```basic +DIM SHARED i1 + SUB modify_global i1 = 3 OUT "Inside after", i1 END SUB -DIM SHARED i1 i1 = 2 OUT "Before", i1 modify_global @@ -174,30 +179,33 @@ OUT "After", i1 ```asm 0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 6:12 -0002: LOADI R0, 2 # 7:6 -0003: LOADI R65, 0 # 8:5 -0004: LOADI R64, 291 # 8:5 -0005: MOVE R67, R0 # 8:15 -0006: LOADI R66, 258 # 8:15 -0007: UPCALL 0, R64 # 8:1, OUT -0008: CALL R64, 15 # 9:1, MODIFY_GLOBAL -0009: LOADI R65, 1 # 10:5 -0010: LOADI R64, 291 # 10:5 -0011: MOVE R67, R0 # 10:14 -0012: LOADI R66, 258 # 10:14 -0013: UPCALL 0, R64 # 10:1, OUT -0014: EOF # 0:0 - --- MODIFY_GLOBAL -0015: ENTER 4 # 0:0 -0016: LOADI R0, 3 # 2:10 -0017: LOADI R65, 2 # 3:9 -0018: LOADI R64, 291 # 3:9 -0019: MOVE R67, R0 # 3:25 -0020: LOADI R66, 258 # 3:25 -0021: UPCALL 0, R64 # 3:5, OUT -0022: RETURN # 4:1 +0001: LOADI R0, 0 # 1:12 +0002: JUMP 11 # 3:5 + +-- MODIFY_GLOBAL (BEGIN) +0003: ENTER 4 # 0:0 +0004: LOADI R0, 3 # 4:10 +0005: LOADI R65, 0 # 5:9 +0006: LOADI R64, 291 # 5:9 +0007: MOVE R67, R0 # 5:25 +0008: LOADI R66, 258 # 5:25 +0009: UPCALL 0, R64 # 5:5, OUT +0010: RETURN # 6:1 +-- MODIFY_GLOBAL (END) + +0011: LOADI R0, 2 # 8:6 +0012: LOADI R65, 1 # 9:5 +0013: LOADI R64, 291 # 9:5 +0014: MOVE R67, R0 # 9:15 +0015: LOADI R66, 258 # 9:15 +0016: UPCALL 0, R64 # 9:1, OUT +0017: CALL R64, 3 # 10:1, MODIFY_GLOBAL +0018: LOADI R65, 2 # 11:5 +0019: LOADI R64, 291 # 11:5 +0020: MOVE R67, R0 # 11:14 +0021: LOADI R66, 258 # 11:14 +0022: UPCALL 0, R64 # 11:1, OUT +0023: EOF # 0:0 ``` ## Output @@ -321,20 +329,23 @@ OUT foo ```asm 0000: ENTER 2 # 0:0 -0001: CALL R65, 5 # 7:5, FOO -0002: LOADI R64, 258 # 7:5 -0003: UPCALL 0, R64 # 7:1, OUT -0004: EOF # 0:0 - --- FOO -0005: LOADI R64, 0 # 1:10 -0006: ENTER 4 # 0:0 -0007: LOADI R65, 5 # 2:9 -0008: LOADI R0, 0 # 3:16 -0009: MOVE R67, R65 # 4:9 -0010: LOADI R66, 258 # 4:9 -0011: UPCALL 0, R66 # 4:5, OUT -0012: RETURN # 5:1 +0001: JUMP 10 # 1:10 + +-- FOO (BEGIN) +0002: LOADI R64, 0 # 1:10 +0003: ENTER 4 # 0:0 +0004: LOADI R65, 5 # 2:9 +0005: LOADI R0, 0 # 3:16 +0006: MOVE R67, R65 # 4:9 +0007: LOADI R66, 258 # 4:9 +0008: UPCALL 0, R66 # 4:5, OUT +0009: RETURN # 5:1 +-- FOO (END) + +0010: CALL R65, 2 # 7:5, FOO +0011: LOADI R64, 258 # 7:5 +0012: UPCALL 0, R64 # 7:1, OUT +0013: EOF # 0:0 ``` ## Output diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 322de114..3336d443 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -20,28 +20,31 @@ OUT "After", a ```asm 0000: ENTER 5 # 0:0 0001: LOADI R64, 10 # 1:5 -0002: LOADI R66, 0 # 8:5 -0003: LOADI R65, 291 # 8:5 -0004: MOVE R68, R64 # 8:15 -0005: LOADI R67, 258 # 8:15 -0006: UPCALL 0, R65 # 8:1, OUT -0007: CALL R65, 14 # 9:1, FOO -0008: LOADI R66, 1 # 10:5 -0009: LOADI R65, 291 # 10:5 -0010: MOVE R68, R64 # 10:14 -0011: LOADI R67, 258 # 10:14 -0012: UPCALL 0, R65 # 10:1, OUT -0013: EOF # 0:0 - --- FOO -0014: ENTER 5 # 0:0 -0015: LOADI R64, 20 # 4:9 -0016: LOADI R66, 2 # 5:9 -0017: LOADI R65, 291 # 5:9 -0018: MOVE R68, R64 # 5:19 -0019: LOADI R67, 258 # 5:19 -0020: UPCALL 0, R65 # 5:5, OUT -0021: RETURN # 6:1 +0002: JUMP 11 # 3:5 + +-- FOO (BEGIN) +0003: ENTER 5 # 0:0 +0004: LOADI R64, 20 # 4:9 +0005: LOADI R66, 0 # 5:9 +0006: LOADI R65, 291 # 5:9 +0007: MOVE R68, R64 # 5:19 +0008: LOADI R67, 258 # 5:19 +0009: UPCALL 0, R65 # 5:5, OUT +0010: RETURN # 6:1 +-- FOO (END) + +0011: LOADI R66, 1 # 8:5 +0012: LOADI R65, 291 # 8:5 +0013: MOVE R68, R64 # 8:15 +0014: LOADI R67, 258 # 8:15 +0015: UPCALL 0, R65 # 8:1, OUT +0016: CALL R65, 3 # 9:1, FOO +0017: LOADI R66, 2 # 10:5 +0018: LOADI R65, 291 # 10:5 +0019: MOVE R68, R64 # 10:14 +0020: LOADI R67, 258 # 10:14 +0021: UPCALL 0, R65 # 10:1, OUT +0022: EOF # 0:0 ``` ## Output @@ -72,20 +75,26 @@ second ```asm 0000: ENTER 0 # 0:0 -0001: CALL R64, 8 # 9:1, SECOND -0002: EOF # 0:0 +0001: JUMP 7 # 1:5 --- FIRST -0003: ENTER 2 # 0:0 -0004: LOADI R65, 0 # 2:9 -0005: LOADI R64, 259 # 2:9 -0006: UPCALL 0, R64 # 2:5, OUT -0007: RETURN # 3:1 +-- FIRST (BEGIN) +0002: ENTER 2 # 0:0 +0003: LOADI R65, 0 # 2:9 +0004: LOADI R64, 259 # 2:9 +0005: UPCALL 0, R64 # 2:5, OUT +0006: RETURN # 3:1 +-- FIRST (END) --- SECOND +0007: JUMP 11 # 5:5 + +-- SECOND (BEGIN) 0008: ENTER 0 # 0:0 -0009: CALL R64, 3 # 6:5, FIRST +0009: CALL R64, 2 # 6:5, FIRST 0010: RETURN # 7:1 +-- SECOND (END) + +0011: CALL R64, 8 # 9:1, SECOND +0012: EOF # 0:0 ``` ## Output @@ -135,45 +144,51 @@ OUT "After modify_1", var ```asm 0000: ENTER 5 # 0:0 -0001: LOADI R64, 0 # 13:7 -0002: LOADI R66, 0 # 14:5 -0003: LOADI R65, 291 # 14:5 -0004: MOVE R68, R64 # 14:24 -0005: LOADI R67, 258 # 14:24 -0006: UPCALL 0, R65 # 14:1, OUT -0007: CALL R65, 22 # 15:1, MODIFY_1 -0008: LOADI R66, 1 # 16:5 -0009: LOADI R65, 291 # 16:5 -0010: MOVE R68, R64 # 16:23 -0011: LOADI R67, 258 # 16:23 -0012: UPCALL 0, R65 # 16:1, OUT -0013: EOF # 0:0 - --- MODIFY_2 -0014: ENTER 5 # 0:0 -0015: LOADI R64, 2 # 2:11 -0016: LOADI R66, 2 # 3:9 -0017: LOADI R65, 291 # 3:9 -0018: MOVE R68, R64 # 3:28 -0019: LOADI R67, 258 # 3:28 -0020: UPCALL 0, R65 # 3:5, OUT -0021: RETURN # 4:1 - --- MODIFY_1 -0022: ENTER 5 # 0:0 -0023: LOADI R64, 1 # 7:11 -0024: LOADI R66, 3 # 8:9 -0025: LOADI R65, 291 # 8:9 -0026: MOVE R68, R64 # 8:28 -0027: LOADI R67, 258 # 8:28 -0028: UPCALL 0, R65 # 8:5, OUT -0029: CALL R65, 14 # 9:5, MODIFY_2 -0030: LOADI R66, 4 # 10:9 -0031: LOADI R65, 291 # 10:9 -0032: MOVE R68, R64 # 10:27 -0033: LOADI R67, 258 # 10:27 -0034: UPCALL 0, R65 # 10:5, OUT -0035: RETURN # 11:1 +0001: JUMP 10 # 1:5 + +-- MODIFY_2 (BEGIN) +0002: ENTER 5 # 0:0 +0003: LOADI R64, 2 # 2:11 +0004: LOADI R66, 0 # 3:9 +0005: LOADI R65, 291 # 3:9 +0006: MOVE R68, R64 # 3:28 +0007: LOADI R67, 258 # 3:28 +0008: UPCALL 0, R65 # 3:5, OUT +0009: RETURN # 4:1 +-- MODIFY_2 (END) + +0010: JUMP 25 # 6:5 + +-- MODIFY_1 (BEGIN) +0011: ENTER 5 # 0:0 +0012: LOADI R64, 1 # 7:11 +0013: LOADI R66, 1 # 8:9 +0014: LOADI R65, 291 # 8:9 +0015: MOVE R68, R64 # 8:28 +0016: LOADI R67, 258 # 8:28 +0017: UPCALL 0, R65 # 8:5, OUT +0018: CALL R65, 2 # 9:5, MODIFY_2 +0019: LOADI R66, 2 # 10:9 +0020: LOADI R65, 291 # 10:9 +0021: MOVE R68, R64 # 10:27 +0022: LOADI R67, 258 # 10:27 +0023: UPCALL 0, R65 # 10:5, OUT +0024: RETURN # 11:1 +-- MODIFY_1 (END) + +0025: LOADI R64, 0 # 13:7 +0026: LOADI R66, 3 # 14:5 +0027: LOADI R65, 291 # 14:5 +0028: MOVE R68, R64 # 14:24 +0029: LOADI R67, 258 # 14:24 +0030: UPCALL 0, R65 # 14:1, OUT +0031: CALL R65, 11 # 15:1, MODIFY_1 +0032: LOADI R66, 4 # 16:5 +0033: LOADI R65, 291 # 16:5 +0034: MOVE R68, R64 # 16:23 +0035: LOADI R67, 258 # 16:23 +0036: UPCALL 0, R65 # 16:1, OUT +0037: EOF # 0:0 ``` ## Output @@ -285,33 +300,36 @@ NEXT ```asm 0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 7:9 -0002: MOVE R65, R64 # 7:5 -0003: LOADI R66, 5 # 7:14 -0004: CMPLEI R65, R65, R66 # 7:11 -0005: JMPF R65, 12 # 7:5 -0006: MOVE R65, R64 # 8:16 -0007: CALL R65, 13 # 8:5, MAYBE_EXIT -0008: MOVE R64, R64 # 7:5 -0009: LOADI R65, 1 # 7:15 -0010: ADDI R64, R64, R65 # 7:11 -0011: JUMP 2 # 7:5 -0012: EOF # 0:0 - --- MAYBE_EXIT -0013: ENTER 3 # 0:0 -0014: LOADI R66, 1 # 2:9 -0015: LOADI R65, 258 # 2:9 -0016: UPCALL 0, R65 # 2:5, OUT -0017: MOVE R65, R64 # 3:8 -0018: LOADI R66, 2 # 3:12 -0019: CMPGTI R65, R65, R66 # 3:10 -0020: JMPF R65, 22 # 3:8 -0021: JUMP 25 # 3:19 -0022: LOADI R66, 2 # 4:9 -0023: LOADI R65, 258 # 4:9 -0024: UPCALL 0, R65 # 4:5, OUT -0025: RETURN # 5:1 +0001: JUMP 15 # 1:5 + +-- MAYBE_EXIT (BEGIN) +0002: ENTER 3 # 0:0 +0003: LOADI R66, 1 # 2:9 +0004: LOADI R65, 258 # 2:9 +0005: UPCALL 0, R65 # 2:5, OUT +0006: MOVE R65, R64 # 3:8 +0007: LOADI R66, 2 # 3:12 +0008: CMPGTI R65, R65, R66 # 3:10 +0009: JMPF R65, 11 # 3:8 +0010: JUMP 14 # 3:19 +0011: LOADI R66, 2 # 4:9 +0012: LOADI R65, 258 # 4:9 +0013: UPCALL 0, R65 # 4:5, OUT +0014: RETURN # 5:1 +-- MAYBE_EXIT (END) + +0015: LOADI R64, 0 # 7:9 +0016: MOVE R65, R64 # 7:5 +0017: LOADI R66, 5 # 7:14 +0018: CMPLEI R65, R65, R66 # 7:11 +0019: JMPF R65, 26 # 7:5 +0020: MOVE R65, R64 # 8:16 +0021: CALL R65, 2 # 8:5, MAYBE_EXIT +0022: MOVE R64, R64 # 7:5 +0023: LOADI R65, 1 # 7:15 +0024: ADDI R64, R64, R65 # 7:11 +0025: JUMP 16 # 7:5 +0026: EOF # 0:0 ``` ## Output @@ -382,28 +400,31 @@ count_down "counter is" ```asm 0000: ENTER 1 # 0:0 0001: LOADI R0, 0 # 1:12 -0002: LOADI R0, 3 # 9:11 -0003: LOADI R64, 0 # 10:12 -0004: CALL R64, 6 # 10:1, COUNT_DOWN -0005: EOF # 0:0 - --- COUNT_DOWN -0006: ENTER 5 # 0:0 -0007: MOVE R66, R64 # 3:9 -0008: LOADI R65, 275 # 3:9 -0009: MOVE R68, R0 # 3:17 -0010: LOADI R67, 258 # 3:17 -0011: UPCALL 0, R65 # 3:5, OUT -0012: MOVE R65, R0 # 4:8 -0013: LOADI R66, 1 # 4:18 -0014: CMPGTI R65, R65, R66 # 4:16 -0015: JMPF R65, 21 # 4:8 -0016: MOVE R0, R0 # 5:19 -0017: LOADI R65, 1 # 5:29 -0018: SUBI R0, R0, R65 # 5:27 -0019: MOVE R65, R64 # 6:20 -0020: CALL R65, 6 # 6:9, COUNT_DOWN -0021: RETURN # 8:1 +0002: JUMP 19 # 2:5 + +-- COUNT_DOWN (BEGIN) +0003: ENTER 5 # 0:0 +0004: MOVE R66, R64 # 3:9 +0005: LOADI R65, 275 # 3:9 +0006: MOVE R68, R0 # 3:17 +0007: LOADI R67, 258 # 3:17 +0008: UPCALL 0, R65 # 3:5, OUT +0009: MOVE R65, R0 # 4:8 +0010: LOADI R66, 1 # 4:18 +0011: CMPGTI R65, R65, R66 # 4:16 +0012: JMPF R65, 18 # 4:8 +0013: MOVE R0, R0 # 5:19 +0014: LOADI R65, 1 # 5:29 +0015: SUBI R0, R0, R65 # 5:27 +0016: MOVE R65, R64 # 6:20 +0017: CALL R65, 3 # 6:9, COUNT_DOWN +0018: RETURN # 8:1 +-- COUNT_DOWN (END) + +0019: LOADI R0, 3 # 9:11 +0020: LOADI R64, 0 # 10:12 +0021: CALL R64, 3 # 10:1, COUNT_DOWN +0022: EOF # 0:0 ``` ## Output @@ -421,6 +442,8 @@ count_down "counter is" ```basic DIM SHARED value AS INTEGER +DECLARE SUB bump_value(n%) + FUNCTION count_value(n%) value = value + 1 IF n = 0 THEN @@ -445,46 +468,52 @@ OUT value ```asm 0000: ENTER 4 # 0:0 0001: LOADI R0, 0 # 1:12 -0002: LOADI R67, 2 # 18:17 -0003: CALL R66, 11 # 18:5, COUNT_VALUE -0004: MOVE R65, R66 # 18:5 -0005: LOADI R64, 258 # 18:5 -0006: UPCALL 0, R64 # 18:1, OUT -0007: MOVE R65, R0 # 19:5 -0008: LOADI R64, 258 # 19:5 -0009: UPCALL 0, R64 # 19:1, OUT -0010: EOF # 0:0 - --- COUNT_VALUE -0011: LOADI R64, 0 # 3:10 -0012: ENTER 4 # 0:0 -0013: MOVE R0, R0 # 4:13 -0014: LOADI R66, 1 # 4:21 -0015: ADDI R0, R0, R66 # 4:19 -0016: MOVE R66, R65 # 5:8 -0017: LOADI R67, 0 # 5:12 -0018: CMPEQI R66, R66, R67 # 5:10 -0019: JMPF R66, 22 # 5:8 -0020: MOVE R64, R0 # 6:23 -0021: JUMP 29 # 5:8 -0022: LOADI R66, 1 # 7:5 -0023: JMPF R66, 29 # 7:5 -0024: MOVE R66, R65 # 8:20 -0025: LOADI R67, 1 # 8:24 -0026: SUBI R66, R66, R67 # 8:22 -0027: CALL R66, 30 # 8:9, BUMP_VALUE -0028: MOVE R64, R0 # 9:23 -0029: RETURN # 11:1 - --- BUMP_VALUE -0030: ENTER 3 # 0:0 -0031: MOVE R0, R0 # 14:13 -0032: LOADI R65, 10 # 14:21 -0033: ADDI R0, R0, R65 # 14:19 -0034: MOVE R66, R64 # 15:25 -0035: CALL R65, 11 # 15:13, COUNT_VALUE -0036: MOVE R0, R65 # 15:13 -0037: RETURN # 16:1 +0002: JUMP 22 # 5:10 + +-- COUNT_VALUE (BEGIN) +0003: LOADI R64, 0 # 5:10 +0004: ENTER 4 # 0:0 +0005: MOVE R0, R0 # 6:13 +0006: LOADI R66, 1 # 6:21 +0007: ADDI R0, R0, R66 # 6:19 +0008: MOVE R66, R65 # 7:8 +0009: LOADI R67, 0 # 7:12 +0010: CMPEQI R66, R66, R67 # 7:10 +0011: JMPF R66, 14 # 7:8 +0012: MOVE R64, R0 # 8:23 +0013: JUMP 21 # 7:8 +0014: LOADI R66, 1 # 9:5 +0015: JMPF R66, 21 # 9:5 +0016: MOVE R66, R65 # 10:20 +0017: LOADI R67, 1 # 10:24 +0018: SUBI R66, R66, R67 # 10:22 +0019: CALL R66, 23 # 10:9, BUMP_VALUE +0020: MOVE R64, R0 # 11:23 +0021: RETURN # 13:1 +-- COUNT_VALUE (END) + +0022: JUMP 31 # 15:5 + +-- BUMP_VALUE (BEGIN) +0023: ENTER 3 # 0:0 +0024: MOVE R0, R0 # 16:13 +0025: LOADI R65, 10 # 16:21 +0026: ADDI R0, R0, R65 # 16:19 +0027: MOVE R66, R64 # 17:25 +0028: CALL R65, 3 # 17:13, COUNT_VALUE +0029: MOVE R0, R65 # 17:13 +0030: RETURN # 18:1 +-- BUMP_VALUE (END) + +0031: LOADI R67, 2 # 20:17 +0032: CALL R66, 3 # 20:5, COUNT_VALUE +0033: MOVE R65, R66 # 20:5 +0034: LOADI R64, 258 # 20:5 +0035: UPCALL 0, R64 # 20:1, OUT +0036: MOVE R65, R0 # 21:5 +0037: LOADI R64, 258 # 21:5 +0038: UPCALL 0, R64 # 21:1, OUT +0039: EOF # 0:0 ``` ## Output @@ -613,11 +642,14 @@ DECLARE SUB foo ```asm 0000: ENTER 0 # 0:0 -0001: EOF # 0:0 +0001: JUMP 4 # 3:5 --- FOO +-- FOO (BEGIN) 0002: ENTER 0 # 0:0 0003: RETURN # 4:1 +-- FOO (END) + +0004: EOF # 0:0 ``` # Test: Sub declarations must be top-level From 6725de36b490b8309358325b53cd4fb2c406540a Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 20 Mar 2026 15:57:55 +0100 Subject: [PATCH 42/53] core2: Add the Compiler type Compilation of a chunk of code doesn't require keeping state around but trying to compile different statements separately as part of the REPL does. To support this in a later change, introduce a Compiler type and hide the internal compile functions from consumers. This type will provide the ability to generate code for independent statements in separate calls. --- core2/examples/config.rs | 8 +++--- core2/src/compiler/mod.rs | 48 ++++++++++++++++++++++++++++++++++-- core2/src/compiler/top.rs | 27 ++++++++------------ core2/src/image.rs | 1 + core2/src/lib.rs | 2 +- core2/src/vm/mod.rs | 36 +++++++++++++-------------- core2/tests/testutils/mod.rs | 5 +++- 7 files changed, 83 insertions(+), 44 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index a6a6269e..24ef9faf 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -20,9 +20,7 @@ //! the scripted code cannot call back into Rust land, so the script's execution is guaranteed to //! not have side-effects. -use endbasic_core2::{ - ConstantDatum, ExprType, GlobalDef, GlobalDefKind, StopReason, Vm, compile, only_metadata, -}; +use endbasic_core2::{Compiler, ConstantDatum, ExprType, GlobalDef, GlobalDefKind, StopReason, Vm}; use std::collections::HashMap; /// Sample configuration file to parse. @@ -78,8 +76,8 @@ fn main() { // Compile the script, making the pre-defined globals visible to it. let upcalls = HashMap::default(); - let image = compile(&mut SCRIPT.as_bytes(), only_metadata(&upcalls), &global_defs) - .expect("Compilation failed"); + let compiler = Compiler::new(&upcalls, &global_defs).expect("Globals initialization failed"); + let image = compiler.compile(&mut SCRIPT.as_bytes()).expect("Compilation failed"); // Load and execute the compiled image. let mut vm = Vm::new(upcalls); diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 73093f89..941a28df 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -18,9 +18,12 @@ use crate::ast::{ExprType, VarRef}; use crate::bytecode::{InvalidExitCodeError, RegisterScope}; use crate::callable::CallableMetadata; -use crate::parser; +use crate::image::Image; use crate::reader::LineCol; +use crate::{Callable, parser}; +use std::collections::HashMap; use std::io; +use std::rc::Rc; mod args; @@ -31,10 +34,12 @@ mod exprs; mod ids; mod syms; +use syms::GlobalSymtable; pub use syms::SymbolKey; mod top; -pub use top::{GlobalDef, GlobalDefKind, compile, only_metadata}; +use top::{Context, prepare_globals}; +pub use top::{GlobalDef, GlobalDefKind, only_metadata}; /// Errors that can occur during compilation. #[derive(Debug, thiserror::Error)] @@ -171,3 +176,42 @@ impl From for Error { /// Result type for compilation operations. pub type Result = std::result::Result; + +/// Compiler context. +/// +/// This exists to support incremental compilation by keeping state and appending code to the +/// image being built, which is useful in REPL scenarios. +pub struct Compiler { + context: Context, + symtable: GlobalSymtable, +} + +impl Compiler { + /// Creates a new compiler instance. + /// + /// `global_defs` provides pre-defined global variables visible to the compiled program. + /// + /// `upcalls` contains the metadata of all built-in callables that the compiled code can use. + pub fn new( + upcalls: &HashMap>, + global_defs: &[GlobalDef], + ) -> Result { + let mut upcalls_metadata = HashMap::with_capacity(upcalls.len()); + for (k, v) in upcalls.iter() { + upcalls_metadata.insert(k.clone(), v.metadata()); + } + + let mut context = Context::default(); + + let mut symtable = GlobalSymtable::new(upcalls_metadata); + + prepare_globals(&mut context, &mut symtable, global_defs)?; + + Ok(Self { context, symtable }) + } + + /// Compiles a chunk of code. + pub fn compile(self, input: &mut dyn io::Read) -> Result { + top::compile(input, self.context, self.symtable) + } +} diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 1b50014b..d8daf76c 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -54,7 +54,7 @@ enum CallableKind { /// This type exists to minimize the number of complex arguments passed across functions. /// If possible, avoid passing it and instead pass the minimum set of required fields. #[derive(Default)] -struct Context { +pub(super) struct Context { /// The code generator accumulating bytecode instructions. codegen: Codegen, @@ -1186,7 +1186,7 @@ pub enum GlobalDefKind { /// /// After execution, use `Vm::get_global*` methods to query the values of these globals (and /// any globals declared via `DIM SHARED` in the program itself). -fn prepare_globals( +pub(super) fn prepare_globals( ctx: &mut Context, symtable: &mut GlobalSymtable, global_defs: &[GlobalDef], @@ -1259,21 +1259,12 @@ fn prepare_globals( Ok(()) } -/// Compiles the `input` into an `Image` that can be executed by the VM, with `global_defs` -/// pre-defined as global variables visible to the compiled program. -/// -/// `upcalls` contains the metadata of all built-in callables that the compiled code can use. +/// Compiles the `input` into an `Image` that can be executed by the VM. pub fn compile( input: &mut dyn io::Read, - upcalls: HashMap>, - global_defs: &[GlobalDef], + mut ctx: Context, + mut symtable: GlobalSymtable, ) -> Result { - let mut ctx = Context::default(); - - let mut symtable = GlobalSymtable::new(upcalls); - - prepare_globals(&mut ctx, &mut symtable, global_defs)?; - compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input))?; ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); @@ -1293,13 +1284,15 @@ pub fn compile( #[cfg(test)] mod tests { use super::*; + use crate::Compiler; use crate::ast::ExprType; use crate::mem::ConstantDatum; use crate::vm::{StopReason, Vm}; fn compile_and_get_global(defs: &[GlobalDef], name: &str) -> ConstantDatum { - let image = compile(&mut "".as_bytes(), HashMap::default(), defs) - .expect("compilation should succeed"); + let compiler = Compiler::new(&HashMap::default(), defs) + .expect("constants initialization must succeed"); + let image = compiler.compile(&mut "".as_bytes()).expect("compilation should succeed"); let mut vm = Vm::new(HashMap::default()); vm.load(image); match vm.exec() { @@ -1381,7 +1374,7 @@ mod tests { initial_value: Some(ConstantDatum::Double(1.5)), }, }]; - let result = compile(&mut "".as_bytes(), HashMap::default(), &defs); + let result = Compiler::new(&HashMap::default(), &defs); assert!(matches!(result, Err(Error::TypeMismatch(..)))); } } diff --git a/core2/src/image.rs b/core2/src/image.rs index 66e9d078..6a63e6d4 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -106,6 +106,7 @@ pub(crate) struct GlobalVarInfo { } /// Per-instruction metadata stored in `DebugInfo`. +#[derive(Clone)] pub(crate) struct InstrMetadata { /// Source location that generated this instruction. pub(crate) linecol: LineCol, diff --git a/core2/src/lib.rs b/core2/src/lib.rs index 00d96c22..a4ac8d56 100644 --- a/core2/src/lib.rs +++ b/core2/src/lib.rs @@ -30,7 +30,7 @@ mod vm; pub use ast::{ArgSep, ExprType}; pub use bytecode::{ExitCode, InvalidExitCodeError, VarArgTag}; pub use callable::*; -pub use compiler::{GlobalDef, GlobalDefKind, SymbolKey, compile, only_metadata}; +pub use compiler::{Compiler, GlobalDef, GlobalDefKind, SymbolKey, only_metadata}; pub use mem::ConstantDatum; pub use vm::{GetGlobalError, GetGlobalResult, StopReason, Vm}; diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 796f6492..bb49171d 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -335,12 +335,13 @@ impl Vm { #[cfg(test)] mod tests { use super::*; + use crate::Compiler; use crate::ast::{ArgSep, ExprType}; use crate::callable::{ ArgSepSyntax, CallResult, CallableMetadata, CallableMetadataBuilder, RequiredValueSyntax, SingularArgSyntax, }; - use crate::compiler::{SymbolKey, compile, only_metadata}; + use crate::compiler::SymbolKey; use crate::image::Image; use crate::reader::LineCol; use crate::testutils::OutCommand; @@ -436,7 +437,8 @@ mod tests { #[test] fn test_exec_empty_compilation_is_eof() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"".as_slice(), HashMap::default(), &[]).unwrap(); + let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); + let image = compiler.compile(&mut b"".as_slice()).unwrap(); vm.load(image); match vm.exec() { StopReason::Eof => (), @@ -450,9 +452,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); - let image = - compile(&mut b"OUT 30: OUT 20".as_slice(), only_metadata(&upcalls_by_name), &[]) - .unwrap(); + let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let image = compiler.compile(&mut b"OUT 30: OUT 20".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); @@ -485,7 +486,8 @@ mod tests { #[tokio::test] async fn test_exec_end_code_default() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END".as_slice(), HashMap::default(), &[]).unwrap(); + let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); + let image = compiler.compile(&mut b"END".as_slice()).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.is_success() => (), @@ -496,7 +498,8 @@ mod tests { #[tokio::test] async fn test_exec_end_code_explicit() { let mut vm = Vm::new(HashMap::default()); - let image = compile(&mut b"END 3".as_slice(), HashMap::default(), &[]).unwrap(); + let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); + let image = compiler.compile(&mut b"END 3".as_slice()).unwrap(); vm.load(image); match vm.exec() { StopReason::End(code) if code.to_i32() == 3 => (), @@ -511,8 +514,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); - let image = - compile(&mut b"POS_CAPTURE".as_slice(), only_metadata(&upcalls_by_name), &[]).unwrap(); + let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let image = compiler.compile(&mut b"POS_CAPTURE".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -528,9 +531,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); - let image = - compile(&mut b"POS_CAPTURE 42".as_slice(), only_metadata(&upcalls_by_name), &[]) - .unwrap(); + let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let image = compiler.compile(&mut b"POS_CAPTURE 42".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -546,9 +548,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); - let image = - compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice(), only_metadata(&upcalls_by_name), &[]) - .unwrap(); + let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let image = compiler.compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; @@ -571,9 +572,8 @@ mod tests { let mut upcalls_by_name: HashMap> = HashMap::new(); upcalls_by_name.insert(SymbolKey::from("POS_CAPTURE"), cmd); - let image = - compile(&mut b"POS_CAPTURE 1 + 2".as_slice(), only_metadata(&upcalls_by_name), &[]) - .unwrap(); + let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let image = compiler.compile(&mut b"POS_CAPTURE 1 + 2".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); vm.load(image); run_to_end(&mut vm).await; diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index f38fe1a7..5a9fcac2 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -321,7 +321,10 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let console = Rc::from(RefCell::from(String::new())); let mut upcalls_by_name: HashMap> = HashMap::default(); callables::register_all(&mut upcalls_by_name, console.clone()); - let image = { compile(&mut source.as_bytes(), only_metadata(&upcalls_by_name), &[]) }; + let image = { + let compiler = Compiler::new(&upcalls_by_name, &[]).expect("Cannot fail"); + compiler.compile(&mut source.as_bytes()) + }; let image = match image { Ok(image) => image, From 826726e8f65b089e5756921fce0387c2de502b18 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 17:28:47 +0100 Subject: [PATCH 43/53] core2: Remove the ENTER and LEAVE instructions These were nice in theory but the need to have an ENTER for the "global" program scope (the code outside functions and subs, with its own local symtable) makes it extra complicated to support incremental compilation. We could have added an EXTEND instruction to grow the register bank during incremental compilation but... ENTER/LEAVE only saved us 256 (total registers) - 64 (global registers) bytes of memory, which is irrelevant considering that we need much more just to run. While doing this, and because this change pretty much invalidates all disassembly golden data, take the chance to "fix" the comment character used in the asm code to be the semicolon so that standard formatting in editors shows comments as such. --- core2/src/bytecode.rs | 26 +- core2/src/compiler/args.rs | 12 +- core2/src/compiler/codegen.rs | 4 - core2/src/compiler/syms.rs | 50 -- core2/src/compiler/top.rs | 28 +- core2/src/image.rs | 8 +- core2/src/vm/context.rs | 18 +- core2/tests/test_args.md | 366 +++++----- core2/tests/test_arithmetic_add.md | 123 ++-- core2/tests/test_arithmetic_div.md | 82 +-- core2/tests/test_arithmetic_mod.md | 82 +-- core2/tests/test_arithmetic_mul.md | 65 +- core2/tests/test_arithmetic_neg.md | 40 +- core2/tests/test_arithmetic_pow.md | 76 +- core2/tests/test_arithmetic_sub.md | 67 +- core2/tests/test_arrays.md | 583 ++++++++-------- core2/tests/test_assignments.md | 89 ++- core2/tests/test_bitwise_and.md | 36 +- core2/tests/test_bitwise_not.md | 34 +- core2/tests/test_bitwise_or.md | 36 +- core2/tests/test_bitwise_shl.md | 54 +- core2/tests/test_bitwise_shr.md | 84 ++- core2/tests/test_bitwise_xor.md | 36 +- core2/tests/test_case_insensitivity.md | 189 +++-- core2/tests/test_data.md | 51 +- core2/tests/test_do.md | 731 ++++++++++---------- core2/tests/test_empty.md | 3 +- core2/tests/test_end.md | 96 ++- core2/tests/test_for.md | 458 ++++++------ core2/tests/test_functions.md | 922 ++++++++++++------------- core2/tests/test_globals.md | 268 ++++--- core2/tests/test_gosub.md | 145 ++-- core2/tests/test_goto.md | 114 ++- core2/tests/test_if.md | 375 +++++----- core2/tests/test_on_error.md | 285 ++++---- core2/tests/test_relational_eq.md | 67 +- core2/tests/test_relational_ge.md | 54 +- core2/tests/test_relational_gt.md | 54 +- core2/tests/test_relational_le.md | 54 +- core2/tests/test_relational_lt.md | 54 +- core2/tests/test_relational_ne.md | 67 +- core2/tests/test_select.md | 698 +++++++++---------- core2/tests/test_strings.md | 43 +- core2/tests/test_subs.md | 392 +++++------ core2/tests/test_types.md | 79 +-- core2/tests/test_while.md | 83 ++- 46 files changed, 3437 insertions(+), 3844 deletions(-) diff --git a/core2/src/bytecode.rs b/core2/src/bytecode.rs index fc392aa0..9ba0d093 100644 --- a/core2/src/bytecode.rs +++ b/core2/src/bytecode.rs @@ -167,6 +167,9 @@ impl RawValue for Register { } impl Register { + /// Maximum number of supported registers. + pub(crate) const MAX: u8 = u8::MAX; + /// Maximum number of supported global registers. pub(crate) const MAX_GLOBAL: u8 = 64; @@ -496,9 +499,6 @@ pub(crate) enum Opcode { /// Compares two strings for equality and stores the result into a third one. EqualText, - /// Allocates local registers (locals and temporaries) when entering a scope. - Enter, - /// Jumps to a subroutine at an address relative to the PC. Gosub, @@ -529,9 +529,6 @@ pub(crate) enum Opcode { /// Jumps to an address relative to the PC if the condition register is false (0). JumpIfFalse, - /// Deallocates the registers allocated by the preamble ENTER, unwinding to the FP. - Leave, - /// Compares two doubles for less-than and stores the result into a third one. LessDouble, @@ -792,13 +789,6 @@ instr!( Register, 0x000000ff, 0, // Register with the return code. ); -#[rustfmt::skip] -instr!( - Opcode::Enter, "ENTER", - make_enter, parse_enter, format_enter, - u8, 0x000000ff, 0, // Number of local registers to allocate. -); - #[rustfmt::skip] instr!( Opcode::Eof, "EOF", @@ -888,12 +878,6 @@ instr!( u16, 0x0000ffff, 0, // Target address. ); -#[rustfmt::skip] -instr!( - Opcode::Leave, "LEAVE", - make_leave, parse_leave, format_leave, -); - #[rustfmt::skip] instr!( Opcode::LessDouble, "CMPLTD", @@ -1427,8 +1411,6 @@ mod tests { test_instr!(test_end, make_end, parse_end, Register::local(1).unwrap()); - test_instr!(test_enter, make_enter, parse_enter, 10); - test_instr!(test_eof, make_eof, parse_eof); test_instr!(test_gosub, make_gosub, parse_gosub, 12345); @@ -1504,8 +1486,6 @@ mod tests { 12345 ); - test_instr!(test_leave, make_leave, parse_leave); - test_instr!( test_less_double, make_less_double, diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index fe1f276b..22866c80 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -401,7 +401,6 @@ mod tests { use super::*; use crate::CallableMetadataBuilder; use crate::callable::RepeatedSyntax; - use crate::compiler::codegen::Fixup; use crate::compiler::syms::GlobalSymtable; use std::borrow::Cow; use std::collections::HashMap; @@ -423,7 +422,6 @@ mod tests { .test_build(); let mut codegen = Codegen::default(); - let enter = codegen.emit(bytecode::make_nop(), LineCol { line: 0, col: 0 }); let upcalls = HashMap::default(); let mut global = GlobalSymtable::new(upcalls); @@ -445,16 +443,12 @@ mod tests { codegen.set_arg_linecols(addr, arg_linecols); codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); - let nlocals = local.leave_scope().map_err(|e| Error::from_syms(e, pos))?; - codegen.add_fixup(enter, Fixup::Enter(nlocals)); - let image = codegen.build_image(HashMap::default(), vec![])?; assert_eq!( vec![ - "0000: ENTER 1 # 0:0".to_owned(), - "0001: LOADI R64, 0 # 1:1".to_owned(), - "0002: UPCALL 0, R64 # 1:1, OUT".to_owned(), - "0003: EOF # 0:0".to_owned(), + "0000: LOADI R64, 0 ; 1:1".to_owned(), + "0001: UPCALL 0, R64 ; 1:1, OUT".to_owned(), + "0002: EOF ; 0:0".to_owned(), ], image.disasm() ); diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index 4a224a5c..f286ebde 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -34,9 +34,6 @@ pub(super) enum Fixup { /// Fixup to resolve a user-defined call target address into a `CALL` instruction. Call(Register, SymbolKey), - /// Fixup to record the number of local variables to allocate in an `ENTER` instruction. - Enter(u8), - /// Fixup to resolve a label target address into a `GOSUB` instruction. Gosub(String), @@ -200,7 +197,6 @@ impl Codegen { self.user_callables_addresses.get(&key).expect("Must be present"); bytecode::make_call(reg, Self::make_target(*target, pos)?) } - Fixup::Enter(nargs) => bytecode::make_enter(nargs), Fixup::Gosub(label) => { let key = SymbolKey::from(&label); let Some(target) = self.labels.get(&key) else { diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index 0469c99d..a27529b8 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -254,15 +254,6 @@ impl<'a> LocalSymtable<'a> { self.symtable } - /// Consumes the local scope and returns the number of local variables defined, which includes - /// the locals themselves and any temporaries used by the scope. - pub(crate) fn leave_scope(self) -> Result { - match u8::try_from(self.locals.len() + usize::from(self.count_temps)) { - Ok(nregs) => Ok(nregs), - Err(_) => Err(Error::OutOfRegisters(RegisterScope::Local)), - } - } - /// Declares a new user-defined `vref` callable with `md` metadata. pub(crate) fn declare_user_callable( &mut self, @@ -687,42 +678,6 @@ mod tests { assert_eq!("Undefined local symbol nope", err.to_string()); } - #[test] - fn test_leave_scope_counts_locals_only() -> Result<()> { - let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(upcalls); - let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; - local.put_local(SymbolKey::from("b"), SymbolPrototype::Scalar(ExprType::Integer))?; - assert_eq!(2, local.leave_scope()?); - Ok(()) - } - - #[test] - fn test_leave_scope_counts_locals_and_temps() -> Result<()> { - let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(upcalls); - let mut local = global.enter_scope(); - local.put_local(SymbolKey::from("a"), SymbolPrototype::Scalar(ExprType::Integer))?; - { - let temp = local.frozen(); - let mut scope = temp.temp_scope(); - scope.alloc()?; - scope.alloc()?; - } - assert_eq!(3, local.leave_scope()?); - Ok(()) - } - - #[test] - fn test_leave_scope_empty() -> Result<()> { - let upcalls = HashMap::default(); - let mut global = GlobalSymtable::new(upcalls); - let local = global.enter_scope(); - assert_eq!(0, local.leave_scope()?); - Ok(()) - } - #[test] fn test_define_and_get_user_callable() -> Result<()> { let upcalls = HashMap::default(); @@ -902,7 +857,6 @@ mod tests { assert_eq!(Register::local(1).unwrap(), scope.alloc()?); } } - assert_eq!(5, local.leave_scope()?); Ok(()) } @@ -922,7 +876,6 @@ mod tests { }, )?; - assert_eq!(3, local.leave_scope()?); Ok(()) } @@ -943,7 +896,6 @@ mod tests { }, )?; - assert_eq!(3, local.leave_scope()?); Ok(()) } @@ -1020,7 +972,6 @@ mod tests { { let mut local = global.enter_scope(); local.put_local(SymbolKey::from("x"), SymbolPrototype::Scalar(ExprType::Integer))?; - assert_eq!(1, local.leave_scope()?); } { @@ -1032,7 +983,6 @@ mod tests { let reg = local.put_local(SymbolKey::from("y"), SymbolPrototype::Scalar(ExprType::Double))?; assert_eq!(Register::local(0).unwrap(), reg); - assert_eq!(1, local.leave_scope()?); } Ok(()) diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index d8daf76c..e31a4a08 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -1009,21 +1009,6 @@ fn compile_stmt( Ok(()) } -/// Compiles a sequence of `stmts` that all live in the same `symtable` scope. -fn compile_scope(ctx: &mut Context, mut symtable: LocalSymtable<'_>, stmts: I) -> Result<()> -where - I: Iterator>, -{ - let enter = ctx.codegen.emit(bytecode::make_nop(), LineCol { line: 0, col: 0 }); - for stmt in stmts { - compile_stmt(ctx, &mut symtable, stmt?)?; - } - let nlocals = - symtable.leave_scope().map_err(|e| Error::from_syms(e, LineCol { line: 0, col: 0 }))?; - ctx.codegen.add_fixup(enter, Fixup::Enter(nlocals)); - Ok(()) -} - /// Declares a callable. /// /// If the callable is already defined, this ensures the new declaration matches the previous one @@ -1114,7 +1099,9 @@ fn compile_user_callable( .map_err(|e| Error::from_syms(e, key_pos))?; } - compile_scope(ctx, symtable, callable.body.into_iter().map(Ok))?; + for stmt in callable.body { + compile_stmt(ctx, &mut symtable, stmt)?; + } let return_addr = ctx.codegen.emit(bytecode::make_return(), callable.end_pos); for (addr, pos) in ctx.callable_exit_jumps.drain(..) { @@ -1234,7 +1221,6 @@ pub(super) fn prepare_globals( if array_globals.is_empty() { return Ok(()); } - ctx.codegen.emit(bytecode::make_enter(max_ndims), preamble_pos); for (reg, def) in array_globals { let GlobalDefKind::Array { subtype, dimensions } = &def.kind else { unreachable!("array_globals only contains array defs per the loop above") @@ -1254,7 +1240,6 @@ pub(super) fn prepare_globals( .map_err(|_| Error::TooManyArrayDimensions(preamble_pos, usize::from(ndims)))?; ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), preamble_pos); } - ctx.codegen.emit(bytecode::make_leave(), preamble_pos); Ok(()) } @@ -1265,7 +1250,12 @@ pub fn compile( mut ctx: Context, mut symtable: GlobalSymtable, ) -> Result { - compile_scope(&mut ctx, symtable.enter_scope(), parser::parse(input))?; + { + let mut symtable = symtable.enter_scope(); + for stmt in parser::parse(input) { + compile_stmt(&mut ctx, &mut symtable, stmt?)?; + } + } ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); let global_vars = symtable diff --git a/core2/src/image.rs b/core2/src/image.rs index 6a63e6d4..355c5313 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -46,7 +46,6 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::EqualText => bytecode::format_equal_text(instr), Opcode::Eof => bytecode::format_eof(instr), Opcode::End => bytecode::format_end(instr), - Opcode::Enter => bytecode::format_enter(instr), Opcode::Gosub => bytecode::format_gosub(instr), Opcode::GreaterDouble => bytecode::format_greater_double(instr), Opcode::GreaterEqualDouble => bytecode::format_greater_equal_double(instr), @@ -57,7 +56,6 @@ pub(crate) fn format_instr(instr: u32) -> String { Opcode::IntegerToDouble => bytecode::format_integer_to_double(instr), Opcode::Jump => bytecode::format_jump(instr), Opcode::JumpIfFalse => bytecode::format_jump_if_false(instr), - Opcode::Leave => bytecode::format_leave(instr), Opcode::LessDouble => bytecode::format_less_double(instr), Opcode::LessEqualDouble => bytecode::format_less_equal_double(instr), Opcode::LessEqualInteger => bytecode::format_less_equal_integer(instr), @@ -209,9 +207,9 @@ impl Image { if let Some((key, is_start)) = self.debug_info.callables.get(&i) { if *is_start { lines.push("".to_owned()); - lines.push(format!("-- {} (BEGIN)", key)); + lines.push(format!(";; {} (BEGIN)", key)); } else { - lines.push(format!("-- {} (END)", key)); + lines.push(format!(";; {} (END)", key)); lines.push("".to_owned()); } } @@ -221,7 +219,7 @@ impl Image { while line.len() < 40 { line.push(' '); } - line.push_str(&format!("# {}", pos)); + line.push_str(&format!("; {}", pos)); match opcode_of(instr) { Opcode::Call => { diff --git a/core2/src/vm/context.rs b/core2/src/vm/context.rs index 415061d6..223c49b5 100644 --- a/core2/src/vm/context.rs +++ b/core2/src/vm/context.rs @@ -178,7 +178,7 @@ impl Default for Context { fp: usize::from(Register::MAX_GLOBAL), stop: None, err_handler: ErrorHandler::None, - regs: vec![0; usize::from(Register::MAX_GLOBAL)], + regs: vec![0; usize::from(Register::MAX)], call_stack: vec![], } } @@ -332,7 +332,6 @@ impl Context { Opcode::EqualInteger => self.do_equal_integer(instr), Opcode::EqualText => self.do_equal_text(instr, &image.constants, heap), Opcode::End => self.do_end(instr), - Opcode::Enter => self.do_enter(instr), Opcode::Eof => self.do_eof(instr), Opcode::Gosub => self.do_gosub(instr), Opcode::GreaterDouble => self.do_greater_double(instr), @@ -346,7 +345,6 @@ impl Context { Opcode::IntegerToDouble => self.do_integer_to_double(instr), Opcode::Jump => self.do_jump(instr), Opcode::JumpIfFalse => self.do_jump_if_false(instr), - Opcode::Leave => self.do_leave(instr), Opcode::LessDouble => self.do_less_double(instr), Opcode::LessEqualDouble => self.do_less_equal_double(instr), Opcode::LessEqualInteger => self.do_less_equal_integer(instr), @@ -667,13 +665,6 @@ impl Context { self.stop = Some(InternalStopReason::End(code)); } - /// Implements the `Enter` opcode. - pub(super) fn do_enter(&mut self, instr: u32) { - let nlocals = bytecode::parse_enter(instr); - self.regs.resize(self.regs.len() + usize::from(nlocals), 0); - self.pc += 1; - } - /// Implements the `Eof` opcode. pub(super) fn do_eof(&mut self, instr: u32) { bytecode::parse_eof(instr); @@ -763,13 +754,6 @@ impl Context { } } - /// Implements the `Leave` opcode. - pub(super) fn do_leave(&mut self, instr: u32) { - bytecode::parse_leave(instr); - self.regs.truncate(self.fp); - self.pc += 1; - } - /// Implements the `LessDouble` opcode. pub(super) fn do_less_double(&mut self, instr: u32) { self.do_binary_double_predicate_op(instr, bytecode::parse_less_double, |l, r| l < r); diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index 46dc5ba2..a78feb78 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -37,10 +37,9 @@ OUT_REQUIRED_VALUE 4 ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 4 # 1:20 -0002: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE -0003: EOF # 0:0 +0000: LOADI R64, 4 ; 1:20 +0001: UPCALL 0, R64 ; 1:1, OUT_REQUIRED_VALUE +0002: EOF ; 0:0 ``` ## Output @@ -60,11 +59,10 @@ OUT_REQUIRED_VALUE 7.8 ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADC R64, 0 # 1:20 -0002: DTOI R64 # 1:20 -0003: UPCALL 0, R64 # 1:1, OUT_REQUIRED_VALUE -0004: EOF # 0:0 +0000: LOADC R64, 0 ; 1:20 +0001: DTOI R64 ; 1:20 +0002: UPCALL 0, R64 ; 1:1, OUT_REQUIRED_VALUE +0003: EOF ; 0:0 ``` ## Output @@ -84,10 +82,9 @@ OUT_OPTIONAL ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 1:1 -0002: UPCALL 0, R64 # 1:1, OUT_OPTIONAL -0003: EOF # 0:0 +0000: LOADI R64, 0 ; 1:1 +0001: UPCALL 0, R64 ; 1:1, OUT_OPTIONAL +0002: EOF ; 0:0 ``` ## Output @@ -107,11 +104,10 @@ OUT_OPTIONAL "Foo" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 1:14 -0002: LOADI R64, 259 # 1:14 -0003: UPCALL 0, R64 # 1:1, OUT_OPTIONAL -0004: EOF # 0:0 +0000: LOADI R65, 0 ; 1:14 +0001: LOADI R64, 259 ; 1:14 +0002: UPCALL 0, R64 ; 1:1, OUT_OPTIONAL +0003: EOF ; 0:0 ``` ## Output @@ -146,14 +142,13 @@ OUT_ANY_VALUE "Text" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 1 # 1:15 -0002: LOADI R64, 256 # 1:15 -0003: UPCALL 0, R64 # 1:1, OUT_ANY_VALUE -0004: LOADI R65, 0 # 2:15 -0005: LOADI R64, 259 # 2:15 -0006: UPCALL 0, R64 # 2:1, OUT_ANY_VALUE -0007: EOF # 0:0 +0000: LOADI R65, 1 ; 1:15 +0001: LOADI R64, 256 ; 1:15 +0002: UPCALL 0, R64 ; 1:1, OUT_ANY_VALUE +0003: LOADI R65, 0 ; 2:15 +0004: LOADI R64, 259 ; 2:15 +0005: UPCALL 0, R64 ; 2:1, OUT_ANY_VALUE +0006: EOF ; 0:0 ``` ## Output @@ -190,16 +185,15 @@ OUT_ANY_VALUE_OPTIONAL "Text" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 0 # 1:1 -0002: UPCALL 0, R64 # 1:1, OUT_ANY_VALUE_OPTIONAL -0003: LOADI R65, 1 # 2:24 -0004: LOADI R64, 256 # 2:24 -0005: UPCALL 0, R64 # 2:1, OUT_ANY_VALUE_OPTIONAL -0006: LOADI R65, 0 # 3:24 -0007: LOADI R64, 259 # 3:24 -0008: UPCALL 0, R64 # 3:1, OUT_ANY_VALUE_OPTIONAL -0009: EOF # 0:0 +0000: LOADI R64, 0 ; 1:1 +0001: UPCALL 0, R64 ; 1:1, OUT_ANY_VALUE_OPTIONAL +0002: LOADI R65, 1 ; 2:24 +0003: LOADI R64, 256 ; 2:24 +0004: UPCALL 0, R64 ; 2:1, OUT_ANY_VALUE_OPTIONAL +0005: LOADI R65, 0 ; 3:24 +0006: LOADI R64, 259 ; 3:24 +0007: UPCALL 0, R64 ; 3:1, OUT_ANY_VALUE_OPTIONAL +0008: EOF ; 0:0 ``` ## Output @@ -237,26 +231,25 @@ OUT_POSITIONAL ; 0 AS 8.2 ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R65, 3 # 1:16 -0002: LOADI R64, 290 # 1:16 -0003: LOADC R66, 0 # 1:19 -0004: DTOI R66 # 1:19 -0005: LOADI R68, 1 # 1:26 -0006: LOADI R67, 259 # 1:26 -0007: UPCALL 0, R64 # 1:1, OUT_POSITIONAL -0008: LOADI R65, 2 # 2:16 -0009: LOADI R64, 275 # 2:16 -0010: LOADI R66, 4 # 2:21 -0011: LOADI R68, 1 # 2:26 -0012: LOADI R67, 259 # 2:26 -0013: UPCALL 0, R64 # 2:1, OUT_POSITIONAL -0014: LOADI R64, 16 # 3:16 -0015: LOADI R65, 0 # 3:18 -0016: LOADC R67, 3 # 3:23 -0017: LOADI R66, 257 # 3:23 -0018: UPCALL 0, R64 # 3:1, OUT_POSITIONAL -0019: EOF # 0:0 +0000: LOADI R65, 3 ; 1:16 +0001: LOADI R64, 290 ; 1:16 +0002: LOADC R66, 0 ; 1:19 +0003: DTOI R66 ; 1:19 +0004: LOADI R68, 1 ; 1:26 +0005: LOADI R67, 259 ; 1:26 +0006: UPCALL 0, R64 ; 1:1, OUT_POSITIONAL +0007: LOADI R65, 2 ; 2:16 +0008: LOADI R64, 275 ; 2:16 +0009: LOADI R66, 4 ; 2:21 +0010: LOADI R68, 1 ; 2:26 +0011: LOADI R67, 259 ; 2:26 +0012: UPCALL 0, R64 ; 2:1, OUT_POSITIONAL +0013: LOADI R64, 16 ; 3:16 +0014: LOADI R65, 0 ; 3:18 +0015: LOADC R67, 3 ; 3:23 +0016: LOADI R66, 257 ; 3:23 +0017: UPCALL 0, R64 ; 3:1, OUT_POSITIONAL +0018: EOF ; 0:0 ``` ## Output @@ -298,10 +291,9 @@ OUT ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 1:1 -0002: UPCALL 0, R64 # 1:1, OUT -0003: EOF # 0:0 +0000: LOADI R64, 0 ; 1:1 +0001: UPCALL 0, R64 ; 1:1, OUT +0002: EOF ; 0:0 ``` ## Output @@ -321,15 +313,14 @@ OUT 100, 200, 300 ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R67, 200 # 1:10 -0004: LOADI R66, 290 # 1:10 -0005: LOADI R69, 300 # 1:15 -0006: LOADI R68, 258 # 1:15 -0007: UPCALL 0, R64 # 1:1, OUT -0008: EOF # 0:0 +0000: LOADI R65, 100 ; 1:5 +0001: LOADI R64, 290 ; 1:5 +0002: LOADI R67, 200 ; 1:10 +0003: LOADI R66, 290 ; 1:10 +0004: LOADI R69, 300 ; 1:15 +0005: LOADI R68, 258 ; 1:15 +0006: UPCALL 0, R64 ; 1:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -349,15 +340,14 @@ OUT 100, , 300, ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R66, 32 # 1:10 -0004: LOADI R68, 300 # 1:12 -0005: LOADI R67, 290 # 1:12 -0006: LOADI R69, 0 # 1:16 -0007: UPCALL 0, R64 # 1:1, OUT -0008: EOF # 0:0 +0000: LOADI R65, 100 ; 1:5 +0001: LOADI R64, 290 ; 1:5 +0002: LOADI R66, 32 ; 1:10 +0003: LOADI R68, 300 ; 1:12 +0004: LOADI R67, 290 ; 1:12 +0005: LOADI R69, 0 ; 1:16 +0006: UPCALL 0, R64 ; 1:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -377,17 +367,16 @@ OUT 100; 200 AS 300; 400 ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 274 # 1:5 -0003: LOADI R67, 200 # 1:10 -0004: LOADI R66, 306 # 1:10 -0005: LOADI R69, 300 # 1:17 -0006: LOADI R68, 274 # 1:17 -0007: LOADI R71, 400 # 1:22 -0008: LOADI R70, 258 # 1:22 -0009: UPCALL 0, R64 # 1:1, OUT -0010: EOF # 0:0 +0000: LOADI R65, 100 ; 1:5 +0001: LOADI R64, 274 ; 1:5 +0002: LOADI R67, 200 ; 1:10 +0003: LOADI R66, 306 ; 1:10 +0004: LOADI R69, 300 ; 1:17 +0005: LOADI R68, 274 ; 1:17 +0006: LOADI R71, 400 ; 1:22 +0007: LOADI R70, 258 ; 1:22 +0008: UPCALL 0, R64 ; 1:1, OUT +0009: EOF ; 0:0 ``` ## Output @@ -407,13 +396,12 @@ OUT 100, "Foo" ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 100 # 1:5 -0002: LOADI R64, 290 # 1:5 -0003: LOADI R67, 0 # 1:10 -0004: LOADI R66, 259 # 1:10 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 100 ; 1:5 +0001: LOADI R64, 290 ; 1:5 +0002: LOADI R67, 0 ; 1:10 +0003: LOADI R66, 259 ; 1:10 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -464,15 +452,14 @@ OUT i ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R0, 8 # 2:5 -0003: LOADRP R64, INTEGER, R0 # 3:24 -0004: UPCALL 0, R64 # 3:1, INCREMENT_REQUIRED_INT -0005: MOVE R65, R0 # 4:5 -0006: LOADI R64, 258 # 4:5 -0007: UPCALL 1, R64 # 4:1, OUT -0008: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R0, 8 ; 2:5 +0002: LOADRP R64, INTEGER, R0 ; 3:24 +0003: UPCALL 0, R64 ; 3:1, INCREMENT_REQUIRED_INT +0004: MOVE R65, R0 ; 4:5 +0005: LOADI R64, 258 ; 4:5 +0006: UPCALL 1, R64 ; 4:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -494,14 +481,13 @@ OUT i ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 8 # 1:5 -0002: LOADRP R65, INTEGER, R64 # 2:24 -0003: UPCALL 0, R65 # 2:1, INCREMENT_REQUIRED_INT -0004: MOVE R66, R64 # 3:5 -0005: LOADI R65, 258 # 3:5 -0006: UPCALL 1, R65 # 3:1, OUT -0007: EOF # 0:0 +0000: LOADI R64, 8 ; 1:5 +0001: LOADRP R65, INTEGER, R64 ; 2:24 +0002: UPCALL 0, R65 ; 2:1, INCREMENT_REQUIRED_INT +0003: MOVE R66, R64 ; 3:5 +0004: LOADI R65, 258 ; 3:5 +0005: UPCALL 1, R65 ; 3:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -540,20 +526,19 @@ OUT i ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:12 -0002: LOADRP R65, INTEGER, R64 # 1:12 -0003: UPCALL 0, R65 # 1:1, DEFINE_ARG -0004: MOVE R66, R64 # 2:5 -0005: LOADI R65, 258 # 2:5 -0006: UPCALL 1, R65 # 2:1, OUT -0007: LOADI R64, 1 # 3:5 -0008: LOADRP R65, INTEGER, R64 # 4:12 -0009: UPCALL 0, R65 # 4:1, DEFINE_ARG -0010: MOVE R66, R64 # 5:5 -0011: LOADI R65, 258 # 5:5 -0012: UPCALL 1, R65 # 5:1, OUT -0013: EOF # 0:0 +0000: LOADI R64, 0 ; 1:12 +0001: LOADRP R65, INTEGER, R64 ; 1:12 +0002: UPCALL 0, R65 ; 1:1, DEFINE_ARG +0003: MOVE R66, R64 ; 2:5 +0004: LOADI R65, 258 ; 2:5 +0005: UPCALL 1, R65 ; 2:1, OUT +0006: LOADI R64, 1 ; 3:5 +0007: LOADRP R65, INTEGER, R64 ; 4:12 +0008: UPCALL 0, R65 ; 4:1, DEFINE_ARG +0009: MOVE R66, R64 ; 5:5 +0010: LOADI R65, 258 ; 5:5 +0011: UPCALL 1, R65 ; 5:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -578,20 +563,19 @@ OUT t ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: ALLOC R64, STRING # 1:12 -0002: LOADRP R65, STRING, R64 # 1:12 -0003: UPCALL 0, R65 # 1:1, DEFINE_ARG -0004: MOVE R66, R64 # 2:5 -0005: LOADI R65, 259 # 2:5 -0006: UPCALL 1, R65 # 2:1, OUT -0007: LOADI R64, 0 # 3:5 -0008: LOADRP R65, STRING, R64 # 4:12 -0009: UPCALL 0, R65 # 4:1, DEFINE_ARG -0010: MOVE R66, R64 # 5:5 -0011: LOADI R65, 259 # 5:5 -0012: UPCALL 1, R65 # 5:1, OUT -0013: EOF # 0:0 +0000: ALLOC R64, STRING ; 1:12 +0001: LOADRP R65, STRING, R64 ; 1:12 +0002: UPCALL 0, R65 ; 1:1, DEFINE_ARG +0003: MOVE R66, R64 ; 2:5 +0004: LOADI R65, 259 ; 2:5 +0005: UPCALL 1, R65 ; 2:1, OUT +0006: LOADI R64, 0 ; 3:5 +0007: LOADRP R65, STRING, R64 ; 4:12 +0008: UPCALL 0, R65 ; 4:1, DEFINE_ARG +0009: MOVE R66, R64 ; 5:5 +0010: LOADI R65, 259 ; 5:5 +0011: UPCALL 1, R65 ; 5:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -616,48 +600,47 @@ OUT b, d, i, s ## Disassembly ```asm -0000: ENTER 12 # 0:0 -0001: LOADI R64, 0 # 1:24 -0002: LOADI R65, 0 # 1:28 -0003: LOADI R66, 0 # 1:32 -0004: ALLOC R67, STRING # 1:36 -0005: LOADRP R69, BOOLEAN, R64 # 1:24 -0006: LOADI R68, 544 # 1:24 -0007: LOADRP R71, DOUBLE, R65 # 1:28 -0008: LOADI R70, 544 # 1:28 -0009: LOADRP R73, INTEGER, R66 # 1:32 -0010: LOADI R72, 544 # 1:32 -0011: LOADRP R75, STRING, R67 # 1:36 -0012: LOADI R74, 512 # 1:36 -0013: UPCALL 0, R68 # 1:1, DEFINE_AND_CHANGE_ARGS -0014: MOVE R69, R64 # 2:5 -0015: LOADI R68, 288 # 2:5 -0016: MOVE R71, R65 # 2:8 -0017: LOADI R70, 289 # 2:8 -0018: MOVE R73, R66 # 2:11 -0019: LOADI R72, 290 # 2:11 -0020: MOVE R75, R67 # 2:14 -0021: LOADI R74, 259 # 2:14 -0022: UPCALL 1, R68 # 2:1, OUT -0023: LOADRP R69, BOOLEAN, R64 # 4:24 -0024: LOADI R68, 544 # 4:24 -0025: LOADRP R71, DOUBLE, R65 # 4:27 -0026: LOADI R70, 544 # 4:27 -0027: LOADRP R73, INTEGER, R66 # 4:30 -0028: LOADI R72, 544 # 4:30 -0029: LOADRP R75, STRING, R67 # 4:33 -0030: LOADI R74, 512 # 4:33 -0031: UPCALL 0, R68 # 4:1, DEFINE_AND_CHANGE_ARGS -0032: MOVE R69, R64 # 5:5 -0033: LOADI R68, 288 # 5:5 -0034: MOVE R71, R65 # 5:8 -0035: LOADI R70, 289 # 5:8 -0036: MOVE R73, R66 # 5:11 -0037: LOADI R72, 290 # 5:11 -0038: MOVE R75, R67 # 5:14 -0039: LOADI R74, 259 # 5:14 -0040: UPCALL 1, R68 # 5:1, OUT -0041: EOF # 0:0 +0000: LOADI R64, 0 ; 1:24 +0001: LOADI R65, 0 ; 1:28 +0002: LOADI R66, 0 ; 1:32 +0003: ALLOC R67, STRING ; 1:36 +0004: LOADRP R69, BOOLEAN, R64 ; 1:24 +0005: LOADI R68, 544 ; 1:24 +0006: LOADRP R71, DOUBLE, R65 ; 1:28 +0007: LOADI R70, 544 ; 1:28 +0008: LOADRP R73, INTEGER, R66 ; 1:32 +0009: LOADI R72, 544 ; 1:32 +0010: LOADRP R75, STRING, R67 ; 1:36 +0011: LOADI R74, 512 ; 1:36 +0012: UPCALL 0, R68 ; 1:1, DEFINE_AND_CHANGE_ARGS +0013: MOVE R69, R64 ; 2:5 +0014: LOADI R68, 288 ; 2:5 +0015: MOVE R71, R65 ; 2:8 +0016: LOADI R70, 289 ; 2:8 +0017: MOVE R73, R66 ; 2:11 +0018: LOADI R72, 290 ; 2:11 +0019: MOVE R75, R67 ; 2:14 +0020: LOADI R74, 259 ; 2:14 +0021: UPCALL 1, R68 ; 2:1, OUT +0022: LOADRP R69, BOOLEAN, R64 ; 4:24 +0023: LOADI R68, 544 ; 4:24 +0024: LOADRP R71, DOUBLE, R65 ; 4:27 +0025: LOADI R70, 544 ; 4:27 +0026: LOADRP R73, INTEGER, R66 ; 4:30 +0027: LOADI R72, 544 ; 4:30 +0028: LOADRP R75, STRING, R67 ; 4:33 +0029: LOADI R74, 512 ; 4:33 +0030: UPCALL 0, R68 ; 4:1, DEFINE_AND_CHANGE_ARGS +0031: MOVE R69, R64 ; 5:5 +0032: LOADI R68, 288 ; 5:5 +0033: MOVE R71, R65 ; 5:8 +0034: LOADI R70, 289 ; 5:8 +0035: MOVE R73, R66 ; 5:11 +0036: LOADI R72, 290 ; 5:11 +0037: MOVE R75, R67 ; 5:14 +0038: LOADI R74, 259 ; 5:14 +0039: UPCALL 1, R68 ; 5:1, OUT +0040: EOF ; 0:0 ``` ## Output @@ -752,15 +735,14 @@ OUT b ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:6 -0002: LOADRP R66, BOOLEAN, R64 # 2:24 -0003: LOADI R65, 512 # 2:24 -0004: UPCALL 0, R65 # 2:1, DEFINE_AND_CHANGE_ARGS -0005: MOVE R66, R64 # 3:5 -0006: LOADI R65, 256 # 3:5 -0007: UPCALL 1, R65 # 3:1, OUT -0008: EOF # 0:0 +0000: LOADI R64, 1 ; 1:6 +0001: LOADRP R66, BOOLEAN, R64 ; 2:24 +0002: LOADI R65, 512 ; 2:24 +0003: UPCALL 0, R65 ; 2:1, DEFINE_AND_CHANGE_ARGS +0004: MOVE R66, R64 ; 3:5 +0005: LOADI R65, 256 ; 3:5 +0006: UPCALL 1, R65 ; 3:1, OUT +0007: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_arithmetic_add.md b/core2/tests/test_arithmetic_add.md index 5a421ee6..b1f748e8 100644 --- a/core2/tests/test_arithmetic_add.md +++ b/core2/tests/test_arithmetic_add.md @@ -9,13 +9,12 @@ OUT 4.5 + 2.3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: ADDD R65, R65, R66 # 1:9 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: ADDD R65, R65, R66 ; 1:9 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2 + 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 3 # 1:9 -0003: ADDI R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 3 ; 1:9 +0002: ADDI R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,13 +59,12 @@ OUT "a" + "b" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:11 -0003: CONCAT R65, R65, R66 # 1:9 -0004: LOADI R64, 259 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:11 +0002: CONCAT R65, R65, R66 ; 1:9 +0003: LOADI R64, 259 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -87,14 +84,13 @@ OUT 2 + 8.3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: ADDD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: ADDD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -114,14 +110,13 @@ OUT 8.3 + 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 2 # 1:11 -0003: ITOD R66 # 1:11 -0004: ADDD R65, R65, R66 # 1:9 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 2 ; 1:11 +0002: ITOD R66 ; 1:11 +0003: ADDD R65, R65, R66 ; 1:9 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -141,11 +136,10 @@ a = 2147483640 + 20 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R64, 0 # 1:5 -0002: LOADI R65, 20 # 1:18 -0003: ADDI R64, R64, R65 # 1:16 -0004: EOF # 0:0 +0000: LOADC R64, 0 ; 1:5 +0001: LOADI R65, 20 ; 1:18 +0002: ADDI R64, R64, R65 ; 1:16 +0003: EOF ; 0:0 ``` ## Runtime errors @@ -169,29 +163,28 @@ OUT a(0) + a(1) + a(2) ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 10 # 2:8 -0004: LOADI R66, 0 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R65, 20 # 3:8 -0007: LOADI R66, 1 # 3:3 -0008: STOREA R64, R65, R66 # 3:1 -0009: LOADI R65, 30 # 4:8 -0010: LOADI R66, 2 # 4:3 -0011: STOREA R64, R65, R66 # 4:1 -0012: LOADI R67, 0 # 5:7 -0013: LOADA R66, R64, R67 # 5:5 -0014: LOADI R68, 1 # 5:14 -0015: LOADA R67, R64, R68 # 5:12 -0016: ADDI R66, R66, R67 # 5:10 -0017: LOADI R68, 2 # 5:21 -0018: LOADA R67, R64, R68 # 5:19 -0019: ADDI R66, R66, R67 # 5:17 -0020: LOADI R65, 258 # 5:5 -0021: UPCALL 0, R65 # 5:1, OUT -0022: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 10 ; 2:8 +0003: LOADI R66, 0 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R65, 20 ; 3:8 +0006: LOADI R66, 1 ; 3:3 +0007: STOREA R64, R65, R66 ; 3:1 +0008: LOADI R65, 30 ; 4:8 +0009: LOADI R66, 2 ; 4:3 +0010: STOREA R64, R65, R66 ; 4:1 +0011: LOADI R67, 0 ; 5:7 +0012: LOADA R66, R64, R67 ; 5:5 +0013: LOADI R68, 1 ; 5:14 +0014: LOADA R67, R64, R68 ; 5:12 +0015: ADDI R66, R66, R67 ; 5:10 +0016: LOADI R68, 2 ; 5:21 +0017: LOADA R67, R64, R68 ; 5:19 +0018: ADDI R66, R66, R67 ; 5:17 +0019: LOADI R65, 258 ; 5:5 +0020: UPCALL 0, R65 ; 5:1, OUT +0021: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_arithmetic_div.md b/core2/tests/test_arithmetic_div.md index 109863a1..dd0168ab 100644 --- a/core2/tests/test_arithmetic_div.md +++ b/core2/tests/test_arithmetic_div.md @@ -9,13 +9,12 @@ OUT 9.0 / 4.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: DIVD R65, R65, R66 # 1:9 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: DIVD R65, R65, R66 ; 1:9 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 10 / 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 10 # 1:5 -0002: LOADI R66, 3 # 1:10 -0003: DIVI R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 10 ; 1:5 +0001: LOADI R66, 3 ; 1:10 +0002: DIVI R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 3 / 1.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: DIVD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: DIVD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,14 +85,13 @@ OUT 9.0 / 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 3 # 1:11 -0003: ITOD R66 # 1:11 -0004: DIVD R65, R65, R66 # 1:9 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 3 ; 1:11 +0002: ITOD R66 ; 1:11 +0003: DIVD R65, R65, R66 ; 1:9 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -115,15 +111,14 @@ a = (-2147483647 - 1) / -1 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R64, 0 # 1:7 -0002: NEGI R64 # 1:6 -0003: LOADI R65, 1 # 1:20 -0004: SUBI R64, R64, R65 # 1:18 -0005: LOADI R65, 1 # 1:26 -0006: NEGI R65 # 1:25 -0007: DIVI R64, R64, R65 # 1:23 -0008: EOF # 0:0 +0000: LOADC R64, 0 ; 1:7 +0001: NEGI R64 ; 1:6 +0002: LOADI R65, 1 ; 1:20 +0003: SUBI R64, R64, R65 ; 1:18 +0004: LOADI R65, 1 ; 1:26 +0005: NEGI R65 ; 1:25 +0006: DIVI R64, R64, R65 ; 1:23 +0007: EOF ; 0:0 ``` ## Runtime errors @@ -143,11 +138,10 @@ a = 5 / 0 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 5 # 1:5 -0002: LOADI R65, 0 # 1:9 -0003: DIVI R64, R64, R65 # 1:7 -0004: EOF # 0:0 +0000: LOADI R64, 5 ; 1:5 +0001: LOADI R65, 0 ; 1:9 +0002: DIVI R64, R64, R65 ; 1:7 +0003: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_mod.md b/core2/tests/test_arithmetic_mod.md index 2d21aa7e..4865a739 100644 --- a/core2/tests/test_arithmetic_mod.md +++ b/core2/tests/test_arithmetic_mod.md @@ -9,13 +9,12 @@ OUT 10.0 MOD 3.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:14 -0003: MODD R65, R65, R66 # 1:10 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:14 +0002: MODD R65, R65, R66 ; 1:10 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 10 MOD 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 10 # 1:5 -0002: LOADI R66, 3 # 1:12 -0003: MODI R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 10 ; 1:5 +0001: LOADI R66, 3 ; 1:12 +0002: MODI R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 3 MOD 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADC R66, 0 # 1:11 -0003: ITOD R65 # 1:7 -0004: MODD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADC R66, 0 ; 1:11 +0002: ITOD R65 ; 1:7 +0003: MODD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,14 +85,13 @@ OUT 10.5 MOD 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 3 # 1:14 -0003: ITOD R66 # 1:14 -0004: MODD R65, R65, R66 # 1:10 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 3 ; 1:14 +0002: ITOD R66 ; 1:14 +0003: MODD R65, R65, R66 ; 1:10 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -115,15 +111,14 @@ a = (-2147483647 - 1) MOD -1 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R64, 0 # 1:7 -0002: NEGI R64 # 1:6 -0003: LOADI R65, 1 # 1:20 -0004: SUBI R64, R64, R65 # 1:18 -0005: LOADI R65, 1 # 1:28 -0006: NEGI R65 # 1:27 -0007: MODI R64, R64, R65 # 1:23 -0008: EOF # 0:0 +0000: LOADC R64, 0 ; 1:7 +0001: NEGI R64 ; 1:6 +0002: LOADI R65, 1 ; 1:20 +0003: SUBI R64, R64, R65 ; 1:18 +0004: LOADI R65, 1 ; 1:28 +0005: NEGI R65 ; 1:27 +0006: MODI R64, R64, R65 ; 1:23 +0007: EOF ; 0:0 ``` ## Runtime errors @@ -143,11 +138,10 @@ a = 5 MOD 0 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 5 # 1:5 -0002: LOADI R65, 0 # 1:11 -0003: MODI R64, R64, R65 # 1:7 -0004: EOF # 0:0 +0000: LOADI R64, 5 ; 1:5 +0001: LOADI R65, 0 ; 1:11 +0002: MODI R64, R64, R65 ; 1:7 +0003: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_mul.md b/core2/tests/test_arithmetic_mul.md index a900f9c7..93797fb4 100644 --- a/core2/tests/test_arithmetic_mul.md +++ b/core2/tests/test_arithmetic_mul.md @@ -9,13 +9,12 @@ OUT 4.0 * 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: MULD R65, R65, R66 # 1:9 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: MULD R65, R65, R66 ; 1:9 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 6 * 7 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 6 # 1:5 -0002: LOADI R66, 7 # 1:9 -0003: MULI R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 6 ; 1:5 +0001: LOADI R66, 7 ; 1:9 +0002: MULI R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 3 * 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: MULD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: MULD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,14 +85,13 @@ OUT 2.5 * 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 3 # 1:11 -0003: ITOD R66 # 1:11 -0004: MULD R65, R65, R66 # 1:9 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 3 ; 1:11 +0002: ITOD R66 ; 1:11 +0003: MULD R65, R65, R66 ; 1:9 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -115,11 +111,10 @@ a = 2147483640 * 10 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R64, 0 # 1:5 -0002: LOADI R65, 10 # 1:18 -0003: MULI R64, R64, R65 # 1:16 -0004: EOF # 0:0 +0000: LOADC R64, 0 ; 1:5 +0001: LOADI R65, 10 ; 1:18 +0002: MULI R64, R64, R65 ; 1:16 +0003: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_neg.md b/core2/tests/test_arithmetic_neg.md index db22ad02..32fc333a 100644 --- a/core2/tests/test_arithmetic_neg.md +++ b/core2/tests/test_arithmetic_neg.md @@ -9,12 +9,11 @@ OUT -3.5 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R65, 0 # 1:6 -0002: NEGD R65 # 1:5 -0003: LOADI R64, 257 # 1:5 -0004: UPCALL 0, R64 # 1:1, OUT -0005: EOF # 0:0 +0000: LOADC R65, 0 ; 1:6 +0001: NEGD R65 ; 1:5 +0002: LOADI R64, 257 ; 1:5 +0003: UPCALL 0, R64 ; 1:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -34,12 +33,11 @@ OUT -7 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 7 # 1:6 -0002: NEGI R65 # 1:5 -0003: LOADI R64, 258 # 1:5 -0004: UPCALL 0, R64 # 1:1, OUT -0005: EOF # 0:0 +0000: LOADI R65, 7 ; 1:6 +0001: NEGI R65 ; 1:5 +0002: LOADI R64, 258 ; 1:5 +0003: UPCALL 0, R64 ; 1:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -59,12 +57,11 @@ OUT -0 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 1:6 -0002: NEGI R65 # 1:5 -0003: LOADI R64, 258 # 1:5 -0004: UPCALL 0, R64 # 1:1, OUT -0005: EOF # 0:0 +0000: LOADI R65, 0 ; 1:6 +0001: NEGI R65 ; 1:5 +0002: LOADI R64, 258 ; 1:5 +0003: UPCALL 0, R64 ; 1:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -98,10 +95,9 @@ a = -(&x80000000) ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADC R64, 0 # 1:7 -0002: NEGI R64 # 1:5 -0003: EOF # 0:0 +0000: LOADC R64, 0 ; 1:7 +0001: NEGI R64 ; 1:5 +0002: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_pow.md b/core2/tests/test_arithmetic_pow.md index e6d7f05d..f88e4adc 100644 --- a/core2/tests/test_arithmetic_pow.md +++ b/core2/tests/test_arithmetic_pow.md @@ -9,13 +9,12 @@ OUT 2.0 ^ 3.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: POWD R65, R65, R66 # 1:9 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: POWD R65, R65, R66 ; 1:9 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2 ^ 8 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 8 # 1:9 -0003: POWI R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 8 ; 1:9 +0002: POWI R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 4 ^ 0.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 4 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: POWD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 4 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: POWD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,14 +85,13 @@ OUT 2.5 ^ 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 3 # 1:11 -0003: ITOD R66 # 1:11 -0004: POWD R65, R65, R66 # 1:9 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 3 ; 1:11 +0002: ITOD R66 ; 1:11 +0003: POWD R65, R65, R66 ; 1:9 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -115,11 +111,10 @@ a = 46341 ^ 2 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 46341 # 1:5 -0002: LOADI R65, 2 # 1:13 -0003: POWI R64, R64, R65 # 1:11 -0004: EOF # 0:0 +0000: LOADI R64, 46341 ; 1:5 +0001: LOADI R65, 2 ; 1:13 +0002: POWI R64, R64, R65 ; 1:11 +0003: EOF ; 0:0 ``` ## Runtime errors @@ -139,12 +134,11 @@ a = 2 ^ -1 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: LOADI R65, 1 # 1:10 -0003: NEGI R65 # 1:9 -0004: POWI R64, R64, R65 # 1:7 -0005: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: LOADI R65, 1 ; 1:10 +0002: NEGI R65 ; 1:9 +0003: POWI R64, R64, R65 ; 1:7 +0004: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arithmetic_sub.md b/core2/tests/test_arithmetic_sub.md index c8c12be4..a0b51e5d 100644 --- a/core2/tests/test_arithmetic_sub.md +++ b/core2/tests/test_arithmetic_sub.md @@ -9,13 +9,12 @@ OUT 5.0 - 3.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: SUBD R65, R65, R66 # 1:9 -0004: LOADI R64, 257 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: SUBD R65, R65, R66 ; 1:9 +0003: LOADI R64, 257 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 10 - 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 10 # 1:5 -0002: LOADI R66, 3 # 1:10 -0003: SUBI R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 10 ; 1:5 +0001: LOADI R66, 3 ; 1:10 +0002: SUBI R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 - 8.3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: SUBD R65, R65, R66 # 1:7 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: SUBD R65, R65, R66 ; 1:7 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,14 +85,13 @@ OUT 8.3 - 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADI R66, 2 # 1:11 -0003: ITOD R66 # 1:11 -0004: SUBD R65, R65, R66 # 1:9 -0005: LOADI R64, 257 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADI R66, 2 ; 1:11 +0002: ITOD R66 ; 1:11 +0003: SUBD R65, R65, R66 ; 1:9 +0004: LOADI R64, 257 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -115,12 +111,11 @@ a = -2147483640 - 20 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADC R64, 0 # 1:6 -0002: NEGI R64 # 1:5 -0003: LOADI R65, 20 # 1:19 -0004: SUBI R64, R64, R65 # 1:17 -0005: EOF # 0:0 +0000: LOADC R64, 0 ; 1:6 +0001: NEGI R64 ; 1:5 +0002: LOADI R65, 20 ; 1:19 +0003: SUBI R64, R64, R65 ; 1:17 +0004: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index 6a0f42b7..88f7de87 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -13,29 +13,28 @@ OUT a(0), a(1), a(2) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 10 # 2:8 -0004: LOADI R66, 0 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R65, 20 # 3:8 -0007: LOADI R66, 1 # 3:3 -0008: STOREA R64, R65, R66 # 3:1 -0009: LOADI R65, 30 # 4:8 -0010: LOADI R66, 2 # 4:3 -0011: STOREA R64, R65, R66 # 4:1 -0012: LOADI R67, 0 # 5:7 -0013: LOADA R66, R64, R67 # 5:5 -0014: LOADI R65, 290 # 5:5 -0015: LOADI R69, 1 # 5:13 -0016: LOADA R68, R64, R69 # 5:11 -0017: LOADI R67, 290 # 5:11 -0018: LOADI R71, 2 # 5:19 -0019: LOADA R70, R64, R71 # 5:17 -0020: LOADI R69, 258 # 5:17 -0021: UPCALL 0, R65 # 5:1, OUT -0022: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 10 ; 2:8 +0003: LOADI R66, 0 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R65, 20 ; 3:8 +0006: LOADI R66, 1 ; 3:3 +0007: STOREA R64, R65, R66 ; 3:1 +0008: LOADI R65, 30 ; 4:8 +0009: LOADI R66, 2 ; 4:3 +0010: STOREA R64, R65, R66 ; 4:1 +0011: LOADI R67, 0 ; 5:7 +0012: LOADA R66, R64, R67 ; 5:5 +0013: LOADI R65, 290 ; 5:5 +0014: LOADI R69, 1 ; 5:13 +0015: LOADA R68, R64, R69 ; 5:11 +0016: LOADI R67, 290 ; 5:11 +0017: LOADI R71, 2 ; 5:19 +0018: LOADA R70, R64, R71 ; 5:17 +0019: LOADI R69, 258 ; 5:17 +0020: UPCALL 0, R65 ; 5:1, OUT +0021: EOF ; 0:0 ``` ## Output @@ -62,52 +61,51 @@ OUT m(0, 0), m(0, 2), m(1, 1), m(1, 2) ## Disassembly ```asm -0000: ENTER 11 # 0:0 -0001: LOADI R65, 2 # 1:7 -0002: LOADI R66, 3 # 1:10 -0003: ALLOCA R64, [2]%, R65 # 1:5 -0004: LOADI R65, 1 # 2:11 -0005: LOADI R66, 0 # 2:3 -0006: LOADI R67, 0 # 2:6 -0007: STOREA R64, R65, R66 # 2:1 -0008: LOADI R65, 2 # 3:11 -0009: LOADI R66, 0 # 3:3 -0010: LOADI R67, 1 # 3:6 -0011: STOREA R64, R65, R66 # 3:1 -0012: LOADI R65, 3 # 4:11 -0013: LOADI R66, 0 # 4:3 -0014: LOADI R67, 2 # 4:6 -0015: STOREA R64, R65, R66 # 4:1 -0016: LOADI R65, 4 # 5:11 -0017: LOADI R66, 1 # 5:3 -0018: LOADI R67, 0 # 5:6 -0019: STOREA R64, R65, R66 # 5:1 -0020: LOADI R65, 5 # 6:11 -0021: LOADI R66, 1 # 6:3 -0022: LOADI R67, 1 # 6:6 -0023: STOREA R64, R65, R66 # 6:1 -0024: LOADI R65, 6 # 7:11 -0025: LOADI R66, 1 # 7:3 -0026: LOADI R67, 2 # 7:6 -0027: STOREA R64, R65, R66 # 7:1 -0028: LOADI R67, 0 # 8:7 -0029: LOADI R68, 0 # 8:10 -0030: LOADA R66, R64, R67 # 8:5 -0031: LOADI R65, 290 # 8:5 -0032: LOADI R69, 0 # 8:16 -0033: LOADI R70, 2 # 8:19 -0034: LOADA R68, R64, R69 # 8:14 -0035: LOADI R67, 290 # 8:14 -0036: LOADI R71, 1 # 8:25 -0037: LOADI R72, 1 # 8:28 -0038: LOADA R70, R64, R71 # 8:23 -0039: LOADI R69, 290 # 8:23 -0040: LOADI R73, 1 # 8:34 -0041: LOADI R74, 2 # 8:37 -0042: LOADA R72, R64, R73 # 8:32 -0043: LOADI R71, 258 # 8:32 -0044: UPCALL 0, R65 # 8:1, OUT -0045: EOF # 0:0 +0000: LOADI R65, 2 ; 1:7 +0001: LOADI R66, 3 ; 1:10 +0002: ALLOCA R64, [2]%, R65 ; 1:5 +0003: LOADI R65, 1 ; 2:11 +0004: LOADI R66, 0 ; 2:3 +0005: LOADI R67, 0 ; 2:6 +0006: STOREA R64, R65, R66 ; 2:1 +0007: LOADI R65, 2 ; 3:11 +0008: LOADI R66, 0 ; 3:3 +0009: LOADI R67, 1 ; 3:6 +0010: STOREA R64, R65, R66 ; 3:1 +0011: LOADI R65, 3 ; 4:11 +0012: LOADI R66, 0 ; 4:3 +0013: LOADI R67, 2 ; 4:6 +0014: STOREA R64, R65, R66 ; 4:1 +0015: LOADI R65, 4 ; 5:11 +0016: LOADI R66, 1 ; 5:3 +0017: LOADI R67, 0 ; 5:6 +0018: STOREA R64, R65, R66 ; 5:1 +0019: LOADI R65, 5 ; 6:11 +0020: LOADI R66, 1 ; 6:3 +0021: LOADI R67, 1 ; 6:6 +0022: STOREA R64, R65, R66 ; 6:1 +0023: LOADI R65, 6 ; 7:11 +0024: LOADI R66, 1 ; 7:3 +0025: LOADI R67, 2 ; 7:6 +0026: STOREA R64, R65, R66 ; 7:1 +0027: LOADI R67, 0 ; 8:7 +0028: LOADI R68, 0 ; 8:10 +0029: LOADA R66, R64, R67 ; 8:5 +0030: LOADI R65, 290 ; 8:5 +0031: LOADI R69, 0 ; 8:16 +0032: LOADI R70, 2 ; 8:19 +0033: LOADA R68, R64, R69 ; 8:14 +0034: LOADI R67, 290 ; 8:14 +0035: LOADI R71, 1 ; 8:25 +0036: LOADI R72, 1 ; 8:28 +0037: LOADA R70, R64, R71 ; 8:23 +0038: LOADI R69, 290 ; 8:23 +0039: LOADI R73, 1 ; 8:34 +0040: LOADI R74, 2 ; 8:37 +0041: LOADA R72, R64, R73 ; 8:32 +0042: LOADI R71, 258 ; 8:32 +0043: UPCALL 0, R65 ; 8:1, OUT +0044: EOF ; 0:0 ``` ## Output @@ -129,20 +127,19 @@ OUT flags(0), flags(1) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 2 # 1:11 -0002: ALLOCA R64, [1]?, R65 # 1:5 -0003: LOADI R65, 1 # 2:12 -0004: LOADI R66, 0 # 2:7 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R67, 0 # 3:11 -0007: LOADA R66, R64, R67 # 3:5 -0008: LOADI R65, 288 # 3:5 -0009: LOADI R69, 1 # 3:21 -0010: LOADA R68, R64, R69 # 3:15 -0011: LOADI R67, 256 # 3:15 -0012: UPCALL 0, R65 # 3:1, OUT -0013: EOF # 0:0 +0000: LOADI R65, 2 ; 1:11 +0001: ALLOCA R64, [1]?, R65 ; 1:5 +0002: LOADI R65, 1 ; 2:12 +0003: LOADI R66, 0 ; 2:7 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R67, 0 ; 3:11 +0006: LOADA R66, R64, R67 ; 3:5 +0007: LOADI R65, 288 ; 3:5 +0008: LOADI R69, 1 ; 3:21 +0009: LOADA R68, R64, R69 ; 3:15 +0010: LOADI R67, 256 ; 3:15 +0011: UPCALL 0, R65 ; 3:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -164,20 +161,19 @@ OUT d(0), d(1) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 2 # 1:7 -0002: ALLOCA R64, [1]#, R65 # 1:5 -0003: LOADC R65, 0 # 2:8 -0004: LOADI R66, 0 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R67, 0 # 3:7 -0007: LOADA R66, R64, R67 # 3:5 -0008: LOADI R65, 289 # 3:5 -0009: LOADI R69, 1 # 3:13 -0010: LOADA R68, R64, R69 # 3:11 -0011: LOADI R67, 257 # 3:11 -0012: UPCALL 0, R65 # 3:1, OUT -0013: EOF # 0:0 +0000: LOADI R65, 2 ; 1:7 +0001: ALLOCA R64, [1]#, R65 ; 1:5 +0002: LOADC R65, 0 ; 2:8 +0003: LOADI R66, 0 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R67, 0 ; 3:7 +0006: LOADA R66, R64, R67 ; 3:5 +0007: LOADI R65, 289 ; 3:5 +0008: LOADI R69, 1 ; 3:13 +0009: LOADA R68, R64, R69 ; 3:11 +0010: LOADI R67, 257 ; 3:11 +0011: UPCALL 0, R65 ; 3:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -200,26 +196,25 @@ OUT s(0), s(1), s(2) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]$, R65 # 1:5 -0003: LOADI R65, 0 # 2:8 -0004: LOADI R66, 0 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R65, 1 # 3:8 -0007: LOADI R66, 1 # 3:3 -0008: STOREA R64, R65, R66 # 3:1 -0009: LOADI R67, 0 # 4:7 -0010: LOADA R66, R64, R67 # 4:5 -0011: LOADI R65, 291 # 4:5 -0012: LOADI R69, 1 # 4:13 -0013: LOADA R68, R64, R69 # 4:11 -0014: LOADI R67, 291 # 4:11 -0015: LOADI R71, 2 # 4:19 -0016: LOADA R70, R64, R71 # 4:17 -0017: LOADI R69, 259 # 4:17 -0018: UPCALL 0, R65 # 4:1, OUT -0019: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]$, R65 ; 1:5 +0002: LOADI R65, 0 ; 2:8 +0003: LOADI R66, 0 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R65, 1 ; 3:8 +0006: LOADI R66, 1 ; 3:3 +0007: STOREA R64, R65, R66 ; 3:1 +0008: LOADI R67, 0 ; 4:7 +0009: LOADA R66, R64, R67 ; 4:5 +0010: LOADI R65, 291 ; 4:5 +0011: LOADI R69, 1 ; 4:13 +0012: LOADA R68, R64, R69 ; 4:11 +0013: LOADI R67, 291 ; 4:11 +0014: LOADI R71, 2 ; 4:19 +0015: LOADA R70, R64, R71 ; 4:17 +0016: LOADI R69, 259 ; 4:17 +0017: UPCALL 0, R65 ; 4:1, OUT +0018: EOF ; 0:0 ``` ## Output @@ -247,31 +242,29 @@ OUT a(0), a(1) ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 2 # 1:14 -0002: ALLOCA R0, [1]%, R64 # 1:12 -0003: JUMP 12 # 3:5 - --- FILL_ARRAY (BEGIN) -0004: ENTER 2 # 0:0 -0005: LOADI R64, 100 # 4:12 -0006: LOADI R65, 0 # 4:7 -0007: STOREA R0, R64, R65 # 4:5 -0008: LOADI R64, 200 # 5:12 -0009: LOADI R65, 1 # 5:7 -0010: STOREA R0, R64, R65 # 5:5 -0011: RETURN # 6:1 --- FILL_ARRAY (END) - -0012: CALL R64, 4 # 8:1, FILL_ARRAY -0013: LOADI R66, 0 # 9:7 -0014: LOADA R65, R0, R66 # 9:5 -0015: LOADI R64, 290 # 9:5 -0016: LOADI R68, 1 # 9:13 -0017: LOADA R67, R0, R68 # 9:11 -0018: LOADI R66, 258 # 9:11 -0019: UPCALL 0, R64 # 9:1, OUT -0020: EOF # 0:0 +0000: LOADI R64, 2 ; 1:14 +0001: ALLOCA R0, [1]%, R64 ; 1:12 +0002: JUMP 10 ; 3:5 + +;; FILL_ARRAY (BEGIN) +0003: LOADI R64, 100 ; 4:12 +0004: LOADI R65, 0 ; 4:7 +0005: STOREA R0, R64, R65 ; 4:5 +0006: LOADI R64, 200 ; 5:12 +0007: LOADI R65, 1 ; 5:7 +0008: STOREA R0, R64, R65 ; 5:5 +0009: RETURN ; 6:1 +;; FILL_ARRAY (END) + +0010: CALL R64, 3 ; 8:1, FILL_ARRAY +0011: LOADI R66, 0 ; 9:7 +0012: LOADA R65, R0, R66 ; 9:5 +0013: LOADI R64, 290 ; 9:5 +0014: LOADI R68, 1 ; 9:13 +0015: LOADA R67, R0, R68 ; 9:11 +0016: LOADI R66, 258 ; 9:11 +0017: UPCALL 0, R64 ; 9:1, OUT +0018: EOF ; 0:0 ``` ## Output @@ -292,14 +285,13 @@ OUT a%(1) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R67, 1 # 2:8 -0004: LOADA R66, R64, R67 # 2:5 -0005: LOADI R65, 258 # 2:5 -0006: UPCALL 0, R65 # 2:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R67, 1 ; 2:8 +0003: LOADA R66, R64, R67 ; 2:5 +0004: LOADI R65, 258 ; 2:5 +0005: UPCALL 0, R65 ; 2:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -336,18 +328,17 @@ OUT a(1.9) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 4 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 99 # 2:8 -0004: LOADI R66, 2 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADC R67, 0 # 3:7 -0007: DTOI R67 # 3:7 -0008: LOADA R66, R64, R67 # 3:5 -0009: LOADI R65, 258 # 3:5 -0010: UPCALL 0, R65 # 3:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 4 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 99 ; 2:8 +0003: LOADI R66, 2 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADC R67, 0 ; 3:7 +0006: DTOI R67 ; 3:7 +0007: LOADA R66, R64, R67 ; 3:5 +0008: LOADI R65, 258 ; 3:5 +0009: UPCALL 0, R65 ; 3:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -440,12 +431,11 @@ DIM a(1, 0, 1) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 1 # 1:7 -0002: LOADI R66, 0 # 1:10 -0003: LOADI R67, 1 # 1:13 -0004: ALLOCA R64, [3]%, R65 # 1:5 -0005: EOF # 0:0 +0000: LOADI R65, 1 ; 1:7 +0001: LOADI R66, 0 ; 1:10 +0002: LOADI R67, 1 ; 1:13 +0003: ALLOCA R64, [3]%, R65 ; 1:5 +0004: EOF ; 0:0 ``` ## Runtime errors @@ -465,13 +455,12 @@ DIM a(1, -5, 1) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 1 # 1:7 -0002: LOADI R66, 5 # 1:11 -0003: NEGI R66 # 1:10 -0004: LOADI R67, 1 # 1:14 -0005: ALLOCA R64, [3]%, R65 # 1:5 -0006: EOF # 0:0 +0000: LOADI R65, 1 ; 1:7 +0001: LOADI R66, 5 ; 1:11 +0002: NEGI R66 ; 1:10 +0003: LOADI R67, 1 ; 1:14 +0004: ALLOCA R64, [3]%, R65 ; 1:5 +0005: EOF ; 0:0 ``` ## Runtime errors @@ -492,13 +481,12 @@ a(5) = 10 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 10 # 2:8 -0004: LOADI R66, 5 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 10 ; 2:8 +0003: LOADI R66, 5 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: EOF ; 0:0 ``` ## Runtime errors @@ -519,14 +507,13 @@ a(-5) = 10 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 10 # 2:9 -0004: LOADI R66, 5 # 2:4 -0005: NEGI R66 # 2:3 -0006: STOREA R64, R65, R66 # 2:1 -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 10 ; 2:9 +0003: LOADI R66, 5 ; 2:4 +0004: NEGI R66 ; 2:3 +0005: STOREA R64, R65, R66 ; 2:1 +0006: EOF ; 0:0 ``` ## Runtime errors @@ -547,14 +534,13 @@ OUT a(3) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R67, 3 # 2:7 -0004: LOADA R66, R64, R67 # 2:5 -0005: LOADI R65, 258 # 2:5 -0006: UPCALL 0, R65 # 2:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R67, 3 ; 2:7 +0003: LOADA R66, R64, R67 ; 2:5 +0004: LOADI R65, 258 ; 2:5 +0005: UPCALL 0, R65 ; 2:1, OUT +0006: EOF ; 0:0 ``` ## Runtime errors @@ -575,15 +561,14 @@ a(10, 50) = 123 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R65, 5 # 1:7 -0002: LOADI R66, 100 # 1:10 -0003: ALLOCA R64, [2]%, R65 # 1:5 -0004: LOADI R65, 123 # 2:13 -0005: LOADI R66, 10 # 2:3 -0006: LOADI R67, 50 # 2:7 -0007: STOREA R64, R65, R66 # 2:1 -0008: EOF # 0:0 +0000: LOADI R65, 5 ; 1:7 +0001: LOADI R66, 100 ; 1:10 +0002: ALLOCA R64, [2]%, R65 ; 1:5 +0003: LOADI R65, 123 ; 2:13 +0004: LOADI R66, 10 ; 2:3 +0005: LOADI R67, 50 ; 2:7 +0006: STOREA R64, R65, R66 ; 2:1 +0007: EOF ; 0:0 ``` ## Runtime errors @@ -610,47 +595,46 @@ OUT s(0), s(1) ## Disassembly ```asm -0000: ENTER 11 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R66, 2 # 2:7 -0004: ALLOCA R65, [1]?, R66 # 2:5 -0005: LOADI R67, 2 # 3:7 -0006: ALLOCA R66, [1]#, R67 # 3:5 -0007: LOADI R68, 2 # 4:7 -0008: ALLOCA R67, [1]$, R68 # 4:5 -0009: LOADI R70, 0 # 5:7 -0010: LOADA R69, R64, R70 # 5:5 -0011: LOADI R68, 290 # 5:5 -0012: LOADI R72, 1 # 5:13 -0013: LOADA R71, R64, R72 # 5:11 -0014: LOADI R70, 290 # 5:11 -0015: LOADI R74, 2 # 5:19 -0016: LOADA R73, R64, R74 # 5:17 -0017: LOADI R72, 258 # 5:17 -0018: UPCALL 0, R68 # 5:1, OUT -0019: LOADI R70, 0 # 6:7 -0020: LOADA R69, R65, R70 # 6:5 -0021: LOADI R68, 288 # 6:5 -0022: LOADI R72, 1 # 6:13 -0023: LOADA R71, R65, R72 # 6:11 -0024: LOADI R70, 256 # 6:11 -0025: UPCALL 0, R68 # 6:1, OUT -0026: LOADI R70, 0 # 7:7 -0027: LOADA R69, R66, R70 # 7:5 -0028: LOADI R68, 289 # 7:5 -0029: LOADI R72, 1 # 7:13 -0030: LOADA R71, R66, R72 # 7:11 -0031: LOADI R70, 257 # 7:11 -0032: UPCALL 0, R68 # 7:1, OUT -0033: LOADI R70, 0 # 8:7 -0034: LOADA R69, R67, R70 # 8:5 -0035: LOADI R68, 291 # 8:5 -0036: LOADI R72, 1 # 8:13 -0037: LOADA R71, R67, R72 # 8:11 -0038: LOADI R70, 259 # 8:11 -0039: UPCALL 0, R68 # 8:1, OUT -0040: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R66, 2 ; 2:7 +0003: ALLOCA R65, [1]?, R66 ; 2:5 +0004: LOADI R67, 2 ; 3:7 +0005: ALLOCA R66, [1]#, R67 ; 3:5 +0006: LOADI R68, 2 ; 4:7 +0007: ALLOCA R67, [1]$, R68 ; 4:5 +0008: LOADI R70, 0 ; 5:7 +0009: LOADA R69, R64, R70 ; 5:5 +0010: LOADI R68, 290 ; 5:5 +0011: LOADI R72, 1 ; 5:13 +0012: LOADA R71, R64, R72 ; 5:11 +0013: LOADI R70, 290 ; 5:11 +0014: LOADI R74, 2 ; 5:19 +0015: LOADA R73, R64, R74 ; 5:17 +0016: LOADI R72, 258 ; 5:17 +0017: UPCALL 0, R68 ; 5:1, OUT +0018: LOADI R70, 0 ; 6:7 +0019: LOADA R69, R65, R70 ; 6:5 +0020: LOADI R68, 288 ; 6:5 +0021: LOADI R72, 1 ; 6:13 +0022: LOADA R71, R65, R72 ; 6:11 +0023: LOADI R70, 256 ; 6:11 +0024: UPCALL 0, R68 ; 6:1, OUT +0025: LOADI R70, 0 ; 7:7 +0026: LOADA R69, R66, R70 ; 7:5 +0027: LOADI R68, 289 ; 7:5 +0028: LOADI R72, 1 ; 7:13 +0029: LOADA R71, R66, R72 ; 7:11 +0030: LOADI R70, 257 ; 7:11 +0031: UPCALL 0, R68 ; 7:1, OUT +0032: LOADI R70, 0 ; 8:7 +0033: LOADA R69, R67, R70 ; 8:5 +0034: LOADI R68, 291 ; 8:5 +0035: LOADI R72, 1 ; 8:13 +0036: LOADA R71, R67, R72 ; 8:11 +0037: LOADI R70, 259 ; 8:11 +0038: UPCALL 0, R68 ; 8:1, OUT +0039: EOF ; 0:0 ``` ## Output @@ -677,25 +661,24 @@ OUT x(0), y(0) ## Disassembly ```asm -0000: ENTER 7 # 0:0 -0001: LOADI R65, 2 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R66, 2 # 2:7 -0004: ALLOCA R65, [1]%, R66 # 2:5 -0005: LOADI R66, 1 # 3:8 -0006: LOADI R67, 0 # 3:3 -0007: STOREA R64, R66, R67 # 3:1 -0008: LOADI R66, 2 # 4:8 -0009: LOADI R67, 0 # 4:3 -0010: STOREA R65, R66, R67 # 4:1 -0011: LOADI R68, 0 # 5:7 -0012: LOADA R67, R64, R68 # 5:5 -0013: LOADI R66, 290 # 5:5 -0014: LOADI R70, 0 # 5:13 -0015: LOADA R69, R65, R70 # 5:11 -0016: LOADI R68, 258 # 5:11 -0017: UPCALL 0, R66 # 5:1, OUT -0018: EOF # 0:0 +0000: LOADI R65, 2 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R66, 2 ; 2:7 +0003: ALLOCA R65, [1]%, R66 ; 2:5 +0004: LOADI R66, 1 ; 3:8 +0005: LOADI R67, 0 ; 3:3 +0006: STOREA R64, R66, R67 ; 3:1 +0007: LOADI R66, 2 ; 4:8 +0008: LOADI R67, 0 ; 4:3 +0009: STOREA R65, R66, R67 ; 4:1 +0010: LOADI R68, 0 ; 5:7 +0011: LOADA R67, R64, R68 ; 5:5 +0012: LOADI R66, 290 ; 5:5 +0013: LOADI R70, 0 ; 5:13 +0014: LOADA R69, R65, R70 ; 5:11 +0015: LOADI R68, 258 ; 5:11 +0016: UPCALL 0, R66 ; 5:1, OUT +0017: EOF ; 0:0 ``` ## Output @@ -723,40 +706,38 @@ OUT sum(0) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: JUMP 24 # 1:10 - --- SUM (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 5 # 0:0 -0004: LOADI R67, 3 # 2:11 -0005: ALLOCA R66, [1]%, R67 # 2:9 -0006: LOADI R67, 10 # 3:12 -0007: LOADI R68, 0 # 3:7 -0008: STOREA R66, R67, R68 # 3:5 -0009: LOADI R67, 20 # 4:12 -0010: LOADI R68, 1 # 4:7 -0011: STOREA R66, R67, R68 # 4:5 -0012: LOADI R67, 30 # 5:12 -0013: LOADI R68, 2 # 5:7 -0014: STOREA R66, R67, R68 # 5:5 -0015: LOADI R67, 0 # 6:13 -0016: LOADA R64, R66, R67 # 6:11 -0017: LOADI R68, 1 # 6:20 -0018: LOADA R67, R66, R68 # 6:18 -0019: ADDI R64, R64, R67 # 6:16 -0020: LOADI R68, 2 # 6:27 -0021: LOADA R67, R66, R68 # 6:25 -0022: ADDI R64, R64, R67 # 6:23 -0023: RETURN # 7:1 --- SUM (END) - -0024: LOADI R67, 0 # 9:9 -0025: CALL R66, 2 # 9:5, SUM -0026: MOVE R65, R66 # 9:5 -0027: LOADI R64, 258 # 9:5 -0028: UPCALL 0, R64 # 9:1, OUT -0029: EOF # 0:0 +0000: JUMP 22 ; 1:10 + +;; SUM (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R67, 3 ; 2:11 +0003: ALLOCA R66, [1]%, R67 ; 2:9 +0004: LOADI R67, 10 ; 3:12 +0005: LOADI R68, 0 ; 3:7 +0006: STOREA R66, R67, R68 ; 3:5 +0007: LOADI R67, 20 ; 4:12 +0008: LOADI R68, 1 ; 4:7 +0009: STOREA R66, R67, R68 ; 4:5 +0010: LOADI R67, 30 ; 5:12 +0011: LOADI R68, 2 ; 5:7 +0012: STOREA R66, R67, R68 ; 5:5 +0013: LOADI R67, 0 ; 6:13 +0014: LOADA R64, R66, R67 ; 6:11 +0015: LOADI R68, 1 ; 6:20 +0016: LOADA R67, R66, R68 ; 6:18 +0017: ADDI R64, R64, R67 ; 6:16 +0018: LOADI R68, 2 ; 6:27 +0019: LOADA R67, R66, R68 ; 6:25 +0020: ADDI R64, R64, R67 ; 6:23 +0021: RETURN ; 7:1 +;; SUM (END) + +0022: LOADI R67, 0 ; 9:9 +0023: CALL R66, 1 ; 9:5, SUM +0024: MOVE R65, R66 ; 9:5 +0025: LOADI R64, 258 ; 9:5 +0026: UPCALL 0, R64 ; 9:1, OUT +0027: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_assignments.md b/core2/tests/test_assignments.md index 46b0d970..dcbdcd1b 100644 --- a/core2/tests/test_assignments.md +++ b/core2/tests/test_assignments.md @@ -12,13 +12,12 @@ OUT a ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:5 -0002: LOADI R64, 2 # 2:5 -0003: MOVE R66, R64 # 4:5 -0004: LOADI R65, 258 # 4:5 -0005: UPCALL 0, R65 # 4:1, OUT -0006: EOF # 0:0 +0000: LOADI R64, 1 ; 1:5 +0001: LOADI R64, 2 ; 2:5 +0002: MOVE R66, R64 ; 4:5 +0003: LOADI R65, 258 ; 4:5 +0004: UPCALL 0, R65 ; 4:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -42,16 +41,15 @@ OUT a ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:6 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 259 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R64, 1 # 4:5 -0006: MOVE R66, R64 # 5:5 -0007: LOADI R65, 259 # 5:5 -0008: UPCALL 0, R65 # 5:1, OUT -0009: EOF # 0:0 +0000: LOADI R64, 0 ; 1:6 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 259 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: LOADI R64, 1 ; 4:5 +0005: MOVE R66, R64 ; 5:5 +0006: LOADI R65, 259 ; 5:5 +0007: UPCALL 0, R65 ; 5:1, OUT +0008: EOF ; 0:0 ``` ## Output @@ -76,16 +74,15 @@ OUT a ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 259 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R64, 1 # 4:6 -0006: MOVE R66, R64 # 5:5 -0007: LOADI R65, 259 # 5:5 -0008: UPCALL 0, R65 # 5:1, OUT -0009: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 259 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: LOADI R64, 1 ; 4:6 +0005: MOVE R66, R64 ; 5:5 +0006: LOADI R65, 259 ; 5:5 +0007: UPCALL 0, R65 ; 5:1, OUT +0008: EOF ; 0:0 ``` ## Output @@ -151,13 +148,12 @@ OUT d ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 6 # 1:6 -0002: ITOD R64 # 1:6 -0003: MOVE R66, R64 # 2:5 -0004: LOADI R65, 257 # 2:5 -0005: UPCALL 0, R65 # 2:1, OUT -0006: EOF # 0:0 +0000: LOADI R64, 6 ; 1:6 +0001: ITOD R64 ; 1:6 +0002: MOVE R66, R64 ; 2:5 +0003: LOADI R65, 257 ; 2:5 +0004: UPCALL 0, R65 ; 2:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -183,19 +179,18 @@ OUT i1, i2 ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 0 # 1:6 -0002: LOADC R64, 0 # 2:6 -0003: DTOI R64 # 2:6 -0004: LOADI R65, 0 # 4:6 -0005: LOADC R65, 1 # 5:6 -0006: DTOI R65 # 5:6 -0007: MOVE R67, R64 # 7:5 -0008: LOADI R66, 290 # 7:5 -0009: MOVE R69, R65 # 7:9 -0010: LOADI R68, 258 # 7:9 -0011: UPCALL 0, R66 # 7:1, OUT -0012: EOF # 0:0 +0000: LOADI R64, 0 ; 1:6 +0001: LOADC R64, 0 ; 2:6 +0002: DTOI R64 ; 2:6 +0003: LOADI R65, 0 ; 4:6 +0004: LOADC R65, 1 ; 5:6 +0005: DTOI R65 ; 5:6 +0006: MOVE R67, R64 ; 7:5 +0007: LOADI R66, 290 ; 7:5 +0008: MOVE R69, R65 ; 7:9 +0009: LOADI R68, 258 ; 7:9 +0010: UPCALL 0, R66 ; 7:1, OUT +0011: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_and.md b/core2/tests/test_bitwise_and.md index a70e5c5f..9421ea48 100644 --- a/core2/tests/test_bitwise_and.md +++ b/core2/tests/test_bitwise_and.md @@ -9,13 +9,12 @@ OUT 12 AND 10 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 12 # 1:5 -0002: LOADI R66, 10 # 1:12 -0003: AND R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 12 ; 1:5 +0001: LOADI R66, 10 ; 1:12 +0002: AND R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -64,18 +63,17 @@ OUT TRUE AND TRUE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 0 # 1:14 -0003: AND R65, R65, R66 # 1:10 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R65, 1 # 2:5 -0007: LOADI R66, 1 # 2:14 -0008: AND R65, R65, R66 # 2:10 -0009: LOADI R64, 256 # 2:5 -0010: UPCALL 0, R64 # 2:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 0 ; 1:14 +0002: AND R65, R65, R66 ; 1:10 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: LOADI R65, 1 ; 2:5 +0006: LOADI R66, 1 ; 2:14 +0007: AND R65, R65, R66 ; 2:10 +0008: LOADI R64, 256 ; 2:5 +0009: UPCALL 0, R64 ; 2:1, OUT +0010: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_not.md b/core2/tests/test_bitwise_not.md index 59aca816..1f090043 100644 --- a/core2/tests/test_bitwise_not.md +++ b/core2/tests/test_bitwise_not.md @@ -9,12 +9,11 @@ OUT NOT 12 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 12 # 1:9 -0002: NOT R65 # 1:5 -0003: LOADI R64, 258 # 1:5 -0004: UPCALL 0, R64 # 1:1, OUT -0005: EOF # 0:0 +0000: LOADI R65, 12 ; 1:9 +0001: NOT R65 ; 1:5 +0002: LOADI R64, 258 ; 1:5 +0003: UPCALL 0, R64 ; 1:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -63,18 +62,17 @@ OUT NOT FALSE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:9 -0002: LOADI R66, 1 # 1:5 -0003: XOR R65, R65, R66 # 1:5 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R65, 0 # 2:9 -0007: LOADI R66, 1 # 2:5 -0008: XOR R65, R65, R66 # 2:5 -0009: LOADI R64, 256 # 2:5 -0010: UPCALL 0, R64 # 2:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 1 ; 1:9 +0001: LOADI R66, 1 ; 1:5 +0002: XOR R65, R65, R66 ; 1:5 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: LOADI R65, 0 ; 2:9 +0006: LOADI R66, 1 ; 2:5 +0007: XOR R65, R65, R66 ; 2:5 +0008: LOADI R64, 256 ; 2:5 +0009: UPCALL 0, R64 ; 2:1, OUT +0010: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_or.md b/core2/tests/test_bitwise_or.md index 15ef2ce6..76265bef 100644 --- a/core2/tests/test_bitwise_or.md +++ b/core2/tests/test_bitwise_or.md @@ -9,13 +9,12 @@ OUT 12 OR 10 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 12 # 1:5 -0002: LOADI R66, 10 # 1:11 -0003: OR R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 12 ; 1:5 +0001: LOADI R66, 10 ; 1:11 +0002: OR R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -64,18 +63,17 @@ OUT FALSE OR FALSE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 0 # 1:13 -0003: OR R65, R65, R66 # 1:10 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R65, 0 # 2:5 -0007: LOADI R66, 0 # 2:14 -0008: OR R65, R65, R66 # 2:11 -0009: LOADI R64, 256 # 2:5 -0010: UPCALL 0, R64 # 2:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 0 ; 1:13 +0002: OR R65, R65, R66 ; 1:10 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: LOADI R65, 0 ; 2:5 +0006: LOADI R66, 0 ; 2:14 +0007: OR R65, R65, R66 ; 2:11 +0008: LOADI R64, 256 ; 2:5 +0009: UPCALL 0, R64 ; 2:1, OUT +0010: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_bitwise_shl.md b/core2/tests/test_bitwise_shl.md index 86f8c262..bea77a52 100644 --- a/core2/tests/test_bitwise_shl.md +++ b/core2/tests/test_bitwise_shl.md @@ -9,13 +9,12 @@ OUT 3 << 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADI R66, 2 # 1:10 -0003: SHL R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADI R66, 2 ; 1:10 +0002: SHL R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 7 << 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 7 # 1:5 -0002: LOADI R66, 0 # 1:10 -0003: SHL R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 7 ; 1:5 +0001: LOADI R66, 0 ; 1:10 +0002: SHL R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,13 +59,12 @@ OUT 1 << 32 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 32 # 1:10 -0003: SHL R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 32 ; 1:10 +0002: SHL R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -101,14 +98,13 @@ OUT 1 << -1 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 1 # 1:11 -0003: NEGI R66 # 1:10 -0004: SHL R65, R65, R66 # 1:7 -0005: LOADI R64, 258 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 1 ; 1:11 +0002: NEGI R66 ; 1:10 +0003: SHL R65, R65, R66 ; 1:7 +0004: LOADI R64, 258 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_bitwise_shr.md b/core2/tests/test_bitwise_shr.md index 7de7d9bf..e2ddb632 100644 --- a/core2/tests/test_bitwise_shr.md +++ b/core2/tests/test_bitwise_shr.md @@ -9,13 +9,12 @@ OUT 12 >> 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 12 # 1:5 -0002: LOADI R66, 2 # 1:11 -0003: SHR R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 12 ; 1:5 +0001: LOADI R66, 2 ; 1:11 +0002: SHR R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 7 >> 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 7 # 1:5 -0002: LOADI R66, 0 # 1:10 -0003: SHR R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 7 ; 1:5 +0001: LOADI R66, 0 ; 1:10 +0002: SHR R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,13 +59,12 @@ OUT 1 >> 32 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 32 # 1:10 -0003: SHR R65, R65, R66 # 1:7 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 32 ; 1:10 +0002: SHR R65, R65, R66 ; 1:7 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -87,14 +84,13 @@ OUT -1 >> 32 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:6 -0002: NEGI R65 # 1:5 -0003: LOADI R66, 32 # 1:11 -0004: SHR R65, R65, R66 # 1:8 -0005: LOADI R64, 258 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 1 ; 1:6 +0001: NEGI R65 ; 1:5 +0002: LOADI R66, 32 ; 1:11 +0003: SHR R65, R65, R66 ; 1:8 +0004: LOADI R64, 258 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -114,14 +110,13 @@ OUT -8 >> 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 8 # 1:6 -0002: NEGI R65 # 1:5 -0003: LOADI R66, 2 # 1:11 -0004: SHR R65, R65, R66 # 1:8 -0005: LOADI R64, 258 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 8 ; 1:6 +0001: NEGI R65 ; 1:5 +0002: LOADI R66, 2 ; 1:11 +0003: SHR R65, R65, R66 ; 1:8 +0004: LOADI R64, 258 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -155,14 +150,13 @@ OUT 1 >> -1 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 1 # 1:11 -0003: NEGI R66 # 1:10 -0004: SHR R65, R65, R66 # 1:7 -0005: LOADI R64, 258 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 1 ; 1:11 +0002: NEGI R66 ; 1:10 +0003: SHR R65, R65, R66 ; 1:7 +0004: LOADI R64, 258 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_bitwise_xor.md b/core2/tests/test_bitwise_xor.md index 9eec36ae..77143a81 100644 --- a/core2/tests/test_bitwise_xor.md +++ b/core2/tests/test_bitwise_xor.md @@ -9,13 +9,12 @@ OUT 12 XOR 10 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 12 # 1:5 -0002: LOADI R66, 10 # 1:12 -0003: XOR R65, R65, R66 # 1:8 -0004: LOADI R64, 258 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 12 ; 1:5 +0001: LOADI R66, 10 ; 1:12 +0002: XOR R65, R65, R66 ; 1:8 +0003: LOADI R64, 258 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -64,18 +63,17 @@ OUT TRUE XOR TRUE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 0 # 1:14 -0003: XOR R65, R65, R66 # 1:10 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: LOADI R65, 1 # 2:5 -0007: LOADI R66, 1 # 2:14 -0008: XOR R65, R65, R66 # 2:10 -0009: LOADI R64, 256 # 2:5 -0010: UPCALL 0, R64 # 2:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 0 ; 1:14 +0002: XOR R65, R65, R66 ; 1:10 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: LOADI R65, 1 ; 2:5 +0006: LOADI R66, 1 ; 2:14 +0007: XOR R65, R65, R66 ; 2:10 +0008: LOADI R64, 256 ; 2:5 +0009: UPCALL 0, R64 ; 2:1, OUT +0010: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_case_insensitivity.md b/core2/tests/test_case_insensitivity.md index 11a06dea..a903c910 100644 --- a/core2/tests/test_case_insensitivity.md +++ b/core2/tests/test_case_insensitivity.md @@ -12,16 +12,15 @@ OUT A ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:5 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 258 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: LOADI R64, 2 # 3:5 -0006: MOVE R66, R64 # 4:5 -0007: LOADI R65, 258 # 4:5 -0008: UPCALL 0, R65 # 4:1, OUT -0009: EOF # 0:0 +0000: LOADI R64, 1 ; 1:5 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 258 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: LOADI R64, 2 ; 3:5 +0005: MOVE R66, R64 ; 4:5 +0006: LOADI R65, 258 ; 4:5 +0007: UPCALL 0, R65 ; 4:1, OUT +0008: EOF ; 0:0 ``` ## Output @@ -45,23 +44,22 @@ OUT A(0), a(1) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R65, 3 # 1:7 -0002: ALLOCA R64, [1]%, R65 # 1:5 -0003: LOADI R65, 10 # 2:8 -0004: LOADI R66, 0 # 2:3 -0005: STOREA R64, R65, R66 # 2:1 -0006: LOADI R65, 20 # 3:8 -0007: LOADI R66, 1 # 3:3 -0008: STOREA R64, R65, R66 # 3:1 -0009: LOADI R67, 0 # 4:7 -0010: LOADA R66, R64, R67 # 4:5 -0011: LOADI R65, 290 # 4:5 -0012: LOADI R69, 1 # 4:13 -0013: LOADA R68, R64, R69 # 4:11 -0014: LOADI R67, 258 # 4:11 -0015: UPCALL 0, R65 # 4:1, OUT -0016: EOF # 0:0 +0000: LOADI R65, 3 ; 1:7 +0001: ALLOCA R64, [1]%, R65 ; 1:5 +0002: LOADI R65, 10 ; 2:8 +0003: LOADI R66, 0 ; 2:3 +0004: STOREA R64, R65, R66 ; 2:1 +0005: LOADI R65, 20 ; 3:8 +0006: LOADI R66, 1 ; 3:3 +0007: STOREA R64, R65, R66 ; 3:1 +0008: LOADI R67, 0 ; 4:7 +0009: LOADA R66, R64, R67 ; 4:5 +0010: LOADI R65, 290 ; 4:5 +0011: LOADI R69, 1 ; 4:13 +0012: LOADA R68, R64, R69 ; 4:11 +0013: LOADI R67, 258 ; 4:11 +0014: UPCALL 0, R65 ; 4:1, OUT +0015: EOF ; 0:0 ``` ## Output @@ -114,16 +112,15 @@ OUT A, a ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R0, 1 # 2:5 -0003: LOADI R0, 2 # 3:5 -0004: MOVE R65, R0 # 4:5 -0005: LOADI R64, 290 # 4:5 -0006: MOVE R67, R0 # 4:8 -0007: LOADI R66, 258 # 4:8 -0008: UPCALL 0, R64 # 4:1, OUT -0009: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R0, 1 ; 2:5 +0002: LOADI R0, 2 ; 3:5 +0003: MOVE R65, R0 ; 4:5 +0004: LOADI R64, 290 ; 4:5 +0005: MOVE R67, R0 ; 4:8 +0006: LOADI R66, 258 ; 4:8 +0007: UPCALL 0, R64 ; 4:1, OUT +0008: EOF ; 0:0 ``` ## Output @@ -147,20 +144,18 @@ OUT FOO ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 6 # 1:10 - --- FOO (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 1 # 0:0 -0004: LOADI R64, 42 # 2:11 -0005: RETURN # 3:1 --- FOO (END) - -0006: CALL R65, 2 # 5:5, FOO -0007: LOADI R64, 258 # 5:5 -0008: UPCALL 0, R64 # 5:1, OUT -0009: EOF # 0:0 +0000: JUMP 4 ; 1:10 + +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 42 ; 2:11 +0003: RETURN ; 3:1 +;; FOO (END) + +0004: CALL R65, 1 ; 5:5, FOO +0005: LOADI R64, 258 ; 5:5 +0006: UPCALL 0, R64 ; 5:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -184,19 +179,17 @@ FOO ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: JUMP 7 # 1:5 - --- FOO (BEGIN) -0002: ENTER 2 # 0:0 -0003: LOADI R65, 0 # 2:9 -0004: LOADI R64, 259 # 2:9 -0005: UPCALL 0, R64 # 2:5, OUT -0006: RETURN # 3:1 --- FOO (END) - -0007: CALL R64, 2 # 5:1, FOO -0008: EOF # 0:0 +0000: JUMP 5 ; 1:5 + +;; FOO (BEGIN) +0001: LOADI R65, 0 ; 2:9 +0002: LOADI R64, 259 ; 2:9 +0003: UPCALL 0, R64 ; 2:5, OUT +0004: RETURN ; 3:1 +;; FOO (END) + +0005: CALL R64, 1 ; 5:1, FOO +0006: EOF ; 0:0 ``` ## Output @@ -219,15 +212,14 @@ OUT "done" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 5 # 1:6 -0002: LOADI R65, 0 # 2:5 -0003: LOADI R64, 259 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R65, 1 # 4:5 -0006: LOADI R64, 259 # 4:5 -0007: UPCALL 0, R64 # 4:1, OUT -0008: EOF # 0:0 +0000: JUMP 4 ; 1:6 +0001: LOADI R65, 0 ; 2:5 +0002: LOADI R64, 259 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R65, 1 ; 4:5 +0005: LOADI R64, 259 ; 4:5 +0006: UPCALL 0, R64 ; 4:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -251,15 +243,14 @@ RETURN ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: GOSUB 4 # 1:7 -0002: LOADI R64, 0 # 2:1 -0003: END R64 # 2:1 -0004: LOADI R65, 0 # 4:5 -0005: LOADI R64, 259 # 4:5 -0006: UPCALL 0, R64 # 4:1, OUT -0007: RETURN # 5:1 -0008: EOF # 0:0 +0000: GOSUB 3 ; 1:7 +0001: LOADI R64, 0 ; 2:1 +0002: END R64 ; 2:1 +0003: LOADI R65, 0 ; 4:5 +0004: LOADI R64, 259 ; 4:5 +0005: UPCALL 0, R64 ; 4:1, OUT +0006: RETURN ; 5:1 +0007: EOF ; 0:0 ``` ## Output @@ -283,24 +274,22 @@ OUT foo(5) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: JUMP 8 # 1:10 - --- FOO (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 3 # 0:0 -0004: MOVE R64, R65 # 2:11 -0005: LOADI R66, 1 # 2:15 -0006: ADDI R64, R64, R66 # 2:13 -0007: RETURN # 3:1 --- FOO (END) - -0008: LOADI R67, 5 # 5:9 -0009: CALL R66, 2 # 5:5, FOO -0010: MOVE R65, R66 # 5:5 -0011: LOADI R64, 258 # 5:5 -0012: UPCALL 0, R64 # 5:1, OUT -0013: EOF # 0:0 +0000: JUMP 6 ; 1:10 + +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: MOVE R64, R65 ; 2:11 +0003: LOADI R66, 1 ; 2:15 +0004: ADDI R64, R64, R66 ; 2:13 +0005: RETURN ; 3:1 +;; FOO (END) + +0006: LOADI R67, 5 ; 5:9 +0007: CALL R66, 1 ; 5:5, FOO +0008: MOVE R65, R66 ; 5:5 +0009: LOADI R64, 258 ; 5:5 +0010: UPCALL 0, R64 ; 5:1, OUT +0011: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_data.md b/core2/tests/test_data.md index 3d1fa289..9367ba2d 100644 --- a/core2/tests/test_data.md +++ b/core2/tests/test_data.md @@ -11,9 +11,8 @@ GETDATA ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: UPCALL 0, R64 # 3:1, GETDATA -0002: EOF # 0:0 +0000: UPCALL 0, R64 ; 3:1, GETDATA +0001: EOF ; 0:0 ``` ## Output @@ -44,26 +43,25 @@ GETDATA ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:4 -0002: JMPF R64, 4 # 1:4 -0003: JUMP 6 # 1:4 -0004: LOADI R64, 1 # 3:1 -0005: JMPF R64, 6 # 3:1 -0006: LOADI R64, 0 # 6:7 -0007: JMPF R64, 9 # 6:7 -0008: JUMP 6 # 6:7 -0009: LOADI R64, 0 # 9:9 -0010: MOVE R65, R64 # 9:5 -0011: LOADI R66, 0 # 9:14 -0012: CMPLEI R65, R65, R66 # 9:11 -0013: JMPF R65, 18 # 9:5 -0014: MOVE R64, R64 # 9:5 -0015: LOADI R65, 1 # 9:15 -0016: ADDI R64, R64, R65 # 9:11 -0017: JUMP 10 # 9:5 -0018: UPCALL 0, R65 # 12:1, GETDATA -0019: EOF # 0:0 +0000: LOADI R64, 0 ; 1:4 +0001: JMPF R64, 3 ; 1:4 +0002: JUMP 5 ; 1:4 +0003: LOADI R64, 1 ; 3:1 +0004: JMPF R64, 5 ; 3:1 +0005: LOADI R64, 0 ; 6:7 +0006: JMPF R64, 8 ; 6:7 +0007: JUMP 5 ; 6:7 +0008: LOADI R64, 0 ; 9:9 +0009: MOVE R65, R64 ; 9:5 +0010: LOADI R66, 0 ; 9:14 +0011: CMPLEI R65, R65, R66 ; 9:11 +0012: JMPF R65, 17 ; 9:5 +0013: MOVE R64, R64 ; 9:5 +0014: LOADI R65, 1 ; 9:15 +0015: ADDI R64, R64, R65 ; 9:11 +0016: JUMP 9 ; 9:5 +0017: UPCALL 0, R65 ; 12:1, GETDATA +0018: EOF ; 0:0 ``` ## Output @@ -86,10 +84,9 @@ GETDATA ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: UPCALL 0, R64 # 1:1, GETDATA -0002: UPCALL 0, R64 # 4:1, GETDATA -0003: EOF # 0:0 +0000: UPCALL 0, R64 ; 1:1, GETDATA +0001: UPCALL 0, R64 ; 4:1, GETDATA +0002: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_do.md b/core2/tests/test_do.md index 2e95c664..6d1e4aeb 100644 --- a/core2/tests/test_do.md +++ b/core2/tests/test_do.md @@ -13,16 +13,15 @@ LOOP ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 2:9 -0002: LOADI R64, 259 # 2:9 -0003: UPCALL 0, R64 # 2:5, OUT -0004: JUMP 9 # 3:5 -0005: LOADI R65, 1 # 4:9 -0006: LOADI R64, 259 # 4:9 -0007: UPCALL 0, R64 # 4:5, OUT -0008: JUMP 1 # 0:0 -0009: EOF # 0:0 +0000: LOADI R65, 0 ; 2:9 +0001: LOADI R64, 259 ; 2:9 +0002: UPCALL 0, R64 ; 2:5, OUT +0003: JUMP 8 ; 3:5 +0004: LOADI R65, 1 ; 4:9 +0005: LOADI R64, 259 ; 4:9 +0006: UPCALL 0, R64 ; 4:5, OUT +0007: JUMP 0 ; 0:0 +0008: EOF ; 0:0 ``` ## Output @@ -46,21 +45,20 @@ LOOP ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPEQI R65, R65, R66 # 2:12 -0005: JMPF R65, 7 # 2:10 -0006: JUMP 14 # 2:10 -0007: MOVE R66, R64 # 3:9 -0008: LOADI R65, 258 # 3:9 -0009: UPCALL 0, R65 # 3:5, OUT -0010: MOVE R64, R64 # 4:9 -0011: LOADI R65, 1 # 4:13 -0012: SUBI R64, R64, R65 # 4:11 -0013: JUMP 2 # 2:10 -0014: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPEQI R65, R65, R66 ; 2:12 +0004: JMPF R65, 6 ; 2:10 +0005: JUMP 13 ; 2:10 +0006: MOVE R66, R64 ; 3:9 +0007: LOADI R65, 258 ; 3:9 +0008: UPCALL 0, R65 ; 3:5, OUT +0009: MOVE R64, R64 ; 4:9 +0010: LOADI R65, 1 ; 4:13 +0011: SUBI R64, R64, R65 ; 4:11 +0012: JUMP 1 ; 2:10 +0013: EOF ; 0:0 ``` # Test: Pre UNTIL DO loop with iterations @@ -78,21 +76,20 @@ LOOP ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPEQI R65, R65, R66 # 2:12 -0005: JMPF R65, 7 # 2:10 -0006: JUMP 14 # 2:10 -0007: MOVE R66, R64 # 3:9 -0008: LOADI R65, 258 # 3:9 -0009: UPCALL 0, R65 # 3:5, OUT -0010: MOVE R64, R64 # 4:9 -0011: LOADI R65, 1 # 4:13 -0012: SUBI R64, R64, R65 # 4:11 -0013: JUMP 2 # 2:10 -0014: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPEQI R65, R65, R66 ; 2:12 +0004: JMPF R65, 6 ; 2:10 +0005: JUMP 13 ; 2:10 +0006: MOVE R66, R64 ; 3:9 +0007: LOADI R65, 258 ; 3:9 +0008: UPCALL 0, R65 ; 3:5, OUT +0009: MOVE R64, R64 ; 4:9 +0010: LOADI R65, 1 ; 4:13 +0011: SUBI R64, R64, R65 ; 4:11 +0012: JUMP 1 ; 2:10 +0013: EOF ; 0:0 ``` ## Output @@ -118,20 +115,19 @@ LOOP ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 13 # 2:10 -0006: MOVE R66, R64 # 3:9 -0007: LOADI R65, 258 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R64, R64 # 4:9 -0010: LOADI R65, 1 # 4:13 -0011: SUBI R64, R64, R65 # 4:11 -0012: JUMP 2 # 2:10 -0013: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 12 ; 2:10 +0005: MOVE R66, R64 ; 3:9 +0006: LOADI R65, 258 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: MOVE R64, R64 ; 4:9 +0009: LOADI R65, 1 ; 4:13 +0010: SUBI R64, R64, R65 ; 4:11 +0011: JUMP 1 ; 2:10 +0012: EOF ; 0:0 ``` # Test: Pre WHILE DO loop with iterations @@ -149,20 +145,19 @@ LOOP ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 13 # 2:10 -0006: MOVE R66, R64 # 3:9 -0007: LOADI R65, 258 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R64, R64 # 4:9 -0010: LOADI R65, 1 # 4:13 -0011: SUBI R64, R64, R65 # 4:11 -0012: JUMP 2 # 2:10 -0013: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 12 ; 2:10 +0005: MOVE R66, R64 ; 3:9 +0006: LOADI R65, 258 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: MOVE R64, R64 ; 4:9 +0009: LOADI R65, 1 ; 4:13 +0010: SUBI R64, R64, R65 ; 4:11 +0011: JUMP 1 ; 2:10 +0012: EOF ; 0:0 ``` ## Output @@ -188,19 +183,18 @@ LOOP UNTIL n = 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:5 -0002: MOVE R66, R64 # 3:9 -0003: LOADI R65, 258 # 3:9 -0004: UPCALL 0, R65 # 3:5, OUT -0005: MOVE R64, R64 # 4:9 -0006: LOADI R65, 1 # 4:13 -0007: SUBI R64, R64, R65 # 4:11 -0008: MOVE R65, R64 # 5:12 -0009: LOADI R66, 0 # 5:16 -0010: CMPEQI R65, R65, R66 # 5:14 -0011: JMPF R65, 2 # 5:12 -0012: EOF # 0:0 +0000: LOADI R64, 1 ; 1:5 +0001: MOVE R66, R64 ; 3:9 +0002: LOADI R65, 258 ; 3:9 +0003: UPCALL 0, R65 ; 3:5, OUT +0004: MOVE R64, R64 ; 4:9 +0005: LOADI R65, 1 ; 4:13 +0006: SUBI R64, R64, R65 ; 4:11 +0007: MOVE R65, R64 ; 5:12 +0008: LOADI R66, 0 ; 5:16 +0009: CMPEQI R65, R65, R66 ; 5:14 +0010: JMPF R65, 1 ; 5:12 +0011: EOF ; 0:0 ``` ## Output @@ -224,19 +218,18 @@ LOOP UNTIL n = 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R66, R64 # 3:9 -0003: LOADI R65, 258 # 3:9 -0004: UPCALL 0, R65 # 3:5, OUT -0005: MOVE R64, R64 # 4:9 -0006: LOADI R65, 1 # 4:13 -0007: SUBI R64, R64, R65 # 4:11 -0008: MOVE R65, R64 # 5:12 -0009: LOADI R66, 0 # 5:16 -0010: CMPEQI R65, R65, R66 # 5:14 -0011: JMPF R65, 2 # 5:12 -0012: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R66, R64 ; 3:9 +0002: LOADI R65, 258 ; 3:9 +0003: UPCALL 0, R65 ; 3:5, OUT +0004: MOVE R64, R64 ; 4:9 +0005: LOADI R65, 1 ; 4:13 +0006: SUBI R64, R64, R65 ; 4:11 +0007: MOVE R65, R64 ; 5:12 +0008: LOADI R66, 0 ; 5:16 +0009: CMPEQI R65, R65, R66 ; 5:14 +0010: JMPF R65, 1 ; 5:12 +0011: EOF ; 0:0 ``` ## Output @@ -262,20 +255,19 @@ LOOP WHILE n > 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:5 -0002: MOVE R66, R64 # 3:9 -0003: LOADI R65, 258 # 3:9 -0004: UPCALL 0, R65 # 3:5, OUT -0005: MOVE R64, R64 # 4:9 -0006: LOADI R65, 1 # 4:13 -0007: SUBI R64, R64, R65 # 4:11 -0008: MOVE R65, R64 # 5:12 -0009: LOADI R66, 0 # 5:16 -0010: CMPGTI R65, R65, R66 # 5:14 -0011: JMPF R65, 13 # 5:12 -0012: JUMP 2 # 5:12 -0013: EOF # 0:0 +0000: LOADI R64, 1 ; 1:5 +0001: MOVE R66, R64 ; 3:9 +0002: LOADI R65, 258 ; 3:9 +0003: UPCALL 0, R65 ; 3:5, OUT +0004: MOVE R64, R64 ; 4:9 +0005: LOADI R65, 1 ; 4:13 +0006: SUBI R64, R64, R65 ; 4:11 +0007: MOVE R65, R64 ; 5:12 +0008: LOADI R66, 0 ; 5:16 +0009: CMPGTI R65, R65, R66 ; 5:14 +0010: JMPF R65, 12 ; 5:12 +0011: JUMP 1 ; 5:12 +0012: EOF ; 0:0 ``` ## Output @@ -299,20 +291,19 @@ LOOP WHILE n > 0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R66, R64 # 3:9 -0003: LOADI R65, 258 # 3:9 -0004: UPCALL 0, R65 # 3:5, OUT -0005: MOVE R64, R64 # 4:9 -0006: LOADI R65, 1 # 4:13 -0007: SUBI R64, R64, R65 # 4:11 -0008: MOVE R65, R64 # 5:12 -0009: LOADI R66, 0 # 5:16 -0010: CMPGTI R65, R65, R66 # 5:14 -0011: JMPF R65, 13 # 5:12 -0012: JUMP 2 # 5:12 -0013: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R66, R64 ; 3:9 +0002: LOADI R65, 258 ; 3:9 +0003: UPCALL 0, R65 ; 3:5, OUT +0004: MOVE R64, R64 ; 4:9 +0005: LOADI R65, 1 ; 4:13 +0006: SUBI R64, R64, R65 ; 4:11 +0007: MOVE R65, R64 ; 5:12 +0008: LOADI R66, 0 ; 5:16 +0009: CMPGTI R65, R65, R66 ; 5:14 +0010: JMPF R65, 12 ; 5:12 +0011: JUMP 1 ; 5:12 +0012: EOF ; 0:0 ``` ## Output @@ -344,46 +335,45 @@ LOOP ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 39 # 2:10 -0006: LOADI R65, 2 # 3:9 -0007: MOVE R66, R65 # 4:14 -0008: LOADI R67, 0 # 4:18 -0009: CMPEQI R66, R66, R67 # 4:16 -0010: JMPF R66, 12 # 4:14 -0011: JUMP 30 # 4:14 -0012: MOVE R67, R64 # 5:13 -0013: LOADI R66, 274 # 5:13 -0014: MOVE R69, R65 # 5:16 -0015: LOADI R68, 258 # 5:16 -0016: UPCALL 0, R66 # 5:9, OUT -0017: MOVE R66, R64 # 6:12 -0018: LOADI R67, 2 # 6:16 -0019: CMPEQI R66, R66, R67 # 6:14 -0020: MOVE R67, R65 # 6:22 -0021: LOADI R68, 2 # 6:26 -0022: CMPEQI R67, R67, R68 # 6:24 -0023: AND R66, R66, R67 # 6:18 -0024: JMPF R66, 26 # 6:12 -0025: JUMP 30 # 6:34 -0026: MOVE R65, R65 # 7:13 -0027: LOADI R66, 1 # 7:17 -0028: SUBI R65, R65, R66 # 7:15 -0029: JUMP 7 # 4:14 -0030: MOVE R66, R64 # 9:8 -0031: LOADI R67, 1 # 9:12 -0032: CMPEQI R66, R66, R67 # 9:10 -0033: JMPF R66, 35 # 9:8 -0034: JUMP 39 # 9:20 -0035: MOVE R64, R64 # 10:9 -0036: LOADI R66, 1 # 10:13 -0037: SUBI R64, R64, R66 # 10:11 -0038: JUMP 2 # 2:10 -0039: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 38 ; 2:10 +0005: LOADI R65, 2 ; 3:9 +0006: MOVE R66, R65 ; 4:14 +0007: LOADI R67, 0 ; 4:18 +0008: CMPEQI R66, R66, R67 ; 4:16 +0009: JMPF R66, 11 ; 4:14 +0010: JUMP 29 ; 4:14 +0011: MOVE R67, R64 ; 5:13 +0012: LOADI R66, 274 ; 5:13 +0013: MOVE R69, R65 ; 5:16 +0014: LOADI R68, 258 ; 5:16 +0015: UPCALL 0, R66 ; 5:9, OUT +0016: MOVE R66, R64 ; 6:12 +0017: LOADI R67, 2 ; 6:16 +0018: CMPEQI R66, R66, R67 ; 6:14 +0019: MOVE R67, R65 ; 6:22 +0020: LOADI R68, 2 ; 6:26 +0021: CMPEQI R67, R67, R68 ; 6:24 +0022: AND R66, R66, R67 ; 6:18 +0023: JMPF R66, 25 ; 6:12 +0024: JUMP 29 ; 6:34 +0025: MOVE R65, R65 ; 7:13 +0026: LOADI R66, 1 ; 7:17 +0027: SUBI R65, R65, R66 ; 7:15 +0028: JUMP 6 ; 4:14 +0029: MOVE R66, R64 ; 9:8 +0030: LOADI R67, 1 ; 9:12 +0031: CMPEQI R66, R66, R67 ; 9:10 +0032: JMPF R66, 34 ; 9:8 +0033: JUMP 38 ; 9:20 +0034: MOVE R64, R64 ; 10:9 +0035: LOADI R66, 1 ; 10:13 +0036: SUBI R64, R64, R66 ; 10:11 +0037: JUMP 1 ; 2:10 +0038: EOF ; 0:0 ``` ## Output @@ -417,37 +407,36 @@ LOOP ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 30 # 2:10 -0006: LOADI R65, 3 # 3:9 -0007: MOVE R66, R65 # 4:14 -0008: LOADI R67, 0 # 4:18 -0009: CMPGTI R66, R66, R67 # 4:16 -0010: JMPF R66, 21 # 4:14 -0011: MOVE R67, R64 # 5:13 -0012: LOADI R66, 274 # 5:13 -0013: MOVE R69, R65 # 5:16 -0014: LOADI R68, 258 # 5:16 -0015: UPCALL 0, R66 # 5:9, OUT -0016: JUMP 21 # 6:9 -0017: MOVE R65, R65 # 7:13 -0018: LOADI R66, 1 # 7:17 -0019: SUBI R65, R65, R66 # 7:15 -0020: JUMP 7 # 4:14 -0021: LOADI R67, 0 # 9:9 -0022: LOADI R66, 275 # 9:9 -0023: MOVE R69, R64 # 9:18 -0024: LOADI R68, 258 # 9:18 -0025: UPCALL 0, R66 # 9:5, OUT -0026: MOVE R64, R64 # 10:9 -0027: LOADI R66, 1 # 10:13 -0028: SUBI R64, R64, R66 # 10:11 -0029: JUMP 2 # 2:10 -0030: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 29 ; 2:10 +0005: LOADI R65, 3 ; 3:9 +0006: MOVE R66, R65 ; 4:14 +0007: LOADI R67, 0 ; 4:18 +0008: CMPGTI R66, R66, R67 ; 4:16 +0009: JMPF R66, 20 ; 4:14 +0010: MOVE R67, R64 ; 5:13 +0011: LOADI R66, 274 ; 5:13 +0012: MOVE R69, R65 ; 5:16 +0013: LOADI R68, 258 ; 5:16 +0014: UPCALL 0, R66 ; 5:9, OUT +0015: JUMP 20 ; 6:9 +0016: MOVE R65, R65 ; 7:13 +0017: LOADI R66, 1 ; 7:17 +0018: SUBI R65, R65, R66 ; 7:15 +0019: JUMP 6 ; 4:14 +0020: LOADI R67, 0 ; 9:9 +0021: LOADI R66, 275 ; 9:9 +0022: MOVE R69, R64 ; 9:18 +0023: LOADI R68, 258 ; 9:18 +0024: UPCALL 0, R66 ; 9:5, OUT +0025: MOVE R64, R64 ; 10:9 +0026: LOADI R66, 1 ; 10:13 +0027: SUBI R64, R64, R66 ; 10:11 +0028: JUMP 1 ; 2:10 +0029: EOF ; 0:0 ``` ## Output @@ -480,39 +469,38 @@ LOOP ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 32 # 2:10 -0006: LOADI R65, 2 # 3:9 -0007: MOVE R66, R65 # 4:14 -0008: LOADI R67, 0 # 4:18 -0009: CMPGTI R66, R66, R67 # 4:16 -0010: JMPF R66, 25 # 4:14 -0011: MOVE R66, R64 # 5:12 -0012: LOADI R67, 2 # 5:16 -0013: CMPEQI R66, R66, R67 # 5:14 -0014: JMPF R66, 16 # 5:12 -0015: JUMP 25 # 5:24 -0016: MOVE R66, R65 # 6:12 -0017: LOADI R67, 1 # 6:16 -0018: CMPEQI R66, R66, R67 # 6:14 -0019: JMPF R66, 21 # 6:12 -0020: JUMP 25 # 6:24 -0021: MOVE R65, R65 # 7:13 -0022: LOADI R66, 1 # 7:17 -0023: SUBI R65, R65, R66 # 7:15 -0024: JUMP 7 # 4:14 -0025: MOVE R67, R64 # 9:9 -0026: LOADI R66, 258 # 9:9 -0027: UPCALL 0, R66 # 9:5, OUT -0028: MOVE R64, R64 # 10:9 -0029: LOADI R66, 1 # 10:13 -0030: SUBI R64, R64, R66 # 10:11 -0031: JUMP 2 # 2:10 -0032: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 31 ; 2:10 +0005: LOADI R65, 2 ; 3:9 +0006: MOVE R66, R65 ; 4:14 +0007: LOADI R67, 0 ; 4:18 +0008: CMPGTI R66, R66, R67 ; 4:16 +0009: JMPF R66, 24 ; 4:14 +0010: MOVE R66, R64 ; 5:12 +0011: LOADI R67, 2 ; 5:16 +0012: CMPEQI R66, R66, R67 ; 5:14 +0013: JMPF R66, 15 ; 5:12 +0014: JUMP 24 ; 5:24 +0015: MOVE R66, R65 ; 6:12 +0016: LOADI R67, 1 ; 6:16 +0017: CMPEQI R66, R66, R67 ; 6:14 +0018: JMPF R66, 20 ; 6:12 +0019: JUMP 24 ; 6:24 +0020: MOVE R65, R65 ; 7:13 +0021: LOADI R66, 1 ; 7:17 +0022: SUBI R65, R65, R66 ; 7:15 +0023: JUMP 6 ; 4:14 +0024: MOVE R67, R64 ; 9:9 +0025: LOADI R66, 258 ; 9:9 +0026: UPCALL 0, R66 ; 9:5, OUT +0027: MOVE R64, R64 ; 10:9 +0028: LOADI R66, 1 ; 10:13 +0029: SUBI R64, R64, R66 ; 10:11 +0030: JUMP 1 ; 2:10 +0031: EOF ; 0:0 ``` ## Output @@ -542,31 +530,30 @@ LOOP ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 24 # 2:10 -0006: LOADI R65, 2 # 3:9 -0007: MOVE R67, R64 # 5:13 -0008: LOADI R66, 274 # 5:13 -0009: MOVE R69, R65 # 5:16 -0010: LOADI R68, 258 # 5:16 -0011: UPCALL 0, R66 # 5:9, OUT -0012: JUMP 20 # 6:9 -0013: MOVE R65, R65 # 7:13 -0014: LOADI R66, 1 # 7:17 -0015: SUBI R65, R65, R66 # 7:15 -0016: MOVE R66, R65 # 8:16 -0017: LOADI R67, 0 # 8:20 -0018: CMPEQI R66, R66, R67 # 8:18 -0019: JMPF R66, 7 # 8:16 -0020: MOVE R64, R64 # 9:9 -0021: LOADI R66, 1 # 9:13 -0022: SUBI R64, R64, R66 # 9:11 -0023: JUMP 2 # 2:10 -0024: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 23 ; 2:10 +0005: LOADI R65, 2 ; 3:9 +0006: MOVE R67, R64 ; 5:13 +0007: LOADI R66, 274 ; 5:13 +0008: MOVE R69, R65 ; 5:16 +0009: LOADI R68, 258 ; 5:16 +0010: UPCALL 0, R66 ; 5:9, OUT +0011: JUMP 19 ; 6:9 +0012: MOVE R65, R65 ; 7:13 +0013: LOADI R66, 1 ; 7:17 +0014: SUBI R65, R65, R66 ; 7:15 +0015: MOVE R66, R65 ; 8:16 +0016: LOADI R67, 0 ; 8:20 +0017: CMPEQI R66, R66, R67 ; 8:18 +0018: JMPF R66, 6 ; 8:16 +0019: MOVE R64, R64 ; 9:9 +0020: LOADI R66, 1 ; 9:13 +0021: SUBI R64, R64, R66 ; 9:11 +0022: JUMP 1 ; 2:10 +0023: EOF ; 0:0 ``` ## Output @@ -595,25 +582,24 @@ LOOP ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 18 # 2:10 -0006: LOADI R65, 1 # 3:9 -0007: MOVE R67, R64 # 5:13 -0008: LOADI R66, 274 # 5:13 -0009: MOVE R69, R65 # 5:16 -0010: LOADI R68, 258 # 5:16 -0011: UPCALL 0, R66 # 5:9, OUT -0012: JUMP 14 # 6:9 -0013: JUMP 7 # 0:0 -0014: MOVE R64, R64 # 8:9 -0015: LOADI R66, 1 # 8:13 -0016: SUBI R64, R64, R66 # 8:11 -0017: JUMP 2 # 2:10 -0018: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 17 ; 2:10 +0005: LOADI R65, 1 ; 3:9 +0006: MOVE R67, R64 ; 5:13 +0007: LOADI R66, 274 ; 5:13 +0008: MOVE R69, R65 ; 5:16 +0009: LOADI R68, 258 ; 5:16 +0010: UPCALL 0, R66 ; 5:9, OUT +0011: JUMP 13 ; 6:9 +0012: JUMP 6 ; 0:0 +0013: MOVE R64, R64 ; 8:9 +0014: LOADI R66, 1 ; 8:13 +0015: SUBI R64, R64, R66 ; 8:11 +0016: JUMP 1 ; 2:10 +0017: EOF ; 0:0 ``` ## Output @@ -640,37 +626,36 @@ LOOP ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 30 # 2:10 -0006: LOADI R65, 2 # 3:9 -0007: MOVE R66, R65 # 4:14 -0008: LOADI R67, 0 # 4:18 -0009: CMPGTI R66, R66, R67 # 4:16 -0010: JMPF R66, 21 # 4:14 -0011: MOVE R67, R64 # 4:25 -0012: LOADI R66, 274 # 4:25 -0013: MOVE R69, R65 # 4:28 -0014: LOADI R68, 258 # 4:28 -0015: UPCALL 0, R66 # 4:21, OUT -0016: JUMP 21 # 4:31 -0017: MOVE R65, R65 # 4:44 -0018: LOADI R66, 1 # 4:48 -0019: SUBI R65, R65, R66 # 4:46 -0020: JUMP 7 # 4:14 -0021: LOADI R67, 0 # 5:9 -0022: LOADI R66, 275 # 5:9 -0023: MOVE R69, R64 # 5:18 -0024: LOADI R68, 258 # 5:18 -0025: UPCALL 0, R66 # 5:5, OUT -0026: MOVE R64, R64 # 6:9 -0027: LOADI R66, 1 # 6:13 -0028: SUBI R64, R64, R66 # 6:11 -0029: JUMP 2 # 2:10 -0030: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 29 ; 2:10 +0005: LOADI R65, 2 ; 3:9 +0006: MOVE R66, R65 ; 4:14 +0007: LOADI R67, 0 ; 4:18 +0008: CMPGTI R66, R66, R67 ; 4:16 +0009: JMPF R66, 20 ; 4:14 +0010: MOVE R67, R64 ; 4:25 +0011: LOADI R66, 274 ; 4:25 +0012: MOVE R69, R65 ; 4:28 +0013: LOADI R68, 258 ; 4:28 +0014: UPCALL 0, R66 ; 4:21, OUT +0015: JUMP 20 ; 4:31 +0016: MOVE R65, R65 ; 4:44 +0017: LOADI R66, 1 ; 4:48 +0018: SUBI R65, R65, R66 ; 4:46 +0019: JUMP 6 ; 4:14 +0020: LOADI R67, 0 ; 5:9 +0021: LOADI R66, 275 ; 5:9 +0022: MOVE R69, R64 ; 5:18 +0023: LOADI R68, 258 ; 5:18 +0024: UPCALL 0, R66 ; 5:5, OUT +0025: MOVE R64, R64 ; 6:9 +0026: LOADI R66, 1 ; 6:13 +0027: SUBI R64, R64, R66 ; 6:11 +0028: JUMP 1 ; 2:10 +0029: EOF ; 0:0 ``` ## Output @@ -703,36 +688,35 @@ LOOP ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 15 # 2:10 -0006: LOADI R66, 0 # 3:9 -0007: LOADI R65, 275 # 3:9 -0008: MOVE R68, R64 # 3:18 -0009: LOADI R67, 258 # 3:18 -0010: UPCALL 0, R65 # 3:5, OUT -0011: MOVE R64, R64 # 4:9 -0012: LOADI R65, 1 # 4:13 -0013: SUBI R64, R64, R65 # 4:11 -0014: JUMP 2 # 2:10 -0015: LOADI R64, 2 # 7:5 -0016: MOVE R65, R64 # 8:10 -0017: LOADI R66, 0 # 8:14 -0018: CMPGTI R65, R65, R66 # 8:12 -0019: JMPF R65, 29 # 8:10 -0020: LOADI R66, 1 # 9:9 -0021: LOADI R65, 275 # 9:9 -0022: MOVE R68, R64 # 9:19 -0023: LOADI R67, 258 # 9:19 -0024: UPCALL 0, R65 # 9:5, OUT -0025: MOVE R64, R64 # 10:9 -0026: LOADI R65, 1 # 10:13 -0027: SUBI R64, R64, R65 # 10:11 -0028: JUMP 16 # 8:10 -0029: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 14 ; 2:10 +0005: LOADI R66, 0 ; 3:9 +0006: LOADI R65, 275 ; 3:9 +0007: MOVE R68, R64 ; 3:18 +0008: LOADI R67, 258 ; 3:18 +0009: UPCALL 0, R65 ; 3:5, OUT +0010: MOVE R64, R64 ; 4:9 +0011: LOADI R65, 1 ; 4:13 +0012: SUBI R64, R64, R65 ; 4:11 +0013: JUMP 1 ; 2:10 +0014: LOADI R64, 2 ; 7:5 +0015: MOVE R65, R64 ; 8:10 +0016: LOADI R66, 0 ; 8:14 +0017: CMPGTI R65, R65, R66 ; 8:12 +0018: JMPF R65, 28 ; 8:10 +0019: LOADI R66, 1 ; 9:9 +0020: LOADI R65, 275 ; 9:9 +0021: MOVE R68, R64 ; 9:19 +0022: LOADI R67, 258 ; 9:19 +0023: UPCALL 0, R65 ; 9:5, OUT +0024: MOVE R64, R64 ; 10:9 +0025: LOADI R65, 1 ; 10:13 +0026: SUBI R64, R64, R65 ; 10:11 +0027: JUMP 15 ; 8:10 +0028: EOF ; 0:0 ``` ## Output @@ -770,49 +754,48 @@ RETURN ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 16 # 2:10 -0006: GOSUB 17 # 3:11 -0007: MOVE R65, R64 # 4:8 -0008: LOADI R66, 1 # 4:12 -0009: CMPEQI R65, R65, R66 # 4:10 -0010: JMPF R65, 12 # 4:8 -0011: JUMP 16 # 4:20 -0012: MOVE R64, R64 # 5:9 -0013: LOADI R65, 1 # 5:13 -0014: SUBI R64, R64, R65 # 5:11 -0015: JUMP 2 # 2:10 -0016: JUMP 42 # 7:6 -0017: LOADI R65, 2 # 9:5 -0018: MOVE R66, R65 # 10:10 -0019: LOADI R67, 0 # 10:14 -0020: CMPEQI R66, R66, R67 # 10:12 -0021: JMPF R66, 23 # 10:10 -0022: JUMP 41 # 10:10 -0023: MOVE R67, R64 # 11:9 -0024: LOADI R66, 274 # 11:9 -0025: MOVE R69, R65 # 11:12 -0026: LOADI R68, 258 # 11:12 -0027: UPCALL 0, R66 # 11:5, OUT -0028: MOVE R66, R64 # 12:8 -0029: LOADI R67, 2 # 12:12 -0030: CMPEQI R66, R66, R67 # 12:10 -0031: MOVE R67, R65 # 12:18 -0032: LOADI R68, 2 # 12:22 -0033: CMPEQI R67, R67, R68 # 12:20 -0034: AND R66, R66, R67 # 12:14 -0035: JMPF R66, 37 # 12:8 -0036: JUMP 41 # 12:30 -0037: MOVE R65, R65 # 13:9 -0038: LOADI R66, 1 # 13:13 -0039: SUBI R65, R65, R66 # 13:11 -0040: JUMP 18 # 10:10 -0041: RETURN # 15:1 -0042: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 15 ; 2:10 +0005: GOSUB 16 ; 3:11 +0006: MOVE R65, R64 ; 4:8 +0007: LOADI R66, 1 ; 4:12 +0008: CMPEQI R65, R65, R66 ; 4:10 +0009: JMPF R65, 11 ; 4:8 +0010: JUMP 15 ; 4:20 +0011: MOVE R64, R64 ; 5:9 +0012: LOADI R65, 1 ; 5:13 +0013: SUBI R64, R64, R65 ; 5:11 +0014: JUMP 1 ; 2:10 +0015: JUMP 41 ; 7:6 +0016: LOADI R65, 2 ; 9:5 +0017: MOVE R66, R65 ; 10:10 +0018: LOADI R67, 0 ; 10:14 +0019: CMPEQI R66, R66, R67 ; 10:12 +0020: JMPF R66, 22 ; 10:10 +0021: JUMP 40 ; 10:10 +0022: MOVE R67, R64 ; 11:9 +0023: LOADI R66, 274 ; 11:9 +0024: MOVE R69, R65 ; 11:12 +0025: LOADI R68, 258 ; 11:12 +0026: UPCALL 0, R66 ; 11:5, OUT +0027: MOVE R66, R64 ; 12:8 +0028: LOADI R67, 2 ; 12:12 +0029: CMPEQI R66, R66, R67 ; 12:10 +0030: MOVE R67, R65 ; 12:18 +0031: LOADI R68, 2 ; 12:22 +0032: CMPEQI R67, R67, R68 ; 12:20 +0033: AND R66, R66, R67 ; 12:14 +0034: JMPF R66, 36 ; 12:8 +0035: JUMP 40 ; 12:30 +0036: MOVE R65, R65 ; 13:9 +0037: LOADI R66, 1 ; 13:13 +0038: SUBI R65, R65, R66 ; 13:11 +0039: JUMP 17 ; 10:10 +0040: RETURN ; 15:1 +0041: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_empty.md b/core2/tests/test_empty.md index 6c5918c4..22e1ffec 100644 --- a/core2/tests/test_empty.md +++ b/core2/tests/test_empty.md @@ -8,6 +8,5 @@ ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: EOF # 0:0 +0000: EOF ; 0:0 ``` diff --git a/core2/tests/test_end.md b/core2/tests/test_end.md index 8a9b9f8e..96fcf112 100644 --- a/core2/tests/test_end.md +++ b/core2/tests/test_end.md @@ -9,10 +9,9 @@ END ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 0 # 1:1 -0002: END R64 # 1:1 -0003: EOF # 0:0 +0000: LOADI R64, 0 ; 1:1 +0001: END R64 ; 1:1 +0002: EOF ; 0:0 ``` # Test: Exit code is an integer immediate @@ -26,10 +25,9 @@ END 42 ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R64, 42 # 1:5 -0002: END R64 # 1:1 -0003: EOF # 0:0 +0000: LOADI R64, 42 ; 1:5 +0001: END R64 ; 1:1 +0002: EOF ; 0:0 ``` ## Exit code @@ -49,11 +47,10 @@ END 43.98 ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADC R64, 0 # 1:5 -0002: DTOI R64 # 1:5 -0003: END R64 # 1:1 -0004: EOF # 0:0 +0000: LOADC R64, 0 ; 1:5 +0001: DTOI R64 ; 1:5 +0002: END R64 ; 1:1 +0003: EOF ; 0:0 ``` ## Exit code @@ -75,12 +72,11 @@ END i ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R0, 5 # 2:5 -0003: MOVE R64, R0 # 3:5 -0004: END R64 # 3:1 -0005: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R0, 5 ; 2:5 +0002: MOVE R64, R0 ; 3:5 +0003: END R64 ; 3:1 +0004: EOF ; 0:0 ``` ## Exit code @@ -101,11 +97,10 @@ END i ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:5 -0003: END R65 # 2:1 -0004: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:5 +0002: END R65 ; 2:1 +0003: EOF ; 0:0 ``` ## Exit code @@ -168,12 +163,11 @@ END i ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 3 # 1:6 -0002: NEGI R64 # 1:5 -0003: MOVE R65, R64 # 2:5 -0004: END R65 # 2:1 -0005: EOF # 0:0 +0000: LOADI R64, 3 ; 1:6 +0001: NEGI R64 ; 1:5 +0002: MOVE R65, R64 ; 2:5 +0003: END R65 ; 2:1 +0004: EOF ; 0:0 ``` ## Runtime errors @@ -194,11 +188,10 @@ END i ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 128 # 1:5 -0002: MOVE R65, R64 # 2:5 -0003: END R65 # 2:1 -0004: EOF # 0:0 +0000: LOADI R64, 128 ; 1:5 +0001: MOVE R65, R64 ; 2:5 +0002: END R65 ; 2:1 +0003: EOF ; 0:0 ``` ## Runtime errors @@ -220,23 +213,22 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:14 -0004: CMPLEI R65, R65, R66 # 1:11 -0005: JMPF R65, 16 # 1:5 -0006: MOVE R65, R64 # 2:8 -0007: LOADI R66, 3 # 2:12 -0008: CMPEQI R65, R65, R66 # 2:10 -0009: JMPF R65, 12 # 2:8 -0010: LOADI R65, 42 # 2:23 -0011: END R65 # 2:19 -0012: MOVE R64, R64 # 1:5 -0013: LOADI R65, 1 # 1:16 -0014: ADDI R64, R64, R65 # 1:11 -0015: JUMP 2 # 1:5 -0016: EOF # 0:0 +0000: LOADI R64, 1 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:14 +0003: CMPLEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 15 ; 1:5 +0005: MOVE R65, R64 ; 2:8 +0006: LOADI R66, 3 ; 2:12 +0007: CMPEQI R65, R65, R66 ; 2:10 +0008: JMPF R65, 11 ; 2:8 +0009: LOADI R65, 42 ; 2:23 +0010: END R65 ; 2:19 +0011: MOVE R64, R64 ; 1:5 +0012: LOADI R65, 1 ; 1:16 +0013: ADDI R64, R64, R65 ; 1:11 +0014: JUMP 1 ; 1:5 +0015: EOF ; 0:0 ``` ## Exit code diff --git a/core2/tests/test_for.md b/core2/tests/test_for.md index 4d0439a8..a4cd9e9a 100644 --- a/core2/tests/test_for.md +++ b/core2/tests/test_for.md @@ -11,20 +11,19 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 3 # 1:14 -0004: CMPLEI R65, R65, R66 # 1:11 -0005: JMPF R65, 13 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 1:5 -0010: LOADI R65, 1 # 1:15 -0011: ADDI R64, R64, R65 # 1:11 -0012: JUMP 2 # 1:5 -0013: EOF # 0:0 +0000: LOADI R64, 0 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 3 ; 1:14 +0003: CMPLEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 12 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 1:5 +0009: LOADI R65, 1 ; 1:15 +0010: ADDI R64, R64, R65 ; 1:11 +0011: JUMP 1 ; 1:5 +0012: EOF ; 0:0 ``` ## Output @@ -49,20 +48,19 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:14 -0004: CMPLEI R65, R65, R66 # 1:11 -0005: JMPF R65, 13 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 1:5 -0010: LOADI R65, 3 # 1:22 -0011: ADDI R64, R64, R65 # 1:11 -0012: JUMP 2 # 1:5 -0013: EOF # 0:0 +0000: LOADI R64, 0 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:14 +0003: CMPLEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 12 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 1:5 +0009: LOADI R65, 3 ; 1:22 +0010: ADDI R64, R64, R65 ; 1:11 +0011: JUMP 1 ; 1:5 +0012: EOF ; 0:0 ``` ## Output @@ -87,20 +85,19 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 10 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 1 # 1:15 -0004: CMPGEI R65, R65, R66 # 1:12 -0005: JMPF R65, 13 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 1:5 -0010: LOADC R65, 0 # 1:23 -0011: ADDI R64, R64, R65 # 1:12 -0012: JUMP 2 # 1:5 -0013: EOF # 0:0 +0000: LOADI R64, 10 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 1 ; 1:15 +0003: CMPGEI R65, R65, R66 ; 1:12 +0004: JMPF R65, 12 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 1:5 +0009: LOADC R65, 0 ; 1:23 +0010: ADDI R64, R64, R65 ; 1:12 +0011: JUMP 1 ; 1:5 +0012: EOF ; 0:0 ``` ## Output @@ -126,20 +123,19 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 10 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 9 # 1:15 -0004: CMPLEI R65, R65, R66 # 1:12 -0005: JMPF R65, 13 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 1:5 -0010: LOADI R65, 1 # 1:16 -0011: ADDI R64, R64, R65 # 1:12 -0012: JUMP 2 # 1:5 -0013: EOF # 0:0 +0000: LOADI R64, 10 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 9 ; 1:15 +0003: CMPLEI R65, R65, R66 ; 1:12 +0004: JMPF R65, 12 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 1:5 +0009: LOADI R65, 1 ; 1:16 +0010: ADDI R64, R64, R65 ; 1:12 +0011: JUMP 1 ; 1:5 +0012: EOF ; 0:0 ``` # Test: FOR loop with invalid direction has zero iterations @@ -155,20 +151,19 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 9 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:14 -0004: CMPGEI R65, R65, R66 # 1:11 -0005: JMPF R65, 13 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 1:5 -0010: LOADC R65, 0 # 1:23 -0011: ADDI R64, R64, R65 # 1:11 -0012: JUMP 2 # 1:5 -0013: EOF # 0:0 +0000: LOADI R64, 9 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:14 +0003: CMPGEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 12 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 1:5 +0009: LOADC R65, 0 ; 1:23 +0010: ADDI R64, R64, R65 ; 1:11 +0011: JUMP 1 ; 1:5 +0012: EOF ; 0:0 ``` # Test: FOR iterator is visible after NEXT @@ -184,20 +179,19 @@ OUT something ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:17 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:22 -0004: CMPLEI R65, R65, R66 # 1:19 -0005: JMPF R65, 10 # 1:5 -0006: MOVE R64, R64 # 1:5 -0007: LOADI R65, 8 # 1:30 -0008: ADDI R64, R64, R65 # 1:19 -0009: JUMP 2 # 1:5 -0010: MOVE R66, R64 # 3:5 -0011: LOADI R65, 258 # 3:5 -0012: UPCALL 0, R65 # 3:1, OUT -0013: EOF # 0:0 +0000: LOADI R64, 1 ; 1:17 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:22 +0003: CMPLEI R65, R65, R66 ; 1:19 +0004: JMPF R65, 9 ; 1:5 +0005: MOVE R64, R64 ; 1:5 +0006: LOADI R65, 8 ; 1:30 +0007: ADDI R64, R64, R65 ; 1:19 +0008: JUMP 1 ; 1:5 +0009: MOVE R66, R64 ; 3:5 +0010: LOADI R65, 258 ; 3:5 +0011: UPCALL 0, R65 ; 3:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -220,23 +214,22 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:17 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 5 # 1:22 -0004: CMPLEI R65, R65, R66 # 1:19 -0005: JMPF R65, 16 # 1:5 -0006: MOVE R66, R64 # 2:9 -0007: LOADI R65, 258 # 2:9 -0008: UPCALL 0, R65 # 2:5, OUT -0009: MOVE R64, R64 # 3:17 -0010: LOADI R65, 1 # 3:29 -0011: ADDI R64, R64, R65 # 3:27 -0012: MOVE R64, R64 # 1:5 -0013: LOADI R65, 1 # 1:23 -0014: ADDI R64, R64, R65 # 1:19 -0015: JUMP 2 # 1:5 -0016: EOF # 0:0 +0000: LOADI R64, 1 ; 1:17 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 5 ; 1:22 +0003: CMPLEI R65, R65, R66 ; 1:19 +0004: JMPF R65, 15 ; 1:5 +0005: MOVE R66, R64 ; 2:9 +0006: LOADI R65, 258 ; 2:9 +0007: UPCALL 0, R65 ; 2:5, OUT +0008: MOVE R64, R64 ; 3:17 +0009: LOADI R65, 1 ; 3:29 +0010: ADDI R64, R64, R65 ; 3:27 +0011: MOVE R64, R64 ; 1:5 +0012: LOADI R65, 1 ; 1:23 +0013: ADDI R64, R64, R65 ; 1:19 +0014: JUMP 1 ; 1:5 +0015: EOF ; 0:0 ``` ## Output @@ -261,26 +254,25 @@ NEXT ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADC R64, 0 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADC R66, 1 # 1:16 -0004: CMPLED R65, R65, R66 # 1:13 -0005: JMPF R65, 19 # 1:5 -0006: MOVE R65, R64 # 2:11 -0007: LOADI R66, 10 # 2:15 -0008: ITOD R66 # 2:15 -0009: MULD R65, R65, R66 # 2:13 -0010: DTOI R65 # 2:11 -0011: MOVE R67, R65 # 3:9 -0012: LOADI R66, 258 # 3:9 -0013: UPCALL 0, R66 # 3:5, OUT -0014: MOVE R64, R64 # 1:5 -0015: LOADI R66, 1 # 1:19 -0016: ITOD R66 # 1:19 -0017: ADDD R64, R64, R66 # 1:13 -0018: JUMP 2 # 1:5 -0019: EOF # 0:0 +0000: LOADC R64, 0 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADC R66, 1 ; 1:16 +0003: CMPLED R65, R65, R66 ; 1:13 +0004: JMPF R65, 18 ; 1:5 +0005: MOVE R65, R64 ; 2:11 +0006: LOADI R66, 10 ; 2:15 +0007: ITOD R66 ; 2:15 +0008: MULD R65, R65, R66 ; 2:13 +0009: DTOI R65 ; 2:11 +0010: MOVE R67, R65 ; 3:9 +0011: LOADI R66, 258 ; 3:9 +0012: UPCALL 0, R66 ; 3:5, OUT +0013: MOVE R64, R64 ; 1:5 +0014: LOADI R66, 1 ; 1:19 +0015: ITOD R66 ; 1:19 +0016: ADDD R64, R64, R66 ; 1:13 +0017: JUMP 1 ; 1:5 +0018: EOF ; 0:0 ``` ## Output @@ -307,38 +299,37 @@ OUT i; b ## Disassembly ```asm -0000: ENTER 7 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: LOADI R65, 0 # 2:12 -0003: ITOD R65 # 2:12 -0004: MOVE R66, R65 # 2:5 -0005: LOADI R67, 2 # 2:17 -0006: ITOD R67 # 2:17 -0007: CMPLED R66, R66, R67 # 2:14 -0008: JMPF R66, 21 # 2:5 -0009: MOVE R64, R64 # 3:9 -0010: LOADI R66, 1 # 3:13 -0011: ADDI R64, R64, R66 # 3:11 -0012: MOVE R66, R64 # 4:8 -0013: LOADI R67, 5 # 4:12 -0014: CMPEQI R66, R66, R67 # 4:10 -0015: JMPF R66, 17 # 4:8 -0016: JUMP 21 # 4:19 -0017: MOVE R65, R65 # 2:5 -0018: LOADC R66, 0 # 2:24 -0019: ADDD R65, R65, R66 # 2:14 -0020: JUMP 4 # 2:5 -0021: MOVE R66, R65 # 6:7 -0022: LOADI R67, 10 # 6:14 -0023: ITOD R67 # 6:14 -0024: MULD R66, R66, R67 # 6:12 -0025: DTOI R66 # 6:7 -0026: MOVE R68, R64 # 7:5 -0027: LOADI R67, 274 # 7:5 -0028: MOVE R70, R66 # 7:8 -0029: LOADI R69, 258 # 7:8 -0030: UPCALL 0, R67 # 7:1, OUT -0031: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: LOADI R65, 0 ; 2:12 +0002: ITOD R65 ; 2:12 +0003: MOVE R66, R65 ; 2:5 +0004: LOADI R67, 2 ; 2:17 +0005: ITOD R67 ; 2:17 +0006: CMPLED R66, R66, R67 ; 2:14 +0007: JMPF R66, 20 ; 2:5 +0008: MOVE R64, R64 ; 3:9 +0009: LOADI R66, 1 ; 3:13 +0010: ADDI R64, R64, R66 ; 3:11 +0011: MOVE R66, R64 ; 4:8 +0012: LOADI R67, 5 ; 4:12 +0013: CMPEQI R66, R66, R67 ; 4:10 +0014: JMPF R66, 16 ; 4:8 +0015: JUMP 20 ; 4:19 +0016: MOVE R65, R65 ; 2:5 +0017: LOADC R66, 0 ; 2:24 +0018: ADDD R65, R65, R66 ; 2:14 +0019: JUMP 3 ; 2:5 +0020: MOVE R66, R65 ; 6:7 +0021: LOADI R67, 10 ; 6:14 +0022: ITOD R67 ; 6:14 +0023: MULD R66, R66, R67 ; 6:12 +0024: DTOI R66 ; 6:7 +0025: MOVE R68, R64 ; 7:5 +0026: LOADI R67, 274 ; 7:5 +0027: MOVE R70, R66 ; 7:8 +0028: LOADI R69, 258 ; 7:8 +0029: UPCALL 0, R67 ; 7:1, OUT +0030: EOF ; 0:0 ``` ## Output @@ -367,34 +358,33 @@ OUT i ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: LOADI R65, 0 # 2:5 -0003: LOADC R65, 0 # 3:9 -0004: DTOI R65 # 3:9 -0005: MOVE R66, R65 # 3:5 -0006: LOADC R67, 1 # 3:16 -0007: ITOD R66 # 3:13 -0008: CMPLED R66, R66, R67 # 3:13 -0009: JMPF R66, 24 # 3:5 -0010: MOVE R64, R64 # 4:9 -0011: LOADI R66, 1 # 4:13 -0012: ADDI R64, R64, R66 # 4:11 -0013: MOVE R66, R64 # 5:8 -0014: LOADI R67, 100 # 5:12 -0015: CMPEQI R66, R66, R67 # 5:10 -0016: JMPF R66, 18 # 5:8 -0017: JUMP 24 # 6:14 -0018: MOVE R65, R65 # 3:5 -0019: LOADC R66, 2 # 3:25 -0020: ITOD R65 # 3:13 -0021: ADDD R65, R65, R66 # 3:13 -0022: DTOI R65 # 3:5 -0023: JUMP 5 # 3:5 -0024: MOVE R67, R64 # 10:5 -0025: LOADI R66, 258 # 10:5 -0026: UPCALL 0, R66 # 10:1, OUT -0027: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: LOADI R65, 0 ; 2:5 +0002: LOADC R65, 0 ; 3:9 +0003: DTOI R65 ; 3:9 +0004: MOVE R66, R65 ; 3:5 +0005: LOADC R67, 1 ; 3:16 +0006: ITOD R66 ; 3:13 +0007: CMPLED R66, R66, R67 ; 3:13 +0008: JMPF R66, 23 ; 3:5 +0009: MOVE R64, R64 ; 4:9 +0010: LOADI R66, 1 ; 4:13 +0011: ADDI R64, R64, R66 ; 4:11 +0012: MOVE R66, R64 ; 5:8 +0013: LOADI R67, 100 ; 5:12 +0014: CMPEQI R66, R66, R67 ; 5:10 +0015: JMPF R66, 17 ; 5:8 +0016: JUMP 23 ; 6:14 +0017: MOVE R65, R65 ; 3:5 +0018: LOADC R66, 2 ; 3:25 +0019: ITOD R65 ; 3:13 +0020: ADDD R65, R65, R66 ; 3:13 +0021: DTOI R65 ; 3:5 +0022: JUMP 4 ; 3:5 +0023: MOVE R67, R64 ; 10:5 +0024: LOADI R66, 258 ; 10:5 +0025: UPCALL 0, R66 ; 10:1, OUT +0026: EOF ; 0:0 ``` ## Output @@ -417,25 +407,24 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 1 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:14 -0004: CMPLEI R65, R65, R66 # 1:11 -0005: JMPF R65, 18 # 1:5 -0006: MOVE R65, R64 # 2:8 -0007: LOADI R66, 5 # 2:12 -0008: CMPEQI R65, R65, R66 # 2:10 -0009: JMPF R65, 11 # 2:8 -0010: JUMP 18 # 2:19 -0011: MOVE R66, R64 # 3:9 -0012: LOADI R65, 258 # 3:9 -0013: UPCALL 0, R65 # 3:5, OUT -0014: MOVE R64, R64 # 1:5 -0015: LOADI R65, 1 # 1:16 -0016: ADDI R64, R64, R65 # 1:11 -0017: JUMP 2 # 1:5 -0018: EOF # 0:0 +0000: LOADI R64, 1 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:14 +0003: CMPLEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 17 ; 1:5 +0005: MOVE R65, R64 ; 2:8 +0006: LOADI R66, 5 ; 2:12 +0007: CMPEQI R65, R65, R66 ; 2:10 +0008: JMPF R65, 10 ; 2:8 +0009: JUMP 17 ; 2:19 +0010: MOVE R66, R64 ; 3:9 +0011: LOADI R65, 258 ; 3:9 +0012: UPCALL 0, R65 ; 3:5, OUT +0013: MOVE R64, R64 ; 1:5 +0014: LOADI R65, 1 ; 1:16 +0015: ADDI R64, R64, R65 ; 1:11 +0016: JUMP 1 ; 1:5 +0017: EOF ; 0:0 ``` ## Output @@ -466,41 +455,40 @@ NEXT ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 1 # 1:9 -0002: MOVE R65, R64 # 1:5 -0003: LOADI R66, 10 # 1:14 -0004: CMPLEI R65, R65, R66 # 1:11 -0005: JMPF R65, 34 # 1:5 -0006: LOADI R65, 5 # 2:9 -0007: MOVE R66, R65 # 3:14 -0008: LOADI R67, 0 # 3:18 -0009: CMPGTI R66, R66, R67 # 3:16 -0010: JMPF R66, 30 # 3:14 -0011: MOVE R66, R65 # 4:12 -0012: LOADI R67, 2 # 4:16 -0013: CMPEQI R66, R66, R67 # 4:14 -0014: JMPF R66, 16 # 4:12 -0015: JUMP 30 # 4:23 -0016: MOVE R66, R64 # 5:12 -0017: LOADI R67, 4 # 5:16 -0018: CMPEQI R66, R66, R67 # 5:14 -0019: JMPF R66, 21 # 5:12 -0020: JUMP 34 # 5:23 -0021: MOVE R67, R64 # 6:13 -0022: LOADI R66, 274 # 6:13 -0023: MOVE R69, R65 # 6:16 -0024: LOADI R68, 258 # 6:16 -0025: UPCALL 0, R66 # 6:9, OUT -0026: MOVE R65, R65 # 7:13 -0027: LOADI R66, 1 # 7:17 -0028: SUBI R65, R65, R66 # 7:15 -0029: JUMP 7 # 3:14 -0030: MOVE R64, R64 # 1:5 -0031: LOADI R66, 1 # 1:16 -0032: ADDI R64, R64, R66 # 1:11 -0033: JUMP 2 # 1:5 -0034: EOF # 0:0 +0000: LOADI R64, 1 ; 1:9 +0001: MOVE R65, R64 ; 1:5 +0002: LOADI R66, 10 ; 1:14 +0003: CMPLEI R65, R65, R66 ; 1:11 +0004: JMPF R65, 33 ; 1:5 +0005: LOADI R65, 5 ; 2:9 +0006: MOVE R66, R65 ; 3:14 +0007: LOADI R67, 0 ; 3:18 +0008: CMPGTI R66, R66, R67 ; 3:16 +0009: JMPF R66, 29 ; 3:14 +0010: MOVE R66, R65 ; 4:12 +0011: LOADI R67, 2 ; 4:16 +0012: CMPEQI R66, R66, R67 ; 4:14 +0013: JMPF R66, 15 ; 4:12 +0014: JUMP 29 ; 4:23 +0015: MOVE R66, R64 ; 5:12 +0016: LOADI R67, 4 ; 5:16 +0017: CMPEQI R66, R66, R67 ; 5:14 +0018: JMPF R66, 20 ; 5:12 +0019: JUMP 33 ; 5:23 +0020: MOVE R67, R64 ; 6:13 +0021: LOADI R66, 274 ; 6:13 +0022: MOVE R69, R65 ; 6:16 +0023: LOADI R68, 258 ; 6:16 +0024: UPCALL 0, R66 ; 6:9, OUT +0025: MOVE R65, R65 ; 7:13 +0026: LOADI R66, 1 ; 7:17 +0027: SUBI R65, R65, R66 ; 7:15 +0028: JUMP 6 ; 3:14 +0029: MOVE R64, R64 ; 1:5 +0030: LOADI R66, 1 ; 1:16 +0031: ADDI R64, R64, R66 ; 1:11 +0032: JUMP 1 ; 1:5 +0033: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 9802bcbf..913d46d7 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -13,20 +13,18 @@ OUT foo$ ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 6 # 1:10 +0000: JUMP 4 ; 1:10 --- FOO (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 1 # 0:0 -0004: LOADI R64, 1 # 2:11 -0005: RETURN # 3:1 --- FOO (END) +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 1 ; 2:11 +0003: RETURN ; 3:1 +;; FOO (END) -0006: CALL R65, 2 # 5:5, FOO -0007: LOADI R64, 259 # 5:5 -0008: UPCALL 0, R64 # 5:1, OUT -0009: EOF # 0:0 +0004: CALL R65, 1 ; 5:5, FOO +0005: LOADI R64, 259 ; 5:5 +0006: UPCALL 0, R64 ; 5:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -74,39 +72,37 @@ OUT "After", a ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 10 # 1:5 -0002: JUMP 13 # 3:10 - --- FOO (BEGIN) -0003: LOADI R64, 0 # 3:10 -0004: ENTER 6 # 0:0 -0005: LOADI R65, 20 # 4:9 -0006: LOADI R67, 0 # 5:9 -0007: LOADI R66, 291 # 5:9 -0008: MOVE R69, R65 # 5:19 -0009: LOADI R68, 258 # 5:19 -0010: UPCALL 0, R66 # 5:5, OUT -0011: LOADI R64, 30 # 6:11 -0012: RETURN # 7:1 --- FOO (END) - -0013: LOADI R66, 1 # 9:5 -0014: LOADI R65, 291 # 9:5 -0015: MOVE R68, R64 # 9:15 -0016: LOADI R67, 258 # 9:15 -0017: UPCALL 0, R65 # 9:1, OUT -0018: LOADI R66, 2 # 10:5 -0019: LOADI R65, 291 # 10:5 -0020: CALL R68, 3 # 10:21, FOO -0021: LOADI R67, 258 # 10:21 -0022: UPCALL 0, R65 # 10:1, OUT -0023: LOADI R66, 3 # 11:5 -0024: LOADI R65, 291 # 11:5 -0025: MOVE R68, R64 # 11:14 -0026: LOADI R67, 258 # 11:14 -0027: UPCALL 0, R65 # 11:1, OUT -0028: EOF # 0:0 +0000: LOADI R64, 10 ; 1:5 +0001: JUMP 11 ; 3:10 + +;; FOO (BEGIN) +0002: LOADI R64, 0 ; 3:10 +0003: LOADI R65, 20 ; 4:9 +0004: LOADI R67, 0 ; 5:9 +0005: LOADI R66, 291 ; 5:9 +0006: MOVE R69, R65 ; 5:19 +0007: LOADI R68, 258 ; 5:19 +0008: UPCALL 0, R66 ; 5:5, OUT +0009: LOADI R64, 30 ; 6:11 +0010: RETURN ; 7:1 +;; FOO (END) + +0011: LOADI R66, 1 ; 9:5 +0012: LOADI R65, 291 ; 9:5 +0013: MOVE R68, R64 ; 9:15 +0014: LOADI R67, 258 ; 9:15 +0015: UPCALL 0, R65 ; 9:1, OUT +0016: LOADI R66, 2 ; 10:5 +0017: LOADI R65, 291 ; 10:5 +0018: CALL R68, 2 ; 10:21, FOO +0019: LOADI R67, 258 ; 10:21 +0020: UPCALL 0, R65 ; 10:1, OUT +0021: LOADI R66, 3 ; 11:5 +0022: LOADI R65, 291 ; 11:5 +0023: MOVE R68, R64 ; 11:14 +0024: LOADI R67, 258 ; 11:14 +0025: UPCALL 0, R65 ; 11:1, OUT +0026: EOF ; 0:0 ``` ## Output @@ -138,32 +134,29 @@ OUT second ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 9 # 1:10 - --- FIRST (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 3 # 0:0 -0004: LOADI R66, 0 # 2:9 -0005: LOADI R65, 259 # 2:9 -0006: UPCALL 0, R65 # 2:5, OUT -0007: LOADI R64, 123 # 3:13 -0008: RETURN # 4:1 --- FIRST (END) - -0009: JUMP 14 # 6:10 - --- SECOND (BEGIN) -0010: LOADI R64, 0 # 6:10 -0011: ENTER 1 # 0:0 -0012: CALL R64, 2 # 7:14, FIRST -0013: RETURN # 8:1 --- SECOND (END) - -0014: CALL R65, 10 # 10:5, SECOND -0015: LOADI R64, 258 # 10:5 -0016: UPCALL 0, R64 # 10:1, OUT -0017: EOF # 0:0 +0000: JUMP 7 ; 1:10 + +;; FIRST (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R66, 0 ; 2:9 +0003: LOADI R65, 259 ; 2:9 +0004: UPCALL 0, R65 ; 2:5, OUT +0005: LOADI R64, 123 ; 3:13 +0006: RETURN ; 4:1 +;; FIRST (END) + +0007: JUMP 11 ; 6:10 + +;; SECOND (BEGIN) +0008: LOADI R64, 0 ; 6:10 +0009: CALL R64, 1 ; 7:14, FIRST +0010: RETURN ; 8:1 +;; SECOND (END) + +0011: CALL R65, 8 ; 10:5, SECOND +0012: LOADI R64, 258 ; 10:5 +0013: UPCALL 0, R64 ; 10:1, OUT +0014: EOF ; 0:0 ``` ## Output @@ -200,55 +193,50 @@ OUT do_call ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 5 # 1:10 - --- DEFAULT_DOUBLE (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 1 # 0:0 -0004: RETURN # 2:1 --- DEFAULT_DOUBLE (END) - -0005: JUMP 9 # 4:10 - --- DEFAULT_INTEGER (BEGIN) -0006: LOADI R64, 0 # 4:10 -0007: ENTER 1 # 0:0 -0008: RETURN # 5:1 --- DEFAULT_INTEGER (END) - -0009: JUMP 13 # 7:10 - --- DEFAULT_STRING (BEGIN) -0010: LOADI R64, 1 # 7:10 -0011: ENTER 1 # 0:0 -0012: RETURN # 8:1 --- DEFAULT_STRING (END) - -0013: JUMP 29 # 10:10 - --- DO_CALL (BEGIN) -0014: LOADI R64, 0 # 10:10 -0015: ENTER 3 # 0:0 -0016: LOADI R66, 300 # 11:9 -0017: LOADI R65, 258 # 11:9 -0018: UPCALL 0, R65 # 11:5, OUT -0019: CALL R66, 2 # 12:9, DEFAULT_DOUBLE -0020: LOADI R65, 257 # 12:9 -0021: UPCALL 0, R65 # 12:5, OUT -0022: CALL R66, 6 # 13:9, DEFAULT_INTEGER -0023: LOADI R65, 258 # 13:9 -0024: UPCALL 0, R65 # 13:5, OUT -0025: CALL R66, 10 # 14:9, DEFAULT_STRING -0026: LOADI R65, 259 # 14:9 -0027: UPCALL 0, R65 # 14:5, OUT -0028: RETURN # 15:1 --- DO_CALL (END) - -0029: CALL R65, 14 # 17:5, DO_CALL -0030: LOADI R64, 258 # 17:5 -0031: UPCALL 0, R64 # 17:1, OUT -0032: EOF # 0:0 +0000: JUMP 3 ; 1:10 + +;; DEFAULT_DOUBLE (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: RETURN ; 2:1 +;; DEFAULT_DOUBLE (END) + +0003: JUMP 6 ; 4:10 + +;; DEFAULT_INTEGER (BEGIN) +0004: LOADI R64, 0 ; 4:10 +0005: RETURN ; 5:1 +;; DEFAULT_INTEGER (END) + +0006: JUMP 9 ; 7:10 + +;; DEFAULT_STRING (BEGIN) +0007: LOADI R64, 1 ; 7:10 +0008: RETURN ; 8:1 +;; DEFAULT_STRING (END) + +0009: JUMP 24 ; 10:10 + +;; DO_CALL (BEGIN) +0010: LOADI R64, 0 ; 10:10 +0011: LOADI R66, 300 ; 11:9 +0012: LOADI R65, 258 ; 11:9 +0013: UPCALL 0, R65 ; 11:5, OUT +0014: CALL R66, 1 ; 12:9, DEFAULT_DOUBLE +0015: LOADI R65, 257 ; 12:9 +0016: UPCALL 0, R65 ; 12:5, OUT +0017: CALL R66, 4 ; 13:9, DEFAULT_INTEGER +0018: LOADI R65, 258 ; 13:9 +0019: UPCALL 0, R65 ; 13:5, OUT +0020: CALL R66, 7 ; 14:9, DEFAULT_STRING +0021: LOADI R65, 259 ; 14:9 +0022: UPCALL 0, R65 ; 14:5, OUT +0023: RETURN ; 15:1 +;; DO_CALL (END) + +0024: CALL R65, 10 ; 17:5, DO_CALL +0025: LOADI R64, 258 ; 17:5 +0026: UPCALL 0, R64 ; 17:1, OUT +0027: EOF ; 0:0 ``` ## Output @@ -289,60 +277,57 @@ OUT "After modify_1", var ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: JUMP 12 # 1:10 - --- MODIFY_2 (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 6 # 0:0 -0004: LOADI R65, 300 # 2:11 -0005: LOADI R64, 2000 # 3:16 -0006: LOADI R67, 0 # 4:9 -0007: LOADI R66, 291 # 4:9 -0008: MOVE R69, R65 # 4:28 -0009: LOADI R68, 258 # 4:28 -0010: UPCALL 0, R66 # 4:5, OUT -0011: RETURN # 5:1 --- MODIFY_2 (END) - -0012: JUMP 31 # 7:10 - --- MODIFY_1 (BEGIN) -0013: LOADI R64, 0 # 7:10 -0014: ENTER 6 # 0:0 -0015: LOADI R65, 200 # 8:11 -0016: LOADI R67, 1 # 9:9 -0017: LOADI R66, 291 # 9:9 -0018: MOVE R69, R65 # 9:28 -0019: LOADI R68, 258 # 9:28 -0020: UPCALL 0, R66 # 9:5, OUT -0021: CALL R67, 2 # 10:9, MODIFY_2 -0022: LOADI R66, 258 # 10:9 -0023: UPCALL 0, R66 # 10:5, OUT -0024: LOADI R67, 2 # 11:9 -0025: LOADI R66, 291 # 11:9 -0026: MOVE R69, R65 # 11:27 -0027: LOADI R68, 258 # 11:27 -0028: UPCALL 0, R66 # 11:5, OUT -0029: LOADI R64, 1000 # 12:16 -0030: RETURN # 13:1 --- MODIFY_1 (END) - -0031: LOADI R64, 100 # 15:7 -0032: LOADI R66, 3 # 16:5 -0033: LOADI R65, 291 # 16:5 -0034: MOVE R68, R64 # 16:24 -0035: LOADI R67, 258 # 16:24 -0036: UPCALL 0, R65 # 16:1, OUT -0037: CALL R66, 13 # 17:5, MODIFY_1 -0038: LOADI R65, 258 # 17:5 -0039: UPCALL 0, R65 # 17:1, OUT -0040: LOADI R66, 4 # 18:5 -0041: LOADI R65, 291 # 18:5 -0042: MOVE R68, R64 # 18:23 -0043: LOADI R67, 258 # 18:23 -0044: UPCALL 0, R65 # 18:1, OUT -0045: EOF # 0:0 +0000: JUMP 10 ; 1:10 + +;; MODIFY_2 (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R65, 300 ; 2:11 +0003: LOADI R64, 2000 ; 3:16 +0004: LOADI R67, 0 ; 4:9 +0005: LOADI R66, 291 ; 4:9 +0006: MOVE R69, R65 ; 4:28 +0007: LOADI R68, 258 ; 4:28 +0008: UPCALL 0, R66 ; 4:5, OUT +0009: RETURN ; 5:1 +;; MODIFY_2 (END) + +0010: JUMP 28 ; 7:10 + +;; MODIFY_1 (BEGIN) +0011: LOADI R64, 0 ; 7:10 +0012: LOADI R65, 200 ; 8:11 +0013: LOADI R67, 1 ; 9:9 +0014: LOADI R66, 291 ; 9:9 +0015: MOVE R69, R65 ; 9:28 +0016: LOADI R68, 258 ; 9:28 +0017: UPCALL 0, R66 ; 9:5, OUT +0018: CALL R67, 1 ; 10:9, MODIFY_2 +0019: LOADI R66, 258 ; 10:9 +0020: UPCALL 0, R66 ; 10:5, OUT +0021: LOADI R67, 2 ; 11:9 +0022: LOADI R66, 291 ; 11:9 +0023: MOVE R69, R65 ; 11:27 +0024: LOADI R68, 258 ; 11:27 +0025: UPCALL 0, R66 ; 11:5, OUT +0026: LOADI R64, 1000 ; 12:16 +0027: RETURN ; 13:1 +;; MODIFY_1 (END) + +0028: LOADI R64, 100 ; 15:7 +0029: LOADI R66, 3 ; 16:5 +0030: LOADI R65, 291 ; 16:5 +0031: MOVE R68, R64 ; 16:24 +0032: LOADI R67, 258 ; 16:24 +0033: UPCALL 0, R65 ; 16:1, OUT +0034: CALL R66, 11 ; 17:5, MODIFY_1 +0035: LOADI R65, 258 ; 17:5 +0036: UPCALL 0, R65 ; 17:1, OUT +0037: LOADI R66, 4 ; 18:5 +0038: LOADI R65, 291 ; 18:5 +0039: MOVE R68, R64 ; 18:23 +0040: LOADI R67, 258 ; 18:23 +0041: UPCALL 0, R65 ; 18:1, OUT +0042: EOF ; 0:0 ``` ## Output @@ -391,30 +376,28 @@ OUT add(3, 5) + add(10, 20) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: JUMP 8 # 1:10 - --- ADD (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 4 # 0:0 -0004: MOVE R64, R65 # 2:11 -0005: MOVE R67, R66 # 2:15 -0006: ADDI R64, R64, R67 # 2:13 -0007: RETURN # 3:1 --- ADD (END) - -0008: LOADI R67, 3 # 5:9 -0009: LOADI R68, 5 # 5:12 -0010: CALL R66, 2 # 5:5, ADD -0011: MOVE R65, R66 # 5:5 -0012: LOADI R68, 10 # 5:21 -0013: LOADI R69, 20 # 5:25 -0014: CALL R67, 2 # 5:17, ADD -0015: MOVE R66, R67 # 5:17 -0016: ADDI R65, R65, R66 # 5:15 -0017: LOADI R64, 258 # 5:5 -0018: UPCALL 0, R64 # 5:1, OUT -0019: EOF # 0:0 +0000: JUMP 6 ; 1:10 + +;; ADD (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: MOVE R64, R65 ; 2:11 +0003: MOVE R67, R66 ; 2:15 +0004: ADDI R64, R64, R67 ; 2:13 +0005: RETURN ; 3:1 +;; ADD (END) + +0006: LOADI R67, 3 ; 5:9 +0007: LOADI R68, 5 ; 5:12 +0008: CALL R66, 1 ; 5:5, ADD +0009: MOVE R65, R66 ; 5:5 +0010: LOADI R68, 10 ; 5:21 +0011: LOADI R69, 20 ; 5:25 +0012: CALL R67, 1 ; 5:17, ADD +0013: MOVE R66, R67 ; 5:17 +0014: ADDI R65, R65, R66 ; 5:15 +0015: LOADI R64, 258 ; 5:5 +0016: UPCALL 0, R64 ; 5:1, OUT +0017: EOF ; 0:0 ``` ## Output @@ -440,26 +423,24 @@ OUT ret ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 8 # 1:10 - --- FOO (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 3 # 0:0 -0004: LOADI R64, 42 # 2:11 -0005: MOVE R66, R65 # 2:16 -0006: ADDI R64, R64, R66 # 2:14 -0007: RETURN # 3:1 --- FOO (END) - -0008: LOADI R0, 0 # 5:12 -0009: LOADI R65, 3 # 6:11 -0010: CALL R64, 2 # 6:7, FOO -0011: MOVE R0, R64 # 6:7 -0012: MOVE R65, R0 # 7:5 -0013: LOADI R64, 258 # 7:5 -0014: UPCALL 0, R64 # 7:1, OUT -0015: EOF # 0:0 +0000: JUMP 6 ; 1:10 + +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 42 ; 2:11 +0003: MOVE R66, R65 ; 2:16 +0004: ADDI R64, R64, R66 ; 2:14 +0005: RETURN ; 3:1 +;; FOO (END) + +0006: LOADI R0, 0 ; 5:12 +0007: LOADI R65, 3 ; 6:11 +0008: CALL R64, 1 ; 6:7, FOO +0009: MOVE R0, R64 ; 6:7 +0010: MOVE R65, R0 ; 7:5 +0011: LOADI R64, 258 ; 7:5 +0012: UPCALL 0, R64 ; 7:1, OUT +0013: EOF ; 0:0 ``` ## Output @@ -493,44 +474,41 @@ OUT s ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: JUMP 6 # 1:10 - --- CHANGE_INTEGER (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 2 # 0:0 -0004: LOADI R65, 3 # 2:9 -0005: RETURN # 3:1 --- CHANGE_INTEGER (END) - -0006: JUMP 11 # 5:10 - --- CHANGE_STRING (BEGIN) -0007: LOADI R64, 0 # 5:10 -0008: ENTER 2 # 0:0 -0009: LOADI R65, 0 # 6:9 -0010: RETURN # 7:1 --- CHANGE_STRING (END) - -0011: LOADI R64, 5 # 9:5 -0012: MOVE R68, R64 # 10:20 -0013: CALL R67, 2 # 10:5, CHANGE_INTEGER -0014: MOVE R66, R67 # 10:5 -0015: LOADI R65, 258 # 10:5 -0016: UPCALL 0, R65 # 10:1, OUT -0017: MOVE R66, R64 # 11:5 -0018: LOADI R65, 258 # 11:5 -0019: UPCALL 0, R65 # 11:1, OUT -0020: LOADI R65, 1 # 13:5 -0021: MOVE R69, R65 # 14:19 -0022: CALL R68, 7 # 14:5, CHANGE_STRING -0023: MOVE R67, R68 # 14:5 -0024: LOADI R66, 258 # 14:5 -0025: UPCALL 0, R66 # 14:1, OUT -0026: MOVE R67, R65 # 15:5 -0027: LOADI R66, 259 # 15:5 -0028: UPCALL 0, R66 # 15:1, OUT -0029: EOF # 0:0 +0000: JUMP 4 ; 1:10 + +;; CHANGE_INTEGER (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R65, 3 ; 2:9 +0003: RETURN ; 3:1 +;; CHANGE_INTEGER (END) + +0004: JUMP 8 ; 5:10 + +;; CHANGE_STRING (BEGIN) +0005: LOADI R64, 0 ; 5:10 +0006: LOADI R65, 0 ; 6:9 +0007: RETURN ; 7:1 +;; CHANGE_STRING (END) + +0008: LOADI R64, 5 ; 9:5 +0009: MOVE R68, R64 ; 10:20 +0010: CALL R67, 1 ; 10:5, CHANGE_INTEGER +0011: MOVE R66, R67 ; 10:5 +0012: LOADI R65, 258 ; 10:5 +0013: UPCALL 0, R65 ; 10:1, OUT +0014: MOVE R66, R64 ; 11:5 +0015: LOADI R65, 258 ; 11:5 +0016: UPCALL 0, R65 ; 11:1, OUT +0017: LOADI R65, 1 ; 13:5 +0018: MOVE R69, R65 ; 14:19 +0019: CALL R68, 5 ; 14:5, CHANGE_STRING +0020: MOVE R67, R68 ; 14:5 +0021: LOADI R66, 258 ; 14:5 +0022: UPCALL 0, R66 ; 14:1, OUT +0023: MOVE R67, R65 ; 15:5 +0024: LOADI R66, 259 ; 15:5 +0025: UPCALL 0, R66 ; 15:1, OUT +0026: EOF ; 0:0 ``` ## Output @@ -553,18 +531,17 @@ OUT SUM_DOUBLES(3.4, 2, 7.1) ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADC R68, 0 # 1:17 -0002: LOADI R67, 289 # 1:17 -0003: LOADI R70, 2 # 1:22 -0004: LOADI R69, 290 # 1:22 -0005: LOADC R72, 1 # 1:25 -0006: LOADI R71, 257 # 1:25 -0007: UPCALL 0, R66 # 1:5, SUM_DOUBLES -0008: MOVE R65, R66 # 1:5 -0009: LOADI R64, 257 # 1:5 -0010: UPCALL 1, R64 # 1:1, OUT -0011: EOF # 0:0 +0000: LOADC R68, 0 ; 1:17 +0001: LOADI R67, 289 ; 1:17 +0002: LOADI R70, 2 ; 1:22 +0003: LOADI R69, 290 ; 1:22 +0004: LOADC R72, 1 ; 1:25 +0005: LOADI R71, 257 ; 1:25 +0006: UPCALL 0, R66 ; 1:5, SUM_DOUBLES +0007: MOVE R65, R66 ; 1:5 +0008: LOADI R64, 257 ; 1:5 +0009: UPCALL 1, R64 ; 1:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -584,18 +561,17 @@ OUT SUM_INTEGERS(3, 2, 7) ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADI R68, 3 # 1:18 -0002: LOADI R67, 290 # 1:18 -0003: LOADI R70, 2 # 1:21 -0004: LOADI R69, 290 # 1:21 -0005: LOADI R72, 7 # 1:24 -0006: LOADI R71, 258 # 1:24 -0007: UPCALL 0, R66 # 1:5, SUM_INTEGERS -0008: MOVE R65, R66 # 1:5 -0009: LOADI R64, 258 # 1:5 -0010: UPCALL 1, R64 # 1:1, OUT -0011: EOF # 0:0 +0000: LOADI R68, 3 ; 1:18 +0001: LOADI R67, 290 ; 1:18 +0002: LOADI R70, 2 ; 1:21 +0003: LOADI R69, 290 ; 1:21 +0004: LOADI R72, 7 ; 1:24 +0005: LOADI R71, 258 ; 1:24 +0006: UPCALL 0, R66 ; 1:5, SUM_INTEGERS +0007: MOVE R65, R66 ; 1:5 +0008: LOADI R64, 258 ; 1:5 +0009: UPCALL 1, R64 ; 1:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -615,18 +591,17 @@ OUT CONCAT$("hello", " ", "world") ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADI R68, 0 # 1:13 -0002: LOADI R67, 291 # 1:13 -0003: LOADI R70, 1 # 1:22 -0004: LOADI R69, 291 # 1:22 -0005: LOADI R72, 2 # 1:27 -0006: LOADI R71, 259 # 1:27 -0007: UPCALL 0, R66 # 1:5, CONCAT -0008: MOVE R65, R66 # 1:5 -0009: LOADI R64, 259 # 1:5 -0010: UPCALL 1, R64 # 1:1, OUT -0011: EOF # 0:0 +0000: LOADI R68, 0 ; 1:13 +0001: LOADI R67, 291 ; 1:13 +0002: LOADI R70, 1 ; 1:22 +0003: LOADI R69, 291 ; 1:22 +0004: LOADI R72, 2 ; 1:27 +0005: LOADI R71, 259 ; 1:27 +0006: UPCALL 0, R66 ; 1:5, CONCAT +0007: MOVE R65, R66 ; 1:5 +0008: LOADI R64, 259 ; 1:5 +0009: UPCALL 1, R64 ; 1:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -646,13 +621,12 @@ OUT IS_POSITIVE?(42) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R67, 42 # 1:18 -0002: UPCALL 0, R66 # 1:5, IS_POSITIVE -0003: MOVE R65, R66 # 1:5 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 1, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R67, 42 ; 1:18 +0001: UPCALL 0, R66 ; 1:5, IS_POSITIVE +0002: MOVE R65, R66 ; 1:5 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 1, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -672,11 +646,10 @@ OUT MEANING_OF_LIFE ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: UPCALL 0, R65 # 1:5, MEANING_OF_LIFE -0002: LOADI R64, 258 # 1:5 -0003: UPCALL 1, R64 # 1:1, OUT -0004: EOF # 0:0 +0000: UPCALL 0, R65 ; 1:5, MEANING_OF_LIFE +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 1, R64 ; 1:1, OUT +0003: EOF ; 0:0 ``` ## Output @@ -698,18 +671,17 @@ OUT x ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADC R66, 0 # 2:17 -0003: LOADI R65, 289 # 2:17 -0004: LOADC R68, 1 # 2:22 -0005: LOADI R67, 257 # 2:22 -0006: UPCALL 0, R64 # 2:5, SUM_DOUBLES -0007: MOVE R0, R64 # 2:5 -0008: MOVE R65, R0 # 3:5 -0009: LOADI R64, 257 # 3:5 -0010: UPCALL 1, R64 # 3:1, OUT -0011: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADC R66, 0 ; 2:17 +0002: LOADI R65, 289 ; 2:17 +0003: LOADC R68, 1 ; 2:22 +0004: LOADI R67, 257 ; 2:22 +0005: UPCALL 0, R64 ; 2:5, SUM_DOUBLES +0006: MOVE R0, R64 ; 2:5 +0007: MOVE R65, R0 ; 3:5 +0008: LOADI R64, 257 ; 3:5 +0009: UPCALL 1, R64 ; 3:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -731,14 +703,13 @@ OUT x ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: UPCALL 0, R64 # 2:5, MEANING_OF_LIFE -0003: MOVE R0, R64 # 2:5 -0004: MOVE R65, R0 # 3:5 -0005: LOADI R64, 258 # 3:5 -0006: UPCALL 1, R64 # 3:1, OUT -0007: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: UPCALL 0, R64 ; 2:5, MEANING_OF_LIFE +0002: MOVE R0, R64 ; 2:5 +0003: MOVE R65, R0 ; 3:5 +0004: LOADI R64, 258 ; 3:5 +0005: UPCALL 1, R64 ; 3:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -758,23 +729,22 @@ OUT SUM_DOUBLES(1.0, 2.0) + SUM_DOUBLES(3.0, 4.0) ## Disassembly ```asm -0000: ENTER 8 # 0:0 -0001: LOADC R68, 0 # 1:17 -0002: LOADI R67, 289 # 1:17 -0003: LOADC R70, 1 # 1:22 -0004: LOADI R69, 257 # 1:22 -0005: UPCALL 0, R66 # 1:5, SUM_DOUBLES -0006: MOVE R65, R66 # 1:5 -0007: LOADC R69, 2 # 1:41 -0008: LOADI R68, 289 # 1:41 -0009: LOADC R71, 3 # 1:46 -0010: LOADI R70, 257 # 1:46 -0011: UPCALL 0, R67 # 1:29, SUM_DOUBLES -0012: MOVE R66, R67 # 1:29 -0013: ADDD R65, R65, R66 # 1:27 -0014: LOADI R64, 257 # 1:5 -0015: UPCALL 1, R64 # 1:1, OUT -0016: EOF # 0:0 +0000: LOADC R68, 0 ; 1:17 +0001: LOADI R67, 289 ; 1:17 +0002: LOADC R70, 1 ; 1:22 +0003: LOADI R69, 257 ; 1:22 +0004: UPCALL 0, R66 ; 1:5, SUM_DOUBLES +0005: MOVE R65, R66 ; 1:5 +0006: LOADC R69, 2 ; 1:41 +0007: LOADI R68, 289 ; 1:41 +0008: LOADC R71, 3 ; 1:46 +0009: LOADI R70, 257 ; 1:46 +0010: UPCALL 0, R67 ; 1:29, SUM_DOUBLES +0011: MOVE R66, R67 ; 1:29 +0012: ADDD R65, R65, R66 ; 1:27 +0013: LOADI R64, 257 ; 1:5 +0014: UPCALL 1, R64 ; 1:1, OUT +0015: EOF ; 0:0 ``` ## Output @@ -876,37 +846,35 @@ NEXT ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: JUMP 12 # 1:10 - --- MAYBE_EXIT (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 4 # 0:0 -0004: LOADI R64, 1 # 2:18 -0005: MOVE R66, R65 # 3:8 -0006: LOADI R67, 2 # 3:12 -0007: CMPGTI R66, R66, R67 # 3:10 -0008: JMPF R66, 10 # 3:8 -0009: JUMP 11 # 3:19 -0010: LOADI R64, 2 # 4:18 -0011: RETURN # 5:1 --- MAYBE_EXIT (END) - -0012: LOADI R64, 0 # 7:9 -0013: MOVE R65, R64 # 7:5 -0014: LOADI R66, 5 # 7:14 -0015: CMPLEI R65, R65, R66 # 7:11 -0016: JMPF R65, 26 # 7:5 -0017: MOVE R68, R64 # 8:20 -0018: CALL R67, 2 # 8:9, MAYBE_EXIT -0019: MOVE R66, R67 # 8:9 -0020: LOADI R65, 258 # 8:9 -0021: UPCALL 0, R65 # 8:5, OUT -0022: MOVE R64, R64 # 7:5 -0023: LOADI R65, 1 # 7:15 -0024: ADDI R64, R64, R65 # 7:11 -0025: JUMP 13 # 7:5 -0026: EOF # 0:0 +0000: JUMP 10 ; 1:10 + +;; MAYBE_EXIT (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 1 ; 2:18 +0003: MOVE R66, R65 ; 3:8 +0004: LOADI R67, 2 ; 3:12 +0005: CMPGTI R66, R66, R67 ; 3:10 +0006: JMPF R66, 8 ; 3:8 +0007: JUMP 9 ; 3:19 +0008: LOADI R64, 2 ; 4:18 +0009: RETURN ; 5:1 +;; MAYBE_EXIT (END) + +0010: LOADI R64, 0 ; 7:9 +0011: MOVE R65, R64 ; 7:5 +0012: LOADI R66, 5 ; 7:14 +0013: CMPLEI R65, R65, R66 ; 7:11 +0014: JMPF R65, 24 ; 7:5 +0015: MOVE R68, R64 ; 8:20 +0016: CALL R67, 1 ; 8:9, MAYBE_EXIT +0017: MOVE R66, R67 ; 8:9 +0018: LOADI R65, 258 ; 8:9 +0019: UPCALL 0, R65 ; 8:5, OUT +0020: MOVE R64, R64 ; 7:5 +0021: LOADI R65, 1 ; 7:15 +0022: ADDI R64, R64, R65 ; 7:11 +0023: JUMP 11 ; 7:5 +0024: EOF ; 0:0 ``` ## Output @@ -968,42 +936,40 @@ OUT calls; factorial(5) ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: JUMP 24 # 2:10 - --- FACTORIAL (BEGIN) -0003: LOADI R64, 0 # 2:10 -0004: ENTER 6 # 0:0 -0005: MOVE R66, R65 # 3:8 -0006: LOADI R67, 1 # 3:12 -0007: CMPEQI R66, R66, R67 # 3:10 -0008: JMPF R66, 11 # 3:8 -0009: LOADI R64, 1 # 3:31 -0010: JUMP 20 # 3:8 -0011: LOADI R66, 1 # 3:33 -0012: JMPF R66, 20 # 3:33 -0013: MOVE R64, R65 # 3:50 -0014: MOVE R68, R65 # 3:64 -0015: LOADI R69, 1 # 3:68 -0016: SUBI R68, R68, R69 # 3:66 -0017: CALL R67, 3 # 3:54, FACTORIAL -0018: MOVE R66, R67 # 3:54 -0019: MULI R64, R64, R66 # 3:52 -0020: MOVE R0, R0 # 4:13 -0021: LOADI R66, 1 # 4:21 -0022: ADDI R0, R0, R66 # 4:19 -0023: RETURN # 5:1 --- FACTORIAL (END) - -0024: MOVE R65, R0 # 6:5 -0025: LOADI R64, 274 # 6:5 -0026: LOADI R69, 5 # 6:22 -0027: CALL R68, 3 # 6:12, FACTORIAL -0028: MOVE R67, R68 # 6:12 -0029: LOADI R66, 258 # 6:12 -0030: UPCALL 0, R64 # 6:1, OUT -0031: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: JUMP 22 ; 2:10 + +;; FACTORIAL (BEGIN) +0002: LOADI R64, 0 ; 2:10 +0003: MOVE R66, R65 ; 3:8 +0004: LOADI R67, 1 ; 3:12 +0005: CMPEQI R66, R66, R67 ; 3:10 +0006: JMPF R66, 9 ; 3:8 +0007: LOADI R64, 1 ; 3:31 +0008: JUMP 18 ; 3:8 +0009: LOADI R66, 1 ; 3:33 +0010: JMPF R66, 18 ; 3:33 +0011: MOVE R64, R65 ; 3:50 +0012: MOVE R68, R65 ; 3:64 +0013: LOADI R69, 1 ; 3:68 +0014: SUBI R68, R68, R69 ; 3:66 +0015: CALL R67, 2 ; 3:54, FACTORIAL +0016: MOVE R66, R67 ; 3:54 +0017: MULI R64, R64, R66 ; 3:52 +0018: MOVE R0, R0 ; 4:13 +0019: LOADI R66, 1 ; 4:21 +0020: ADDI R0, R0, R66 ; 4:19 +0021: RETURN ; 5:1 +;; FACTORIAL (END) + +0022: MOVE R65, R0 ; 6:5 +0023: LOADI R64, 274 ; 6:5 +0024: LOADI R69, 5 ; 6:22 +0025: CALL R68, 2 ; 6:12, FACTORIAL +0026: MOVE R67, R68 ; 6:12 +0027: LOADI R66, 258 ; 6:12 +0028: UPCALL 0, R64 ; 6:1, OUT +0029: EOF ; 0:0 ``` ## Output @@ -1043,69 +1009,66 @@ OUT ping(3) ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: JUMP 25 # 3:10 - --- PING (BEGIN) -0002: LOADI R64, 0 # 3:10 -0003: ENTER 6 # 0:0 -0004: LOADI R67, 0 # 4:9 -0005: LOADI R66, 275 # 4:9 -0006: MOVE R69, R65 # 4:17 -0007: LOADI R68, 258 # 4:17 -0008: UPCALL 0, R66 # 4:5, OUT -0009: MOVE R66, R65 # 5:8 -0010: LOADI R67, 0 # 5:12 -0011: CMPEQI R66, R66, R67 # 5:10 -0012: JMPF R66, 15 # 5:8 -0013: LOADI R64, 100 # 6:16 -0014: JUMP 24 # 5:8 -0015: LOADI R66, 1 # 7:5 -0016: JMPF R66, 24 # 7:5 -0017: MOVE R67, R65 # 8:21 -0018: LOADI R68, 1 # 8:25 -0019: SUBI R67, R67, R68 # 8:23 -0020: CALL R66, 26 # 8:16, PONG -0021: MOVE R64, R66 # 8:16 -0022: LOADI R66, 1 # 8:30 -0023: ADDI R64, R64, R66 # 8:28 -0024: RETURN # 10:1 --- PING (END) - -0025: JUMP 49 # 12:10 - --- PONG (BEGIN) -0026: LOADI R64, 0 # 12:10 -0027: ENTER 6 # 0:0 -0028: LOADI R67, 1 # 13:9 -0029: LOADI R66, 275 # 13:9 -0030: MOVE R69, R65 # 13:17 -0031: LOADI R68, 258 # 13:17 -0032: UPCALL 0, R66 # 13:5, OUT -0033: MOVE R66, R65 # 14:8 -0034: LOADI R67, 0 # 14:12 -0035: CMPEQI R66, R66, R67 # 14:10 -0036: JMPF R66, 39 # 14:8 -0037: LOADI R64, 200 # 15:16 -0038: JUMP 48 # 14:8 -0039: LOADI R66, 1 # 16:5 -0040: JMPF R66, 48 # 16:5 -0041: MOVE R67, R65 # 17:21 -0042: LOADI R68, 1 # 17:25 -0043: SUBI R67, R67, R68 # 17:23 -0044: CALL R66, 2 # 17:16, PING -0045: MOVE R64, R66 # 17:16 -0046: LOADI R66, 10 # 17:30 -0047: ADDI R64, R64, R66 # 17:28 -0048: RETURN # 19:1 --- PONG (END) - -0049: LOADI R67, 3 # 21:10 -0050: CALL R66, 2 # 21:5, PING -0051: MOVE R65, R66 # 21:5 -0052: LOADI R64, 258 # 21:5 -0053: UPCALL 0, R64 # 21:1, OUT -0054: EOF # 0:0 +0000: JUMP 23 ; 3:10 + +;; PING (BEGIN) +0001: LOADI R64, 0 ; 3:10 +0002: LOADI R67, 0 ; 4:9 +0003: LOADI R66, 275 ; 4:9 +0004: MOVE R69, R65 ; 4:17 +0005: LOADI R68, 258 ; 4:17 +0006: UPCALL 0, R66 ; 4:5, OUT +0007: MOVE R66, R65 ; 5:8 +0008: LOADI R67, 0 ; 5:12 +0009: CMPEQI R66, R66, R67 ; 5:10 +0010: JMPF R66, 13 ; 5:8 +0011: LOADI R64, 100 ; 6:16 +0012: JUMP 22 ; 5:8 +0013: LOADI R66, 1 ; 7:5 +0014: JMPF R66, 22 ; 7:5 +0015: MOVE R67, R65 ; 8:21 +0016: LOADI R68, 1 ; 8:25 +0017: SUBI R67, R67, R68 ; 8:23 +0018: CALL R66, 24 ; 8:16, PONG +0019: MOVE R64, R66 ; 8:16 +0020: LOADI R66, 1 ; 8:30 +0021: ADDI R64, R64, R66 ; 8:28 +0022: RETURN ; 10:1 +;; PING (END) + +0023: JUMP 46 ; 12:10 + +;; PONG (BEGIN) +0024: LOADI R64, 0 ; 12:10 +0025: LOADI R67, 1 ; 13:9 +0026: LOADI R66, 275 ; 13:9 +0027: MOVE R69, R65 ; 13:17 +0028: LOADI R68, 258 ; 13:17 +0029: UPCALL 0, R66 ; 13:5, OUT +0030: MOVE R66, R65 ; 14:8 +0031: LOADI R67, 0 ; 14:12 +0032: CMPEQI R66, R66, R67 ; 14:10 +0033: JMPF R66, 36 ; 14:8 +0034: LOADI R64, 200 ; 15:16 +0035: JUMP 45 ; 14:8 +0036: LOADI R66, 1 ; 16:5 +0037: JMPF R66, 45 ; 16:5 +0038: MOVE R67, R65 ; 17:21 +0039: LOADI R68, 1 ; 17:25 +0040: SUBI R67, R67, R68 ; 17:23 +0041: CALL R66, 1 ; 17:16, PING +0042: MOVE R64, R66 ; 17:16 +0043: LOADI R66, 10 ; 17:30 +0044: ADDI R64, R64, R66 ; 17:28 +0045: RETURN ; 19:1 +;; PONG (END) + +0046: LOADI R67, 3 ; 21:10 +0047: CALL R66, 1 ; 21:5, PING +0048: MOVE R65, R66 ; 21:5 +0049: LOADI R64, 258 ; 21:5 +0050: UPCALL 0, R64 ; 21:1, OUT +0051: EOF ; 0:0 ``` ## Output @@ -1246,8 +1209,7 @@ DECLARE FUNCTION bar(a AS STRING) ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: EOF # 0:0 +0000: EOF ; 0:0 ``` # Test: Function declarations match definition @@ -1266,16 +1228,14 @@ DECLARE FUNCTION foo ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: JUMP 5 # 3:10 +0000: JUMP 3 ; 3:10 --- FOO (BEGIN) -0002: LOADI R64, 0 # 3:10 -0003: ENTER 1 # 0:0 -0004: RETURN # 4:1 --- FOO (END) +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 3:10 +0002: RETURN ; 4:1 +;; FOO (END) -0005: EOF # 0:0 +0003: EOF ; 0:0 ``` # Test: Function declarations must be top-level diff --git a/core2/tests/test_globals.md b/core2/tests/test_globals.md index 3503dd67..5b9aa4b1 100644 --- a/core2/tests/test_globals.md +++ b/core2/tests/test_globals.md @@ -15,24 +15,23 @@ OUT b1, d1, i1, i2, s1 ## Disassembly ```asm -0000: ENTER 10 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R1, 0 # 2:12 -0003: LOADI R2, 0 # 3:12 -0004: LOADI R3, 0 # 4:12 -0005: ALLOC R4, STRING # 5:12 -0006: MOVE R65, R0 # 7:5 -0007: LOADI R64, 288 # 7:5 -0008: MOVE R67, R1 # 7:9 -0009: LOADI R66, 289 # 7:9 -0010: MOVE R69, R2 # 7:13 -0011: LOADI R68, 290 # 7:13 -0012: MOVE R71, R3 # 7:17 -0013: LOADI R70, 290 # 7:17 -0014: MOVE R73, R4 # 7:21 -0015: LOADI R72, 259 # 7:21 -0016: UPCALL 0, R64 # 7:1, OUT -0017: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R1, 0 ; 2:12 +0002: LOADI R2, 0 ; 3:12 +0003: LOADI R3, 0 ; 4:12 +0004: ALLOC R4, STRING ; 5:12 +0005: MOVE R65, R0 ; 7:5 +0006: LOADI R64, 288 ; 7:5 +0007: MOVE R67, R1 ; 7:9 +0008: LOADI R66, 289 ; 7:9 +0009: MOVE R69, R2 ; 7:13 +0010: LOADI R68, 290 ; 7:13 +0011: MOVE R71, R3 ; 7:17 +0012: LOADI R70, 290 ; 7:17 +0013: MOVE R73, R4 ; 7:21 +0014: LOADI R72, 259 ; 7:21 +0015: UPCALL 0, R64 ; 7:1, OUT +0016: EOF ; 0:0 ``` ## Output @@ -63,29 +62,28 @@ OUT b1, d1, i1, i2, s1 ## Disassembly ```asm -0000: ENTER 10 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R1, 0 # 2:12 -0003: LOADI R2, 0 # 3:12 -0004: LOADI R3, 0 # 4:12 -0005: ALLOC R4, STRING # 5:12 -0006: LOADI R0, 1 # 7:6 -0007: LOADC R1, 0 # 8:6 -0008: LOADI R2, 5 # 9:6 -0009: LOADI R3, 7 # 10:6 -0010: LOADI R4, 1 # 11:6 -0011: MOVE R65, R0 # 12:5 -0012: LOADI R64, 288 # 12:5 -0013: MOVE R67, R1 # 12:9 -0014: LOADI R66, 289 # 12:9 -0015: MOVE R69, R2 # 12:13 -0016: LOADI R68, 290 # 12:13 -0017: MOVE R71, R3 # 12:17 -0018: LOADI R70, 290 # 12:17 -0019: MOVE R73, R4 # 12:21 -0020: LOADI R72, 259 # 12:21 -0021: UPCALL 0, R64 # 12:1, OUT -0022: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R1, 0 ; 2:12 +0002: LOADI R2, 0 ; 3:12 +0003: LOADI R3, 0 ; 4:12 +0004: ALLOC R4, STRING ; 5:12 +0005: LOADI R0, 1 ; 7:6 +0006: LOADC R1, 0 ; 8:6 +0007: LOADI R2, 5 ; 9:6 +0008: LOADI R3, 7 ; 10:6 +0009: LOADI R4, 1 ; 11:6 +0010: MOVE R65, R0 ; 12:5 +0011: LOADI R64, 288 ; 12:5 +0012: MOVE R67, R1 ; 12:9 +0013: LOADI R66, 289 ; 12:9 +0014: MOVE R69, R2 ; 12:13 +0015: LOADI R68, 290 ; 12:13 +0016: MOVE R71, R3 ; 12:17 +0017: LOADI R70, 290 ; 12:17 +0018: MOVE R73, R4 ; 12:21 +0019: LOADI R72, 259 ; 12:21 +0020: UPCALL 0, R64 ; 12:1, OUT +0021: EOF ; 0:0 ``` ## Output @@ -115,37 +113,35 @@ OUT "After", i1 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: JUMP 12 # 3:10 - --- MODIFY_GLOBAL (BEGIN) -0003: LOADI R64, 0 # 3:10 -0004: ENTER 5 # 0:0 -0005: LOADI R0, 3 # 4:10 -0006: LOADI R66, 0 # 5:9 -0007: LOADI R65, 291 # 5:9 -0008: MOVE R68, R0 # 5:25 -0009: LOADI R67, 258 # 5:25 -0010: UPCALL 0, R65 # 5:5, OUT -0011: RETURN # 6:1 --- MODIFY_GLOBAL (END) - -0012: LOADI R0, 2 # 8:6 -0013: LOADI R65, 1 # 9:5 -0014: LOADI R64, 291 # 9:5 -0015: MOVE R67, R0 # 9:15 -0016: LOADI R66, 258 # 9:15 -0017: UPCALL 0, R64 # 9:1, OUT -0018: CALL R65, 3 # 10:5, MODIFY_GLOBAL -0019: LOADI R64, 258 # 10:5 -0020: UPCALL 0, R64 # 10:1, OUT -0021: LOADI R65, 2 # 11:5 -0022: LOADI R64, 291 # 11:5 -0023: MOVE R67, R0 # 11:14 -0024: LOADI R66, 258 # 11:14 -0025: UPCALL 0, R64 # 11:1, OUT -0026: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: JUMP 10 ; 3:10 + +;; MODIFY_GLOBAL (BEGIN) +0002: LOADI R64, 0 ; 3:10 +0003: LOADI R0, 3 ; 4:10 +0004: LOADI R66, 0 ; 5:9 +0005: LOADI R65, 291 ; 5:9 +0006: MOVE R68, R0 ; 5:25 +0007: LOADI R67, 258 ; 5:25 +0008: UPCALL 0, R65 ; 5:5, OUT +0009: RETURN ; 6:1 +;; MODIFY_GLOBAL (END) + +0010: LOADI R0, 2 ; 8:6 +0011: LOADI R65, 1 ; 9:5 +0012: LOADI R64, 291 ; 9:5 +0013: MOVE R67, R0 ; 9:15 +0014: LOADI R66, 258 ; 9:15 +0015: UPCALL 0, R64 ; 9:1, OUT +0016: CALL R65, 2 ; 10:5, MODIFY_GLOBAL +0017: LOADI R64, 258 ; 10:5 +0018: UPCALL 0, R64 ; 10:1, OUT +0019: LOADI R65, 2 ; 11:5 +0020: LOADI R64, 291 ; 11:5 +0021: MOVE R67, R0 ; 11:14 +0022: LOADI R66, 258 ; 11:14 +0023: UPCALL 0, R64 ; 11:1, OUT +0024: EOF ; 0:0 ``` ## Output @@ -178,34 +174,32 @@ OUT "After", i1 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: JUMP 11 # 3:5 - --- MODIFY_GLOBAL (BEGIN) -0003: ENTER 4 # 0:0 -0004: LOADI R0, 3 # 4:10 -0005: LOADI R65, 0 # 5:9 -0006: LOADI R64, 291 # 5:9 -0007: MOVE R67, R0 # 5:25 -0008: LOADI R66, 258 # 5:25 -0009: UPCALL 0, R64 # 5:5, OUT -0010: RETURN # 6:1 --- MODIFY_GLOBAL (END) - -0011: LOADI R0, 2 # 8:6 -0012: LOADI R65, 1 # 9:5 -0013: LOADI R64, 291 # 9:5 -0014: MOVE R67, R0 # 9:15 -0015: LOADI R66, 258 # 9:15 -0016: UPCALL 0, R64 # 9:1, OUT -0017: CALL R64, 3 # 10:1, MODIFY_GLOBAL -0018: LOADI R65, 2 # 11:5 -0019: LOADI R64, 291 # 11:5 -0020: MOVE R67, R0 # 11:14 -0021: LOADI R66, 258 # 11:14 -0022: UPCALL 0, R64 # 11:1, OUT -0023: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: JUMP 9 ; 3:5 + +;; MODIFY_GLOBAL (BEGIN) +0002: LOADI R0, 3 ; 4:10 +0003: LOADI R65, 0 ; 5:9 +0004: LOADI R64, 291 ; 5:9 +0005: MOVE R67, R0 ; 5:25 +0006: LOADI R66, 258 ; 5:25 +0007: UPCALL 0, R64 ; 5:5, OUT +0008: RETURN ; 6:1 +;; MODIFY_GLOBAL (END) + +0009: LOADI R0, 2 ; 8:6 +0010: LOADI R65, 1 ; 9:5 +0011: LOADI R64, 291 ; 9:5 +0012: MOVE R67, R0 ; 9:15 +0013: LOADI R66, 258 ; 9:15 +0014: UPCALL 0, R64 ; 9:1, OUT +0015: CALL R64, 2 ; 10:1, MODIFY_GLOBAL +0016: LOADI R65, 2 ; 11:5 +0017: LOADI R64, 291 ; 11:5 +0018: MOVE R67, R0 ; 11:14 +0019: LOADI R66, 258 ; 11:14 +0020: UPCALL 0, R64 ; 11:1, OUT +0021: EOF ; 0:0 ``` ## Output @@ -229,14 +223,13 @@ OUT d ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R0, 6 # 2:5 -0003: ITOD R0 # 2:5 -0004: MOVE R65, R0 # 3:5 -0005: LOADI R64, 257 # 3:5 -0006: UPCALL 0, R64 # 3:1, OUT -0007: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R0, 6 ; 2:5 +0002: ITOD R0 ; 2:5 +0003: MOVE R65, R0 ; 3:5 +0004: LOADI R64, 257 ; 3:5 +0005: UPCALL 0, R64 ; 3:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -260,19 +253,18 @@ OUT i1, i2 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: LOADI R1, 0 # 2:12 -0003: LOADC R0, 0 # 3:6 -0004: DTOI R0 # 3:6 -0005: LOADC R1, 1 # 4:6 -0006: DTOI R1 # 4:6 -0007: MOVE R65, R0 # 5:5 -0008: LOADI R64, 290 # 5:5 -0009: MOVE R67, R1 # 5:9 -0010: LOADI R66, 258 # 5:9 -0011: UPCALL 0, R64 # 5:1, OUT -0012: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: LOADI R1, 0 ; 2:12 +0002: LOADC R0, 0 ; 3:6 +0003: DTOI R0 ; 3:6 +0004: LOADC R1, 1 ; 4:6 +0005: DTOI R1 ; 4:6 +0006: MOVE R65, R0 ; 5:5 +0007: LOADI R64, 290 ; 5:5 +0008: MOVE R67, R1 ; 5:9 +0009: LOADI R66, 258 ; 5:9 +0010: UPCALL 0, R64 ; 5:1, OUT +0011: EOF ; 0:0 ``` ## Output @@ -328,24 +320,22 @@ OUT foo ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 10 # 1:10 - --- FOO (BEGIN) -0002: LOADI R64, 0 # 1:10 -0003: ENTER 4 # 0:0 -0004: LOADI R65, 5 # 2:9 -0005: LOADI R0, 0 # 3:16 -0006: MOVE R67, R65 # 4:9 -0007: LOADI R66, 258 # 4:9 -0008: UPCALL 0, R66 # 4:5, OUT -0009: RETURN # 5:1 --- FOO (END) - -0010: CALL R65, 2 # 7:5, FOO -0011: LOADI R64, 258 # 7:5 -0012: UPCALL 0, R64 # 7:1, OUT -0013: EOF # 0:0 +0000: JUMP 8 ; 1:10 + +;; FOO (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R65, 5 ; 2:9 +0003: LOADI R0, 0 ; 3:16 +0004: MOVE R67, R65 ; 4:9 +0005: LOADI R66, 258 ; 4:9 +0006: UPCALL 0, R66 ; 4:5, OUT +0007: RETURN ; 5:1 +;; FOO (END) + +0008: CALL R65, 1 ; 7:5, FOO +0009: LOADI R64, 258 ; 7:5 +0010: UPCALL 0, R64 ; 7:1, OUT +0011: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_gosub.md b/core2/tests/test_gosub.md index de518c0f..59537a94 100644 --- a/core2/tests/test_gosub.md +++ b/core2/tests/test_gosub.md @@ -16,21 +16,20 @@ RETURN ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R64, 259 # 1:5 -0003: UPCALL 0, R64 # 1:1, OUT -0004: GOSUB 10 # 2:7 -0005: LOADI R65, 1 # 3:5 -0006: LOADI R64, 259 # 3:5 -0007: UPCALL 0, R64 # 3:1, OUT -0008: LOADI R64, 0 # 4:1 -0009: END R64 # 4:1 -0010: LOADI R65, 2 # 7:5 -0011: LOADI R64, 259 # 7:5 -0012: UPCALL 0, R64 # 7:1, OUT -0013: RETURN # 8:1 -0014: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R64, 259 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: GOSUB 9 ; 2:7 +0004: LOADI R65, 1 ; 3:5 +0005: LOADI R64, 259 ; 3:5 +0006: UPCALL 0, R64 ; 3:1, OUT +0007: LOADI R64, 0 ; 4:1 +0008: END R64 ; 4:1 +0009: LOADI R65, 2 ; 7:5 +0010: LOADI R64, 259 ; 7:5 +0011: UPCALL 0, R64 ; 7:1, OUT +0012: RETURN ; 8:1 +0013: EOF ; 0:0 ``` ## Output @@ -55,15 +54,14 @@ RETURN ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: GOSUB 4 # 1:7 -0002: LOADI R64, 0 # 2:1 -0003: END R64 # 2:1 -0004: LOADI R65, 0 # 3:9 -0005: LOADI R64, 259 # 3:9 -0006: UPCALL 0, R64 # 3:5, OUT -0007: RETURN # 4:1 -0008: EOF # 0:0 +0000: GOSUB 3 ; 1:7 +0001: LOADI R64, 0 ; 2:1 +0002: END R64 ; 2:1 +0003: LOADI R65, 0 ; 3:9 +0004: LOADI R64, 259 ; 3:9 +0005: UPCALL 0, R64 ; 3:5, OUT +0006: RETURN ; 4:1 +0007: EOF ; 0:0 ``` ## Output @@ -102,37 +100,36 @@ RETURN ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R64, 259 # 1:5 -0003: UPCALL 0, R64 # 1:1, OUT -0004: GOSUB 10 # 2:7 -0005: LOADI R65, 1 # 3:5 -0006: LOADI R64, 259 # 3:5 -0007: UPCALL 0, R64 # 3:1, OUT -0008: LOADI R64, 0 # 4:1 -0009: END R64 # 4:1 -0010: LOADI R65, 2 # 7:5 -0011: LOADI R64, 259 # 7:5 -0012: UPCALL 0, R64 # 7:1, OUT -0013: GOSUB 18 # 8:7 -0014: LOADI R65, 3 # 9:5 -0015: LOADI R64, 259 # 9:5 -0016: UPCALL 0, R64 # 9:1, OUT -0017: RETURN # 10:1 -0018: LOADI R65, 4 # 13:5 -0019: LOADI R64, 259 # 13:5 -0020: UPCALL 0, R64 # 13:1, OUT -0021: GOSUB 26 # 14:7 -0022: LOADI R65, 5 # 15:5 -0023: LOADI R64, 259 # 15:5 -0024: UPCALL 0, R64 # 15:1, OUT -0025: RETURN # 16:1 -0026: LOADI R65, 6 # 19:5 -0027: LOADI R64, 259 # 19:5 -0028: UPCALL 0, R64 # 19:1, OUT -0029: RETURN # 20:1 -0030: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R64, 259 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: GOSUB 9 ; 2:7 +0004: LOADI R65, 1 ; 3:5 +0005: LOADI R64, 259 ; 3:5 +0006: UPCALL 0, R64 ; 3:1, OUT +0007: LOADI R64, 0 ; 4:1 +0008: END R64 ; 4:1 +0009: LOADI R65, 2 ; 7:5 +0010: LOADI R64, 259 ; 7:5 +0011: UPCALL 0, R64 ; 7:1, OUT +0012: GOSUB 17 ; 8:7 +0013: LOADI R65, 3 ; 9:5 +0014: LOADI R64, 259 ; 9:5 +0015: UPCALL 0, R64 ; 9:1, OUT +0016: RETURN ; 10:1 +0017: LOADI R65, 4 ; 13:5 +0018: LOADI R64, 259 ; 13:5 +0019: UPCALL 0, R64 ; 13:1, OUT +0020: GOSUB 25 ; 14:7 +0021: LOADI R65, 5 ; 15:5 +0022: LOADI R64, 259 ; 15:5 +0023: UPCALL 0, R64 ; 15:1, OUT +0024: RETURN ; 16:1 +0025: LOADI R65, 6 ; 19:5 +0026: LOADI R64, 259 ; 19:5 +0027: UPCALL 0, R64 ; 19:1, OUT +0028: RETURN ; 20:1 +0029: EOF ; 0:0 ``` ## Output @@ -158,9 +155,8 @@ RETURN ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: RETURN # 1:1 -0002: EOF # 0:0 +0000: RETURN ; 1:1 +0001: EOF ; 0:0 ``` ## Runtime errors @@ -187,14 +183,13 @@ GOSUB @s ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 6 # 1:6 -0002: LOADI R65, 0 # 4:5 -0003: LOADI R64, 259 # 4:5 -0004: UPCALL 0, R64 # 4:1, OUT -0005: RETURN # 5:1 -0006: GOSUB 2 # 8:7 -0007: EOF # 0:0 +0000: JUMP 5 ; 1:6 +0001: LOADI R65, 0 ; 4:5 +0002: LOADI R64, 259 ; 4:5 +0003: UPCALL 0, R64 ; 4:1, OUT +0004: RETURN ; 5:1 +0005: GOSUB 1 ; 8:7 +0006: EOF ; 0:0 ``` ## Output @@ -214,12 +209,11 @@ GOSUB @sub: @sub: OUT 1 ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: GOSUB 2 # 1:7 -0002: LOADI R65, 1 # 1:23 -0003: LOADI R64, 258 # 1:23 -0004: UPCALL 0, R64 # 1:19, OUT -0005: EOF # 0:0 +0000: GOSUB 1 ; 1:7 +0001: LOADI R65, 1 ; 1:23 +0002: LOADI R64, 258 ; 1:23 +0003: UPCALL 0, R64 ; 1:19, OUT +0004: EOF ; 0:0 ``` ## Output @@ -241,10 +235,9 @@ RETURN ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: JUMP 2 # 1:6 -0002: RETURN # 3:1 -0003: EOF # 0:0 +0000: JUMP 1 ; 1:6 +0001: RETURN ; 3:1 +0002: EOF ; 0:0 ``` ## Runtime errors diff --git a/core2/tests/test_goto.md b/core2/tests/test_goto.md index 4c8b1ccf..09ee4bc9 100644 --- a/core2/tests/test_goto.md +++ b/core2/tests/test_goto.md @@ -13,18 +13,17 @@ OUT "c" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R64, 259 # 1:5 -0003: UPCALL 0, R64 # 1:1, OUT -0004: JUMP 8 # 2:6 -0005: LOADI R65, 1 # 3:5 -0006: LOADI R64, 259 # 3:5 -0007: UPCALL 0, R64 # 3:1, OUT -0008: LOADI R65, 2 # 5:5 -0009: LOADI R64, 259 # 5:5 -0010: UPCALL 0, R64 # 5:1, OUT -0011: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R64, 259 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: JUMP 7 ; 2:6 +0004: LOADI R65, 1 ; 3:5 +0005: LOADI R64, 259 ; 3:5 +0006: UPCALL 0, R64 ; 3:1, OUT +0007: LOADI R65, 2 ; 5:5 +0008: LOADI R64, 259 ; 5:5 +0009: UPCALL 0, R64 ; 5:1, OUT +0010: EOF ; 0:0 ``` ## Output @@ -47,12 +46,11 @@ OUT "a" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 5 # 1:6 -0002: LOADI R65, 0 # 2:5 -0003: LOADI R64, 259 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: EOF # 0:0 +0000: JUMP 4 ; 1:6 +0001: LOADI R65, 0 ; 2:5 +0002: LOADI R64, 259 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: EOF ; 0:0 ``` # Test: GOTO target requires backwards jump @@ -73,17 +71,16 @@ GOTO @print_it ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 9 # 1:6 -0002: LOADI R65, 0 # 2:5 -0003: LOADI R64, 259 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R65, 1 # 4:5 -0006: LOADI R64, 259 # 4:5 -0007: UPCALL 0, R64 # 4:1, OUT -0008: JUMP 10 # 5:6 -0009: JUMP 5 # 7:6 -0010: EOF # 0:0 +0000: JUMP 8 ; 1:6 +0001: LOADI R65, 0 ; 2:5 +0002: LOADI R64, 259 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R65, 1 ; 4:5 +0005: LOADI R64, 259 ; 4:5 +0006: UPCALL 0, R64 ; 4:1, OUT +0007: JUMP 9 ; 5:6 +0008: JUMP 4 ; 7:6 +0009: EOF ; 0:0 ``` ## Output @@ -105,15 +102,14 @@ OUT "skipped" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 5 # 1:6 -0002: LOADI R65, 0 # 2:5 -0003: LOADI R64, 259 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R65, 1 # 3:8 -0006: LOADI R64, 259 # 3:8 -0007: UPCALL 0, R64 # 3:4, OUT -0008: EOF # 0:0 +0000: JUMP 4 ; 1:6 +0001: LOADI R65, 0 ; 2:5 +0002: LOADI R64, 259 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R65, 1 ; 3:8 +0005: LOADI R64, 259 ; 3:8 +0006: UPCALL 0, R64 ; 3:4, OUT +0007: EOF ; 0:0 ``` ## Output @@ -134,15 +130,14 @@ OUT "skipped": 20 OUT "target" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: JUMP 5 # 1:6 -0002: LOADI R65, 0 # 2:5 -0003: LOADI R64, 259 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R65, 1 # 2:23 -0006: LOADI R64, 259 # 2:23 -0007: UPCALL 0, R64 # 2:19, OUT -0008: EOF # 0:0 +0000: JUMP 4 ; 1:6 +0001: LOADI R65, 0 ; 2:5 +0002: LOADI R64, 259 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R65, 1 ; 2:23 +0005: LOADI R64, 259 ; 2:23 +0006: UPCALL 0, R64 ; 2:19, OUT +0007: EOF ; 0:0 ``` ## Output @@ -232,19 +227,18 @@ GOTO @again ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: MOVE R65, R64 # 3:4 -0003: LOADI R66, 5 # 3:8 -0004: CMPEQI R65, R65, R66 # 3:6 -0005: JMPF R65, 8 # 3:4 -0006: MOVE R65, R64 # 3:19 -0007: END R65 # 3:15 -0008: MOVE R64, R64 # 4:5 -0009: LOADI R65, 1 # 4:9 -0010: ADDI R64, R64, R65 # 4:7 -0011: JUMP 2 # 5:6 -0012: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: MOVE R65, R64 ; 3:4 +0002: LOADI R66, 5 ; 3:8 +0003: CMPEQI R65, R65, R66 ; 3:6 +0004: JMPF R65, 7 ; 3:4 +0005: MOVE R65, R64 ; 3:19 +0006: END R65 ; 3:15 +0007: MOVE R64, R64 ; 4:5 +0008: LOADI R65, 1 ; 4:9 +0009: ADDI R64, R64, R65 ; 4:7 +0010: JUMP 1 ; 5:6 +0011: EOF ; 0:0 ``` ## Exit code diff --git a/core2/tests/test_if.md b/core2/tests/test_if.md index 9ad34241..b535a466 100644 --- a/core2/tests/test_if.md +++ b/core2/tests/test_if.md @@ -11,13 +11,12 @@ END IF ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 1 # 1:4 -0002: JMPF R64, 6 # 1:4 -0003: LOADI R65, 0 # 2:9 -0004: LOADI R64, 259 # 2:9 -0005: UPCALL 0, R64 # 2:5, OUT -0006: EOF # 0:0 +0000: LOADI R64, 1 ; 1:4 +0001: JMPF R64, 5 ; 1:4 +0002: LOADI R65, 0 ; 2:9 +0003: LOADI R64, 259 ; 2:9 +0004: UPCALL 0, R64 ; 2:5, OUT +0005: EOF ; 0:0 ``` ## Output @@ -40,16 +39,15 @@ OUT "after" ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 0 # 1:4 -0002: JMPF R64, 6 # 1:4 -0003: LOADI R65, 0 # 2:9 -0004: LOADI R64, 259 # 2:9 -0005: UPCALL 0, R64 # 2:5, OUT -0006: LOADI R65, 1 # 4:5 -0007: LOADI R64, 259 # 4:5 -0008: UPCALL 0, R64 # 4:1, OUT -0009: EOF # 0:0 +0000: LOADI R64, 0 ; 1:4 +0001: JMPF R64, 5 ; 1:4 +0002: LOADI R65, 0 ; 2:9 +0003: LOADI R64, 259 ; 2:9 +0004: UPCALL 0, R64 ; 2:5, OUT +0005: LOADI R65, 1 ; 4:5 +0006: LOADI R64, 259 ; 4:5 +0007: UPCALL 0, R64 ; 4:1, OUT +0008: EOF ; 0:0 ``` ## Output @@ -77,27 +75,26 @@ END IF ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 0 # 1:9 -0002: LOADI R65, 1 # 2:9 -0003: MOVE R66, R64 # 3:4 -0004: JMPF R66, 9 # 3:4 -0005: LOADI R67, 0 # 4:9 -0006: LOADI R66, 259 # 4:9 -0007: UPCALL 0, R66 # 4:5, OUT -0008: JUMP 20 # 3:4 -0009: MOVE R66, R65 # 5:8 -0010: JMPF R66, 15 # 5:8 -0011: LOADI R67, 1 # 6:9 -0012: LOADI R66, 259 # 6:9 -0013: UPCALL 0, R66 # 6:5, OUT -0014: JUMP 20 # 5:8 -0015: LOADI R66, 1 # 7:1 -0016: JMPF R66, 20 # 7:1 -0017: LOADI R67, 2 # 8:9 -0018: LOADI R66, 259 # 8:9 -0019: UPCALL 0, R66 # 8:5, OUT -0020: EOF # 0:0 +0000: LOADI R64, 0 ; 1:9 +0001: LOADI R65, 1 ; 2:9 +0002: MOVE R66, R64 ; 3:4 +0003: JMPF R66, 8 ; 3:4 +0004: LOADI R67, 0 ; 4:9 +0005: LOADI R66, 259 ; 4:9 +0006: UPCALL 0, R66 ; 4:5, OUT +0007: JUMP 19 ; 3:4 +0008: MOVE R66, R65 ; 5:8 +0009: JMPF R66, 14 ; 5:8 +0010: LOADI R67, 1 ; 6:9 +0011: LOADI R66, 259 ; 6:9 +0012: UPCALL 0, R66 ; 6:5, OUT +0013: JUMP 19 ; 5:8 +0014: LOADI R66, 1 ; 7:1 +0015: JMPF R66, 19 ; 7:1 +0016: LOADI R67, 2 ; 8:9 +0017: LOADI R66, 259 ; 8:9 +0018: UPCALL 0, R66 ; 8:5, OUT +0019: EOF ; 0:0 ``` ## Output @@ -123,21 +120,20 @@ END IF ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 1 # 1:9 -0002: LOADI R65, 0 # 2:9 -0003: MOVE R66, R64 # 3:4 -0004: JMPF R66, 9 # 3:4 -0005: LOADI R67, 0 # 4:9 -0006: LOADI R66, 259 # 4:9 -0007: UPCALL 0, R66 # 4:5, OUT -0008: JUMP 14 # 3:4 -0009: MOVE R66, R65 # 5:8 -0010: JMPF R66, 14 # 5:8 -0011: LOADI R67, 1 # 6:9 -0012: LOADI R66, 259 # 6:9 -0013: UPCALL 0, R66 # 6:5, OUT -0014: EOF # 0:0 +0000: LOADI R64, 1 ; 1:9 +0001: LOADI R65, 0 ; 2:9 +0002: MOVE R66, R64 ; 3:4 +0003: JMPF R66, 8 ; 3:4 +0004: LOADI R67, 0 ; 4:9 +0005: LOADI R66, 259 ; 4:9 +0006: UPCALL 0, R66 ; 4:5, OUT +0007: JUMP 13 ; 3:4 +0008: MOVE R66, R65 ; 5:8 +0009: JMPF R66, 13 ; 5:8 +0010: LOADI R67, 1 ; 6:9 +0011: LOADI R66, 259 ; 6:9 +0012: UPCALL 0, R66 ; 6:5, OUT +0013: EOF ; 0:0 ``` ## Output @@ -162,20 +158,19 @@ END IF ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:8 -0002: MOVE R65, R64 # 2:4 -0003: JMPF R65, 8 # 2:4 -0004: LOADI R66, 0 # 3:9 -0005: LOADI R65, 259 # 3:9 -0006: UPCALL 0, R65 # 3:5, OUT -0007: JUMP 13 # 2:4 -0008: LOADI R65, 1 # 4:1 -0009: JMPF R65, 13 # 4:1 -0010: LOADI R66, 1 # 5:9 -0011: LOADI R65, 259 # 5:9 -0012: UPCALL 0, R65 # 5:5, OUT -0013: EOF # 0:0 +0000: LOADI R64, 0 ; 1:8 +0001: MOVE R65, R64 ; 2:4 +0002: JMPF R65, 7 ; 2:4 +0003: LOADI R66, 0 ; 3:9 +0004: LOADI R65, 259 ; 3:9 +0005: UPCALL 0, R65 ; 3:5, OUT +0006: JUMP 12 ; 2:4 +0007: LOADI R65, 1 ; 4:1 +0008: JMPF R65, 12 ; 4:1 +0009: LOADI R66, 1 ; 5:9 +0010: LOADI R65, 259 ; 5:9 +0011: UPCALL 0, R65 ; 5:5, OUT +0012: EOF ; 0:0 ``` ## Output @@ -201,17 +196,16 @@ END IF ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 1 # 1:9 -0002: LOADI R65, 1 # 2:9 -0003: MOVE R66, R64 # 3:4 -0004: JMPF R66, 10 # 3:4 -0005: MOVE R66, R65 # 4:8 -0006: JMPF R66, 10 # 4:8 -0007: LOADI R67, 0 # 5:13 -0008: LOADI R66, 259 # 5:13 -0009: UPCALL 0, R66 # 5:9, OUT -0010: EOF # 0:0 +0000: LOADI R64, 1 ; 1:9 +0001: LOADI R65, 1 ; 2:9 +0002: MOVE R66, R64 ; 3:4 +0003: JMPF R66, 9 ; 3:4 +0004: MOVE R66, R65 ; 4:8 +0005: JMPF R66, 9 ; 4:8 +0006: LOADI R67, 0 ; 5:13 +0007: LOADI R66, 259 ; 5:13 +0008: UPCALL 0, R66 ; 5:9, OUT +0009: EOF ; 0:0 ``` ## Output @@ -239,23 +233,22 @@ END IF ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 1 # 1:5 -0002: LOADI R65, 0 # 2:5 -0003: MOVE R66, R64 # 3:4 -0004: JMPF R66, 16 # 3:4 -0005: MOVE R66, R65 # 4:8 -0006: JMPF R66, 11 # 4:8 -0007: LOADI R67, 0 # 5:13 -0008: LOADI R66, 259 # 5:13 -0009: UPCALL 0, R66 # 5:9, OUT -0010: JUMP 16 # 4:8 -0011: LOADI R66, 1 # 6:5 -0012: JMPF R66, 16 # 6:5 -0013: LOADI R67, 1 # 7:13 -0014: LOADI R66, 259 # 7:13 -0015: UPCALL 0, R66 # 7:9, OUT -0016: EOF # 0:0 +0000: LOADI R64, 1 ; 1:5 +0001: LOADI R65, 0 ; 2:5 +0002: MOVE R66, R64 ; 3:4 +0003: JMPF R66, 15 ; 3:4 +0004: MOVE R66, R65 ; 4:8 +0005: JMPF R66, 10 ; 4:8 +0006: LOADI R67, 0 ; 5:13 +0007: LOADI R66, 259 ; 5:13 +0008: UPCALL 0, R66 ; 5:9, OUT +0009: JUMP 15 ; 4:8 +0010: LOADI R66, 1 ; 6:5 +0011: JMPF R66, 15 ; 6:5 +0012: LOADI R67, 1 ; 7:13 +0013: LOADI R66, 259 ; 7:13 +0014: UPCALL 0, R66 ; 7:9, OUT +0015: EOF ; 0:0 ``` ## Output @@ -281,23 +274,22 @@ END IF ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:4 -0003: LOADI R66, 3 # 2:8 -0004: CMPEQI R65, R65, R66 # 2:6 -0005: JMPF R65, 9 # 2:4 -0006: LOADI R66, 0 # 3:9 -0007: LOADI R65, 259 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R65, R64 # 5:4 -0010: LOADI R66, 3 # 5:9 -0011: CMPNEI R65, R65, R66 # 5:6 -0012: JMPF R65, 16 # 5:4 -0013: LOADI R66, 1 # 6:9 -0014: LOADI R65, 259 # 6:9 -0015: UPCALL 0, R65 # 6:5, OUT -0016: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:4 +0002: LOADI R66, 3 ; 2:8 +0003: CMPEQI R65, R65, R66 ; 2:6 +0004: JMPF R65, 8 ; 2:4 +0005: LOADI R66, 0 ; 3:9 +0006: LOADI R65, 259 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: MOVE R65, R64 ; 5:4 +0009: LOADI R66, 3 ; 5:9 +0010: CMPNEI R65, R65, R66 ; 5:6 +0011: JMPF R65, 15 ; 5:4 +0012: LOADI R66, 1 ; 6:9 +0013: LOADI R65, 259 ; 6:9 +0014: UPCALL 0, R65 ; 6:5, OUT +0015: EOF ; 0:0 ``` ## Output @@ -323,23 +315,22 @@ END IF ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 5 # 1:5 -0002: MOVE R65, R64 # 2:4 -0003: LOADI R66, 3 # 2:8 -0004: CMPEQI R65, R65, R66 # 2:6 -0005: JMPF R65, 9 # 2:4 -0006: LOADI R66, 0 # 3:9 -0007: LOADI R65, 259 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R65, R64 # 5:4 -0010: LOADI R66, 3 # 5:9 -0011: CMPNEI R65, R65, R66 # 5:6 -0012: JMPF R65, 16 # 5:4 -0013: LOADI R66, 1 # 6:9 -0014: LOADI R65, 259 # 6:9 -0015: UPCALL 0, R65 # 6:5, OUT -0016: EOF # 0:0 +0000: LOADI R64, 5 ; 1:5 +0001: MOVE R65, R64 ; 2:4 +0002: LOADI R66, 3 ; 2:8 +0003: CMPEQI R65, R65, R66 ; 2:6 +0004: JMPF R65, 8 ; 2:4 +0005: LOADI R66, 0 ; 3:9 +0006: LOADI R65, 259 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: MOVE R65, R64 ; 5:4 +0009: LOADI R66, 3 ; 5:9 +0010: CMPNEI R65, R65, R66 ; 5:6 +0011: JMPF R65, 15 ; 5:4 +0012: LOADI R66, 1 ; 6:9 +0013: LOADI R65, 259 ; 6:9 +0014: UPCALL 0, R65 ; 6:5, OUT +0015: EOF ; 0:0 ``` ## Output @@ -368,38 +359,37 @@ END IF ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:4 -0003: LOADI R66, 1 # 2:8 -0004: CMPEQI R65, R65, R66 # 2:6 -0005: JMPF R65, 10 # 2:4 -0006: LOADI R66, 0 # 3:9 -0007: LOADI R65, 259 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: JUMP 31 # 2:4 -0010: MOVE R65, R64 # 4:8 -0011: LOADI R66, 2 # 4:12 -0012: CMPEQI R65, R65, R66 # 4:10 -0013: JMPF R65, 18 # 4:8 -0014: LOADI R66, 1 # 5:9 -0015: LOADI R65, 259 # 5:9 -0016: UPCALL 0, R65 # 5:5, OUT -0017: JUMP 31 # 4:8 -0018: MOVE R65, R64 # 6:8 -0019: LOADI R66, 3 # 6:12 -0020: CMPEQI R65, R65, R66 # 6:10 -0021: JMPF R65, 26 # 6:8 -0022: LOADI R66, 2 # 7:9 -0023: LOADI R65, 259 # 7:9 -0024: UPCALL 0, R65 # 7:5, OUT -0025: JUMP 31 # 6:8 -0026: LOADI R65, 1 # 8:1 -0027: JMPF R65, 31 # 8:1 -0028: LOADI R66, 3 # 9:9 -0029: LOADI R65, 259 # 9:9 -0030: UPCALL 0, R65 # 9:5, OUT -0031: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:4 +0002: LOADI R66, 1 ; 2:8 +0003: CMPEQI R65, R65, R66 ; 2:6 +0004: JMPF R65, 9 ; 2:4 +0005: LOADI R66, 0 ; 3:9 +0006: LOADI R65, 259 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: JUMP 30 ; 2:4 +0009: MOVE R65, R64 ; 4:8 +0010: LOADI R66, 2 ; 4:12 +0011: CMPEQI R65, R65, R66 ; 4:10 +0012: JMPF R65, 17 ; 4:8 +0013: LOADI R66, 1 ; 5:9 +0014: LOADI R65, 259 ; 5:9 +0015: UPCALL 0, R65 ; 5:5, OUT +0016: JUMP 30 ; 4:8 +0017: MOVE R65, R64 ; 6:8 +0018: LOADI R66, 3 ; 6:12 +0019: CMPEQI R65, R65, R66 ; 6:10 +0020: JMPF R65, 25 ; 6:8 +0021: LOADI R66, 2 ; 7:9 +0022: LOADI R65, 259 ; 7:9 +0023: UPCALL 0, R65 ; 7:5, OUT +0024: JUMP 30 ; 6:8 +0025: LOADI R65, 1 ; 8:1 +0026: JMPF R65, 30 ; 8:1 +0027: LOADI R66, 3 ; 9:9 +0028: LOADI R65, 259 ; 9:9 +0029: UPCALL 0, R65 ; 9:5, OUT +0030: EOF ; 0:0 ``` ## Output @@ -428,38 +418,37 @@ END IF ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 4 # 1:5 -0002: MOVE R65, R64 # 2:4 -0003: LOADI R66, 1 # 2:8 -0004: CMPEQI R65, R65, R66 # 2:6 -0005: JMPF R65, 10 # 2:4 -0006: LOADI R66, 0 # 3:9 -0007: LOADI R65, 259 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: JUMP 31 # 2:4 -0010: MOVE R65, R64 # 4:8 -0011: LOADI R66, 2 # 4:12 -0012: CMPEQI R65, R65, R66 # 4:10 -0013: JMPF R65, 18 # 4:8 -0014: LOADI R66, 1 # 5:9 -0015: LOADI R65, 259 # 5:9 -0016: UPCALL 0, R65 # 5:5, OUT -0017: JUMP 31 # 4:8 -0018: MOVE R65, R64 # 6:8 -0019: LOADI R66, 3 # 6:12 -0020: CMPEQI R65, R65, R66 # 6:10 -0021: JMPF R65, 26 # 6:8 -0022: LOADI R66, 2 # 7:9 -0023: LOADI R65, 259 # 7:9 -0024: UPCALL 0, R65 # 7:5, OUT -0025: JUMP 31 # 6:8 -0026: LOADI R65, 1 # 8:1 -0027: JMPF R65, 31 # 8:1 -0028: LOADI R66, 3 # 9:9 -0029: LOADI R65, 259 # 9:9 -0030: UPCALL 0, R65 # 9:5, OUT -0031: EOF # 0:0 +0000: LOADI R64, 4 ; 1:5 +0001: MOVE R65, R64 ; 2:4 +0002: LOADI R66, 1 ; 2:8 +0003: CMPEQI R65, R65, R66 ; 2:6 +0004: JMPF R65, 9 ; 2:4 +0005: LOADI R66, 0 ; 3:9 +0006: LOADI R65, 259 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: JUMP 30 ; 2:4 +0009: MOVE R65, R64 ; 4:8 +0010: LOADI R66, 2 ; 4:12 +0011: CMPEQI R65, R65, R66 ; 4:10 +0012: JMPF R65, 17 ; 4:8 +0013: LOADI R66, 1 ; 5:9 +0014: LOADI R65, 259 ; 5:9 +0015: UPCALL 0, R65 ; 5:5, OUT +0016: JUMP 30 ; 4:8 +0017: MOVE R65, R64 ; 6:8 +0018: LOADI R66, 3 ; 6:12 +0019: CMPEQI R65, R65, R66 ; 6:10 +0020: JMPF R65, 25 ; 6:8 +0021: LOADI R66, 2 ; 7:9 +0022: LOADI R65, 259 ; 7:9 +0023: UPCALL 0, R65 ; 7:5, OUT +0024: JUMP 30 ; 6:8 +0025: LOADI R65, 1 ; 8:1 +0026: JMPF R65, 30 ; 8:1 +0027: LOADI R66, 3 ; 9:9 +0028: LOADI R65, 259 ; 9:9 +0029: UPCALL 0, R65 ; 9:5, OUT +0030: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_on_error.md b/core2/tests/test_on_error.md index 11a6f969..40497181 100644 --- a/core2/tests/test_on_error.md +++ b/core2/tests/test_on_error.md @@ -13,23 +13,22 @@ OUT 2 ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH JUMP, 13 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R67, 0 # 3:12 -0006: UPCALL 1, R66 # 3:5, RAISEF -0007: MOVE R65, R66 # 3:5 -0008: LOADI R64, 256 # 3:5 -0009: UPCALL 0, R64 # 3:1, OUT -0010: LOADI R65, 2 # 4:5 -0011: LOADI R64, 258 # 4:5 -0012: UPCALL 0, R64 # 4:1, OUT -0013: UPCALL 2, R65 # 5:9, LAST_ERROR -0014: LOADI R64, 259 # 5:9 -0015: UPCALL 0, R64 # 5:5, OUT -0016: EOF # 0:0 +0000: SETEH JUMP, 12 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R67, 0 ; 3:12 +0005: UPCALL 1, R66 ; 3:5, RAISEF +0006: MOVE R65, R66 ; 3:5 +0007: LOADI R64, 256 ; 3:5 +0008: UPCALL 0, R64 ; 3:1, OUT +0009: LOADI R65, 2 ; 4:5 +0010: LOADI R64, 258 ; 4:5 +0011: UPCALL 0, R64 ; 4:1, OUT +0012: UPCALL 2, R65 ; 5:9, LAST_ERROR +0013: LOADI R64, 259 ; 5:9 +0014: UPCALL 0, R64 ; 5:5, OUT +0015: EOF ; 0:0 ``` ## Output @@ -55,23 +54,22 @@ OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH JUMP, 13 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R67, 0 # 3:12 -0006: UPCALL 1, R66 # 3:5, RAISEF -0007: MOVE R65, R66 # 3:5 -0008: LOADI R64, 256 # 3:5 -0009: UPCALL 0, R64 # 3:1, OUT -0010: LOADI R65, 2 # 4:5 -0011: LOADI R64, 258 # 4:5 -0012: UPCALL 0, R64 # 4:1, OUT -0013: UPCALL 2, R65 # 6:5, LAST_ERROR -0014: LOADI R64, 259 # 6:5 -0015: UPCALL 0, R64 # 6:1, OUT -0016: EOF # 0:0 +0000: SETEH JUMP, 12 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R67, 0 ; 3:12 +0005: UPCALL 1, R66 ; 3:5, RAISEF +0006: MOVE R65, R66 ; 3:5 +0007: LOADI R64, 256 ; 3:5 +0008: UPCALL 0, R64 ; 3:1, OUT +0009: LOADI R65, 2 ; 4:5 +0010: LOADI R64, 258 ; 4:5 +0011: UPCALL 0, R64 ; 4:1, OUT +0012: UPCALL 2, R65 ; 6:5, LAST_ERROR +0013: LOADI R64, 259 ; 6:5 +0014: UPCALL 0, R64 ; 6:1, OUT +0015: EOF ; 0:0 ``` ## Output @@ -98,26 +96,25 @@ OUT RAISEF("internal") ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH JUMP, 10 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R67, 0 # 3:12 -0006: UPCALL 1, R66 # 3:5, RAISEF -0007: MOVE R65, R66 # 3:5 -0008: LOADI R64, 256 # 3:5 -0009: UPCALL 0, R64 # 3:1, OUT -0010: SETEH NONE, 0 # 5:1 -0011: LOADI R65, 2 # 6:5 -0012: LOADI R64, 258 # 6:5 -0013: UPCALL 0, R64 # 6:1, OUT -0014: LOADI R67, 0 # 7:12 -0015: UPCALL 1, R66 # 7:5, RAISEF -0016: MOVE R65, R66 # 7:5 -0017: LOADI R64, 256 # 7:5 -0018: UPCALL 0, R64 # 7:1, OUT -0019: EOF # 0:0 +0000: SETEH JUMP, 9 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R67, 0 ; 3:12 +0005: UPCALL 1, R66 ; 3:5, RAISEF +0006: MOVE R65, R66 ; 3:5 +0007: LOADI R64, 256 ; 3:5 +0008: UPCALL 0, R64 ; 3:1, OUT +0009: SETEH NONE, 0 ; 5:1 +0010: LOADI R65, 2 ; 6:5 +0011: LOADI R64, 258 ; 6:5 +0012: UPCALL 0, R64 ; 6:1, OUT +0013: LOADI R67, 0 ; 7:12 +0014: UPCALL 1, R66 ; 7:5, RAISEF +0015: MOVE R65, R66 ; 7:5 +0016: LOADI R64, 256 ; 7:5 +0017: UPCALL 0, R64 ; 7:1, OUT +0018: EOF ; 0:0 ``` ## Runtime errors @@ -147,20 +144,19 @@ OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R67, 0 # 3:12 -0006: UPCALL 1, R66 # 3:5, RAISEF -0007: MOVE R65, R66 # 3:5 -0008: LOADI R64, 256 # 3:5 -0009: UPCALL 0, R64 # 3:1, OUT -0010: UPCALL 2, R65 # 4:5, LAST_ERROR -0011: LOADI R64, 259 # 4:5 -0012: UPCALL 0, R64 # 4:1, OUT -0013: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R67, 0 ; 3:12 +0005: UPCALL 1, R66 ; 3:5, RAISEF +0006: MOVE R65, R66 ; 3:5 +0007: LOADI R64, 256 ; 3:5 +0008: UPCALL 0, R64 ; 3:1, OUT +0009: UPCALL 2, R65 ; 4:5, LAST_ERROR +0010: LOADI R64, 259 ; 4:5 +0011: UPCALL 0, R64 ; 4:1, OUT +0012: EOF ; 0:0 ``` ## Output @@ -184,17 +180,16 @@ OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R64, 0 # 3:7 -0006: UPCALL 1, R64 # 3:1, RAISE -0007: UPCALL 2, R65 # 4:5, LAST_ERROR -0008: LOADI R64, 259 # 4:5 -0009: UPCALL 0, R64 # 4:1, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R64, 0 ; 3:7 +0005: UPCALL 1, R64 ; 3:1, RAISE +0006: UPCALL 2, R65 ; 4:5, LAST_ERROR +0007: LOADI R64, 259 ; 4:5 +0008: UPCALL 0, R64 ; 4:1, OUT +0009: EOF ; 0:0 ``` ## Output @@ -216,20 +211,19 @@ OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R67, 0 # 2:19 -0006: UPCALL 1, R66 # 2:12, RAISEF -0007: MOVE R65, R66 # 2:12 -0008: LOADI R64, 256 # 2:12 -0009: UPCALL 0, R64 # 2:8, OUT -0010: UPCALL 2, R65 # 2:36, LAST_ERROR -0011: LOADI R64, 259 # 2:36 -0012: UPCALL 0, R64 # 2:32, OUT -0013: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R67, 0 ; 2:19 +0005: UPCALL 1, R66 ; 2:12, RAISEF +0006: MOVE R65, R66 ; 2:12 +0007: LOADI R64, 256 ; 2:12 +0008: UPCALL 0, R64 ; 2:8, OUT +0009: UPCALL 2, R65 ; 2:36, LAST_ERROR +0010: LOADI R64, 259 ; 2:36 +0011: UPCALL 0, R64 ; 2:32, OUT +0012: EOF ; 0:0 ``` ## Output @@ -251,17 +245,16 @@ OUT 1: RAISE "internal": OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R65, 1 # 2:5 -0003: LOADI R64, 258 # 2:5 -0004: UPCALL 0, R64 # 2:1, OUT -0005: LOADI R64, 0 # 2:14 -0006: UPCALL 1, R64 # 2:8, RAISE -0007: UPCALL 2, R65 # 2:30, LAST_ERROR -0008: LOADI R64, 259 # 2:30 -0009: UPCALL 0, R64 # 2:26, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R65, 1 ; 2:5 +0002: LOADI R64, 258 ; 2:5 +0003: UPCALL 0, R64 ; 2:1, OUT +0004: LOADI R64, 0 ; 2:14 +0005: UPCALL 1, R64 ; 2:8, RAISE +0006: UPCALL 2, R65 ; 2:30, LAST_ERROR +0007: LOADI R64, 259 ; 2:30 +0008: UPCALL 0, R64 ; 2:26, OUT +0009: EOF ; 0:0 ``` ## Output @@ -282,17 +275,16 @@ ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R67, 0 # 1:34 -0003: UPCALL 0, R66 # 1:27, RAISEF -0004: MOVE R65, R66 # 1:27 -0005: LOADI R64, 256 # 1:27 -0006: UPCALL 1, R64 # 1:23, OUT -0007: UPCALL 2, R65 # 1:51, LAST_ERROR -0008: LOADI R64, 259 # 1:51 -0009: UPCALL 1, R64 # 1:47, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: UPCALL 0, R66 ; 1:27, RAISEF +0003: MOVE R65, R66 ; 1:27 +0004: LOADI R64, 256 ; 1:27 +0005: UPCALL 1, R64 ; 1:23, OUT +0006: UPCALL 2, R65 ; 1:51, LAST_ERROR +0007: LOADI R64, 259 ; 1:51 +0008: UPCALL 1, R64 ; 1:47, OUT +0009: EOF ; 0:0 ``` ## Output @@ -312,17 +304,16 @@ ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R67, 0 # 1:34 -0003: UPCALL 0, R66 # 1:27, RAISEF -0004: MOVE R65, R66 # 1:27 -0005: LOADI R64, 256 # 1:27 -0006: UPCALL 1, R64 # 1:23, OUT -0007: UPCALL 2, R65 # 1:47, LAST_ERROR -0008: LOADI R64, 259 # 1:47 -0009: UPCALL 1, R64 # 1:43, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: UPCALL 0, R66 ; 1:27, RAISEF +0003: MOVE R65, R66 ; 1:27 +0004: LOADI R64, 256 ; 1:27 +0005: UPCALL 1, R64 ; 1:23, OUT +0006: UPCALL 2, R65 ; 1:47, LAST_ERROR +0007: LOADI R64, 259 ; 1:47 +0008: UPCALL 1, R64 ; 1:43, OUT +0009: EOF ; 0:0 ``` ## Output @@ -342,17 +333,16 @@ ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R67, 0 # 1:34 -0003: UPCALL 0, R66 # 1:27, RAISEF -0004: MOVE R65, R66 # 1:27 -0005: LOADI R64, 256 # 1:27 -0006: UPCALL 1, R64 # 1:23, OUT -0007: UPCALL 2, R65 # 1:51, LAST_ERROR -0008: LOADI R64, 259 # 1:51 -0009: UPCALL 1, R64 # 1:47, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: UPCALL 0, R66 ; 1:27, RAISEF +0003: MOVE R65, R66 ; 1:27 +0004: LOADI R64, 256 ; 1:27 +0005: UPCALL 1, R64 ; 1:23, OUT +0006: UPCALL 2, R65 ; 1:51, LAST_ERROR +0007: LOADI R64, 259 ; 1:51 +0008: UPCALL 1, R64 ; 1:47, OUT +0009: EOF ; 0:0 ``` ## Output @@ -372,17 +362,16 @@ ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: SETEH RESUME_NEXT, 0 # 1:1 -0002: LOADI R67, 0 # 1:34 -0003: UPCALL 0, R66 # 1:27, RAISEF -0004: MOVE R65, R66 # 1:27 -0005: LOADI R64, 256 # 1:27 -0006: UPCALL 1, R64 # 1:23, OUT -0007: UPCALL 2, R65 # 1:45, LAST_ERROR -0008: LOADI R64, 259 # 1:45 -0009: UPCALL 1, R64 # 1:41, OUT -0010: EOF # 0:0 +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: UPCALL 0, R66 ; 1:27, RAISEF +0003: MOVE R65, R66 ; 1:27 +0004: LOADI R64, 256 ; 1:27 +0005: UPCALL 1, R64 ; 1:23, OUT +0006: UPCALL 2, R65 ; 1:45, LAST_ERROR +0007: LOADI R64, 259 ; 1:45 +0008: UPCALL 1, R64 ; 1:41, OUT +0009: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_eq.md b/core2/tests/test_relational_eq.md index bfa3221b..7fe57b1a 100644 --- a/core2/tests/test_relational_eq.md +++ b/core2/tests/test_relational_eq.md @@ -9,13 +9,12 @@ OUT 2 = 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 2 # 1:9 -0003: CMPEQI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 2 ; 1:9 +0002: CMPEQI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 = 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: CMPEQD R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: CMPEQD R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 = 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: CMPEQD R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: CMPEQD R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" = "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:13 -0003: CMPEQS R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:13 +0002: CMPEQS R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -114,13 +110,12 @@ OUT TRUE = FALSE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 0 # 1:12 -0003: CMPEQB R65, R65, R66 # 1:10 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 0 ; 1:12 +0002: CMPEQB R65, R65, R66 ; 1:10 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_ge.md b/core2/tests/test_relational_ge.md index 63e74c9f..fa96730a 100644 --- a/core2/tests/test_relational_ge.md +++ b/core2/tests/test_relational_ge.md @@ -9,13 +9,12 @@ OUT 3 >= 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADI R66, 2 # 1:10 -0003: CMPGEI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADI R66, 2 ; 1:10 +0002: CMPGEI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 >= 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:12 -0003: CMPGED R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:12 +0002: CMPGED R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 >= 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:10 -0003: ITOD R65 # 1:7 -0004: CMPGED R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:10 +0002: ITOD R65 ; 1:7 +0003: CMPGED R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" >= "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:14 -0003: CMPGES R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:14 +0002: CMPGES R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_gt.md b/core2/tests/test_relational_gt.md index f8519b7f..426e2636 100644 --- a/core2/tests/test_relational_gt.md +++ b/core2/tests/test_relational_gt.md @@ -9,13 +9,12 @@ OUT 3 > 2 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 3 # 1:5 -0002: LOADI R66, 2 # 1:9 -0003: CMPGTI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 3 ; 1:5 +0001: LOADI R66, 2 ; 1:9 +0002: CMPGTI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 > 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: CMPGTD R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: CMPGTD R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 > 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: CMPGTD R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: CMPGTD R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" > "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:13 -0003: CMPGTS R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:13 +0002: CMPGTS R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_le.md b/core2/tests/test_relational_le.md index b911dc5f..6a748b4e 100644 --- a/core2/tests/test_relational_le.md +++ b/core2/tests/test_relational_le.md @@ -9,13 +9,12 @@ OUT 2 <= 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 3 # 1:10 -0003: CMPLEI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 3 ; 1:10 +0002: CMPLEI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 <= 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:12 -0003: CMPLED R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:12 +0002: CMPLED R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 <= 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:10 -0003: ITOD R65 # 1:7 -0004: CMPLED R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:10 +0002: ITOD R65 ; 1:7 +0003: CMPLED R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" <= "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:14 -0003: CMPLES R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:14 +0002: CMPLES R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_lt.md b/core2/tests/test_relational_lt.md index f2d4c8b0..c0e543df 100644 --- a/core2/tests/test_relational_lt.md +++ b/core2/tests/test_relational_lt.md @@ -9,13 +9,12 @@ OUT 2 < 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 3 # 1:9 -0003: CMPLTI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 3 ; 1:9 +0002: CMPLTI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 < 2.0 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 1 # 1:11 -0003: CMPLTD R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 1 ; 1:11 +0002: CMPLTD R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 < 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:9 -0003: ITOD R65 # 1:7 -0004: CMPLTD R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:9 +0002: ITOD R65 ; 1:7 +0003: CMPLTD R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" < "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:13 -0003: CMPLTS R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:13 +0002: CMPLTS R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_relational_ne.md b/core2/tests/test_relational_ne.md index 2ea917f3..4295aa38 100644 --- a/core2/tests/test_relational_ne.md +++ b/core2/tests/test_relational_ne.md @@ -9,13 +9,12 @@ OUT 2 <> 3 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADI R66, 3 # 1:10 -0003: CMPNEI R65, R65, R66 # 1:7 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADI R66, 3 ; 1:10 +0002: CMPNEI R65, R65, R66 ; 1:7 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -35,13 +34,12 @@ OUT 2.5 <> 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R65, 0 # 1:5 -0002: LOADC R66, 0 # 1:12 -0003: CMPNED R65, R65, R66 # 1:9 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADC R65, 0 ; 1:5 +0001: LOADC R66, 0 ; 1:12 +0002: CMPNED R65, R65, R66 ; 1:9 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -61,14 +59,13 @@ OUT 2 <> 2.5 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 2 # 1:5 -0002: LOADC R66, 0 # 1:10 -0003: ITOD R65 # 1:7 -0004: CMPNED R65, R65, R66 # 1:7 -0005: LOADI R64, 256 # 1:5 -0006: UPCALL 0, R64 # 1:1, OUT -0007: EOF # 0:0 +0000: LOADI R65, 2 ; 1:5 +0001: LOADC R66, 0 ; 1:10 +0002: ITOD R65 ; 1:7 +0003: CMPNED R65, R65, R66 ; 1:7 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 ``` ## Output @@ -88,13 +85,12 @@ OUT "foo" <> "bar" ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 0 # 1:5 -0002: LOADI R66, 1 # 1:14 -0003: CMPNES R65, R65, R66 # 1:11 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R66, 1 ; 1:14 +0002: CMPNES R65, R65, R66 ; 1:11 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output @@ -114,13 +110,12 @@ OUT TRUE <> FALSE ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R65, 1 # 1:5 -0002: LOADI R66, 0 # 1:13 -0003: CMPNEB R65, R65, R66 # 1:10 -0004: LOADI R64, 256 # 1:5 -0005: UPCALL 0, R64 # 1:1, OUT -0006: EOF # 0:0 +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R66, 0 ; 1:13 +0002: CMPNEB R65, R65, R66 ; 1:10 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 0, R64 ; 1:1, OUT +0005: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_select.md b/core2/tests/test_select.md index 14bcb926..d57868da 100644 --- a/core2/tests/test_select.md +++ b/core2/tests/test_select.md @@ -21,99 +21,98 @@ END SELECT ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 1 # 3:10 -0005: CMPEQI R68, R66, R67 # 3:10 -0006: JMPF R68, 8 # 3:10 -0007: JUMP 73 # 3:10 -0008: MOVE R66, R65 # 3:13 -0009: LOADI R67, 3 # 3:13 -0010: CMPEQI R68, R66, R67 # 3:13 -0011: JMPF R68, 13 # 3:13 -0012: JUMP 73 # 3:13 -0013: MOVE R66, R65 # 3:16 -0014: LOADI R67, 5 # 3:16 -0015: CMPEQI R68, R66, R67 # 3:16 -0016: JMPF R68, 18 # 3:16 -0017: JUMP 73 # 3:16 -0018: MOVE R66, R65 # 3:19 -0019: LOADI R67, 7 # 3:19 -0020: CMPEQI R68, R66, R67 # 3:19 -0021: JMPF R68, 23 # 3:19 -0022: JUMP 73 # 3:19 -0023: MOVE R66, R65 # 3:22 -0024: LOADI R67, 9 # 3:22 -0025: CMPEQI R68, R66, R67 # 3:22 -0026: JMPF R68, 28 # 3:22 -0027: JUMP 73 # 3:22 -0028: JUMP 29 # 13:1 -0029: MOVE R66, R65 # 5:10 -0030: LOADI R67, 0 # 5:10 -0031: CMPEQI R68, R66, R67 # 5:10 -0032: JMPF R68, 34 # 5:10 -0033: JUMP 77 # 5:10 -0034: MOVE R66, R65 # 5:13 -0035: LOADI R67, 2 # 5:13 -0036: CMPEQI R68, R66, R67 # 5:13 -0037: JMPF R68, 39 # 5:13 -0038: JUMP 77 # 5:13 -0039: MOVE R66, R65 # 5:16 -0040: LOADI R67, 4 # 5:16 -0041: CMPEQI R68, R66, R67 # 5:16 -0042: JMPF R68, 44 # 5:16 -0043: JUMP 77 # 5:16 -0044: MOVE R66, R65 # 5:19 -0045: LOADI R67, 6 # 5:19 -0046: CMPEQI R68, R66, R67 # 5:19 -0047: JMPF R68, 49 # 5:19 -0048: JUMP 77 # 5:19 -0049: MOVE R66, R65 # 5:22 -0050: LOADI R67, 8 # 5:22 -0051: CMPEQI R68, R66, R67 # 5:22 -0052: JMPF R68, 54 # 5:22 -0053: JUMP 77 # 5:22 -0054: JUMP 55 # 13:1 -0055: MOVE R66, R65 # 7:10 -0056: LOADI R67, 10 # 7:10 -0057: CMPGEI R68, R66, R67 # 7:10 -0058: MOVE R69, R65 # 7:10 -0059: LOADI R70, 20 # 7:16 -0060: CMPLEI R71, R69, R70 # 7:10 -0061: AND R72, R68, R71 # 7:10 -0062: JMPF R72, 64 # 7:10 -0063: JUMP 81 # 7:10 -0064: JUMP 65 # 13:1 -0065: MOVE R66, R65 # 9:15 -0066: LOADI R67, 0 # 9:15 -0067: CMPLTI R68, R66, R67 # 9:15 -0068: JMPF R68, 70 # 9:15 -0069: JUMP 85 # 9:15 -0070: JUMP 71 # 13:1 -0071: JUMP 89 # 13:1 -0072: JUMP 92 # 13:1 -0073: LOADI R66, 0 # 4:13 -0074: LOADI R65, 259 # 4:13 -0075: UPCALL 0, R65 # 4:9, OUT -0076: JUMP 92 # 13:1 -0077: LOADI R66, 1 # 6:13 -0078: LOADI R65, 259 # 6:13 -0079: UPCALL 0, R65 # 6:9, OUT -0080: JUMP 92 # 13:1 -0081: LOADI R66, 2 # 8:13 -0082: LOADI R65, 259 # 8:13 -0083: UPCALL 0, R65 # 8:9, OUT -0084: JUMP 92 # 13:1 -0085: LOADI R66, 3 # 10:13 -0086: LOADI R65, 259 # 10:13 -0087: UPCALL 0, R65 # 10:9, OUT -0088: JUMP 92 # 13:1 -0089: LOADI R66, 4 # 12:13 -0090: LOADI R65, 259 # 12:13 -0091: UPCALL 0, R65 # 12:9, OUT -0092: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 1 ; 3:10 +0004: CMPEQI R68, R66, R67 ; 3:10 +0005: JMPF R68, 7 ; 3:10 +0006: JUMP 72 ; 3:10 +0007: MOVE R66, R65 ; 3:13 +0008: LOADI R67, 3 ; 3:13 +0009: CMPEQI R68, R66, R67 ; 3:13 +0010: JMPF R68, 12 ; 3:13 +0011: JUMP 72 ; 3:13 +0012: MOVE R66, R65 ; 3:16 +0013: LOADI R67, 5 ; 3:16 +0014: CMPEQI R68, R66, R67 ; 3:16 +0015: JMPF R68, 17 ; 3:16 +0016: JUMP 72 ; 3:16 +0017: MOVE R66, R65 ; 3:19 +0018: LOADI R67, 7 ; 3:19 +0019: CMPEQI R68, R66, R67 ; 3:19 +0020: JMPF R68, 22 ; 3:19 +0021: JUMP 72 ; 3:19 +0022: MOVE R66, R65 ; 3:22 +0023: LOADI R67, 9 ; 3:22 +0024: CMPEQI R68, R66, R67 ; 3:22 +0025: JMPF R68, 27 ; 3:22 +0026: JUMP 72 ; 3:22 +0027: JUMP 28 ; 13:1 +0028: MOVE R66, R65 ; 5:10 +0029: LOADI R67, 0 ; 5:10 +0030: CMPEQI R68, R66, R67 ; 5:10 +0031: JMPF R68, 33 ; 5:10 +0032: JUMP 76 ; 5:10 +0033: MOVE R66, R65 ; 5:13 +0034: LOADI R67, 2 ; 5:13 +0035: CMPEQI R68, R66, R67 ; 5:13 +0036: JMPF R68, 38 ; 5:13 +0037: JUMP 76 ; 5:13 +0038: MOVE R66, R65 ; 5:16 +0039: LOADI R67, 4 ; 5:16 +0040: CMPEQI R68, R66, R67 ; 5:16 +0041: JMPF R68, 43 ; 5:16 +0042: JUMP 76 ; 5:16 +0043: MOVE R66, R65 ; 5:19 +0044: LOADI R67, 6 ; 5:19 +0045: CMPEQI R68, R66, R67 ; 5:19 +0046: JMPF R68, 48 ; 5:19 +0047: JUMP 76 ; 5:19 +0048: MOVE R66, R65 ; 5:22 +0049: LOADI R67, 8 ; 5:22 +0050: CMPEQI R68, R66, R67 ; 5:22 +0051: JMPF R68, 53 ; 5:22 +0052: JUMP 76 ; 5:22 +0053: JUMP 54 ; 13:1 +0054: MOVE R66, R65 ; 7:10 +0055: LOADI R67, 10 ; 7:10 +0056: CMPGEI R68, R66, R67 ; 7:10 +0057: MOVE R69, R65 ; 7:10 +0058: LOADI R70, 20 ; 7:16 +0059: CMPLEI R71, R69, R70 ; 7:10 +0060: AND R72, R68, R71 ; 7:10 +0061: JMPF R72, 63 ; 7:10 +0062: JUMP 80 ; 7:10 +0063: JUMP 64 ; 13:1 +0064: MOVE R66, R65 ; 9:15 +0065: LOADI R67, 0 ; 9:15 +0066: CMPLTI R68, R66, R67 ; 9:15 +0067: JMPF R68, 69 ; 9:15 +0068: JUMP 84 ; 9:15 +0069: JUMP 70 ; 13:1 +0070: JUMP 88 ; 13:1 +0071: JUMP 91 ; 13:1 +0072: LOADI R66, 0 ; 4:13 +0073: LOADI R65, 259 ; 4:13 +0074: UPCALL 0, R65 ; 4:9, OUT +0075: JUMP 91 ; 13:1 +0076: LOADI R66, 1 ; 6:13 +0077: LOADI R65, 259 ; 6:13 +0078: UPCALL 0, R65 ; 6:9, OUT +0079: JUMP 91 ; 13:1 +0080: LOADI R66, 2 ; 8:13 +0081: LOADI R65, 259 ; 8:13 +0082: UPCALL 0, R65 ; 8:9, OUT +0083: JUMP 91 ; 13:1 +0084: LOADI R66, 3 ; 10:13 +0085: LOADI R65, 259 ; 10:13 +0086: UPCALL 0, R65 ; 10:9, OUT +0087: JUMP 91 ; 13:1 +0088: LOADI R66, 4 ; 12:13 +0089: LOADI R65, 259 ; 12:13 +0090: UPCALL 0, R65 ; 12:9, OUT +0091: EOF ; 0:0 ``` ## Output @@ -139,25 +138,24 @@ END SELECT ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 21 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 1 # 3:10 -0005: CMPEQI R68, R66, R67 # 3:10 -0006: JMPF R68, 8 # 3:10 -0007: JUMP 11 # 3:10 -0008: JUMP 9 # 7:1 -0009: JUMP 15 # 7:1 -0010: JUMP 18 # 7:1 -0011: LOADI R66, 0 # 4:13 -0012: LOADI R65, 259 # 4:13 -0013: UPCALL 0, R65 # 4:9, OUT -0014: JUMP 18 # 7:1 -0015: LOADI R66, 1 # 6:13 -0016: LOADI R65, 259 # 6:13 -0017: UPCALL 0, R65 # 6:9, OUT -0018: EOF # 0:0 +0000: LOADI R64, 21 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 1 ; 3:10 +0004: CMPEQI R68, R66, R67 ; 3:10 +0005: JMPF R68, 7 ; 3:10 +0006: JUMP 10 ; 3:10 +0007: JUMP 8 ; 7:1 +0008: JUMP 14 ; 7:1 +0009: JUMP 17 ; 7:1 +0010: LOADI R66, 0 ; 4:13 +0011: LOADI R65, 259 ; 4:13 +0012: UPCALL 0, R65 ; 4:9, OUT +0013: JUMP 17 ; 7:1 +0014: LOADI R66, 1 ; 6:13 +0015: LOADI R65, 259 ; 6:13 +0016: UPCALL 0, R65 ; 6:9, OUT +0017: EOF ; 0:0 ``` ## Output @@ -182,31 +180,30 @@ END SELECT ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: UPCALL 0, R64 # 1:13, MEANING_OF_LIFE -0002: LOADI R65, 1 # 1:31 -0003: ADDI R64, R64, R65 # 1:29 -0004: MOVE R65, R64 # 2:10 -0005: LOADI R66, 43 # 2:10 -0006: CMPEQI R67, R65, R66 # 2:10 -0007: JMPF R67, 9 # 2:10 -0008: JUMP 17 # 2:10 -0009: JUMP 10 # 6:1 -0010: MOVE R65, R64 # 4:10 -0011: LOADI R66, 100 # 4:10 -0012: CMPEQI R67, R65, R66 # 4:10 -0013: JMPF R67, 15 # 4:10 -0014: JUMP 21 # 4:10 -0015: JUMP 16 # 6:1 -0016: JUMP 24 # 6:1 -0017: LOADI R65, 0 # 3:13 -0018: LOADI R64, 259 # 3:13 -0019: UPCALL 1, R64 # 3:9, OUT -0020: JUMP 24 # 6:1 -0021: LOADI R65, 1 # 5:13 -0022: LOADI R64, 259 # 5:13 -0023: UPCALL 1, R64 # 5:9, OUT -0024: EOF # 0:0 +0000: UPCALL 0, R64 ; 1:13, MEANING_OF_LIFE +0001: LOADI R65, 1 ; 1:31 +0002: ADDI R64, R64, R65 ; 1:29 +0003: MOVE R65, R64 ; 2:10 +0004: LOADI R66, 43 ; 2:10 +0005: CMPEQI R67, R65, R66 ; 2:10 +0006: JMPF R67, 8 ; 2:10 +0007: JUMP 16 ; 2:10 +0008: JUMP 9 ; 6:1 +0009: MOVE R65, R64 ; 4:10 +0010: LOADI R66, 100 ; 4:10 +0011: CMPEQI R67, R65, R66 ; 4:10 +0012: JMPF R67, 14 ; 4:10 +0013: JUMP 20 ; 4:10 +0014: JUMP 15 ; 6:1 +0015: JUMP 23 ; 6:1 +0016: LOADI R65, 0 ; 3:13 +0017: LOADI R64, 259 ; 3:13 +0018: UPCALL 1, R64 ; 3:9, OUT +0019: JUMP 23 ; 6:1 +0020: LOADI R65, 1 ; 5:13 +0021: LOADI R64, 259 ; 5:13 +0022: UPCALL 1, R64 ; 5:9, OUT +0023: EOF ; 0:0 ``` ## Output @@ -227,12 +224,11 @@ END SELECT ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: UPCALL 0, R64 # 1:13, MEANING_OF_LIFE -0002: LOADI R65, 1 # 1:31 -0003: ADDI R64, R64, R65 # 1:29 -0004: JUMP 5 # 2:1 -0005: EOF # 0:0 +0000: UPCALL 0, R64 ; 1:13, MEANING_OF_LIFE +0001: LOADI R65, 1 ; 1:31 +0002: ADDI R64, R64, R65 ; 1:29 +0003: JUMP 4 ; 2:1 +0004: EOF ; 0:0 ``` # Test: CASE IS and TO with strings @@ -254,44 +250,43 @@ END SELECT ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADI R64, 0 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 1 # 3:10 -0005: CMPEQS R68, R66, R67 # 3:10 -0006: JMPF R68, 8 # 3:10 -0007: JUMP 26 # 3:10 -0008: JUMP 9 # 9:1 -0009: MOVE R66, R65 # 5:15 -0010: LOADI R67, 2 # 5:15 -0011: CMPGTS R68, R66, R67 # 5:15 -0012: JMPF R68, 14 # 5:15 -0013: JUMP 30 # 5:15 -0014: JUMP 15 # 9:1 -0015: MOVE R66, R65 # 7:10 -0016: LOADI R67, 3 # 7:10 -0017: CMPGES R68, R66, R67 # 7:10 -0018: MOVE R69, R65 # 7:10 -0019: LOADI R70, 4 # 7:17 -0020: CMPLES R71, R69, R70 # 7:10 -0021: AND R72, R68, R71 # 7:10 -0022: JMPF R72, 24 # 7:10 -0023: JUMP 34 # 7:10 -0024: JUMP 25 # 9:1 -0025: JUMP 37 # 9:1 -0026: LOADI R66, 1 # 4:13 -0027: LOADI R65, 259 # 4:13 -0028: UPCALL 0, R65 # 4:9, OUT -0029: JUMP 37 # 9:1 -0030: LOADI R66, 5 # 6:13 -0031: LOADI R65, 259 # 6:13 -0032: UPCALL 0, R65 # 6:9, OUT -0033: JUMP 37 # 9:1 -0034: LOADI R66, 6 # 8:13 -0035: LOADI R65, 259 # 8:13 -0036: UPCALL 0, R65 # 8:9, OUT -0037: EOF # 0:0 +0000: LOADI R64, 0 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 1 ; 3:10 +0004: CMPEQS R68, R66, R67 ; 3:10 +0005: JMPF R68, 7 ; 3:10 +0006: JUMP 25 ; 3:10 +0007: JUMP 8 ; 9:1 +0008: MOVE R66, R65 ; 5:15 +0009: LOADI R67, 2 ; 5:15 +0010: CMPGTS R68, R66, R67 ; 5:15 +0011: JMPF R68, 13 ; 5:15 +0012: JUMP 29 ; 5:15 +0013: JUMP 14 ; 9:1 +0014: MOVE R66, R65 ; 7:10 +0015: LOADI R67, 3 ; 7:10 +0016: CMPGES R68, R66, R67 ; 7:10 +0017: MOVE R69, R65 ; 7:10 +0018: LOADI R70, 4 ; 7:17 +0019: CMPLES R71, R69, R70 ; 7:10 +0020: AND R72, R68, R71 ; 7:10 +0021: JMPF R72, 23 ; 7:10 +0022: JUMP 33 ; 7:10 +0023: JUMP 24 ; 9:1 +0024: JUMP 36 ; 9:1 +0025: LOADI R66, 1 ; 4:13 +0026: LOADI R65, 259 ; 4:13 +0027: UPCALL 0, R65 ; 4:9, OUT +0028: JUMP 36 ; 9:1 +0029: LOADI R66, 5 ; 6:13 +0030: LOADI R65, 259 ; 6:13 +0031: UPCALL 0, R65 ; 6:9, OUT +0032: JUMP 36 ; 9:1 +0033: LOADI R66, 6 ; 8:13 +0034: LOADI R65, 259 ; 8:13 +0035: UPCALL 0, R65 ; 8:9, OUT +0036: EOF ; 0:0 ``` ## Output @@ -317,32 +312,31 @@ END SELECT ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADC R64, 0 # 1:6 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 2 # 3:10 -0005: ITOD R67 # 3:10 -0006: CMPEQD R68, R66, R67 # 3:10 -0007: JMPF R68, 9 # 3:10 -0008: JUMP 18 # 3:10 -0009: JUMP 10 # 7:1 -0010: MOVE R66, R65 # 5:15 -0011: LOADI R67, 5 # 5:15 -0012: ITOD R67 # 5:15 -0013: CMPGTD R68, R66, R67 # 5:15 -0014: JMPF R68, 16 # 5:15 -0015: JUMP 22 # 5:15 -0016: JUMP 17 # 7:1 -0017: JUMP 25 # 7:1 -0018: LOADI R66, 1 # 4:13 -0019: LOADI R65, 259 # 4:13 -0020: UPCALL 0, R65 # 4:9, OUT -0021: JUMP 25 # 7:1 -0022: LOADI R66, 2 # 6:13 -0023: LOADI R65, 259 # 6:13 -0024: UPCALL 0, R65 # 6:9, OUT -0025: EOF # 0:0 +0000: LOADC R64, 0 ; 1:6 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 2 ; 3:10 +0004: ITOD R67 ; 3:10 +0005: CMPEQD R68, R66, R67 ; 3:10 +0006: JMPF R68, 8 ; 3:10 +0007: JUMP 17 ; 3:10 +0008: JUMP 9 ; 7:1 +0009: MOVE R66, R65 ; 5:15 +0010: LOADI R67, 5 ; 5:15 +0011: ITOD R67 ; 5:15 +0012: CMPGTD R68, R66, R67 ; 5:15 +0013: JMPF R68, 15 ; 5:15 +0014: JUMP 21 ; 5:15 +0015: JUMP 16 ; 7:1 +0016: JUMP 24 ; 7:1 +0017: LOADI R66, 1 ; 4:13 +0018: LOADI R65, 259 ; 4:13 +0019: UPCALL 0, R65 ; 4:9, OUT +0020: JUMP 24 ; 7:1 +0021: LOADI R66, 2 ; 6:13 +0022: LOADI R65, 259 ; 6:13 +0023: UPCALL 0, R65 ; 6:9, OUT +0024: EOF ; 0:0 ``` ## Output @@ -370,55 +364,54 @@ END SELECT ## Disassembly ```asm -0000: ENTER 9 # 0:0 -0001: LOADI R64, 11 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADC R67, 0 # 3:10 -0005: ITOD R66 # 3:10 -0006: CMPEQD R68, R66, R67 # 3:10 -0007: JMPF R68, 9 # 3:10 -0008: JUMP 37 # 3:10 -0009: JUMP 10 # 9:1 -0010: MOVE R66, R65 # 5:10 -0011: LOADC R67, 1 # 5:10 -0012: ITOD R66 # 5:10 -0013: CMPEQD R68, R66, R67 # 5:10 -0014: JMPF R68, 16 # 5:10 -0015: JUMP 41 # 5:10 -0016: MOVE R66, R65 # 5:15 -0017: LOADC R67, 2 # 5:16 -0018: NEGD R67 # 5:15 -0019: ITOD R66 # 5:15 -0020: CMPEQD R68, R66, R67 # 5:15 -0021: JMPF R68, 23 # 5:15 -0022: JUMP 41 # 5:15 -0023: JUMP 24 # 9:1 -0024: MOVE R66, R65 # 7:10 -0025: LOADC R67, 3 # 7:10 -0026: ITOD R66 # 7:10 -0027: CMPGED R68, R66, R67 # 7:10 -0028: MOVE R69, R65 # 7:10 -0029: LOADC R70, 4 # 7:18 -0030: ITOD R69 # 7:10 -0031: CMPLED R71, R69, R70 # 7:10 -0032: AND R72, R68, R71 # 7:10 -0033: JMPF R72, 35 # 7:10 -0034: JUMP 45 # 7:10 -0035: JUMP 36 # 9:1 -0036: JUMP 48 # 9:1 -0037: LOADI R66, 5 # 4:13 -0038: LOADI R65, 259 # 4:13 -0039: UPCALL 0, R65 # 4:9, OUT -0040: JUMP 48 # 9:1 -0041: LOADI R66, 5 # 6:13 -0042: LOADI R65, 259 # 6:13 -0043: UPCALL 0, R65 # 6:9, OUT -0044: JUMP 48 # 9:1 -0045: LOADI R66, 6 # 8:13 -0046: LOADI R65, 259 # 8:13 -0047: UPCALL 0, R65 # 8:9, OUT -0048: EOF # 0:0 +0000: LOADI R64, 11 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADC R67, 0 ; 3:10 +0004: ITOD R66 ; 3:10 +0005: CMPEQD R68, R66, R67 ; 3:10 +0006: JMPF R68, 8 ; 3:10 +0007: JUMP 36 ; 3:10 +0008: JUMP 9 ; 9:1 +0009: MOVE R66, R65 ; 5:10 +0010: LOADC R67, 1 ; 5:10 +0011: ITOD R66 ; 5:10 +0012: CMPEQD R68, R66, R67 ; 5:10 +0013: JMPF R68, 15 ; 5:10 +0014: JUMP 40 ; 5:10 +0015: MOVE R66, R65 ; 5:15 +0016: LOADC R67, 2 ; 5:16 +0017: NEGD R67 ; 5:15 +0018: ITOD R66 ; 5:15 +0019: CMPEQD R68, R66, R67 ; 5:15 +0020: JMPF R68, 22 ; 5:15 +0021: JUMP 40 ; 5:15 +0022: JUMP 23 ; 9:1 +0023: MOVE R66, R65 ; 7:10 +0024: LOADC R67, 3 ; 7:10 +0025: ITOD R66 ; 7:10 +0026: CMPGED R68, R66, R67 ; 7:10 +0027: MOVE R69, R65 ; 7:10 +0028: LOADC R70, 4 ; 7:18 +0029: ITOD R69 ; 7:10 +0030: CMPLED R71, R69, R70 ; 7:10 +0031: AND R72, R68, R71 ; 7:10 +0032: JMPF R72, 34 ; 7:10 +0033: JUMP 44 ; 7:10 +0034: JUMP 35 ; 9:1 +0035: JUMP 47 ; 9:1 +0036: LOADI R66, 5 ; 4:13 +0037: LOADI R65, 259 ; 4:13 +0038: UPCALL 0, R65 ; 4:9, OUT +0039: JUMP 47 ; 9:1 +0040: LOADI R66, 5 ; 6:13 +0041: LOADI R65, 259 ; 6:13 +0042: UPCALL 0, R65 ; 6:9, OUT +0043: JUMP 47 ; 9:1 +0044: LOADI R66, 6 ; 8:13 +0045: LOADI R65, 259 ; 8:13 +0046: UPCALL 0, R65 ; 8:9, OUT +0047: EOF ; 0:0 ``` ## Output @@ -449,42 +442,41 @@ END SELECT ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 5 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 5 # 3:10 -0005: CMPEQI R68, R66, R67 # 3:10 -0006: JMPF R68, 8 # 3:10 -0007: JUMP 16 # 3:10 -0008: JUMP 9 # 12:1 -0009: MOVE R66, R65 # 10:10 -0010: LOADI R67, 6 # 10:10 -0011: CMPEQI R68, R66, R67 # 10:10 -0012: JMPF R68, 14 # 10:10 -0013: JUMP 32 # 10:10 -0014: JUMP 15 # 12:1 -0015: JUMP 35 # 12:1 -0016: LOADI R66, 0 # 4:13 -0017: LOADI R65, 259 # 4:13 -0018: UPCALL 0, R65 # 4:9, OUT -0019: LOADI R64, 6 # 5:13 -0020: MOVE R65, R64 # 6:21 -0021: MOVE R66, R65 # 7:18 -0022: LOADI R67, 6 # 7:18 -0023: CMPEQI R68, R66, R67 # 7:18 -0024: JMPF R68, 26 # 7:18 -0025: JUMP 28 # 7:18 -0026: JUMP 27 # 9:9 -0027: JUMP 31 # 9:9 -0028: LOADI R66, 1 # 8:21 -0029: LOADI R65, 259 # 8:21 -0030: UPCALL 0, R65 # 8:17, OUT -0031: JUMP 35 # 12:1 -0032: LOADI R66, 2 # 11:13 -0033: LOADI R65, 259 # 11:13 -0034: UPCALL 0, R65 # 11:9, OUT -0035: EOF # 0:0 +0000: LOADI R64, 5 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 5 ; 3:10 +0004: CMPEQI R68, R66, R67 ; 3:10 +0005: JMPF R68, 7 ; 3:10 +0006: JUMP 15 ; 3:10 +0007: JUMP 8 ; 12:1 +0008: MOVE R66, R65 ; 10:10 +0009: LOADI R67, 6 ; 10:10 +0010: CMPEQI R68, R66, R67 ; 10:10 +0011: JMPF R68, 13 ; 10:10 +0012: JUMP 31 ; 10:10 +0013: JUMP 14 ; 12:1 +0014: JUMP 34 ; 12:1 +0015: LOADI R66, 0 ; 4:13 +0016: LOADI R65, 259 ; 4:13 +0017: UPCALL 0, R65 ; 4:9, OUT +0018: LOADI R64, 6 ; 5:13 +0019: MOVE R65, R64 ; 6:21 +0020: MOVE R66, R65 ; 7:18 +0021: LOADI R67, 6 ; 7:18 +0022: CMPEQI R68, R66, R67 ; 7:18 +0023: JMPF R68, 25 ; 7:18 +0024: JUMP 27 ; 7:18 +0025: JUMP 26 ; 9:9 +0026: JUMP 30 ; 9:9 +0027: LOADI R66, 1 ; 8:21 +0028: LOADI R65, 259 ; 8:21 +0029: UPCALL 0, R65 ; 8:17, OUT +0030: JUMP 34 ; 12:1 +0031: LOADI R66, 2 ; 11:13 +0032: LOADI R65, 259 ; 11:13 +0033: UPCALL 0, R65 ; 11:9, OUT +0034: EOF ; 0:0 ``` ## Output @@ -521,45 +513,44 @@ RETURN ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 5 # 1:5 -0002: MOVE R65, R64 # 2:13 -0003: MOVE R66, R65 # 3:10 -0004: LOADI R67, 5 # 3:10 -0005: CMPEQI R68, R66, R67 # 3:10 -0006: JMPF R68, 8 # 3:10 -0007: JUMP 16 # 3:10 -0008: JUMP 9 # 8:1 -0009: MOVE R66, R65 # 6:10 -0010: LOADI R67, 6 # 6:10 -0011: CMPEQI R68, R66, R67 # 6:10 -0012: JMPF R68, 14 # 6:10 -0013: JUMP 21 # 6:10 -0014: JUMP 15 # 8:1 -0015: JUMP 24 # 8:1 -0016: LOADI R66, 0 # 4:13 -0017: LOADI R65, 259 # 4:13 -0018: UPCALL 0, R65 # 4:9, OUT -0019: GOSUB 25 # 5:15 -0020: JUMP 24 # 8:1 -0021: LOADI R66, 1 # 7:13 -0022: LOADI R65, 259 # 7:13 -0023: UPCALL 0, R65 # 7:9, OUT -0024: JUMP 38 # 9:6 -0025: LOADI R64, 6 # 11:5 -0026: MOVE R65, R64 # 12:13 -0027: MOVE R66, R65 # 13:10 -0028: LOADI R67, 6 # 13:10 -0029: CMPEQI R68, R66, R67 # 13:10 -0030: JMPF R68, 32 # 13:10 -0031: JUMP 34 # 13:10 -0032: JUMP 33 # 15:1 -0033: JUMP 37 # 15:1 -0034: LOADI R66, 2 # 14:13 -0035: LOADI R65, 259 # 14:13 -0036: UPCALL 0, R65 # 14:9, OUT -0037: RETURN # 16:1 -0038: EOF # 0:0 +0000: LOADI R64, 5 ; 1:5 +0001: MOVE R65, R64 ; 2:13 +0002: MOVE R66, R65 ; 3:10 +0003: LOADI R67, 5 ; 3:10 +0004: CMPEQI R68, R66, R67 ; 3:10 +0005: JMPF R68, 7 ; 3:10 +0006: JUMP 15 ; 3:10 +0007: JUMP 8 ; 8:1 +0008: MOVE R66, R65 ; 6:10 +0009: LOADI R67, 6 ; 6:10 +0010: CMPEQI R68, R66, R67 ; 6:10 +0011: JMPF R68, 13 ; 6:10 +0012: JUMP 20 ; 6:10 +0013: JUMP 14 ; 8:1 +0014: JUMP 23 ; 8:1 +0015: LOADI R66, 0 ; 4:13 +0016: LOADI R65, 259 ; 4:13 +0017: UPCALL 0, R65 ; 4:9, OUT +0018: GOSUB 24 ; 5:15 +0019: JUMP 23 ; 8:1 +0020: LOADI R66, 1 ; 7:13 +0021: LOADI R65, 259 ; 7:13 +0022: UPCALL 0, R65 ; 7:9, OUT +0023: JUMP 37 ; 9:6 +0024: LOADI R64, 6 ; 11:5 +0025: MOVE R65, R64 ; 12:13 +0026: MOVE R66, R65 ; 13:10 +0027: LOADI R67, 6 ; 13:10 +0028: CMPEQI R68, R66, R67 ; 13:10 +0029: JMPF R68, 31 ; 13:10 +0030: JUMP 33 ; 13:10 +0031: JUMP 32 ; 15:1 +0032: JUMP 36 ; 15:1 +0033: LOADI R66, 2 ; 14:13 +0034: LOADI R65, 259 ; 14:13 +0035: UPCALL 0, R65 ; 14:9, OUT +0036: RETURN ; 16:1 +0037: EOF ; 0:0 ``` ## Output @@ -601,29 +592,28 @@ OUT "done" ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R64, 42 # 1:13 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 1 # 2:10 -0004: CMPEQI R67, R65, R66 # 2:10 -0005: JMPF R67, 7 # 2:10 -0006: JUMP 15 # 2:10 -0007: JUMP 8 # 5:1 -0008: MOVE R65, R64 # 4:10 -0009: LOADI R66, 2 # 4:10 -0010: CMPEQI R67, R65, R66 # 4:10 -0011: JMPF R67, 13 # 4:10 -0012: JUMP 19 # 4:10 -0013: JUMP 14 # 5:1 -0014: JUMP 19 # 5:1 -0015: LOADI R65, 0 # 3:13 -0016: LOADI R64, 259 # 3:13 -0017: UPCALL 0, R64 # 3:9, OUT -0018: JUMP 19 # 5:1 -0019: LOADI R65, 1 # 6:5 -0020: LOADI R64, 259 # 6:5 -0021: UPCALL 0, R64 # 6:1, OUT -0022: EOF # 0:0 +0000: LOADI R64, 42 ; 1:13 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 1 ; 2:10 +0003: CMPEQI R67, R65, R66 ; 2:10 +0004: JMPF R67, 6 ; 2:10 +0005: JUMP 14 ; 2:10 +0006: JUMP 7 ; 5:1 +0007: MOVE R65, R64 ; 4:10 +0008: LOADI R66, 2 ; 4:10 +0009: CMPEQI R67, R65, R66 ; 4:10 +0010: JMPF R67, 12 ; 4:10 +0011: JUMP 18 ; 4:10 +0012: JUMP 13 ; 5:1 +0013: JUMP 18 ; 5:1 +0014: LOADI R65, 0 ; 3:13 +0015: LOADI R64, 259 ; 3:13 +0016: UPCALL 0, R64 ; 3:9, OUT +0017: JUMP 18 ; 5:1 +0018: LOADI R65, 1 ; 6:5 +0019: LOADI R64, 259 ; 6:5 +0020: UPCALL 0, R64 ; 6:1, OUT +0021: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_strings.md b/core2/tests/test_strings.md index dd533e9d..9e29ce9c 100644 --- a/core2/tests/test_strings.md +++ b/core2/tests/test_strings.md @@ -18,28 +18,27 @@ OUT c4 ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 0 # 1:6 -0002: LOADI R65, 1 # 2:6 -0003: MOVE R66, R64 # 4:6 -0004: MOVE R67, R65 # 4:11 -0005: CONCAT R66, R66, R67 # 4:9 -0006: MOVE R67, R66 # 5:6 -0007: LOADI R68, 2 # 5:11 -0008: CONCAT R67, R67, R68 # 5:9 -0009: MOVE R69, R64 # 7:5 -0010: LOADI R68, 259 # 7:5 -0011: UPCALL 0, R68 # 7:1, OUT -0012: MOVE R69, R65 # 8:5 -0013: LOADI R68, 259 # 8:5 -0014: UPCALL 0, R68 # 8:1, OUT -0015: MOVE R69, R66 # 9:5 -0016: LOADI R68, 259 # 9:5 -0017: UPCALL 0, R68 # 9:1, OUT -0018: MOVE R69, R67 # 10:5 -0019: LOADI R68, 259 # 10:5 -0020: UPCALL 0, R68 # 10:1, OUT -0021: EOF # 0:0 +0000: LOADI R64, 0 ; 1:6 +0001: LOADI R65, 1 ; 2:6 +0002: MOVE R66, R64 ; 4:6 +0003: MOVE R67, R65 ; 4:11 +0004: CONCAT R66, R66, R67 ; 4:9 +0005: MOVE R67, R66 ; 5:6 +0006: LOADI R68, 2 ; 5:11 +0007: CONCAT R67, R67, R68 ; 5:9 +0008: MOVE R69, R64 ; 7:5 +0009: LOADI R68, 259 ; 7:5 +0010: UPCALL 0, R68 ; 7:1, OUT +0011: MOVE R69, R65 ; 8:5 +0012: LOADI R68, 259 ; 8:5 +0013: UPCALL 0, R68 ; 8:1, OUT +0014: MOVE R69, R66 ; 9:5 +0015: LOADI R68, 259 ; 9:5 +0016: UPCALL 0, R68 ; 9:1, OUT +0017: MOVE R69, R67 ; 10:5 +0018: LOADI R68, 259 ; 10:5 +0019: UPCALL 0, R68 ; 10:1, OUT +0020: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 3336d443..083e12ed 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -18,33 +18,31 @@ OUT "After", a ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: LOADI R64, 10 # 1:5 -0002: JUMP 11 # 3:5 - --- FOO (BEGIN) -0003: ENTER 5 # 0:0 -0004: LOADI R64, 20 # 4:9 -0005: LOADI R66, 0 # 5:9 -0006: LOADI R65, 291 # 5:9 -0007: MOVE R68, R64 # 5:19 -0008: LOADI R67, 258 # 5:19 -0009: UPCALL 0, R65 # 5:5, OUT -0010: RETURN # 6:1 --- FOO (END) - -0011: LOADI R66, 1 # 8:5 -0012: LOADI R65, 291 # 8:5 -0013: MOVE R68, R64 # 8:15 -0014: LOADI R67, 258 # 8:15 -0015: UPCALL 0, R65 # 8:1, OUT -0016: CALL R65, 3 # 9:1, FOO -0017: LOADI R66, 2 # 10:5 -0018: LOADI R65, 291 # 10:5 -0019: MOVE R68, R64 # 10:14 -0020: LOADI R67, 258 # 10:14 -0021: UPCALL 0, R65 # 10:1, OUT -0022: EOF # 0:0 +0000: LOADI R64, 10 ; 1:5 +0001: JUMP 9 ; 3:5 + +;; FOO (BEGIN) +0002: LOADI R64, 20 ; 4:9 +0003: LOADI R66, 0 ; 5:9 +0004: LOADI R65, 291 ; 5:9 +0005: MOVE R68, R64 ; 5:19 +0006: LOADI R67, 258 ; 5:19 +0007: UPCALL 0, R65 ; 5:5, OUT +0008: RETURN ; 6:1 +;; FOO (END) + +0009: LOADI R66, 1 ; 8:5 +0010: LOADI R65, 291 ; 8:5 +0011: MOVE R68, R64 ; 8:15 +0012: LOADI R67, 258 ; 8:15 +0013: UPCALL 0, R65 ; 8:1, OUT +0014: CALL R65, 2 ; 9:1, FOO +0015: LOADI R66, 2 ; 10:5 +0016: LOADI R65, 291 ; 10:5 +0017: MOVE R68, R64 ; 10:14 +0018: LOADI R67, 258 ; 10:14 +0019: UPCALL 0, R65 ; 10:1, OUT +0020: EOF ; 0:0 ``` ## Output @@ -74,27 +72,24 @@ second ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: JUMP 7 # 1:5 +0000: JUMP 5 ; 1:5 --- FIRST (BEGIN) -0002: ENTER 2 # 0:0 -0003: LOADI R65, 0 # 2:9 -0004: LOADI R64, 259 # 2:9 -0005: UPCALL 0, R64 # 2:5, OUT -0006: RETURN # 3:1 --- FIRST (END) +;; FIRST (BEGIN) +0001: LOADI R65, 0 ; 2:9 +0002: LOADI R64, 259 ; 2:9 +0003: UPCALL 0, R64 ; 2:5, OUT +0004: RETURN ; 3:1 +;; FIRST (END) -0007: JUMP 11 # 5:5 +0005: JUMP 8 ; 5:5 --- SECOND (BEGIN) -0008: ENTER 0 # 0:0 -0009: CALL R64, 2 # 6:5, FIRST -0010: RETURN # 7:1 --- SECOND (END) +;; SECOND (BEGIN) +0006: CALL R64, 1 ; 6:5, FIRST +0007: RETURN ; 7:1 +;; SECOND (END) -0011: CALL R64, 8 # 9:1, SECOND -0012: EOF # 0:0 +0008: CALL R64, 6 ; 9:1, SECOND +0009: EOF ; 0:0 ``` ## Output @@ -143,52 +138,49 @@ OUT "After modify_1", var ## Disassembly ```asm -0000: ENTER 5 # 0:0 -0001: JUMP 10 # 1:5 - --- MODIFY_2 (BEGIN) -0002: ENTER 5 # 0:0 -0003: LOADI R64, 2 # 2:11 -0004: LOADI R66, 0 # 3:9 -0005: LOADI R65, 291 # 3:9 -0006: MOVE R68, R64 # 3:28 -0007: LOADI R67, 258 # 3:28 -0008: UPCALL 0, R65 # 3:5, OUT -0009: RETURN # 4:1 --- MODIFY_2 (END) - -0010: JUMP 25 # 6:5 - --- MODIFY_1 (BEGIN) -0011: ENTER 5 # 0:0 -0012: LOADI R64, 1 # 7:11 -0013: LOADI R66, 1 # 8:9 -0014: LOADI R65, 291 # 8:9 -0015: MOVE R68, R64 # 8:28 -0016: LOADI R67, 258 # 8:28 -0017: UPCALL 0, R65 # 8:5, OUT -0018: CALL R65, 2 # 9:5, MODIFY_2 -0019: LOADI R66, 2 # 10:9 -0020: LOADI R65, 291 # 10:9 -0021: MOVE R68, R64 # 10:27 -0022: LOADI R67, 258 # 10:27 -0023: UPCALL 0, R65 # 10:5, OUT -0024: RETURN # 11:1 --- MODIFY_1 (END) - -0025: LOADI R64, 0 # 13:7 -0026: LOADI R66, 3 # 14:5 -0027: LOADI R65, 291 # 14:5 -0028: MOVE R68, R64 # 14:24 -0029: LOADI R67, 258 # 14:24 -0030: UPCALL 0, R65 # 14:1, OUT -0031: CALL R65, 11 # 15:1, MODIFY_1 -0032: LOADI R66, 4 # 16:5 -0033: LOADI R65, 291 # 16:5 -0034: MOVE R68, R64 # 16:23 -0035: LOADI R67, 258 # 16:23 -0036: UPCALL 0, R65 # 16:1, OUT -0037: EOF # 0:0 +0000: JUMP 8 ; 1:5 + +;; MODIFY_2 (BEGIN) +0001: LOADI R64, 2 ; 2:11 +0002: LOADI R66, 0 ; 3:9 +0003: LOADI R65, 291 ; 3:9 +0004: MOVE R68, R64 ; 3:28 +0005: LOADI R67, 258 ; 3:28 +0006: UPCALL 0, R65 ; 3:5, OUT +0007: RETURN ; 4:1 +;; MODIFY_2 (END) + +0008: JUMP 22 ; 6:5 + +;; MODIFY_1 (BEGIN) +0009: LOADI R64, 1 ; 7:11 +0010: LOADI R66, 1 ; 8:9 +0011: LOADI R65, 291 ; 8:9 +0012: MOVE R68, R64 ; 8:28 +0013: LOADI R67, 258 ; 8:28 +0014: UPCALL 0, R65 ; 8:5, OUT +0015: CALL R65, 1 ; 9:5, MODIFY_2 +0016: LOADI R66, 2 ; 10:9 +0017: LOADI R65, 291 ; 10:9 +0018: MOVE R68, R64 ; 10:27 +0019: LOADI R67, 258 ; 10:27 +0020: UPCALL 0, R65 ; 10:5, OUT +0021: RETURN ; 11:1 +;; MODIFY_1 (END) + +0022: LOADI R64, 0 ; 13:7 +0023: LOADI R66, 3 ; 14:5 +0024: LOADI R65, 291 ; 14:5 +0025: MOVE R68, R64 ; 14:24 +0026: LOADI R67, 258 ; 14:24 +0027: UPCALL 0, R65 ; 14:1, OUT +0028: CALL R65, 9 ; 15:1, MODIFY_1 +0029: LOADI R66, 4 ; 16:5 +0030: LOADI R65, 291 ; 16:5 +0031: MOVE R68, R64 ; 16:23 +0032: LOADI R67, 258 ; 16:23 +0033: UPCALL 0, R65 ; 16:1, OUT +0034: EOF ; 0:0 ``` ## Output @@ -299,37 +291,35 @@ NEXT ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: JUMP 15 # 1:5 - --- MAYBE_EXIT (BEGIN) -0002: ENTER 3 # 0:0 -0003: LOADI R66, 1 # 2:9 -0004: LOADI R65, 258 # 2:9 -0005: UPCALL 0, R65 # 2:5, OUT -0006: MOVE R65, R64 # 3:8 -0007: LOADI R66, 2 # 3:12 -0008: CMPGTI R65, R65, R66 # 3:10 -0009: JMPF R65, 11 # 3:8 -0010: JUMP 14 # 3:19 -0011: LOADI R66, 2 # 4:9 -0012: LOADI R65, 258 # 4:9 -0013: UPCALL 0, R65 # 4:5, OUT -0014: RETURN # 5:1 --- MAYBE_EXIT (END) - -0015: LOADI R64, 0 # 7:9 -0016: MOVE R65, R64 # 7:5 -0017: LOADI R66, 5 # 7:14 -0018: CMPLEI R65, R65, R66 # 7:11 -0019: JMPF R65, 26 # 7:5 -0020: MOVE R65, R64 # 8:16 -0021: CALL R65, 2 # 8:5, MAYBE_EXIT -0022: MOVE R64, R64 # 7:5 -0023: LOADI R65, 1 # 7:15 -0024: ADDI R64, R64, R65 # 7:11 -0025: JUMP 16 # 7:5 -0026: EOF # 0:0 +0000: JUMP 13 ; 1:5 + +;; MAYBE_EXIT (BEGIN) +0001: LOADI R66, 1 ; 2:9 +0002: LOADI R65, 258 ; 2:9 +0003: UPCALL 0, R65 ; 2:5, OUT +0004: MOVE R65, R64 ; 3:8 +0005: LOADI R66, 2 ; 3:12 +0006: CMPGTI R65, R65, R66 ; 3:10 +0007: JMPF R65, 9 ; 3:8 +0008: JUMP 12 ; 3:19 +0009: LOADI R66, 2 ; 4:9 +0010: LOADI R65, 258 ; 4:9 +0011: UPCALL 0, R65 ; 4:5, OUT +0012: RETURN ; 5:1 +;; MAYBE_EXIT (END) + +0013: LOADI R64, 0 ; 7:9 +0014: MOVE R65, R64 ; 7:5 +0015: LOADI R66, 5 ; 7:14 +0016: CMPLEI R65, R65, R66 ; 7:11 +0017: JMPF R65, 24 ; 7:5 +0018: MOVE R65, R64 ; 8:16 +0019: CALL R65, 1 ; 8:5, MAYBE_EXIT +0020: MOVE R64, R64 ; 7:5 +0021: LOADI R65, 1 ; 7:15 +0022: ADDI R64, R64, R65 ; 7:11 +0023: JUMP 14 ; 7:5 +0024: EOF ; 0:0 ``` ## Output @@ -398,33 +388,31 @@ count_down "counter is" ## Disassembly ```asm -0000: ENTER 1 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: JUMP 19 # 2:5 - --- COUNT_DOWN (BEGIN) -0003: ENTER 5 # 0:0 -0004: MOVE R66, R64 # 3:9 -0005: LOADI R65, 275 # 3:9 -0006: MOVE R68, R0 # 3:17 -0007: LOADI R67, 258 # 3:17 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R65, R0 # 4:8 -0010: LOADI R66, 1 # 4:18 -0011: CMPGTI R65, R65, R66 # 4:16 -0012: JMPF R65, 18 # 4:8 -0013: MOVE R0, R0 # 5:19 -0014: LOADI R65, 1 # 5:29 -0015: SUBI R0, R0, R65 # 5:27 -0016: MOVE R65, R64 # 6:20 -0017: CALL R65, 3 # 6:9, COUNT_DOWN -0018: RETURN # 8:1 --- COUNT_DOWN (END) - -0019: LOADI R0, 3 # 9:11 -0020: LOADI R64, 0 # 10:12 -0021: CALL R64, 3 # 10:1, COUNT_DOWN -0022: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: JUMP 17 ; 2:5 + +;; COUNT_DOWN (BEGIN) +0002: MOVE R66, R64 ; 3:9 +0003: LOADI R65, 275 ; 3:9 +0004: MOVE R68, R0 ; 3:17 +0005: LOADI R67, 258 ; 3:17 +0006: UPCALL 0, R65 ; 3:5, OUT +0007: MOVE R65, R0 ; 4:8 +0008: LOADI R66, 1 ; 4:18 +0009: CMPGTI R65, R65, R66 ; 4:16 +0010: JMPF R65, 16 ; 4:8 +0011: MOVE R0, R0 ; 5:19 +0012: LOADI R65, 1 ; 5:29 +0013: SUBI R0, R0, R65 ; 5:27 +0014: MOVE R65, R64 ; 6:20 +0015: CALL R65, 2 ; 6:9, COUNT_DOWN +0016: RETURN ; 8:1 +;; COUNT_DOWN (END) + +0017: LOADI R0, 3 ; 9:11 +0018: LOADI R64, 0 ; 10:12 +0019: CALL R64, 2 ; 10:1, COUNT_DOWN +0020: EOF ; 0:0 ``` ## Output @@ -466,54 +454,51 @@ OUT value ## Disassembly ```asm -0000: ENTER 4 # 0:0 -0001: LOADI R0, 0 # 1:12 -0002: JUMP 22 # 5:10 - --- COUNT_VALUE (BEGIN) -0003: LOADI R64, 0 # 5:10 -0004: ENTER 4 # 0:0 -0005: MOVE R0, R0 # 6:13 -0006: LOADI R66, 1 # 6:21 -0007: ADDI R0, R0, R66 # 6:19 -0008: MOVE R66, R65 # 7:8 -0009: LOADI R67, 0 # 7:12 -0010: CMPEQI R66, R66, R67 # 7:10 -0011: JMPF R66, 14 # 7:8 -0012: MOVE R64, R0 # 8:23 -0013: JUMP 21 # 7:8 -0014: LOADI R66, 1 # 9:5 -0015: JMPF R66, 21 # 9:5 -0016: MOVE R66, R65 # 10:20 -0017: LOADI R67, 1 # 10:24 -0018: SUBI R66, R66, R67 # 10:22 -0019: CALL R66, 23 # 10:9, BUMP_VALUE -0020: MOVE R64, R0 # 11:23 -0021: RETURN # 13:1 --- COUNT_VALUE (END) - -0022: JUMP 31 # 15:5 - --- BUMP_VALUE (BEGIN) -0023: ENTER 3 # 0:0 -0024: MOVE R0, R0 # 16:13 -0025: LOADI R65, 10 # 16:21 -0026: ADDI R0, R0, R65 # 16:19 -0027: MOVE R66, R64 # 17:25 -0028: CALL R65, 3 # 17:13, COUNT_VALUE -0029: MOVE R0, R65 # 17:13 -0030: RETURN # 18:1 --- BUMP_VALUE (END) - -0031: LOADI R67, 2 # 20:17 -0032: CALL R66, 3 # 20:5, COUNT_VALUE -0033: MOVE R65, R66 # 20:5 -0034: LOADI R64, 258 # 20:5 -0035: UPCALL 0, R64 # 20:1, OUT -0036: MOVE R65, R0 # 21:5 -0037: LOADI R64, 258 # 21:5 -0038: UPCALL 0, R64 # 21:1, OUT -0039: EOF # 0:0 +0000: LOADI R0, 0 ; 1:12 +0001: JUMP 20 ; 5:10 + +;; COUNT_VALUE (BEGIN) +0002: LOADI R64, 0 ; 5:10 +0003: MOVE R0, R0 ; 6:13 +0004: LOADI R66, 1 ; 6:21 +0005: ADDI R0, R0, R66 ; 6:19 +0006: MOVE R66, R65 ; 7:8 +0007: LOADI R67, 0 ; 7:12 +0008: CMPEQI R66, R66, R67 ; 7:10 +0009: JMPF R66, 12 ; 7:8 +0010: MOVE R64, R0 ; 8:23 +0011: JUMP 19 ; 7:8 +0012: LOADI R66, 1 ; 9:5 +0013: JMPF R66, 19 ; 9:5 +0014: MOVE R66, R65 ; 10:20 +0015: LOADI R67, 1 ; 10:24 +0016: SUBI R66, R66, R67 ; 10:22 +0017: CALL R66, 21 ; 10:9, BUMP_VALUE +0018: MOVE R64, R0 ; 11:23 +0019: RETURN ; 13:1 +;; COUNT_VALUE (END) + +0020: JUMP 28 ; 15:5 + +;; BUMP_VALUE (BEGIN) +0021: MOVE R0, R0 ; 16:13 +0022: LOADI R65, 10 ; 16:21 +0023: ADDI R0, R0, R65 ; 16:19 +0024: MOVE R66, R64 ; 17:25 +0025: CALL R65, 2 ; 17:13, COUNT_VALUE +0026: MOVE R0, R65 ; 17:13 +0027: RETURN ; 18:1 +;; BUMP_VALUE (END) + +0028: LOADI R67, 2 ; 20:17 +0029: CALL R66, 2 ; 20:5, COUNT_VALUE +0030: MOVE R65, R66 ; 20:5 +0031: LOADI R64, 258 ; 20:5 +0032: UPCALL 0, R64 ; 20:1, OUT +0033: MOVE R65, R0 ; 21:5 +0034: LOADI R64, 258 ; 21:5 +0035: UPCALL 0, R64 ; 21:1, OUT +0036: EOF ; 0:0 ``` ## Output @@ -621,8 +606,7 @@ DECLARE SUB bar(a AS STRING) ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: EOF # 0:0 +0000: EOF ; 0:0 ``` # Test: Sub declarations match definition @@ -641,15 +625,13 @@ DECLARE SUB foo ## Disassembly ```asm -0000: ENTER 0 # 0:0 -0001: JUMP 4 # 3:5 +0000: JUMP 2 ; 3:5 --- FOO (BEGIN) -0002: ENTER 0 # 0:0 -0003: RETURN # 4:1 --- FOO (END) +;; FOO (BEGIN) +0001: RETURN ; 4:1 +;; FOO (END) -0004: EOF # 0:0 +0002: EOF ; 0:0 ``` # Test: Sub declarations must be top-level diff --git a/core2/tests/test_types.md b/core2/tests/test_types.md index 88b8daca..465f7af7 100644 --- a/core2/tests/test_types.md +++ b/core2/tests/test_types.md @@ -11,15 +11,14 @@ OUT bool_1, bool_2 ## Disassembly ```asm -0000: ENTER 6 # 0:0 -0001: LOADI R64, 0 # 1:10 -0002: LOADI R65, 1 # 2:10 -0003: MOVE R67, R64 # 3:5 -0004: LOADI R66, 288 # 3:5 -0005: MOVE R69, R65 # 3:13 -0006: LOADI R68, 256 # 3:13 -0007: UPCALL 0, R66 # 3:1, OUT -0008: EOF # 0:0 +0000: LOADI R64, 0 ; 1:10 +0001: LOADI R65, 1 ; 2:10 +0002: MOVE R67, R64 ; 3:5 +0003: LOADI R66, 288 ; 3:5 +0004: MOVE R69, R65 ; 3:13 +0005: LOADI R68, 256 ; 3:13 +0006: UPCALL 0, R66 ; 3:1, OUT +0007: EOF ; 0:0 ``` ## Output @@ -43,21 +42,20 @@ OUT zero_double, small_double, large_double, tiny_double ## Disassembly ```asm -0000: ENTER 12 # 0:0 -0001: LOADC R64, 0 # 1:15 -0002: LOADC R65, 1 # 2:16 -0003: LOADC R66, 2 # 3:16 -0004: LOADC R67, 3 # 4:15 -0005: MOVE R69, R64 # 5:5 -0006: LOADI R68, 289 # 5:5 -0007: MOVE R71, R65 # 5:18 -0008: LOADI R70, 289 # 5:18 -0009: MOVE R73, R66 # 5:32 -0010: LOADI R72, 289 # 5:32 -0011: MOVE R75, R67 # 5:46 -0012: LOADI R74, 257 # 5:46 -0013: UPCALL 0, R68 # 5:1, OUT -0014: EOF # 0:0 +0000: LOADC R64, 0 ; 1:15 +0001: LOADC R65, 1 ; 2:16 +0002: LOADC R66, 2 ; 3:16 +0003: LOADC R67, 3 ; 4:15 +0004: MOVE R69, R64 ; 5:5 +0005: LOADI R68, 289 ; 5:5 +0006: MOVE R71, R65 ; 5:18 +0007: LOADI R70, 289 ; 5:18 +0008: MOVE R73, R66 ; 5:32 +0009: LOADI R72, 289 ; 5:32 +0010: MOVE R75, R67 ; 5:46 +0011: LOADI R74, 257 ; 5:46 +0012: UPCALL 0, R68 ; 5:1, OUT +0013: EOF ; 0:0 ``` ## Output @@ -78,12 +76,11 @@ OUT small_int ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 123 # 1:13 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 258 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: EOF # 0:0 +0000: LOADI R64, 123 ; 1:13 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 258 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -104,12 +101,11 @@ OUT large_int ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADC R64, 0 # 1:13 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 258 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: EOF # 0:0 +0000: LOADC R64, 0 ; 1:13 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 258 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: EOF ; 0:0 ``` ## Output @@ -130,12 +126,11 @@ OUT text ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 0 # 1:8 -0002: MOVE R66, R64 # 2:5 -0003: LOADI R65, 259 # 2:5 -0004: UPCALL 0, R65 # 2:1, OUT -0005: EOF # 0:0 +0000: LOADI R64, 0 ; 1:8 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 259 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: EOF ; 0:0 ``` ## Output diff --git a/core2/tests/test_while.md b/core2/tests/test_while.md index a93b2f47..a984d364 100644 --- a/core2/tests/test_while.md +++ b/core2/tests/test_while.md @@ -13,20 +13,19 @@ WEND ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 3 # 1:5 -0002: MOVE R65, R64 # 2:7 -0003: LOADI R66, 0 # 2:11 -0004: CMPGTI R65, R65, R66 # 2:9 -0005: JMPF R65, 13 # 2:7 -0006: MOVE R66, R64 # 3:9 -0007: LOADI R65, 258 # 3:9 -0008: UPCALL 0, R65 # 3:5, OUT -0009: MOVE R64, R64 # 4:9 -0010: LOADI R65, 1 # 4:13 -0011: SUBI R64, R64, R65 # 4:11 -0012: JUMP 2 # 2:7 -0013: EOF # 0:0 +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R65, R64 ; 2:7 +0002: LOADI R66, 0 ; 2:11 +0003: CMPGTI R65, R65, R66 ; 2:9 +0004: JMPF R65, 12 ; 2:7 +0005: MOVE R66, R64 ; 3:9 +0006: LOADI R65, 258 ; 3:9 +0007: UPCALL 0, R65 ; 3:5, OUT +0008: MOVE R64, R64 ; 4:9 +0009: LOADI R65, 1 ; 4:13 +0010: SUBI R64, R64, R65 ; 4:11 +0011: JUMP 1 ; 2:7 +0012: EOF ; 0:0 ``` ## Output @@ -50,14 +49,13 @@ WEND ## Disassembly ```asm -0000: ENTER 2 # 0:0 -0001: LOADI R64, 0 # 1:7 -0002: JMPF R64, 7 # 1:7 -0003: LOADI R65, 1 # 2:9 -0004: LOADI R64, 258 # 2:9 -0005: UPCALL 0, R64 # 2:5, OUT -0006: JUMP 1 # 1:7 -0007: EOF # 0:0 +0000: LOADI R64, 0 ; 1:7 +0001: JMPF R64, 6 ; 1:7 +0002: LOADI R65, 1 ; 2:9 +0003: LOADI R64, 258 ; 2:9 +0004: UPCALL 0, R64 ; 2:5, OUT +0005: JUMP 0 ; 1:7 +0006: EOF ; 0:0 ``` # Test: WHILE guard must be boolean @@ -141,27 +139,26 @@ OUT 9 ## Disassembly ```asm -0000: ENTER 3 # 0:0 -0001: LOADI R64, 2 # 1:5 -0002: MOVE R65, R64 # 2:10 -0003: LOADI R66, 0 # 2:14 -0004: CMPGTI R65, R65, R66 # 2:12 -0005: JMPF R65, 17 # 2:10 -0006: LOADI R65, 1 # 3:11 -0007: JMPF R65, 10 # 3:11 -0008: JUMP 17 # 4:9 -0009: JUMP 6 # 3:11 -0010: MOVE R66, R64 # 6:9 -0011: LOADI R65, 258 # 6:9 -0012: UPCALL 0, R65 # 6:5, OUT -0013: MOVE R64, R64 # 7:9 -0014: LOADI R65, 1 # 7:13 -0015: SUBI R64, R64, R65 # 7:11 -0016: JUMP 2 # 2:10 -0017: LOADI R66, 9 # 9:5 -0018: LOADI R65, 258 # 9:5 -0019: UPCALL 0, R65 # 9:1, OUT -0020: EOF # 0:0 +0000: LOADI R64, 2 ; 1:5 +0001: MOVE R65, R64 ; 2:10 +0002: LOADI R66, 0 ; 2:14 +0003: CMPGTI R65, R65, R66 ; 2:12 +0004: JMPF R65, 16 ; 2:10 +0005: LOADI R65, 1 ; 3:11 +0006: JMPF R65, 9 ; 3:11 +0007: JUMP 16 ; 4:9 +0008: JUMP 5 ; 3:11 +0009: MOVE R66, R64 ; 6:9 +0010: LOADI R65, 258 ; 6:9 +0011: UPCALL 0, R65 ; 6:5, OUT +0012: MOVE R64, R64 ; 7:9 +0013: LOADI R65, 1 ; 7:13 +0014: SUBI R64, R64, R65 ; 7:11 +0015: JUMP 1 ; 2:10 +0016: LOADI R66, 9 ; 9:5 +0017: LOADI R65, 258 ; 9:5 +0018: UPCALL 0, R65 ; 9:1, OUT +0019: EOF ; 0:0 ``` ## Output From d69545c442abb9e62d05bc4dede78da0046edbdd Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Tue, 24 Mar 2026 18:32:22 +0100 Subject: [PATCH 44/53] core2: Add incremental compilation This extends the Compiler to support compiling code in chunks which is what the REPL will need to operate. To make this possible, we have to add the concept of a "snapshot" of the LocalSymtable so that we can deal with lifetime issues in the Compiler: we need to store a GlobalSymtable in it, and we need to track the program-scope state as a LocalSymtable... but Rust doesn't let us do this. The snapshot is a mechanism to workaround this limitation. To test this, teach the integration tests to process multiple Source sections and keep going after compilation failures. With that, add coverage for REPL-style symbol, callable, and DATA state across incremental passes. --- core2/src/compiler/codegen.rs | 23 +- core2/src/compiler/ids.rs | 7 +- core2/src/compiler/mod.rs | 24 +- core2/src/compiler/syms.rs | 46 ++++ core2/src/compiler/top.rs | 21 +- core2/tests/integration_test.rs | 1 + core2/tests/test_incremental.md | 475 ++++++++++++++++++++++++++++++++ core2/tests/testutils/mod.rs | 293 +++++++++++++------- 8 files changed, 776 insertions(+), 114 deletions(-) create mode 100644 core2/tests/test_incremental.md diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index f286ebde..a9987c11 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -30,6 +30,7 @@ type Address = usize; /// Represents a fixup that needs to be applied to an instruction after all symbols have been /// located. +#[derive(Clone)] pub(super) enum Fixup { /// Fixup to resolve a user-defined call target address into a `CALL` instruction. Call(Register, SymbolKey), @@ -45,7 +46,7 @@ pub(super) enum Fixup { } /// The code generator. -#[derive(Default)] +#[derive(Clone, Default)] pub(super) struct Codegen { /// The instructions being generated. code: Vec, @@ -100,6 +101,14 @@ impl Codegen { self.instrs[addr].arg_linecols = arg_linecols; } + /// Removes the EOF instruction from the program, if any. + pub(super) fn pop_eof(&mut self) { + if let Some(instr) = self.code.pop() { + debug_assert_eq!(bytecode::make_eof(), instr); + self.instrs.pop(); + } + } + /// Emits code to set `reg` to the default value for `vtype`. pub(super) fn emit_default(&mut self, reg: Register, vtype: ExprType, pos: LineCol) { let instr = match vtype { @@ -246,27 +255,27 @@ impl Codegen { /// Consumes the code generator and builds a ready-to-use `Image`. pub(super) fn build_image( - mut self, + &mut self, global_vars: HashMap, data: Vec>, ) -> Result { self.apply_fixups()?; let mut callables = HashMap::default(); - for (key, (start_pc, end_pc)) in self.user_callables_addresses { - let previous = callables.insert(start_pc, (key.clone(), true)); + for (key, (start_pc, end_pc)) in &self.user_callables_addresses { + let previous = callables.insert(*start_pc, (key.clone(), true)); debug_assert!(previous.is_none(), "An address can only start one callable"); - let previous = callables.insert(end_pc, (key, false)); + let previous = callables.insert(*end_pc, (key.clone(), false)); debug_assert!(previous.is_none(), "An address can only start one callable"); } Ok(Image::new( - self.code, + self.code.clone(), self.upcalls.keys_to_vec(), self.constants.keys_to_vec(), data, - DebugInfo { instrs: self.instrs, callables, global_vars }, + DebugInfo { instrs: self.instrs.clone(), callables, global_vars }, )) } } diff --git a/core2/src/compiler/ids.rs b/core2/src/compiler/ids.rs index 810a2a70..75e70667 100644 --- a/core2/src/compiler/ids.rs +++ b/core2/src/compiler/ids.rs @@ -22,6 +22,7 @@ use std::hash::Hash; /// Hash map that assigns sequential identifiers to elements as they are inserted and /// allows later retrieval of these identifiers and retrieving the inserted values in /// insertion order. +#[derive(Clone)] pub(super) struct HashMapWithIds { /// The underlying storage mapping keys to their values and assigned identifiers. map: HashMap, @@ -89,10 +90,10 @@ where } /// Returns the keys in insertion order. - pub(super) fn keys_to_vec(self) -> Vec { - let mut reverse = self.map.into_iter().collect::>(); + pub(super) fn keys_to_vec(&self) -> Vec { + let mut reverse = self.map.iter().collect::>(); reverse.sort_by_key(|(_key, (_value, index))| *index); - reverse.into_iter().map(|(key, _index)| key).collect() + reverse.into_iter().map(|(key, _index)| key.clone()).collect() } } diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 941a28df..5d9d29f7 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -34,8 +34,8 @@ mod exprs; mod ids; mod syms; -use syms::GlobalSymtable; pub use syms::SymbolKey; +use syms::{GlobalSymtable, LocalSymtable, LocalSymtableSnapshot}; mod top; use top::{Context, prepare_globals}; @@ -184,6 +184,7 @@ pub type Result = std::result::Result; pub struct Compiler { context: Context, symtable: GlobalSymtable, + program_scope: LocalSymtableSnapshot, } impl Compiler { @@ -204,14 +205,27 @@ impl Compiler { let mut context = Context::default(); let mut symtable = GlobalSymtable::new(upcalls_metadata); - prepare_globals(&mut context, &mut symtable, global_defs)?; - Ok(Self { context, symtable }) + Ok(Self { context, symtable, program_scope: LocalSymtableSnapshot::default() }) + } + + /// Compiles a chunk of code. + pub fn compile(mut self, input: &mut dyn io::Read) -> Result { + let symtable = LocalSymtable::restore(&mut self.symtable, self.program_scope); + let (image, _) = top::compile(input, &mut self.context, symtable)?; + Ok(image) } /// Compiles a chunk of code. - pub fn compile(self, input: &mut dyn io::Read) -> Result { - top::compile(input, self.context, self.symtable) + pub fn compile_more(&mut self, input: &mut dyn io::Read) -> Result { + let mut new_context = self.context.clone(); + let mut new_symtable = self.symtable.clone(); + let program_scope = LocalSymtable::restore(&mut new_symtable, self.program_scope.clone()); + let (image, snapshot) = top::compile(input, &mut new_context, program_scope)?; + self.context = new_context; + self.symtable = new_symtable; + self.program_scope = snapshot; + Ok(image) } } diff --git a/core2/src/compiler/syms.rs b/core2/src/compiler/syms.rs index a27529b8..43252a6b 100644 --- a/core2/src/compiler/syms.rs +++ b/core2/src/compiler/syms.rs @@ -146,6 +146,7 @@ where /// Representation of the symbol table for global symbols. /// /// Globals are variables and callables that are visible from any scope. +#[derive(Clone)] pub(crate) struct GlobalSymtable { /// Map of global variable names to their prototypes and assigned registers. globals: HashMapWithIds, @@ -218,6 +219,20 @@ impl GlobalSymtable { } } +#[derive(Clone, Default)] +pub(crate) struct LocalSymtableSnapshot { + /// Map of local variable names to their prototypes and assigned registers. + locals: HashMapWithIds, + + /// Maximum number of allocated temporary registers in all possible evaluation scopes created + /// by this local symtable. This is used to determine the size of the scope for register + /// allocation purposes at runtime. + count_temps: u8, + + /// Number of reserved temporary registers that are active outside of `TempScope`. + active_temps: Rc>, +} + /// Representation of the symbol table for a local scope. /// /// A local scope can see all global symbols and defines its own symbols, which can shadow the @@ -249,6 +264,30 @@ impl<'a> LocalSymtable<'a> { } } + /// Preserves the state of this local symbol table, detached from the global symbol table + /// it belongs to. + pub(crate) fn save(self) -> LocalSymtableSnapshot { + LocalSymtableSnapshot { + locals: self.locals, + count_temps: self.count_temps, + active_temps: self.active_temps, + } + } + + /// Reattaches a previous local symbol table content to a global symbol table so that it + /// can be used again for compilation. + pub(crate) fn restore( + symtable: &'a mut GlobalSymtable, + snapshot: LocalSymtableSnapshot, + ) -> Self { + Self { + symtable, + locals: snapshot.locals, + count_temps: snapshot.count_temps, + active_temps: snapshot.active_temps, + } + } + /// Obtains mutable access to the parent global symtable. pub(crate) fn global(&mut self) -> &mut GlobalSymtable { self.symtable @@ -347,6 +386,13 @@ impl<'a> LocalSymtable<'a> { self.symtable.contains_global(key) } + /// Iterates over all global variables, yielding `(key, prototype, register_index)` tuples. + pub(crate) fn iter_globals( + &self, + ) -> impl Iterator + '_ { + self.symtable.iter_globals().map(|(k, v, i)| (k.clone(), v, i)) + } + /// Changes the type of an existing local variable `vref` to `new_etype`. /// /// This is used for type inference on first assignment. diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index e31a4a08..0fb8c280 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -39,6 +39,8 @@ use std::io; use std::iter::Iterator; use std::rc::Rc; +use super::syms::LocalSymtableSnapshot; + /// Kind of a user-defined callable. #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum CallableKind { @@ -53,7 +55,7 @@ enum CallableKind { /// /// This type exists to minimize the number of complex arguments passed across functions. /// If possible, avoid passing it and instead pass the minimum set of required fields. -#[derive(Default)] +#[derive(Clone, Default)] pub(super) struct Context { /// The code generator accumulating bytecode instructions. codegen: Codegen, @@ -1219,6 +1221,8 @@ pub(super) fn prepare_globals( // We use a short-lived `ENTER/LEAVE` scope to borrow local registers for the dimension // temporaries without permanently consuming global register slots. if array_globals.is_empty() { + // `compile` starts by popping an EOF, so make sure there is one. + ctx.codegen.emit(bytecode::make_eof(), preamble_pos); return Ok(()); } for (reg, def) in array_globals { @@ -1241,19 +1245,21 @@ pub(super) fn prepare_globals( ctx.codegen.emit(bytecode::make_alloc_array(reg, packed, first_dim_reg), preamble_pos); } + // `compile` starts by popping an EOF, so make sure there is one. + ctx.codegen.emit(bytecode::make_eof(), preamble_pos); Ok(()) } /// Compiles the `input` into an `Image` that can be executed by the VM. pub fn compile( input: &mut dyn io::Read, - mut ctx: Context, - mut symtable: GlobalSymtable, -) -> Result { + ctx: &mut Context, + mut symtable: LocalSymtable, +) -> Result<(Image, LocalSymtableSnapshot)> { + ctx.codegen.pop_eof(); { - let mut symtable = symtable.enter_scope(); for stmt in parser::parse(input) { - compile_stmt(&mut ctx, &mut symtable, stmt?)?; + compile_stmt(ctx, &mut symtable, stmt?)?; } } ctx.codegen.emit(bytecode::make_eof(), LineCol { line: 0, col: 0 }); @@ -1268,7 +1274,8 @@ pub fn compile( (key.clone(), GlobalVarInfo { reg, subtype, ndims }) }) .collect(); - ctx.codegen.build_image(global_vars, ctx.data) + let image = ctx.codegen.build_image(global_vars, ctx.data.clone())?; + Ok((image, symtable.save())) } #[cfg(test)] diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index f578eb8e..b2e154bc 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -58,6 +58,7 @@ one_test!(test_globals); one_test!(test_gosub); one_test!(test_goto); one_test!(test_if); +one_test!(test_incremental); one_test!(test_locals); one_test!(test_on_error); one_test!(test_out_of_registers); diff --git a/core2/tests/test_incremental.md b/core2/tests/test_incremental.md new file mode 100644 index 00000000..ca822e2e --- /dev/null +++ b/core2/tests/test_incremental.md @@ -0,0 +1,475 @@ +# Test: Smoke test + +## Source (partial) + +```basic +OUT 1 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=1% +``` + +## Source (partial) + +```basic +OUT 2 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R65, 2 ; 1:5 +0004: LOADI R64, 258 ; 1:5 +0005: UPCALL 0, R64 ; 1:1, OUT +0006: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=1% +0=2% +``` + +# Test: Program-scope variables persist across compiles + +## Source (partial) + +```basic +a = 3 +OUT a +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 258 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=3% +``` + +## Source (partial) + +```basic +OUT a +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R66, R64 ; 2:5 +0002: LOADI R65, 258 ; 2:5 +0003: UPCALL 0, R65 ; 2:1, OUT +0004: MOVE R66, R64 ; 1:5 +0005: LOADI R65, 258 ; 1:5 +0006: UPCALL 0, R65 ; 1:1, OUT +0007: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=3% +0=3% +``` + +# Test: Program-scope variables do not leak into functions + +## Source (partial) + +```basic +a = 3 +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 3 ; 1:5 +0001: EOF ; 0:0 +``` + +## Source (partial) + +```basic +FUNCTION get_a + OUT a +END FUNCTION +``` + +## Compiler errors (partial) + +```plain +2:9: Undefined global symbol a +``` + +## Source (partial) + +```basic +OUT a +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 3 ; 1:5 +0001: MOVE R66, R64 ; 1:5 +0002: LOADI R65, 258 ; 1:5 +0003: UPCALL 0, R65 ; 1:1, OUT +0004: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=3% +``` + +# Test: User-defined callables are available in later compiles + +## Source (partial) + +```basic +FUNCTION double_it(n AS INTEGER) + double_it = n * 2 +END FUNCTION + +SUB say_hello + OUT "hello" +END SUB +``` + +## Disassembly (full) + +```asm +0000: JUMP 6 ; 1:10 + +;; DOUBLE_IT (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: MOVE R64, R65 ; 2:17 +0003: LOADI R66, 2 ; 2:21 +0004: MULI R64, R64, R66 ; 2:19 +0005: RETURN ; 3:1 +;; DOUBLE_IT (END) + +0006: JUMP 11 ; 5:5 + +;; SAY_HELLO (BEGIN) +0007: LOADI R65, 0 ; 6:9 +0008: LOADI R64, 259 ; 6:9 +0009: UPCALL 0, R64 ; 6:5, OUT +0010: RETURN ; 7:1 +;; SAY_HELLO (END) + +0011: EOF ; 0:0 +``` + +## Source (partial) + +```basic +OUT double_it(4) +say_hello +``` + +## Disassembly (full) + +```asm +0000: JUMP 6 ; 1:10 + +;; DOUBLE_IT (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: MOVE R64, R65 ; 2:17 +0003: LOADI R66, 2 ; 2:21 +0004: MULI R64, R64, R66 ; 2:19 +0005: RETURN ; 3:1 +;; DOUBLE_IT (END) + +0006: JUMP 11 ; 5:5 + +;; SAY_HELLO (BEGIN) +0007: LOADI R65, 0 ; 6:9 +0008: LOADI R64, 259 ; 6:9 +0009: UPCALL 0, R64 ; 6:5, OUT +0010: RETURN ; 7:1 +;; SAY_HELLO (END) + +0011: LOADI R67, 4 ; 1:15 +0012: CALL R66, 1 ; 1:5, DOUBLE_IT +0013: MOVE R65, R66 ; 1:5 +0014: LOADI R64, 258 ; 1:5 +0015: UPCALL 0, R64 ; 1:1, OUT +0016: CALL R64, 7 ; 2:1, SAY_HELLO +0017: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=8% +0=hello$ +``` + +# Test: DATA accumulates across later compiles + +## Source (partial) + +```basic +DATA 1 +``` + +## Disassembly (full) + +```asm +0000: EOF ; 0:0 +``` + +## Source (partial) + +```basic +DATA 2 +GETDATA +``` + +## Disassembly (full) + +```asm +0000: UPCALL 0, R64 ; 2:1, GETDATA +0001: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=1% 1=2% +``` + +## Source (partial) + +```basic +DATA 3 +GETDATA +``` + +## Disassembly (full) + +```asm +0000: UPCALL 0, R64 ; 2:1, GETDATA +0001: UPCALL 0, R64 ; 2:1, GETDATA +0002: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=1% 1=2% 2=3% +0=1% 1=2% 2=3% +``` + +# Test: Failed compile does not define ghost program variables + +## Source (partial) + +```basic +a = 5 +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 5 ; 1:5 +0001: EOF ; 0:0 +``` + +## Source (partial) + +```basic +c = b + 3 +``` + +## Compiler errors (partial) + +```plain +1:5: Undefined global symbol b +``` + +## Source (partial) + +```basic +OUT a, c +``` + +## Compiler errors (partial) + +```plain +1:8: Undefined global symbol c +``` + +## Source (partial) + +```basic +OUT a +``` + +## Disassembly (full) + +```asm +0000: LOADI R64, 5 ; 1:5 +0001: MOVE R66, R64 ; 1:5 +0002: LOADI R65, 258 ; 1:5 +0003: UPCALL 0, R65 ; 1:1, OUT +0004: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=5% +``` + +# Test: Failed compile does not define ghost callables + +## Source (partial) + +```basic +FUNCTION stable + stable = 7 +END FUNCTION +``` + +## Disassembly (full) + +```asm +0000: JUMP 4 ; 1:10 + +;; STABLE (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 7 ; 2:14 +0003: RETURN ; 3:1 +;; STABLE (END) + +0004: EOF ; 0:0 +``` + +## Source (partial) + +```basic +FUNCTION broken + broken = missing + 1 +END FUNCTION +``` + +## Compiler errors (partial) + +```plain +2:14: Undefined global symbol missing +``` + +## Source (partial) + +```basic +OUT stable, broken +``` + +## Compiler errors (partial) + +```plain +1:13: Undefined global symbol broken +``` + +## Source (partial) + +```basic +OUT stable +``` + +## Disassembly (full) + +```asm +0000: JUMP 4 ; 1:10 + +;; STABLE (BEGIN) +0001: LOADI R64, 0 ; 1:10 +0002: LOADI R64, 7 ; 2:14 +0003: RETURN ; 3:1 +;; STABLE (END) + +0004: CALL R65, 1 ; 1:5, STABLE +0005: LOADI R64, 258 ; 1:5 +0006: UPCALL 0, R64 ; 1:1, OUT +0007: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=7% +``` + +# Test: Failed DATA chunk does not taint prior state + +## Source (partial) + +```basic +DATA 9 +``` + +## Disassembly (full) + +```asm +0000: EOF ; 0:0 +``` + +## Source (partial) + +```basic +DATA 1 + 2 +``` + +## Compiler errors (partial) + +```plain +1:8: Expected comma after datum but found + +``` + +## Source (partial) + +```basic +DATA 10 +GETDATA +``` + +## Disassembly (full) + +```asm +0000: UPCALL 0, R64 ; 2:1, GETDATA +0001: EOF ; 0:0 +``` + +## Output (full) + +```plain +0=9% 1=10% +``` diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index 5a9fcac2..d5e409e8 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -50,61 +50,91 @@ pub(super) fn src_path(name: &str) -> PathBuf { dir.join(name) } +/// A parsed test case from a golden data file. +#[derive(Debug, Eq, PartialEq)] +struct Test { + name: String, + sources: Vec, +} + /// A type describing the golden data of various tests in a file. -/// -/// The first string is the test's name and the second is the input source code. -type Tests = Vec<(String, String)>; +type Tests = Vec; + +/// Returns true if the `line` corresponds to a source section. +fn is_source_header(line: &str) -> bool { + line == "## Source" || line == "## Source (partial)" +} /// Reads the source sections of a golden test description file. fn read_sources(path: &Path) -> io::Result { let file = File::open(path).expect("Failed to open golden data file"); let reader = BufReader::new(file); - fn add_test(tests: &mut Tests, name: String, source: Option) -> io::Result<()> { - match source { - Some(source) => { - tests.push((name, source.trim_end().to_owned())); - Ok(()) - } - None => Err(io::Error::new( + fn add_test(tests: &mut Tests, name: String, sources: Vec) -> io::Result<()> { + if sources.is_empty() { + Err(io::Error::new( io::ErrorKind::InvalidData, format!("Test case '{}' has no Source section", name), - )), + )) + } else { + tests.push(Test { name, sources }); + Ok(()) } } + fn finish_source(sources: &mut Vec, source: &mut Option) { + if let Some(source) = source.take() { + sources.push(source.trim_end().to_owned()); + } + } + + #[derive(Clone, Copy, Eq, PartialEq)] + enum Section { + Other, + Source, + } + let mut tests = vec![]; let mut current_test = None; + let mut current_section = Section::Other; + let mut sources = vec![]; let mut source: Option = None; for line in reader.lines() { let line = line?; if let Some(stripped) = line.strip_prefix("# Test: ") { + finish_source(&mut sources, &mut source); if let Some(name) = current_test.take() { - add_test(&mut tests, name, source.take())?; + add_test(&mut tests, name, std::mem::take(&mut sources))?; } current_test = Some(stripped.to_owned()); - source = None; + current_section = Section::Other; continue; } else if line.starts_with("# ") { return Err(io::Error::new( io::ErrorKind::InvalidData, format!("Unexpected section header {}", line), )); + } else if is_source_header(&line) { + current_section = Section::Source; + continue; + } else if line.starts_with("## ") { + finish_source(&mut sources, &mut source); + current_section = Section::Other; + continue; } else if line == "```basic" { - if current_test.is_none() { - return Err(io::Error::new( - io::ErrorKind::InvalidData, - "Source section without test header", - )); + if current_section == Section::Source { + if current_test.is_none() { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + "Source section without test header", + )); + } + source = Some(String::new()); } - source = Some(String::new()); continue; } else if line == "```" { - if let Some(name) = current_test.take() { - add_test(&mut tests, name, source.take())?; - } - source = None; + finish_source(&mut sources, &mut source); continue; } @@ -114,8 +144,9 @@ fn read_sources(path: &Path) -> io::Result { } } + finish_source(&mut sources, &mut source); if let Some(name) = current_test { - add_test(&mut tests, name, source.take())?; + add_test(&mut tests, name, std::mem::take(&mut sources))?; } if tests.is_empty() { @@ -154,7 +185,7 @@ foo bar file.flush()?; assert_eq!( - [("first".to_owned(), "First line\n\nSecond line".to_owned())], + [Test { name: "first".to_owned(), sources: vec!["First line\n\nSecond line".to_owned()] }], read_sources(file.path())?.as_slice() ); @@ -196,8 +227,11 @@ The line assert_eq!( [ - ("first".to_owned(), "First line\n\nSecond line".to_owned()), - ("second".to_owned(), "The line".to_owned()), + Test { + name: "first".to_owned(), + sources: vec!["First line\n\nSecond line".to_owned()], + }, + Test { name: "second".to_owned(), sources: vec!["The line".to_owned()] }, ], read_sources(file.path())?.as_slice() ); @@ -205,6 +239,81 @@ The line Ok(()) } +#[test] +fn test_read_sources_many_sources_per_test() -> io::Result<()> { + let mut file = NamedTempFile::new()?; + write!( + file, + "junk +# Test: first + +## Source (partial) + +```basic +First line +``` + +## Output + +```plain +ignored +``` + +## Source (partial) + +```basic +Second line + +Third line +``` +" + )?; + file.flush()?; + + assert_eq!( + [Test { + name: "first".to_owned(), + sources: vec!["First line".to_owned(), "Second line\n\nThird line".to_owned()], + }], + read_sources(file.path())?.as_slice() + ); + + Ok(()) +} + +/// Collection of section markers for a golden file. +struct Labels { + source: &'static str, + disassembly: &'static str, + compiler_errors: &'static str, + exit_code: &'static str, + output: &'static str, + runtime_errors: &'static str, +} + +/// Obtains the section markers to use when writing out the data of `test`. +fn labels_for(test: &Test) -> Labels { + if test.sources.len() > 1 { + Labels { + source: "## Source (partial)", + disassembly: "## Disassembly (full)", + compiler_errors: "## Compiler errors (partial)", + exit_code: "## Exit code (full)", + output: "## Output (full)", + runtime_errors: "## Runtime errors (full)", + } + } else { + Labels { + source: "## Source", + disassembly: "## Disassembly", + compiler_errors: "## Compilation errors", + exit_code: "## Exit code", + output: "## Output", + runtime_errors: "## Runtime errors", + } + } +} + /// Generates a textual diff of `golden` and `generated`. The output is meant to be useful for /// human consumption when a test fails and is not guaranteed to be in patch format. /// @@ -296,6 +405,23 @@ fn test_diff_different() -> io::Result<()> { Ok(()) } +/// Executes the image already loaded in `vm` through completion, and converts the result +/// into an exit code. +async fn run_image(vm: &mut Vm) -> Result { + loop { + match vm.exec() { + StopReason::End(code) => return Ok(code.to_i32()), + StopReason::Eof => return Ok(0), + StopReason::Upcall(handle) => { + if let Err(e) = handle.invoke().await { + return Err(e.to_string()); + } + } + StopReason::Exception(pos, e) => return Err(format!("{}: {}", pos, e)), + } + } +} + /// Given a `golden` test definition, executes its source part and writes the corresponding /// `generated` file. The test is expected to pass when both match, but the caller is responsible /// for checking this condition. @@ -304,89 +430,72 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let tests = read_sources(golden)?; let mut first = true; - for (name, source) in tests { + for test in tests { if !first { write!(generated, "\n")?; } - write!(generated, "# Test: {}\n\n", name)?; + write!(generated, "# Test: {}\n", test.name)?; first = false; - - write!(generated, "## Source\n\n")?; - write!(generated, "```basic\n")?; - if !source.is_empty() { - write!(generated, "{}\n", source)?; - } - write!(generated, "```\n")?; + let labels = labels_for(&test); let console = Rc::from(RefCell::from(String::new())); let mut upcalls_by_name: HashMap> = HashMap::default(); callables::register_all(&mut upcalls_by_name, console.clone()); - let image = { - let compiler = Compiler::new(&upcalls_by_name, &[]).expect("Cannot fail"); - compiler.compile(&mut source.as_bytes()) - }; - - let image = match image { - Ok(image) => image, - Err(e) => { - write!(generated, "\n## Compilation errors\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", e)?; - write!(generated, "```\n")?; - continue; + let mut compiler = Compiler::new(&upcalls_by_name, &[]).expect("Cannot fail"); + + for source in test.sources { + write!(generated, "\n{}\n\n", labels.source)?; + write!(generated, "```basic\n")?; + if !source.is_empty() { + write!(generated, "{}\n", source)?; } - }; + write!(generated, "```\n")?; - write!(generated, "\n## Disassembly\n\n")?; - write!(generated, "```asm\n")?; - for line in image.disasm() { - write!(generated, "{}\n", line)?; - } - write!(generated, "```\n")?; - - let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - let mut stop: Option> = None; - while stop.is_none() { - match vm.exec() { - StopReason::End(code) => stop = Some(Ok(code.to_i32())), - StopReason::Eof => stop = Some(Ok(0)), - StopReason::Upcall(handle) => { - if let Err(e) = handle.invoke().await { - stop = Some(Err(e.to_string())); - } - } - StopReason::Exception(pos, e) => { - stop = Some(Err(format!("{}: {}", pos, e))); + let image = match compiler.compile_more(&mut source.as_bytes()) { + Ok(image) => image, + Err(e) => { + write!(generated, "\n{}\n\n", labels.compiler_errors)?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + continue; } - } - } + }; - match stop.expect("The loop can only exit when this is set") { - Ok(0) => { - // Keep quiet in the common case. + write!(generated, "\n{}\n\n", labels.disassembly)?; + write!(generated, "```asm\n")?; + for line in image.disasm() { + write!(generated, "{}\n", line)?; } - Ok(i) => { - write!(generated, "\n## Exit code\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", i)?; - write!(generated, "```\n")?; + write!(generated, "```\n")?; + + console.borrow_mut().clear(); + let mut vm = Vm::new(upcalls_by_name.clone()); + vm.load(image); + match run_image(&mut vm).await { + Ok(0) => (), + Ok(i) => { + write!(generated, "\n{}\n\n", labels.exit_code)?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", i)?; + write!(generated, "```\n")?; + } + Err(e) => { + write!(generated, "\n{}\n\n", labels.runtime_errors)?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + } } - Err(e) => { - write!(generated, "\n## Runtime errors\n\n")?; + + let console = console.borrow(); + if !console.is_empty() { + write!(generated, "\n{}\n\n", labels.output)?; write!(generated, "```plain\n")?; - write!(generated, "{}\n", e)?; + write!(generated, "{}", console)?; write!(generated, "```\n")?; } } - - let console = console.borrow(); - if !console.is_empty() { - write!(generated, "\n## Output\n\n")?; - write!(generated, "```plain\n")?; - write!(generated, "{}", console)?; - write!(generated, "```\n")?; - } } Ok(()) From af50487d3d0ec68cbc9c0bf7d89310b4a7f655b4 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Thu, 26 Mar 2026 20:24:53 -0700 Subject: [PATCH 45/53] core2: Append images incrementally Make the compiler append new bytecode into a caller-owned Image and run the VM against that image directly. This avoids rebuilding and reloading the full image on every incremental compilation and updates the incremental tests for the new single-VM behavior. --- core2/examples/config.rs | 23 ++- core2/src/compiler/codegen.rs | 63 ++++++-- core2/src/compiler/ids.rs | 7 + core2/src/compiler/mod.rs | 13 +- core2/src/compiler/top.rs | 14 +- core2/src/image.rs | 46 +++++- core2/src/lib.rs | 1 + core2/src/vm/mod.rs | 234 ++++++++++++++++++----------- core2/tests/test_incremental.md | 255 ++++++++++++++++++++++++++++++-- core2/tests/testutils/mod.rs | 36 ++--- 10 files changed, 536 insertions(+), 156 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 24ef9faf..3c155688 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -79,10 +79,9 @@ fn main() { let compiler = Compiler::new(&upcalls, &global_defs).expect("Globals initialization failed"); let image = compiler.compile(&mut SCRIPT.as_bytes()).expect("Compilation failed"); - // Load and execute the compiled image. + // Execute the compiled image. let mut vm = Vm::new(upcalls); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { StopReason::End(code) => { if !code.is_success() { eprintln!("Script exited with code {}", code.to_i32()); @@ -100,19 +99,19 @@ fn main() { } // Query the global variables by name. - match vm.get_global("foo_value") { + match vm.get_global(&image, "foo_value") { Ok(Some(ConstantDatum::Integer(v))) => println!("foo_value% = {}", v), Ok(Some(other)) => println!("foo_value% has unexpected type: {:?}", other), Ok(None) => println!("foo_value% is not set"), Err(e) => println!("foo_value%: error: {}", e), } - match vm.get_global("result_total") { + match vm.get_global(&image, "result_total") { Ok(Some(ConstantDatum::Integer(v))) => println!("result_total% = {}", v), Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), Ok(None) => println!("result_total% is not set"), Err(e) => println!("result_total%: error: {}", e), } - match vm.get_global("status") { + match vm.get_global(&image, "status") { Ok(Some(ConstantDatum::Text(v))) => println!("status$ = {:?}", v), Ok(Some(other)) => println!("status$ has unexpected type: {:?}", other), Ok(None) => println!("status$ is not set"), @@ -120,7 +119,7 @@ fn main() { } // defined_within was not provided upfront but was declared as a global within // the script. We can query it here too. - match vm.get_global("defined_within") { + match vm.get_global(&image, "defined_within") { Ok(Some(ConstantDatum::Integer(v))) => println!("defined_within% = {}", v), Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), Ok(None) => println!("defined_within% is not set"), @@ -128,7 +127,7 @@ fn main() { } // optional_flag was declared but the script never assigned it, so it should // receive its "zero value". - match vm.get_global("optional_flag") { + match vm.get_global(&image, "optional_flag") { Ok(Some(ConstantDatum::Boolean(v))) => println!("optional_flag? = {}", v), Ok(Some(other)) => println!("optional_flag? has unexpected type: {:?}", other), Ok(None) => println!("optional_flag? is not declared"), @@ -136,20 +135,20 @@ fn main() { } // injected_value was pre-initialized to 5 before compilation. The script incremented // it by 1, so we expect 6 here. - match vm.get_global("injected_value") { + match vm.get_global(&image, "injected_value") { Ok(Some(ConstantDatum::Integer(v))) => println!("injected_value% = {}", v), Ok(Some(other)) => println!("injected_value% has unexpected type: {:?}", other), Ok(None) => println!("injected_value% is not set"), Err(e) => println!("injected_value%: error: {}", e), } // "unknown" was never declared at all, so get_global returns Ok(None). - match vm.get_global("unknown") { + match vm.get_global(&image, "unknown") { Ok(Some(v)) => println!("unknown = {:?}", v), Ok(None) => println!("unknown is not declared"), Err(e) => println!("unknown: error: {}", e), } for i in 0..3_i32 { - match vm.get_global_array("results", &[i]) { + match vm.get_global_array(&image, "results", &[i]) { Ok(Some(ConstantDatum::Integer(v))) => println!("results%({}) = {}", i, v), Ok(Some(other)) => println!("results%({}) has unexpected type: {:?}", i, other), Ok(None) => println!("results%({}) is not set", i), @@ -157,7 +156,7 @@ fn main() { } } // Demonstrate that querying a scalar as an array yields an error. - match vm.get_global_array("foo_value", &[0]) { + match vm.get_global_array(&image, "foo_value", &[0]) { Ok(v) => println!("foo_value%(0) = {:?}", v), Err(e) => println!("foo_value%(0): error: {}", e), } diff --git a/core2/src/compiler/codegen.rs b/core2/src/compiler/codegen.rs index a9987c11..0fbf8b18 100644 --- a/core2/src/compiler/codegen.rs +++ b/core2/src/compiler/codegen.rs @@ -19,7 +19,7 @@ use crate::ast::ExprType; use crate::bytecode::{self, ErrorHandlerMode, Register}; use crate::compiler::ids::HashMapWithIds; use crate::compiler::{Error, Result, SymbolKey}; -use crate::image::{DebugInfo, GlobalVarInfo, Image, InstrMetadata}; +use crate::image::{GlobalVarInfo, Image, ImageDelta, InstrMetadata}; use crate::mem::ConstantDatum; use crate::reader::LineCol; use std::collections::HashMap; @@ -253,14 +253,38 @@ impl Codegen { } } - /// Consumes the code generator and builds a ready-to-use `Image`. - pub(super) fn build_image( + /// Builds an incremental update to append into `image`. + pub(super) fn build_image_delta( &mut self, + image: &Image, global_vars: HashMap, - data: Vec>, - ) -> Result { + data: &[Option], + ) -> Result { self.apply_fixups()?; + let code_start = image.code.len().saturating_sub(1); + let constants_start = image.constants.len(); + let instrs_start = image.debug_info.instrs.len().saturating_sub(1); + let upcalls_start = image.upcalls.len(); + + debug_assert_eq!(code_start, instrs_start); + debug_assert!(code_start <= self.code.len()); + debug_assert!(constants_start <= self.constants.len()); + debug_assert!(image.data.len() <= data.len()); + debug_assert!(upcalls_start <= self.upcalls.len()); + + debug_assert_eq!(&image.code[..code_start], &self.code[..code_start]); + debug_assert_eq!(&image.debug_info.instrs[..instrs_start], &self.instrs[..instrs_start],); + debug_assert!( + self.constants.keys_to_vec().starts_with(image.constants.as_slice()), + "Image constants must match the compiler state prefix", + ); + debug_assert!( + self.upcalls.keys_to_vec().starts_with(image.upcalls.as_slice()), + "Image upcalls must match the compiler state prefix", + ); + debug_assert_eq!(image.data.as_slice(), &data[..image.data.len()]); + let mut callables = HashMap::default(); for (key, (start_pc, end_pc)) in &self.user_callables_addresses { let previous = callables.insert(*start_pc, (key.clone(), true)); @@ -270,12 +294,27 @@ impl Codegen { debug_assert!(previous.is_none(), "An address can only start one callable"); } - Ok(Image::new( - self.code.clone(), - self.upcalls.keys_to_vec(), - self.constants.keys_to_vec(), - data, - DebugInfo { instrs: self.instrs.clone(), callables, global_vars }, - )) + Ok(ImageDelta { + code: self.code[code_start..].to_vec(), + upcalls: self.upcalls.keys_to_vec_from(upcalls_start), + constants: self.constants.keys_to_vec_from(constants_start), + data: data[image.data.len()..].to_vec(), + instrs: self.instrs[instrs_start..].to_vec(), + callables, + global_vars, + }) + } + + #[cfg(test)] + /// Builds a ready-to-use `Image`. + pub(super) fn build_image( + &mut self, + global_vars: HashMap, + data: Vec>, + ) -> Result { + let mut image = Image::default(); + let delta = self.build_image_delta(&image, global_vars, &data)?; + image.append(delta); + Ok(image) } } diff --git a/core2/src/compiler/ids.rs b/core2/src/compiler/ids.rs index 75e70667..02454ca0 100644 --- a/core2/src/compiler/ids.rs +++ b/core2/src/compiler/ids.rs @@ -95,6 +95,11 @@ where reverse.sort_by_key(|(_key, (_value, index))| *index); reverse.into_iter().map(|(key, _index)| key.clone()).collect() } + + /// Returns the keys with identifiers greater than or equal to `start`, in insertion order. + pub(super) fn keys_to_vec_from(&self, start: usize) -> Vec { + self.keys_to_vec().into_iter().skip(start).collect() + } } #[cfg(test)] @@ -136,6 +141,7 @@ mod tests { assert_eq!(Some((Some(()), 1)), map.insert("bar", ())); assert_eq!(["foo", "bar", "baz"], map.keys_to_vec().as_slice()); + assert_eq!(["bar", "baz"], map.keys_to_vec_from(1).as_slice()); } #[test] @@ -149,6 +155,7 @@ mod tests { assert_eq!(Some((Some(()), 1)), map.insert("bar", ())); assert_eq!(["foo", "bar", "baz"], map.keys_to_vec().as_slice()); + assert_eq!(["baz"], map.keys_to_vec_from(2).as_slice()); } #[test] diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index 5d9d29f7..e62ed802 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -212,20 +212,21 @@ impl Compiler { /// Compiles a chunk of code. pub fn compile(mut self, input: &mut dyn io::Read) -> Result { - let symtable = LocalSymtable::restore(&mut self.symtable, self.program_scope); - let (image, _) = top::compile(input, &mut self.context, symtable)?; + let mut image = Image::default(); + self.compile_more(&mut image, input)?; Ok(image) } - /// Compiles a chunk of code. - pub fn compile_more(&mut self, input: &mut dyn io::Read) -> Result { + /// Compiles a chunk of code and appends it to `image`. + pub fn compile_more(&mut self, image: &mut Image, input: &mut dyn io::Read) -> Result<()> { let mut new_context = self.context.clone(); let mut new_symtable = self.symtable.clone(); let program_scope = LocalSymtable::restore(&mut new_symtable, self.program_scope.clone()); - let (image, snapshot) = top::compile(input, &mut new_context, program_scope)?; + let (delta, snapshot) = top::compile(input, image, &mut new_context, program_scope)?; + image.append(delta); self.context = new_context; self.symtable = new_symtable; self.program_scope = snapshot; - Ok(image) + Ok(()) } } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 0fb8c280..74168262 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -28,7 +28,7 @@ use crate::compiler::syms::{ self, GlobalSymtable, LocalSymtable, SymbolKey, SymbolPrototype, TempSymtable, }; use crate::compiler::{Error, Result}; -use crate::image::{GlobalVarInfo, Image}; +use crate::image::{GlobalVarInfo, Image, ImageDelta}; use crate::mem::ConstantDatum; use crate::reader::LineCol; use crate::{Callable, CallableMetadataBuilder, parser}; @@ -1253,9 +1253,10 @@ pub(super) fn prepare_globals( /// Compiles the `input` into an `Image` that can be executed by the VM. pub fn compile( input: &mut dyn io::Read, + image: &Image, ctx: &mut Context, mut symtable: LocalSymtable, -) -> Result<(Image, LocalSymtableSnapshot)> { +) -> Result<(ImageDelta, LocalSymtableSnapshot)> { ctx.codegen.pop_eof(); { for stmt in parser::parse(input) { @@ -1274,8 +1275,8 @@ pub fn compile( (key.clone(), GlobalVarInfo { reg, subtype, ndims }) }) .collect(); - let image = ctx.codegen.build_image(global_vars, ctx.data.clone())?; - Ok((image, symtable.save())) + let delta = ctx.codegen.build_image_delta(image, global_vars, &ctx.data)?; + Ok((delta, symtable.save())) } #[cfg(test)] @@ -1291,15 +1292,14 @@ mod tests { .expect("constants initialization must succeed"); let image = compiler.compile(&mut "".as_bytes()).expect("compilation should succeed"); let mut vm = Vm::new(HashMap::default()); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { StopReason::End(code) if code.is_success() => {} StopReason::End(code) => panic!("unexpected exit code: {}", code.to_i32()), StopReason::Eof => {} StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"), StopReason::Upcall(_) => panic!("unexpected upcall"), } - vm.get_global(name).expect("get_global failed").expect("global not found") + vm.get_global(&image, name).expect("get_global failed").expect("global not found") } #[test] diff --git a/core2/src/image.rs b/core2/src/image.rs index 355c5313..e7f2f29f 100644 --- a/core2/src/image.rs +++ b/core2/src/image.rs @@ -92,6 +92,7 @@ pub(crate) fn format_instr(instr: u32) -> String { } /// Information about a global variable tracked for post-execution querying. +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct GlobalVarInfo { /// Global register index (0 to `Register::MAX_GLOBAL - 1`). pub(crate) reg: u8, @@ -104,7 +105,7 @@ pub(crate) struct GlobalVarInfo { } /// Per-instruction metadata stored in `DebugInfo`. -#[derive(Clone)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct InstrMetadata { /// Source location that generated this instruction. pub(crate) linecol: LineCol, @@ -136,6 +137,30 @@ pub struct DebugInfo { pub(crate) global_vars: HashMap, } +/// Incremental update to append into an existing `Image`. +pub(crate) struct ImageDelta { + /// Suffix of bytecode instructions to append after dropping the current EOF terminator. + pub(crate) code: Vec, + + /// Additional upcall names referenced by the updated program. + pub(crate) upcalls: Vec, + + /// Additional constants referenced by the updated program. + pub(crate) constants: Vec, + + /// Additional `DATA` values captured by the updated program. + pub(crate) data: Vec>, + + /// Per-instruction metadata matching `code`. + pub(crate) instrs: Vec, + + /// Full user-callable metadata for the updated program. + pub(crate) callables: HashMap, + + /// Full global variable metadata for the updated program. + pub(crate) global_vars: HashMap, +} + /// Representation of a compiled EndBASIC program. /// /// Images always have at least one instruction so that the VM can make this assumption. @@ -196,6 +221,25 @@ impl Image { Self { code, upcalls, constants, data, debug_info, _internal: () } } + /// Appends `delta` to the image, replacing the current trailing EOF terminator. + pub(crate) fn append(&mut self, delta: ImageDelta) { + debug_assert_eq!(self.code.last().copied(), Some(bytecode::make_eof())); + debug_assert_eq!(self.debug_info.instrs.len(), self.code.len()); + debug_assert_eq!(delta.code.len(), delta.instrs.len()); + debug_assert_eq!(delta.code.last().copied(), Some(bytecode::make_eof())); + + self.code.pop(); + self.debug_info.instrs.pop(); + + self.code.extend(delta.code); + self.upcalls.extend(delta.upcalls); + self.constants.extend(delta.constants); + self.data.extend(delta.data); + self.debug_info.instrs.extend(delta.instrs); + self.debug_info.callables = delta.callables; + self.debug_info.global_vars = delta.global_vars; + } + /// Disassembles the image into a textual representation for debugging. pub fn disasm(&self) -> Vec { let mut lines = Vec::with_capacity(self.code.len()); diff --git a/core2/src/lib.rs b/core2/src/lib.rs index a4ac8d56..aac6da06 100644 --- a/core2/src/lib.rs +++ b/core2/src/lib.rs @@ -31,6 +31,7 @@ pub use ast::{ArgSep, ExprType}; pub use bytecode::{ExitCode, InvalidExitCodeError, VarArgTag}; pub use callable::*; pub use compiler::{Compiler, GlobalDef, GlobalDefKind, SymbolKey, only_metadata}; +pub use image::Image; pub use mem::ConstantDatum; pub use vm::{GetGlobalError, GetGlobalResult, StopReason, Vm}; diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index bb49171d..90f55926 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -52,24 +52,26 @@ pub enum GetGlobalError { pub type GetGlobalResult = Result; /// Opaque handle to invoke a pending upcall. -pub struct UpcallHandler<'a>(&'a mut Vm); +pub struct UpcallHandler<'a> { + vm: &'a mut Vm, + image: &'a Image, +} impl<'a> UpcallHandler<'a> { /// Invokes the pending upcall. pub async fn invoke(self) -> CallResult<()> { - let vm = self.0; + let vm = self.vm; + let image = self.image; let (index, first_reg, upcall_pc) = vm .pending_upcall .take() .expect("This is only reachable when the VM has a pending upcall"); let upcall = vm.upcalls[usize::from(index)].clone(); - match upcall.exec(vm.upcall_scope(first_reg, upcall_pc)).await { + match upcall.exec(vm.upcall_scope(image, first_reg, upcall_pc)).await { Ok(()) => Ok(()), Err(e) => { - let pos_override = vm.image.as_ref().and_then(|image| { - image.debug_info.instrs[upcall_pc].arg_linecols.first().copied() - }); - vm.handle_exception(upcall_pc, e.to_string(), pos_override); + let pos_override = image.debug_info.instrs[upcall_pc].arg_linecols.first().copied(); + vm.handle_exception(image, upcall_pc, e.to_string(), pos_override); Ok(()) } } @@ -96,10 +98,10 @@ pub struct Vm { /// Mapping of all available upcall names to their handlers. upcalls_by_name: HashMap>, - /// Active image for execution. - image: Option, + /// Upcall names already resolved into `upcalls`. + upcall_names: Vec, - /// Upcalls used by the loaded image in index order. + /// Upcalls used by the current image in index order. upcalls: Vec>, /// Heap memory for dynamic allocations. @@ -126,7 +128,7 @@ impl Vm { pub fn new(upcalls_by_name: HashMap>) -> Self { Self { upcalls_by_name, - image: None, + upcall_names: vec![], upcalls: vec![], heap: vec![], context: Context::default(), @@ -136,68 +138,75 @@ impl Vm { } } - /// Loads an `image` into the VM for execution, resetting any previous execution state. - pub fn load(&mut self, image: Image) { + /// Resets any existing execution state. + pub fn reset(&mut self) { + self.upcall_names.clear(); self.upcalls.clear(); - for key in &image.upcalls { + self.heap.clear(); + self.context = Context::default(); + self.last_error = None; + self.pending_exception = None; + self.pending_upcall = None; + } + + /// Synchronizes cached upcall handlers with the externally-owned `image`. + fn sync_upcalls(&mut self, image: &Image) { + debug_assert!( + image.upcalls.starts_with(self.upcall_names.as_slice()), + "Vm::reset() is required before executing a different image", + ); + + for key in &image.upcalls[self.upcalls.len()..] { self.upcalls.push( self.upcalls_by_name .get(key) .expect("All upcalls exposed during compilation must be present at runtime") .clone(), ); + self.upcall_names.push(key.clone()); } + } - self.image = Some(image); - - self.heap.clear(); - self.context = Context::default(); - self.last_error = None; - self.pending_exception = None; - self.pending_upcall = None; + /// Parks execution at the current EOF instruction so later appended code can resume. + fn park_at_eof(&mut self, image: &Image) { + debug_assert!(!image.code.is_empty()); + self.context.set_pc(image.code.len() - 1); } /// Constructs a `Scope` for an upcall with arguments starting at `reg`. /// /// `upcall_pc` is the address of the UPCALL instruction in the image, used to look up /// per-argument source locations from `DebugInfo`. - fn upcall_scope<'a>(&'a mut self, reg: Register, upcall_pc: usize) -> Scope<'a> { - let (constants, arg_linecols, data) = match self.image.as_ref() { - Some(image) => ( - image.constants.as_slice(), - image - .debug_info - .instrs - .get(upcall_pc) - .map(|m| m.arg_linecols.as_slice()) - .unwrap_or(&[]), - image.data.as_slice(), - ), - None => (&[][..], &[][..], &[][..]), - }; + fn upcall_scope<'a>( + &'a mut self, + image: &'a Image, + reg: Register, + upcall_pc: usize, + ) -> Scope<'a> { + let arg_linecols = image + .debug_info + .instrs + .get(upcall_pc) + .map(|m| m.arg_linecols.as_slice()) + .unwrap_or(&[]); self.context.upcall_scope( reg, - constants, + image.constants.as_slice(), &mut self.heap, arg_linecols, &self.last_error, - data, + image.data.as_slice(), ) } /// Handles an exception raised at `pc` with `message`. Returns true if the error was handled. fn handle_exception( &mut self, + image: &Image, pc: usize, message: String, pos_override: Option, ) -> bool { - let Some(image) = self.image.as_ref() else { - let pos = pos_override.unwrap_or(LineCol { line: 0, col: 0 }); - self.pending_exception = Some((pos, message)); - return false; - }; - let pos = pos_override.unwrap_or(image.debug_info.instrs[pc].linecol); self.last_error = Some(format!("{}: {}", pos, message)); self.pending_exception = None; @@ -230,11 +239,8 @@ impl Vm { /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the /// variable was not declared). Returns `Err` if the variable exists but is an /// array; in that case, use `get_global_array` instead. - pub fn get_global(&self, name: &str) -> GetGlobalResult> { + pub fn get_global(&self, image: &Image, name: &str) -> GetGlobalResult> { let key = SymbolKey::from(name); - let Some(image) = self.image.as_ref() else { - return Ok(None); - }; let Some(info) = image.debug_info.global_vars.get(&key) else { return Ok(None); }; @@ -262,13 +268,11 @@ impl Vm { /// (use `get_global` instead), or if the subscripts are out of bounds. pub fn get_global_array( &self, + image: &Image, name: &str, subscripts: &[i32], ) -> GetGlobalResult> { let key = SymbolKey::from(name); - let Some(image) = self.image.as_ref() else { - return Ok(None); - }; let Some(info) = image.debug_info.global_vars.get(&key) else { return Ok(None); }; @@ -295,37 +299,40 @@ impl Vm { Ok(Some(datum)) } - /// Starts or resumes execution of the loaded image. + /// Starts or resumes execution of `image`. /// /// Returns a `StopReason` indicating why execution stopped, which may be due to program /// termination, an exception, or a pending upcall that requires caller handling. - pub fn exec(&mut self) -> StopReason<'_> { + pub fn exec<'a>(&'a mut self, image: &'a Image) -> StopReason<'a> { + self.sync_upcalls(image); + loop { if let Some((pos, message)) = self.pending_exception.take() { + self.park_at_eof(image); return StopReason::Exception(pos, message); } - let Some(image) = self.image.as_ref() else { - return StopReason::Eof; - }; - if self.pending_upcall.is_some() { - return StopReason::Upcall(UpcallHandler(self)); + return StopReason::Upcall(UpcallHandler { vm: self, image }); } match self.context.exec(image, &mut self.heap) { - InternalStopReason::End(code) => return StopReason::End(code), + InternalStopReason::End(code) => { + self.park_at_eof(image); + return StopReason::End(code); + } InternalStopReason::Eof => return StopReason::Eof, InternalStopReason::Exception(pc, e) => { - if !self.handle_exception(pc, e, None) + if !self.handle_exception(image, pc, e, None) && let Some((pos, message)) = self.pending_exception.take() { + self.park_at_eof(image); return StopReason::Exception(pos, message); } } InternalStopReason::Upcall(index, first_reg, upcall_pc) => { self.pending_upcall = Some((index, first_reg, upcall_pc)); - return StopReason::Upcall(UpcallHandler(self)); + return StopReason::Upcall(UpcallHandler { vm: self, image }); } } } @@ -404,9 +411,9 @@ mod tests { } /// Runs the VM to completion, invoking every upcall as it is encountered. - async fn run_to_end(vm: &mut Vm) { + async fn run_to_end(vm: &mut Vm, image: &Image) { loop { - match vm.exec() { + match vm.exec(image) { StopReason::End(_) => break, StopReason::Eof => break, StopReason::Exception(_, msg) => panic!("Unexpected exception: {}", msg), @@ -418,7 +425,8 @@ mod tests { #[test] fn test_exec_without_load_is_eof() { let mut vm = Vm::new(HashMap::default()); - match vm.exec() { + let image = Image::default(); + match vm.exec(&image) { StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } @@ -427,8 +435,8 @@ mod tests { #[test] fn test_exec_empty_image_is_eof() { let mut vm = Vm::new(HashMap::default()); - vm.load(Image::default()); - match vm.exec() { + let image = Image::default(); + match vm.exec(&image) { StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } @@ -439,8 +447,7 @@ mod tests { let mut vm = Vm::new(HashMap::default()); let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); let image = compiler.compile(&mut b"".as_slice()).unwrap(); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { StopReason::Eof => (), _ => panic!("Unexpected stop reason"), } @@ -456,27 +463,26 @@ mod tests { let image = compiler.compile(&mut b"OUT 30: OUT 20".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { StopReason::Upcall(_handler) => (), _ => panic!("First exec should stop at the first upcall"), } assert!(data.borrow().is_empty()); - match vm.exec() { + match vm.exec(&image) { StopReason::Upcall(handler) => handler.invoke().await.unwrap(), _ => panic!("Second exec should stop at the same upcall (not yet executed)"), } assert_eq!(["30"], *data.borrow().as_slice()); - match vm.exec() { + match vm.exec(&image) { StopReason::Upcall(handler) => handler.invoke().await.unwrap(), _ => panic!("Third exec should stop at the second upcall"), } assert_eq!(["30", "20"], *data.borrow().as_slice()); - match vm.exec() { + match vm.exec(&image) { StopReason::Eof => (), _ => panic!("Fourth exec should stop at EOF"), } @@ -488,8 +494,7 @@ mod tests { let mut vm = Vm::new(HashMap::default()); let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); let image = compiler.compile(&mut b"END".as_slice()).unwrap(); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { StopReason::End(code) if code.is_success() => (), _ => panic!("Unexpected stop reason"), } @@ -500,11 +505,76 @@ mod tests { let mut vm = Vm::new(HashMap::default()); let compiler = Compiler::new(&HashMap::default(), &[]).unwrap(); let image = compiler.compile(&mut b"END 3".as_slice()).unwrap(); - vm.load(image); - match vm.exec() { + match vm.exec(&image) { + StopReason::End(code) if code.to_i32() == 3 => (), + _ => panic!("Unexpected stop reason"), + } + } + + #[tokio::test] + async fn test_exec_end_can_resume_after_append() { + let data = Rc::from(RefCell::from(vec![])); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); + + let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let mut image = Image::default(); + compiler.compile_more(&mut image, &mut b"END 3".as_slice()).unwrap(); + + let mut vm = Vm::new(upcalls_by_name); + match vm.exec(&image) { StopReason::End(code) if code.to_i32() == 3 => (), _ => panic!("Unexpected stop reason"), } + match vm.exec(&image) { + StopReason::Eof => (), + _ => panic!("Execution should park at EOF after END"), + } + + compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap(); + match vm.exec(&image) { + StopReason::Upcall(handler) => handler.invoke().await.unwrap(), + _ => panic!("Execution should resume at newly appended code"), + } + assert_eq!(["2"], *data.borrow().as_slice()); + + match vm.exec(&image) { + StopReason::Eof => (), + _ => panic!("Execution should stop at EOF after appended code"), + } + } + + #[tokio::test] + async fn test_exec_exception_can_resume_after_append() { + let data = Rc::from(RefCell::from(vec![])); + let mut upcalls_by_name: HashMap> = HashMap::new(); + upcalls_by_name.insert(SymbolKey::from("OUT"), OutCommand::new(data.clone())); + + let mut compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); + let mut image = Image::default(); + compiler.compile_more(&mut image, &mut b"a = 1 / 0".as_slice()).unwrap(); + + let mut vm = Vm::new(upcalls_by_name); + match vm.exec(&image) { + StopReason::Exception(_, msg) if msg == "Division by zero" => (), + _ => panic!("Unexpected stop reason"), + } + match vm.exec(&image) { + StopReason::Eof => (), + _ => panic!("Execution should park at EOF after an exception"), + } + + compiler.compile_more(&mut image, &mut b"OUT 2".as_slice()).unwrap(); + match vm.exec(&image) { + StopReason::Upcall(handler) => handler.invoke().await.unwrap(), + _ => panic!("Execution should resume at newly appended code"), + } + assert_eq!(["2"], *data.borrow().as_slice()); + + match vm.exec(&image) { + StopReason::Eof => (), + _ => panic!("Execution should stop at EOF after appended code"), + } } #[tokio::test] @@ -517,8 +587,7 @@ mod tests { let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); let image = compiler.compile(&mut b"POS_CAPTURE".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - run_to_end(&mut vm).await; + run_to_end(&mut vm, &image).await; let pos = positions.borrow(); assert_eq!(&[] as &[LineCol], pos.as_slice()); @@ -534,8 +603,7 @@ mod tests { let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); let image = compiler.compile(&mut b"POS_CAPTURE 42".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - run_to_end(&mut vm).await; + run_to_end(&mut vm, &image).await; let pos = positions.borrow(); assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice()); @@ -551,8 +619,7 @@ mod tests { let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); let image = compiler.compile(&mut b"POS_CAPTURE 1, 2, 3".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - run_to_end(&mut vm).await; + run_to_end(&mut vm, &image).await; let pos = positions.borrow(); assert_eq!( @@ -575,8 +642,7 @@ mod tests { let compiler = Compiler::new(&upcalls_by_name, &[]).unwrap(); let image = compiler.compile(&mut b"POS_CAPTURE 1 + 2".as_slice()).unwrap(); let mut vm = Vm::new(upcalls_by_name); - vm.load(image); - run_to_end(&mut vm).await; + run_to_end(&mut vm, &image).await; let pos = positions.borrow(); assert_eq!(&[LineCol { line: 1, col: 13 }], pos.as_slice()); diff --git a/core2/tests/test_incremental.md b/core2/tests/test_incremental.md index ca822e2e..071d0bf4 100644 --- a/core2/tests/test_incremental.md +++ b/core2/tests/test_incremental.md @@ -15,7 +15,7 @@ OUT 1 0003: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=1% @@ -39,10 +39,9 @@ OUT 2 0006: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain -0=1% 0=2% ``` @@ -65,7 +64,7 @@ OUT a 0004: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=3% @@ -90,11 +89,10 @@ OUT a 0007: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=3% -0=3% ``` # Test: Program-scope variables do not leak into functions @@ -142,7 +140,7 @@ OUT a 0004: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=3% @@ -225,7 +223,7 @@ say_hello 0017: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=8% @@ -260,7 +258,7 @@ GETDATA 0001: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=1% 1=2% @@ -281,11 +279,10 @@ GETDATA 0002: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=1% 1=2% 2=3% -0=1% 1=2% 2=3% ``` # Test: Failed compile does not define ghost program variables @@ -343,7 +340,7 @@ OUT a 0004: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=5% @@ -422,7 +419,7 @@ OUT stable 0007: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=7% @@ -468,8 +465,238 @@ GETDATA 0001: EOF ; 0:0 ``` -## Output (full) +## Output (partial) ```plain 0=9% 1=10% ``` + +# Test: Explicit END does not block later compiles + +## Source (partial) + +```basic +OUT 1 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=1% +``` + +## Source (partial) + +```basic +END 3 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 3 ; 1:5 +0004: END R64 ; 1:1 +0005: EOF ; 0:0 +``` + +## Exit code (partial) + +```plain +3 +``` + +## Source (partial) + +```basic +OUT 2 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 3 ; 1:5 +0004: END R64 ; 1:1 +0005: LOADI R65, 2 ; 1:5 +0006: LOADI R64, 258 ; 1:5 +0007: UPCALL 0, R64 ; 1:1, OUT +0008: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=2% +``` + +# Test: Runtime errors do not block later compiles + +## Source (partial) + +```basic +OUT 1 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=1% +``` + +## Source (partial) + +```basic +a = 1 / 0 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 1 ; 1:5 +0004: LOADI R65, 0 ; 1:9 +0005: DIVI R64, R64, R65 ; 1:7 +0006: EOF ; 0:0 +``` + +## Runtime errors (partial) + +```plain +1:7: Division by zero +``` + +## Source (partial) + +```basic +OUT 2 +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 1 ; 1:5 +0001: LOADI R64, 258 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 1 ; 1:5 +0004: LOADI R65, 0 ; 1:9 +0005: DIVI R64, R64, R65 ; 1:7 +0006: LOADI R66, 2 ; 1:5 +0007: LOADI R65, 258 ; 1:5 +0008: UPCALL 0, R65 ; 1:1, OUT +0009: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=2% +``` + +# Test: Labels + +## Source (partial) + +```basic +OUT "before" +a = 0 +@first +a = a + 1 +OUT a +IF a = 5 THEN END +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R64, 259 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 0 ; 2:5 +0004: MOVE R64, R64 ; 4:5 +0005: LOADI R65, 1 ; 4:9 +0006: ADDI R64, R64, R65 ; 4:7 +0007: MOVE R66, R64 ; 5:5 +0008: LOADI R65, 258 ; 5:5 +0009: UPCALL 0, R65 ; 5:1, OUT +0010: MOVE R65, R64 ; 6:4 +0011: LOADI R66, 5 ; 6:8 +0012: CMPEQI R65, R65, R66 ; 6:6 +0013: JMPF R65, 16 ; 6:4 +0014: LOADI R65, 0 ; 6:15 +0015: END R65 ; 6:15 +0016: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=before$ +0=1% +``` + +## Source (partial) + +```basic +GOTO @first +OUT "done" +``` + +## Disassembly (full) + +```asm +0000: LOADI R65, 0 ; 1:5 +0001: LOADI R64, 259 ; 1:5 +0002: UPCALL 0, R64 ; 1:1, OUT +0003: LOADI R64, 0 ; 2:5 +0004: MOVE R64, R64 ; 4:5 +0005: LOADI R65, 1 ; 4:9 +0006: ADDI R64, R64, R65 ; 4:7 +0007: MOVE R66, R64 ; 5:5 +0008: LOADI R65, 258 ; 5:5 +0009: UPCALL 0, R65 ; 5:1, OUT +0010: MOVE R65, R64 ; 6:4 +0011: LOADI R66, 5 ; 6:8 +0012: CMPEQI R65, R65, R66 ; 6:6 +0013: JMPF R65, 16 ; 6:4 +0014: LOADI R65, 0 ; 6:15 +0015: END R65 ; 6:15 +0016: JUMP 4 ; 1:6 +0017: LOADI R66, 1 ; 2:5 +0018: LOADI R65, 259 ; 2:5 +0019: UPCALL 0, R65 ; 2:1, OUT +0020: EOF ; 0:0 +``` + +## Output (partial) + +```plain +0=2% +0=3% +0=4% +0=5% +``` diff --git a/core2/tests/testutils/mod.rs b/core2/tests/testutils/mod.rs index d5e409e8..987f8d1d 100644 --- a/core2/tests/testutils/mod.rs +++ b/core2/tests/testutils/mod.rs @@ -298,9 +298,9 @@ fn labels_for(test: &Test) -> Labels { source: "## Source (partial)", disassembly: "## Disassembly (full)", compiler_errors: "## Compiler errors (partial)", - exit_code: "## Exit code (full)", - output: "## Output (full)", - runtime_errors: "## Runtime errors (full)", + exit_code: "## Exit code (partial)", + output: "## Output (partial)", + runtime_errors: "## Runtime errors (partial)", } } else { Labels { @@ -405,11 +405,10 @@ fn test_diff_different() -> io::Result<()> { Ok(()) } -/// Executes the image already loaded in `vm` through completion, and converts the result -/// into an exit code. -async fn run_image(vm: &mut Vm) -> Result { +/// Executes `image` through completion in `vm`, and converts the result into an exit code. +async fn run_image(vm: &mut Vm, image: &Image) -> Result { loop { - match vm.exec() { + match vm.exec(image) { StopReason::End(code) => return Ok(code.to_i32()), StopReason::Eof => return Ok(0), StopReason::Upcall(handle) => { @@ -442,6 +441,8 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() let mut upcalls_by_name: HashMap> = HashMap::default(); callables::register_all(&mut upcalls_by_name, console.clone()); let mut compiler = Compiler::new(&upcalls_by_name, &[]).expect("Cannot fail"); + let mut image = Image::default(); + let mut vm = Vm::new(upcalls_by_name.clone()); for source in test.sources { write!(generated, "\n{}\n\n", labels.source)?; @@ -451,16 +452,13 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() } write!(generated, "```\n")?; - let image = match compiler.compile_more(&mut source.as_bytes()) { - Ok(image) => image, - Err(e) => { - write!(generated, "\n{}\n\n", labels.compiler_errors)?; - write!(generated, "```plain\n")?; - write!(generated, "{}\n", e)?; - write!(generated, "```\n")?; - continue; - } - }; + if let Err(e) = compiler.compile_more(&mut image, &mut source.as_bytes()) { + write!(generated, "\n{}\n\n", labels.compiler_errors)?; + write!(generated, "```plain\n")?; + write!(generated, "{}\n", e)?; + write!(generated, "```\n")?; + continue; + } write!(generated, "\n{}\n\n", labels.disassembly)?; write!(generated, "```asm\n")?; @@ -470,9 +468,7 @@ async fn regenerate(golden: &Path, generated: &mut W) -> io::Result<() write!(generated, "```\n")?; console.borrow_mut().clear(); - let mut vm = Vm::new(upcalls_by_name.clone()); - vm.load(image); - match run_image(&mut vm).await { + match run_image(&mut vm, &image).await { Ok(0) => (), Ok(i) => { write!(generated, "\n{}\n\n", labels.exit_code)?; From 8ab6506f977f479b84756e88ad14571c0ae30ec1 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 27 Mar 2026 18:42:46 -0700 Subject: [PATCH 46/53] core2: Validate repeated argument separators Reject invalid separators for repeated callable arguments in the compiler and keep accepting the implicit end-of-call marker for the last repeated argument. --- core2/src/compiler/args.rs | 5 +++++ core2/tests/test_args.md | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 22866c80..777413ae 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -62,6 +62,10 @@ fn validate_syn_argsep( } ArgSepSyntax::OneOf(exp_seps) => { + if sep == ArgSep::End { + return Ok(()); + } + let mut found = false; for exp_sep in *exp_seps { debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); @@ -337,6 +341,7 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; arg_linecols.push(arg_pos); + validate_syn_argsep(&md, arg_pos, &syn.sep, arg_iter.peek().is_none(), sep)?; let tag = match expr { None => { diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index a78feb78..7c4bac1e 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -722,6 +722,34 @@ DEFINE_AND_CHANGE_ARGS 5 1:24: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] ``` +# Test: Repeated references with require_one, invalid first separator + +## Source + +```basic +DEFINE_AND_CHANGE_ARGS b AS d +``` + +## Compilation errors + +```plain +1:24: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +``` + +# Test: Repeated references with require_one, invalid later separator + +## Source + +```basic +DEFINE_AND_CHANGE_ARGS b, d AS i +``` + +## Compilation errors + +```plain +1:27: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +``` + # Test: Repeated references with require_one, single argument ## Source From 0441a7609de44191daf8ec3546550409fd206044 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sun, 29 Mar 2026 07:42:21 -0700 Subject: [PATCH 47/53] core2: Remove scope from "Unknown symbol" messages Telling the user whether the missing symbol is global or local is confusing because, when we conclude a symbol is missing, it is always on the "global lookup" path. This helps align error messages with the old core for fewer test update churn. --- core2/src/compiler/exprs.rs | 10 +++------- core2/src/compiler/mod.rs | 6 +++--- core2/src/compiler/top.rs | 8 ++------ core2/tests/test_args.md | 2 +- core2/tests/test_arrays.md | 4 ++-- core2/tests/test_functions.md | 2 +- core2/tests/test_incremental.md | 10 +++++----- core2/tests/test_subs.md | 2 +- 8 files changed, 18 insertions(+), 26 deletions(-) diff --git a/core2/src/compiler/exprs.rs b/core2/src/compiler/exprs.rs index ef1f8121..a6973dd5 100644 --- a/core2/src/compiler/exprs.rs +++ b/core2/src/compiler/exprs.rs @@ -16,7 +16,7 @@ //! Functions to convert expressions into bytecode. use crate::ast::{Expr, ExprType}; -use crate::bytecode::{self, Register, RegisterScope}; +use crate::bytecode::{self, Register}; use crate::compiler::args::compile_args; use crate::compiler::codegen::{Codegen, Fixup}; use crate::compiler::syms::{self, SymbolKey, SymbolPrototype, TempScope, TempSymtable}; @@ -646,11 +646,7 @@ pub(super) fn compile_expr( codegen, symtable, reg, key_pos, arr_reg, &info, span.args, ), Err(syms::Error::UndefinedSymbol(..)) | Ok((_, SymbolPrototype::Scalar(_))) => { - return Err(Error::UndefinedSymbol( - span.vref_pos, - span.vref, - RegisterScope::Global, - )); + return Err(Error::UndefinedSymbol(span.vref_pos, span.vref)); } Err(e) => return Err(Error::from_syms(e, key_pos)), } @@ -681,7 +677,7 @@ pub(super) fn compile_expr( let key = SymbolKey::from(&span.vref.name); let Some(md) = symtable.get_callable(&key) else { - return Err(Error::UndefinedSymbol(span.pos, span.vref, RegisterScope::Global)); + return Err(Error::UndefinedSymbol(span.pos, span.vref)); }; let Some(etype) = md.return_type() else { diff --git a/core2/src/compiler/mod.rs b/core2/src/compiler/mod.rs index e62ed802..de7e1d90 100644 --- a/core2/src/compiler/mod.rs +++ b/core2/src/compiler/mod.rs @@ -134,8 +134,8 @@ pub enum Error { TypeMismatch(LineCol, ExprType, ExprType), /// Reference to an undefined symbol. - #[error("{0}: Undefined {2} symbol {1}")] - UndefinedSymbol(LineCol, VarRef, RegisterScope), + #[error("{0}: Undefined symbol {1}")] + UndefinedSymbol(LineCol, VarRef), /// Reference to an unknown label. #[error("{0}: Unknown label {1}")] @@ -160,7 +160,7 @@ impl Error { Error::IncompatibleTypeAnnotationInReference(pos, vref) } syms::Error::OutOfRegisters(scope) => Error::OutOfRegisters(pos, scope), - syms::Error::UndefinedSymbol(vref, scope) => Error::UndefinedSymbol(pos, vref, scope), + syms::Error::UndefinedSymbol(vref, _scope) => Error::UndefinedSymbol(pos, vref), } } } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 74168262..065c1f8b 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -19,7 +19,7 @@ use crate::ast::{ ArgSep, AssignmentSpan, CallableSpan, CaseGuardSpan, CaseRelOp, DoGuard, DoSpan, Expr, ExprType, ForSpan, IfSpan, OnErrorSpan, SelectSpan, Statement, VarRef, WhileSpan, }; -use crate::bytecode::{self, ErrorHandlerMode, PackedArrayType, Register, RegisterScope}; +use crate::bytecode::{self, ErrorHandlerMode, PackedArrayType, Register}; use crate::callable::{ArgSepSyntax, CallableMetadata, RequiredValueSyntax, SingularArgSyntax}; use crate::compiler::args::{compile_args, define_new_args}; use crate::compiler::codegen::{Codegen, Fixup}; @@ -751,11 +751,7 @@ fn compile_stmt( let key_pos = span.vref_pos; let Some(md) = symtable.get_callable(&key) else { - return Err(Error::UndefinedSymbol( - key_pos, - span.vref.clone(), - RegisterScope::Global, - )); + return Err(Error::UndefinedSymbol(key_pos, span.vref.clone())); }; if md.return_type().is_some() { return Err(Error::NotAFunction(span.vref_pos, span.vref)); diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index 7c4bac1e..dd59a1e7 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -508,7 +508,7 @@ OUT i ## Compilation errors ```plain -1:24: Undefined global symbol i +1:24: Undefined symbol i ``` # Test: Singular required reference, define output variable with default type diff --git a/core2/tests/test_arrays.md b/core2/tests/test_arrays.md index 88f7de87..d74ef48a 100644 --- a/core2/tests/test_arrays.md +++ b/core2/tests/test_arrays.md @@ -388,7 +388,7 @@ OUT foo(1) ## Compilation errors ```plain -1:5: Undefined global symbol foo +1:5: Undefined symbol foo ``` # Test: Scalar used as array-style expression @@ -403,7 +403,7 @@ OUT a(1) ## Compilation errors ```plain -2:5: Undefined global symbol a +2:5: Undefined symbol a ``` # Test: Array dimension too large diff --git a/core2/tests/test_functions.md b/core2/tests/test_functions.md index 913d46d7..a8c0567e 100644 --- a/core2/tests/test_functions.md +++ b/core2/tests/test_functions.md @@ -358,7 +358,7 @@ OUT local_var ## Compilation errors ```plain -6:5: Undefined global symbol local_var +6:5: Undefined symbol local_var ``` # Test: Argument passing diff --git a/core2/tests/test_incremental.md b/core2/tests/test_incremental.md index 071d0bf4..4293cae3 100644 --- a/core2/tests/test_incremental.md +++ b/core2/tests/test_incremental.md @@ -121,7 +121,7 @@ END FUNCTION ## Compiler errors (partial) ```plain -2:9: Undefined global symbol a +2:9: Undefined symbol a ``` ## Source (partial) @@ -309,7 +309,7 @@ c = b + 3 ## Compiler errors (partial) ```plain -1:5: Undefined global symbol b +1:5: Undefined symbol b ``` ## Source (partial) @@ -321,7 +321,7 @@ OUT a, c ## Compiler errors (partial) ```plain -1:8: Undefined global symbol c +1:8: Undefined symbol c ``` ## Source (partial) @@ -381,7 +381,7 @@ END FUNCTION ## Compiler errors (partial) ```plain -2:14: Undefined global symbol missing +2:14: Undefined symbol missing ``` ## Source (partial) @@ -393,7 +393,7 @@ OUT stable, broken ## Compiler errors (partial) ```plain -1:13: Undefined global symbol broken +1:13: Undefined symbol broken ``` ## Source (partial) diff --git a/core2/tests/test_subs.md b/core2/tests/test_subs.md index 083e12ed..ea305495 100644 --- a/core2/tests/test_subs.md +++ b/core2/tests/test_subs.md @@ -209,7 +209,7 @@ OUT local_var ## Compilation errors ```plain -6:5: Undefined global symbol local_var +6:5: Undefined symbol local_var ``` # Test: Calling a command as a function with arguments From 271cc17bbf0e52cb4d5975adeb0c41e6ceb59abb Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 27 Mar 2026 18:30:05 -0700 Subject: [PATCH 48/53] core2: Make get_global take SymbolKeys This allows for more efficient queries when we already have the data in the right shape, which is the case for a lot of tests in std. --- core2/examples/config.rs | 20 ++++++++++---------- core2/src/compiler/top.rs | 3 ++- core2/src/vm/mod.rs | 22 ++++++++++++---------- core2/tests/config.out | 2 +- 4 files changed, 25 insertions(+), 22 deletions(-) diff --git a/core2/examples/config.rs b/core2/examples/config.rs index 3c155688..5da5aa8d 100644 --- a/core2/examples/config.rs +++ b/core2/examples/config.rs @@ -98,20 +98,20 @@ fn main() { } } - // Query the global variables by name. - match vm.get_global(&image, "foo_value") { + // Query the global variables by key. + match vm.get_global(&image, &"foo_value".into()) { Ok(Some(ConstantDatum::Integer(v))) => println!("foo_value% = {}", v), Ok(Some(other)) => println!("foo_value% has unexpected type: {:?}", other), Ok(None) => println!("foo_value% is not set"), Err(e) => println!("foo_value%: error: {}", e), } - match vm.get_global(&image, "result_total") { + match vm.get_global(&image, &"result_total".into()) { Ok(Some(ConstantDatum::Integer(v))) => println!("result_total% = {}", v), Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), Ok(None) => println!("result_total% is not set"), Err(e) => println!("result_total%: error: {}", e), } - match vm.get_global(&image, "status") { + match vm.get_global(&image, &"status".into()) { Ok(Some(ConstantDatum::Text(v))) => println!("status$ = {:?}", v), Ok(Some(other)) => println!("status$ has unexpected type: {:?}", other), Ok(None) => println!("status$ is not set"), @@ -119,7 +119,7 @@ fn main() { } // defined_within was not provided upfront but was declared as a global within // the script. We can query it here too. - match vm.get_global(&image, "defined_within") { + match vm.get_global(&image, &"defined_within".into()) { Ok(Some(ConstantDatum::Integer(v))) => println!("defined_within% = {}", v), Ok(Some(other)) => println!("result_total% has unexpected type: {:?}", other), Ok(None) => println!("defined_within% is not set"), @@ -127,7 +127,7 @@ fn main() { } // optional_flag was declared but the script never assigned it, so it should // receive its "zero value". - match vm.get_global(&image, "optional_flag") { + match vm.get_global(&image, &"optional_flag".into()) { Ok(Some(ConstantDatum::Boolean(v))) => println!("optional_flag? = {}", v), Ok(Some(other)) => println!("optional_flag? has unexpected type: {:?}", other), Ok(None) => println!("optional_flag? is not declared"), @@ -135,20 +135,20 @@ fn main() { } // injected_value was pre-initialized to 5 before compilation. The script incremented // it by 1, so we expect 6 here. - match vm.get_global(&image, "injected_value") { + match vm.get_global(&image, &"injected_value".into()) { Ok(Some(ConstantDatum::Integer(v))) => println!("injected_value% = {}", v), Ok(Some(other)) => println!("injected_value% has unexpected type: {:?}", other), Ok(None) => println!("injected_value% is not set"), Err(e) => println!("injected_value%: error: {}", e), } // "unknown" was never declared at all, so get_global returns Ok(None). - match vm.get_global(&image, "unknown") { + match vm.get_global(&image, &"unknown".into()) { Ok(Some(v)) => println!("unknown = {:?}", v), Ok(None) => println!("unknown is not declared"), Err(e) => println!("unknown: error: {}", e), } for i in 0..3_i32 { - match vm.get_global_array(&image, "results", &[i]) { + match vm.get_global_array(&image, &"results".into(), &[i]) { Ok(Some(ConstantDatum::Integer(v))) => println!("results%({}) = {}", i, v), Ok(Some(other)) => println!("results%({}) has unexpected type: {:?}", i, other), Ok(None) => println!("results%({}) is not set", i), @@ -156,7 +156,7 @@ fn main() { } } // Demonstrate that querying a scalar as an array yields an error. - match vm.get_global_array(&image, "foo_value", &[0]) { + match vm.get_global_array(&image, &"foo_value".into(), &[0]) { Ok(v) => println!("foo_value%(0) = {:?}", v), Err(e) => println!("foo_value%(0): error: {}", e), } diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index 065c1f8b..f5e7d69c 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -1295,7 +1295,8 @@ mod tests { StopReason::Exception(pos, msg) => panic!("exception at {pos}: {msg}"), StopReason::Upcall(_) => panic!("unexpected upcall"), } - vm.get_global(&image, name).expect("get_global failed").expect("global not found") + let key = SymbolKey::from(name); + vm.get_global(&image, &key).expect("get_global failed").expect("global not found") } #[test] diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index 90f55926..abfefb7b 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -234,18 +234,21 @@ impl Vm { } } - /// Returns the value of the global scalar variable `name` as a `ConstantDatum`. + /// Returns the value of the global scalar variable `key` as a `ConstantDatum`. /// /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the /// variable was not declared). Returns `Err` if the variable exists but is an /// array; in that case, use `get_global_array` instead. - pub fn get_global(&self, image: &Image, name: &str) -> GetGlobalResult> { - let key = SymbolKey::from(name); - let Some(info) = image.debug_info.global_vars.get(&key) else { + pub fn get_global( + &self, + image: &Image, + key: &SymbolKey, + ) -> GetGlobalResult> { + let Some(info) = image.debug_info.global_vars.get(key) else { return Ok(None); }; if info.ndims != 0 { - return Err(GetGlobalError::IsArray(name.to_owned())); + return Err(GetGlobalError::IsArray(key.to_string())); } let raw = self.context.get_global_reg_raw(info.reg); let datum = match info.subtype { @@ -260,7 +263,7 @@ impl Vm { Ok(Some(datum)) } - /// Returns the value of an element in the global array variable `name` at the given + /// Returns the value of an element in the global array variable `key` at the given /// `subscripts` as a `ConstantDatum`. /// /// Returns `Ok(None)` if the variable is not defined (no image is loaded or the @@ -269,15 +272,14 @@ impl Vm { pub fn get_global_array( &self, image: &Image, - name: &str, + key: &SymbolKey, subscripts: &[i32], ) -> GetGlobalResult> { - let key = SymbolKey::from(name); - let Some(info) = image.debug_info.global_vars.get(&key) else { + let Some(info) = image.debug_info.global_vars.get(key) else { return Ok(None); }; if info.ndims == 0 { - return Err(GetGlobalError::IsScalar(name.to_owned())); + return Err(GetGlobalError::IsScalar(key.to_string())); } let raw = self.context.get_global_reg_raw(info.reg); let ptr = DatumPtr::from(raw); diff --git a/core2/tests/config.out b/core2/tests/config.out index c02d6f97..465bd373 100644 --- a/core2/tests/config.out +++ b/core2/tests/config.out @@ -8,4 +8,4 @@ unknown is not declared results%(0) = 10 results%(1) = 20 results%(2) = 30 -foo_value%(0): error: 'foo_value' is a scalar variable; use get_global to access it +foo_value%(0): error: 'FOO_VALUE' is a scalar variable; use get_global to access it From 7b4892eb08ed8f49d839885ef05597f4ac744736 Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 27 Mar 2026 18:30:05 -0700 Subject: [PATCH 49/53] core2: Add From conversors for ConstantDatum This is to help with unit tests. --- core2/src/mem.rs | 59 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/core2/src/mem.rs b/core2/src/mem.rs index 711b8144..7acfe12f 100644 --- a/core2/src/mem.rs +++ b/core2/src/mem.rs @@ -101,6 +101,36 @@ impl Hash for ConstantDatum { } } +impl From for ConstantDatum { + fn from(value: bool) -> Self { + Self::Boolean(value) + } +} + +impl From for ConstantDatum { + fn from(value: f64) -> Self { + Self::Double(value) + } +} + +impl From for ConstantDatum { + fn from(value: i32) -> Self { + Self::Integer(value) + } +} + +impl From<&str> for ConstantDatum { + fn from(value: &str) -> Self { + Self::Text(value.to_owned()) + } +} + +impl From for ConstantDatum { + fn from(value: String) -> Self { + Self::Text(value) + } +} + impl ConstantDatum { /// Returns the type of the constant datum. pub(crate) fn etype(&self) -> ExprType { @@ -192,3 +222,32 @@ impl DatumPtr { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_constant_datum_from_scalars() { + assert_eq!(ConstantDatum::Boolean(true), ConstantDatum::from(true)); + assert_eq!(ConstantDatum::Double(3.25), ConstantDatum::from(3.25)); + assert_eq!(ConstantDatum::Integer(-7), ConstantDatum::from(-7)); + } + + #[test] + fn test_constant_datum_from_str() { + let mut text = "hello".to_owned(); + let datum = ConstantDatum::from(text.as_str()); + text.clear(); + + assert_eq!(ConstantDatum::Text("hello".to_owned()), datum); + } + + #[test] + fn test_constant_datum_from_string() { + assert_eq!( + ConstantDatum::Text("hello".to_owned()), + ConstantDatum::from("hello".to_owned()) + ); + } +} From 3105cd71f2831b3ae21d5a7ee13ec564139650ba Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Sat, 18 Apr 2026 16:18:51 +0200 Subject: [PATCH 50/53] core2: Fix positions in error messages for upcalls Pointing at the first argument instead of the callable (as a default position) is incorrect for most cases. I'm not sure why I did that in the first place, so fix it now. I'm keeping the "pos_override" feature in "handle_exception", even if the existing two callers set it to None, because I'll have to use this in an upcoming change to add more precise locations when the problem is actually in a specific argument. --- core2/src/vm/mod.rs | 3 +-- core2/tests/test_on_error.md | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index abfefb7b..ea29b8c5 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -70,8 +70,7 @@ impl<'a> UpcallHandler<'a> { match upcall.exec(vm.upcall_scope(image, first_reg, upcall_pc)).await { Ok(()) => Ok(()), Err(e) => { - let pos_override = image.debug_info.instrs[upcall_pc].arg_linecols.first().copied(); - vm.handle_exception(image, upcall_pc, e.to_string(), pos_override); + vm.handle_exception(image, upcall_pc, e.to_string(), None); Ok(()) } } diff --git a/core2/tests/test_on_error.md b/core2/tests/test_on_error.md index 40497181..64364c4c 100644 --- a/core2/tests/test_on_error.md +++ b/core2/tests/test_on_error.md @@ -35,7 +35,7 @@ OUT 2 ```plain 0=1% -0=3:12: Some internal error$ +0=3:5: Some internal error$ ``` # Test: ON ERROR GOTO label @@ -76,7 +76,7 @@ OUT LAST_ERROR ```plain 0=1% -0=3:12: Some internal error$ +0=3:5: Some internal error$ ``` # Test: ON ERROR reset @@ -120,7 +120,7 @@ OUT RAISEF("internal") ## Runtime errors ```plain -7:12: Some internal error +7:5: Some internal error ``` ## Output @@ -163,7 +163,7 @@ OUT LAST_ERROR ```plain 0=1% -0=3:12: Some internal error$ +0=3:5: Some internal error$ ``` # Test: ON ERROR RESUME NEXT command failure @@ -196,7 +196,7 @@ OUT LAST_ERROR ```plain 0=1% -0=3:7: Some internal error$ +0=3:1: Some internal error$ ``` # Test: ON ERROR RESUME NEXT function failure in statement @@ -230,7 +230,7 @@ OUT 1: OUT RAISEF("internal"): OUT LAST_ERROR ```plain 0=1% -0=2:19: Some internal error$ +0=2:12: Some internal error$ ``` # Test: ON ERROR RESUME NEXT command failure in statement @@ -261,7 +261,7 @@ OUT 1: RAISE "internal": OUT LAST_ERROR ```plain 0=1% -0=2:14: Some internal error$ +0=2:8: Some internal error$ ``` # Test: ON ERROR RESUME NEXT argument error @@ -290,7 +290,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("argument"): OUT LAST_ERROR ## Output ```plain -0=1:34: Bad argument$ +0=1:27: Bad argument$ ``` # Test: ON ERROR RESUME NEXT eval error @@ -319,7 +319,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("eval"): OUT LAST_ERROR ## Output ```plain -0=1:34: Some eval error$ +0=1:27: Some eval error$ ``` # Test: ON ERROR RESUME NEXT internal error @@ -348,7 +348,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("internal"): OUT LAST_ERROR ## Output ```plain -0=1:34: Some internal error$ +0=1:27: Some internal error$ ``` # Test: ON ERROR RESUME NEXT I/O error @@ -377,7 +377,7 @@ ON ERROR RESUME NEXT: OUT RAISEF("io"): OUT LAST_ERROR ## Output ```plain -0=1:34: Some I/O error$ +0=1:27: Some I/O error$ ``` # Test: ON ERROR GOTO unknown label From 7d410e95aa792cff0497778f4412316af7171ddc Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 17 Apr 2026 16:30:37 +0200 Subject: [PATCH 51/53] core2: Fix separator positions in error messages --- core2/src/compiler/args.rs | 20 ++++++++++---------- core2/tests/test_args.md | 20 +++++++++++++++++--- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/core2/src/compiler/args.rs b/core2/src/compiler/args.rs index 777413ae..d5bd573c 100644 --- a/core2/src/compiler/args.rs +++ b/core2/src/compiler/args.rs @@ -35,17 +35,17 @@ use std::rc::Rc; /// /// `syn` contains the details about the separator syntax that is accepted. /// -/// `sep` and `sep_pos` are the details about the separator being compiled. -/// /// `is_last` indicates whether this is the last separator in the command call and is used /// only for diagnostics purposes. +/// +/// `sep` and `sep_pos` are the details about the separator being compiled. #[allow(clippy::too_many_arguments)] fn validate_syn_argsep( md: &Rc, - pos: LineCol, syn: &ArgSepSyntax, is_last: bool, sep: ArgSep, + sep_pos: LineCol, ) -> Result<()> { debug_assert!( (!is_last || sep == ArgSep::End) && (is_last || sep != ArgSep::End), @@ -56,7 +56,7 @@ fn validate_syn_argsep( ArgSepSyntax::Exactly(exp_sep) => { debug_assert!(*exp_sep != ArgSep::End, "Use ArgSepSyntax::End"); if sep != ArgSep::End && sep != *exp_sep { - return Err(Error::CallableSyntax(pos, md.as_ref().clone())); + return Err(Error::CallableSyntax(sep_pos, md.as_ref().clone())); } Ok(()) } @@ -75,7 +75,7 @@ fn validate_syn_argsep( } } if !found { - return Err(Error::CallableSyntax(pos, md.as_ref().clone())); + return Err(Error::CallableSyntax(sep_pos, md.as_ref().clone())); } Ok(()) } @@ -215,7 +215,7 @@ pub(super) fn compile_args( let temp_value = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; arg_linecols.push(arg_pos); compile_expr_as_type(codegen, symtable, temp_value, expr, details.vtype)?; - validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + validate_syn_argsep(&md, exp_sep, arg_iter.peek().is_none(), sep, sep_pos)?; } } } @@ -244,7 +244,7 @@ pub(super) fn compile_args( let temp = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; arg_linecols.push(arg_pos); codegen.emit(bytecode::make_load_register_ptr(temp, vtype, reg), arg_pos); - validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + validate_syn_argsep(&md, exp_sep, arg_iter.peek().is_none(), sep, sep_pos)?; } Some(expr) => { return Err(Error::CallableSyntax(expr.start_pos(), md.as_ref().clone())); @@ -274,7 +274,7 @@ pub(super) fn compile_args( bytecode::VarArgTag::Immediate(sep, details.vtype) } }; - validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + validate_syn_argsep(&md, exp_sep, arg_iter.peek().is_none(), sep, sep_pos)?; codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); } @@ -305,7 +305,7 @@ pub(super) fn compile_args( bytecode::VarArgTag::Immediate(sep, etype) } }; - validate_syn_argsep(&md, arg_pos, exp_sep, arg_iter.peek().is_none(), sep)?; + validate_syn_argsep(&md, exp_sep, arg_iter.peek().is_none(), sep, sep_pos)?; codegen.emit(bytecode::make_load_integer(temp_tag, tag.make_u16()), arg_pos); } }; @@ -341,7 +341,7 @@ pub(super) fn compile_args( let arg_pos = expr.as_ref().map(|e| e.start_pos()).unwrap_or(sep_pos); let temp_tag = scope.alloc().map_err(|e| Error::from_syms(e, arg_pos))?; arg_linecols.push(arg_pos); - validate_syn_argsep(&md, arg_pos, &syn.sep, arg_iter.peek().is_none(), sep)?; + validate_syn_argsep(&md, &syn.sep, arg_iter.peek().is_none(), sep, sep_pos)?; let tag = match expr { None => { diff --git a/core2/tests/test_args.md b/core2/tests/test_args.md index dd59a1e7..4bf7376c 100644 --- a/core2/tests/test_args.md +++ b/core2/tests/test_args.md @@ -277,7 +277,21 @@ OUT_POSITIONAL 3 AS 5.6 AS "Foo" ## Compilation errors ```plain -1:16: OUT_POSITIONAL expected [arg1] <,|;> arg2% AS arg3 +1:18: OUT_POSITIONAL expected [arg1] <,|;> arg2% AS arg3 +``` + +# Test: Singular arguments of various kinds, second separator mismatched + +## Source + +```basic +OUT_POSITIONAL 3, 5; "Foo" +``` + +## Compilation errors + +```plain +1:20: OUT_POSITIONAL expected [arg1] <,|;> arg2% AS arg3 ``` # Test: Repeated arguments, none provided @@ -733,7 +747,7 @@ DEFINE_AND_CHANGE_ARGS b AS d ## Compilation errors ```plain -1:24: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +1:26: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] ``` # Test: Repeated references with require_one, invalid later separator @@ -747,7 +761,7 @@ DEFINE_AND_CHANGE_ARGS b, d AS i ## Compilation errors ```plain -1:27: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] +1:29: DEFINE_AND_CHANGE_ARGS expected arg1[ <,|;> .. <,|;> argN] ``` # Test: Repeated references with require_one, single argument From 879d64e8f9b6ffbecb4c43a98906295bc4b1d34c Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 17 Apr 2026 16:28:41 +0200 Subject: [PATCH 52/53] core2: Make GlobalDefs cloneable This is necessary for the std Tester to be cloneable too, which is something that various test cases expect. --- core2/src/compiler/top.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core2/src/compiler/top.rs b/core2/src/compiler/top.rs index f5e7d69c..55656816 100644 --- a/core2/src/compiler/top.rs +++ b/core2/src/compiler/top.rs @@ -1130,6 +1130,7 @@ pub fn only_metadata( } /// Descriptor for a single global variable to be pre-defined before compilation. +#[derive(Clone)] pub struct GlobalDef { /// Name of the variable (case-insensitive, as in EndBASIC). pub name: String, @@ -1139,6 +1140,7 @@ pub struct GlobalDef { } /// Kind of a pre-defined global variable. +#[derive(Clone)] pub enum GlobalDefKind { /// A scalar (non-array) variable. Scalar { From e24de2dc956c9c1073092ca09f9754f0f4a7d5fe Mon Sep 17 00:00:00 2001 From: Julio Merino Date: Fri, 17 Apr 2026 16:28:41 +0200 Subject: [PATCH 53/53] core2: Add the CallError::Syntax variant This error variant is to be used by upcalls in the std library to report problems with arguments at runtime and pinpoint where they occur. (E.g. LOCATE receives screen positions and it needs to validate those.) To test this, extend RAISE and RAISEF test callables with a two-argument form and new "syntax", "syntax0", and "syntax1" keywords that raise CallError::Syntax with the position pointing at the first or second argument. Add integration tests in test_call_errors.md to verify that the VM propagates these position overrides correctly for both commands and functions, including under ON ERROR RESUME NEXT and ON ERROR GOTO. --- core2/src/callable.rs | 4 + core2/src/vm/mod.rs | 8 +- core2/tests/integration_test.rs | 1 + core2/tests/test_call_errors.md | 264 +++++++++++++++++++ core2/tests/testutils/callables/raise_cmd.rs | 63 +++-- core2/tests/testutils/callables/raisef_fn.rs | 63 +++-- 6 files changed, 371 insertions(+), 32 deletions(-) create mode 100644 core2/tests/test_call_errors.md diff --git a/core2/src/callable.rs b/core2/src/callable.rs index cdd2943b..9cb5d913 100644 --- a/core2/src/callable.rs +++ b/core2/src/callable.rs @@ -35,6 +35,10 @@ pub enum CallError { /// Generic error with a static message. #[error("{0}")] Other(&'static str), + + /// Indicates a syntax error only detectable at runtime. + #[error("{1}")] + Syntax(LineCol, String), } /// Result type for callable execution. diff --git a/core2/src/vm/mod.rs b/core2/src/vm/mod.rs index ea29b8c5..1804f867 100644 --- a/core2/src/vm/mod.rs +++ b/core2/src/vm/mod.rs @@ -15,7 +15,6 @@ //! Virtual machine for EndBASIC execution. -use crate::CallResult; use crate::ast::ExprType; use crate::bytecode::{ExitCode, Register}; use crate::callable::{Callable, Scope}; @@ -23,6 +22,7 @@ use crate::compiler::SymbolKey; use crate::image::Image; use crate::mem::{ConstantDatum, DatumPtr, HeapDatum}; use crate::reader::LineCol; +use crate::{CallError, CallResult}; use std::collections::HashMap; use std::rc::Rc; @@ -70,7 +70,11 @@ impl<'a> UpcallHandler<'a> { match upcall.exec(vm.upcall_scope(image, first_reg, upcall_pc)).await { Ok(()) => Ok(()), Err(e) => { - vm.handle_exception(image, upcall_pc, e.to_string(), None); + let pos_override = match e { + CallError::Syntax(pos, _) => Some(pos), + _ => None, + }; + vm.handle_exception(image, upcall_pc, e.to_string(), pos_override); Ok(()) } } diff --git a/core2/tests/integration_test.rs b/core2/tests/integration_test.rs index b2e154bc..24d4d11e 100644 --- a/core2/tests/integration_test.rs +++ b/core2/tests/integration_test.rs @@ -47,6 +47,7 @@ one_test!(test_bitwise_or); one_test!(test_bitwise_shl); one_test!(test_bitwise_shr); one_test!(test_bitwise_xor); +one_test!(test_call_errors); one_test!(test_case_insensitivity); one_test!(test_data); one_test!(test_do); diff --git a/core2/tests/test_call_errors.md b/core2/tests/test_call_errors.md new file mode 100644 index 00000000..58b129ae --- /dev/null +++ b/core2/tests/test_call_errors.md @@ -0,0 +1,264 @@ +# Test: Command syntax error from single arg + +## Source + +```basic +RAISE "syntax" +``` + +## Disassembly + +```asm +0000: LOADI R64, 0 ; 1:7 +0001: UPCALL 0, R64 ; 1:1, RAISE +0002: EOF ; 0:0 +``` + +## Runtime errors + +```plain +1:7: Some syntax error +``` + +# Test: Command syntax error pointing at first arg + +## Source + +```basic +RAISE "syntax0", 5 +``` + +## Disassembly + +```asm +0000: LOADI R64, 0 ; 1:7 +0001: LOADI R65, 5 ; 1:18 +0002: UPCALL 0, R64 ; 1:1, RAISE +0003: EOF ; 0:0 +``` + +## Runtime errors + +```plain +1:7: Some syntax error +``` + +# Test: Command syntax error pointing at second arg + +## Source + +```basic +RAISE "syntax1", 5 +``` + +## Disassembly + +```asm +0000: LOADI R64, 0 ; 1:7 +0001: LOADI R65, 5 ; 1:18 +0002: UPCALL 0, R64 ; 1:1, RAISE +0003: EOF ; 0:0 +``` + +## Runtime errors + +```plain +1:18: Some syntax error +``` + +# Test: Command syntax error at second arg with ON ERROR RESUME NEXT + +## Source + +```basic +ON ERROR RESUME NEXT: RAISE "syntax1", 5: OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R64, 0 ; 1:29 +0002: LOADI R65, 5 ; 1:40 +0003: UPCALL 0, R64 ; 1:23, RAISE +0004: UPCALL 1, R65 ; 1:47, LAST_ERROR +0005: LOADI R64, 259 ; 1:47 +0006: UPCALL 2, R64 ; 1:43, OUT +0007: EOF ; 0:0 +``` + +## Output + +```plain +0=1:40: Some syntax error$ +``` + +# Test: Command syntax error at first arg with ON ERROR RESUME NEXT + +## Source + +```basic +ON ERROR RESUME NEXT: RAISE "syntax0", 5: OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R64, 0 ; 1:29 +0002: LOADI R65, 5 ; 1:40 +0003: UPCALL 0, R64 ; 1:23, RAISE +0004: UPCALL 1, R65 ; 1:47, LAST_ERROR +0005: LOADI R64, 259 ; 1:47 +0006: UPCALL 2, R64 ; 1:43, OUT +0007: EOF ; 0:0 +``` + +## Output + +```plain +0=1:29: Some syntax error$ +``` + +# Test: Command syntax error with ON ERROR GOTO + +## Source + +```basic +ON ERROR GOTO @handler +RAISE "syntax" +OUT 1 +@handler +OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: SETEH JUMP, 6 ; 1:1 +0001: LOADI R64, 0 ; 2:7 +0002: UPCALL 0, R64 ; 2:1, RAISE +0003: LOADI R65, 1 ; 3:5 +0004: LOADI R64, 258 ; 3:5 +0005: UPCALL 1, R64 ; 3:1, OUT +0006: UPCALL 2, R65 ; 5:5, LAST_ERROR +0007: LOADI R64, 259 ; 5:5 +0008: UPCALL 1, R64 ; 5:1, OUT +0009: EOF ; 0:0 +``` + +## Output + +```plain +0=2:7: Some syntax error$ +``` + +# Test: Function syntax error from single arg + +## Source + +```basic +OUT RAISEF("syntax") +``` + +## Disassembly + +```asm +0000: LOADI R67, 0 ; 1:12 +0001: UPCALL 0, R66 ; 1:5, RAISEF +0002: MOVE R65, R66 ; 1:5 +0003: LOADI R64, 256 ; 1:5 +0004: UPCALL 1, R64 ; 1:1, OUT +0005: EOF ; 0:0 +``` + +## Runtime errors + +```plain +1:12: Some syntax error +``` + +# Test: Function syntax error pointing at second arg + +## Source + +```basic +OUT RAISEF("syntax1", 5) +``` + +## Disassembly + +```asm +0000: LOADI R67, 0 ; 1:12 +0001: LOADI R68, 5 ; 1:23 +0002: UPCALL 0, R66 ; 1:5, RAISEF +0003: MOVE R65, R66 ; 1:5 +0004: LOADI R64, 256 ; 1:5 +0005: UPCALL 1, R64 ; 1:1, OUT +0006: EOF ; 0:0 +``` + +## Runtime errors + +```plain +1:23: Some syntax error +``` + +# Test: Function syntax error with ON ERROR RESUME NEXT + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("syntax"): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: UPCALL 0, R66 ; 1:27, RAISEF +0003: MOVE R65, R66 ; 1:27 +0004: LOADI R64, 256 ; 1:27 +0005: UPCALL 1, R64 ; 1:23, OUT +0006: UPCALL 2, R65 ; 1:49, LAST_ERROR +0007: LOADI R64, 259 ; 1:49 +0008: UPCALL 1, R64 ; 1:45, OUT +0009: EOF ; 0:0 +``` + +## Output + +```plain +0=1:34: Some syntax error$ +``` + +# Test: Function syntax error at second arg with ON ERROR RESUME NEXT + +## Source + +```basic +ON ERROR RESUME NEXT: OUT RAISEF("syntax1", 5): OUT LAST_ERROR +``` + +## Disassembly + +```asm +0000: SETEH RESUME_NEXT, 0 ; 1:1 +0001: LOADI R67, 0 ; 1:34 +0002: LOADI R68, 5 ; 1:45 +0003: UPCALL 0, R66 ; 1:27, RAISEF +0004: MOVE R65, R66 ; 1:27 +0005: LOADI R64, 256 ; 1:27 +0006: UPCALL 1, R64 ; 1:23, OUT +0007: UPCALL 2, R65 ; 1:53, LAST_ERROR +0008: LOADI R64, 259 ; 1:53 +0009: UPCALL 1, R64 ; 1:49, OUT +0010: EOF ; 0:0 +``` + +## Output + +```plain +0=1:45: Some syntax error$ +``` diff --git a/core2/tests/testutils/callables/raise_cmd.rs b/core2/tests/testutils/callables/raise_cmd.rs index a83a4946..1cc1e955 100644 --- a/core2/tests/testutils/callables/raise_cmd.rs +++ b/core2/tests/testutils/callables/raise_cmd.rs @@ -21,6 +21,13 @@ use std::borrow::Cow; use std::rc::Rc; /// A command that raises an error based on a string argument. +/// +/// The first argument is a keyword that determines the error type. When the +/// keyword is `"syntax"`, `"syntax0"`, or `"syntax1"`, the error is a +/// `CallError::Syntax` with the position overridden to point at the string +/// argument, the first argument, or the second argument respectively. This +/// lets integration tests verify that the VM correctly propagates the position +/// override from `CallError::Syntax`. pub(super) struct RaiseCommand { metadata: Rc, } @@ -29,13 +36,37 @@ impl RaiseCommand { pub(super) fn new() -> Rc { Rc::from(Self { metadata: CallableMetadataBuilder::new("RAISE") - .with_syntax(&[( - &[SingularArgSyntax::RequiredValue( - RequiredValueSyntax { name: Cow::Borrowed("arg"), vtype: ExprType::Text }, - ArgSepSyntax::End, - )], - None, - )]) + .with_syntax(&[ + ( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Text, + }, + ArgSepSyntax::End, + )], + None, + ), + ( + &[ + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Text, + }, + ArgSepSyntax::Exactly(ArgSep::Long), + ), + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("n"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::End, + ), + ], + None, + ), + ]) .test_build(), }) } @@ -49,13 +80,15 @@ impl Callable for RaiseCommand { async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { let arg = scope.get_string(0); - let message = match arg { - "argument" => "Bad argument", - "eval" => "Some eval error", - "internal" => "Some internal error", - "io" => "Some I/O error", - _ => "Invalid arguments", - }; - Err(CallError::Other(message)) + match arg { + "argument" => Err(CallError::Other("Bad argument")), + "eval" => Err(CallError::Other("Some eval error")), + "internal" => Err(CallError::Other("Some internal error")), + "io" => Err(CallError::Other("Some I/O error")), + "syntax" => Err(CallError::Syntax(scope.get_pos(0), "Some syntax error".to_owned())), + "syntax0" => Err(CallError::Syntax(scope.get_pos(0), "Some syntax error".to_owned())), + "syntax1" => Err(CallError::Syntax(scope.get_pos(1), "Some syntax error".to_owned())), + _ => Err(CallError::Other("Invalid arguments")), + } } } diff --git a/core2/tests/testutils/callables/raisef_fn.rs b/core2/tests/testutils/callables/raisef_fn.rs index efda4261..eaa0ab87 100644 --- a/core2/tests/testutils/callables/raisef_fn.rs +++ b/core2/tests/testutils/callables/raisef_fn.rs @@ -21,6 +21,13 @@ use std::borrow::Cow; use std::rc::Rc; /// A function that raises an error based on a string argument. +/// +/// The first argument is a keyword that determines the error type. When the +/// keyword is `"syntax"`, `"syntax0"`, or `"syntax1"`, the error is a +/// `CallError::Syntax` with the position overridden to point at the string +/// argument, the first argument, or the second argument respectively. This +/// lets integration tests verify that the VM correctly propagates the position +/// override from `CallError::Syntax`. pub(super) struct RaisefFunction { metadata: Rc, } @@ -30,13 +37,37 @@ impl RaisefFunction { Rc::from(Self { metadata: CallableMetadataBuilder::new("RAISEF") .with_return_type(ExprType::Boolean) - .with_syntax(&[( - &[SingularArgSyntax::RequiredValue( - RequiredValueSyntax { name: Cow::Borrowed("arg"), vtype: ExprType::Text }, - ArgSepSyntax::End, - )], - None, - )]) + .with_syntax(&[ + ( + &[SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Text, + }, + ArgSepSyntax::End, + )], + None, + ), + ( + &[ + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("arg"), + vtype: ExprType::Text, + }, + ArgSepSyntax::Exactly(ArgSep::Long), + ), + SingularArgSyntax::RequiredValue( + RequiredValueSyntax { + name: Cow::Borrowed("n"), + vtype: ExprType::Integer, + }, + ArgSepSyntax::End, + ), + ], + None, + ), + ]) .test_build(), }) } @@ -50,13 +81,15 @@ impl Callable for RaisefFunction { async fn exec(&self, scope: Scope<'_>) -> CallResult<()> { let arg = scope.get_string(1); - let message = match arg { - "argument" => "Bad argument", - "eval" => "Some eval error", - "internal" => "Some internal error", - "io" => "Some I/O error", - _ => "Invalid arguments", - }; - Err(CallError::Other(message)) + match arg { + "argument" => Err(CallError::Other("Bad argument")), + "eval" => Err(CallError::Other("Some eval error")), + "internal" => Err(CallError::Other("Some internal error")), + "io" => Err(CallError::Other("Some I/O error")), + "syntax" => Err(CallError::Syntax(scope.get_pos(0), "Some syntax error".to_owned())), + "syntax0" => Err(CallError::Syntax(scope.get_pos(0), "Some syntax error".to_owned())), + "syntax1" => Err(CallError::Syntax(scope.get_pos(1), "Some syntax error".to_owned())), + _ => Err(CallError::Other("Invalid arguments")), + } } }