diff --git a/CHANGELOG.md b/CHANGELOG.md index 19b7f521..570c985d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 # Changelog +## Unreleased + +### Language + +- Added the conditional expression `condition ? first : second` and truthy + coalescing `left ?: fallback`. Exactly one result is evaluated, and the left + operand of `?:` is evaluated once. Results are limited to `bool` and numeric + types for now. +- Added the compound assignment statements `+=`, `-=`, `*=`, `/=`, `//=`, `%=`, + `**=`, `<<=`, `>>=`, `&=`, `^=`, and `|=`, including in `for` update lists, + and the discard statement `_ = value;`. +- `??` and `??=` are now recognized and rejected with dedicated diagnostics + until nullable types exist. + +### Compiler pipeline + +- Added the lazy `CoreConditional` expression to the Haskell and native Core + models, verifiers, and wire codecs. The Core wire format is now v8; CorePrep, + Xpp, and Xmm are unchanged because the expression is lowered to control flow + before CorePrep. +- Taught effect inference, integer facts, constant folding, liveness, and + inlining to treat the two arms as alternative paths. +- The native Core-to-CorePrep adapter now binds a `let` value with its own + operation, matching the Haskell adapter. + ## 0.4.0 - 2026-09-27 ### Compiler pipeline diff --git a/Compiler/Backend/LLVM/Tests/BUILD.bazel b/Compiler/Backend/LLVM/Tests/BUILD.bazel index 4853bfcc..59670458 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", + "ConditionalExecutionTests.cpp", "LLVMBackendTests.cpp", "JitSessionTests.cpp", "LoopExecutionTests.cpp", diff --git a/Compiler/Backend/LLVM/Tests/ConditionalExecutionTests.cpp b/Compiler/Backend/LLVM/Tests/ConditionalExecutionTests.cpp new file mode 100644 index 00000000..2943642e --- /dev/null +++ b/Compiler/Backend/LLVM/Tests/ConditionalExecutionTests.cpp @@ -0,0 +1,627 @@ +// 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 conditional-expression regressions. Each program is compiled +// from Core through Xpp, Xmm and LLVM, run by the ORC JIT, and compared with +// a value the test computes itself. Where it matters, one arm is only well +// defined when the test selects it: a division whose divisor the test +// excludes, or a recursive call the test terminates. A pipeline that +// evaluates both arms traps or never returns, so these programs observe the +// laziness of the conditional rather than only the value it selects. + +namespace +{ + namespace Core = Visual::XSharp::Core; + namespace Llvm = Visual::XSharp::Backend::LLVM; + namespace Pipeline = Visual::XSharp::Pipeline; + + constexpr std::uint64_t kValue = 2U; + constexpr std::uint64_t kTotal = 3U; + constexpr std::uint64_t kLocal = 4U; + constexpr std::uint64_t kSum = 10U; + constexpr std::uint64_t kSumParameter = 11U; + + [[nodiscard]] auto + Integer(std::int64_t value) -> Core::Expression + { + return Core::Expression::Constant(value, Core::Type::int64()); + } + + [[nodiscard]] auto + Boolean(bool value) -> Core::Expression + { + return Core::Expression::Constant(value, Core::Type::boolean()); + } + + [[nodiscard]] auto + Floating(std::string spelling) -> Core::Expression + { + return Core::Expression::Constant( + visual_xsharp::core::FloatingLiteral{ std::move(spelling) }, + Core::Type::float64()); + } + + [[nodiscard]] auto + Variable(std::uint64_t id, + std::u32string spelling, + Core::Type type = Core::Type::int64()) -> Core::Expression + { + return Core::Expression::Variable({ id, std::move(spelling) }, + std::move(type)); + } + + [[nodiscard]] auto + Value() -> Core::Expression + { + return Variable(kValue, U"value"); + } + + [[nodiscard]] auto + Total() -> Core::Expression + { + return Variable(kTotal, U"total"); + } + + [[nodiscard]] auto + Binary(Core::Primitive operation, + Core::Expression left, + Core::Expression right, + Core::Type type = Core::Type::int64()) -> Core::Expression + { + return Core::Expression::InvokePrimitive( + operation, + { std::move(left), std::move(right) }, + std::move(type)); + } + + [[nodiscard]] auto + Compare(Core::Primitive operation, + Core::Expression left, + Core::Expression right) -> Core::Expression + { + return Binary(operation, + std::move(left), + std::move(right), + Core::Type::boolean()); + } + + [[nodiscard]] auto + Choose(Core::Expression test, + Core::Expression whenTrue, + Core::Expression whenFalse, + Core::Type type = Core::Type::int64()) -> Core::Expression + { + return Core::Expression::Conditional(std::move(test), + std::move(whenTrue), + std::move(whenFalse), + std::move(type)); + } + + [[nodiscard]] auto + SetTotal(Core::Expression value) -> Core::Statement + { + return Core::Statement::Assign({ kTotal, U"total" }, std::move(value)); + } + + /// `int value = ; int total = 0; ; return total;` + [[nodiscard]] auto + Evaluate(std::int64_t input, std::vector statements) + -> Core::Function + { + std::vector body{ + Core::Statement::Bind({ { kValue, U"value" }, + Core::Type::int64(), + true, + Integer(input) }), + Core::Statement::Bind( + { { kTotal, U"total" }, Core::Type::int64(), true, Integer(0) }) + }; + body.insert(body.end(), + std::make_move_iterator(statements.begin()), + std::make_move_iterator(statements.end())); + body.push_back(Core::Statement::Return(Total())); + return { { 1U, U"Evaluate" }, + {}, + Core::Type::int64(), + std::move(body) }; + } + + [[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 = "Conditional.Evaluate.1"; + Llvm::JitSession session; + const auto rejected = session.AddModule(pipeline.llvm->bitcode, + "conditional-execution", + kSymbol, + Core::Type::int64()); + REQUIRE_FALSE(rejected); + const auto result = session.InvokeScalar(kSymbol, Core::Type::int64()); + REQUIRE(result); + return std::get(result.value->payload); + } + + void + CheckBothPipelines(std::vector functions, + std::int64_t expected) + { + const Core::Module module{ { U"Conditional" }, std::move(functions) }; + CHECK(Run(module, false) == expected); + CHECK(Run(module, true) == expected); + } +} // namespace + +TEST_CASE("a conditional does not evaluate the division its test excludes", + "[llvm][conditional][execution]") +{ + for (std::int64_t input = -3; input <= 3; ++input) + { + CAPTURE(input); + SECTION("the division is the first result") + { + const std::int64_t expected = input != 0 ? 12 / input : 99; + CheckBothPipelines( + { Evaluate( + input, + { SetTotal(Choose( + Compare(Core::Primitive::NotEqual, Value(), Integer(0)), + Binary(Core::Primitive::Divide, Integer(12), Value()), + Integer(99))) }) }, + expected); + } + SECTION("the division is the second result") + { + const std::int64_t expected = input == 0 ? 99 : 12 / input; + CheckBothPipelines( + { Evaluate( + input, + { SetTotal(Choose( + Compare(Core::Primitive::Equal, Value(), Integer(0)), + Integer(99), + Binary(Core::Primitive::Divide, + Integer(12), + Value()))) }) }, + expected); + } + } +} + +TEST_CASE("a numeric conditional test selects by truth", + "[llvm][conditional][execution]") +{ + for (std::int64_t input = -2; input <= 2; ++input) + { + CAPTURE(input); + CheckBothPipelines( + { Evaluate(input, + { SetTotal(Choose(Value(), Integer(5), Integer(9))) }) }, + input != 0 ? 5 : 9); + } +} + +TEST_CASE("chained conditionals select exactly one leaf", + "[llvm][conditional][execution]") +{ + // value < 0 ? (value < -1 ? 1 : 2) : value == 0 ? 3 : value > 1 ? 4 : 5 + const auto classify = [] { + return Choose( + Compare(Core::Primitive::LessThan, Value(), Integer(0)), + Choose(Compare(Core::Primitive::LessThan, Value(), Integer(-1)), + Integer(1), + Integer(2)), + Choose( + Compare(Core::Primitive::Equal, Value(), Integer(0)), + Integer(3), + Choose( + Compare(Core::Primitive::GreaterThan, Value(), Integer(1)), + Integer(4), + Integer(5)))); + }; + for (std::int64_t input = -3; input <= 3; ++input) + { + const std::int64_t expected = input < 0 ? (input < -1 ? 1 : 2) + : input == 0 ? 3 + : input > 1 ? 4 + : 5; + CAPTURE(input); + CheckBothPipelines({ Evaluate(input, { SetTotal(classify()) }) }, + expected); + } +} + +TEST_CASE("a conditional test may itself be a conditional", + "[llvm][conditional][execution]") +{ + // (value > 0 ? value < 3 : value < -2) ? 7 : 11 + for (std::int64_t input = -4; input <= 4; ++input) + { + const bool test = input > 0 ? input < 3 : input < -2; + CAPTURE(input); + CheckBothPipelines( + { Evaluate( + input, + { SetTotal(Choose( + Choose( + Compare(Core::Primitive::GreaterThan, + Value(), + Integer(0)), + Compare(Core::Primitive::LessThan, Value(), Integer(3)), + Compare(Core::Primitive::LessThan, + Value(), + Integer(-2)), + Core::Type::boolean()), + Integer(7), + Integer(11))) }) }, + test ? 7 : 11); + } +} + +TEST_CASE("a conditional is an ordinary operand after its join", + "[llvm][conditional][execution]") +{ + // total = value * 10 + (value > 1 ? value : 0 - value) - 1 + for (std::int64_t input = -2; input <= 4; ++input) + { + const std::int64_t expected + = input * 10 + (input > 1 ? input : 0 - input) - 1; + CAPTURE(input); + CheckBothPipelines( + { Evaluate( + input, + { SetTotal(Binary( + Core::Primitive::Subtract, + Binary( + Core::Primitive::Add, + Binary(Core::Primitive::Multiply, Value(), Integer(10)), + Choose(Compare(Core::Primitive::GreaterThan, + Value(), + Integer(1)), + Value(), + Binary(Core::Primitive::Subtract, + Integer(0), + Value()))), + Integer(1))) }) }, + expected); + } +} + +TEST_CASE("a Boolean conditional value survives later statements", + "[llvm][conditional][execution]") +{ + // The value, not only a branch on it, must be the selected arm: bind + // it, overwrite an unrelated variable, then test the binding. + for (std::int64_t input = -2; input <= 2; ++input) + { + const bool flag = input != 0 ? 8 / input == 4 : true; + CAPTURE(input); + CheckBothPipelines( + { Evaluate(input, + { Core::Statement::Bind( + { { kLocal, U"flag" }, + Core::Type::boolean(), + false, + Choose(Compare(Core::Primitive::NotEqual, + Value(), + Integer(0)), + Compare(Core::Primitive::Equal, + Binary(Core::Primitive::Divide, + Integer(8), + Value()), + Integer(4)), + Boolean(true), + Core::Type::boolean()) }), + SetTotal(Integer(5)), + Core::Statement::If( + Variable(kLocal, U"flag", Core::Type::boolean()), + { SetTotal(Integer(7)) }, + {}) }) }, + flag ? 7 : 5); + } +} + +TEST_CASE("a floating conditional keeps the selected value", + "[llvm][conditional][execution]") +{ + // double ratio = value > 0 ? 1.5 : -2.5; + // total = ratio > 0.0 ? 1 : ratio < -2.0 ? 2 : 3; + for (std::int64_t input = -1; input <= 1; ++input) + { + const double ratio = input > 0 ? 1.5 : -2.5; + const std::int64_t expected = ratio > 0.0 ? 1 : ratio < -2.0 ? 2 : 3; + const auto ratioRead = [] { + return Variable(kLocal, U"ratio", Core::Type::float64()); + }; + CAPTURE(input); + CheckBothPipelines( + { Evaluate( + input, + { Core::Statement::Bind( + { { kLocal, U"ratio" }, + Core::Type::float64(), + false, + Choose(Compare(Core::Primitive::GreaterThan, + Value(), + Integer(0)), + Floating("1.5"), + Floating("-2.5"), + Core::Type::float64()) }), + SetTotal(Choose(Compare(Core::Primitive::GreaterThan, + ratioRead(), + Floating("0.0")), + Integer(1), + Choose(Compare(Core::Primitive::LessThan, + ratioRead(), + Floating("-2.0")), + Integer(2), + Integer(3)))) }) }, + expected); + } +} + +TEST_CASE("guarded recursion terminates through a conditional", + "[llvm][conditional][execution]") +{ + // int Sum(int n) { return n == 0 ? 0 : n + Sum(n - 1); } + // Evaluating the second result eagerly recurses below zero without + // bound. + const auto sumType + = Core::Type::function({ Core::Type::int64() }, Core::Type::int64()); + const auto parameter = [] { + return Variable(kSumParameter, U"n"); + }; + const Core::Function sum{ + { kSum, U"Sum" }, + { { { kSumParameter, U"n" }, Core::Type::int64() } }, + Core::Type::int64(), + { Core::Statement::Return(Choose( + Compare(Core::Primitive::Equal, parameter(), Integer(0)), + Integer(0), + Binary(Core::Primitive::Add, + parameter(), + Core::Expression::Apply( + Core::Expression::Variable({ kSum, U"Sum" }, sumType), + { Binary(Core::Primitive::Subtract, + parameter(), + Integer(1)) }, + Core::Type::int64())))) }, + }; + for (std::int64_t input = 0; input <= 8; ++input) + { + CAPTURE(input); + CheckBothPipelines( + { sum, + Evaluate( + input, + { SetTotal(Core::Expression::Apply( + Core::Expression::Variable({ kSum, U"Sum" }, sumType), + { Value() }, + Core::Type::int64())) }) }, + input * (input + 1) / 2); + } +} + +TEST_CASE("a conditional loop condition is re-evaluated on every iteration", + "[llvm][conditional][execution]") +{ + // while (value > 3 ? value < 10 : value < 2) { value++; total++; } + for (std::int64_t input = -1; input <= 6; ++input) + { + std::int64_t value = input; + std::int64_t expected = 0; + while (value > 3 ? value < 10 : value < 2) + { + ++value; + ++expected; + } + CAPTURE(input); + CheckBothPipelines( + { Evaluate( + input, + { Core::Statement::While( + Choose( + Compare(Core::Primitive::GreaterThan, + Value(), + Integer(3)), + Compare(Core::Primitive::LessThan, + Value(), + Integer(10)), + Compare(Core::Primitive::LessThan, Value(), Integer(2)), + Core::Type::boolean()), + { Core::Statement::Assign( + { kValue, U"value" }, + Binary(Core::Primitive::Add, Value(), Integer(1))), + SetTotal(Binary(Core::Primitive::Add, + Total(), + Integer(1))) }) }) }, + expected); + } +} + +TEST_CASE("the truthy-coalescing lowering keeps a nonzero left value", + "[llvm][conditional][execution]") +{ + // `left ?: fallback` reaches Core as + // let subject = left in (subject ? subject : fallback) + // The fallback divides by `3 - value`. It is selected only at value == 2, + // where the divisor is one; at value == 3 it is undefined and must not + // be evaluated. + for (std::int64_t input = 0; input <= 5; ++input) + { + const std::int64_t left = input - 2; + const std::int64_t expected = left != 0 ? left : 12 / (3 - input); + const auto subject = [] { + return Variable(kLocal, U"$coalesce4"); + }; + CAPTURE(input); + CheckBothPipelines( + { Evaluate( + input, + { SetTotal(Core::Expression::Let( + { kLocal, U"$coalesce4" }, + Core::Type::int64(), + Binary(Core::Primitive::Subtract, Value(), Integer(2)), + Choose(subject(), + subject(), + Binary(Core::Primitive::Divide, + Integer(12), + Binary(Core::Primitive::Subtract, + Integer(3), + Value()))), + Core::Type::int64())) }) }, + expected); + } +} + +TEST_CASE("compound assignment lowering reads the target it writes", + "[llvm][conditional][execution]") +{ + // The frontend lowers `total op= operand` to `total = total op operand`. + // Run one of each integer operator over the same accumulator. + struct Step final + { + Core::Primitive operation; + std::int64_t operand; + }; + const std::vector steps{ + { Core::Primitive::Add, 9 }, + { Core::Primitive::Multiply, 6 }, + { Core::Primitive::Subtract, 4 }, + { Core::Primitive::ShiftLeft, 3 }, + { Core::Primitive::BitwiseXor, 21 }, + { Core::Primitive::BitwiseOr, 64 }, + { Core::Primitive::BitwiseAnd, 1023 }, + { Core::Primitive::ShiftRight, 1 }, + { Core::Primitive::Remainder, 37 }, + { Core::Primitive::Divide, 2 }, + }; + for (std::int64_t input = 1; input <= 4; ++input) + { + std::int64_t expected = input; + std::vector statements{ SetTotal(Value()) }; + for (const auto &step : steps) + { + switch (step.operation) + { + case Core::Primitive::Add: + expected += step.operand; + break; + case Core::Primitive::Multiply: + expected *= step.operand; + break; + case Core::Primitive::Subtract: + expected -= step.operand; + break; + case Core::Primitive::ShiftLeft: + expected <<= step.operand; + break; + case Core::Primitive::BitwiseXor: + expected ^= step.operand; + break; + case Core::Primitive::BitwiseOr: + expected |= step.operand; + break; + case Core::Primitive::BitwiseAnd: + expected &= step.operand; + break; + case Core::Primitive::ShiftRight: + expected >>= step.operand; + break; + case Core::Primitive::Remainder: + expected %= step.operand; + break; + default: + expected /= step.operand; + break; + } + statements.push_back(SetTotal( + Binary(step.operation, Total(), Integer(step.operand)))); + } + // Every intermediate value is non-negative, so the host operators + // above agree with the language's truncating and logical rules. + REQUIRE(expected >= 0); + CAPTURE(input); + CheckBothPipelines({ Evaluate(input, std::move(statements)) }, + expected); + } +} + +TEST_CASE("a discarded non-call value lowers through every stage", + "[llvm][conditional][execution]") +{ + // `_ = value;` evaluates an arbitrary expression and drops the result. + // Later stages accept a dropped result only from a call or a closure + // creation, so any other value has to be computed into a temporary. + // Each program below was rejected by the Xmm verifier before that. + const auto discard = [](Core::Expression value) { + return Core::Statement::Evaluate(std::move(value)); + }; + SECTION("an arithmetic primitive") + { + CheckBothPipelines({ Evaluate(4, + { discard(Binary(Core::Primitive::Divide, + Integer(12), + Value())), + SetTotal(Integer(3)) }) }, + 3); + } + SECTION("a conditional expression") + { + CheckBothPipelines( + { Evaluate( + 4, + { discard(Choose( + Compare(Core::Primitive::GreaterThan, + Value(), + Integer(1)), + Binary(Core::Primitive::Divide, Integer(12), Value()), + Integer(0))), + SetTotal(Integer(5)) }) }, + 5); + } + SECTION("a plain variable and a literal") + { + CheckBothPipelines({ Evaluate(4, + { discard(Value()), + discard(Integer(9)), + SetTotal(Integer(7)) }) }, + 7); + } + SECTION("a comparison of a let-bound value") + { + CheckBothPipelines( + { Evaluate(4, + { discard(Core::Expression::Let( + { kLocal, U"subject" }, + Core::Type::int64(), + Binary(Core::Primitive::Add, Value(), Integer(1)), + Compare(Core::Primitive::GreaterThan, + Variable(kLocal, U"subject"), + Integer(3)), + Core::Type::boolean())), + SetTotal(Integer(11)) }) }, + 11); + } +} diff --git a/Compiler/Core/CorePrep/Prepare.cpp b/Compiler/Core/CorePrep/Prepare.cpp index af0078a8..417dac26 100644 --- a/Compiler/Core/CorePrep/Prepare.cpp +++ b/Compiler/Core/CorePrep/Prepare.cpp @@ -295,6 +295,74 @@ namespace Visual::XSharp::Core::CorePrep return Prepared::Atom::variable(std::move(result), Type::boolean()); } + [[nodiscard]] auto + IsConditional(const Expression &expression) -> bool + { + return expression.kind == Expression::Kind::Conditional + && expression.operands.size() == 3U; + } + + /** + * @brief Lower a conditional expression to a branch over a slot. + * + * Exactly one arm is evaluated. The result slot is bound before + * the branch with the neutral literal of its type and each arm + * overwrites it on its own path, so the join block reads an + * initialized value from either predecessor without a phi node. + * The neutral value is never observable: no path reaches the join + * without passing through one of the two assignments. + * + * The symbol and block allocation order matches the Haskell + * CorePrep lowering. + */ + [[nodiscard]] auto + AtomizeConditional(Cursor &cursor, const Expression &expression) + -> Prepared::Atom + { + auto condition + = Booleanize(cursor, Atomize(cursor, expression.operands[0])); + auto result = cursor.Temporary(U"$conditional"); + cursor.Emit(Prepared::Instruction{ + Prepared::Instruction::Kind::Bind, + result, + expression.type, + true, + Prepared::Operation::Copy, + { expression.type == Type::boolean() + ? Prepared::Atom::constant(Prepared::Literal{ false }, + Type::boolean()) + : ZeroForBooleanContext(expression.type) }, + {}, + {}, + }); + const auto trueId = cursor.state.nextBlock; + const auto falseId = trueId + 1U; + const auto joinId = falseId + 1U; + cursor.state.nextBlock = joinId + 1U; + cursor.Branch(std::move(condition), trueId, falseId); + + const auto prepareArm + = [&](const Prepared::BlockId armId, const Expression &arm) { + cursor.Open(armId); + auto value = Atomize(cursor, arm); + cursor.Emit(Prepared::Instruction{ + Prepared::Instruction::Kind::Assign, + result, + expression.type, + false, + Prepared::Operation::Copy, + { std::move(value) }, + {}, + {}, + }); + cursor.Jump(joinId); + }; + prepareArm(trueId, expression.operands[1]); + prepareArm(falseId, expression.operands[2]); + cursor.Open(joinId); + return Prepared::Atom::variable(std::move(result), expression.type); + } + struct PreparedFunctionResult final { Prepared::Function function; @@ -415,7 +483,7 @@ namespace Visual::XSharp::Core::CorePrep -> OperationResult { if (expression.kind == Expression::Kind::Let - || IsShortCircuit(expression)) + || IsShortCircuit(expression) || IsConditional(expression)) return { Prepared::Operation::Copy, { Atomize(cursor, expression) }, {}, @@ -497,20 +565,31 @@ namespace Visual::XSharp::Core::CorePrep { if (!expression.letValue || !expression.letBody) std::abort(); - auto value = Atomize(cursor, *expression.letValue); + // The bound value keeps its own operation, as in the + // Haskell lowering; an intermediate temporary would make + // the two adapters disagree on every non-atomic value. + auto value = AtomizeOperation(cursor, *expression.letValue); cursor.Emit( Prepared::Instruction{ Prepared::Instruction::Kind::Bind, expression.letSymbol, expression.letType, false, - Prepared::Operation::Copy, - { std::move(value) }, - {}, - {} }); + value.operation, + std::move(value.operands), + std::move(value.closureFunction), + std::move(value.captures) }); return Atomize(cursor, *expression.letBody); } if (IsShortCircuit(expression)) return AtomizeShortCircuit(cursor, expression); + if (expression.kind == Expression::Kind::Conditional) + { + // Core verification requires three operands, but keep this + // total for direct native API callers as well. + if (!IsConditional(expression)) + std::abort(); + return AtomizeConditional(cursor, expression); + } auto operation = AtomizeOperation(cursor, expression); auto temporary = cursor.Temporary(U"$coreprep"); @@ -631,6 +710,19 @@ namespace Visual::XSharp::Core::CorePrep } case Statement::Kind::Evaluate: { + // Only a call may be an instruction whose result is + // dropped: the record has no result type on the + // wire, and a reader recovers it from the callee. + // Any other value is computed into an ordinary + // temporary, so its operands still run and may + // trap, and the unused atom is ignored. + if (statement.expression.kind + != Expression::Kind::Apply) + { + static_cast( + Atomize(cursor, statement.expression)); + break; + } auto operation = AtomizeOperation(cursor, statement.expression); cursor.Emit(Prepared::Instruction{ diff --git a/Compiler/Core/CorePrep/Wire/Decode.cpp b/Compiler/Core/CorePrep/Wire/Decode.cpp index 9f6573ca..4baf8a8f 100644 --- a/Compiler/Core/CorePrep/Wire/Decode.cpp +++ b/Compiler/Core/CorePrep/Wire/Decode.cpp @@ -551,6 +551,18 @@ namespace visual_xsharp::core::wire break; case Instruction::Kind::Evaluate: operation(value); + // The record carries no result type. A discarded + // call still produces its callee's result, and Xpp + // checks that type against the signature, so it is + // recovered from the callee atom rather than left + // as Unit. + if (value.operation == Operation::Call + && !value.operands.empty() + && value.operands.front().type.kind + == Type::Kind::Function + && !value.operands.front().type.components.empty()) + value.type + = value.operands.front().type.components.back(); break; } return value; diff --git a/Compiler/Core/IR.cpp b/Compiler/Core/IR.cpp index fdc79217..d38e02d8 100644 --- a/Compiler/Core/IR.cpp +++ b/Compiler/Core/IR.cpp @@ -96,6 +96,22 @@ namespace Visual::XSharp::Core return expression; } + auto + Expression::Conditional(Expression test, + Expression whenTrue, + Expression whenFalse, + Type resultType) -> Expression + { + Expression expression; + expression.kind = Kind::Conditional; + expression.type = std::move(resultType); + expression.operands.reserve(3U); + expression.operands.push_back(std::move(test)); + expression.operands.push_back(std::move(whenTrue)); + expression.operands.push_back(std::move(whenFalse)); + return expression; + } + auto Expression::operator==(const Expression &other) const -> bool { diff --git a/Compiler/Core/Tests/BUILD.bazel b/Compiler/Core/Tests/BUILD.bazel index 50b1cc03..7c328595 100644 --- a/Compiler/Core/Tests/BUILD.bazel +++ b/Compiler/Core/Tests/BUILD.bazel @@ -15,6 +15,7 @@ cc_binary( cc_binary( name = "core_pipeline_tests", srcs = [ + "ConditionalLoweringTests.cpp", "CorePipelineTests.cpp", "LoopLoweringTests.cpp", "ShortCircuitLoweringTests.cpp", diff --git a/Compiler/Core/Tests/ConditionalLoweringTests.cpp b/Compiler/Core/Tests/ConditionalLoweringTests.cpp new file mode 100644 index 00000000..b1ee4965 --- /dev/null +++ b/Compiler/Core/Tests/ConditionalLoweringTests.cpp @@ -0,0 +1,731 @@ +// SPDX-FileCopyrightText: 2026 Progmasoft +// SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Visual/XSharp/Core/CorePrep/Prepare.hpp" +#include "Visual/XSharp/Core/CorePrep/Verifier.hpp" +#include "Visual/XSharp/Core/CorePrep/Wire.hpp" +#include "Visual/XSharp/Core/Scalar.hpp" +#include "Visual/XSharp/Core/Verifier.hpp" +#include "Visual/XSharp/Core/Wire.hpp" + +// A Core conditional evaluates exactly one of its two arms. In CorePrep that +// is control flow: each arm lives in its own block, reached only through the +// edge that selects it, and both write one result slot that the join reads. +// These tests pin that shape on the native Core-to-CorePrep adapter, the +// verifier rules that make the shape sound, and the wire encoding that +// carries the expression from the Haskell frontend. + +namespace +{ + namespace Core = Visual::XSharp::Core; + namespace Prepared = visual_xsharp::core; + + constexpr std::uint64_t kFlag = 2U; + constexpr std::uint64_t kLeft = 3U; + constexpr std::uint64_t kRight = 4U; + constexpr std::uint64_t kOther = 5U; + constexpr std::uint64_t kScoped = 6U; + + [[nodiscard]] auto + Integer(std::int64_t value) -> Core::Expression + { + return Core::Expression::Constant(value, Core::Type::int64()); + } + + [[nodiscard]] auto + Boolean(bool value) -> Core::Expression + { + return Core::Expression::Constant(value, Core::Type::boolean()); + } + + [[nodiscard]] auto + Flag() -> Core::Expression + { + return Core::Expression::Variable({ kFlag, U"flag" }, + Core::Type::boolean()); + } + + [[nodiscard]] auto + Other() -> Core::Expression + { + return Core::Expression::Variable({ kOther, U"other" }, + Core::Type::boolean()); + } + + [[nodiscard]] auto + Left() -> Core::Expression + { + return Core::Expression::Variable({ kLeft, U"left" }, + Core::Type::int64()); + } + + [[nodiscard]] auto + Right() -> Core::Expression + { + return Core::Expression::Variable({ kRight, U"right" }, + Core::Type::int64()); + } + + /// `12 / operand`; the division marks the block that owns an arm. + [[nodiscard]] auto + Quotient(Core::Expression operand) -> Core::Expression + { + return Core::Expression::InvokePrimitive( + Core::Primitive::Divide, + { Integer(12), std::move(operand) }, + Core::Type::int64()); + } + + [[nodiscard]] auto + Choose(Core::Expression test, + Core::Expression whenTrue, + Core::Expression whenFalse, + Core::Type type = Core::Type::int64()) -> Core::Expression + { + return Core::Expression::Conditional(std::move(test), + std::move(whenTrue), + std::move(whenFalse), + std::move(type)); + } + + /// `Evaluate(bool flag, int left, int right, bool other)` with one body. + [[nodiscard]] auto + Module(Core::Type returnType, std::vector body) + -> Core::Module + { + return { { U"Conditional" }, + { Core::Function{ + { 1U, U"Evaluate" }, + { { { kFlag, U"flag" }, Core::Type::boolean() }, + { { kLeft, U"left" }, Core::Type::int64() }, + { { kRight, U"right" }, Core::Type::int64() }, + { { kOther, U"other" }, Core::Type::boolean() } }, + std::move(returnType), + std::move(body) } } }; + } + + [[nodiscard]] auto + Returning(Core::Expression value) -> Core::Module + { + auto type = value.type; + return Module(std::move(type), + { Core::Statement::Return(std::move(value)) }); + } + + [[nodiscard]] auto + PrepareVerified(const Core::Module &module) -> Prepared::Function + { + for (const auto &issue : Core::Verify(module)) + FAIL_CHECK("Core " << issue.code << ": " << issue.message); + REQUIRE(Core::Verify(module).empty()); + auto prepared = Core::CorePrep::Prepare(module); + for (const auto &issue : Prepared::verify(prepared)) + FAIL_CHECK("CorePrep " << issue.code << ": " << issue.message + << " (block " << issue.block << ")"); + REQUIRE(Prepared::verify(prepared).empty()); + REQUIRE(prepared.functions.size() == 1U); + return std::move(prepared.functions.front()); + } + + [[nodiscard]] auto + Find(const Prepared::Function &function, Prepared::BlockId id) + -> const Prepared::Block & + { + const auto found + = std::ranges::find(function.blocks, id, &Prepared::Block::id); + REQUIRE(found != function.blocks.end()); + return *found; + } + + [[nodiscard]] auto + IsJumpTo(const Prepared::Block &block, Prepared::BlockId target) -> bool + { + return block.terminator.kind == Prepared::Terminator::Kind::Jump + && block.terminator.true_target == target; + } + + [[nodiscard]] auto + Count(const Prepared::Block &block, Prepared::Operation operation) + -> std::size_t + { + return static_cast( + std::ranges::count(block.instructions, + operation, + &Prepared::Instruction::operation)); + } + + [[nodiscard]] auto + CountBranches(const Prepared::Function &function) -> std::size_t + { + return static_cast( + std::ranges::count_if(function.blocks, [](const auto &block) { + return block.terminator.kind + == Prepared::Terminator::Kind::Branch; + })); + } + + /// Mutable bindings of generated `$conditional` result slots. + [[nodiscard]] auto + SlotSeeds(const Prepared::Function &function) + -> std::vector + { + std::vector seeds; + for (const auto &block : function.blocks) + for (const auto &instruction : block.instructions) + if (instruction.kind == Prepared::Instruction::Kind::Bind + && instruction.destination.spelling.starts_with( + U"$conditional")) + seeds.push_back(instruction); + return seeds; + } + + [[nodiscard]] auto + HasIssue(const Core::Module &module, std::string_view code) -> bool + { + return std::ranges::any_of(Core::Verify(module), + [code](const auto &issue) { + return issue.code == code; + }); + } +} // namespace + +TEST_CASE("a conditional lowers to a branch, two arms and one join", + "[coreprep][conditional]") +{ + const auto function + = PrepareVerified(Returning(Choose(Flag(), Left(), Right()))); + REQUIRE(function.blocks.size() == 4U); + + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + const auto trueId = entry.terminator.true_target; + const auto falseId = entry.terminator.false_target; + CHECK(trueId != falseId); + CHECK(trueId != function.entry); + CHECK(falseId != function.entry); + + // The Boolean parameter is branched on directly, without a comparison. + REQUIRE(entry.terminator.value.kind == Prepared::Atom::Kind::Variable); + CHECK(entry.terminator.value.symbol.id == kFlag); + CHECK(Count(entry, Prepared::Operation::NotEqual) == 0U); + + const auto &whenTrue = Find(function, trueId); + const auto &whenFalse = Find(function, falseId); + REQUIRE(whenTrue.terminator.kind == Prepared::Terminator::Kind::Jump); + const auto joinId = whenTrue.terminator.true_target; + CHECK(IsJumpTo(whenFalse, joinId)); + CHECK(joinId != trueId); + CHECK(joinId != falseId); + + // The join returns the slot; each arm stores its own operand into it. + const auto &join = Find(function, joinId); + REQUIRE(join.terminator.kind == Prepared::Terminator::Kind::Return); + REQUIRE(join.terminator.value.kind == Prepared::Atom::Kind::Variable); + const auto slot = join.terminator.value.symbol.id; + CHECK(join.instructions.empty()); + + REQUIRE(whenTrue.instructions.size() == 1U); + CHECK(whenTrue.instructions.front().kind + == Prepared::Instruction::Kind::Assign); + CHECK(whenTrue.instructions.front().destination.id == slot); + REQUIRE(whenTrue.instructions.front().operands.size() == 1U); + CHECK(whenTrue.instructions.front().operands.front().symbol.id == kLeft); + + REQUIRE(whenFalse.instructions.size() == 1U); + CHECK(whenFalse.instructions.front().kind + == Prepared::Instruction::Kind::Assign); + CHECK(whenFalse.instructions.front().destination.id == slot); + REQUIRE(whenFalse.instructions.front().operands.size() == 1U); + CHECK(whenFalse.instructions.front().operands.front().symbol.id == kRight); +} + +TEST_CASE("the result slot is seeded once before the branch", + "[coreprep][conditional]") +{ + const auto function + = PrepareVerified(Returning(Choose(Flag(), Left(), Right()))); + const auto seeds = SlotSeeds(function); + REQUIRE(seeds.size() == 1U); + CHECK(seeds.front().mutable_binding); + CHECK(seeds.front().type == Core::Type::int64()); + CHECK(seeds.front().operation == Prepared::Operation::Copy); + REQUIRE(seeds.front().operands.size() == 1U); + CHECK(seeds.front().operands.front().kind == Prepared::Atom::Kind::Literal); + CHECK(seeds.front().operands.front().literal + == Prepared::Literal{ Prepared::integer_from_signed(0) }); + + // The seed dominates both arms: it is in the block that branches. + const auto &entry = Find(function, function.entry); + CHECK(std::ranges::any_of(entry.instructions, + [&seeds](const auto &instruction) { + return instruction.destination.id + == seeds.front().destination.id; + })); +} + +TEST_CASE("slot seeds follow the result type", "[coreprep][conditional]") +{ + SECTION("a Boolean result starts from false") + { + const auto function = PrepareVerified(Returning( + Choose(Flag(), Other(), Boolean(true), Core::Type::boolean()))); + const auto seeds = SlotSeeds(function); + REQUIRE(seeds.size() == 1U); + CHECK(seeds.front().type == Core::Type::boolean()); + REQUIRE(seeds.front().operands.size() == 1U); + CHECK(seeds.front().operands.front().literal + == Prepared::Literal{ false }); + } + SECTION("a floating result starts from a floating zero") + { + const auto half + = Core::Expression::Constant(Prepared::FloatingLiteral{ "0.5" }, + Core::Type::float64()); + const auto whole + = Core::Expression::Constant(Prepared::FloatingLiteral{ "2.0" }, + Core::Type::float64()); + const auto function = PrepareVerified( + Returning(Choose(Flag(), half, whole, Core::Type::float64()))); + const auto seeds = SlotSeeds(function); + REQUIRE(seeds.size() == 1U); + CHECK(seeds.front().type == Core::Type::float64()); + REQUIRE(seeds.front().operands.size() == 1U); + CHECK(seeds.front().operands.front().literal + == Prepared::Literal{ Prepared::FloatingLiteral{ "0" } }); + } +} + +TEST_CASE("a numeric test is compared with zero before the branch", + "[coreprep][conditional]") +{ + const auto function + = PrepareVerified(Returning(Choose(Left(), Left(), Right()))); + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + CHECK(Count(entry, Prepared::Operation::NotEqual) == 1U); + REQUIRE(entry.terminator.value.kind == Prepared::Atom::Kind::Variable); + CHECK(entry.terminator.value.type == Core::Type::boolean()); + CHECK(entry.terminator.value.symbol.id != kLeft); +} + +TEST_CASE("each arm's computation stays in that arm's block", + "[coreprep][conditional]") +{ + // flag ? 12 / left : 12 / right. Neither division may be hoisted into + // the entry block or the join: one of the divisors may be zero. + const auto function = PrepareVerified( + Returning(Choose(Flag(), Quotient(Left()), Quotient(Right())))); + REQUIRE(function.blocks.size() == 4U); + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + const auto &whenTrue = Find(function, entry.terminator.true_target); + const auto &whenFalse = Find(function, entry.terminator.false_target); + const auto &join = Find(function, whenTrue.terminator.true_target); + + CHECK(Count(entry, Prepared::Operation::Divide) == 0U); + CHECK(Count(join, Prepared::Operation::Divide) == 0U); + REQUIRE(Count(whenTrue, Prepared::Operation::Divide) == 1U); + REQUIRE(Count(whenFalse, Prepared::Operation::Divide) == 1U); + + const auto divisor = [](const Prepared::Block &block) { + const auto found = std::ranges::find(block.instructions, + Prepared::Operation::Divide, + &Prepared::Instruction::operation); + return found->operands.back().symbol.id; + }; + CHECK(divisor(whenTrue) == kLeft); + CHECK(divisor(whenFalse) == kRight); +} + +TEST_CASE("nested conditionals create one region each", + "[coreprep][conditional]") +{ + SECTION("nested in the first arm") + { + // flag ? (other ? left : right) : 0 + const auto function = PrepareVerified(Returning( + Choose(Flag(), Choose(Other(), Left(), Right()), Integer(0)))); + CHECK(function.blocks.size() == 7U); + CHECK(CountBranches(function) == 2U); + CHECK(SlotSeeds(function).size() == 2U); + + // The inner test is evaluated only on the outer true edge. + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + const auto &outerTrue = Find(function, entry.terminator.true_target); + REQUIRE(outerTrue.terminator.kind + == Prepared::Terminator::Kind::Branch); + CHECK(outerTrue.terminator.value.symbol.id == kOther); + const auto &outerFalse = Find(function, entry.terminator.false_target); + CHECK(outerFalse.terminator.kind == Prepared::Terminator::Kind::Jump); + } + SECTION("chained in the second arm") + { + // flag ? left : (other ? right : 0) + const auto function = PrepareVerified(Returning( + Choose(Flag(), Left(), Choose(Other(), Right(), Integer(0))))); + CHECK(function.blocks.size() == 7U); + CHECK(CountBranches(function) == 2U); + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + const auto &outerFalse = Find(function, entry.terminator.false_target); + REQUIRE(outerFalse.terminator.kind + == Prepared::Terminator::Kind::Branch); + CHECK(outerFalse.terminator.value.symbol.id == kOther); + } + SECTION("as the test of another conditional") + { + // (flag ? other : false) ? left : right + const auto function = PrepareVerified(Returning(Choose( + Choose(Flag(), Other(), Boolean(false), Core::Type::boolean()), + Left(), + Right()))); + CHECK(function.blocks.size() == 7U); + CHECK(CountBranches(function) == 2U); + CHECK(SlotSeeds(function).size() == 2U); + } +} + +TEST_CASE("a conditional composes with the surrounding statement", + "[coreprep][conditional]") +{ + SECTION("as a binding initializer") + { + constexpr std::uint64_t kChosen = 7U; + const auto function = PrepareVerified( + Module(Core::Type::int64(), + { Core::Statement::Bind({ { kChosen, U"chosen" }, + Core::Type::int64(), + false, + Choose(Flag(), Left(), Right()) }), + Core::Statement::Return( + Core::Expression::Variable({ kChosen, U"chosen" }, + Core::Type::int64())) })); + REQUIRE(function.blocks.size() == 4U); + // The binding continues in the join, after both arms have stored. + const auto &entry = Find(function, function.entry); + const auto &whenTrue = Find(function, entry.terminator.true_target); + const auto &join = Find(function, whenTrue.terminator.true_target); + REQUIRE(join.instructions.size() == 1U); + CHECK(join.instructions.front().destination.id == kChosen); + CHECK(join.terminator.kind == Prepared::Terminator::Kind::Return); + } + SECTION("as an operand of a primitive") + { + // left + (flag ? left : right): the left operand is read before the + // branch and the addition happens after the join. + const auto function + = PrepareVerified(Returning(Core::Expression::InvokePrimitive( + Core::Primitive::Add, + { Left(), Choose(Flag(), Left(), Right()) }, + Core::Type::int64()))); + REQUIRE(function.blocks.size() == 4U); + const auto &entry = Find(function, function.entry); + const auto &whenTrue = Find(function, entry.terminator.true_target); + const auto &join = Find(function, whenTrue.terminator.true_target); + CHECK(Count(entry, Prepared::Operation::Add) == 0U); + CHECK(Count(join, Prepared::Operation::Add) == 1U); + } + SECTION("as an if condition") + { + const auto function = PrepareVerified(Module( + Core::Type::int64(), + { Core::Statement::If( + Choose(Flag(), Other(), Boolean(true), Core::Type::boolean()), + { Core::Statement::Return(Left()) }, + {}), + Core::Statement::Return(Right()) })); + CHECK(CountBranches(function) == 2U); + } + SECTION("as a while condition keeps one back-edge") + { + constexpr std::uint64_t kCounter = 7U; + const auto counter = [] { + return Core::Expression::Variable({ kCounter, U"counter" }, + Core::Type::int64()); + }; + const auto positive = [&counter] { + return Core::Expression::InvokePrimitive( + Core::Primitive::GreaterThan, + { counter(), Integer(0) }, + Core::Type::boolean()); + }; + const auto function = PrepareVerified(Module( + Core::Type::int64(), + { Core::Statement::Bind({ { kCounter, U"counter" }, + Core::Type::int64(), + true, + Left() }), + Core::Statement::While( + Choose(Flag(), + positive(), + Boolean(false), + Core::Type::boolean()), + { Core::Statement::Assign({ kCounter, U"counter" }, + Core::Expression::InvokePrimitive( + Core::Primitive::Subtract, + { counter(), Integer(1) }, + Core::Type::int64())) }), + Core::Statement::Return(counter()) })); + CHECK(CountBranches(function) == 2U); + const auto backEdges + = std::ranges::count_if(function.blocks, [](const auto &block) { + return block.terminator.kind + == Prepared::Terminator::Kind::Jump + && block.terminator.true_target < block.id; + }); + CHECK(backEdges == 1); + } +} + +TEST_CASE("a let inside an arm binds in that arm", "[coreprep][conditional]") +{ + // flag ? (let scoped = 12 / left in scoped) : right + const auto function = PrepareVerified(Returning(Choose( + Flag(), + Core::Expression::Let({ kScoped, U"scoped" }, + Core::Type::int64(), + Quotient(Left()), + Core::Expression::Variable({ kScoped, U"scoped" }, + Core::Type::int64()), + Core::Type::int64()), + Right()))); + const auto &entry = Find(function, function.entry); + REQUIRE(entry.terminator.kind == Prepared::Terminator::Kind::Branch); + const auto &whenTrue = Find(function, entry.terminator.true_target); + // The bound value keeps its own operation instead of a copied temporary. + const auto binding + = std::ranges::find_if(whenTrue.instructions, + [](const auto &instruction) { + return instruction.destination.id == kScoped; + }); + REQUIRE(binding != whenTrue.instructions.end()); + CHECK(binding->operation == Prepared::Operation::Divide); + CHECK(Count(entry, Prepared::Operation::Divide) == 0U); +} + +TEST_CASE("the Core verifier enforces the conditional contract", + "[core][verifier][conditional]") +{ + const auto text = Core::Expression::Constant(std::u32string{ U"text" }, + Core::Type::string()); + CHECK(Core::Verify(Returning(Choose(Flag(), Left(), Right()))).empty()); + CHECK(Core::Verify(Returning(Choose(Left(), Left(), Right()))).empty()); + + SECTION("the test must be bool or numeric") + { + CHECK(HasIssue(Returning(Choose(text, Left(), Right())), "VXC1067")); + } + SECTION("the first arm must have the result type") + { + CHECK(HasIssue(Returning(Choose(Flag(), Other(), Right())), "VXC1068")); + } + SECTION("the second arm must have the result type") + { + CHECK(HasIssue(Returning(Choose(Flag(), Left(), Other())), "VXC1069")); + } + SECTION("the result must be a scalar") + { + CHECK(HasIssue( + Returning(Choose(Flag(), text, text, Core::Type::string())), + "VXC1070")); + } + SECTION("a conditional needs exactly three operands") + { + auto malformed = Choose(Flag(), Left(), Right()); + malformed.operands.pop_back(); + CHECK(HasIssue(Returning(malformed), "VXC1071")); + malformed.operands.clear(); + CHECK(HasIssue(Returning(malformed), "VXC1071")); + } + SECTION("names are checked in the test and in both arms") + { + const auto missing = Core::Expression::Variable({ 90U, U"missing" }, + Core::Type::int64()); + const auto missingFlag + = Core::Expression::Variable({ 91U, U"missing" }, + Core::Type::boolean()); + CHECK(HasIssue(Returning(Choose(Flag(), missing, Right())), "VXC1020")); + CHECK(HasIssue(Returning(Choose(Flag(), Left(), missing)), "VXC1020")); + CHECK(HasIssue(Returning(Choose(missingFlag, Left(), Right())), + "VXC1020")); + } + SECTION("a let in one arm is not visible in the other") + { + const auto scoped = Core::Expression::Variable({ kScoped, U"scoped" }, + Core::Type::int64()); + CHECK(HasIssue( + Returning(Choose(Flag(), + Core::Expression::Let({ kScoped, U"scoped" }, + Core::Type::int64(), + Left(), + scoped, + Core::Type::int64()), + scoped)), + "VXC1020")); + } +} + +TEST_CASE("Core wire v8 round-trips conditional expressions", + "[core][wire][conditional]") +{ + const auto module = Returning( + Choose(Flag(), + Choose(Other(), Quotient(Left()), Right()), + Core::Expression::Let( + { kScoped, U"scoped" }, + Core::Type::int64(), + Right(), + Choose(Core::Expression::Variable({ kScoped, U"scoped" }, + Core::Type::int64()), + Left(), + Integer(7)), + Core::Type::int64()))); + REQUIRE(Core::Verify(module).empty()); + const auto encoded = Core::Wire::Encode(module); + REQUIRE(encoded); + CHECK(encoded.bytes.at(4) == 8U); + const auto decoded = Core::Wire::Decode(encoded.bytes); + REQUIRE(decoded); + CHECK(*decoded.module == module); + + SECTION("every proper prefix is rejected") + { + // The three children have fixed positions; no shorter payload can + // be mistaken for a complete conditional. + for (std::size_t size = 0U; size < encoded.bytes.size(); ++size) + { + const std::vector prefix( + encoded.bytes.begin(), + encoded.bytes.begin() + static_cast(size)); + CAPTURE(size); + CHECK_FALSE(Core::Wire::Decode(prefix)); + } + } + SECTION("trailing bytes are rejected") + { + auto extended = encoded.bytes; + extended.push_back(0U); + CHECK_FALSE(Core::Wire::Decode(extended)); + } + SECTION("the previous schema version is rejected") + { + auto previous = encoded.bytes; + previous[4] = 7U; + const auto rejected = Core::Wire::Decode(previous); + REQUIRE_FALSE(rejected); + CHECK(rejected.error->kind + == Core::Wire::ErrorKind::UnsupportedVersion); + } +} + +TEST_CASE("the wire writer rejects a malformed conditional", + "[core][wire][conditional]") +{ + auto malformed = Choose(Flag(), Left(), Right()); + malformed.operands.pop_back(); + const auto encoded = Core::Wire::Encode(Returning(malformed)); + REQUIRE_FALSE(encoded); + CHECK(encoded.error->kind == Core::Wire::ErrorKind::InvalidCount); +} + +TEST_CASE("a discarded call keeps its result type across the CorePrep wire", + "[coreprep][wire][conditional]") +{ + // int Next() { return 7; } + // int Evaluate(...) { Next(); ; return left; } + // The evaluate record has no type field. The reader used to leave the + // decoded instruction as Unit, which disagrees with the adapter and + // fails the Xpp signature check for every value-returning callee. + constexpr std::uint64_t kNext = 20U; + const auto nextType = Core::Type::function({}, Core::Type::int64()); + const auto call = [&nextType] { + return Core::Expression::Apply( + Core::Expression::Variable({ kNext, U"Next" }, nextType), + {}, + Core::Type::int64()); + }; + auto module + = Module(Core::Type::int64(), + { Core::Statement::Evaluate(call()), + Core::Statement::Evaluate(Quotient(Left())), + Core::Statement::Evaluate(Choose(Flag(), call(), Right())), + Core::Statement::Evaluate(Left()), + Core::Statement::Return(Left()) }); + module.functions.push_back( + Core::Function{ { kNext, U"Next" }, + {}, + Core::Type::int64(), + { Core::Statement::Return(Integer(7)) } }); + REQUIRE(Core::Verify(module).empty()); + // Read the module back from Core wire first, as the pipeline does. The + // reader stores integers in their canonical sign and magnitude form, + // which is also what the CorePrep reader produces. + const auto coreBytes = Core::Wire::Encode(module); + REQUIRE(coreBytes); + const auto canonical = Core::Wire::Decode(coreBytes.bytes); + REQUIRE(canonical); + const auto prepared = Core::CorePrep::Prepare(*canonical.module); + REQUIRE(Prepared::verify(prepared).empty()); + + // Exactly one result-dropping instruction: the direct call. The + // division and the conditional are bound; the plain read emits nothing. + std::vector evaluations; + for (const auto &block : prepared.functions.front().blocks) + for (const auto &instruction : block.instructions) + if (instruction.kind == Prepared::Instruction::Kind::Evaluate) + evaluations.push_back(instruction); + REQUIRE(evaluations.size() == 1U); + CHECK(evaluations.front().operation == Prepared::Operation::Call); + CHECK(evaluations.front().type == Core::Type::int64()); + + const auto encoded = Prepared::wire::encode(prepared); + REQUIRE_FALSE(encoded.error); + const auto decoded = Prepared::wire::decode(encoded.bytes); + REQUIRE(decoded); + REQUIRE(decoded.module->functions.size() == prepared.functions.size()); + // Literal operands are not compared here: the two readers may hold the + // same integer in different payload alternatives. The fields below are + // the ones the evaluate record must reproduce. + for (std::size_t function = 0U; function < prepared.functions.size(); + ++function) + { + const auto &expected = prepared.functions[function]; + const auto &actual = decoded.module->functions[function]; + CAPTURE(function); + REQUIRE(actual.blocks.size() == expected.blocks.size()); + for (std::size_t block = 0U; block < expected.blocks.size(); ++block) + { + CAPTURE(block); + const auto &expectedBlock = expected.blocks[block]; + const auto &actualBlock = actual.blocks[block]; + CHECK(actualBlock.terminator.kind == expectedBlock.terminator.kind); + REQUIRE(actualBlock.instructions.size() + == expectedBlock.instructions.size()); + for (std::size_t index = 0U; + index < expectedBlock.instructions.size(); + ++index) + { + CAPTURE(index); + const auto &want = expectedBlock.instructions[index]; + const auto &got = actualBlock.instructions[index]; + CHECK(got.kind == want.kind); + CHECK(got.destination == want.destination); + CHECK(got.type == want.type); + CHECK(got.mutable_binding == want.mutable_binding); + CHECK(got.operation == want.operation); + CHECK(got.operands.size() == want.operands.size()); + } + } + } +} diff --git a/Compiler/Core/Tests/CorePipelineTests.cpp b/Compiler/Core/Tests/CorePipelineTests.cpp index e6c7edcc..ab043188 100644 --- a/Compiler/Core/Tests/CorePipelineTests.cpp +++ b/Compiler/Core/Tests/CorePipelineTests.cpp @@ -200,7 +200,7 @@ namespace } [[nodiscard]] auto - ReadGoldenHex(std::string_view filename = "wire-v7.hex") + ReadGoldenHex(std::string_view filename = "wire-v8.hex") -> std::vector { const auto path = std::filesystem::path(__FILE__).parent_path() @@ -234,7 +234,7 @@ namespace } } // namespace -TEST_CASE("native VXCR v7 codec matches the Haskell golden contract") +TEST_CASE("native VXCR v8 codec matches the Haskell golden contract") { const auto expected = ReadGoldenHex(); const auto encoded = Core::Wire::Encode(GoldenModule()); @@ -284,7 +284,7 @@ TEST_CASE("VXCR reader rejects malformed boundaries and configured limits") } } -TEST_CASE("VXCR v7 carries Haskell Core closure and source-owner fields") +TEST_CASE("VXCR v8 carries Haskell Core closure and source-owner fields") { const auto source = ClosureModule(); REQUIRE(Core::Verify(source).empty()); @@ -309,7 +309,7 @@ TEST_CASE("native pipeline consumes a closure artifact emitted by Haskell") // This golden file is emitted from closure-boundary.vxs by vxs-frontend, // rather than re-encoded by the C++ model. It therefore locks the actual // cross-language expression tag and field order that production uses. - const auto bytes = ReadGoldenHex("wire-v7-closure.hex"); + const auto bytes = ReadGoldenHex("wire-v8-closure.hex"); const auto decoded = Core::Wire::Decode(bytes); REQUIRE(decoded); REQUIRE(Core::Verify(*decoded.module).empty()); @@ -359,7 +359,7 @@ TEST_CASE("native pipeline preserves non-empty Haskell source ownership bytes") { // Both owner fields are non-empty in this golden so field order cannot be // accidentally hidden by interchangeable zero-length encodings. - const auto bytes = ReadGoldenHex("wire-v7-project-source.hex"); + const auto bytes = ReadGoldenHex("wire-v8-project-source.hex"); const auto decoded = Core::Wire::Decode(bytes); REQUIRE(decoded); REQUIRE(Core::Verify(*decoded.module).empty()); @@ -545,7 +545,7 @@ TEST_CASE("Core adapter creates explicit CorePrep CFG and temporaries") REQUIRE(visual_xsharp::core::verify(prepared).empty()); } -TEST_CASE("Core v7 loops round-trip and lower to explicit back-edges") +TEST_CASE("Core v8 loops round-trip and lower to explicit back-edges") { const auto integer = [](std::int64_t value) { return Core::Expression::Constant(value, Core::Type::int64()); @@ -596,7 +596,7 @@ TEST_CASE("Core v7 loops round-trip and lower to explicit back-edges") }; const Core::Module module{ { U"Iteration" }, { std::move(function) } }; - CHECK(Core::Wire::kCurrentVersion == 7U); + CHECK(Core::Wire::kCurrentVersion == 8U); REQUIRE(Core::Verify(module).empty()); const auto encoded = Core::Wire::Encode(module); REQUIRE(encoded); diff --git a/Compiler/Core/Tests/Fixtures/Core/wire-v7-closure.hex b/Compiler/Core/Tests/Fixtures/Core/wire-v8-closure.hex similarity index 88% rename from Compiler/Core/Tests/Fixtures/Core/wire-v7-closure.hex rename to Compiler/Core/Tests/Fixtures/Core/wire-v8-closure.hex index 6a689a82..627dabad 100644 --- a/Compiler/Core/Tests/Fixtures/Core/wire-v7-closure.hex +++ b/Compiler/Core/Tests/Fixtures/Core/wire-v8-closure.hex @@ -1,6 +1,6 @@ -# Visual X# Core wire v7 closure document generated by the Haskell frontend. +# Visual X# Core wire v8 closure document generated by the Haskell frontend. # Source: closure-boundary.vxs -56 58 43 52 07 00 00 00 01 00 00 00 0f 00 00 00 +56 58 43 52 08 00 00 00 01 00 00 00 0f 00 00 00 43 00 00 00 6c 00 00 00 6f 00 00 00 73 00 00 00 75 00 00 00 72 00 00 00 65 00 00 00 42 00 00 00 6f 00 00 00 75 00 00 00 6e 00 00 00 64 00 00 00 diff --git a/Compiler/Core/Tests/Fixtures/Core/wire-v7-project-source.hex b/Compiler/Core/Tests/Fixtures/Core/wire-v8-project-source.hex similarity index 87% rename from Compiler/Core/Tests/Fixtures/Core/wire-v7-project-source.hex rename to Compiler/Core/Tests/Fixtures/Core/wire-v8-project-source.hex index e30b369c..0541b6db 100644 --- a/Compiler/Core/Tests/Fixtures/Core/wire-v7-project-source.hex +++ b/Compiler/Core/Tests/Fixtures/Core/wire-v8-project-source.hex @@ -1,6 +1,6 @@ -# Visual X# Core wire v7 document emitted by the Haskell Core writer. +# Visual X# Core wire v8 document emitted by the Haskell Core writer. # Module Demo; function Main belongs to Sources/Main.vxs. -56 58 43 52 07 00 00 00 01 00 00 00 +56 58 43 52 08 00 00 00 01 00 00 00 04 00 00 00 44 00 00 00 65 00 00 00 6d 00 00 00 6f 00 00 00 01 00 00 00 10 00 00 00 53 00 00 00 6f 00 00 00 diff --git a/Compiler/Core/Tests/Fixtures/Core/wire-v7.hex b/Compiler/Core/Tests/Fixtures/Core/wire-v8.hex similarity index 81% rename from Compiler/Core/Tests/Fixtures/Core/wire-v7.hex rename to Compiler/Core/Tests/Fixtures/Core/wire-v8.hex index 17662b18..bb2b4877 100644 --- a/Compiler/Core/Tests/Fixtures/Core/wire-v7.hex +++ b/Compiler/Core/Tests/Fixtures/Core/wire-v8.hex @@ -1,6 +1,6 @@ -# Visual X# Core wire v7 golden document +# Visual X# Core wire v8 golden document # module Demo; function Main() -> unit { return unit; } -56 58 43 52 07 00 00 00 +56 58 43 52 08 00 00 00 01 00 00 00 04 00 00 00 44 00 00 00 65 00 00 00 6d 00 00 00 6f 00 00 00 00 00 00 00 diff --git a/Compiler/Core/Tests/TemplateTests.cpp b/Compiler/Core/Tests/TemplateTests.cpp index dfe79ac5..9ed86f06 100644 --- a/Compiler/Core/Tests/TemplateTests.cpp +++ b/Compiler/Core/Tests/TemplateTests.cpp @@ -420,7 +420,7 @@ TEST_CASE("rendered identity length-prefixes qualified components") CHECK(second.find("1:") != std::string::npos); } -TEST_CASE("Core v7 preserves ordered template arguments and source ownership") +TEST_CASE("Core v8 preserves ordered template arguments and source ownership") { const auto type = Applied(U"Mix", { TypeArgument(Model::Type::string()), @@ -434,7 +434,7 @@ TEST_CASE("Core v7 preserves ordered template arguments and source ownership") const auto decoded = Core::Wire::Decode(encoded.bytes); REQUIRE(decoded); CHECK(*decoded.module == module); - CHECK(Core::Wire::kCurrentVersion == 7U); + CHECK(Core::Wire::kCurrentVersion == 8U); } TEST_CASE( diff --git a/Compiler/Core/Verifier.cpp b/Compiler/Core/Verifier.cpp index 4bf51522..a5bdd808 100644 --- a/Compiler/Core/Verifier.cpp +++ b/Compiler/Core/Verifier.cpp @@ -397,6 +397,44 @@ namespace Visual::XSharp::Core "Core let result disagrees with its body"); return; } + case Expression::Kind::Conditional: + { + if (expression.operands.size() != 3U) + { + Add("VXC1071", + "Core conditional must contain a test and two " + "arms"); + return; + } + const auto &test = expression.operands[0]; + const auto &whenTrue = expression.operands[1]; + const auto &whenFalse = expression.operands[2]; + VerifyExpression(test, environment); + if (!accepts_boolean_context(test.type)) + Add("VXC1067", + "Core conditional test must be bool or " + "numeric"); + VerifyExpression(whenTrue, environment); + VerifyExpression(whenFalse, environment); + CheckSameType(expression.type, + whenTrue.type, + "VXC1068", + "Core conditional result type disagrees " + "with its first arm"); + CheckSameType(expression.type, + whenFalse.type, + "VXC1069", + "Core conditional result type disagrees " + "with its second arm"); + // The result is materialized in a plain storage + // slot. Owned values would need move and release + // rules for that slot. + if (!accepts_boolean_context(expression.type)) + Add("VXC1070", + "Core conditional result must be bool or " + "numeric"); + return; + } } } void diff --git a/Compiler/Core/Wire.cpp b/Compiler/Core/Wire.cpp index 07fe0144..29d778fb 100644 --- a/Compiler/Core/Wire.cpp +++ b/Compiler/Core/Wire.cpp @@ -515,6 +515,18 @@ namespace Visual::XSharp::Core::Wire std::move(body), std::move(valueType)); } + case 6: + { + // The three children have fixed positions, so the + // payload carries no count. + auto test = ReadExpression(depth + 1U); + auto whenTrue = ReadExpression(depth + 1U); + auto whenFalse = ReadExpression(depth + 1U); + return Expression::Conditional(std::move(test), + std::move(whenTrue), + std::move(whenFalse), + std::move(valueType)); + } default: Fail(ErrorKind::InvalidTag, "expression tag", @@ -1156,6 +1168,18 @@ namespace Visual::XSharp::Core::Wire WriteExpression(*expression.letValue, depth + 1U); WriteExpression(*expression.letBody, depth + 1U); return; + case Expression::Kind::Conditional: + if (expression.operands.size() != 3U) + { + Fail(ErrorKind::InvalidCount, + "conditional expression", + "Core conditional must contain a test and " + "two arms"); + return; + } + for (const auto &operand : expression.operands) + WriteExpression(operand, depth + 1U); + return; } } void diff --git a/Compiler/Driver/Tests/ClosurePipelineTests.cpp b/Compiler/Driver/Tests/ClosurePipelineTests.cpp index 9709c8a4..f7a42494 100644 --- a/Compiler/Driver/Tests/ClosurePipelineTests.cpp +++ b/Compiler/Driver/Tests/ClosurePipelineTests.cpp @@ -220,7 +220,7 @@ namespace } } // namespace -TEST_CASE("Core v7 round-trips closure targets captures and modes") +TEST_CASE("Core v8 round-trips closure targets captures and modes") { const auto source = ClosureModule(); const auto encoded = Core::wire::encode(source); diff --git a/Compiler/Driver/Tests/ScalarPipelineTests.cpp b/Compiler/Driver/Tests/ScalarPipelineTests.cpp index a39a6ef1..1239c6ec 100644 --- a/Compiler/Driver/Tests/ScalarPipelineTests.cpp +++ b/Compiler/Driver/Tests/ScalarPipelineTests.cpp @@ -426,10 +426,10 @@ TEST_CASE("CorePrep wire v6 round-trips every scalar family", } } -TEST_CASE("Core wire v7 round-trips every scalar family", +TEST_CASE("Core wire v8 round-trips every scalar family", "[scalar][wire][core]") { - CHECK(native_wire::kCurrentVersion == 7U); + CHECK(native_wire::kCurrentVersion == 8U); CHECK(native_wire_round_trips(core::Type::unit())); CHECK(native_wire_round_trips(core::Type::string())); for (const auto &entry : kScalarCases) diff --git a/Compiler/Fuzzing/Corpus/differential/coalesce-compound.seed b/Compiler/Fuzzing/Corpus/differential/coalesce-compound.seed new file mode 100644 index 00000000..f3492f3d --- /dev/null +++ b/Compiler/Fuzzing/Corpus/differential/coalesce-compound.seed @@ -0,0 +1 @@ +4F9 \ No newline at end of file diff --git a/Compiler/Fuzzing/Corpus/differential/conditional-recursion.seed b/Compiler/Fuzzing/Corpus/differential/conditional-recursion.seed new file mode 100644 index 00000000..1f6b4785 --- /dev/null +++ b/Compiler/Fuzzing/Corpus/differential/conditional-recursion.seed @@ -0,0 +1 @@ +4>= p &= q ^= r |= s; _ = t(); x ? y ? 1 : 2 : z ?: 3 \ No newline at end of file diff --git a/Compiler/Fuzzing/Corpus/parser/conditional-forms.seed b/Compiler/Fuzzing/Corpus/parser/conditional-forms.seed new file mode 100644 index 00000000..60c99b95 --- /dev/null +++ b/Compiler/Fuzzing/Corpus/parser/conditional-forms.seed @@ -0,0 +1 @@ +class Program { public static int Evaluate(_ bool flag, _ int left) { int value = flag ? left : ; value += ; _ = ; value ??= 1; left() -= 2; return flag ? left ?: 1 : flag ? 2 ; } } \ No newline at end of file diff --git a/Compiler/Fuzzing/Corpus/source/conditional-forms.seed b/Compiler/Fuzzing/Corpus/source/conditional-forms.seed new file mode 100644 index 00000000..8f8dd30e --- /dev/null +++ b/Compiler/Fuzzing/Corpus/source/conditional-forms.seed @@ -0,0 +1 @@ +namespace Fuzz; class Program { public static long Sum(_ long n) { return n == 0 ? 0 : n + Sum(n - 1); } public static long Evaluate() { long value = 5; long total = value > 3 ? Sum(value) : 12 / (value - 5); total += value ?: 7; total -= 0 ?: 2; total *= 2; total //= 3; total %= 11; total <<= 1; total >>= 1; total &= 255; total ^= 3; total |= 8; _ = Sum(2); for (long index = 0; index < 4; index += 2) { total += index ? index : 1; } return total > 20 ? total < 40 ? 1 : 2 : total == 0 ? 3 : 4 ?: 5; } } \ No newline at end of file diff --git a/Compiler/Fuzzing/Corpus/source/discarded-operator.seed b/Compiler/Fuzzing/Corpus/source/discarded-operator.seed new file mode 100644 index 00000000..9ec30430 --- /dev/null +++ b/Compiler/Fuzzing/Corpus/source/discarded-operator.seed @@ -0,0 +1 @@ +namespace Fuzz; class Progzam { public static long Sum(_ long n) { return n == 1 ? 0 : n + Sum(n - 1); } public static long Evaluate() { long value = 6; long total = value > 3 ? Sum(value) : 12 / (value - 5); total += value ?: 7; total -= 0 ?: 2; total *= 2; total //= 3; total %= 11; total <<= 1; total >>= 1; total &= 255; total ^= 6; total |= 8; _ =! Sum(2); for (long index = 0; index < 4; index += 2) { total += index ? index : 1; } return total > 20 ? total < 40 ? 1 : 2 : total == 0 ? 3 : 4 ?: 0; } } \ No newline at end of file diff --git a/Compiler/Fuzzing/SourceFuzz.cpp b/Compiler/Fuzzing/SourceFuzz.cpp index ed862872..6b70cfee 100644 --- a/Compiler/Fuzzing/SourceFuzz.cpp +++ b/Compiler/Fuzzing/SourceFuzz.cpp @@ -83,7 +83,7 @@ namespace Visual::XSharp::Fuzzing expected = expression.value; std::string body = "return " + expression.source + ";"; std::string members; - const auto mode = NextByte(bytes, cursor) % 6U; + const auto mode = NextByte(bytes, cursor) % 9U; const auto limit = static_cast(NextByte(bytes, cursor) % 12U); if (mode == 1U) @@ -157,6 +157,58 @@ namespace Visual::XSharp::Fuzzing + std::to_string(limit) + " < 6) { return " + std::to_string(limit) + "; } return 0 - 1;"; } + else if (mode == 6U) + { + // Both conditionals are correct only when the unselected + // result is not evaluated: the recursion ends at the first + // result, and the division is defined only in the second. + const auto sum = limit * (limit + 1) / 2; + expected = sum + (limit == 0 ? 100 : 60 / limit); + members = " public static int Sum(_ int n) { return n == 0 " + "? 0 : n + Sum(n - 1); }\n"; + body = "int value = " + std::to_string(limit) + + "; return Sum(value) + (value == 0 ? 100 : 60 / " + "value);"; + } + else if (mode == 7U) + { + // Truthy coalescing keeps a nonzero left value and evaluates + // the fallback otherwise; every compound operator reads the + // target it writes. The host loop is the reference. + std::int64_t total + = expression.value != 0 ? expression.value : 7; + for (std::int64_t index = 0; index < limit; index += 2) + { + total += index != 0 ? index : 3; + total ^= index; + } + total *= 2; + total -= limit; + total %= 1000003; + expected = total; + body = "int total = " + expression.source + + " ?: 7; for (int index = 0; index < " + + std::to_string(limit) + + "; index += 2) { total += index ?: 3; total ^= " + "index; } total *= 2; total -= " + + std::to_string(limit) + + "; total %= 1000003; return total;"; + } + else if (mode == 8U) + { + // Chained conditionals group to the right and nest in a + // first result without parentheses; each trip count selects + // a different leaf of the same expression. + const auto value = limit - 5; + expected = value < 0 ? (value < -3 ? 1 : 2) + : value == 0 ? expression.value + : value > 3 ? 4 + : 5; + body = "int value = " + std::to_string(limit) + + "; value -= 5; return value < 0 ? value < 0 - 3 ? 1 " + ": 2 : value == 0 ? " + + expression.source + " : value > 3 ? 4 : 5 ?: 6;"; + } return "namespace Fuzz;\n" "class Program {\n" + members diff --git a/Compiler/Fuzzing/SourceFuzzSmoke.cpp b/Compiler/Fuzzing/SourceFuzzSmoke.cpp index 79d860e7..069930cd 100644 --- a/Compiler/Fuzzing/SourceFuzzSmoke.cpp +++ b/Compiler/Fuzzing/SourceFuzzSmoke.cpp @@ -28,7 +28,7 @@ main() // agree. They combine forms the generated programs below keep separate: // loops inside loops, short-circuit operators as loop conditions, and // several functions sharing one module-wide symbol numbering. - constexpr std::array accepted{ + constexpr std::array accepted{ "namespace Parity; class Program { public static int Evaluate() { " "int total = 0; for (int outer = 0; outer < 4; outer++) { " "if (outer == 2) { continue; } int inner = 0; " @@ -50,6 +50,47 @@ main() "return result; } } class Second { public static long Other() { " "long value = 3; for (int step = 0; step < 2; step++) { " "value = value * 2; } return value; } }", + // Conditional expressions: Boolean and numeric tests, nesting in + // either result and in the test, and a recursion the first result + // terminates. + "namespace Parity; class Program { " + "public static int Sum(_ int n) { return n == 0 ? 0 : n + Sum(n - 1); " + "} public static int Pick(_ bool flag, _ int left, _ int right) { " + "return flag ? left > right ? left : right : left ? 0 - left : " + "right; } public static int Evaluate() { return (Sum(4) > 5 ? " + "Pick(true, 3, 9) > 4 : false) ? Pick(false, 0, 7) : Sum(2); } }", + // Truthy coalescing binds its left operand once. A call on the left + // is the case where an adapter that copies the bound value through + // an extra temporary disagrees with one that binds the call itself. + "namespace Parity; class Program { " + "public static int Next(_ int n) { return n > 2 ? Next(n - 3) : n; } " + "public static int Evaluate() { int value = Next(7) ?: Next(5); " + "return value + (Next(9) ?: 4) + (value ?: Next(8) ?: 6); } }", + // Compound assignments in statement and loop-update position, with + // a conditional and a coalescing operand. + "namespace Parity; class Program { public static int Evaluate() { " + "int total = 3; total += 4; total -= 1; total *= 5; total /= 2; " + "total //= 2; total %= 7; total <<= 3; total >>= 1; total &= 127; " + "total ^= 9; total |= 64; for (int index = 0; index < 6; index += 2) " + "{ total += index ? index : 1; total -= index ?: 2; } " + "return total; } }", + // Discarded values: a call keeps its result-dropping instruction; an + // operator, a conditional and a division are computed into + // temporaries. + "namespace Parity; class Program { " + "public static int Down(_ int n) { return n > 0 ? Down(n - 1) : 0; } " + "public static int Evaluate() { int value = 5; _ = Down(value); " + "_ = !Down(2); _ = 12 / value; _ = value > 3 ? Down(1) : value; " + "value > 4 ? Down(3) : 0; _ = Down(value) ?: 7; return value; } }", + // Conditional forms as loop conditions and as the operands of + // short-circuit operators, in a module with several functions. + "namespace Parity; class Program { " + "public static bool Small(_ int n) { return n < 3 ? true : n == 9; } " + "public static int Evaluate() { int index = 0; int total = 0; " + "while (index < 4 ? Small(index) || total < 9 : false) { " + "total += index ?: 5; index += 1; } " + "do { total -= 1; } while (total > 3 && (total ?: 1) \\= 2); " + "return Small(total) && total > 0 ? total : 0 - total; } }", }; for (const auto text : accepted) Visual::XSharp::Fuzzing::ExerciseAcceptedSource( @@ -74,18 +115,25 @@ main() // A leaf selector followed by an explicit mode and limit byte reaches // every generated control-flow shape at every trip count, including the // zero-trip, continue and break paths, instead of only the shapes the - // four cycling selectors above happen to select. - constexpr std::uint8_t kModes = 6U; + // four cycling selectors above happen to select. The two leaves are the + // literals 0 and 4, so a form that tests its generated expression sees + // both a false and a true value. + constexpr std::uint8_t kModes = 9U; constexpr std::uint8_t kLimits = 12U; - for (std::uint8_t mode = 0U; mode < kModes; ++mode) + constexpr std::array leaves{ 0U, 4U }; + for (const auto leaf : leaves) { - for (std::uint8_t limit = 0U; limit < kLimits; ++limit) + for (std::uint8_t mode = 0U; mode < kModes; ++mode) { - const std::array seed{ 0U, mode, limit }; - llvm::errs() << "Differential smoke: mode " - << static_cast(mode) << " limit " - << static_cast(limit) << '\n'; - Visual::XSharp::Fuzzing::ExerciseDifferentialOracle(seed); + for (std::uint8_t limit = 0U; limit < kLimits; ++limit) + { + const std::array seed{ leaf, mode, limit }; + llvm::errs() << "Differential smoke: leaf " + << static_cast(leaf) << " mode " + << static_cast(mode) << " limit " + << static_cast(limit) << '\n'; + Visual::XSharp::Fuzzing::ExerciseDifferentialOracle(seed); + } } } return 0; diff --git a/Compiler/Haskell/Core/Benches/Main.hs b/Compiler/Haskell/Core/Benches/Main.hs index b4f6b7eb..09517eb8 100644 --- a/Compiler/Haskell/Core/Benches/Main.hs +++ b/Compiler/Haskell/Core/Benches/Main.hs @@ -365,6 +365,8 @@ expressionDigest expression = case expression of CoreApply callee arguments _ -> 1 + expressionDigest callee + sum (map expressionDigest arguments) CorePrimitive _ arguments _ -> 1 + sum (map expressionDigest arguments) CoreLet _ _ value body _ -> 1 + expressionDigest value + expressionDigest body + CoreConditional condition whenTrue whenFalse _ -> + 1 + sum (map expressionDigest [condition, whenTrue, whenFalse]) CoreClosure captures parameters _ body _ -> 1 + length parameters diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs index 9784ebd6..b7c00790 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core.hs @@ -104,6 +104,14 @@ data CoreExpression -- | Bind one expression result before evaluating the continuation. CoreLet ResolvedName Type CoreExpression CoreExpression Type + | {- | Conditional expression: test the condition in Boolean context, then + evaluate exactly one of the two arms and yield its value. The arm that + is not selected is never evaluated, so its calls, failures and + non-termination do not happen. Both arms have the result type. This is + the Core form of source @condition ? first : second@ and, combined + with 'CoreLet', of @left ?: fallback@. + -} + CoreConditional CoreExpression CoreExpression CoreExpression Type | -- | Closure with captures, parameters, body, and callable type. CoreClosure [CoreCapture] @@ -208,4 +216,5 @@ expressionType expression = case expression of CoreApply _ _ value -> value CorePrimitive _ _ value -> value CoreLet _ _ _ _ value -> value + CoreConditional _ _ _ value -> value CoreClosure _ _ _ _ value -> value diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/CorePrep.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/CorePrep.hs index ea412ef2..47651d1f 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/CorePrep.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/CorePrep.hs @@ -22,6 +22,7 @@ module Visual.XSharp.Core.CorePrep import Data.Map.Strict qualified as Map import Visual.XSharp.AST import Visual.XSharp.Core +import Visual.XSharp.Core.Scalar (isCoreFloatingType) import Visual.XSharp.Core.Verifier (verifyCore) import Visual.XSharp.Diagnostic @@ -179,6 +180,8 @@ expressionSymbolIds expression = case expression of CoreApply callee arguments _ -> expressionSymbolIds callee ++ concatMap expressionSymbolIds arguments CorePrimitive _ arguments _ -> concatMap expressionSymbolIds arguments CoreLet name _ value body _ -> symbol name : expressionSymbolIds value ++ expressionSymbolIds body + CoreConditional condition whenTrue whenFalse _ -> + concatMap expressionSymbolIds [condition, whenTrue, whenFalse] CoreClosure captures parameters _ body _ -> map (symbol . coreCaptureName) captures ++ concatMap (expressionSymbolIds . coreCaptureValue) captures @@ -199,10 +202,19 @@ prepareStatements state open (statement : remaining) = case statement of let (closed, continued, atom, after) = atomize state open value (later, final) = prepareStatements after (appendInstruction continued (CorePrepAssign name atom)) remaining in (closed ++ later, final) - CoreEvaluate value -> - let (closed, continued, operation, after) = atomizeOperation state open value - (later, final) = prepareStatements after (appendInstruction continued (CorePrepEvaluate operation)) remaining - in (closed ++ later, final) + CoreEvaluate value + | discardsItsOperation value -> + let (closed, continued, operation, after) = atomizeOperation state open value + (later, final) = prepareStatements after (appendInstruction continued (CorePrepEvaluate operation)) remaining + in (closed ++ later, final) + | otherwise -> + -- Only a call may be an instruction whose result is dropped. + -- Any other value is computed into an ordinary temporary, so + -- its operands still run and may trap, and the unused atom is + -- ignored. + let (closed, continued, _, after) = atomize state open value + (later, final) = prepareStatements after continued remaining + in (closed ++ later, final) CoreReturn value -> let (closed, continued, atom, after) = atomize state open value in (closed ++ [closeBlock continued (CorePrepReturn atom)], after) @@ -318,6 +330,15 @@ prepareFor state incoming condition body update = finalState = afterUpdate {loopTargets = loopTargets state} in ([entry] ++ conditionBlocks ++ [branch] ++ bodyEnd ++ updateEnd, OpenBlock exitId [], finalState) +-- | Whether an evaluated expression lowers to one result-discarding +-- instruction. The record has no result type on the wire; a reader recovers +-- it from the callee, which only a call has. A discarded closure creation is +-- therefore bound like any other value. +discardsItsOperation :: CoreExpression -> Bool +discardsItsOperation expression = case expression of + CoreApply {} -> True + _ -> False + jumpOpenBlocks :: Int -> [CorePrepBlock] -> [CorePrepBlock] jumpOpenBlocks target = map connect where @@ -379,6 +400,8 @@ atomize state open expression = case expression of CorePrimitive primitive [left, right] _ | primitive == CoreLogicalAnd || primitive == CoreLogicalOr -> atomizeShortCircuit state open primitive left right + CoreConditional condition whenTrue whenFalse valueType -> + atomizeConditional state open condition whenTrue whenFalse valueType _ -> let (closed, continued, operation, afterOperation) = atomizeOperation state open expression temporary = @@ -400,6 +423,9 @@ atomizeOperation state open expression = case expression of | primitive == CoreLogicalAnd || primitive == CoreLogicalOr -> let (closed, continued, atom, after) = atomizeShortCircuit state open primitive left right in (closed, continued, CorePrepCopy atom, after) + CoreConditional condition whenTrue whenFalse valueType -> + let (closed, continued, atom, after) = atomizeConditional state open condition whenTrue whenFalse valueType + in (closed, continued, CorePrepCopy atom, after) CoreApply callee arguments _ -> let (calleeBlocks, calleeOpen, calleeAtom, afterCallee) = atomize state open callee (argumentBlocks, argumentOpen, argumentAtoms, afterArguments) = atomizeMany afterCallee calleeOpen arguments @@ -476,6 +502,66 @@ atomizeShortCircuit state open primitive left right = , final ) +{- | Lower a conditional expression to a two-way branch over a result slot. + +Exactly one arm is evaluated. The slot is bound before the branch with the +neutral literal of its type and each arm overwrites it on its own path, so +the join block reads an initialized value from either predecessor without a +phi node. The neutral value is never observable: no path reaches the join +without passing through one of the two assignments. +-} +atomizeConditional :: + PrepState -> + OpenBlock -> + CoreExpression -> + CoreExpression -> + CoreExpression -> + Type -> + ([CorePrepBlock], OpenBlock, CorePrepAtom, PrepState) +atomizeConditional state open condition whenTrue whenFalse valueType = + let (conditionBlocks, conditionOpen, conditionAtom, afterCondition) = atomize state open condition + (booleanOpen, predicate, afterBoolean) = booleanizeAtom afterCondition conditionOpen conditionAtom + resultId = nextTemporary afterBoolean + resultName = ResolvedName (SymbolId resultId) (Identifier ("$conditional" ++ show resultId)) + initializedOpen = + appendInstruction + booleanOpen + (CorePrepBind resultName valueType True (CorePrepCopy (neutralAtom valueType))) + trueId = nextBlock afterBoolean + falseId = trueId + 1 + joinId = falseId + 1 + afterReservation = + afterBoolean + { nextTemporary = resultId + 1 + , nextBlock = joinId + 1 + } + header = closeBlock initializedOpen (CorePrepBranch predicate trueId falseId) + (trueBlocks, trueExit, afterTrue) = prepareArm afterReservation trueId whenTrue + (falseBlocks, falseExit, afterFalse) = prepareArm afterTrue falseId whenFalse + prepareArm armState armId arm = + let (armBlocks, armOpen, armAtom, afterArm) = atomize armState (OpenBlock armId []) arm + assigned = appendInstruction armOpen (CorePrepAssign resultName armAtom) + in (armBlocks, closeBlock assigned (CorePrepJump joinId), afterArm) + in ( conditionBlocks ++ [header] ++ trueBlocks ++ [trueExit] ++ falseBlocks ++ [falseExit] + , OpenBlock joinId [] + , CorePrepVariable resultName valueType + , afterFalse + ) + +-- | Literal that initializes a storage slot before its first real assignment. +neutralAtom :: Type -> CorePrepAtom +neutralAtom valueType + | valueType == boolType = CorePrepLiteral (CoreBoolean False) valueType + | otherwise = zeroAtom valueType + +-- | Zero of a numeric type, spelled as the native adapter spells its slot +-- initializer: a floating zero is a floating literal, not an integer payload +-- under a floating type. +zeroAtom :: Type -> CorePrepAtom +zeroAtom valueType + | isCoreFloatingType valueType = CorePrepLiteral (CoreFloating "0") valueType + | otherwise = CorePrepLiteral (CoreInteger 0) valueType + atomizeMany :: PrepState -> OpenBlock -> [CoreExpression] -> ([CorePrepBlock], OpenBlock, [CorePrepAtom], PrepState) atomizeMany state open [] = ([], open, [], state) atomizeMany state open (value : remaining) = diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Monomorphization/Discovery.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Monomorphization/Discovery.hs index 1d73e970..9081a9d4 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Monomorphization/Discovery.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Monomorphization/Discovery.hs @@ -108,6 +108,10 @@ discoverExpression location expression = discoverTypeAt (appendPath location BindingTypePath) bindingType ++ discoverExpression (appendPath location BindingValuePath) value ++ discoverExpression (appendPath location EvaluatedValuePath) body + CoreConditional condition whenTrue whenFalse _ -> + discoverExpression (appendPath location ConditionPath) condition + ++ discoverExpression (appendPath location TrueBranchPath) whenTrue + ++ discoverExpression (appendPath location FalseBranchPath) whenFalse CoreClosure captures parameters returnType body _ -> concat [ discoverCapture (appendPath location (ClosureCapturePath index)) capture diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Analysis.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Analysis.hs index 17add5da..a00cad78 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Analysis.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Analysis.hs @@ -104,6 +104,8 @@ expressionEffectWithFacts environment facts expression binding = CoreBind (CoreBinding name valueType False value) bodyFacts = transferStatementFacts afterValue binding in combineEffect valueEffect (expressionEffectWithFacts environment bodyFacts body) + CoreConditional condition whenTrue whenFalse _ -> + conditionalEffect environment facts condition whenTrue whenFalse CoreApply callee arguments _ -> let (nested, _) = expressionListEffect environment facts (callee : arguments) invoked = case callee of @@ -128,6 +130,21 @@ shortCircuitEffect environment facts isAnd left right = | otherwise = PureEffect in combineEffect leftEffect rightEffect +-- Only the selected arm runs. An arm whose edge is infeasible under the +-- incoming facts contributes no effect, exactly like a skipped right operand. +conditionalEffect :: + EffectEnvironment -> IntegerFacts -> CoreExpression -> CoreExpression -> CoreExpression -> Effect +conditionalEffect environment facts condition whenTrue whenFalse = + let conditionEffect = expressionEffectWithFacts environment facts condition + afterCondition = transferExpressionFacts facts condition + truth = conditionTruthFromFacts facts condition + armEffect desired arm = + let input = refineConditionFacts desired condition afterCondition + in if truth == Just (not desired) || isUnreachableFacts input + then PureEffect + else expressionEffectWithFacts environment input arm + in combineEffect conditionEffect (combineEffect (armEffect True whenTrue) (armEffect False whenFalse)) + expressionListEffect :: EffectEnvironment -> IntegerFacts -> [CoreExpression] -> (Effect, IntegerFacts) expressionListEffect _ facts [] = (PureEffect, facts) expressionListEffect environment facts (expression : remaining) = @@ -185,6 +202,8 @@ expressionSymbols expression = case expression of CorePrimitive _ arguments _ -> Set.unions (map expressionSymbols arguments) CoreLet name _ value body _ -> Set.union (expressionSymbols value) (Set.delete (resolvedSymbol name) (expressionSymbols body)) + CoreConditional condition whenTrue whenFalse _ -> + Set.unions (map expressionSymbols [condition, whenTrue, whenFalse]) CoreClosure captures _ _ _ _ -> Set.unions (map (expressionSymbols . coreCaptureValue) captures) statementSymbols :: CoreStatement -> Set SymbolId diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Constant.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Constant.hs index e916a009..e67ab136 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Constant.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Constant.hs @@ -179,6 +179,20 @@ simplifyExpressionUsing environment facts expression = case expression of else Map.delete (resolvedSymbol name) environment bodyFacts = transferStatementFacts facts (CoreBind (CoreBinding name bindingType False simplifiedValue)) in CoreLet name bindingType simplifiedValue (simplifyExpressionWithFacts bodyEnvironment bodyFacts body) valueType + CoreConditional condition whenTrue whenFalse valueType -> + let simplifiedCondition = simplifyExpressionUsing environment facts condition + afterCondition = transferExpressionFacts facts simplifiedCondition + simplifyArm desired = + simplifyExpressionWithFacts + environment + (refineConditionFacts desired simplifiedCondition afterCondition) + in -- A literal test has no effects, so the conditional is its selected arm. + case simplifiedCondition of + CoreLiteral (CoreBoolean selected) _ -> + if selected then simplifyArm True whenTrue else simplifyArm False whenFalse + CoreLiteral (CoreInteger selected) _ -> + if selected /= 0 then simplifyArm True whenTrue else simplifyArm False whenFalse + _ -> CoreConditional simplifiedCondition (simplifyArm True whenTrue) (simplifyArm False whenFalse) valueType CoreClosure captures parameters returnType body valueType -> let simplifiedCaptures = map simplifyCapture captures captureConstants = diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/ControlFlow.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/ControlFlow.hs index 4488b485..a13d06ae 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/ControlFlow.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/ControlFlow.hs @@ -90,6 +90,17 @@ simplifyNestedExpression environment expression = case expression of (simplifyNestedExpression environment value) (simplifyNestedExpression environment body) valueType + CoreConditional condition whenTrue whenFalse valueType -> + let simplifiedCondition = simplifyNestedExpression environment condition + in case conditionTruth simplifiedCondition of + Just True -> simplifyNestedExpression environment whenTrue + Just False -> simplifyNestedExpression environment whenFalse + Nothing -> + CoreConditional + simplifiedCondition + (simplifyNestedExpression environment whenTrue) + (simplifyNestedExpression environment whenFalse) + valueType CoreClosure captures parameters returnType body valueType -> CoreClosure [ capture {coreCaptureValue = simplifyNestedExpression environment (coreCaptureValue capture)} diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/EffectInference.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/EffectInference.hs index c95a26ae..d69a40b6 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/EffectInference.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/EffectInference.hs @@ -228,6 +228,15 @@ expressionFacts knownSymbols pureKnown facts (CoreLet name valueType value body boundState = transferStatementFacts afterValue (CoreBind (CoreBinding name valueType False value)) (bodyFacts, afterBody) = expressionFacts knownSymbols pureKnown boundState body in (combineFacts valueFacts bodyFacts, afterBody) +expressionFacts knownSymbols pureKnown facts (CoreConditional condition whenTrue whenFalse _) = + -- Summaries are path-insensitive: both arms contribute their direct + -- effects and callees, and the continuation keeps only shared facts. + let (conditionFacts, afterCondition) = expressionFacts knownSymbols pureKnown facts condition + (trueFacts, afterTrue) = + expressionFacts knownSymbols pureKnown (refineConditionFacts True condition afterCondition) whenTrue + (falseFacts, afterFalse) = + expressionFacts knownSymbols pureKnown (refineConditionFacts False condition afterCondition) whenFalse + in (combineFacts conditionFacts (combineFacts trueFacts falseFacts), joinIntegerFacts afterTrue afterFalse) expressionFacts knownSymbols pureKnown facts (CoreApply callee arguments _) = let (children, _) = expressionListFacts knownSymbols pureKnown facts (callee : arguments) invoked = case directCallee callee of diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Inline.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Inline.hs index 23279861..b1e8d12c 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Inline.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Inline.hs @@ -196,6 +196,13 @@ rewriteExpression maximumNodes candidates state expression = case expression of let (rewrittenValue, afterValue) = rewriteExpression maximumNodes candidates state value (rewrittenBody, afterBody) = rewriteExpression maximumNodes candidates afterValue body in (CoreLet name bindingType rewrittenValue rewrittenBody valueType, afterBody) + -- Inlined bodies stay nested inside the arm that contained the call, so + -- an unselected arm still evaluates nothing. + CoreConditional condition whenTrue whenFalse valueType -> + let (rewrittenCondition, afterCondition) = rewriteExpression maximumNodes candidates state condition + (rewrittenTrue, afterTrue) = rewriteExpression maximumNodes candidates afterCondition whenTrue + (rewrittenFalse, afterFalse) = rewriteExpression maximumNodes candidates afterTrue whenFalse + in (CoreConditional rewrittenCondition rewrittenTrue rewrittenFalse valueType, afterFalse) CoreClosure captures parameters returnType body valueType -> let (rewrittenCaptures, afterCaptures) = mapAccumulating rewriteCapture state captures (rewrittenBody, finalState) = rewriteStatements maximumNodes candidates afterCaptures body @@ -318,6 +325,11 @@ cloneExpression environment expression state = case expression of bodyEnvironment = Map.insert (resolvedSymbol oldName) (CoreVariable fresh bindingType) environment (clonedBody, afterBody) = cloneExpression bodyEnvironment body afterName in (CoreLet fresh bindingType clonedValue clonedBody valueType, afterBody) + CoreConditional condition whenTrue whenFalse valueType -> + let (clonedCondition, afterCondition) = cloneExpression environment condition state + (clonedTrue, afterTrue) = cloneExpression environment whenTrue afterCondition + (clonedFalse, afterFalse) = cloneExpression environment whenFalse afterTrue + in (CoreConditional clonedCondition clonedTrue clonedFalse valueType, afterFalse) CoreClosure captures parameters returnType body valueType -> let (clonedValues, afterValues) = mapAccumulating cloneCaptureValue state captures (captureEnvironment, clonedCaptures, afterCaptures) = cloneCaptureNames environment clonedValues afterValues @@ -424,6 +436,8 @@ expressionNodeCount expression = case expression of CoreApply callee arguments _ -> 1 + sum (map expressionNodeCount (callee : arguments)) CorePrimitive _ arguments _ -> 1 + sum (map expressionNodeCount arguments) CoreLet _ _ value body _ -> 1 + expressionNodeCount value + expressionNodeCount body + CoreConditional condition whenTrue whenFalse _ -> + 1 + sum (map expressionNodeCount [condition, whenTrue, whenFalse]) CoreClosure captures _ _ body _ -> 1 + sum (map (expressionNodeCount . coreCaptureValue) captures) + sum (map statementNodeCount body) statementNodeCount :: CoreStatement -> Int diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/IntegerFacts.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/IntegerFacts.hs index bbf63157..350046fa 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/IntegerFacts.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/IntegerFacts.hs @@ -197,6 +197,19 @@ factOfIntegerExpression facts expression = case expression of let afterValue = transferExpressionFacts facts value bound = setIntegerFact afterValue (resolvedSymbol name) (factOfIntegerExpression afterValue value) in factOfIntegerExpression bound body + CoreConditional condition whenTrue whenFalse resultType + | isCoreIntegerType resultType -> + let afterCondition = transferExpressionFacts facts condition + trueFacts = refineConditionFacts True condition afterCondition + falseFacts = refineConditionFacts False condition afterCondition + in case (isUnreachableFacts trueFacts, isUnreachableFacts falseFacts) of + (True, True) -> Nothing + (True, False) -> factOfIntegerExpression falseFacts whenFalse + (False, True) -> factOfIntegerExpression trueFacts whenTrue + (False, False) -> + joinFact + <$> factOfIntegerExpression trueFacts whenTrue + <*> factOfIntegerExpression falseFacts whenFalse _ -> Nothing combineIntegerFacts :: CorePrimitive -> Type -> IntegerFact -> IntegerFact -> Maybe IntegerFact @@ -270,6 +283,11 @@ transferExpressionFacts facts (CorePrimitive CoreLogicalAnd [left, right] _) = transferShortCircuitFacts True facts left right transferExpressionFacts facts (CorePrimitive CoreLogicalOr [left, right] _) = transferShortCircuitFacts False facts left right +transferExpressionFacts facts (CoreConditional condition whenTrue whenFalse _) = + let afterCondition = transferExpressionFacts facts condition + afterTrue = transferExpressionFacts (refineConditionFacts True condition afterCondition) whenTrue + afterFalse = transferExpressionFacts (refineConditionFacts False condition afterCondition) whenFalse + in joinIntegerFacts afterTrue afterFalse transferExpressionFacts facts expression | expressionInvokesCallable expression = emptyIntegerFacts | otherwise = facts @@ -295,6 +313,8 @@ expressionInvokesCallable expression = case expression of CoreApply {} -> True CorePrimitive _ arguments _ -> any expressionInvokesCallable arguments CoreLet _ _ value body _ -> expressionInvokesCallable value || expressionInvokesCallable body + CoreConditional condition whenTrue whenFalse _ -> + any expressionInvokesCallable [condition, whenTrue, whenFalse] -- A closure body is deferred; only its capture initializers run now. CoreClosure captures _ _ _ _ -> any (expressionInvokesCallable . coreCaptureValue) captures @@ -601,6 +621,15 @@ refineConditionFacts desired expression facts = refine expression afterBinding = transferStatementFacts afterInitializer binding afterBody = refineConditionFacts desired body afterBinding in forgetIntegerFact afterBody (resolvedSymbol name) + -- The requested truth can come from either arm; each arm is + -- reached only along its own edge of the selecting condition. + CoreConditional condition whenTrue whenFalse _ -> + let afterCondition = transferExpressionFacts facts condition + throughTrue = + refineConditionFacts desired whenTrue (refineConditionFacts True condition afterCondition) + throughFalse = + refineConditionFacts desired whenFalse (refineConditionFacts False condition afterCondition) + in joinIntegerFacts throughTrue throughFalse _ | expressionInvokesCallable value -> emptyIntegerFacts | otherwise -> facts @@ -810,6 +839,11 @@ conditionTruthFromFacts facts expression CorePrimitive CoreLogicalOr [left, right] _ -> (||) <$> conditionTruthFromFacts facts left <*> conditionTruthFromFacts facts right CorePrimitive primitive [left, right] _ -> comparisonTruth facts primitive left right + CoreConditional condition whenTrue whenFalse _ -> + case conditionTruthFromFacts facts condition of + Just True -> conditionTruthFromFacts (refineConditionFacts True condition facts) whenTrue + Just False -> conditionTruthFromFacts (refineConditionFacts False condition facts) whenFalse + Nothing -> Nothing _ -> Nothing comparisonTruth :: IntegerFacts -> CorePrimitive -> CoreExpression -> CoreExpression -> Maybe Bool diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Liveness.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Liveness.hs index ca2fa9b7..ea3c225d 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Liveness.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Liveness.hs @@ -205,6 +205,12 @@ optimizeExpression environment expression = case expression of (optimizeExpression environment value) (optimizeExpression environment body) valueType + CoreConditional condition whenTrue whenFalse valueType -> + CoreConditional + (optimizeExpression environment condition) + (optimizeExpression environment whenTrue) + (optimizeExpression environment whenFalse) + valueType CoreClosure captures parameters returnType body valueType -> CoreClosure [capture {coreCaptureValue = optimizeExpression environment (coreCaptureValue capture)} | capture <- captures] diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Types.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Types.hs index e6e031cb..5e5f7a39 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Types.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Optimizer/Types.hs @@ -142,6 +142,8 @@ measureExpression expression = (foldl addMetrics (measureExpression callee) (map measureExpression arguments)) {metricCalls = 1} CorePrimitive _ arguments _ -> foldl addMetrics emptyMetrics (map measureExpression arguments) CoreLet _ _ value body _ -> addMetrics (measureExpression value) (measureExpression body) + CoreConditional condition whenTrue whenFalse _ -> + foldl addMetrics (measureExpression condition) (map measureExpression [whenTrue, whenFalse]) CoreClosure captures _ _ body _ -> ( addMetrics (foldl addMetrics emptyMetrics (map (measureExpression . coreCaptureValue) captures)) diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Symbols.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Symbols.hs index 33df0c64..175f9687 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Symbols.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Symbols.hs @@ -69,6 +69,8 @@ coreExpressionSymbols expression = case expression of CorePrimitive _ arguments _ -> concatMap coreExpressionSymbols arguments CoreLet name _ value body _ -> name : coreExpressionSymbols value ++ coreExpressionSymbols body + CoreConditional condition whenTrue whenFalse _ -> + coreExpressionSymbols condition ++ coreExpressionSymbols whenTrue ++ coreExpressionSymbols whenFalse CoreClosure captures parameters _ body _ -> map coreCaptureName captures ++ map fst parameters diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Verifier.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Verifier.hs index 9d4494bf..19d1d9d9 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Verifier.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Verifier.hs @@ -196,6 +196,28 @@ verifyExpression environment expression = ++ typeMismatch "VXC1042" "Core let value has the wrong type" bindingType (expressionType value) ++ verifyExpression (Map.insert (resolvedSymbol name) (bindingType, False) environment) body ++ typeMismatch "VXC1043" "Core let result type disagrees with its body" valueType (expressionType body) + CoreConditional condition whenTrue whenFalse valueType -> + verifyExpression environment condition + ++ [ problem "VXC1067" "Core conditional test must be bool or numeric" + | expressionType condition /= boolType && not (isCoreNumericType (expressionType condition)) + ] + ++ verifyExpression environment whenTrue + ++ verifyExpression environment whenFalse + ++ typeMismatch + "VXC1068" + "Core conditional result type disagrees with its first arm" + valueType + (expressionType whenTrue) + ++ typeMismatch + "VXC1069" + "Core conditional result type disagrees with its second arm" + valueType + (expressionType whenFalse) + -- The result is materialized in a plain storage slot. Owned + -- values would need move and release rules for that slot. + ++ [ problem "VXC1070" "Core conditional result must be bool or numeric" + | valueType /= boolType && not (isCoreNumericType valueType) + ] CoreClosure captures parameters returnType body valueType -> verifyClosure environment captures parameters returnType body valueType diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Wire.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Wire.hs index 51ea8547..14c251f0 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Core/Wire.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Core/Wire.hs @@ -33,7 +33,7 @@ newtype CoreWireVersion = CoreWireVersion {coreWireVersionNumber :: Word16} -- | Schema version emitted by the current Core writer. currentCoreWireVersion :: CoreWireVersion -currentCoreWireVersion = CoreWireVersion 7 +currentCoreWireVersion = CoreWireVersion 8 -- | Finite bounds for total bytes, recursion, and individual collections. data CoreWireLimits = CoreWireLimits @@ -287,6 +287,12 @@ encodeExpression limits depth expression encodedValue <- encodeExpression limits (depth + 1) value encodedBody <- encodeExpression limits (depth + 1) body pure ([5] ++ encodedType ++ encodedName ++ encodedBindingType ++ encodedValue ++ encodedBody) + CoreConditional condition whenTrue whenFalse valueType -> do + encodedType <- encodeType limits 0 valueType + encodedCondition <- encodeExpression limits (depth + 1) condition + encodedTrue <- encodeExpression limits (depth + 1) whenTrue + encodedFalse <- encodeExpression limits (depth + 1) whenFalse + pure ([6] ++ encodedType ++ encodedCondition ++ encodedTrue ++ encodedFalse) encodeCoreCapture :: CoreWireLimits -> Int -> CoreCapture -> Encoder encodeCoreCapture limits depth capture = do @@ -571,6 +577,12 @@ decodeExpression depth = do value <- decodeExpression (depth + 1) body <- decodeExpression (depth + 1) pure (CoreLet name bindingType value body valueType) + 6 -> do + valueType <- decodeType 0 + condition <- decodeExpression (depth + 1) + whenTrue <- decodeExpression (depth + 1) + whenFalse <- decodeExpression (depth + 1) + pure (CoreConditional condition whenTrue whenFalse valueType) _ -> invalidTag "expression tag" tag decodeCoreCapture :: Int -> Decoder CoreCapture diff --git a/Compiler/Haskell/Core/src/Visual/XSharp/Desugarer.hs b/Compiler/Haskell/Core/src/Visual/XSharp/Desugarer.hs index 369a4e7d..47935ea3 100644 --- a/Compiler/Haskell/Core/src/Visual/XSharp/Desugarer.hs +++ b/Compiler/Haskell/Core/src/Visual/XSharp/Desugarer.hs @@ -80,10 +80,16 @@ functionSources = concatMap declarationFunctionSources type Lower = StateT Int (Either [Diagnostic]) freshPatternSubject :: Lower ResolvedName -freshPatternSubject = do +freshPatternSubject = freshGenerated "$pattern" + +freshCoalesceSubject :: Lower ResolvedName +freshCoalesceSubject = freshGenerated "$coalesce" + +freshGenerated :: String -> Lower ResolvedName +freshGenerated prefix = do identifier <- get put (identifier + 1) - pure (ResolvedName (SymbolId identifier) (Identifier ("$pattern" ++ show identifier))) + pure (ResolvedName (SymbolId identifier) (Identifier (prefix ++ show identifier))) lowerTop :: Declaration ResolvedName Type -> Lower [CoreFunction] lowerTop TypeDeclaration {typeMembers = members} = mapM lowerDeclaration members @@ -158,6 +164,14 @@ lowerStatement statement = case statement of one = CoreLiteral (CoreInteger 1) loweredType operation = if isIncrement then CoreAdd else CoreSubtract in pure [CoreAssign name (CorePrimitive operation [value, one] loweredType)] + -- The type checker has established that the operator result has the + -- target type, so the stored primitive needs no conversion. + CompoundAssignmentStatement _ operator name valueType value -> do + lowered <- lowerExpression value + let loweredType = lowerBoundaryType valueType + current = CoreVariable name loweredType + pure [CoreAssign name (CorePrimitive (lowerBinary operator) [current, lowered] loweredType)] + DiscardStatement _ value -> (: []) . CoreEvaluate <$> lowerExpression value BreakStatement _ Nothing -> pure [CoreBreak] BreakStatement spanValue (Just _) -> lift @@ -207,6 +221,28 @@ lowerExpression expression = case expression of subjectRead = CoreVariable subjectName subjectType predicate = lowerPattern subjectRead subjectType patternValue pure (CoreLet subjectName subjectType loweredSubject predicate boolType) + ConditionalExpression _ condition first second valueType -> + CoreConditional + <$> lowerExpression condition + <*> lowerExpression first + <*> lowerExpression second + <*> pure (lowerBoundaryType valueType) + -- The left operand is both the test and the first result. Binding it once + -- keeps its effects single even though it is read twice. + CoalesceExpression _ left fallback valueType -> do + loweredLeft <- lowerExpression left + loweredFallback <- lowerExpression fallback + subjectName <- freshCoalesceSubject + let loweredType = lowerBoundaryType valueType + subjectRead = CoreVariable subjectName loweredType + pure + ( CoreLet + subjectName + loweredType + loweredLeft + (CoreConditional subjectRead subjectRead loweredFallback loweredType) + loweredType + ) CallableExpression _ explicit captures parameters body valueType -> do let loweredParameters = [(parameterName parameter, lowerBoundaryType (parameterAnnotation parameter)) | parameter <- parameters] @@ -248,6 +284,8 @@ expressionAnnotation expression = case expression of UnaryExpression _ _ _ valueType -> valueType BinaryExpression _ _ _ _ valueType -> valueType IsPatternExpression _ _ _ valueType -> valueType + ConditionalExpression _ _ _ _ valueType -> valueType + CoalesceExpression _ _ _ valueType -> valueType CallableExpression _ _ _ _ _ valueType -> valueType lowerPattern :: CoreExpression -> Type -> Pattern ResolvedName Type -> CoreExpression @@ -353,6 +391,8 @@ expressionReads expression = case expression of CorePrimitive _ arguments _ -> concatMap expressionReads arguments CoreLet name _ value body _ -> expressionReads value ++ filter ((/= resolvedSymbol name) . resolvedSymbol . fst) (expressionReads body) + CoreConditional condition whenTrue whenFalse _ -> + expressionReads condition ++ expressionReads whenTrue ++ expressionReads whenFalse CoreClosure captures _ _ body _ -> concatMap (expressionReads . coreCaptureValue) captures ++ statementReads body uniqueReads :: [(ResolvedName, Type)] -> [(ResolvedName, Type)] @@ -424,6 +464,8 @@ statementIds statement = case statement of ForEachStatement _ _ _ name _ source body -> symbolValue name : expressionIds source ++ blockSymbolIds body IncrementStatement _ name _ _ -> [symbolValue name] + CompoundAssignmentStatement _ _ name _ value -> symbolValue name : expressionIds value + DiscardStatement _ value -> expressionIds value BreakStatement _ value -> maybe [] expressionIds value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionIds value @@ -437,6 +479,8 @@ expressionIds expression = case expression of UnaryExpression _ _ value _ -> expressionIds value BinaryExpression _ _ left right _ -> expressionIds left ++ expressionIds right IsPatternExpression _ subject _ _ -> expressionIds subject + ConditionalExpression _ condition first second _ -> concatMap expressionIds [condition, first, second] + CoalesceExpression _ left fallback _ -> expressionIds left ++ expressionIds fallback CallableExpression _ _ captures parameters body _ -> map (symbolValue . captureName) captures ++ concatMap (maybe [] expressionIds . captureInitializer) captures diff --git a/Compiler/Haskell/Driver/test/ClosureTests.hs b/Compiler/Haskell/Driver/test/ClosureTests.hs index c95341da..e7e7318a 100644 --- a/Compiler/Haskell/Driver/test/ClosureTests.hs +++ b/Compiler/Haskell/Driver/test/ClosureTests.hs @@ -62,7 +62,7 @@ closureTests = , ("Core verifier accepts a well-formed closure", coreVerifierAcceptsClosure) , ("Core verifier rejects mismatched closure type", coreVerifierRejectsTypeMismatch) , ("Core verifier rejects capture initializer mismatch", coreVerifierRejectsCaptureMismatch) - , ("Core wire v7 round-trips closure values and ownership", coreWireClosureRoundTrip) + , ("Core wire v8 round-trips closure values and ownership", coreWireClosureRoundTrip) , ("CorePrep wire v6 round-trips closure creation and ownership", corePrepWireClosureRoundTrip) , ("CorePrep verifier accepts converted closure", corePrepVerifierAcceptsClosure) , ("CorePrep verifier rejects primitive weak capture", corePrepVerifierRejectsWeakPrimitive) @@ -351,6 +351,8 @@ statementCallable statement = case statement of `orElse` firstJust (map statementCallable updates) ForEachStatement _ _ _ _ _ collection body -> expressionCallable collection `orElse` blockCallable body IncrementStatement {} -> Nothing + CompoundAssignmentStatement _ _ _ _ value -> expressionCallable value + DiscardStatement _ value -> expressionCallable value BreakStatement _ value -> value >>= expressionCallable ContinueStatement {} -> Nothing ExpressionStatement _ value _ -> expressionCallable value @@ -404,6 +406,8 @@ symbols expression = case expression of UnaryExpression _ _ value _ -> symbols value BinaryExpression _ _ left right _ -> symbols left ++ symbols right IsPatternExpression _ value _ _ -> symbols value + ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap symbols [condition, whenTrue, whenFalse] + CoalesceExpression _ left fallback _ -> symbols left ++ symbols fallback CallableExpression _ _ captures parameters body _ -> map (resolvedSymbol . captureName) captures ++ map (resolvedSymbol . parameterName) parameters @@ -428,6 +432,8 @@ statementSymbols statement = case statement of ForEachStatement _ _ _ name _ collection body -> resolvedSymbol name : symbols collection ++ blockSymbols body IncrementStatement _ name _ _ -> [resolvedSymbol name] + CompoundAssignmentStatement _ _ name _ value -> resolvedSymbol name : symbols value + DiscardStatement _ value -> symbols value BreakStatement _ value -> maybe [] symbols value ContinueStatement {} -> [] ExpressionStatement _ value _ -> symbols value @@ -594,7 +600,7 @@ invalidPreparedWeakCapture = -- Keep a textual assertion near the wire tests so failures caused by an -- accidental version rollback explain themselves in the test output. _wireVersionContext :: String -_wireVersionContext = "closures require Core wire version 7 and CorePrep wire version 6" +_wireVersionContext = "closures require Core wire version 8 and CorePrep wire version 6" _diagnosticContext :: Diagnostic -> Bool _diagnosticContext diagnostic = "closure" `isInfixOf` diagnosticMessage diagnostic diff --git a/Compiler/Haskell/Driver/test/ConditionalExpressionTests.hs b/Compiler/Haskell/Driver/test/ConditionalExpressionTests.hs new file mode 100644 index 00000000..f22b2e8f --- /dev/null +++ b/Compiler/Haskell/Driver/test/ConditionalExpressionTests.hs @@ -0,0 +1,923 @@ +-- SPDX-FileCopyrightText: 2026 Progmasoft +-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1 + +{- | Vertical tests for the conditional forms and the statement forms that +landed with them: @condition ? first : second@, @left ?: fallback@, compound +assignment, and the discard statement. + +Shape assertions read the unoptimized Core wherever a call must stay visible, +because the optimizer is free to fold a pure callee. Laziness is asserted on +CorePrep blocks, which is the representation the native backend executes. +-} +module ConditionalExpressionTests (conditionalExpressionTests) where + +import Data.List (isInfixOf, isPrefixOf) +import Visual.XSharp.AST +import Visual.XSharp.Compiler +import Visual.XSharp.Core +import Visual.XSharp.Core.CorePrep +import Visual.XSharp.Core.CorePrep.Verifier +import Visual.XSharp.Core.Verifier +import Visual.XSharp.Core.Wire +import Visual.XSharp.Diagnostic +import Visual.XSharp.Frontend +import Visual.XSharp.Lexer +import Visual.XSharp.Parser +import Visual.XSharp.Template.Application +import Visual.XSharp.Template.Specialization + +conditionalExpressionTests :: [(String, Bool)] +conditionalExpressionTests = + lexerTests + ++ parserTests + ++ typeTests + ++ loweringTests + ++ verifierTests + ++ optimizerTests + ++ corePrepTests + +-- ---------------------------------------------------------------- lexer + +lexerTests :: [(String, Bool)] +lexerTests = + [ ("a spaced conditional lexes '?' and ':' separately", symbolsOf "a ? b : c" == ["?", ":"]) + , ("the omitted-middle operator is one token", symbolsOf "a ?: b" == ["?:"]) + , ("a conditional with a spaced empty middle is not the coalescing token", symbolsOf "a ? : b" == ["?", ":"]) + , ("null coalescing lexes as its own token", symbolsOf "a ?? b" == ["??"]) + , ("null-coalescing assignment lexes as its own token", symbolsOf "a ??= b" == ["??="]) + , ("every compound assignment operator is one token", all compoundIsOneToken compoundSpellings) + , ("rounded-division assignment is not a division followed by '/='", symbolsOf "a //= b" == ["//="]) + , ("power assignment is not a multiplication followed by '*='", symbolsOf "a **= b" == ["**="]) + , ("shift assignments are single tokens", symbolsOf "a <<= b >>= c" == ["<<=", ">>="]) + , ("relational operators keep their spelling next to assignments", symbolsOf "a <= b >= c" == ["<=", ">="]) + , ("nested generic closers stay separate tokens", symbolsOf "A> value" == ["<", "<", ">", ">"]) + , ("a line comment still starts at a spaced '--'", symbolsOf "a -= b -- c ? d : e" == ["-="]) + ] + +compoundSpellings :: [String] +compoundSpellings = ["+=", "-=", "*=", "/=", "//=", "%=", "**=", "<<=", ">>=", "&=", "^=", "|="] + +compoundIsOneToken :: String -> Bool +compoundIsOneToken operatorText = symbolsOf ("target " ++ operatorText ++ " 1") == [operatorText] + +symbolsOf :: String -> [String] +symbolsOf text = case runLexer defaultLexer (LexerInput "conditional.vxs" text) of + Right tokens -> [tokenText token | token <- tokens, tokenKind token == SymbolToken] + Left _ -> [""] + +-- ---------------------------------------------------------------- parser + +parserTests :: [(String, Bool)] +parserTests = + [ ("a conditional parses into its three operands", conditionalParses) + , ("chained conditionals group to the right", conditionalIsRightAssociative) + , ("a conditional may nest in its first result without parentheses", conditionalNestsInFirstResult) + , ("logical OR binds more tightly than a conditional", logicalOrBindsTighter) + , ("the conditional is the weakest expression level in a call argument", conditionalIsCallArgument) + , ("truthy coalescing parses into its two operands", coalesceParses) + , ("a spaced empty middle is the same omitted-middle form", spacedCoalesceParses) + , ("chained truthy coalescing groups to the right", coalesceIsRightAssociative) + , ("coalescing in a conditional result stays inside that result", coalesceNestsInConditional) + , ("a conditional without ':' is a dedicated syntax error", parseRejectedWith "VXP0029" (returning "flag ? 1")) + , ("null coalescing is rejected until nullable types exist", parseRejectedWith "VXP0030" (returning "left ?? right")) + , ("null-coalescing assignment is rejected until nullable types exist", parseRejectedWith "VXP0031" (body "left ??= right; return 0;")) + , ("every compound operator parses to its binary operator", all compoundParses compoundOperators) + , ("a compound assignment needs a named target", parseRejectedWith "VXP0003" (body "Next() += 1; return 0;")) + , ("a compound assignment needs its terminator", parseRejected (body "left += 1 return left;")) + , ("a for update may be a compound assignment", forUpdateIsCompound) + , ("a for update list may mix increments and compound assignments", forUpdateListParses) + , ("the discard statement parses as its own statement", discardParses) + , ("discard is not an expression", parseRejected (returning "(_ = Next())")) + , ("discard needs its terminator", parseRejected (body "_ = Next() return 0;")) + , ("a conditional is not a template value argument", parseRejectedWith "VXP0018" templateConditionalArgument) + ] + +compoundOperators :: [(String, BinaryOperator)] +compoundOperators = + [ ("+=", Add) + , ("-=", Subtract) + , ("*=", Multiply) + , ("/=", Divide) + , ("//=", FloorDivide) + , ("%=", Remainder) + , ("**=", Power) + , ("<<=", ShiftLeft) + , (">>=", ShiftRight) + , ("&=", BitwiseAnd) + , ("^=", BitwiseXor) + , ("|=", BitwiseOr) + ] + +conditionalParses :: Bool +conditionalParses = case returned "flag ? left : right" of + Just (ConditionalExpression _ condition first second ()) -> + isName "flag" condition && isName "left" first && isName "right" second + _ -> False + +conditionalIsRightAssociative :: Bool +conditionalIsRightAssociative = case returned "flag ? 1 : other ? 2 : 3" of + Just (ConditionalExpression _ condition _ (ConditionalExpression _ inner _ _ ()) ()) -> + isName "flag" condition && isName "other" inner + _ -> False + +conditionalNestsInFirstResult :: Bool +conditionalNestsInFirstResult = case returned "flag ? other ? 1 : 2 : 3" of + Just (ConditionalExpression _ condition (ConditionalExpression _ inner _ _ ()) third ()) -> + isName "flag" condition && isName "other" inner && isInteger 3 third + _ -> False + +logicalOrBindsTighter :: Bool +logicalOrBindsTighter = case returned "flag || other ? 1 : 2" of + Just (ConditionalExpression _ (BinaryExpression _ LogicalOr _ _ ()) first second ()) -> + isInteger 1 first && isInteger 2 second + _ -> False + +conditionalIsCallArgument :: Bool +conditionalIsCallArgument = case returned "Twice(flag ? 1 : 2)" of + Just (CallExpression _ _ [ConditionalExpression {}] ()) -> True + _ -> False + +coalesceParses :: Bool +coalesceParses = case returned "left ?: right" of + Just (CoalesceExpression _ left fallback ()) -> isName "left" left && isName "right" fallback + _ -> False + +spacedCoalesceParses :: Bool +spacedCoalesceParses = case returned "left ? : right" of + Just (CoalesceExpression _ left fallback ()) -> isName "left" left && isName "right" fallback + _ -> False + +coalesceIsRightAssociative :: Bool +coalesceIsRightAssociative = case returned "left ?: right ?: 9" of + Just (CoalesceExpression _ left (CoalesceExpression _ middle final ()) ()) -> + isName "left" left && isName "right" middle && isInteger 9 final + _ -> False + +coalesceNestsInConditional :: Bool +coalesceNestsInConditional = case returned "flag ? left ?: 1 : right ?: 2" of + Just (ConditionalExpression _ _ CoalesceExpression {} CoalesceExpression {} ()) -> True + _ -> False + +compoundParses :: (String, BinaryOperator) -> Bool +compoundParses (operatorText, operator) = + case firstStatements (body ("left " ++ operatorText ++ " right; return left;")) of + Just (CompoundAssignmentStatement _ parsed (Identifier "left") () value : _) -> + parsed == operator && isName "right" value + _ -> False + +forUpdateIsCompound :: Bool +forUpdateIsCompound = + case firstStatements (body "for (int index = 0; index < left; index += 2) {} return left;") of + Just (ForStatement _ _ _ [CompoundAssignmentStatement _ Add (Identifier "index") () _] _ : _) -> True + _ -> False + +forUpdateListParses :: Bool +forUpdateListParses = + case firstStatements (body "for (int index = 0; index < left; index++, right -= 1) {} return left;") of + Just (ForStatement _ _ _ [IncrementStatement {}, CompoundAssignmentStatement _ Subtract _ () _] _ : _) -> True + _ -> False + +discardParses :: Bool +discardParses = case firstStatements (body "_ = Next(); return 0;") of + Just (DiscardStatement _ CallExpression {} : _) -> True + _ -> False + +templateConditionalArgument :: String +templateConditionalArgument = + unlines + [ "template class Buffer {}" + , "class Program {" + , " public static int Evaluate() {" + , " final Buffer<+(1 ? 2 : 3)> value = 0;" + , " return 0;" + , " }" + , "}" + ] + +isName :: String -> Expression Identifier () -> Bool +isName expected expression = case expression of + NameExpression _ (Identifier actual) () -> actual == expected + _ -> False + +isInteger :: Integer -> Expression Identifier () -> Bool +isInteger expected expression = case expression of + LiteralExpression _ (IntegerLiteral actual) () -> actual == expected + _ -> False + +parseSource :: String -> Either [Diagnostic] ParsedAST +parseSource text = do + tokens <- runLexer defaultLexer (LexerInput "conditional.vxs" text) + runParser defaultParser (ParserInput "conditional.vxs" tokens) + +parseRejected :: String -> Bool +parseRejected text = either (const True) (const False) (parseSource text) + +parseRejectedWith :: String -> String -> Bool +parseRejectedWith code text = case parseSource text of + Left problems -> any ((== code) . diagnosticCode) problems + Right _ -> False + +-- | Statements of the first method of the first class in a parsed unit. +firstStatements :: String -> Maybe [Statement Identifier ()] +firstStatements text = case parseSource text of + Right (ParsedAST (SyntaxTree _ (TypeDeclaration {typeMembers = member : _} : _))) -> case member of + FunctionDeclaration {declarationBody = Block statements} -> Just statements + _ -> Nothing + _ -> Nothing + +returned :: String -> Maybe (Expression Identifier ()) +returned expression = case firstStatements (returning expression) of + Just [ReturnStatement _ value] -> value + _ -> Nothing + +-- ---------------------------------------------------------------- sources + +-- | One method whose parameters give every test non-constant operands. +body :: String -> String +body statements = + unlines + [ "class Program {" + , " public static int Evaluate(_ bool flag, _ bool other, _ int left, _ int right) {" + , " " ++ statements + , " }" + , " public static int Next() { return 7; }" + , " public static int Twice(_ int value) { return value + value; }" + , " public static void Touch() { }" + , " public static int Countdown(_ int value) { return value > 0 ? Countdown(value - 1) : 0; }" + , "}" + ] + +returning :: String -> String +returning expression = body ("return " ++ expression ++ ";") + +compileSource :: String -> Either [Diagnostic] FrontendArtifacts +compileSource text = compileToCorePrep (CompilerInput "conditional.vxs" text) + +accepted :: String -> Bool +accepted text = either (const False) (const True) (compileSource text) + +rejectedWith :: String -> String -> Bool +rejectedWith code text = case compileSource text of + Left problems -> any ((== code) . diagnosticCode) problems + Right _ -> False + +-- ---------------------------------------------------------------- type checker + +typeTests :: [(String, Bool)] +typeTests = + [ ("a conditional over int operands is accepted", accepted (returning "flag ? left : right")) + , ("a conditional test may be numeric", accepted (returning "left ? left : right")) + , ("a conditional test must be bool or numeric", rejectedWith "VXT0036" (returning "\"text\" ? left : right")) + , ("conditional results must have one type", rejectedWith "VXT0037" (returning "flag ? left : other")) + , ("conditional results are limited to scalars for now", rejectedWith "VXT0039" (body "bool same = (flag ? \"a\" : \"b\") == \"a\"; return 0;")) + , ("a void call is not a conditional result", rejected (body "flag ? Touch() : Touch(); return 0;")) + , ("a literal first result takes the type of the second", accepted (longBody "long chosen = flag ? 1 : wide; return 0;")) + , ("a literal second result takes the type of the first", accepted (longBody "long chosen = flag ? wide : 1; return 0;")) + , ("an untyped literal pair takes the expected type", accepted (longBody "long chosen = flag ? 1 : 2; return 0;")) + , ("an expected type does not convert a computed result", rejected (longBody "long chosen = flag ? narrow : 2; return 0;")) + , ("a conditional result out of the target range is diagnosed", rejectedWith "VXT0016" (body "byte small = flag ? 300 : 1; return 0;")) + , ("a floating conditional is accepted", accepted (body "double ratio = flag ? 1.5 : 2.5; return 0;")) + , ("a Boolean conditional is accepted", accepted (body "bool chosen = flag ? other : false; return 0;")) + , ("a conditional with a call is an effect-capable statement", accepted (body "flag ? Next() : 0; return 0;")) + , ("a call-free conditional statement is a pure value statement", rejectedWith "VXT0013" (body "flag ? left : right; return 0;")) + , ("truthy coalescing over int operands is accepted", accepted (returning "left ?: right")) + , ("truthy coalescing over bool operands is accepted", accepted (body "bool chosen = flag ?: other; return 0;")) + , ("truthy coalescing operands must have one type", rejectedWith "VXT0038" (returning "left ?: flag")) + , ("truthy coalescing needs a condition-compatible left operand", rejectedWith "VXT0039" (body "bool same = (\"a\" ?: \"b\") == \"a\"; return 0;")) + , ("a literal left operand takes the type of the fallback", accepted (longBody "long chosen = 0 ?: wide; return 0;")) + , ("a literal fallback takes the type of the left operand", accepted (longBody "long chosen = wide ?: 5; return 0;")) + , ("every arithmetic compound assignment type-checks on int", all compoundAccepted ["+=", "-=", "*=", "/=", "//=", "%=", "**=", "<<=", ">>=", "&=", "^=", "|="]) + , ("a compound literal operand takes the target type", accepted (longBody "wide += 1; return 0;")) + , ("a compound assignment cannot modify an immutable binding", rejectedWith "VXT0003" (body "final int fixed = 1; fixed += 1; return fixed;")) + , ("compound operands must match the target type", rejectedWith "VXT0012" (longBody "narrow += wide; return 0;")) + , ("a bitwise compound assignment needs an integer target", rejectedWith "VXT0012" (body "double ratio = 1.5; ratio &= 2.5; return 0;")) + , ("a compound assignment needs a numeric target", rejectedWith "VXT0012" (body "bool state = flag; state += other; return 0;")) + , ("a compound result must have the target type", rejectedWith "VXT0035" (body "double ratio = 7.5; ratio //= 2.0; return 0;")) + , ("a compound assignment to an unknown name is a resolution error", rejected (body "missing += 1; return 0;")) + , ("discard accepts a call", accepted (body "_ = Next(); return 0;")) + , ("discard accepts a void call", accepted (body "_ = Touch(); return 0;")) + , ("discard accepts a pure value", accepted (body "_ = left + right; return 0;")) + , ("discard still type-checks its operand", rejectedWith "VXT0012" (body "_ = left + flag; return 0;")) + , ("the new forms survive template instantiation", templateInstantiates) + , ("template reachability follows calls in both conditional results", templateReachesBothResults) + , ("template reachability follows calls in a coalescing fallback and a discard", templateReachesFallbackAndDiscard) + , ("a closure body may use the conditional forms", accepted closureSource) + ] + +rejected :: String -> Bool +rejected = not . accepted + +compoundAccepted :: String -> Bool +compoundAccepted operatorText = accepted (body ("int value = left; value " ++ operatorText ++ " 2; return value;")) + +longBody :: String -> String +longBody statements = body ("long wide = 5; int narrow = 3; " ++ statements) + +templateSource :: String +templateSource = + unwords + [ "template class Box {" + , "int Entry(_ bool flag, _ int seed) {" + , "int value = seed;" + , "value += 2;" + , "_ = Audit(value);" + , "return flag ? First(value) : Second(seed) ?: Fallback();" + , "}" + , "int First(_ int value) { return value; }" + , "int Second(_ int value) { return value; }" + , "int Fallback() { return 7; }" + , "int Audit(_ int value) { return value; }" + , "int Unused() { return 9; }" + , "}" + ] + +-- | Specialize @Box@ for its @Entry@ member only. +templatePlan :: Maybe TemplateSpecializationPlan +templatePlan = case analyzeSemantics (CompilerInput "conditional-template.vxs" templateSource) of + Right artifacts -> + either + (const Nothing) + Just + ( planTemplateSpecializations + defaultTemplateSpecializationLimits + (semanticTypedAST artifacts) + [ TemplateSpecializationDemand + (TemplateApplication (QualifiedName [Identifier "Box"]) [TypeTemplateArgument stringType]) + (TemplateMemberDemand [Identifier "Entry"]) + "conditional-template" + ] + ) + Left _ -> Nothing + +specializedMembers :: [Declaration ResolvedName Type] +specializedMembers = case templatePlan of + Just plan -> + concat + [ members + | TypeDeclaration {typeMembers = members} <- + map templateSpecializationDeclaration (plannedTemplateSpecializations plan) + ] + Nothing -> [] + +specializedNames :: [String] +specializedNames = map (identifierText . resolvedSpelling . declarationName) specializedMembers + +-- The instantiated declaration is a fresh, closed copy of the template +-- member. Every new node must have been copied by the template passes. +templateInstantiates :: Bool +templateInstantiates = + let closed = show specializedMembers + in all + (`isInfixOf` closed) + ["ConditionalExpression", "CoalesceExpression", "CompoundAssignmentStatement", "DiscardStatement"] + +templateReachesBothResults :: Bool +templateReachesBothResults = all (`elem` specializedNames) ["Entry", "First", "Second"] + +templateReachesFallbackAndDiscard :: Bool +templateReachesFallbackAndDiscard = + all (`elem` specializedNames) ["Fallback", "Audit"] && "Unused" `notElem` specializedNames + +closureSource :: String +closureSource = + unlines + [ "class Program {" + , " public static int Evaluate(_ bool flag, _ int left, _ int right) {" + , " auto choose = \\ -> flag ? left : right ?: 1;" + , " return choose();" + , " }" + , "}" + ] + +-- ---------------------------------------------------------------- lowering + +loweringTests :: [(String, Bool)] +loweringTests = + [ ("a conditional lowers to one Core conditional", conditionalLowers) + , ("truthy coalescing binds its left operand once", coalesceBindsLeftOnce) + , ("truthy coalescing reads the bound value as test and result", coalesceReadsBinding) + , ("every compound operator lowers to its primitive", all compoundLowers compoundPrimitives) + , ("a compound assignment reads its target as the left operand", compoundReadsTarget) + , ("a for update compound assignment lowers into the update region", forUpdateLowers) + , ("discard lowers to an evaluated expression", discardLowers) + , ("the lowered conditional forms verify as Core", loweredCoreVerifies) + , ("Core wire round-trips the conditional forms", wireRoundTrips) + , ("a truncated conditional payload is rejected by the wire reader", wireRejectsTruncation) + , ("a closure captures the operands of its conditional body", closureCapturesOperands) + ] + +compoundPrimitives :: [(String, CorePrimitive)] +compoundPrimitives = + [ ("+=", CoreAdd) + , ("-=", CoreSubtract) + , ("*=", CoreMultiply) + , ("/=", CoreDivide) + , ("//=", CoreFloorDivide) + , ("%=", CoreRemainder) + , ("**=", CorePower) + , ("<<=", CoreShiftLeft) + , (">>=", CoreShiftRight) + , ("&=", CoreBitwiseAnd) + , ("^=", CoreBitwiseXor) + , ("|=", CoreBitwiseOr) + ] + +-- | Unoptimized Core body of the method named @Evaluate@. +loweredBody :: String -> Maybe [CoreStatement] +loweredBody text = case compileSource text of + Right artifacts -> evaluateBody (artifactCore artifacts) + Left _ -> Nothing + +evaluateBody :: CoreModule -> Maybe [CoreStatement] +evaluateBody moduleValue = + case [coreFunctionBody function | function <- coreModuleFunctions moduleValue, named "Evaluate" function] of + statements : _ -> Just statements + [] -> Nothing + +named :: String -> CoreFunction -> Bool +named expected function = identifierText (resolvedSpelling (coreFunctionName function)) == expected + +conditionalLowers :: Bool +conditionalLowers = case loweredBody (returning "flag ? left : right") of + Just [CoreReturn (CoreConditional (CoreVariable condition _) (CoreVariable first _) (CoreVariable second _) valueType)] -> + map spelling [condition, first, second] == ["flag", "left", "right"] && valueType == intType + _ -> False + +coalesceBindsLeftOnce :: Bool +coalesceBindsLeftOnce = case loweredBody (returning "Next() ?: right") of + Just [CoreReturn expression@(CoreLet _ _ CoreApply {} _ _)] -> countCalls expression == 1 + _ -> False + +coalesceReadsBinding :: Bool +coalesceReadsBinding = case loweredBody (returning "Next() ?: right") of + Just [CoreReturn (CoreLet bound _ _ (CoreConditional (CoreVariable test _) (CoreVariable first _) (CoreVariable second _) _) _)] -> + resolvedSymbol test == resolvedSymbol bound + && resolvedSymbol first == resolvedSymbol bound + && spelling second == "right" + && "$coalesce" `isPrefixOf` spelling bound + _ -> False + +compoundLowers :: (String, CorePrimitive) -> Bool +compoundLowers (text, primitive) = + case loweredBody (body ("int value = left; value " ++ text ++ " right; return value;")) of + Just [_, CoreAssign target (CorePrimitive actual [_, _] valueType), _] -> + spelling target == "value" && actual == primitive && valueType == intType + _ -> False + +compoundReadsTarget :: Bool +compoundReadsTarget = case loweredBody (body "int value = left; value -= right; return value;") of + Just [_, CoreAssign target (CorePrimitive CoreSubtract [CoreVariable current _, CoreVariable operand _] _), _] -> + resolvedSymbol current == resolvedSymbol target && spelling operand == "right" + _ -> False + +forUpdateLowers :: Bool +forUpdateLowers = + case loweredBody (body "int total = 0; for (int index = 0; index < left; index += 2) { total += index; } return total;") of + Just [_, _, CoreFor _ [CoreAssign _ (CorePrimitive CoreAdd _ _)] [CoreAssign index (CorePrimitive CoreAdd _ _)], _] -> + spelling index == "index" + _ -> False + +discardLowers :: Bool +discardLowers = case loweredBody (body "_ = Next(); return 0;") of + Just [CoreEvaluate CoreApply {}, CoreReturn _] -> True + _ -> False + +allFormsSource :: String +allFormsSource = + body $ + unwords + [ "int value = flag ? left : right;" + , "value += other ? 1 : 2;" + , "value = Twice(value) ?: Next();" + , "_ = Countdown(value);" + , "for (int index = 0; index < left; index += 1) { value -= index ?: 1; }" + , "return flag ? other ? value : left : right ?: 3;" + ] + +loweredCoreVerifies :: Bool +loweredCoreVerifies = case compileSource allFormsSource of + Right artifacts -> + verifyCore (artifactCore artifacts) == Right (artifactCore artifacts) + && verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts) + Left _ -> False + +wireRoundTrips :: Bool +wireRoundTrips = case compileSource allFormsSource of + Right artifacts -> + let core = artifactCore artifacts + in moduleHasConditional core + && (encodeCore defaultCoreWireLimits core >>= decodeCore defaultCoreWireLimits) == Right core + Left _ -> False + +wireRejectsTruncation :: Bool +wireRejectsTruncation = case encodeCore defaultCoreWireLimits conditionalModule of + Right bytes -> + -- Every proper prefix must fail: a conditional has no count that a + -- shorter payload could satisfy. + all (isLeft . decodeCore defaultCoreWireLimits) [take size bytes | size <- [0 .. length bytes - 1]] + && decodeCore defaultCoreWireLimits bytes == Right conditionalModule + Left _ -> False + where + isLeft = either (const True) (const False) + +closureCapturesOperands :: Bool +closureCapturesOperands = case compileSource closureSource of + Right artifacts -> + let captured = + [ spelling (coreCaptureName capture) + | function <- coreModuleFunctions (artifactCore artifacts) + , CoreClosure captures _ _ _ _ <- concatMap statementExpressions (coreFunctionBody function) + , capture <- captures + ] + in captured == ["flag", "left", "right"] + Left _ -> False + +spelling :: ResolvedName -> String +spelling = identifierText . resolvedSpelling + +-- | Top-level expressions of a statement list, without descending into them. +statementExpressions :: CoreStatement -> [CoreExpression] +statementExpressions statement = case statement of + CoreBind binding -> [coreBindingValue binding] + CoreAssign _ value -> [value] + CoreReturn value -> [value] + CoreEvaluate value -> [value] + CoreIf condition yes no -> condition : concatMap statementExpressions (yes ++ no) + CoreWhile condition loopBody -> condition : concatMap statementExpressions loopBody + CoreDoWhile loopBody condition -> concatMap statementExpressions loopBody ++ [condition] + CoreFor condition loopBody update -> condition : concatMap statementExpressions (loopBody ++ update) + CoreBreak -> [] + CoreContinue -> [] + +-- | Every expression node reachable from an expression, itself included. +subexpressions :: CoreExpression -> [CoreExpression] +subexpressions expression = + expression : case expression of + CoreVariable {} -> [] + CoreLiteral {} -> [] + CoreApply callee arguments _ -> concatMap subexpressions (callee : arguments) + CorePrimitive _ arguments _ -> concatMap subexpressions arguments + CoreLet _ _ value letBody _ -> subexpressions value ++ subexpressions letBody + CoreConditional condition whenTrue whenFalse _ -> concatMap subexpressions [condition, whenTrue, whenFalse] + CoreClosure captures _ _ closureBody _ -> + concatMap (subexpressions . coreCaptureValue) captures + ++ concatMap subexpressions (concatMap statementExpressions closureBody) + +moduleExpressions :: CoreModule -> [CoreExpression] +moduleExpressions moduleValue = + concatMap + subexpressions + (concatMap statementExpressions (concatMap coreFunctionBody (coreModuleFunctions moduleValue))) + +countCalls :: CoreExpression -> Int +countCalls expression = length [() | CoreApply {} <- subexpressions expression] + +isConditional :: CoreExpression -> Bool +isConditional expression = case expression of + CoreConditional {} -> True + _ -> False + +moduleHasConditional :: CoreModule -> Bool +moduleHasConditional = any isConditional . moduleExpressions + +-- ---------------------------------------------------------------- Core verifier + +verifierTests :: [(String, Bool)] +verifierTests = + [ ("the Core verifier accepts a well-typed conditional", verifyCore conditionalModule == Right conditionalModule) + , ("the Core verifier rejects a non-condition test", coreRejectedWith "VXC1067" (conditionalOf text one two intType)) + , ("the Core verifier rejects a mistyped first result", coreRejectedWith "VXC1068" (conditionalOf flagRead truth two intType)) + , ("the Core verifier rejects a mistyped second result", coreRejectedWith "VXC1069" (conditionalOf flagRead one truth intType)) + , ("the Core verifier rejects a non-scalar result", coreRejectedWith "VXC1070" (conditionalOf flagRead text text stringType)) + , ("the Core verifier rejects a void result", coreRejectedWith "VXC1070" (conditionalOf flagRead unit unit unitType)) + , ("the Core verifier checks names inside both results", coreRejectedWith "VXC1020" (conditionalOf flagRead one missing intType)) + , ("the Core verifier checks names inside the test", coreRejectedWith "VXC1020" (conditionalOf missingFlag one two intType)) + , ("a let inside a result does not leak into the other result", coreRejectedWith "VXC1020" leakingLet) + ] + where + text = CoreLiteral (CoreString "text") stringType + truth = CoreLiteral (CoreBoolean True) boolType + unit = CoreLiteral CoreUnit unitType + missing = CoreVariable (ResolvedName (SymbolId 90) (Identifier "missing")) intType + missingFlag = CoreVariable (ResolvedName (SymbolId 91) (Identifier "missing")) boolType + scoped = ResolvedName (SymbolId 50) (Identifier "scoped") + leakingLet = + conditionalOf + flagRead + (CoreLet scoped intType one (CoreVariable scoped intType) intType) + (CoreVariable scoped intType) + intType + +flagName :: ResolvedName +flagName = ResolvedName (SymbolId 2) (Identifier "flag") + +flagRead :: CoreExpression +flagRead = CoreVariable flagName boolType + +one, two :: CoreExpression +one = CoreLiteral (CoreInteger 1) intType +two = CoreLiteral (CoreInteger 2) intType + +-- | A one-function module returning the given conditional. +conditionalOf :: CoreExpression -> CoreExpression -> CoreExpression -> Type -> CoreModule +conditionalOf condition whenTrue whenFalse valueType = + CoreModuleWithSources + (QualifiedName [Identifier "Conditional"]) + [ CoreFunction + (ResolvedName (SymbolId 1) (Identifier "Choose")) + [(flagName, boolType)] + valueType + [CoreReturn (CoreConditional condition whenTrue whenFalse valueType)] + ] + [] + [] + +conditionalModule :: CoreModule +conditionalModule = conditionalOf flagRead one two intType + +coreRejectedWith :: String -> CoreModule -> Bool +coreRejectedWith code moduleValue = case verifyCore moduleValue of + Left problems -> any ((== code) . diagnosticCode) problems + Right _ -> False + +-- ---------------------------------------------------------------- optimizer + +optimizerTests :: [(String, Bool)] +optimizerTests = + [ ("a constant true test leaves only the first result", foldsTo "true ? left : right" "left") + , ("a constant false test leaves only the second result", foldsTo "false ? left : right" "right") + , ("a numeric constant test selects by truth", foldsTo "0 ? left : right" "right") + , ("a non-constant test keeps the conditional", optimizedHasConditional (returning "flag ? left : right")) + , ("a truthy constant left operand removes the fallback", coalesceKeepsConstantLeft) + , ("a zero left operand selects the fallback", coalesceSelectsFallback) + , ("constants fold inside a kept conditional", constantsFoldInResults) + , ("a pure discarded value disappears", optimizedBodyLength (body "_ = left + right; return 0;") == Just 1) + , ("a recursive call stays behind its conditional", optimizedKeepsRecursiveCall) + , ("the optimized conditional forms still verify", optimizedVerifies) + ] + +optimizedBody :: String -> Maybe [CoreStatement] +optimizedBody text = case compileSource text of + Right artifacts -> evaluateBody (artifactOptimizedCore artifacts) + Left _ -> Nothing + +optimizedBodyLength :: String -> Maybe Int +optimizedBodyLength = fmap length . optimizedBody + +foldsTo :: String -> String -> Bool +foldsTo expression expected = case optimizedBody (returning expression) of + Just [CoreReturn (CoreVariable name _)] -> spelling name == expected + _ -> False + +-- The constant folder propagates the bound literal into the body but keeps +-- the now unread binding; only the conditional itself must be gone. +coalesceKeepsConstantLeft :: Bool +coalesceKeepsConstantLeft = case optimizedBody (returning "5 ?: right") of + Just [CoreReturn (CoreLet _ _ (CoreLiteral (CoreInteger 5) _) (CoreLiteral (CoreInteger 5) _) _)] -> True + _ -> False + +coalesceSelectsFallback :: Bool +coalesceSelectsFallback = case optimizedBody (returning "0 ?: right") of + Just [CoreReturn (CoreLet _ _ (CoreLiteral (CoreInteger 0) _) (CoreVariable name _) _)] -> spelling name == "right" + _ -> False + +optimizedHasConditional :: String -> Bool +optimizedHasConditional text = case optimizedBody text of + Just statements -> any isConditional (concatMap subexpressions (concatMap statementExpressions statements)) + Nothing -> False + +constantsFoldInResults :: Bool +constantsFoldInResults = case optimizedBody (returning "flag ? 2 + 3 : 4 * 5") of + Just [CoreReturn (CoreConditional _ (CoreLiteral (CoreInteger 5) _) (CoreLiteral (CoreInteger 20) _) _)] -> True + _ -> False + +optimizedKeepsRecursiveCall :: Bool +optimizedKeepsRecursiveCall = case compileSource (returning "Countdown(left)") of + Right artifacts -> + case [coreFunctionBody function | function <- coreModuleFunctions (artifactOptimizedCore artifacts), named "Countdown" function] of + [[CoreReturn (CoreConditional _ whenTrue (CoreLiteral (CoreInteger 0) _) _)]] -> countCalls whenTrue == 1 + _ -> False + Left _ -> False + +optimizedVerifies :: Bool +optimizedVerifies = case compileSource allFormsSource of + Right artifacts -> verifyCore (artifactOptimizedCore artifacts) == Right (artifactOptimizedCore artifacts) + Left _ -> False + +-- ---------------------------------------------------------------- CorePrep + +corePrepTests :: [(String, Bool)] +corePrepTests = + [ ("a conditional lowers to a branch, two arms and a join", conditionalHasFourBlocks) + , ("both arms assign the result slot and jump to the join", armsAssignAndJoin) + , ("the result slot has exactly one mutable seed", slotHasOneSeed) + , ("the join returns the result slot", joinReturnsSlot) + , ("a numeric test is compared with zero before the branch", numericTestIsBooleanized) + , ("a Boolean test is branched on directly", booleanTestHasNoComparison) + , ("a floating result slot starts from a floating zero", floatingSlotSeed) + , ("a Boolean result slot starts from false", booleanSlotSeed) + , ("a call in a result is emitted only in that arm", callStaysInItsArm) + , ("nested conditionals create one region each", nestedConditionalRegions) + , ("truthy coalescing evaluates its fallback behind a branch", coalesceFallbackIsConditional) + , ("a conditional in a loop condition keeps the back-edge", conditionalInLoopCondition) + , ("every generated block id is unique", all blockIdsAreUnique (preparedFunctions allFormsSource)) + , ("every branch and jump target exists", all targetsExist (preparedFunctions allFormsSource)) + , ("CorePrep of the conditional forms survives its verifier", preparedVerifies) + , ("a discarded call stays one result-dropping instruction", discardedCallIsEvaluated) + , ("a discarded non-call value is computed into a temporary", discardedOperatorIsBound) + , ("a discarded division still executes", discardedDivisionIsBound) + , ("a conditional statement leaves no result-dropping copy", conditionalStatementHasNoEvaluate) + , ("a discarded closure creation is bound, not dropped", discardedClosureIsBound) + ] + +preparedFunctions :: String -> [CorePrepFunction] +preparedFunctions text = case compileSource text of + Right artifacts -> corePrepModuleFunctions (artifactCorePrep artifacts) + Left _ -> [] + +preparedNamed :: String -> String -> Maybe CorePrepFunction +preparedNamed name text = + case [function | function <- preparedFunctions text, spelling (corePrepFunctionName function) == name] of + function : _ -> Just function + [] -> Nothing + +prepared :: String -> Maybe CorePrepFunction +prepared = preparedNamed "Evaluate" + +blocksOf :: String -> [CorePrepBlock] +blocksOf = maybe [] corePrepFunctionBlocks . prepared + +instructionsOf :: String -> [CorePrepInstruction] +instructionsOf = concatMap corePrepBlockInstructions . blocksOf + +simple :: String +simple = returning "flag ? left : right" + +conditionalHasFourBlocks :: Bool +conditionalHasFourBlocks = case blocksOf simple of + [entry, whenTrue, whenFalse, joinBlock] -> + corePrepBlockTerminator entry + `isBranchTo` (corePrepBlockId whenTrue, corePrepBlockId whenFalse) + && corePrepBlockTerminator whenTrue == CorePrepJump (corePrepBlockId joinBlock) + && corePrepBlockTerminator whenFalse == CorePrepJump (corePrepBlockId joinBlock) + _ -> False + +isBranchTo :: CorePrepTerminator -> (Int, Int) -> Bool +isBranchTo terminator (expectedTrue, expectedFalse) = case terminator of + CorePrepBranch _ actualTrue actualFalse -> actualTrue == expectedTrue && actualFalse == expectedFalse + _ -> False + +armsAssignAndJoin :: Bool +armsAssignAndJoin = case blocksOf simple of + [_, whenTrue, whenFalse, _] -> + map assignedFrom (corePrepBlockInstructions whenTrue) == [Just "left"] + && map assignedFrom (corePrepBlockInstructions whenFalse) == [Just "right"] + _ -> False + where + assignedFrom instruction = case instruction of + CorePrepAssign target (CorePrepVariable value _) | isSlot target -> Just (spelling value) + _ -> Nothing + +isSlot :: ResolvedName -> Bool +isSlot name = "$conditional" `isPrefixOf` spelling name + +slotSeeds :: [CorePrepInstruction] -> [(Type, CorePrepOperation)] +slotSeeds instructions = + [(valueType, operation) | CorePrepBind name valueType True operation <- instructions, isSlot name] + +slotHasOneSeed :: Bool +slotHasOneSeed = case blocksOf simple of + entry : rest -> + slotSeeds (corePrepBlockInstructions entry) == [(intType, CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType))] + && null (slotSeeds (concatMap corePrepBlockInstructions rest)) + [] -> False + +joinReturnsSlot :: Bool +joinReturnsSlot = case reverse (blocksOf simple) of + joinBlock : _ -> case corePrepBlockTerminator joinBlock of + CorePrepReturn (CorePrepVariable name _) -> isSlot name + _ -> False + [] -> False + +comparisons :: [CorePrepInstruction] -> Int +comparisons instructions = + length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreNotEqual _) <- instructions] + +numericTestIsBooleanized :: Bool +numericTestIsBooleanized = comparisons (instructionsOf (returning "left ? left : right")) == 1 + +booleanTestHasNoComparison :: Bool +booleanTestHasNoComparison = comparisons (instructionsOf simple) == 0 + +floatingSlotSeed :: Bool +floatingSlotSeed = + map snd (slotSeeds (instructionsOf (body "double ratio = flag ? 1.5 : 2.5; return ratio > 2.0 ? 1 : 0;"))) + == [ CorePrepCopy (CorePrepLiteral (CoreFloating "0") doubleType) + , CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType) + ] + where + doubleType = namedType "double" + +booleanSlotSeed :: Bool +booleanSlotSeed = + map snd (slotSeeds (instructionsOf (body "bool chosen = flag ? other : left > right; return chosen ? 1 : 0;"))) + == [ CorePrepCopy (CorePrepLiteral (CoreBoolean False) boolType) + , CorePrepCopy (CorePrepLiteral (CoreInteger 0) intType) + ] + +isCall :: CorePrepInstruction -> Bool +isCall instruction = case instruction of + CorePrepBind _ _ _ CorePrepCall {} -> True + _ -> False + +callStaysInItsArm :: Bool +callStaysInItsArm = case maybe [] corePrepFunctionBlocks (preparedNamed "Countdown" simple) of + [entry, whenTrue, whenFalse, joinBlock] -> + not (any isCall (corePrepBlockInstructions entry)) + && length (filter isCall (corePrepBlockInstructions whenTrue)) == 1 + && not (any isCall (corePrepBlockInstructions whenFalse)) + && not (any isCall (corePrepBlockInstructions joinBlock)) + _ -> False + +branches :: [CorePrepBlock] -> Int +branches blocks = length [() | CorePrepBranch {} <- map corePrepBlockTerminator blocks] + +nestedConditionalRegions :: Bool +nestedConditionalRegions = + let blocks = blocksOf (returning "flag ? other ? left : right : 0") + in branches blocks == 2 + && length (slotSeeds (concatMap corePrepBlockInstructions blocks)) == 2 + && length blocks == 7 + +coalesceFallbackIsConditional :: Bool +coalesceFallbackIsConditional = + case blocksOf (returning "left ?: Countdown(right)") of + [entry, whenTrue, whenFalse, _] -> + not (any isCall (corePrepBlockInstructions entry)) + && not (any isCall (corePrepBlockInstructions whenTrue)) + && length (filter isCall (corePrepBlockInstructions whenFalse)) == 1 + _ -> False + +conditionalInLoopCondition :: Bool +conditionalInLoopCondition = + let blocks = blocksOf (body "int value = left; while (flag ? value > 0 : value > right) { value -= 1; } return value;") + backward block = case corePrepBlockTerminator block of + CorePrepJump target -> target < corePrepBlockId block + _ -> False + in branches blocks == 2 && length (filter backward blocks) == 1 + +evaluations :: [CorePrepInstruction] -> [CorePrepOperation] +evaluations instructions = [operation | CorePrepEvaluate operation <- instructions] + +discardedCallIsEvaluated :: Bool +discardedCallIsEvaluated = + case evaluations (instructionsOf (body "_ = Countdown(left); return 0;")) of + [CorePrepCall {}] -> True + _ -> False + +-- `_ = !Countdown(left);` once reached later stages as a discarded bitwise +-- instruction, which only calls and closure creations may be. +discardedOperatorIsBound :: Bool +discardedOperatorIsBound = + let instructions = instructionsOf (body "_ = !Countdown(left); return 0;") + in null (evaluations instructions) + && length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreBitwiseNot _) <- instructions] == 1 + && length (filter isCall instructions) == 1 + +discardedDivisionIsBound :: Bool +discardedDivisionIsBound = + let instructions = instructionsOf (body "_ = 12 / left; return 0;") + in null (evaluations instructions) + && length [() | CorePrepBind _ _ _ (CorePrepPrimitive CoreDivide _) <- instructions] == 1 + +conditionalStatementHasNoEvaluate :: Bool +conditionalStatementHasNoEvaluate = + let blocks = blocksOf (body "flag ? Countdown(left) : 0; return 0;") + instructions = concatMap corePrepBlockInstructions blocks + in null (evaluations instructions) && branches blocks == 1 && length (filter isCall instructions) == 1 + +discardedClosureIsBound :: Bool +discardedClosureIsBound = + let instructions = instructionsOf (body "_ = \\ -> left; return 0;") + in null (evaluations instructions) + && length [() | CorePrepBind _ _ _ CorePrepMakeClosure {} <- instructions] == 1 + +blockIdsAreUnique :: CorePrepFunction -> Bool +blockIdsAreUnique function = + let identifiers = map corePrepBlockId (corePrepFunctionBlocks function) + in all (\identifier -> length (filter (== identifier) identifiers) == 1) identifiers + +targetsExist :: CorePrepFunction -> Bool +targetsExist function = + let blocks = corePrepFunctionBlocks function + identifiers = map corePrepBlockId blocks + targets block = case corePrepBlockTerminator block of + CorePrepJump target -> [target] + CorePrepBranch _ whenTrue whenFalse -> [whenTrue, whenFalse] + _ -> [] + in all (`elem` identifiers) (concatMap targets blocks) + +preparedVerifies :: Bool +preparedVerifies = case compileSource allFormsSource of + Right artifacts -> verifyCorePrep (artifactCorePrep artifacts) == Right (artifactCorePrep artifacts) + Left _ -> False diff --git a/Compiler/Haskell/Driver/test/CoreOptimizerSourceTests.hs b/Compiler/Haskell/Driver/test/CoreOptimizerSourceTests.hs index 03821a4d..affed31e 100644 --- a/Compiler/Haskell/Driver/test/CoreOptimizerSourceTests.hs +++ b/Compiler/Haskell/Driver/test/CoreOptimizerSourceTests.hs @@ -470,6 +470,7 @@ maximumModuleSymbol moduleValue = maximum (0 : concatMap functionSymbols (coreMo CoreApply callee arguments _ -> concatMap expressionNames (callee : arguments) CorePrimitive _ arguments _ -> concatMap expressionNames arguments CoreLet name _ value body _ -> name : expressionNames value ++ expressionNames body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionNames [condition, whenTrue, whenFalse] CoreClosure captures parameters _ body _ -> map coreCaptureName captures ++ map fst parameters @@ -729,5 +730,6 @@ expressionHasDivision expression = case expression of (elem operator [CoreDivide, CoreFloorDivide, CoreRemainder] && elem valueType (map namedType coreIntegerTypeNames)) || any expressionHasDivision arguments CoreLet _ _ value body _ -> expressionHasDivision value || expressionHasDivision body + CoreConditional condition whenTrue whenFalse _ -> any expressionHasDivision [condition, whenTrue, whenFalse] CoreClosure captures _ _ body _ -> any (expressionHasDivision . coreCaptureValue) captures || containsIntegerDivision body diff --git a/Compiler/Haskell/Driver/test/IntegerFlowTests.hs b/Compiler/Haskell/Driver/test/IntegerFlowTests.hs index 20542a6c..5e8b3227 100644 --- a/Compiler/Haskell/Driver/test/IntegerFlowTests.hs +++ b/Compiler/Haskell/Driver/test/IntegerFlowTests.hs @@ -565,6 +565,7 @@ expressionContainsPrimitive primitive expression = case expression of current == primitive || any (expressionContainsPrimitive primitive) arguments CoreLet _ _ value body _ -> expressionContainsPrimitive primitive value || expressionContainsPrimitive primitive body + CoreConditional condition whenTrue whenFalse _ -> any (expressionContainsPrimitive primitive) [condition, whenTrue, whenFalse] CoreClosure captures _ _ body _ -> any (expressionContainsPrimitive primitive . coreCaptureValue) captures || containsPrimitiveInStatements primitive body @@ -591,6 +592,7 @@ expressionContainsCall expression = case expression of CoreApply {} -> True CorePrimitive _ arguments _ -> any expressionContainsCall arguments CoreLet _ _ value body _ -> expressionContainsCall value || expressionContainsCall body + CoreConditional condition whenTrue whenFalse _ -> any expressionContainsCall [condition, whenTrue, whenFalse] CoreClosure captures _ _ body _ -> any (expressionContainsCall . coreCaptureValue) captures || any statementContainsCall body diff --git a/Compiler/Haskell/Driver/test/IterationTests.hs b/Compiler/Haskell/Driver/test/IterationTests.hs index 53acf785..099d3330 100644 --- a/Compiler/Haskell/Driver/test/IterationTests.hs +++ b/Compiler/Haskell/Driver/test/IterationTests.hs @@ -34,7 +34,7 @@ iterationTests = , ("continue outside a loop is rejected", continueOutsideLoopIsRejected) , ("value-carrying break remains an explicit unsupported feature", valuedBreakIsRejected) , ("enumerable for remains guarded by its missing generator ABI", forEachIsRejected) - , ("Core v7 round-trips all structured loop statement tags", loopCoreRoundTrips) + , ("Core v8 round-trips all structured loop statement tags", loopCoreRoundTrips) , ("loop CorePrep survives its verifier", loopCorePrepVerifies) ] diff --git a/Compiler/Haskell/Driver/test/LoopFlowTests.hs b/Compiler/Haskell/Driver/test/LoopFlowTests.hs index 5926e510..821dd2fc 100644 --- a/Compiler/Haskell/Driver/test/LoopFlowTests.hs +++ b/Compiler/Haskell/Driver/test/LoopFlowTests.hs @@ -452,6 +452,7 @@ expressionContainsDivision name expression = case expression of CorePrimitive _ arguments _ -> any (expressionContainsDivision name) arguments CoreLet _ _ value body _ -> expressionContainsDivision name value || expressionContainsDivision name body + CoreConditional condition whenTrue whenFalse _ -> any (expressionContainsDivision name) [condition, whenTrue, whenFalse] CoreClosure captures _ _ body _ -> any (expressionContainsDivision name . coreCaptureValue) captures || any (statementContainsDivision name) body diff --git a/Compiler/Haskell/Driver/test/Main.hs b/Compiler/Haskell/Driver/test/Main.hs index d229a7af..93a909a1 100644 --- a/Compiler/Haskell/Driver/test/Main.hs +++ b/Compiler/Haskell/Driver/test/Main.hs @@ -3,6 +3,7 @@ module Main (main) where import ClosureTests (closureTests) +import ConditionalExpressionTests (conditionalExpressionTests) import CompileTimeParityTests (compileTimeParityTests) import Control.Exception (finally) import CoreInliningTests (coreInliningTests) @@ -114,8 +115,8 @@ main = do check "Core wire rejects trailing bytes" coreWireRejectsTrailingInput check "Core wire rejects unresolved types" coreWireRejectsUnresolvedType check "Core wire preserves Unicode scalar values" coreWirePreservesUnicode - check "Core wire v7 provenance fields remain stable" coreWireGoldenDocument - check "Core wire v7 preserves non-empty source-owner field order" coreWireProjectSourceGolden + check "Core wire v8 provenance fields remain stable" coreWireGoldenDocument + check "Core wire v8 preserves non-empty source-owner field order" coreWireProjectSourceGolden check "CorePrep wire codec round-trips the frontend result" wireRoundTrip check "CorePrep wire codec rejects truncated input" wireRejectsTruncation check "CorePrep wire codec rejects trailing input" wireRejectsTrailingInput @@ -163,6 +164,7 @@ main = do mapM_ (uncurry check) parserContractTests mapM_ (uncurry check) scalarWireTests mapM_ (uncurry check) shortCircuitTests + mapM_ (uncurry check) conditionalExpressionTests mapM_ (uncurry check) specializationTests mapM_ (uncurry check) voidTests @@ -530,7 +532,7 @@ coreWireGoldenDocument = moduleValue = CoreModuleWithSources (QualifiedName [Identifier "Demo"]) [mainFunction] [] [] bytes = upgradeSimpleV5 - 0x07 + 0x08 [ 0x56 , 0x58 , 0x43 @@ -625,7 +627,7 @@ coreWireProjectSourceGolden = [(symbolIdValue (resolvedSymbol mainName), source)] goldenHex = unwords - [ "56 58 43 52 07 00 00 00 01 00 00 00" + [ "56 58 43 52 08 00 00 00 01 00 00 00" , "04 00 00 00 44 00 00 00 65 00 00 00" , "6d 00 00 00 6f 00 00 00 01 00 00 00" , "10 00 00 00 53 00 00 00 6f 00 00 00" @@ -656,7 +658,7 @@ coreWireProjectSourceGolden = _ -> Nothing -- Adding the empty source catalog and function owner to the compact v5 golden --- shape gives an independent byte-level v7 expectation. The owner follows the +-- shape gives an independent byte-level v8 expectation. The owner follows the -- function symbol, before its parameter vector, matching the wire contract. upgradeSimpleV5 :: Word8 -> [Word8] -> [Word8] upgradeSimpleV5 currentVersion v5Bytes = @@ -828,7 +830,7 @@ coreArtifactRoundTrip = case compile sample of Left _ -> pure False Right artifacts -> do temporary <- getTemporaryDirectory - let path = temporary "visual-xsharp-core-wire-v7.core" + let path = temporary "visual-xsharp-core-wire-v8.core" cleanup = doesFileExist path >>= \exists -> if exists then removeFile path else pure () value = artifactOptimizedCore artifacts ( do diff --git a/Compiler/Haskell/Driver/test/PatternTests.hs b/Compiler/Haskell/Driver/test/PatternTests.hs index 1debc3ce..ed7abcee 100644 --- a/Compiler/Haskell/Driver/test/PatternTests.hs +++ b/Compiler/Haskell/Driver/test/PatternTests.hs @@ -150,6 +150,7 @@ countCalls expression = case expression of CoreApply callee arguments _ -> 1 + countCalls callee + sum (map countCalls arguments) CorePrimitive _ arguments _ -> sum (map countCalls arguments) CoreLet _ _ value body _ -> countCalls value + countCalls body + CoreConditional condition whenTrue whenFalse _ -> sum (map countCalls [condition, whenTrue, whenFalse]) CoreClosure captures _ _ body _ -> sum (map (countCalls . coreCaptureValue) captures) + sum (map statementCalls body) @@ -175,6 +176,7 @@ expressionSymbols expression = case expression of CoreApply callee arguments _ -> expressionSymbols callee ++ concatMap expressionSymbols arguments CorePrimitive _ arguments _ -> concatMap expressionSymbols arguments CoreLet _ _ value body _ -> expressionSymbols value ++ expressionSymbols body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionSymbols [condition, whenTrue, whenFalse] CoreClosure captures _ _ body _ -> concatMap (expressionSymbols . coreCaptureValue) captures ++ concatMap statementSymbols body diff --git a/Compiler/Haskell/Driver/test/ScalarWireTests.hs b/Compiler/Haskell/Driver/test/ScalarWireTests.hs index 086f96d7..0cd1f19d 100644 --- a/Compiler/Haskell/Driver/test/ScalarWireTests.hs +++ b/Compiler/Haskell/Driver/test/ScalarWireTests.hs @@ -24,7 +24,7 @@ scalarWireTests = versionTests :: [(String, Bool)] versionTests = - [ ("Core wire current version is 7", currentCoreWireVersion == CoreWireVersion 7) + [ ("Core wire current version is 8", currentCoreWireVersion == CoreWireVersion 8) , ("CorePrep wire current version is 6", currentWireVersion == WireVersion 6) , ("Core numeric payload default is bounded", maximumCoreNumericBytes defaultCoreWireLimits == 4096) , ("CorePrep numeric payload default is bounded", maximumNumericBytes defaultWireLimits == 4096) @@ -286,7 +286,8 @@ discardedFloorDivideModule leftType left rightType right = malformedWireTests :: [(String, Bool)] malformedWireTests = [ ("Core wire rejects v2 input", rejectsCoreVersion 2) - , ("Core wire rejects future input", rejectsCoreVersion 8) + , ("Core wire rejects the previous v7 schema", rejectsCoreVersion 7) + , ("Core wire rejects future input", rejectsCoreVersion 9) , ("CorePrep wire rejects v2 input", rejectsCorePrepVersion 2) , ("CorePrep wire rejects future input", rejectsCorePrepVersion 7) , ("Core wire enforces numeric byte limit", coreNumericLimit) diff --git a/Compiler/Haskell/Driver/test/StaticMemberCoreTests.hs b/Compiler/Haskell/Driver/test/StaticMemberCoreTests.hs index 0231da77..86efd608 100644 --- a/Compiler/Haskell/Driver/test/StaticMemberCoreTests.hs +++ b/Compiler/Haskell/Driver/test/StaticMemberCoreTests.hs @@ -361,6 +361,7 @@ callArgumentsFor spelling moduleValue = concatMap (functionCalls . coreFunctionB CoreApply callee arguments _ -> expressionCalls callee ++ concatMap expressionCalls arguments CorePrimitive _ arguments _ -> concatMap expressionCalls arguments CoreLet _ _ value body _ -> expressionCalls value ++ expressionCalls body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionCalls [condition, whenTrue, whenFalse] CoreClosure _ _ _ statements _ -> concatMap statementCalls statements callTypesFor :: String -> CoreModule -> [Type] @@ -387,6 +388,7 @@ callTypesFor spelling moduleValue = concatMap (functionCallTypes . coreFunctionB CoreApply callee arguments _ -> expressionTypes callee ++ concatMap expressionTypes arguments CorePrimitive _ arguments _ -> concatMap expressionTypes arguments CoreLet _ _ value body _ -> expressionTypes value ++ expressionTypes body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTypes [condition, whenTrue, whenFalse] CoreClosure _ _ _ statements _ -> concatMap statementTypes statements coreCallTargets :: CoreModule -> [ResolvedName] @@ -412,6 +414,7 @@ expressionTargets expression = case expression of CoreApply callee arguments _ -> calledTarget callee ++ expressionTargets callee ++ concatMap expressionTargets arguments CorePrimitive _ arguments _ -> concatMap expressionTargets arguments CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse] CoreClosure _ _ _ body _ -> concatMap statementTargets body where calledTarget (CoreVariable name _) = [name] diff --git a/Compiler/Haskell/Driver/test/StaticMemberOverloadTests.hs b/Compiler/Haskell/Driver/test/StaticMemberOverloadTests.hs index 8be0f378..8cd48bb5 100644 --- a/Compiler/Haskell/Driver/test/StaticMemberOverloadTests.hs +++ b/Compiler/Haskell/Driver/test/StaticMemberOverloadTests.hs @@ -296,6 +296,8 @@ callsInStatement statement = case statement of ++ callsInBlock body ForEachStatement _ _ _ _ _ source body -> callsInExpression source ++ callsInBlock body IncrementStatement {} -> [] + CompoundAssignmentStatement _ _ _ _ value -> callsInExpression value + DiscardStatement _ value -> callsInExpression value BreakStatement _ value -> maybe [] callsInExpression value ContinueStatement {} -> [] ExpressionStatement _ value _ -> callsInExpression value @@ -312,6 +314,8 @@ callsInExpression expression = case expression of UnaryExpression _ _ value _ -> callsInExpression value BinaryExpression _ _ left right _ -> callsInExpression left ++ callsInExpression right IsPatternExpression _ subject _ _ -> callsInExpression subject + ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap callsInExpression [condition, whenTrue, whenFalse] + CoalesceExpression _ left fallback _ -> callsInExpression left ++ callsInExpression fallback CallableExpression _ _ _ _ body _ -> callsInCallableBody body where isSelectedCall (CallExpression _ (NameExpression _ name _) _ _) = resolvedSpelling name == Identifier "Select" diff --git a/Compiler/Haskell/Driver/test/StaticMemberParserTests.hs b/Compiler/Haskell/Driver/test/StaticMemberParserTests.hs index b82a1a6f..6f2cc209 100644 --- a/Compiler/Haskell/Driver/test/StaticMemberParserTests.hs +++ b/Compiler/Haskell/Driver/test/StaticMemberParserTests.hs @@ -427,6 +427,8 @@ spanOf expression = case expression of UnaryExpression spanValue _ _ _ -> spanValue BinaryExpression spanValue _ _ _ _ -> spanValue IsPatternExpression spanValue _ _ _ -> spanValue + ConditionalExpression spanValue _ _ _ _ -> spanValue + CoalesceExpression spanValue _ _ _ -> spanValue CallableExpression spanValue _ _ _ _ _ -> spanValue isLeft :: Either [a] b -> Bool -> Bool diff --git a/Compiler/Haskell/Driver/test/StaticMemberProjectTests.hs b/Compiler/Haskell/Driver/test/StaticMemberProjectTests.hs index b0caa19a..c2445aa7 100644 --- a/Compiler/Haskell/Driver/test/StaticMemberProjectTests.hs +++ b/Compiler/Haskell/Driver/test/StaticMemberProjectTests.hs @@ -235,6 +235,7 @@ expressionTargets expression = case expression of CoreApply callee arguments _ -> target callee ++ expressionTargets callee ++ concatMap expressionTargets arguments CorePrimitive _ arguments _ -> concatMap expressionTargets arguments CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse] CoreClosure _ _ _ body _ -> concatMap statementTargets body where target (CoreVariable name _) = [name] diff --git a/Compiler/Haskell/Driver/test/StaticMemberSemanticTests.hs b/Compiler/Haskell/Driver/test/StaticMemberSemanticTests.hs index bc629df8..d8e7472d 100644 --- a/Compiler/Haskell/Driver/test/StaticMemberSemanticTests.hs +++ b/Compiler/Haskell/Driver/test/StaticMemberSemanticTests.hs @@ -536,6 +536,8 @@ statementCalls statement = case statement of ++ blockCalls body ForEachStatement _ _ _ _ _ source body -> expressionCalls source ++ blockCalls body IncrementStatement {} -> [] + CompoundAssignmentStatement _ _ _ _ value -> expressionCalls value + DiscardStatement _ value -> expressionCalls value BreakStatement _ value -> maybe [] expressionCalls value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionCalls value @@ -550,6 +552,8 @@ expressionCalls expression = case expression of UnaryExpression _ _ value _ -> expressionCalls value BinaryExpression _ _ left right _ -> expressionCalls left ++ expressionCalls right IsPatternExpression _ subject _ _ -> expressionCalls subject + ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap expressionCalls [condition, whenTrue, whenFalse] + CoalesceExpression _ left fallback _ -> expressionCalls left ++ expressionCalls fallback CallableExpression _ _ _ _ body _ -> callableBodyCalls body where isCall CallExpression {} = True @@ -583,6 +587,7 @@ expressionTargets expression = case expression of CoreApply callee arguments _ -> calledTarget callee ++ expressionTargets callee ++ concatMap expressionTargets arguments CorePrimitive _ arguments _ -> concatMap expressionTargets arguments CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body + CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse] CoreClosure _ _ _ statements _ -> concatMap statementTargets statements where calledTarget (CoreVariable name _) = [name] diff --git a/Compiler/Haskell/Driver/test/TemplateDiscoveryTests.hs b/Compiler/Haskell/Driver/test/TemplateDiscoveryTests.hs index e9ef7b73..0cec2602 100644 --- a/Compiler/Haskell/Driver/test/TemplateDiscoveryTests.hs +++ b/Compiler/Haskell/Driver/test/TemplateDiscoveryTests.hs @@ -275,6 +275,9 @@ renameBoxApplication replacement (TypedAST tree) = TypedAST tree {syntaxDeclarat (rewriteBlock body) IncrementStatement spanValue name annotation isPrefix -> IncrementStatement spanValue name (rewriteType annotation) isPrefix + CompoundAssignmentStatement spanValue operator name annotation value -> + CompoundAssignmentStatement spanValue operator name (rewriteType annotation) (rewriteExpression value) + DiscardStatement spanValue value -> DiscardStatement spanValue (rewriteExpression value) BreakStatement spanValue value -> BreakStatement spanValue (rewriteExpression <$> value) ContinueStatement {} -> statement ExpressionStatement spanValue value terminated -> ExpressionStatement spanValue (rewriteExpression value) terminated @@ -295,6 +298,15 @@ renameBoxApplication replacement (TypedAST tree) = TypedAST tree {syntaxDeclarat (rewriteExpression value) (rewritePattern patternValue) (rewriteType annotation) + ConditionalExpression spanValue condition whenTrue whenFalse annotation -> + ConditionalExpression + spanValue + (rewriteExpression condition) + (rewriteExpression whenTrue) + (rewriteExpression whenFalse) + (rewriteType annotation) + CoalesceExpression spanValue left fallback annotation -> + CoalesceExpression spanValue (rewriteExpression left) (rewriteExpression fallback) (rewriteType annotation) CallableExpression spanValue isStatic captures parameters body annotation -> CallableExpression spanValue isStatic captures parameters body (rewriteType annotation) rewritePattern patternValue = case patternValue of diff --git a/Compiler/Haskell/Driver/test/TemplateMemberReachabilityTests.hs b/Compiler/Haskell/Driver/test/TemplateMemberReachabilityTests.hs index a46ebb51..a93bc2a6 100644 --- a/Compiler/Haskell/Driver/test/TemplateMemberReachabilityTests.hs +++ b/Compiler/Haskell/Driver/test/TemplateMemberReachabilityTests.hs @@ -614,6 +614,8 @@ statementTargets statement = case statement of ++ blockTargets body ForEachStatement _ _ _ _ _ source body -> expressionTargets source ++ blockTargets body IncrementStatement {} -> [] + CompoundAssignmentStatement _ _ _ _ value -> expressionTargets value + DiscardStatement _ value -> expressionTargets value BreakStatement _ value -> maybe [] expressionTargets value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionTargets value @@ -626,6 +628,8 @@ expressionTargets expression = case expression of CallExpression _ (NameExpression _ name _) arguments _ -> resolvedSymbol name : concatMap expressionTargets arguments CallExpression _ callee arguments _ -> expressionTargets callee ++ concatMap expressionTargets arguments UnaryExpression _ _ value _ -> expressionTargets value + ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse] + CoalesceExpression _ left fallback _ -> expressionTargets left ++ expressionTargets fallback BinaryExpression _ _ left right _ -> expressionTargets left ++ expressionTargets right CallableExpression _ _ captures _ body _ -> concatMap (maybe [] expressionTargets . captureInitializer) captures diff --git a/Compiler/Haskell/Driver/test/TemplateTests.hs b/Compiler/Haskell/Driver/test/TemplateTests.hs index ba2cf429..a8f230c9 100644 --- a/Compiler/Haskell/Driver/test/TemplateTests.hs +++ b/Compiler/Haskell/Driver/test/TemplateTests.hs @@ -339,12 +339,12 @@ identityTests = wireTests :: [(String, Bool)] wireTests = - [ ("Core v7 round-trips fixed array type", coreTypeRoundTrip (fixed intType 4096)) - , ("Core v7 round-trips Boolean template value", coreTypeRoundTrip (applied "Flag" [boolean True])) - , ("Core v7 round-trips character template value", coreTypeRoundTrip (applied "Code" [character 0x10ffff])) - , ("Core v7 round-trips template value parameter", coreTypeRoundTrip valueParameter) + [ ("Core v8 round-trips fixed array type", coreTypeRoundTrip (fixed intType 4096)) + , ("Core v8 round-trips Boolean template value", coreTypeRoundTrip (applied "Flag" [boolean True])) + , ("Core v8 round-trips character template value", coreTypeRoundTrip (applied "Code" [character 0x10ffff])) + , ("Core v8 round-trips template value parameter", coreTypeRoundTrip valueParameter) , - ( "Core v7 round-trips mixed arguments" + ( "Core v8 round-trips mixed arguments" , coreTypeRoundTrip (applied "Mix" [value (-3), typeArg stringType, boolean False]) ) , ("CorePrep v6 round-trips fixed array type", corePrepTypeRoundTrip (fixed intType 4096)) diff --git a/Compiler/Haskell/Driver/visual-xsharp-compiler.cabal b/Compiler/Haskell/Driver/visual-xsharp-compiler.cabal index 47622271..fa590dac 100644 --- a/Compiler/Haskell/Driver/visual-xsharp-compiler.cabal +++ b/Compiler/Haskell/Driver/visual-xsharp-compiler.cabal @@ -135,6 +135,7 @@ test-suite visual-xsharp-compiler-tests FloatingOptimizerTests IntegerEvaluationTests IntegerFlowTests + ConditionalExpressionTests IterationTests LoopFlowTests LoopFlowOracleTests diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Closure/Analysis.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Closure/Analysis.hs index 422650d1..5509bbb2 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Closure/Analysis.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Closure/Analysis.hs @@ -161,6 +161,8 @@ walkStatement parent state statement = case statement of ForEachStatement _ _ _ _ _ source body -> walkBlock parent (walkExpression parent state source) body IncrementStatement {} -> state + CompoundAssignmentStatement _ _ _ _ value -> walkExpression parent state value + DiscardStatement _ value -> walkExpression parent state value BreakStatement _ value -> maybe state (walkExpression parent state) value ContinueStatement {} -> state ExpressionStatement _ value _ -> walkExpression parent state value @@ -176,6 +178,10 @@ walkExpression parent state expression = case expression of BinaryExpression _ _ left right _ -> walkExpression parent (walkExpression parent state left) right IsPatternExpression _ subject _ _ -> walkExpression parent state subject + ConditionalExpression _ condition first second _ -> + foldl (walkExpression parent) state [condition, first, second] + CoalesceExpression _ left fallback _ -> + walkExpression parent (walkExpression parent state left) fallback callable@CallableExpression {} -> walkCallable parent state callable walkCallable :: Maybe ClosureId -> WalkState -> Expression ResolvedName Type -> WalkState @@ -271,6 +277,11 @@ statementFacts statement = case statement of let nested = expressionFacts source `appendFacts` blockFacts body in nested {factLocals = name : factLocals nested, factWrites = name : factWrites nested} IncrementStatement _ name annotation _ -> emptyFacts {factReads = [(name, annotation)], factWrites = [name]} + -- A compound assignment reads its target before storing the result. + CompoundAssignmentStatement _ _ name annotation value -> + let nested = expressionFacts value + in nested {factReads = (name, annotation) : factReads nested, factWrites = name : factWrites nested} + DiscardStatement _ value -> expressionFacts value BreakStatement _ value -> maybe emptyFacts expressionFacts value ContinueStatement {} -> emptyFacts ExpressionStatement _ value _ -> expressionFacts value @@ -285,6 +296,9 @@ expressionFacts expression = case expression of UnaryExpression _ _ value _ -> expressionFacts value BinaryExpression _ _ left right _ -> expressionFacts left `appendFacts` expressionFacts right IsPatternExpression _ subject _ _ -> expressionFacts subject + ConditionalExpression _ condition first second _ -> + foldl appendFacts (expressionFacts condition) (map expressionFacts [first, second]) + CoalesceExpression _ left fallback _ -> expressionFacts left `appendFacts` expressionFacts fallback CallableExpression {} -> emptyFacts captureUse :: BodyFacts -> Bool -> Int -> Capture ResolvedName Type -> CaptureUse @@ -374,6 +388,8 @@ statementContainsCall statement = case statement of ForEachStatement _ _ _ _ _ source body -> expressionContainsCall source || any statementContainsCall (blockStatements body) IncrementStatement {} -> False + CompoundAssignmentStatement _ _ _ _ value -> expressionContainsCall value + DiscardStatement _ value -> expressionContainsCall value BreakStatement _ value -> maybe False expressionContainsCall value ContinueStatement {} -> False ExpressionStatement _ value _ -> expressionContainsCall value @@ -385,8 +401,11 @@ expressionContainsCall expression = case expression of UnaryExpression _ _ value _ -> expressionContainsCall value BinaryExpression _ _ left right _ -> expressionContainsCall left || expressionContainsCall right IsPatternExpression _ subject _ _ -> expressionContainsCall subject + ConditionalExpression _ condition first second _ -> any expressionContainsCall [condition, first, second] + CoalesceExpression _ left fallback _ -> expressionContainsCall left || expressionContainsCall fallback CallableExpression {} -> False - _ -> False + NameExpression {} -> False + LiteralExpression {} -> False descendantsOf :: [ClosureId] -> ClosureCatalog -> [ClosureId] descendantsOf roots catalog = roots ++ concatMap children roots diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/ConstantEvaluation.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/ConstantEvaluation.hs index c48c8677..6e754a31 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/ConstantEvaluation.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/ConstantEvaluation.hs @@ -68,6 +68,18 @@ evaluateConstantInteger expression = case expression of leftValue <- evaluateConstantInteger left rightValue <- evaluateConstantInteger right evaluateBinary operator leftValue rightValue + -- Only the selected operand is evaluated, so a definite arithmetic error + -- in the other one is not a property of this expression. + ConditionalExpression _ condition first second _ -> do + test <- evaluateConstantInteger condition + case test of + Just number -> evaluateConstantInteger (if number /= 0 then first else second) + Nothing -> pure Nothing + CoalesceExpression _ left fallback _ -> do + leftValue <- evaluateConstantInteger left + case leftValue of + Just 0 -> evaluateConstantInteger fallback + other -> pure other _ -> pure Nothing typedBitwiseComplement :: Type -> Integer -> Integer diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/NameResolution.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/NameResolution.hs index 5e199f79..5a28ae03 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/NameResolution.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/NameResolution.hs @@ -122,6 +122,12 @@ resolveStatement statement = case statement of IncrementStatement spanValue name annotation direction -> let (resolvedName, problems) = resolveName spanValue name in (IncrementStatement spanValue resolvedName annotation direction, problems) + CompoundAssignmentStatement spanValue operator name _ value -> + let (resolvedName, nameProblems) = resolveName spanValue name + (resolvedValue, valueProblems) = resolveExpression value + in (CompoundAssignmentStatement spanValue operator resolvedName () resolvedValue, nameProblems ++ valueProblems) + DiscardStatement spanValue value -> + let (resolved, problems) = resolveExpression value in (DiscardStatement spanValue resolved, problems) BreakStatement spanValue value -> let (resolvedValue, problems) = resolveOptional value in (BreakStatement spanValue resolvedValue, problems) @@ -150,6 +156,17 @@ resolveExpression expression = case expression of let (resolvedSubject, subjectProblems) = resolveExpression subject (resolvedPattern, patternProblems) = resolvePattern patternValue in (IsPatternExpression spanValue resolvedSubject resolvedPattern (), subjectProblems ++ patternProblems) + ConditionalExpression spanValue condition first second _ -> + let (resolvedCondition, conditionProblems) = resolveExpression condition + (resolvedFirst, firstProblems) = resolveExpression first + (resolvedSecond, secondProblems) = resolveExpression second + in ( ConditionalExpression spanValue resolvedCondition resolvedFirst resolvedSecond () + , conditionProblems ++ firstProblems ++ secondProblems + ) + CoalesceExpression spanValue left fallback _ -> + let (resolvedLeft, leftProblems) = resolveExpression left + (resolvedFallback, fallbackProblems) = resolveExpression fallback + in (CoalesceExpression spanValue resolvedLeft resolvedFallback (), leftProblems ++ fallbackProblems) CallableExpression spanValue explicit captures parameters body _ -> let resolvedCaptures = map resolveCapture captures resolvedParameters = map resolveParameter parameters diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/Renamer.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/Renamer.hs index b09d62f3..d9ffa272 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/Renamer.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Resolver/Renamer.hs @@ -266,6 +266,16 @@ renameStatement environment next statement = case statement of ) IncrementStatement spanValue name _ direction -> (IncrementStatement spanValue (valueOrMissing name environment) () direction, environment, next, []) + CompoundAssignmentStatement spanValue operator name _ value -> + let (renamedValue, after, problems) = renameExpression environment next value + in ( CompoundAssignmentStatement spanValue operator (valueOrMissing name environment) () renamedValue + , environment + , after + , problems + ) + DiscardStatement spanValue value -> + let (renamedValue, after, problems) = renameExpression environment next value + in (DiscardStatement spanValue renamedValue, environment, after, problems) BreakStatement spanValue value -> let (renamed, after, problems) = renameOptional environment next value in (BreakStatement spanValue renamed, environment, after, problems) @@ -312,6 +322,18 @@ renameExpression environment next expression = case expression of , afterPattern , subjectProblems ++ patternProblems ) + ConditionalExpression spanValue condition first second _ -> + let (renamedCondition, afterCondition, conditionProblems) = renameExpression environment next condition + (renamedFirst, afterFirst, firstProblems) = renameExpression environment afterCondition first + (renamedSecond, afterSecond, secondProblems) = renameExpression environment afterFirst second + in ( ConditionalExpression spanValue renamedCondition renamedFirst renamedSecond () + , afterSecond + , conditionProblems ++ firstProblems ++ secondProblems + ) + CoalesceExpression spanValue left fallback _ -> + let (renamedLeft, afterLeft, leftProblems) = renameExpression environment next left + (renamedFallback, afterFallback, fallbackProblems) = renameExpression environment afterLeft fallback + in (CoalesceExpression spanValue renamedLeft renamedFallback (), afterFallback, leftProblems ++ fallbackProblems) CallableExpression spanValue explicit sourceCaptures sourceParameters sourceBody _ -> let (captures, captureEnvironment, afterCaptures, captureProblems) = renameCaptures environment next sourceCaptures diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Discovery.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Discovery.hs index da0fd291..bd31e9d9 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Discovery.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Discovery.hs @@ -240,6 +240,16 @@ discoverStatement catalog namespace root index statement state = case statement afterSource = discoverExpression catalog namespace origin source afterAnnotation in discoverBlock catalog namespace origin body afterSource IncrementStatement _ _ annotation _ -> discoverType catalog namespace root annotation state + CompoundAssignmentStatement spanValue _ _ annotation value -> + let origin = root {discoverySites = [AssignmentTypeSite index], discoverySpan = spanValue} + in discoverExpression catalog namespace origin value (discoverType catalog namespace origin annotation state) + DiscardStatement spanValue expression -> + discoverExpression + catalog + namespace + (root {discoverySites = [ExpressionTypeSite index], discoverySpan = spanValue}) + expression + state BreakStatement _ value -> maybe state (\expression -> discoverExpression catalog namespace root expression state) value ContinueStatement {} -> state ExpressionStatement spanValue expression _ -> @@ -276,6 +286,16 @@ discoverExpression catalog namespace origin expression state = case expression o let afterType = discoverType catalog namespace origin annotation state afterSubject = discoverExpression catalog namespace origin subject afterType in discoverPattern catalog namespace origin patternValue afterSubject + ConditionalExpression _ condition first second annotation -> + let afterType = discoverType catalog namespace origin annotation state + in foldl' + (\current value -> discoverExpression catalog namespace origin value current) + afterType + [condition, first, second] + CoalesceExpression _ left fallback annotation -> + let afterType = discoverType catalog namespace origin annotation state + afterLeft = discoverExpression catalog namespace origin left afterType + in discoverExpression catalog namespace origin fallback afterLeft CallableExpression spanValue _ captures parameters body annotation -> let callableOrigin = origin diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Freshen.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Freshen.hs index 286ebe84..4678b33d 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Freshen.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Freshen.hs @@ -184,6 +184,12 @@ freshStatement statement = case statement of pure (ForEachStatement spanValue kind syntax closedName closedAnnotation closedSource closedBody) IncrementStatement spanValue name annotation direction -> IncrementStatement spanValue <$> freshReference name <*> freshType annotation <*> pure direction + CompoundAssignmentStatement spanValue operator name annotation value -> + CompoundAssignmentStatement spanValue operator + <$> freshReference name + <*> freshType annotation + <*> freshExpression value + DiscardStatement spanValue value -> DiscardStatement spanValue <$> freshExpression value BreakStatement spanValue value -> BreakStatement spanValue <$> traverse freshExpression value ContinueStatement spanValue -> pure (ContinueStatement spanValue) ExpressionStatement spanValue value terminated -> @@ -214,6 +220,17 @@ freshExpression expression = case expression of <$> freshExpression subject <*> freshPattern patternValue <*> freshType annotation + ConditionalExpression spanValue condition first second annotation -> + ConditionalExpression spanValue + <$> freshExpression condition + <*> freshExpression first + <*> freshExpression second + <*> freshType annotation + CoalesceExpression spanValue left fallback annotation -> + CoalesceExpression spanValue + <$> freshExpression left + <*> freshExpression fallback + <*> freshType annotation CallableExpression spanValue explicit captures parameters body annotation -> do closedCaptures <- traverse freshCaptureDefinition captures closedParameters <- traverse freshParameterDefinition parameters @@ -310,6 +327,9 @@ statementSymbols statement = case statement of ForEachStatement _ _ _ name annotation source body -> nameSymbol name : typeSymbols annotation ++ expressionSymbols source ++ blockSymbols body IncrementStatement _ name annotation _ -> nameSymbol name : typeSymbols annotation + CompoundAssignmentStatement _ _ name annotation value -> + nameSymbol name : typeSymbols annotation ++ expressionSymbols value + DiscardStatement _ value -> expressionSymbols value BreakStatement _ value -> maybe [] expressionSymbols value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionSymbols value @@ -326,6 +346,10 @@ expressionSymbols expression = case expression of expressionSymbols left ++ expressionSymbols right ++ typeSymbols annotation IsPatternExpression _ subject patternValue annotation -> expressionSymbols subject ++ patternSymbols patternValue ++ typeSymbols annotation + ConditionalExpression _ condition first second annotation -> + concatMap expressionSymbols [condition, first, second] ++ typeSymbols annotation + CoalesceExpression _ left fallback annotation -> + expressionSymbols left ++ expressionSymbols fallback ++ typeSymbols annotation CallableExpression _ _ captures parameters body annotation -> concatMap captureSymbols captures ++ concatMap parameterSymbols parameters diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Instantiation.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Instantiation.hs index 3d74db52..c6a8b477 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Instantiation.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Instantiation.hs @@ -150,6 +150,12 @@ instantiateStatement binding statement = case statement of pure (ForEachStatement spanValue kind syntax name closedAnnotation closedSource closedBody) IncrementStatement spanValue name annotation direction -> IncrementStatement spanValue name <$> instantiateType binding annotation <*> pure direction + CompoundAssignmentStatement spanValue operator name annotation value -> do + closedAnnotation <- instantiateType binding annotation + closedValue <- instantiateExpression binding value + pure (CompoundAssignmentStatement spanValue operator name closedAnnotation closedValue) + DiscardStatement spanValue value -> + DiscardStatement spanValue <$> instantiateExpression binding value BreakStatement spanValue value -> BreakStatement spanValue <$> traverse (instantiateExpression binding) value ContinueStatement spanValue -> pure (ContinueStatement spanValue) @@ -189,6 +195,17 @@ instantiateExpression binding expression = case expression of closedPattern <- instantiatePattern binding patternValue closedAnnotation <- instantiateType binding annotation pure (IsPatternExpression spanValue closedSubject closedPattern closedAnnotation) + ConditionalExpression spanValue condition first second annotation -> do + closedCondition <- instantiateExpression binding condition + closedFirst <- instantiateExpression binding first + closedSecond <- instantiateExpression binding second + closedAnnotation <- instantiateType binding annotation + pure (ConditionalExpression spanValue closedCondition closedFirst closedSecond closedAnnotation) + CoalesceExpression spanValue left fallback annotation -> do + closedLeft <- instantiateExpression binding left + closedFallback <- instantiateExpression binding fallback + closedAnnotation <- instantiateType binding annotation + pure (CoalesceExpression spanValue closedLeft closedFallback closedAnnotation) CallableExpression spanValue explicit captures parameters body annotation -> do closedCaptures <- traverse (instantiateCapture binding) captures closedParameters <- traverse (instantiateParameter binding) parameters diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/MemberReachability.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/MemberReachability.hs index 6c8c6acd..ed615523 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/MemberReachability.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/MemberReachability.hs @@ -357,6 +357,8 @@ statementCalls owner statement = case statement of ++ blockCalls owner body ForEachStatement _ _ _ _ _ source body -> expressionCalls owner source ++ blockCalls owner body IncrementStatement {} -> [] + CompoundAssignmentStatement _ _ _ _ value -> expressionCalls owner value + DiscardStatement _ value -> expressionCalls owner value BreakStatement _ value -> maybe [] (expressionCalls owner) value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionCalls owner value @@ -373,6 +375,10 @@ expressionCalls owner expression = case expression of UnaryExpression _ _ value _ -> expressionCalls owner value BinaryExpression _ _ left right _ -> expressionCalls owner left ++ expressionCalls owner right IsPatternExpression _ subject _ _ -> expressionCalls owner subject + -- Reachability is path-insensitive: a call in either arm may execute. + ConditionalExpression _ condition first second _ -> + concatMap (expressionCalls owner) [condition, first, second] + CoalesceExpression _ left fallback _ -> expressionCalls owner left ++ expressionCalls owner fallback CallableExpression _ _ captures _ body _ -> concatMap (maybe [] (expressionCalls owner) . captureInitializer) captures ++ callableBodyCalls owner body diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization.hs index 2d1dd656..e3b61b9a 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization.hs @@ -538,6 +538,8 @@ statementTypes statement = case statement of ++ blockTypes body ForEachStatement _ _ _ _ annotation source body -> annotation : expressionTypes source ++ blockTypes body IncrementStatement _ _ annotation _ -> [annotation] + CompoundAssignmentStatement _ _ _ annotation value -> annotation : expressionTypes value + DiscardStatement _ value -> expressionTypes value BreakStatement _ value -> maybe [] expressionTypes value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionTypes value @@ -553,6 +555,10 @@ expressionTypes expression = case expression of BinaryExpression _ _ left right annotation -> annotation : expressionTypes left ++ expressionTypes right IsPatternExpression _ subject patternValue annotation -> annotation : expressionTypes subject ++ patternTypes patternValue + ConditionalExpression _ condition whenTrue whenFalse annotation -> + annotation : concatMap expressionTypes [condition, whenTrue, whenFalse] + CoalesceExpression _ left fallback annotation -> + annotation : expressionTypes left ++ expressionTypes fallback CallableExpression _ _ captures parameters body annotation -> annotation : concatMap captureTypes captures diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization/Verifier.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization/Verifier.hs index 3890f6ca..2d82ec5a 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization/Verifier.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/Template/Specialization/Verifier.hs @@ -297,6 +297,8 @@ statementDefinitionSymbols statement = case statement of ForEachStatement _ _ _ name _ source body -> resolvedSymbol name : expressionDefinitionSymbols source ++ blockDefinitionSymbols body IncrementStatement _ name _ _ -> [resolvedSymbol name] + CompoundAssignmentStatement _ _ _ _ value -> expressionDefinitionSymbols value + DiscardStatement _ value -> expressionDefinitionSymbols value BreakStatement _ value -> maybe [] expressionDefinitionSymbols value ContinueStatement {} -> [] ExpressionStatement _ expression _ -> expressionDefinitionSymbols expression @@ -309,11 +311,16 @@ expressionDefinitionSymbols expression = case expression of UnaryExpression _ _ value _ -> expressionDefinitionSymbols value BinaryExpression _ _ left right _ -> expressionDefinitionSymbols left ++ expressionDefinitionSymbols right IsPatternExpression _ subject _ _ -> expressionDefinitionSymbols subject + ConditionalExpression _ condition first second _ -> + concatMap expressionDefinitionSymbols [condition, first, second] + CoalesceExpression _ left fallback _ -> + expressionDefinitionSymbols left ++ expressionDefinitionSymbols fallback CallableExpression _ _ captures parameters body _ -> map (resolvedSymbol . captureName) captures ++ map (resolvedSymbol . parameterName) parameters ++ callableBodyDefinitionSymbols body - _ -> [] + NameExpression {} -> [] + LiteralExpression {} -> [] callableBodyDefinitionSymbols :: CallableBody ResolvedName Type -> [SymbolId] callableBodyDefinitionSymbols body = case body of @@ -353,6 +360,8 @@ statementTypes statement = case statement of ++ blockTypes body ForEachStatement _ _ _ _ annotation source body -> annotation : expressionTypes source ++ blockTypes body IncrementStatement _ _ annotation _ -> [annotation] + CompoundAssignmentStatement _ _ _ annotation value -> annotation : expressionTypes value + DiscardStatement _ value -> expressionTypes value BreakStatement _ value -> maybe [] expressionTypes value ContinueStatement {} -> [] ExpressionStatement _ value _ -> expressionTypes value @@ -368,6 +377,10 @@ expressionTypes expression = case expression of BinaryExpression _ _ left right annotation -> annotation : expressionTypes left ++ expressionTypes right IsPatternExpression _ subject patternValue annotation -> annotation : expressionTypes subject ++ patternTypes patternValue + ConditionalExpression _ condition first second annotation -> + annotation : concatMap expressionTypes [condition, first, second] + CoalesceExpression _ left fallback annotation -> + annotation : expressionTypes left ++ expressionTypes fallback CallableExpression _ _ captures parameters body annotation -> annotation : map captureAnnotation captures diff --git a/Compiler/Haskell/Frontend/src/Visual/XSharp/TypeChecker.hs b/Compiler/Haskell/Frontend/src/Visual/XSharp/TypeChecker.hs index c31db7b8..0a8bb534 100644 --- a/Compiler/Haskell/Frontend/src/Visual/XSharp/TypeChecker.hs +++ b/Compiler/Haskell/Frontend/src/Visual/XSharp/TypeChecker.hs @@ -511,6 +511,32 @@ checkStatementWith context environment expected loopDepth statement = case state then [] else [problem spanValue "VXT0024" "increment or decrement target must have a numeric type"] in (IncrementStatement spanValue name targetType direction, environment, [], writableProblems ++ numericProblems) + CompoundAssignmentStatement spanValue operator name _ value -> + -- `target op= value` has the typing of `target = target op value`: + -- the operator rule is applied to the target type and the result + -- must be storable without a conversion. + let target = lookup (resolvedSymbol name) environment + targetType = maybe ErrorType fst target + operandExpected = if targetType == ErrorType then Nothing else Just targetType + (typedValue, valueType, problems) = checkExpressionExpectedWith context environment operandExpected value + immutable = case target of + Just (_, False) -> [problem spanValue "VXT0003" "cannot assign to an immutable binding"] + _ -> [] + rule = binaryNumericRule operator targetType valueType + known = targetType /= ErrorType && valueType /= ErrorType + operatorProblems = if known then ruleProblems spanValue "VXT0012" rule else [] + resultProblems = + if known && null operatorProblems && numericRuleType rule /= targetType + then [problem spanValue "VXT0035" "compound assignment result does not have the target type"] + else [] + in ( CompoundAssignmentStatement spanValue operator name targetType typedValue + , environment + , [] + , problems ++ immutable ++ operatorProblems ++ resultProblems + ) + DiscardStatement spanValue value -> + let (typedValue, _, problems) = checkExpressionWith context environment value + in (DiscardStatement spanValue typedValue, environment, [], problems) BreakStatement spanValue value -> let (typedValue, _, valueProblems) = checkOptionalWith context environment value outsideProblems = @@ -560,11 +586,15 @@ typedExpressionType expression = case expression of UnaryExpression _ _ _ valueType -> valueType BinaryExpression _ _ _ _ valueType -> valueType IsPatternExpression _ _ _ valueType -> valueType + ConditionalExpression _ _ _ _ valueType -> valueType + CoalesceExpression _ _ _ valueType -> valueType CallableExpression _ _ _ _ _ valueType -> valueType effectCapable :: Expression name annotation -> Bool effectCapable CallExpression {} = True effectCapable (IsPatternExpression _ subject _ _) = effectCapable subject +effectCapable (ConditionalExpression _ condition first second _) = any effectCapable [condition, first, second] +effectCapable (CoalesceExpression _ left fallback _) = effectCapable left || effectCapable fallback effectCapable CallableExpression {} = False effectCapable _ = False @@ -671,6 +701,35 @@ checkExpressionExpectedWith context environment expected expression = case expre , boolType , subjectProblems ++ patternProblems ) + ConditionalExpression spanValue condition first second _ -> + let (typedCondition, conditionType, conditionProblems) = checkExpressionWith context environment condition + conditionMismatch = + [ problem (sourceSpanOf condition) "VXT0036" "conditional test must be bool or numeric" + | conditionType /= ErrorType + , not (booleanContextType conditionType) + ] + ((typedFirst, firstType, firstProblems), (typedSecond, secondType, secondProblems)) = + checkOperandPair context environment expected first second + (resultType, resultProblems) = + selectedValueType spanValue "VXT0037" "conditional results must have the same type" firstType secondType + in ( ConditionalExpression spanValue typedCondition typedFirst typedSecond resultType + , resultType + , conditionProblems ++ conditionMismatch ++ firstProblems ++ secondProblems ++ resultProblems + ) + CoalesceExpression spanValue left fallback _ -> + let ((typedLeft, leftType, leftProblems), (typedFallback, fallbackType, fallbackProblems)) = + checkOperandPair context environment expected left fallback + (resultType, resultProblems) = + selectedValueType + spanValue + "VXT0038" + "truthy coalescing operands must have the same type" + leftType + fallbackType + in ( CoalesceExpression spanValue typedLeft typedFallback resultType + , resultType + , leftProblems ++ fallbackProblems ++ resultProblems + ) CallableExpression spanValue explicit captures parameters body _ -> let checkedCaptures = checkCapturesWith context environment captures captureEnvironment = @@ -698,6 +757,61 @@ checkExpressionExpectedWith context environment expected expression = case expre , captureProblems ++ parameterProblems ++ bodyProblems ) +{- | Check the two value operands of a conditional form exactly once each. + +An operand made only of untyped numeric literals takes its type from the +other operand, in either direction, so @flag ? 1 : wide@ selects the type of +@wide@ just as @flag ? wide : 1@ does. The order is chosen from syntax +before either operand is checked; checking an operand twice would make the +cost exponential in the nesting depth of chained conditionals. +-} +checkOperandPair :: + TemplateContext -> + TypeEnvironment -> + Maybe Type -> + Expression ResolvedName () -> + Expression ResolvedName () -> + ( (Expression ResolvedName Type, Type, [Diagnostic]) + , (Expression ResolvedName Type, Type, [Diagnostic]) + ) +checkOperandPair context environment expected first second + | expected == Nothing && takesContextualType first && not (takesContextualType second) = + let secondResult@(_, secondType, _) = checkExpressionWith context environment second + in (checkExpressionExpectedWith context environment (contextFrom secondType) first, secondResult) + | otherwise = + let firstResult@(_, firstType, _) = checkExpressionExpectedWith context environment expected first + secondExpected = maybe (contextFrom firstType) Just expected + in (firstResult, checkExpressionExpectedWith context environment secondExpected second) + where + contextFrom valueType = if valueType == ErrorType then Nothing else Just valueType + +-- | Whether an expression consists only of numeric literals and arithmetic. +takesContextualType :: Expression name annotation -> Bool +takesContextualType expression = case expression of + LiteralExpression _ (IntegerLiteral _) _ -> True + LiteralExpression _ (FloatingLiteral _) _ -> True + UnaryExpression _ operator value _ -> operator /= LogicalNot && takesContextualType value + BinaryExpression _ operator left right _ -> + not (booleanResult operator) && takesContextualType left && takesContextualType right + _ -> False + +{- | Result type shared by the two value operands of a conditional form. + +The result is materialized in one storage slot, so both operands must have +the same type. Only bool and numeric results are lowered today; owned values +need move and release rules for the slot that the backend does not have yet. +-} +selectedValueType :: SourceSpan -> String -> String -> Type -> Type -> (Type, [Diagnostic]) +selectedValueType spanValue mismatchCode mismatchMessage firstType secondType + | firstType == ErrorType = (secondType, []) + | secondType == ErrorType = (firstType, []) + | firstType /= secondType = (firstType, [problem spanValue mismatchCode mismatchMessage]) + | not (booleanContextType firstType) = + ( firstType + , [problem spanValue "VXT0039" "conditional expressions currently support only bool and numeric results"] + ) + | otherwise = (firstType, []) + checkOrdinaryCall :: TemplateContext -> TypeEnvironment -> @@ -798,6 +912,8 @@ sourceSpanOf expression = case expression of UnaryExpression spanValue _ _ _ -> spanValue BinaryExpression spanValue _ _ _ _ -> spanValue IsPatternExpression spanValue _ _ _ -> spanValue + ConditionalExpression spanValue _ _ _ _ -> spanValue + CoalesceExpression spanValue _ _ _ -> spanValue CallableExpression spanValue _ _ _ _ _ -> spanValue checkMemberOverloadCall :: diff --git a/Compiler/Haskell/Syntax/src/Visual/XSharp/AST.hs b/Compiler/Haskell/Syntax/src/Visual/XSharp/AST.hs index bbfaac1d..ec1dddd9 100644 --- a/Compiler/Haskell/Syntax/src/Visual/XSharp/AST.hs +++ b/Compiler/Haskell/Syntax/src/Visual/XSharp/AST.hs @@ -255,6 +255,13 @@ data Statement name annotation (Expression name annotation) (Block name annotation) | IncrementStatement SourceSpan name annotation Bool + | -- @target op= value@ reads and writes one named storage location. The + -- annotation is the target type; the operator is the binary operation + -- whose result is stored back. + CompoundAssignmentStatement SourceSpan BinaryOperator name annotation (Expression name annotation) + | -- @_ = value;@ evaluates its operand and drops the result. It is a + -- statement of its own, not an assignment to a binding named @_@. + DiscardStatement SourceSpan (Expression name annotation) | BreakStatement SourceSpan (Maybe (Expression name annotation)) | ContinueStatement SourceSpan | ExpressionStatement SourceSpan (Expression name annotation) Bool @@ -272,6 +279,17 @@ data Expression name annotation | UnaryExpression SourceSpan UnaryOperator (Expression name annotation) annotation | BinaryExpression SourceSpan BinaryOperator (Expression name annotation) (Expression name annotation) annotation | IsPatternExpression SourceSpan (Expression name annotation) (Pattern name annotation) annotation + | -- @condition ? first : second@ evaluates exactly one arm. + ConditionalExpression + SourceSpan + (Expression name annotation) + (Expression name annotation) + (Expression name annotation) + annotation + | -- @left ?: fallback@ yields the left value when it is true in Boolean + -- context and evaluates the fallback only otherwise. The left operand + -- is evaluated once. + CoalesceExpression SourceSpan (Expression name annotation) (Expression name annotation) annotation | CallableExpression SourceSpan Bool diff --git a/Compiler/Haskell/Syntax/src/Visual/XSharp/Lexer.hs b/Compiler/Haskell/Syntax/src/Visual/XSharp/Lexer.hs index 8fd96beb..8fc4e863 100644 --- a/Compiler/Haskell/Syntax/src/Visual/XSharp/Lexer.hs +++ b/Compiler/Haskell/Syntax/src/Visual/XSharp/Lexer.hs @@ -294,6 +294,21 @@ longestSymbol :: String -> Maybe String longestSymbol source = firstMatch [ "..." + , "<<=" + , ">>=" + , "**=" + , "//=" + , "??=" + , "+=" + , "-=" + , "*=" + , "/=" + , "%=" + , "&=" + , "|=" + , "^=" + , "??" + , "?:" , "==" , "\\=" , "<=" @@ -325,6 +340,7 @@ longestSymbol source = , "<" , ">" , "!" + , "?" , "[" , "]" , "\\" diff --git a/Compiler/Haskell/Syntax/src/Visual/XSharp/Parser.hs b/Compiler/Haskell/Syntax/src/Visual/XSharp/Parser.hs index a52dd612..fda899c9 100644 --- a/Compiler/Haskell/Syntax/src/Visual/XSharp/Parser.hs +++ b/Compiler/Haskell/Syntax/src/Visual/XSharp/Parser.hs @@ -388,6 +388,8 @@ requireTemplateValue expression = case expression of TemplateBinarySyntax spanValue operator <$> requireTemplateValue left <*> requireTemplateValue right CallExpression spanValue _ _ _ -> unsupported spanValue IsPatternExpression spanValue _ _ _ -> unsupported spanValue + ConditionalExpression spanValue _ _ _ _ -> unsupported spanValue + CoalesceExpression spanValue _ _ _ -> unsupported spanValue CallableExpression spanValue _ _ _ _ _ -> unsupported spanValue where unsupported spanValue = @@ -421,6 +423,8 @@ statementSpan statement = case statement of ForStatement value _ _ _ _ -> value ForEachStatement value _ _ _ _ _ _ -> value IncrementStatement value _ _ _ -> value + CompoundAssignmentStatement value _ _ _ _ -> value + DiscardStatement value _ -> value BreakStatement value _ -> value ContinueStatement value -> value ExpressionStatement value _ _ -> value @@ -441,6 +445,8 @@ parseStatement allowFinalExpression = first : _ | tokenKind first == KeywordToken && tokenText first == "break" -> parseBreak first : _ | tokenKind first == KeywordToken && tokenText first == "continue" -> parseContinue _ | startsIncrement tokens -> parseIncrementStatement + _ | startsCompoundAssignment tokens -> parseCompoundAssignmentStatement + _ | startsDiscard tokens -> parseDiscard first : _ | tokenKind first == KeywordToken && tokenText first == "final" -> parseBinding -- `unit` is forbidden specifically in type position. Commit here -- so declaration lookahead cannot hide VXP0013 behind an unrelated @@ -606,6 +612,7 @@ parseForAction = do first : second : _ | tokenKind first == IdentifierToken && tokenText second `elem` ["++", "--"] -> parseIncrement (tokenText second == "++") False + _ | startsCompoundAssignment tokens -> parseCompoundAssignment _ -> do value <- parseExpression assignment <- optionalSymbol "=" @@ -649,6 +656,76 @@ parseIncrement isIncrement prefix = do where operator = if isIncrement then "++" else "--" +-- Every compound operator stores the result of its binary operator back into +-- the target. Comparison and logical operators have no compound spelling. +compoundAssignmentOperators :: [(String, BinaryOperator)] +compoundAssignmentOperators = + [ ("+=", Add) + , ("-=", Subtract) + , ("*=", Multiply) + , ("/=", Divide) + , ("//=", FloorDivide) + , ("%=", Remainder) + , ("**=", Power) + , ("<<=", ShiftLeft) + , (">>=", ShiftRight) + , ("&=", BitwiseAnd) + , ("^=", BitwiseXor) + , ("|=", BitwiseOr) + ] + +compoundAssignmentOperator :: Token -> Maybe BinaryOperator +compoundAssignmentOperator token + | tokenKind token == SymbolToken = lookup (tokenText token) compoundAssignmentOperators + | otherwise = Nothing + +startsCompoundAssignment :: [Token] -> Bool +startsCompoundAssignment tokens = case tokens of + first : second : _ -> + tokenKind first == IdentifierToken && compoundAssignmentOperator second /= Nothing + _ -> False + +-- The header form has no terminator; a `for` update list separates its +-- actions with commas and closes them with the parenthesis. +parseCompoundAssignment :: P (Statement Identifier ()) +parseCompoundAssignment = do + (name, nameSpan) <- identifier + operatorTokenValue <- takeToken + case compoundAssignmentOperator operatorTokenValue of + Nothing -> + failAt (tokenSpan operatorTokenValue) "VXP0006" "expected a compound assignment operator" + Just operator -> do + value <- parseExpression + pure (CompoundAssignmentStatement (mergeSpan nameSpan (expressionSpan value)) operator name () value) + +parseCompoundAssignmentStatement :: P (Statement Identifier ()) +parseCompoundAssignmentStatement = do + assignment <- parseCompoundAssignment + end <- symbol ";" + pure $ case assignment of + CompoundAssignmentStatement spanValue operator name annotation value -> + CompoundAssignmentStatement (mergeSpan spanValue (tokenSpan end)) operator name annotation value + _ -> assignment + +-- `_` never names a binding, so `_ =` can be recognized from two tokens and +-- cannot be confused with an assignment to a declared name. +startsDiscard :: [Token] -> Bool +startsDiscard tokens = case tokens of + first : second : _ -> + tokenKind first == IdentifierToken + && tokenText first == "_" + && tokenKind second == SymbolToken + && tokenText second == "=" + _ -> False + +parseDiscard :: P (Statement Identifier ()) +parseDiscard = do + start <- takeToken + _ <- symbol "=" + value <- parseExpression + end <- symbol ";" + pure (DiscardStatement (mergeSpan (tokenSpan start) (tokenSpan end)) value) + parseBreak :: P (Statement Identifier ()) parseBreak = do start <- keyword "break" @@ -688,6 +765,19 @@ parseAssignmentOrExpression allowFinalExpression = do pure (AssignmentStatement (mergeSpan start (tokenSpan end)) name () value) _ -> failAt (expressionSpan expression) "VXP0003" "assignment target must be a name" else do + -- A named target was dispatched before this parser ran, so a + -- compound operator here follows a non-storage expression. + next <- peekToken + case next of + Just token + | compoundAssignmentOperator token /= Nothing -> + failAt (expressionSpan expression) "VXP0003" "assignment target must be a name" + | tokenKind token == SymbolToken && tokenText token == "??=" -> + failAt + (tokenSpan token) + "VXP0031" + "null-coalescing assignment requires nullable types, which are not implemented" + _ -> pure () terminated <- peekText ";" if terminated then do @@ -702,7 +792,53 @@ parseAssignmentOrExpression allowFinalExpression = do pure (ExpressionStatement (expressionSpan expression) expression True) parseExpression :: P (Expression Identifier ()) -parseExpression = parseLogicalOr +parseExpression = parseConditional + +-- The conditional forms bind more weakly than every binary operator and are +-- right-associative: `a ? b : c ? d : e` groups as `a ? b : (c ? d : e)` and +-- `a ?: b ?: c` as `a ?: (b ?: c)`. +parseConditional :: P (Expression Identifier ()) +parseConditional = do + condition <- parseLogicalOr + next <- peekToken + case next of + Just token | tokenKind token == SymbolToken && tokenText token == "?" -> do + _ <- takeToken + omittedMiddle <- peekText ":" + if omittedMiddle + then do + -- `left ? : fallback` is the same omitted-middle form + -- as the adjacent `?:` spelling. + _ <- symbol ":" + coalesce condition + else do + first <- parseConditional + separator <- peekText ":" + if separator + then do + _ <- symbol ":" + second <- parseConditional + pure + ( ConditionalExpression + (mergeSpan (expressionSpan condition) (expressionSpan second)) + condition + first + second + () + ) + else failCurrent "VXP0029" "conditional expression requires ':' and a second result" + Just token | tokenKind token == SymbolToken && tokenText token == "?:" -> do + _ <- takeToken + coalesce condition + Just token + | tokenKind token == SymbolToken && tokenText token == "??" -> + failAt (tokenSpan token) "VXP0030" "null coalescing requires nullable types, which are not implemented" + _ -> pure condition + where + coalesce left = do + fallback <- parseConditional + pure (CoalesceExpression (mergeSpan (expressionSpan left) (expressionSpan fallback)) left fallback ()) + parseLogicalOr , parseLogicalAnd , parseEquality @@ -1037,6 +1173,8 @@ expressionSpan expression = case expression of UnaryExpression value _ _ _ -> value BinaryExpression value _ _ _ _ -> value IsPatternExpression value _ _ _ -> value + ConditionalExpression value _ _ _ _ -> value + CoalesceExpression value _ _ _ -> value CallableExpression value _ _ _ _ _ -> value patternSpan :: Pattern name annotation -> SourceSpan diff --git a/Compiler/Headers/Visual/XSharp/Core/IR.hpp b/Compiler/Headers/Visual/XSharp/Core/IR.hpp index 8f2a9ae1..92eee59f 100644 --- a/Compiler/Headers/Visual/XSharp/Core/IR.hpp +++ b/Compiler/Headers/Visual/XSharp/Core/IR.hpp @@ -93,7 +93,10 @@ namespace Visual::XSharp::Core Apply, ///< Function invocation. Primitive, ///< Built-in operation application. Closure, ///< Anonymous function with explicit captures. - Let ///< Lexical binding followed by a body expression. + Let, ///< Lexical binding followed by a body expression. + /// Two-way selection that evaluates exactly one of two arms. + /// operands holds the test, the first arm and the second arm. + Conditional }; /// Active expression category. @@ -116,7 +119,8 @@ namespace Visual::XSharp::Core SymbolName letSymbol{}; /// Binding type for the Let expression. Type letType{ Type::unit() }; - /// Ordered call or primitive arguments. + /// Ordered call or primitive arguments; for Conditional, the test + /// followed by the first and the second arm. std::vector operands; /// Lexical values captured by a closure. std::vector captures; @@ -183,6 +187,17 @@ namespace Visual::XSharp::Core Expression value, Expression body, Type resultType) -> Expression; + /// Construct a selection between two lazily evaluated arms. + /// @param test Value tested in Boolean context. + /// @param whenTrue Arm evaluated when the test is true. + /// @param whenFalse Arm evaluated when the test is false. + /// @param resultType Static type shared by both arms. + /// @return A Conditional expression owning the three children. + [[nodiscard]] static auto + Conditional(Expression test, + Expression whenTrue, + Expression whenFalse, + Type resultType) -> Expression; /// Compare kind-specific payloads and their child expressions. /// @param other Expression to compare with this value. /// @return true when both trees have identical structure and types. diff --git a/Compiler/Headers/Visual/XSharp/Core/Wire.hpp b/Compiler/Headers/Visual/XSharp/Core/Wire.hpp index f30af5f8..a44a37b7 100644 --- a/Compiler/Headers/Visual/XSharp/Core/Wire.hpp +++ b/Compiler/Headers/Visual/XSharp/Core/Wire.hpp @@ -14,7 +14,7 @@ namespace Visual::XSharp::Core::Wire { /// Current VXCR schema version. Decoders require an exact match. - inline constexpr std::uint16_t kCurrentVersion = 7; + inline constexpr std::uint16_t kCurrentVersion = 8; /// Per-call resource ceilings for encoding and decoding. /// Untrusted input is checked against these bounds before allocation. diff --git a/Documents/ARTIFACT-WIRE.md b/Documents/ARTIFACT-WIRE.md index af7f46fc..ec07a19a 100644 --- a/Documents/ARTIFACT-WIRE.md +++ b/Documents/ARTIFACT-WIRE.md @@ -50,7 +50,7 @@ still fit its declared scalar width. ## Source ownership -Core v6 introduced, and Core v7 retains, the physical project source catalog +Core v6 introduced, and Core v8 retains, the physical project source catalog and the source owner of each function. CorePrep v6 and Xpp/Xmm v5 also carry this provenance. The catalog is ordered exactly like the frontend's deterministic project-relative source set. @@ -80,7 +80,7 @@ source that caused its emission. Wire v5 introduced explicit tags for unit/no-result, boolean, string, function, named, variable, character, every signed and unsigned integer width, and every -floating width. Wire v6 added source ownership, and Core v7 retains both that +floating width. Wire v6 added source ownership, and Core v8 retains both that metadata and the scalar catalog while adding loop statement tags. A decoder reconstructs the exact type; it does not infer width from the literal byte count. @@ -91,7 +91,7 @@ the bit pattern is identical. ### Core scalar tags (introduced in v5) -The native and Haskell Core codecs retain these assignments in version 7. This +The native and Haskell Core codecs retain these assignments in version 8. This table is an implementation-maintenance aid, not a user extension API. | Tag | Type | Tag | Type | @@ -139,7 +139,7 @@ type record is decoded. Core and CorePrep encode named-type arguments as typed values rather than treating every argument as another type. Their current format retains that -ordered sum; Core v7 and CorePrep v6 also carry source provenance. Each argument +ordered sum; Core v8 and CorePrep v6 also carry source provenance. Each argument starts with a kind tag and is decoded in source order: | Tag | Argument payload | @@ -188,8 +188,8 @@ different role. ## Core loop statement tags -Core wire v7 preserves the existing statement tags 0 through 4 and appends -explicit loop-control records. The tag is followed by each field in the order +Core wire v7 kept the existing statement tags 0 through 4 and appended +explicit loop-control records; v8 retains them unchanged. The tag is followed by each field in the order shown; nested statement lists use the ordinary bounded statement-vector encoding. @@ -210,10 +210,37 @@ before the condition is evaluated again. A `do/while` condition follows the body because it is evaluated after every entered body, including on the first iteration. +## Core expression tags + +Every Core expression starts with one tag byte. A primitive is followed by its +primitive tag; then every expression writes its result type, and the remaining +payload follows in the order shown. + +| Tag | Expression | Payload order after the result type | +| ---: | --- | --- | +| 0 | `CoreVariable` | symbol | +| 1 | `CoreLiteral` | literal payload | +| 2 | `CoreApply` | callee, argument vector | +| 3 | `CorePrimitive` | operand vector | +| 4 | `CoreClosure` | capture vector, parameter vector, return type, body | +| 5 | `CoreLet` | symbol, binding type, value, body | +| 6 | `CoreConditional` | test, first arm, second arm | + +Core wire v8 added tag 6. Its three children have fixed positions, so the +record carries no count: a shorter payload is a truncation, never a smaller +conditional. Each child counts one level against the expression depth limit, +exactly like a primitive operand. The reader does not check that the arms +agree with the result type; that is the Core verifier's rule and runs on every +decoded module. + +CorePrep, Xpp, and Xmm have no conditional-expression record. The expression +is lowered to blocks, a branch, and assignments to one slot before CorePrep is +serialized, so their versions did not change. + ### Version transition Versions are strict, not feature-negotiated. Core readers accept only version -7, CorePrep readers accept only version 6, and Xpp/Xmm readers accept only +8, CorePrep readers accept only version 6, and Xpp/Xmm readers accept only version 5. Every older or future version fails at the version field before body decoding. The compiler does not guess whether a document happens to contain only fields from an older schema. @@ -420,8 +447,9 @@ verified again before serialization or forward lowering. ## Compatibility policy The version field describes the entire schema. Core v6 and CorePrep v6 added -project source catalogs and per-function ownership; Core v7 additionally adds -structured `while`, `do/while`, classic `for`, `break`, and `continue` records. +project source catalogs and per-function ownership; Core v7 additionally added +structured `while`, `do/while`, classic `for`, `break`, and `continue` records, +and Core v8 adds the conditional expression record. Xpp/Xmm began independently at version 1; their current version 5 retains the explicit ownership operations, template values, and type-test operation, and adds source catalogs and function diff --git a/Documents/CLOSURE-PIPELINE.md b/Documents/CLOSURE-PIPELINE.md index 398f6cf9..e86c4f57 100644 --- a/Documents/CLOSURE-PIPELINE.md +++ b/Documents/CLOSURE-PIPELINE.md @@ -84,11 +84,11 @@ symbol validity, parameter uniqueness, nested expressions, and return behavior. Optimization recursively folds capture initializers and closure bodies without reordering captures. -Core wire version 7 serializes ownership, captures, parameters, return type, +Core wire version 8 serializes ownership, captures, parameters, return type, nested statements, and structured loop statements. Existing byte, count, type-depth, and expression-depth limits also apply to closures. -The native VXCR v7 reader and writer carry the same closure expression tag and +The native VXCR v8 reader and writer carry the same closure expression tag and field order as the Haskell frontend. The C++ Core verifier validates capture ownership, callable shape, nested body returns, and capture mutation before CorePrep lifting. This keeps callable-containing `.vxs` input on the ordinary diff --git a/Documents/COMPILER-PIPELINE.md b/Documents/COMPILER-PIPELINE.md index cb781be8..9e776a23 100644 --- a/Documents/COMPILER-PIPELINE.md +++ b/Documents/COMPILER-PIPELINE.md @@ -311,7 +311,7 @@ Artifact ownership is explicit: `check` writes no artifact. Binary emission creates the required entry bridge, writes a temporary object, invokes LLD with a typed argument vector rather than a shell string, validates the resulting executable, and removes its temporary object. -Project binary builds produce one executable. The entry namespace's Core v7 +Project binary builds produce one executable. The entry namespace's Core v8 preserves source ownership through CorePrep v6, Xpp v5, and Xmm v5. Project object and assembly emission lowers each source in that selected namespace's source catalog into a separate `.o` or `.asm` in the selected output directory. diff --git a/Documents/CORE-IR.md b/Documents/CORE-IR.md index 472b93f4..fb224c87 100644 --- a/Documents/CORE-IR.md +++ b/Documents/CORE-IR.md @@ -261,21 +261,33 @@ the same loop shape: the right operand and overwrites the slot. Calls, traps, and non-termination in the right operand therefore stay conditional. A short-circuit condition of a loop is evaluated starting at the loop header, so the loop's own - body/exit branch may sit in the operator's join block. - -`LoopLoweringTests.cpp` and `ShortCircuitLoweringTests.cpp` under -`Compiler/Core/Tests/` assert these edges exactly on the native adapter. -`LoopExecutionTests.cpp` and `ShortCircuitExecutionTests.cpp` under + body/exit branch may sit in the operator's join block; +- a conditional expression is control flow as well. Its result slot is bound + before the branch with the neutral literal of its type (`false`, an integer + zero, or a floating zero), the test selects one of two arm blocks, each arm + block computes only its own operand and assigns the slot, and both jump to + one join block that continues the surrounding expression. No path reaches + the join without one of the two assignments, so the neutral value is never + observable. + +`LoopLoweringTests.cpp`, `ShortCircuitLoweringTests.cpp`, and +`ConditionalLoweringTests.cpp` under `Compiler/Core/Tests/` assert these edges +exactly on the native adapter. `LoopExecutionTests.cpp`, +`ShortCircuitExecutionTests.cpp`, and `ConditionalExecutionTests.cpp` under `Compiler/Backend/LLVM/Tests/` execute each form through CorePrep, Xpp, Xmm, and LLVM with both native optimizer settings and compare the result with host -code; the short-circuit programs guard a division or a recursive call, so an -eager right operand traps or never returns instead of merely producing the -same Boolean. A CorePrep, Xpp, +code; the short-circuit and conditional programs guard a division or a +recursive call, so an eagerly evaluated operand traps or never returns instead +of merely producing the same value. A CorePrep, Xpp, or Xmm verifier cannot reject a wrong back-edge by itself: a block that jumps to itself is a well-formed control-flow graph. +Both lowerings bind a `CoreLet` value with the value's own operation rather +than through an extra copied temporary, so a let over a call is one call +instruction in either adapter. + Both lowerings also allocate generated symbols the same way. Temporaries, -condition and short-circuit slots, and lifted closure names come from one +condition, short-circuit and conditional slots, and lifted closure names come from one counter that starts above every symbol identity in the whole module and is never reset between functions. Identities are module-wide and CorePrep verification rejects one identity with two spellings, so a counter seeded @@ -329,6 +341,57 @@ Arithmetic operands must be numeric and use the same type. Comparisons return `bool`. Logical operands accept bool or numeric context and return `bool`. Unary primitives take one operand; other primitives take two. +### Let + +`CoreLet` binds one immutable symbol to a value and evaluates a body with that +symbol in scope. The value is evaluated exactly once, before the body. The +binding is visible only in the body; the verifier rejects a read of the symbol +anywhere else, including the other arm of an enclosing conditional. Lowering +uses it wherever a source operand is read more than once but must be evaluated +once: pattern subjects, inlined call arguments, and the left operand of truthy +coalescing. + +### Conditional + +`CoreConditional` contains a test, a first arm, a second arm, and a result +type. The test is evaluated in Boolean context; then exactly one arm is +evaluated and becomes the value. The other arm is not evaluated at all: its +calls, traps, and non-termination do not happen. + +The verifier requires: + +- a test of `bool` or numeric type (`VXC1067`); +- both arms to have exactly the result type (`VXC1068`, `VXC1069`); and +- a `bool` or numeric result type (`VXC1070`). + +The last rule is a storage rule, not a language rule. CorePrep materializes the +result in one slot that both arms assign; a slot that held an owned value would +need move and release rules that the backend does not define yet. The native +verifier additionally rejects an in-memory conditional that does not carry +exactly three operands (`VXC1071`); the wire reader cannot produce one. + +Source `condition ? first : second` lowers to one `CoreConditional`. Source +`left ?: fallback` lowers to + +```text +let $coalesceN = left in ($coalesceN ? $coalesceN : fallback) +``` + +so the left operand is evaluated once and is both the test and the first +result. + +Analyses treat the two arms as alternative paths, not as a sequence: + +- effect inference adds the test's effect to the effect of each arm that is + feasible under the incoming integer facts; an arm excluded by a known test + contributes nothing; +- integer facts are refined by the test on each edge, the arms are transferred + separately, and the continuation keeps only the join of both results; +- constant folding replaces a conditional whose test is a literal by the + selected arm, and folds inside both arms otherwise; and +- inlining rewrites a call inside an arm in place, so an inlined body stays + behind the same test. + ### Closure `CoreClosure` contains: @@ -397,6 +460,8 @@ stop after the first malformed statement. Diagnostic groups cover: - variable lookup and type agreement; - call signatures; - primitive arity, operand, and result types; +- let binding types and scope; +- conditional test, arm, and result types; - literal payload/range validity; and - closure callable/capture contracts. @@ -414,7 +479,8 @@ Its passes may: - propagate immutable literal bindings; - fold exact integer and boolean primitives; -- select known branches; +- select known branches and the selected arm of a conditional with a literal + test; - remove unreachable statements; - delete unused pure bindings, writes, and evaluations; and - optimize nested closure bodies. @@ -430,6 +496,8 @@ CorePrep converts nested expression evaluation to atoms and operations. It: - introduces deterministic temporary symbols; - creates explicit basic blocks; - translates `CoreIf` to branch/jump structure; +- translates short-circuit operators and conditional expressions to branches + over a result slot; - lifts closure bodies to functions; - materializes closure creation operations; and - verifies targets, definitions, operation types, and terminators. diff --git a/Documents/DIAGNOSTICS.md b/Documents/DIAGNOSTICS.md index 37a842eb..132f3d6b 100644 --- a/Documents/DIAGNOSTICS.md +++ b/Documents/DIAGNOSTICS.md @@ -165,6 +165,39 @@ and logical operators. A `bool` return type does not turn the integers in equality operands, and constant folding compares their actual values. Numeric equality never falls back to comparing truthiness. +### Conditional and assignment forms + +`condition ? first : second` and `left ?: fallback` are the weakest expression +level and group to the right. A `?` that is not followed by a result and `:` +produces `VXP0029`. `left ? : fallback` with a spaced empty middle is the same +omitted-middle form as `?:`. + +`??` and `??=` are recognized as tokens so that they are not read as two +conditionals. They need nullable types, which are not implemented; the parser +reports `VXP0030` and `VXP0031` instead of guessing a meaning. + +A compound assignment (`+=`, `-=`, `*=`, `/=`, `//=`, `%=`, `**=`, `<<=`, +`>>=`, `&=`, `^=`, `|=`) is a statement with a named target; another target +expression produces `VXP0003`, the same diagnostic as for `=`. `_ = value;` is +the discard statement, not an assignment to a binding named `_`, and is not an +expression: `(_ = value)` is a syntax error. + +The type checker reports: + +| Code | Meaning | +| --- | --- | +| `VXT0035` | the operator result of a compound assignment does not have the target type, for example floating `//=` | +| `VXT0036` | a conditional test is neither `bool` nor numeric | +| `VXT0037` | the two results of a conditional have different types | +| `VXT0038` | the two operands of truthy coalescing have different types | +| `VXT0039` | the result of a conditional form is neither `bool` nor numeric; other result types are not lowered yet | + +A compound assignment otherwise reuses the assignment and operator +diagnostics: `VXT0003` for an immutable target and `VXT0012` for operands the +operator does not accept. An untyped numeric literal operand of a conditional +form takes the type of the other operand in either direction, and both take +the expected type when there is one; a computed operand is never converted. + ### Supplied token streams Embedding clients may supply a token list through `ParserInput`. An empty list diff --git a/Documents/FUZZING.md b/Documents/FUZZING.md index 49ca0b6e..6c321ed8 100644 --- a/Documents/FUZZING.md +++ b/Documents/FUZZING.md @@ -25,8 +25,11 @@ memory safety or complete language coverage. The differential oracle independently evaluates one generated program per input: a bounded arithmetic expression, a classic `for` loop with `continue` and `break`, a `do`/`while` loop, an `if`/`else` over that expression, a -`while` loop preceded by its own initializers, or a recursion that terminates -only because `||` and `&&` skip their right operands. The expected value is computed +`while` loop preceded by its own initializers, a recursion that terminates +only because `||` and `&&` skip their right operands, a recursion and a +division that are defined only because a conditional expression evaluates one +arm, a loop built from truthy coalescing and compound assignments, or a chain +of nested conditionals. The expected value is computed by ordinary host code in the harness, never by a second compiler path. It compiles the source once, lowers the same verified Core with Xpp/Xmm optimizations both disabled and enabled, executes both verified @@ -183,7 +186,8 @@ whole corpus and sanitizer cache rather than just the final input. documents. It runs independently of libFuzzer and does not claim guided coverage. `source_fuzz_smoke` checks valid-source lowering and then runs the differential oracle on every generated program shape at trip counts 0 through -11 before mutation campaigns begin. Set `VXS_FUZZ_TRACE=1` to print each +11, once with a zero and once with a nonzero generated expression, before +mutation campaigns begin. Set `VXS_FUZZ_TRACE=1` to print each generated source with its reference and optimized LLVM IR. Neither smoke program nor the HPC engine has libFuzzer's per-input timeout, and @@ -227,7 +231,9 @@ The harnesses above are evidence about the inputs they ran, not proofs: arithmetic model. They do not model every option interaction, lockfile refresh, project evaluation or REPL declaration form; - the differential generator covers integer arithmetic, three loop forms, - one conditional and one guarded recursion. Closures, other scalar types, + one conditional statement, guarded recursion through short-circuit operators + and through conditional expressions, truthy coalescing, and five compound + assignment operators. Closures, other scalar types, ownership and templates have no generated-program oracle here; their executable checks live in the component test suites; - `ownership_fuzzer` varies thread count and iteration count, not arbitrary diff --git a/Documents/IMPLEMENTATION.md b/Documents/IMPLEMENTATION.md index 9ca4c83a..9780ade6 100644 --- a/Documents/IMPLEMENTATION.md +++ b/Documents/IMPLEMENTATION.md @@ -38,7 +38,10 @@ The Haskell package exposes separate modules for: - pipeline diagnostics and orchestration. The current language slice covers namespace and class declarations, member methods, typed and inferred local bindings, -assignments, calls, returns, conditionals, core operator precedence, entry-point validation, and basic CorePrep control flow. +assignments, compound assignments, the discard statement, calls, returns, conditionals, conditional and truthy-coalescing +expressions over `bool` and numeric values, core operator precedence, entry-point validation, and basic CorePrep control flow. +Assignment is a statement: the value-producing forms `a = b = 10` and `(value += 5)`, null coalescing `??`, and `??=` are not +implemented. It does not yet implement the complete language catalog in `Spec/`. Core optimization is connected, verifier-guarded, and fixed-point driven. It performs immutable literal propagation, diff --git a/Documents/MONOMORPHIZATION.md b/Documents/MONOMORPHIZATION.md index 1d11954f..a1a2003d 100644 --- a/Documents/MONOMORPHIZATION.md +++ b/Documents/MONOMORPHIZATION.md @@ -26,7 +26,7 @@ The implemented slice includes: - unambiguous literal value arguments such as `Buffer<32>`; - exact integer, character, Boolean, unary, and binary constant evaluation; - an ordered `TemplateArgument` sum in typed AST, Core, CorePrep, Xpp, and Xmm; -- strict Core v7, CorePrep v6, and Xpp/Xmm v5 codecs; +- strict Core v8, CorePrep v6, and Xpp/Xmm v5 codecs; - recursive structural validation and parameter collection; - independent type-parameter and value-parameter substitution; - deterministic structural identity rendering; diff --git a/Documents/PROJECT-ARTIFACTS.md b/Documents/PROJECT-ARTIFACTS.md index 38411bd0..818a6c92 100644 --- a/Documents/PROJECT-ARTIFACTS.md +++ b/Documents/PROJECT-ARTIFACTS.md @@ -85,7 +85,7 @@ explicit single-file module may omit project ownership metadata. A project object/assembly build requires a complete, valid catalog and owners for every function in the selected module. -Current wire versions are Core v7, CorePrep v6, Xpp v5, and Xmm v5. Older +Current wire versions are Core v8, CorePrep v6, Xpp v5, and Xmm v5. Older versions are rejected by their owning reader. The new fields are part of the schema and are not silently defaulted when reading an older project artifact. See [Artifact wire contracts](ARTIFACT-WIRE.md) for field order, decoding @@ -233,7 +233,7 @@ The relevant tests are kept with their owners: - `Compiler/Artifact/Tests/SourcePathTests.cpp` covers canonical source identity and portable path rejection. -- `Compiler/Core/Tests/CorePipelineTests.cpp` covers the non-empty Haskell v7 +- `Compiler/Core/Tests/CorePipelineTests.cpp` covers the non-empty Haskell v8 golden, metadata propagation through Xpp/Xmm, per-source object and assembly output, empty source units, and collision preservation. - `Compiler/Driver/Tests/ProjectArtifactTests.cpp` covers flattening, diff --git a/Documents/README.md b/Documents/README.md index 9a247f3c..c95cc1b1 100644 --- a/Documents/README.md +++ b/Documents/README.md @@ -68,7 +68,7 @@ following vocabulary: - [Template monomorphization](MONOMORPHIZATION.md) defines concrete specialization identity, placement, demand discovery, and current limits. - [Numeric types](NUMERIC-TYPES.md) records fixed scalar widths and the gap between frontend semantics and the current Core transport. -- [Scalar pipeline](SCALAR-PIPELINE.md) follows fixed-width values through verification, Core wire v7, Xpp, Xmm, and LLVM. +- [Scalar pipeline](SCALAR-PIPELINE.md) follows fixed-width values through verification, Core wire v8, Xpp, Xmm, and LLVM. - [Artifact wire](ARTIFACT-WIRE.md) defines the bounded internal Core and CorePrep transport contracts. - [Control-flow safety](CONTROL-FLOW-SAFETY.md) defines short-circuit lowering and the shared Xpp/Xmm definite-initialization contract. diff --git a/Documents/SCALAR-PIPELINE.md b/Documents/SCALAR-PIPELINE.md index a1223338..1df30a7a 100644 --- a/Documents/SCALAR-PIPELINE.md +++ b/Documents/SCALAR-PIPELINE.md @@ -107,7 +107,7 @@ canonical. These rules make the representation independent of endianness and make equality deterministic. Compatibility alternatives for the historical signed 32-bit and 64-bit -payloads remain readable in the in-memory variant. Current Core v7 and CorePrep +payloads remain readable in the in-memory variant. Current Core v8 and CorePrep v6 producers use the structured representation for the complete scalar catalog. @@ -137,9 +137,9 @@ numeric types; CorePrep compares each numeric operand with a same-typed zero so Xpp, Xmm, and LLVM receive canonical booleans. Numeric branch conditions use the same conversion. -## Core wire v7 +## Core wire v8 -Core wire v7 writes a distinct type tag for every catalog member and retains +Core wire v8 writes a distinct type tag for every catalog member and retains the project source ownership introduced in Core v6. It adds structured loop statement tags without changing scalar payloads. Integer payloads contain: @@ -158,7 +158,7 @@ larger than all current scalar widths but protects decoders before semantic range checking. A decoder rejects unknown versions, unknown tags, invalid sign bytes, over-limit lengths, truncated payloads, and trailing bytes. -The Core decoder accepts only version 7. Versions 2 through 6 are rejected +The Core decoder accepts only version 8. Versions 2 through 7 are rejected before body decoding; no older tag is reinterpreted under the current schema. ## CorePrep adaptation