diff --git a/Compiler/Backend/LLVM/Tests/ArithmeticExecutionTests.cpp b/Compiler/Backend/LLVM/Tests/ArithmeticExecutionTests.cpp new file mode 100644 index 00000000..5e77ef65 --- /dev/null +++ b/Compiler/Backend/LLVM/Tests/ArithmeticExecutionTests.cpp @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 + +#include +#include +#include +#include +#include +#include + +#include "Visual/XSharp/Backend/LLVM.hpp" +#include "Visual/XSharp/Core/IR.hpp" +#include "Visual/XSharp/Core/Wire.hpp" +#include "Visual/XSharp/Pipeline.hpp" + +// Executable integer-division regressions. The existing backend tests check +// that the rounded-division helper appears in the generated IR; these run the +// code and compare each quotient and remainder with the value the language +// specification gives, for every sign combination, through both native +// optimizer settings. + +namespace +{ + namespace Core = Visual::XSharp::Core; + namespace Llvm = Visual::XSharp::Backend::LLVM; + namespace Pipeline = Visual::XSharp::Pipeline; + + /// `int left = ; int right = ; return left right;` + /// The operands are mutable locals so the operation is lowered as a + /// run-time instruction in the unoptimized pipeline. + [[nodiscard]] auto + Binary(Core::Primitive operation, std::int64_t left, std::int64_t right) + -> Core::Module + { + const auto local = + [](std::uint64_t id, std::u32string name, std::int64_t value) { + return Core::Statement::Bind( + { { id, std::move(name) }, + Core::Type::int64(), + true, + Core::Expression::Constant(value, Core::Type::int64()) }); + }; + const auto read = [](std::uint64_t id, std::u32string name) { + return Core::Expression::Variable({ id, std::move(name) }, + Core::Type::int64()); + }; + return { + { U"Arithmetic" }, + { Core::Function{ + { 1U, U"Evaluate" }, + {}, + Core::Type::int64(), + { local(2U, U"left", left), + local(3U, U"right", right), + Core::Statement::Return(Core::Expression::InvokePrimitive( + operation, + { read(2U, U"left"), read(3U, U"right") }, + Core::Type::int64())) }, + } } + }; + } + + [[nodiscard]] auto + Run(const Core::Module &module, bool optimize) -> std::int64_t + { + const auto encoded = Core::Wire::Encode(module); + REQUIRE(encoded); + Pipeline::Options options; + options.optimize_xpp = optimize; + options.optimize_xmm = optimize; + options.llvm.optimization = optimize ? Llvm::OptimizationLevel::Default + : Llvm::OptimizationLevel::Debug; + const auto pipeline = Pipeline::ConsumeCore(encoded.bytes, options); + REQUIRE(pipeline); + REQUIRE(pipeline.llvm); + + constexpr std::string_view kSymbol = "Arithmetic.Evaluate.1"; + Llvm::JitSession session; + REQUIRE_FALSE(session.AddModule(pipeline.llvm->bitcode, + "arithmetic-execution", + kSymbol, + Core::Type::int64())); + const auto result = session.InvokeScalar(kSymbol, Core::Type::int64()); + REQUIRE(result); + return std::get(result.value->payload); + } + + void + CheckBothPipelines(Core::Primitive operation, + std::int64_t left, + std::int64_t right, + std::int64_t expected) + { + CAPTURE(left, right); + const auto module = Binary(operation, left, right); + CHECK(Run(module, false) == expected); + CHECK(Run(module, true) == expected); + } + + /// The specification's rounded division: nearest integer, exact halves + /// away from zero. Written without the compiler's own formula. + [[nodiscard]] auto + RoundedQuotient(std::int64_t left, std::int64_t right) -> std::int64_t + { + const bool negative = (left < 0) != (right < 0); + const std::int64_t dividend = left < 0 ? -left : left; + const std::int64_t divisor = right < 0 ? -right : right; + const std::int64_t magnitude = (2 * dividend + divisor) / (2 * divisor); + return negative ? -magnitude : magnitude; + } +} // namespace + +TEST_CASE("rounded integer division matches the specification examples", + "[llvm][arithmetic][execution]") +{ + // Spec/Language/Operators.vxs, "Rounded Integer Division //". + CheckBothPipelines(Core::Primitive::FloorDivide, 7, 2, 4); + CheckBothPipelines(Core::Primitive::FloorDivide, 6, 2, 3); + CheckBothPipelines(Core::Primitive::FloorDivide, 5, 2, 3); + CheckBothPipelines(Core::Primitive::FloorDivide, -7, 2, -4); + CheckBothPipelines(Core::Primitive::FloorDivide, -5, 2, -3); + // Exact halves round away from zero. + CheckBothPipelines(Core::Primitive::FloorDivide, 1, 2, 1); + CheckBothPipelines(Core::Primitive::FloorDivide, -1, 2, -1); +} + +TEST_CASE("rounded integer division rounds to nearest for every sign pair", + "[llvm][arithmetic][execution]") +{ + for (std::int64_t left = -13; left <= 13; ++left) + for (std::int64_t right = -5; right <= 5; ++right) + if (right != 0) + CheckBothPipelines(Core::Primitive::FloorDivide, + left, + right, + RoundedQuotient(left, right)); +} + +TEST_CASE("remainder keeps the sign of its left operand", + "[llvm][arithmetic][execution]") +{ + // Spec/Language/Operators.vxs, "Remainder %". + CheckBothPipelines(Core::Primitive::Remainder, 7, 3, 1); + CheckBothPipelines(Core::Primitive::Remainder, -7, 3, -1); + CheckBothPipelines(Core::Primitive::Remainder, 7, -3, 1); + for (std::int64_t left = -9; left <= 9; ++left) + for (std::int64_t right = -4; right <= 4; ++right) + if (right != 0) + CheckBothPipelines(Core::Primitive::Remainder, + left, + right, + left % right); +} diff --git a/Compiler/Backend/LLVM/Tests/BUILD.bazel b/Compiler/Backend/LLVM/Tests/BUILD.bazel index 59670458..ac678dee 100644 --- a/Compiler/Backend/LLVM/Tests/BUILD.bazel +++ b/Compiler/Backend/LLVM/Tests/BUILD.bazel @@ -6,6 +6,7 @@ cc_binary( name = "llvm_backend_tests", srcs = [ "CallableInvocationTests.cpp", + "ArithmeticExecutionTests.cpp", "ConditionalExecutionTests.cpp", "LLVMBackendTests.cpp", "JitSessionTests.cpp", diff --git a/Compiler/Backend/LLVM/Tests/LoopExecutionTests.cpp b/Compiler/Backend/LLVM/Tests/LoopExecutionTests.cpp index 3f1e50a1..f0630d57 100644 --- a/Compiler/Backend/LLVM/Tests/LoopExecutionTests.cpp +++ b/Compiler/Backend/LLVM/Tests/LoopExecutionTests.cpp @@ -398,3 +398,128 @@ TEST_CASE("nested loops transfer only within their own loop", CheckBothPipelines(module, expected); } } + +TEST_CASE("return inside a loop leaves the function with the current value", + "[llvm][loop][execution]") +{ + // for (index = 0; index < limit; index++) { + // if (index == 4) { return total + 1000; } + // total = total + index; + // } + // return total; + for (std::int64_t limit = 0; limit < kLimits; ++limit) + { + const auto host = [limit]() -> std::int64_t { + std::int64_t total{}; + for (std::int64_t index = 0; index < limit; ++index) + { + if (index == 4) + return total + 1000; + total += index; + } + return total; + }; + CAPTURE(limit); + CheckBothPipelines( + LoopModule(Core::Statement::For( + Compare(Core::Primitive::LessThan, kIndex, limit), + { Core::Statement::If( + Compare(Core::Primitive::Equal, kIndex, 4), + { Core::Statement::Return( + Core::Expression::InvokePrimitive( + Core::Primitive::Add, + { Variable(kTotal), Integer(1000) }, + Core::Type::int64())) }, + {}), + Add(kTotal, Variable(kIndex)) }, + { Increment(kIndex) })), + host()); + } +} + +TEST_CASE("return from a nested loop skips every enclosing update", + "[llvm][loop][execution]") +{ + // The inner loop returns from inside a while that sits in a for body; + // neither the inner increment nor the outer update may run afterwards. + for (std::int64_t limit = 0; limit < 7; ++limit) + { + const auto host = [limit]() -> std::int64_t { + std::int64_t total{}; + for (std::int64_t index = 0; index < limit; ++index) + { + std::int64_t inner{}; + while (inner < 3) + { + if (index == 2 && inner == 1) + return total * 10 + index; + total += inner; + ++inner; + } + } + return total; + }; + auto reset + = Core::Statement::Assign({ kInner, Spelling(kInner) }, Integer(0)); + auto module = LoopModule(Core::Statement::For( + Compare(Core::Primitive::LessThan, kIndex, limit), + { reset, + Core::Statement::While( + Compare(Core::Primitive::LessThan, kInner, 3), + { Core::Statement::If( + Compare(Core::Primitive::Equal, kIndex, 2), + { When(kInner, + 1, + Core::Statement::Return( + Core::Expression::InvokePrimitive( + Core::Primitive::Add, + { Core::Expression::InvokePrimitive( + Core::Primitive::Multiply, + { Variable(kTotal), Integer(10) }, + Core::Type::int64()), + Variable(kIndex) }, + Core::Type::int64()))) }, + {}), + Add(kTotal, Variable(kInner)), + Increment(kInner) }) }, + { Increment(kIndex) })); + auto &body = module.functions.front().body; + body.insert(body.begin() + 2, Declare(kInner)); + CAPTURE(limit); + CheckBothPipelines(module, host()); + } +} + +TEST_CASE("do-while returns from its body before the trailing test", + "[llvm][loop][execution]") +{ + for (std::int64_t limit = 0; limit < kLimits; ++limit) + { + const auto host = [limit]() -> std::int64_t { + std::int64_t total{}; + std::int64_t index{}; + do + { + ++index; + if (index == 6) + return -total; + total += index; + } while (index < limit); + return total; + }; + CAPTURE(limit); + CheckBothPipelines( + LoopModule(Core::Statement::DoWhile( + { Increment(kIndex), + When( + kIndex, + 6, + Core::Statement::Return(Core::Expression::InvokePrimitive( + Core::Primitive::Negate, + { Variable(kTotal) }, + Core::Type::int64()))), + Add(kTotal, Variable(kIndex)) }, + Compare(Core::Primitive::LessThan, kIndex, limit))), + host()); + } +} diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs index b7c00790..6eaac169 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs @@ -46,7 +46,9 @@ data CorePrimitive CoreMultiply | -- | Truncating division. CoreDivide - | -- | Floor division. + | {- | Rounded division @//@: nearest integer, halves away from zero. The + constructor keeps its historical name; it never floors. + -} CoreFloorDivide | -- | Remainder operation. CoreRemainder diff --git a/Compiler/Headers/Visual/XSharp/Core/CorePrep.hpp b/Compiler/Headers/Visual/XSharp/Core/CorePrep.hpp index 3942b675..8006462f 100644 --- a/Compiler/Headers/Visual/XSharp/Core/CorePrep.hpp +++ b/Compiler/Headers/Visual/XSharp/Core/CorePrep.hpp @@ -528,13 +528,15 @@ namespace visual_xsharp::core /// Operation encoded by a CorePrep instruction. enum class Operation : std::uint8_t { - Copy, ///< Copy one operand into the destination. - Call, ///< Call the function supplied as the first operand. - Add, ///< Arithmetic addition. - Subtract, ///< Arithmetic subtraction. - Multiply, ///< Arithmetic multiplication. - Divide, ///< Truncating arithmetic division. - FloorDivide, ///< Floor-rounded arithmetic division. + Copy, ///< Copy one operand into the destination. + Call, ///< Call the function supplied as the first operand. + Add, ///< Arithmetic addition. + Subtract, ///< Arithmetic subtraction. + Multiply, ///< Arithmetic multiplication. + Divide, ///< Truncating arithmetic division. + /// Rounded division `//`: nearest integer, halves away from zero. + /// The enumerator keeps its historical name; it never floors. + FloorDivide, Remainder, ///< Arithmetic remainder. LessThan, ///< Ordered less-than comparison. LessEqual, ///< Ordered less-than-or-equal comparison. diff --git a/Compiler/Headers/Visual/XSharp/Core/IR.hpp b/Compiler/Headers/Visual/XSharp/Core/IR.hpp index 92eee59f..259c13c7 100644 --- a/Compiler/Headers/Visual/XSharp/Core/IR.hpp +++ b/Compiler/Headers/Visual/XSharp/Core/IR.hpp @@ -25,11 +25,13 @@ namespace Visual::XSharp::Core /// Built-in operation represented directly in a typed Core expression. enum class Primitive : std::uint8_t { - Add, ///< Arithmetic addition. - Subtract, ///< Arithmetic subtraction. - Multiply, ///< Arithmetic multiplication. - Divide, ///< Truncating arithmetic division. - FloorDivide, ///< Floor-rounded arithmetic division. + Add, ///< Arithmetic addition. + Subtract, ///< Arithmetic subtraction. + Multiply, ///< Arithmetic multiplication. + Divide, ///< Truncating arithmetic division. + /// Rounded division `//`: nearest integer, halves away from zero. + /// The enumerator keeps its historical name; it never floors. + FloorDivide, Remainder, ///< Arithmetic remainder. LessThan, ///< Ordered less-than comparison. LessEqual, ///< Ordered less-than-or-equal comparison. diff --git a/Compiler/Headers/Visual/XSharp/Xmm/IR.hpp b/Compiler/Headers/Visual/XSharp/Xmm/IR.hpp index cd47904e..7154a202 100644 --- a/Compiler/Headers/Visual/XSharp/Xmm/IR.hpp +++ b/Compiler/Headers/Visual/XSharp/Xmm/IR.hpp @@ -18,18 +18,20 @@ namespace visual_xsharp::xmm /// Typed operation encoded by an Xmm instruction. enum class Opcode : std::uint8_t { - LoadImmediate, ///< Materialize a constant into a virtual register. - Move, ///< Copy a value into the destination register. - Call, ///< Invoke a function value or direct function. - Add, ///< Arithmetic addition. - Subtract, ///< Arithmetic subtraction. - Multiply, ///< Arithmetic multiplication. - Divide, ///< Truncating arithmetic division. - FloorDivide, ///< Floor-rounded arithmetic division. - Remainder, ///< Arithmetic remainder. - CompareLess, ///< Ordered less-than comparison. - CompareLessEqual, ///< Ordered less-than-or-equal comparison. - CompareGreater, ///< Ordered greater-than comparison. + LoadImmediate, ///< Materialize a constant into a virtual register. + Move, ///< Copy a value into the destination register. + Call, ///< Invoke a function value or direct function. + Add, ///< Arithmetic addition. + Subtract, ///< Arithmetic subtraction. + Multiply, ///< Arithmetic multiplication. + Divide, ///< Truncating arithmetic division. + /// Rounded division `//`: nearest integer, halves away from zero. + /// The enumerator keeps its historical name; it never floors. + FloorDivide, + Remainder, ///< Arithmetic remainder. + CompareLess, ///< Ordered less-than comparison. + CompareLessEqual, ///< Ordered less-than-or-equal comparison. + CompareGreater, ///< Ordered greater-than comparison. CompareGreaterEqual, ///< Ordered greater-than-or-equal comparison. CompareEqual, ///< Value equality comparison. CompareNotEqual, ///< Value inequality comparison. diff --git a/Compiler/Headers/Visual/XSharp/Xpp/IR.hpp b/Compiler/Headers/Visual/XSharp/Xpp/IR.hpp index 0fe756f7..8a399635 100644 --- a/Compiler/Headers/Visual/XSharp/Xpp/IR.hpp +++ b/Compiler/Headers/Visual/XSharp/Xpp/IR.hpp @@ -18,13 +18,15 @@ namespace visual_xsharp::xpp /// Typed operation encoded by an Xpp instruction. enum class Opcode : std::uint8_t { - Copy, ///< Copy an operand to the destination. - Call, ///< Invoke the function in the first operand. - Add, ///< Arithmetic addition. - Subtract, ///< Arithmetic subtraction. - Multiply, ///< Arithmetic multiplication. - Divide, ///< Truncating division. - FloorDivide, ///< Floor-rounded division. + Copy, ///< Copy an operand to the destination. + Call, ///< Invoke the function in the first operand. + Add, ///< Arithmetic addition. + Subtract, ///< Arithmetic subtraction. + Multiply, ///< Arithmetic multiplication. + Divide, ///< Truncating division. + /// Rounded division `//`: nearest integer, halves away from zero. + /// The enumerator keeps its historical name; it never floors. + FloorDivide, Remainder, ///< Arithmetic remainder. CompareLess, ///< Ordered less-than comparison. CompareLessEqual, ///< Ordered less-than-or-equal comparison. diff --git a/Documents/CORE-IR.md b/Documents/CORE-IR.md index 303848cd..de0d26b4 100644 --- a/Documents/CORE-IR.md +++ b/Documents/CORE-IR.md @@ -93,7 +93,7 @@ second argument of `System.Array` as a type would make specialization identity unsound and prevent `[T; N]` from reaching Core. Concrete fixed-array size expressions are evaluated exactly by TypeChecker. -Host integer width is irrelevant. Division, floor division, and remainder by +Host integer width is irrelevant. Division, rounded division `//`, and remainder by zero are diagnosed; a negative or non-integer fixed size is rejected. Calls, closures, strings, and floating values cannot enter fixed-array type syntax as compile-time sizes. diff --git a/Documents/INTEGER-FLOW-ANALYSIS.md b/Documents/INTEGER-FLOW-ANALYSIS.md index c96fe694..ccfad00f 100644 --- a/Documents/INTEGER-FLOW-ANALYSIS.md +++ b/Documents/INTEGER-FLOW-ANALYSIS.md @@ -530,7 +530,7 @@ statistical performance gates. ## Effect inference and dead-code elimination -Integer division, floor division, and remainder over integer types can fail +Integer division, rounded division `//`, and remainder over integer types can fail when the divisor is zero. Without a proof, their local effect remains `FailureEffect`; a dead result must therefore remain observable. A literal nonzero divisor is immediately safe. A variable divisor is safe only when the diff --git a/Documents/LLVM-BACKEND.md b/Documents/LLVM-BACKEND.md index 3766c811..e585f1f6 100644 --- a/Documents/LLVM-BACKEND.md +++ b/Documents/LLVM-BACKEND.md @@ -52,8 +52,12 @@ exists. Every branch and jump target must belong to the current function. Branch conditions must be `Bool`; returns must match the declared function result. Calls resolve by stable symbol identity and must match their complete function signature. -Integer division uses signed LLVM division. Floor division additionally adjusts a truncated quotient when the remainder is -nonzero and operand signs differ, preserving mathematical floor semantics for negative operands. +Integer division uses signed LLVM division. Rounded division `//`, which the IR names `FloorDivide` for historical reasons, +rounds to the nearest integer with exact halves away from zero, as the specification requires: `7 // 2` is `4` and +`-7 // 2` is `-4`. The backend adjusts the truncated quotient by one, away from zero, when twice the remainder's magnitude +reaches the divisor's magnitude. It does not floor: `1 // 2` is `1`, not `0`. +`Compiler/Backend/LLVM/Tests/ArithmeticExecutionTests.cpp` executes the specification examples and every sign +combination of small operands through both native optimizer settings. Numeric boolean context is normalized before Xpp. A branch therefore receives a canonical boolean value rather than asking LLVM to reinterpret every integer width differently. This keeps source semantics out of the backend and lets the Xmm diff --git a/Documents/SCALAR-PIPELINE.md b/Documents/SCALAR-PIPELINE.md index 1df30a7a..f2f561c1 100644 --- a/Documents/SCALAR-PIPELINE.md +++ b/Documents/SCALAR-PIPELINE.md @@ -213,9 +213,9 @@ Instruction selection depends on the scalar family: | equality | `icmp` | `icmp` | ordered `fcmp` | | rounded division | nearest quotient, halves away from zero | nearest quotient, halves upward | `round(fdiv)` then signed `int` conversion | -Floor division for signed integers corrects truncation when a nonzero -remainder and opposite operand signs require rounding toward negative -zero. Signed and unsigned integer lowering compares the exact remainder against +Rounded division `//` (named `FloorDivide` in the IR for historical +reasons) rounds to the nearest integer with halves away from zero; it does +not floor. Signed and unsigned integer lowering compares the exact remainder against half the divisor without converting through floating point. Floating rounded division calls the LLVM round intrinsic on the quotient and converts that rounded value to the signed 64-bit `int` result.