From 46e4d9c16dcd2e72635fa20c3083f66e3708322e Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sun, 23 Nov 2025 21:26:01 +0000 Subject: [PATCH 01/11] add basic mutability --- antlr4/DepCParser.g4 | 5 +- dep0/lib/00_core/include/dep0/match.hpp | 6 +- .../00_core/test/dep0_core_match_tests.cpp | 10 ++ dep0/lib/01_ast/CMakeLists.txt | 2 + .../dep0/ast/alpha_equivalence_impl.hpp | 8 + dep0/lib/01_ast/include/dep0/ast/ast.hpp | 10 +- .../include/dep0/ast/hash_code_impl.hpp | 4 + .../include/dep0/ast/max_index_impl.hpp | 4 + .../dep0/ast/mutable_place_expression.hpp | 55 +++++++ .../include/dep0/ast/occurs_in_impl.hpp | 4 + .../01_ast/include/dep0/ast/pretty_print.hpp | 3 + .../include/dep0/ast/pretty_print_impl.hpp | 8 + .../01_ast/include/dep0/ast/replace_impl.hpp | 5 + .../lib/01_ast/include/dep0/ast/size_impl.hpp | 4 + .../01_ast/src/mutable_place_expression.cpp | 7 + dep0/lib/02_parser/src/parse.cpp | 19 ++- dep0/lib/02_parser/test/CMakeLists.txt | 1 + .../dep0_parser_tests_0024_mutability.cpp | 146 ++++++++++++++++++ dep0/lib/03_typecheck/CMakeLists.txt | 2 + .../include/dep0/typecheck/context.hpp | 11 +- dep0/lib/03_typecheck/src/beta_reduction.cpp | 13 ++ dep0/lib/03_typecheck/src/check.cpp | 68 ++++++-- dep0/lib/03_typecheck/src/context.cpp | 35 +++-- dep0/lib/03_typecheck/src/delta_unfold.cpp | 7 + .../lib/03_typecheck/src/derivation_rules.cpp | 10 +- dep0/lib/03_typecheck/src/is_impossible.cpp | 1 + dep0/lib/03_typecheck/src/is_terminator.cpp | 1 + dep0/lib/03_typecheck/src/max_scope.cpp | 4 + .../lib/03_typecheck/src/private/root_var.hpp | 33 ++++ dep0/lib/03_typecheck/src/rewrite.cpp | 18 ++- dep0/lib/03_typecheck/src/root_var.cpp | 69 +++++++++ dep0/lib/03_typecheck/src/substitute.cpp | 5 + dep0/lib/03_typecheck/src/type_assign.cpp | 1 + dep0/lib/03_typecheck/test/CMakeLists.txt | 1 + .../dep0_typecheck_tests_0024_mutability.cpp | 91 +++++++++++ dep0/lib/05_llvmgen/src/gen_body.cpp | 4 + .../dep0/testing/ast_predicates/func_arg.hpp | 28 +++- .../dep0/testing/ast_predicates/stmt.hpp | 24 +++ testfiles/0024_mutability/pass_000.depc | 28 ++++ testfiles/0024_mutability/pass_001.depc | 10 ++ testfiles/0024_mutability/pass_002.depc | 11 ++ .../0024_mutability/typecheck_error_000.depc | 11 ++ .../0024_mutability/typecheck_error_001.depc | 11 ++ .../0024_mutability/typecheck_error_002.depc | 16 ++ .../0024_mutability/typecheck_error_003.depc | 12 ++ .../0024_mutability/typecheck_error_004.depc | 18 +++ .../0024_mutability/typecheck_error_005.depc | 14 ++ .../0024_mutability/typecheck_error_006.depc | 19 +++ .../0024_mutability/typecheck_error_007.depc | 14 ++ 49 files changed, 847 insertions(+), 44 deletions(-) create mode 100644 dep0/lib/01_ast/include/dep0/ast/mutable_place_expression.hpp create mode 100644 dep0/lib/01_ast/src/mutable_place_expression.cpp create mode 100644 dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp create mode 100644 dep0/lib/03_typecheck/src/private/root_var.hpp create mode 100644 dep0/lib/03_typecheck/src/root_var.cpp create mode 100644 dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp create mode 100644 testfiles/0024_mutability/pass_000.depc create mode 100644 testfiles/0024_mutability/pass_001.depc create mode 100644 testfiles/0024_mutability/pass_002.depc create mode 100644 testfiles/0024_mutability/typecheck_error_000.depc create mode 100644 testfiles/0024_mutability/typecheck_error_001.depc create mode 100644 testfiles/0024_mutability/typecheck_error_002.depc create mode 100644 testfiles/0024_mutability/typecheck_error_003.depc create mode 100644 testfiles/0024_mutability/typecheck_error_004.depc create mode 100644 testfiles/0024_mutability/typecheck_error_005.depc create mode 100644 testfiles/0024_mutability/typecheck_error_006.depc create mode 100644 testfiles/0024_mutability/typecheck_error_007.depc diff --git a/antlr4/DepCParser.g4 b/antlr4/DepCParser.g4 index 99c49eab..6c652f80 100644 --- a/antlr4/DepCParser.g4 +++ b/antlr4/DepCParser.g4 @@ -55,7 +55,7 @@ structDef: 'struct' name=ID '{' (fieldDecl SEMI)* '}' SEMI; fieldDecl: fieldType=expr fieldName=ID; // Types -funcArg: ({one_of("0", "1")}? qty=INT)? ('typename' | expr) name=ID?; +funcArg: ({one_of("0", "1")}? qty=INT)? ('typename' | expr) 'mutable'? name=ID?; type: primitiveType | funcType | tupleType | typeVar; primitiveType: 'bool_t' | 'cstr_t' | 'unit_t' | 'i8_t' | 'i16_t' | 'i32_t' | 'i64_t' | 'u8_t' | 'u16_t' | 'u32_t' | 'u64_t'; funcType: '(' (funcArg (',' funcArg)*)? ')' 'mutable'? '->' ('typename' | retType=expr); @@ -65,9 +65,10 @@ typeVar: name=ID; // Statements body: '{' stmt* '}'; -stmt: funcCallStmt | ifElse | returnStmt | impossibleStmt; +stmt: funcCallStmt | assignment | ifElse | returnStmt | impossibleStmt; funcCallStmt: func=expr '(' (expr (',' expr)*)? ')' ';'; +assignment: lhs=expr '=' rhs=expr ';'; ifElse: 'if' '(' cond=expr ')' true_branch=bodyOrStmt ('else' false_branch=bodyOrStmt)?; bodyOrStmt: body | stmt; returnStmt: 'return' expr? ';'; diff --git a/dep0/lib/00_core/include/dep0/match.hpp b/dep0/lib/00_core/include/dep0/match.hpp index 6d09c32a..93e96a5e 100644 --- a/dep0/lib/00_core/include/dep0/match.hpp +++ b/dep0/lib/00_core/include/dep0/match.hpp @@ -29,10 +29,9 @@ template decltype(auto) match(std::variant const& x, Fs&&... fs) { using F = decltype(boost::hana::overload(std::forward(fs)...)); - using R = decltype(std::declval()(std::get<0>(x))); auto const jump_table = [&] (std::index_sequence) { - static_assert((std::is_same_v()(std::get(x)))> or ...)); + using R = std::common_reference_t()(std::get(x)))...>; return std::array{ (+[] (std::variant const& x, F&& f) -> R { @@ -52,10 +51,9 @@ template decltype(auto) match(std::variant& x, Fs&&... fs) { using F = decltype(boost::hana::overload(std::forward(fs)...)); - using R = decltype(std::declval()(std::get<0>(x))); auto const jump_table = [&] (std::index_sequence) { - static_assert((std::is_same_v()(std::get(x)))> or ...)); + using R = std::common_reference_t()(std::get(x)))...>; return std::array{ (+[] (std::variant& x, F&& f) -> R { diff --git a/dep0/lib/00_core/test/dep0_core_match_tests.cpp b/dep0/lib/00_core/test/dep0_core_match_tests.cpp index 82f9ff83..1a05d66e 100644 --- a/dep0/lib/00_core/test/dep0_core_match_tests.cpp +++ b/dep0/lib/00_core/test/dep0_core_match_tests.cpp @@ -57,6 +57,16 @@ BOOST_AUTO_TEST_CASE(three_match_three_out_of_order) BOOST_TEST(dep0::match(z, f3, f1, f2) == "test"); } +BOOST_AUTO_TEST_CASE(deduce_common_return_type) +{ + std::variant x{23}; + auto const f1 = [] (int) { return nullptr; }; + auto const f2 = [] (dummy_t) { return ""; }; + auto const f3 = [] (std::string const& x) { return x.c_str(); }; + static_assert(std::is_same_v); + static_assert(std::is_same_v); +} + BOOST_AUTO_TEST_CASE(return_const_ref) { std::variant x{dummy_t{23}}; diff --git a/dep0/lib/01_ast/CMakeLists.txt b/dep0/lib/01_ast/CMakeLists.txt index 7846b1f0..d74cec94 100644 --- a/dep0/lib/01_ast/CMakeLists.txt +++ b/dep0/lib/01_ast/CMakeLists.txt @@ -16,6 +16,7 @@ add_library(dep0_ast_lib include/dep0/ast/max_index.hpp include/dep0/ast/max_index_impl.hpp include/dep0/ast/mutable.hpp + include/dep0/ast/mutable_place_expression.hpp include/dep0/ast/occurs_in.hpp include/dep0/ast/occurs_in_impl.hpp include/dep0/ast/place_expression.hpp @@ -40,6 +41,7 @@ add_library(dep0_ast_lib src/hash_code.cpp src/max_index.cpp src/mutable.cpp + src/mutable_place_expression.cpp src/occurs_in.cpp src/place_expression.cpp src/pretty_print.cpp diff --git a/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp index fc150a3d..32fa1bf6 100644 --- a/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp @@ -282,6 +282,14 @@ struct alpha_equivalence_visitor return is_alpha_equivalent_impl(x.value.get(), y.value.get()); } + result_t operator()(typename stmt_t

::assign_t& x, typename stmt_t

::assign_t& y) const + { + auto eq = is_alpha_equivalent_impl(x.lhs, y.lhs); + if (eq) + eq = is_alpha_equivalent_impl(x.rhs, y.rhs); + return eq; + } + result_t operator()(typename stmt_t

::if_else_t& x, typename stmt_t

::if_else_t& y) const { auto eq = is_alpha_equivalent_impl(x.cond, y.cond); diff --git a/dep0/lib/01_ast/include/dep0/ast/ast.hpp b/dep0/lib/01_ast/include/dep0/ast/ast.hpp index bfca00fb..a2ee897f 100644 --- a/dep0/lib/01_ast/include/dep0/ast/ast.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/ast.hpp @@ -424,6 +424,7 @@ struct func_arg_t properties_t properties; qty_t qty; + is_mutable_t is_mutable; expr_t type; std::optional var; }; @@ -440,6 +441,13 @@ struct stmt_t using body_t = ast::body_t

; using expr_t = ast::expr_t

; + /** @brief Represents an assignment `lhs = rhs;`, for example `x = x+1;` or `x.values[0] = f(23);` */ + struct assign_t + { + expr_t lhs; + expr_t rhs; + }; + /** @brief Represents an `if` or `if-else` statement, whose condition must be of type `%bool_t`. */ struct if_else_t { @@ -473,7 +481,7 @@ struct stmt_t std::optional reason; }; - using value_t = std::variant; + using value_t = std::variant; properties_t properties; value_t value; diff --git a/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp index 94fa6cdd..d0c10571 100644 --- a/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp @@ -101,6 +101,10 @@ std::size_t hash_code_impl(hash_code_state_t

& state, stmt_t

const& x) { return hash_code_impl

(state, x); }, + [&] (stmt_t

::assign_t const& assign) + { + return combine(hash_code_impl(state, assign.lhs), hash_code_impl(state, assign.rhs)); + }, [&] (stmt_t

::if_else_t const& if_) { return combine( diff --git a/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp index 393a35c9..e02128b6 100644 --- a/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp @@ -47,6 +47,10 @@ std::size_t max_index(body_t

const& x) { return max_index

(x); }, + [] (stmt_t

::assign_t const& x) + { + return std::max(max_index(x.lhs), max_index(x.rhs)); + }, [] (stmt_t

::if_else_t const& if_) { return std::max( diff --git a/dep0/lib/01_ast/include/dep0/ast/mutable_place_expression.hpp b/dep0/lib/01_ast/include/dep0/ast/mutable_place_expression.hpp new file mode 100644 index 00000000..6d8074b9 --- /dev/null +++ b/dep0/lib/01_ast/include/dep0/ast/mutable_place_expression.hpp @@ -0,0 +1,55 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +/** + * @file + * @brief Single-function header declaring `dep0::ast::is_mutable_place_expression()`. + */ +#pragma once + +#include "dep0/ast/ast.hpp" +#include "dep0/ast/unwrap_because.hpp" + +#include "dep0/match.hpp" + +#include + +namespace dep0::ast { + +/** @brief The expression passed to `is_mutable_place_expression()` is not a mutable place. */ +struct immutable_place_t { }; + +template +using is_mutable_place_expression_result_t = + std::variant< + immutable_place_t, + std::reference_wrapper::var_t const>, + std::reference_wrapper::member_t const>, + std::reference_wrapper::subscript_t const>>; + +/** + * @brief Decide whether or not the given expression is a mutable place expression, + * possibly by looking inside because-expressions, eg `x because reason`. + * + * A mutable place expression is an expression that yields a **possibly** mutable memory location, + * for example the name of a variable or the element of an array. + * Whether or not such place is really mutable depends on whether the root variable was declared mutable. + * + * @remarks This concept is fundamentally the same as a place expression but without dereferences. + */ +template +is_mutable_place_expression_result_t

is_mutable_place_expression(expr_t

const& expr) +{ + using result_t = is_mutable_place_expression_result_t

; + return match( + unwrap_because(expr).value, + [] (expr_t

::var_t const& x) { return result_t{std::cref(x)}; }, + [] (expr_t

::member_t const& x) { return result_t{std::cref(x)}; }, + [] (expr_t

::subscript_t const& x) { return result_t{std::cref(x)}; }, + [] (auto const&) { return result_t{immutable_place_t{}}; }); +} + +} // namespace dep0::ast diff --git a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp index 1a008339..fa7af1b8 100644 --- a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp @@ -39,6 +39,10 @@ bool occurs_in(typename expr_t

::var_t const& var, body_t

const& x, occurre { return occurs_in

(var, app, style); }, + [&] (stmt_t

::assign_t const& assign) + { + return occurs_in(var, assign.lhs, style) or occurs_in(var, assign.rhs, style); + }, [&] (stmt_t

::if_else_t const& if_) { return occurs_in(var, if_.cond, style) diff --git a/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp b/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp index 5e21e6f6..9d752fd9 100644 --- a/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp @@ -53,6 +53,9 @@ std::ostream& pretty_print(std::ostream&, body_t

const&, std::size_t indent = template std::ostream& pretty_print(std::ostream&, stmt_t

const&, std::size_t indent = 0ul); +template +std::ostream& pretty_print(std::ostream&, typename stmt_t

::assign_t const&, std::size_t indent = 0ul); + template std::ostream& pretty_print(std::ostream&, typename stmt_t

::if_else_t const&, std::size_t indent = 0ul); diff --git a/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp index 5615f201..8e6188d9 100644 --- a/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp @@ -242,6 +242,14 @@ std::ostream& pretty_print(std::ostream& os, stmt_t

const& x, std::size_t con return os; } +template +std::ostream& pretty_print(std::ostream& os, typename stmt_t

::assign_t const& x, std::size_t const indent) +{ + pretty_print(os, x.lhs, indent) << " = "; + pretty_print(os, x.rhs, indent) << ';'; + return os; +} + template std::ostream& pretty_print(std::ostream& os, typename stmt_t

::if_else_t const& x, std::size_t const indent) { diff --git a/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp index 2d99a6e9..f35015e3 100644 --- a/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp @@ -39,6 +39,11 @@ void replace(typename expr_t

::var_t const& from, typename expr_t

::var_t co { return replace

(from, to, app); }, + [&] (typename stmt_t

::assign_t& assign) + { + replace(from, to, assign.lhs); + replace(from, to, assign.rhs); + }, [&] (typename stmt_t

::if_else_t& if_) { replace(from, to, if_.cond); diff --git a/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp index f9c72f90..f6b9d120 100644 --- a/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp @@ -54,6 +54,10 @@ std::size_t size(stmt_t

const& x) { return size

(x); }, + [] (stmt_t

::assign_t const& assign) + { + return 1ul + std::max(size(assign.lhs), size(assign.rhs)); + }, [] (stmt_t

::if_else_t const& if_) { return 1ul + std::max( diff --git a/dep0/lib/01_ast/src/mutable_place_expression.cpp b/dep0/lib/01_ast/src/mutable_place_expression.cpp new file mode 100644 index 00000000..4b8e2d96 --- /dev/null +++ b/dep0/lib/01_ast/src/mutable_place_expression.cpp @@ -0,0 +1,7 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#include "dep0/ast/mutable_place_expression.hpp" diff --git a/dep0/lib/02_parser/src/parse.cpp b/dep0/lib/02_parser/src/parse.cpp index e3be1a34..7a9ed9a2 100644 --- a/dep0/lib/02_parser/src/parse.cpp +++ b/dep0/lib/02_parser/src/parse.cpp @@ -282,10 +282,11 @@ struct parse_visitor_t : dep0::DepCParserVisitor if (qty == "1") return ast::qty_t::one; throw error_t("unexpected quantity when parsing FuncArgContext", loc); }(get_text(src, *ctx->qty)); + auto const is_mutable = ctx->KW_MUTABLE() ? ast::is_mutable_t::yes : ast::is_mutable_t::no; if (ctx->KW_TYPENAME()) - return func_arg_t{loc, qty, visitTypename(ctx->KW_TYPENAME()), get_name()}; + return func_arg_t{loc, qty, is_mutable, visitTypename(ctx->KW_TYPENAME()), get_name()}; if (ctx->expr()) - return func_arg_t{loc, qty, visitExpr(ctx->expr()), get_name()}; + return func_arg_t{loc, qty, is_mutable, visitExpr(ctx->expr()), get_name()}; throw error_t("unexpected alternative when parsing FuncArgContext", loc); } @@ -302,6 +303,7 @@ struct parse_visitor_t : dep0::DepCParserVisitor { assert(ctx); if (ctx->funcCallStmt()) return std::any_cast(visitFuncCallStmt(ctx->funcCallStmt())); + if (ctx->assignment()) return std::any_cast(visitAssignment(ctx->assignment())); if (ctx->ifElse()) return std::any_cast(visitIfElse(ctx->ifElse())); if (ctx->returnStmt()) return std::any_cast(visitReturnStmt(ctx->returnStmt())); if (ctx->impossibleStmt()) return std::any_cast(visitImpossibleStmt(ctx->impossibleStmt())); @@ -326,6 +328,19 @@ struct parse_visitor_t : dep0::DepCParserVisitor [this] (DepCParser::ExprContext* ctx) { return visitExpr(ctx); })}}; } + virtual std::any visitAssignment(DepCParser::AssignmentContext* ctx) override + { + assert(ctx); + assert(ctx->lhs); + assert(ctx->rhs); + return stmt_t{ + get_loc(src, *ctx), + stmt_t::assign_t{ + visitExpr(ctx->lhs), + visitExpr(ctx->rhs) + }}; + } + virtual std::any visitIfElse(DepCParser::IfElseContext* ctx) override { assert(ctx); diff --git a/dep0/lib/02_parser/test/CMakeLists.txt b/dep0/lib/02_parser/test/CMakeLists.txt index a8164853..d1305620 100644 --- a/dep0/lib/02_parser/test/CMakeLists.txt +++ b/dep0/lib/02_parser/test/CMakeLists.txt @@ -48,3 +48,4 @@ add_dep0_parser_test(0020_builtins) add_dep0_parser_test(0021_tuples) add_dep0_parser_test(0022_structs) add_dep0_parser_test(0023_references) +add_dep0_parser_test(0024_mutability) diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp new file mode 100644 index 00000000..0205914c --- /dev/null +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -0,0 +1,146 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#define BOOST_TEST_MODULE dep0_parser_tests_0024_mutability +#include + +#include "parser_tests_fixture.hpp" + +using namespace dep0::testing; + +BOOST_FIXTURE_TEST_SUITE(dep0_parser_tests_0024_mutability, ParserTestsFixture) + +BOOST_AUTO_TEST_CASE(pass_000) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_000.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 4ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("y", is_i32))); + { + auto const f = std::get_if(&pass_result->entries[1]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f0"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], var("x"), plus(var("x"), constant(1)))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } + { + auto const f = std::get_if(&pass_result->entries[2]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f1"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], array_of(is_i32, constant(3)), "xs", yes)); + BOOST_TEST(is_array_of(f->value.ret_type.get(), is_i32, constant(3))); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST( + is_assign( + f->value.body.stmts[0ul], + subscript_of(var("xs"), constant(0)), + subscript_of(var("xs"), constant(1)))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("xs"))); + } + { + auto const f = std::get_if(&pass_result->entries[3]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f2"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], var("t"), "x", yes)); + BOOST_TEST(is_var(f->value.ret_type.get(), "t")); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "y"), constant(0))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } +} + +BOOST_AUTO_TEST_CASE(pass_001) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_001.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 1ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + { + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f"); + BOOST_TEST_REQUIRE(f->value.args.size() == 3ul); + BOOST_TEST(is_arg(f->value.args[0], is_u64, "n", yes)); + BOOST_TEST(is_arg(f->value.args[1], true_t_of(lt(constant(0), var("n"))), std::nullopt, dep0::ast::qty_t::zero)); + BOOST_TEST(is_arg(f->value.args[2], array_of(is_i32, var("n")), "xs")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST(is_return_of(f->value.body.stmts[0ul], subscript_of(var("xs"), constant(0)))); + } +} + +BOOST_AUTO_TEST_CASE(pass_002) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_002.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 1ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + { + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f"); + BOOST_TEST_REQUIRE(f->value.args.size() == 3ul); + BOOST_TEST(is_arg(f->value.args[0], is_u64, "n", yes)); + BOOST_TEST(is_arg(f->value.args[1], true_t_of(lt(constant(0), var("n"))), std::nullopt, dep0::ast::qty_t::zero)); + BOOST_TEST(is_arg(f->value.args[2], array_of(is_i32, var("n")), "xs")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], var("n"), constant(0))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], subscript_of(var("xs"), constant(0)))); + } +} + +BOOST_AUTO_TEST_CASE(typecheck_error_000) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/typecheck_error_000.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 1ul); + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::no)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], var("x"), plus(var("x"), constant(1)))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); +} + +BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(pass("0024_mutability/typecheck_error_001.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_002) { BOOST_TEST(pass("0024_mutability/typecheck_error_002.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_003) { BOOST_TEST(pass("0024_mutability/typecheck_error_003.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_004) { BOOST_TEST(pass("0024_mutability/typecheck_error_004.depc")); } + + +BOOST_AUTO_TEST_CASE(typecheck_error_005) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/typecheck_error_005.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 1ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_u64, "n", yes)); + BOOST_TEST(is_arg(f->value.args[1], array_of(is_i32, var("n")), "xs")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], var("n"), constant(1))); + BOOST_TEST( + is_if_else( + f->value.body.stmts[1ul], + lt(constant(0), var("n")), + std::tuple{return_of(subscript_of(var("xs"), constant(0)))}, + std::tuple{return_of(constant(0))})); +} + +BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(pass("0024_mutability/typecheck_error_006.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(pass("0024_mutability/typecheck_error_007.depc")); } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/CMakeLists.txt b/dep0/lib/03_typecheck/CMakeLists.txt index 09357667..17bd3b60 100644 --- a/dep0/lib/03_typecheck/CMakeLists.txt +++ b/dep0/lib/03_typecheck/CMakeLists.txt @@ -43,6 +43,7 @@ add_library(dep0_typecheck_lib src/private/proof_state.hpp src/private/returns_from_all_branches.hpp src/private/rewrite.hpp + src/private/root_var.hpp src/private/substitute.hpp src/private/unification.hpp src/private/usage.hpp @@ -80,6 +81,7 @@ add_library(dep0_typecheck_lib src/proof_state.cpp src/returns_from_all_branches.cpp src/rewrite.cpp + src/root_var.cpp src/subscript_access.cpp src/substitute.cpp src/unification.cpp diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp index 87f96c21..92e8e4f8 100644 --- a/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp @@ -59,11 +59,13 @@ class ctx_t std::optional scope_id, expr_t::var_t var, ast::qty_t const qty, + ast::is_mutable_t const is_mutable, expr_t type ) : origin(std::move(origin)), scope_id(std::move(scope_id)), var(std::move(var)), qty(qty), + is_mutable(is_mutable), type(std::move(type)) { } @@ -72,6 +74,7 @@ class ctx_t std::optional scope_id; /**< Scope ID of the originating context or empty if unscoped. */ expr_t::var_t var; /**< Copy of the variable to which this declaration was bound, eg `x` in `0 i32_t x`. */ ast::qty_t qty; /**< Quantity of the variable declaration, eg `0` in `0 i32_t x`. */ + ast::is_mutable_t is_mutable; /**< Whether the variable was declared mutable or not. */ expr_t type; /**< Type of the variable declaration, eg `i32_t` in `0 i32_t x`. */ }; @@ -129,7 +132,10 @@ class ctx_t // non-const member functions - /** @brief Add a new variable declaration with the given type and quantity and an automatically generated name. */ + /** + * @brief Add a new immutable variable declaration with the + * given type and quantity and an automatically generated name. + */ void add_unnamed(ast::qty_t, expr_t type); /** @@ -139,7 +145,8 @@ class ctx_t * * If the variable name is nullopt, an automatically generated name will be used as if `add_unnamed()` was invoked. */ - dep0::expected try_emplace(source_text, std::optional, ast::qty_t, expr_t type); + dep0::expected + try_emplace(source_text, std::optional, ast::qty_t, ast::is_mutable_t, expr_t type); private: enum class scope_flavour_t { scoped_v, unscoped_v }; diff --git a/dep0/lib/03_typecheck/src/beta_reduction.cpp b/dep0/lib/03_typecheck/src/beta_reduction.cpp index 7876d470..87c88592 100644 --- a/dep0/lib/03_typecheck/src/beta_reduction.cpp +++ b/dep0/lib/03_typecheck/src/beta_reduction.cpp @@ -27,6 +27,7 @@ namespace dep0::typecheck { namespace impl { static bool beta_normalize(stmt_t&); +static bool beta_normalize(stmt_t::assign_t&); static bool beta_normalize(stmt_t::if_else_t&); static bool beta_normalize(stmt_t::return_t&); static bool beta_normalize(stmt_t::impossible_t&); @@ -73,6 +74,13 @@ bool beta_normalize(stmt_t& stmt) return match(stmt.value, [&] (auto& x) { return beta_normalize(x); }); } +bool beta_normalize(stmt_t::assign_t& assign) +{ + bool changed = beta_normalize(assign.lhs); + changed |= beta_normalize(assign.rhs); + return changed; +} + bool beta_normalize(stmt_t::if_else_t& if_) { bool changed = beta_normalize(if_.cond); @@ -230,6 +238,11 @@ bool beta_normalize(body_t& body) // keep anything else return is_mutable(app) ? std::next(it) : body.stmts.erase(it); }, + [&] (stmt_t::assign_t& assign) + { + // TODO could drop trivial self-assignments, eg `x = x`; + return std::next(it); + }, [&] (stmt_t::if_else_t& if_) { // this step is arguably iota-reduction diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index eba2f856..b7d220f0 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -14,14 +14,16 @@ #include "private/derivation_rules.hpp" #include "private/proof_search.hpp" #include "private/returns_from_all_branches.hpp" +#include "private/root_var.hpp" #include "private/substitute.hpp" #include "private/type_assign.hpp" #include "dep0/typecheck/beta_delta_reduction.hpp" #include "dep0/typecheck/list_initialization.hpp" -#include "dep0/ast/views.hpp" +#include "dep0/ast/mutable_place_expression.hpp" #include "dep0/ast/pretty_print.hpp" +#include "dep0/ast/views.hpp" #include "dep0/fmap.hpp" #include "dep0/match.hpp" @@ -160,7 +162,7 @@ expected check_type_def(env_t& env, parser::type_def_t const& type_d pretty_print(err << "incomplete type for field `", field.var) << '`'; return error_t(err.str(), loc, {std::move(ok.error())}); } - auto var = ctx.try_emplace(field.var.name, loc, ast::qty_t::many, *type); + auto var = ctx.try_emplace(field.var.name, loc, ast::qty_t::many, ast::is_mutable_t::no, *type); if (not var) return std::move(var.error()); return type_def_t::struct_t::field_t{std::move(*type), std::move(*var)}; @@ -315,6 +317,54 @@ check_stmt( else return std::move(app.error()); }, + [&] (parser::stmt_t::assign_t const& x) -> expected + { + auto lhs = type_assign(env, state.context, x.lhs, is_mutable, usage, usage_multiplier); + if (not lhs) + return lhs.error(); + auto rhs = + check_expr(env, state.context, x.rhs, lhs->properties.sort.get(), is_mutable, usage, usage_multiplier); + if (not rhs) + return rhs.error(); + auto const root = match( + ast::is_mutable_place_expression(*lhs), + [&] (ast::immutable_place_t) + { + return error_t("target of assignment is not a mutable place expression", loc); + }, + [&] (std::reference_wrapper const var) -> expected + { + return var.get(); + }, + [&] (std::reference_wrapper const member) -> expected + { + auto const root = root_var(member); + if (not root) + return error_t("target of assignment is not rooted in a variable", loc); + return *root; + }, + [&] (std::reference_wrapper const subscript) -> expected + { + auto const root = root_var(subscript); + if (not root) + return error_t("target of assignment is not rooted in a variable", loc); + return *root; + }); + if (not root) + return root.error(); + auto const decl = state.context[*root]; + assert(decl and "root of assignment was a variable but it did not exist in current context"); + if (decl->is_mutable == ast::is_mutable_t::no) + return error_t("cannot mutate immutable variable"); + auto modified_context = state.context.extend(); + auto const new_var = + modified_context.try_emplace( + decl->var.name, decl->origin, decl->qty, ast::is_mutable_t::yes, decl->type); + if (not new_var) + return new_var.error(); // should not happen, but just in case + state.context = std::move(modified_context); + return make_legal_stmt(stmt_t::assign_t{std::move(*lhs), std::move(*rhs)}); + }, [&] (parser::stmt_t::if_else_t const& x) -> expected { auto cond = @@ -815,18 +865,18 @@ expected check_pi_type( } if (arg_name) { - auto var = ctx.try_emplace(*arg_name, arg_loc, arg.qty, *type); + auto var = ctx.try_emplace(*arg_name, arg_loc, arg.qty, arg.is_mutable, *type); if (not var) return std::move(var.error()); - if (auto ok = unscoped_ctx.try_emplace(*arg_name, arg_loc, arg.qty, *type); not ok) + if (auto ok = unscoped_ctx.try_emplace(*arg_name, arg_loc, arg.qty, arg.is_mutable, *type); not ok) return std::move(ok.error()); // this is not actually possible, but whatever - return make_legal_func_arg(arg.qty, std::move(*type), std::move(*var)); + return make_legal_func_arg(arg.qty, arg.is_mutable, std::move(*type), std::move(*var)); } else { ctx.add_unnamed(arg.qty, *type); unscoped_ctx.add_unnamed(arg.qty, *type); - return make_legal_func_arg(arg.qty, std::move(*type), std::nullopt); + return make_legal_func_arg(arg.qty, arg.is_mutable, std::move(*type), std::nullopt); } }); if (not args) @@ -866,15 +916,15 @@ expected check_sigma_type( } if (arg_name) { - auto var = ctx.try_emplace(*arg_name, arg_loc, arg.qty, *type); + auto var = ctx.try_emplace(*arg_name, arg_loc, arg.qty, arg.is_mutable, *type); if (not var) return std::move(var.error()); - return make_legal_func_arg(arg.qty, std::move(*type), std::move(*var)); + return make_legal_func_arg(arg.qty, arg.is_mutable, std::move(*type), std::move(*var)); } else { ctx.add_unnamed(arg.qty, *type); - return make_legal_func_arg(arg.qty, std::move(*type), std::nullopt); + return make_legal_func_arg(arg.qty, arg.is_mutable, std::move(*type), std::nullopt); } }); if (not args) diff --git a/dep0/lib/03_typecheck/src/context.cpp b/dep0/lib/03_typecheck/src/context.cpp index d5d4dc3b..732a5b63 100644 --- a/dep0/lib/03_typecheck/src/context.cpp +++ b/dep0/lib/03_typecheck/src/context.cpp @@ -98,12 +98,18 @@ void ctx_t::add_unnamed(ast::qty_t const qty, expr_t type) static std::size_t unnamed_idx = 0; static const source_text empty = source_text::from_literal("auto"); auto const var = expr_t::var_t{empty, 0ul, unnamed_idx++}; - bool const inserted = m_values.try_emplace(var, std::nullopt, scope(), var, qty, std::move(type)).second; + auto constexpr immutable = ast::is_mutable_t::no; + bool const inserted = m_values.try_emplace(var, std::nullopt, scope(), var, qty, immutable, std::move(type)).second; assert(inserted and "failed to add unnamed variable to context"); } dep0::expected -ctx_t::try_emplace(source_text name, std::optional const loc, ast::qty_t const qty, expr_t type) +ctx_t::try_emplace( + source_text name, + std::optional const loc, + ast::qty_t const qty, + ast::is_mutable_t const is_mutable, + expr_t type) { auto const prev_var = m_index[name]; auto const shadow_id = prev_var ? prev_var->shadow_id + 1ul : 0ul; @@ -111,7 +117,7 @@ ctx_t::try_emplace(source_text name, std::optional const loc, ast: auto const [it, inserted] = m_index.try_emplace(name, var); if (inserted) { - bool const inserted = m_values.try_emplace(var, loc, scope(), var, qty, std::move(type)).second; + bool const inserted = m_values.try_emplace(var, loc, scope(), var, qty, is_mutable, std::move(type)).second; assert(inserted and "failed to add named variable to context"); return var; } @@ -144,19 +150,18 @@ std::ostream& for_each_line(std::ostream& os, R&& r, F&& f) std::ostream& pretty_print(std::ostream& os, ctx_t const& ctx) { - auto const length_of = [] (expr_t::var_t const& var) + auto const length_of = [] (ctx_t::decl_t const& x) { - // length of the name, plus: - // 1. length of its quantity ("0 ", "1 ", " ") - // 2. and possibly the length of the index with the colon separator - auto const log10 = [] (std::size_t const x) { return static_cast(std::log10(x)); }; - return var.name.size() + 2 + (var.idx == 0ul ? 2ul : 2ul + log10(var.idx)) - + (var.shadow_id == 0ul ? 2ul : 2ul + log10(var.shadow_id)); + auto const& var = x.var; + std::ostringstream tmp; + pretty_print(tmp, x.var); + auto length = tmp.str().size() + 2ul; // including one of ("0 ", "1 ", " ") + if (x.is_mutable == ast::is_mutable_t::yes) + length += std::string_view(" mutable").size(); + return length; }; auto const& decls = ctx.decls(); - auto const longest = - decls.empty() ? 0ul : - std::ranges::max(decls | std::views::transform([&] (ctx_t::decl_t const& x) { return length_of(x.var); })); + auto const longest = decls.empty() ? 0ul : std::ranges::max(decls | std::views::transform(length_of)); auto const indent = (longest / 4ul) + 1ul; auto const alignment = indent * 4ul; for_each_line( @@ -166,7 +171,9 @@ std::ostream& pretty_print(std::ostream& os, ctx_t const& ctx) { os << (decl.qty == ast::qty_t::zero ? "0 " : decl.qty == ast::qty_t::one ? "1 " : " "); pretty_print(os, decl.var); - padding.resize(alignment - length_of(decl.var), ' '); + if (decl.is_mutable == ast::is_mutable_t::yes) + os << " mutable"; + padding.resize(alignment - length_of(decl), ' '); os << padding << ": "; auto copy = decl.type; bool const changed = beta_delta_normalize(copy); diff --git a/dep0/lib/03_typecheck/src/delta_unfold.cpp b/dep0/lib/03_typecheck/src/delta_unfold.cpp index 9529df92..ec9488f9 100644 --- a/dep0/lib/03_typecheck/src/delta_unfold.cpp +++ b/dep0/lib/03_typecheck/src/delta_unfold.cpp @@ -35,6 +35,7 @@ namespace dep0::typecheck { namespace impl { static bool delta_unfold(stmt_t&); +static bool delta_unfold(stmt_t::assign_t&); static bool delta_unfold(stmt_t::if_else_t&); static bool delta_unfold(stmt_t::return_t&); static bool delta_unfold(stmt_t::impossible_t&); @@ -122,6 +123,12 @@ bool delta_unfold(stmt_t& stmt) [&] (auto& x) { return delta_unfold(x); }); } +bool delta_unfold(stmt_t::assign_t& assign) +{ + // It is most likely that we can unfold something in rhs, so try that first. + return delta_unfold(assign.rhs) or delta_unfold(assign.lhs); +} + bool delta_unfold(stmt_t::if_else_t& if_) { return delta_unfold(if_.cond) diff --git a/dep0/lib/03_typecheck/src/derivation_rules.cpp b/dep0/lib/03_typecheck/src/derivation_rules.cpp index 4d3a5aac..daae054e 100644 --- a/dep0/lib/03_typecheck/src/derivation_rules.cpp +++ b/dep0/lib/03_typecheck/src/derivation_rules.cpp @@ -21,7 +21,7 @@ expr_t derivation_rules::make_true_t(env_t const& env, ctx_t const& ctx) kind_t{}, // TODO need to add a test to make sure this is correct expr_t::pi_t{ ast::is_mutable_t::no, - std::vector{make_legal_func_arg(ast::qty_t::zero, make_bool(env, ctx))}, + std::vector{make_legal_func_arg(ast::qty_t::zero, ast::is_mutable_t::no, make_bool(env, ctx))}, make_typename(env, ctx)}), expr_t::true_t{}); } @@ -53,8 +53,8 @@ expr_t derivation_rules::make_ref_t(env_t const& env, ctx_t const& ctx) expr_t::pi_t{ ast::is_mutable_t::no, std::vector{ - make_legal_func_arg(ast::qty_t::zero, make_typename(env, ctx)), - make_legal_func_arg(ast::qty_t::zero, make_scope_t(env, ctx)) + make_legal_func_arg(ast::qty_t::zero, ast::is_mutable_t::no, make_typename(env, ctx)), + make_legal_func_arg(ast::qty_t::zero, ast::is_mutable_t::no, make_scope_t(env, ctx)) }, make_typename(env, ctx)}), expr_t::ref_t{}); @@ -161,8 +161,8 @@ expr_t derivation_rules::make_array(env_t const& env, ctx_t const& ctx) expr_t::pi_t{ ast::is_mutable_t::no, std::vector{ - make_legal_func_arg(ast::qty_t::zero, make_typename(env, ctx)), - make_legal_func_arg(ast::qty_t::zero, make_u64(env, ctx)) + make_legal_func_arg(ast::qty_t::zero, ast::is_mutable_t::no, make_typename(env, ctx)), + make_legal_func_arg(ast::qty_t::zero, ast::is_mutable_t::no, make_u64(env, ctx)) }, make_typename(env, ctx)}), expr_t::array_t{}); diff --git a/dep0/lib/03_typecheck/src/is_impossible.cpp b/dep0/lib/03_typecheck/src/is_impossible.cpp index 7060b719..41bcd6e6 100644 --- a/dep0/lib/03_typecheck/src/is_impossible.cpp +++ b/dep0/lib/03_typecheck/src/is_impossible.cpp @@ -66,6 +66,7 @@ bool is_impossible(stmt_t const& s) return match( s.value, [] (expr_t::app_t const& x) { return impl::is_impossible(x); }, + [] (stmt_t::assign_t const& x) { return impl::is_impossible(x.lhs) or impl::is_impossible(x.rhs); }, [] (stmt_t::if_else_t const& x) { // an if-statement is impossible if the condition is impossible or diff --git a/dep0/lib/03_typecheck/src/is_terminator.cpp b/dep0/lib/03_typecheck/src/is_terminator.cpp index 7541665b..014451af 100644 --- a/dep0/lib/03_typecheck/src/is_terminator.cpp +++ b/dep0/lib/03_typecheck/src/is_terminator.cpp @@ -17,6 +17,7 @@ bool is_terminator(stmt_t const& s) return match( s.value, [] (expr_t::app_t const&) { return false; }, + [] (stmt_t::assign_t const&) { return false; }, [] (stmt_t::if_else_t const& x) { return returns_from_all_branches(x); }, [] (stmt_t::return_t const&) { return true; }, [] (stmt_t::impossible_t const&) diff --git a/dep0/lib/03_typecheck/src/max_scope.cpp b/dep0/lib/03_typecheck/src/max_scope.cpp index 1b2d4461..3933ceb5 100644 --- a/dep0/lib/03_typecheck/src/max_scope.cpp +++ b/dep0/lib/03_typecheck/src/max_scope.cpp @@ -87,6 +87,10 @@ expected max_scope_stmt(ctx_t const& ctx, stmt_t const& stmt) { return max_scope_app(ctx, app); }, + [&] (stmt_t::assign_t const& assign) -> expected + { + return max_scope_combine(max_scope_expr(ctx, assign.lhs), max_scope_expr(ctx, assign.rhs)); + }, [&] (stmt_t::if_else_t const& if_stmt) -> expected { auto cond_result = max_scope_expr(ctx, if_stmt.cond); diff --git a/dep0/lib/03_typecheck/src/private/root_var.hpp b/dep0/lib/03_typecheck/src/private/root_var.hpp new file mode 100644 index 00000000..4b780c19 --- /dev/null +++ b/dep0/lib/03_typecheck/src/private/root_var.hpp @@ -0,0 +1,33 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +/** + * @file + * @brief Single-function header declaring all overloads of `dep0::typecheck::root_var()`. + */ +#pragma once + +#include "dep0/typecheck/ast.hpp" + +namespace dep0::typecheck { + +/** + * @brief If the given member-access is rooted in a variable, return the address of such variable; nullptr otherwise. + * + * The root of a member-access is the top level variable, for example `x.y.z` is rooted in `x`, + * whereas `f().y.z` is not rooted in a variable. + */ +expr_t::var_t const* root_var(expr_t::member_t const&); + +/** + * @brief If the given subscript-access is rooted in a variable, return the address of such variable; nullptr otherwise. + * + * The root of a member-access is the top level variable, for example `x[y]` is rooted in `x`, + * whereas `f()[y]` is not rooted in a variable. + */ +expr_t::var_t const* root_var(expr_t::subscript_t const&); + +} // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/src/rewrite.cpp b/dep0/lib/03_typecheck/src/rewrite.cpp index 6b8ea2ae..23c4570f 100644 --- a/dep0/lib/03_typecheck/src/rewrite.cpp +++ b/dep0/lib/03_typecheck/src/rewrite.cpp @@ -83,6 +83,17 @@ std::optional rewrite(expr_t const& from, expr_t const& to, stmt_t const if (auto new_app = rewrite(from, to, app)) result.emplace(old.properties, std::move(*new_app)); }, + [&] (stmt_t::assign_t const& assign) + { + auto new_lhs = rewrite(from, to, assign.lhs); + auto new_rhs = rewrite(from, to, assign.rhs); + if (new_lhs or new_rhs) + result.emplace( + old.properties, + stmt_t::assign_t{ + impl::choose(std::move(new_lhs), assign.lhs), + impl::choose(std::move(new_rhs), assign.rhs)}); + }, [&] (stmt_t::if_else_t const& if_else) { auto new_cond = rewrite(from, to, if_else.cond); @@ -163,7 +174,12 @@ void rewrite( ast::occurs_in(*old_arg.var, from, ast::occurrence_style::free) or ast::occurs_in(*old_arg.var, to, ast::occurrence_style::free); if (new_type) - new_arg.emplace(old_arg.properties, old_arg.qty, std::move(*new_type), old_arg.var); + new_arg.emplace( + old_arg.properties, + old_arg.qty, + old_arg.is_mutable, + std::move(*new_type), + old_arg.var); } return new_arg; }); diff --git a/dep0/lib/03_typecheck/src/root_var.cpp b/dep0/lib/03_typecheck/src/root_var.cpp new file mode 100644 index 00000000..6a633c12 --- /dev/null +++ b/dep0/lib/03_typecheck/src/root_var.cpp @@ -0,0 +1,69 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#include "private/root_var.hpp" + +#include "dep0/match.hpp" + +namespace dep0::typecheck { + +namespace { + +expr_t::var_t const* root_var(expr_t const& x) +{ + return match( + x.value, + [] (expr_t::typename_t) { return nullptr; }, + [] (expr_t::true_t) { return nullptr; }, + [] (expr_t::auto_t) { return nullptr; }, + [] (expr_t::bool_t) { return nullptr; }, + [] (expr_t::cstr_t) { return nullptr; }, + [] (expr_t::unit_t) { return nullptr; }, + [] (expr_t::i8_t) { return nullptr; }, + [] (expr_t::i16_t) { return nullptr; }, + [] (expr_t::i32_t) { return nullptr; }, + [] (expr_t::i64_t) { return nullptr; }, + [] (expr_t::u8_t) { return nullptr; }, + [] (expr_t::u16_t) { return nullptr; }, + [] (expr_t::u32_t) { return nullptr; }, + [] (expr_t::u64_t) { return nullptr; }, + [] (expr_t::boolean_constant_t const&) { return nullptr; }, + [] (expr_t::numeric_constant_t const&) { return nullptr; }, + [] (expr_t::string_literal_t const&) { return nullptr; }, + [] (expr_t::boolean_expr_t const&) { return nullptr; }, + [] (expr_t::relation_expr_t const&) { return nullptr; }, + [] (expr_t::arith_expr_t const&) { return nullptr; }, + [] (expr_t::var_t const& x) { return &x; }, + [] (expr_t::global_t const&) { return nullptr; }, + [] (expr_t::app_t const&) { return nullptr; }, + [] (expr_t::abs_t const&) { return nullptr; }, + [] (expr_t::pi_t const&) { return nullptr; }, + [] (expr_t::sigma_t const&) { return nullptr; }, + [] (expr_t::ref_t) { return nullptr; }, + [] (expr_t::scope_t) { return nullptr; }, + [] (expr_t::addressof_t const&) { return nullptr; }, + [] (expr_t::deref_t const&) { return nullptr; }, + [] (expr_t::scopeof_t const&) { return nullptr; }, + [] (expr_t::array_t const&) { return nullptr; }, + [] (expr_t::init_list_t const&) { return nullptr; }, + [] (expr_t::member_t const& x) { return root_var(x.object.get()); }, + [] (expr_t::subscript_t const& x) { return root_var(x.object.get()); }, + [] (expr_t::because_t const& x) { return root_var(x.value.get()); }); +} + +} // namespace + +expr_t::var_t const* root_var(expr_t::member_t const& x) +{ + return root_var(x.object.get()); +} + +expr_t::var_t const* root_var(expr_t::subscript_t const& x) +{ + return root_var(x.object.get()); +} + +} // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/src/substitute.cpp b/dep0/lib/03_typecheck/src/substitute.cpp index a64ca178..f3ddb301 100644 --- a/dep0/lib/03_typecheck/src/substitute.cpp +++ b/dep0/lib/03_typecheck/src/substitute.cpp @@ -30,6 +30,11 @@ void substitute(expr_t::var_t const& var, expr_t const& expr, body_t& body) { substitute(var, expr, app); }, + [&] (stmt_t::assign_t& assign) + { + substitute(var, expr, assign.lhs); + substitute(var, expr, assign.rhs); + }, [&] (stmt_t::if_else_t& if_) { substitute(var, expr, if_.cond); diff --git a/dep0/lib/03_typecheck/src/type_assign.cpp b/dep0/lib/03_typecheck/src/type_assign.cpp index bfd45879..873cbaa5 100644 --- a/dep0/lib/03_typecheck/src/type_assign.cpp +++ b/dep0/lib/03_typecheck/src/type_assign.cpp @@ -375,6 +375,7 @@ type_assign( [&] (parser::expr_t::scope_t) -> expected { return derivation_rules::make_scope_t(env, ctx); }, [&] (parser::expr_t::addressof_t const& x) -> expected { + // TODO can only take reference of immutable variables, needs a test auto expr = type_assign(env, ctx, x.expr.get(), is_mutable_allowed, usage, usage_multiplier); if (not expr) return expr; diff --git a/dep0/lib/03_typecheck/test/CMakeLists.txt b/dep0/lib/03_typecheck/test/CMakeLists.txt index 92d07b8d..4fc53a8a 100644 --- a/dep0/lib/03_typecheck/test/CMakeLists.txt +++ b/dep0/lib/03_typecheck/test/CMakeLists.txt @@ -49,3 +49,4 @@ add_dep0_typecheck_test(0020_builtins) add_dep0_typecheck_test(0021_tuples) add_dep0_typecheck_test(0022_structs) add_dep0_typecheck_test(0023_references) +add_dep0_typecheck_test(0024_mutability) diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp new file mode 100644 index 00000000..c7c5ef85 --- /dev/null +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -0,0 +1,91 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#define BOOST_TEST_MODULE dep0_typecheck_tests_0024_mutability +#include + +#include "typecheck_tests_fixture.hpp" + +using namespace dep0::testing; + +BOOST_FIXTURE_TEST_SUITE(dep0_typecheck_tests_0024_mutability, TypecheckTestsFixture) + +BOOST_AUTO_TEST_CASE(pass_000) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_000.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 4ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("y", is_i32))); + { + auto const f = std::get_if(&pass_result->entries[1]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f0"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], var("x"), plus(var("x"), constant(1)))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } + { + auto const f = std::get_if(&pass_result->entries[2]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f1"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], array_of(is_i32, constant(3)), "xs", yes)); + BOOST_TEST(is_array_of(f->value.ret_type.get(), is_i32, constant(3))); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST( + is_assign( + f->value.body.stmts[0ul], + subscript_of(var("xs"), constant(0)), + subscript_of(var("xs"), constant(1)))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("xs"))); + } + { + auto const f = std::get_if(&pass_result->entries[3]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f2"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], global("t"), "x", yes)); + BOOST_TEST(is_global(f->value.ret_type.get(), "t")); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "y"), constant(0))); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } +} + +BOOST_AUTO_TEST_CASE(pass_001) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_001.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 1ul); + auto constexpr yes = dep0::ast::is_mutable_t::yes; + { + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f"); + BOOST_TEST_REQUIRE(f->value.args.size() == 3ul); + BOOST_TEST(is_arg(f->value.args[0], is_u64, "n", yes)); + BOOST_TEST(is_arg(f->value.args[1], true_t_of(lt(constant(0), var("n"))), std::nullopt, dep0::ast::qty_t::zero)); + BOOST_TEST(is_arg(f->value.args[2], array_of(is_i32, var("n")), "xs")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST(is_return_of(f->value.body.stmts[0ul], subscript_of(var("xs"), constant(0)))); + } +} + +BOOST_AUTO_TEST_CASE(pass_002) { BOOST_TEST(pass("0024_mutability/pass_002.depc")); } + +BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(fail("0024_mutability/typecheck_error_001.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_002) { BOOST_TEST(fail("0024_mutability/typecheck_error_002.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_003) { BOOST_TEST(fail("0024_mutability/typecheck_error_003.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_004) { BOOST_TEST(fail("0024_mutability/typecheck_error_004.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_005) { BOOST_TEST(fail("0024_mutability/typecheck_error_005.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(fail("0024_mutability/typecheck_error_006.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(fail("0024_mutability/typecheck_error_007.depc")); } + +BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/05_llvmgen/src/gen_body.cpp b/dep0/lib/05_llvmgen/src/gen_body.cpp index adbb0a27..cf14caa8 100644 --- a/dep0/lib/05_llvmgen/src/gen_body.cpp +++ b/dep0/lib/05_llvmgen/src/gen_body.cpp @@ -345,6 +345,10 @@ void gen_stmt( gen_func_call(global, local_stmt, builder, x, value_category_t::temporary, nullptr); gen_destructors(global, local_stmt, builder); }, + [] (typecheck::stmt_t::assign_t const&) + { + assert(false and "assignment not implemented yet"); + }, [&] (typecheck::stmt_t::if_else_t const& x) { // Let's eliminate impossible branches. diff --git a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/func_arg.hpp b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/func_arg.hpp index 94f34979..74c805a8 100644 --- a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/func_arg.hpp +++ b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/func_arg.hpp @@ -24,7 +24,8 @@ boost::test_tools::predicate_result is_arg( ast::func_arg_t

const& arg, F&& type_predicate, std::optional const name, - ast::qty_t const qty = ast::qty_t::many) + ast::qty_t const qty = ast::qty_t::many, + ast::is_mutable_t const is_mutable = ast::is_mutable_t::no) { if (arg.qty != qty) { @@ -40,6 +41,11 @@ boost::test_tools::predicate_result is_arg( }; return failure("argument quantity ", to_string(arg.qty), " != ", to_string(qty)); } + if (arg.is_mutable != is_mutable) + { + auto constexpr yes = ast::is_mutable_t::yes; + return failure("argument should be ", is_mutable == yes ? "mutable" : "immutable" , " but it is not"); + } if (auto const result = std::forward(type_predicate)(arg.type); not result) return failure("argument type predicate failed: ", result.message()); if (name) @@ -54,6 +60,16 @@ boost::test_tools::predicate_result is_arg( return true; } +template > F> +boost::test_tools::predicate_result is_arg( + ast::func_arg_t

const& arg, + F&& type_predicate, + std::optional const name, + ast::is_mutable_t const is_mutable) +{ + return is_arg(arg, std::forward(type_predicate), name, ast::qty_t::many, is_mutable); +} + inline auto typename_( std::optional name = std::nullopt, ast::qty_t const qty = ast::qty_t::many) @@ -65,11 +81,15 @@ inline auto typename_( } template > F> -inline auto arg_of(F&& f, std::optional name = std::nullopt, ast::qty_t const qty = ast::qty_t::many) +inline auto arg_of( + F&& f, + std::optional name = std::nullopt, + ast::qty_t const qty = ast::qty_t::many, + ast::is_mutable_t const is_mutable = ast::is_mutable_t::no) { - return [f=std::forward(f), name=std::move(name), qty] (ast::func_arg_t

const& x) + return [f=std::forward(f), name=std::move(name), qty, is_mutable] (ast::func_arg_t

const& x) { - return is_arg

(x, f, name, qty); + return is_arg

(x, f, name, qty, is_mutable); }; } diff --git a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp index 730462cc..23590dea 100644 --- a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp +++ b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp @@ -48,6 +48,30 @@ boost::test_tools::predicate_result is_func_call_of(ast::stmt_t

const& stmt, return true; } +template > F_lhs, Predicate> F_rhs> +boost::test_tools::predicate_result is_assign(ast::stmt_t

const& stmt, F_lhs&& f_lhs, F_rhs&& f_rhs) +{ + auto const assign = std::get_if::assign_t>(&stmt.value); + if (not assign) + return failure("statement is not an assignment but ", pretty_name(stmt.value)); + if (auto const result = std::forward(f_lhs)(assign->lhs); not result) + return failure("inside lhs: ", result.message()); + if (auto const result = std::forward(f_rhs)(assign->rhs); not result) + return failure("inside rhs: ", result.message()); + return true; +} + +template > F_lhs, Predicate> F_rhs> +constexpr auto assign_of(F_lhs&& f_lhs, F_rhs&& f_rhs) +{ + return + [f_lhs=std::forward(f_lhs), f_rhs=std::forward(f_rhs)] + (ast::stmt_t

const& stmt) + { + return is_assign(stmt, f_lhs, f_rhs); + }; +} + template < ast::Properties P, Predicate> F_cond, diff --git a/testfiles/0024_mutability/pass_000.depc b/testfiles/0024_mutability/pass_000.depc new file mode 100644 index 00000000..c3548b83 --- /dev/null +++ b/testfiles/0024_mutability/pass_000.depc @@ -0,0 +1,28 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + i32_t y; +}; + +func f0(i32_t mutable x) -> i32_t +{ + x = x+1; + return x; +} + +func f1(array_t(i32_t, 3) mutable xs) -> array_t(i32_t, 3) +{ + xs[0] = xs[1]; + return xs; +} + +func f2(t mutable x) -> t +{ + x.y = 0; + return x; +} diff --git a/testfiles/0024_mutability/pass_001.depc b/testfiles/0024_mutability/pass_001.depc new file mode 100644 index 00000000..6eea81a6 --- /dev/null +++ b/testfiles/0024_mutability/pass_001.depc @@ -0,0 +1,10 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable n, 0 true_t(0 < n), array_t(i32_t, n) xs) -> i32_t +{ + return xs[0]; +} diff --git a/testfiles/0024_mutability/pass_002.depc b/testfiles/0024_mutability/pass_002.depc new file mode 100644 index 00000000..fc120e89 --- /dev/null +++ b/testfiles/0024_mutability/pass_002.depc @@ -0,0 +1,11 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable n, 0 true_t(0 < n), array_t(i32_t, n) xs) -> i32_t +{ + n = 0; + return xs[0]; // still ok because the original proof is still valid +} diff --git a/testfiles/0024_mutability/typecheck_error_000.depc b/testfiles/0024_mutability/typecheck_error_000.depc new file mode 100644 index 00000000..22afe8b6 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_000.depc @@ -0,0 +1,11 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(i32_t x) -> i32_t +{ + x = x+1; // typecheck error: cannot mutate immutable variable + return x; +} diff --git a/testfiles/0024_mutability/typecheck_error_001.depc b/testfiles/0024_mutability/typecheck_error_001.depc new file mode 100644 index 00000000..bba1dfdf --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_001.depc @@ -0,0 +1,11 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(array_t(i32_t, 3) xs) -> array_t(i32_t, 3) +{ + xs[0] = xs[1]; // typecheck error: cannot mutate immutable variable + return xs; +} diff --git a/testfiles/0024_mutability/typecheck_error_002.depc b/testfiles/0024_mutability/typecheck_error_002.depc new file mode 100644 index 00000000..32397983 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_002.depc @@ -0,0 +1,16 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + i32_t y; +}; + +func f(t x) -> t +{ + x.y = 0; // typecheck error: cannot mutate immutable variable + return x; +} diff --git a/testfiles/0024_mutability/typecheck_error_003.depc b/testfiles/0024_mutability/typecheck_error_003.depc new file mode 100644 index 00000000..392d2024 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_003.depc @@ -0,0 +1,12 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func make_array() -> array_t(i32_t, 3) { return {1,2,3}; } +func f(array_t(i32_t, 3) xs) -> array_t(i32_t, 3) +{ + make_array()[0] = xs[1]; // typecheck error: assignment target not rooted in a mutable variable + return xs; +} diff --git a/testfiles/0024_mutability/typecheck_error_004.depc b/testfiles/0024_mutability/typecheck_error_004.depc new file mode 100644 index 00000000..04ab6a98 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_004.depc @@ -0,0 +1,18 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + i32_t y; +}; + +func make_t() -> t { return {1}; } + +func f(t x) -> t +{ + make_t().y = 0; // typecheck error: assignment target not rooted in a mutable variable + return x; +} diff --git a/testfiles/0024_mutability/typecheck_error_005.depc b/testfiles/0024_mutability/typecheck_error_005.depc new file mode 100644 index 00000000..0c5285fc --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_005.depc @@ -0,0 +1,14 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable n, array_t(i32_t, n) xs) -> i32_t +{ + n = 1; + if (0 < n) + return xs[0]; // typecheck error: cannot verify that access within bounds, because n has been modified now + else + return 0; +} diff --git a/testfiles/0024_mutability/typecheck_error_006.depc b/testfiles/0024_mutability/typecheck_error_006.depc new file mode 100644 index 00000000..86b452e9 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_006.depc @@ -0,0 +1,19 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + u64_t n; +}; + +func f(t mutable x, array_t(i32_t, x.n) xs) -> i32_t +{ + x.n = 1; + if (0 < x.n) + return xs[0]; // typecheck error: cannot verify that access within bounds, because x.n has been modified now + else + return 0; +} diff --git a/testfiles/0024_mutability/typecheck_error_007.depc b/testfiles/0024_mutability/typecheck_error_007.depc new file mode 100644 index 00000000..c5b10754 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_007.depc @@ -0,0 +1,14 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f((u64_t, i64_t) mutable x, array_t(i32_t, x[0]) xs) -> i32_t +{ + x[0] = 1; + if (0 < x[0]) + return xs[0]; // typecheck error: cannot verify that access within bounds, because x[0] has been modified now + else + return 0; +} From 56ce330bc8ece61f938762434c78d5487ac0ecd2 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sun, 23 Nov 2025 23:47:36 +0000 Subject: [PATCH 02/11] check member/subscript access do not carry dependencies --- .../lib/01_ast/include/dep0/ast/occurs_in.hpp | 8 +++ .../include/dep0/ast/occurs_in_impl.hpp | 20 ++++++- .../dep0_parser_tests_0024_mutability.cpp | 25 ++++++-- dep0/lib/03_typecheck/src/check.cpp | 60 ++++++++++++++++--- .../dep0_typecheck_tests_0024_mutability.cpp | 25 ++++++-- .../dep0/testing/ast_predicates/sigma.hpp | 1 + testfiles/0024_mutability/pass_000.depc | 13 +++- .../0024_mutability/typecheck_error_008.depc | 11 ++++ .../0024_mutability/typecheck_error_009.depc | 17 ++++++ 9 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 testfiles/0024_mutability/typecheck_error_008.depc create mode 100644 testfiles/0024_mutability/typecheck_error_009.depc diff --git a/dep0/lib/01_ast/include/dep0/ast/occurs_in.hpp b/dep0/lib/01_ast/include/dep0/ast/occurs_in.hpp index 2c8b631a..abe8174c 100644 --- a/dep0/lib/01_ast/include/dep0/ast/occurs_in.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/occurs_in.hpp @@ -53,6 +53,14 @@ bool occurs_in( body_t

const* body, occurrence_style); +/** @brief Returns true if the given variable name appears (free or anywhere) in any of the given struct fields. */ +template +bool occurs_in( + typename expr_t

::var_t const&, + typename std::vector::struct_t::field_t>::const_iterator begin, + typename std::vector::struct_t::field_t>::const_iterator end, + occurrence_style); + } // namespace dep0::ast #include "dep0/ast/occurs_in_impl.hpp" diff --git a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp index fa7af1b8..e68ef911 100644 --- a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp @@ -218,5 +218,23 @@ bool occurs_in( return occurs_in(var, ret_type, style) or (body and impl::occurs_in(var, *body, style)); } -} // namespace dep0::ast +template +bool occurs_in( + typename expr_t

::var_t const& var, + typename std::vector::struct_t::field_t>::const_iterator const begin, + typename std::vector::struct_t::field_t>::const_iterator const end, + occurrence_style const style) +{ + for (auto const& arg: std::ranges::subrange(begin, end)) + { + if (occurs_in(var, arg.type, style)) + return true; + if (arg.var == var) + // If we are looking for occurrences anywhere, return true because this is a valid one. + // If we are looking for free occurrences, return false because any later occurrence is now bound. + return style == occurrence_style::anywhere; + } + return false; +} +} // namespace dep0::ast diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 0205914c..8348566b 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -16,9 +16,9 @@ BOOST_FIXTURE_TEST_SUITE(dep0_parser_tests_0024_mutability, ParserTestsFixture) BOOST_AUTO_TEST_CASE(pass_000) { BOOST_TEST_REQUIRE(pass("0024_mutability/pass_000.depc")); - BOOST_TEST_REQUIRE(pass_result->entries.size() == 4ul); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 5ul); auto constexpr yes = dep0::ast::is_mutable_t::yes; - BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("y", is_i32))); + BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("a", is_i32), struct_field("b", is_i64))); { auto const f = std::get_if(&pass_result->entries[1]); BOOST_TEST_REQUIRE(f); @@ -52,9 +52,22 @@ BOOST_AUTO_TEST_CASE(pass_000) BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); BOOST_TEST(is_arg(f->value.args[0], var("t"), "x", yes)); BOOST_TEST(is_var(f->value.ret_type.get(), "t")); - BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); - BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "y"), constant(0))); - BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 3ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "a"), constant(0))); + BOOST_TEST(is_assign(f->value.body.stmts[1ul], member_of(var("x"), "b"), constant(1))); + BOOST_TEST(is_return_of(f->value.body.stmts[2ul], var("x"))); + } + { + auto const f = std::get_if(&pass_result->entries[4]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f3"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], sigma_of(std::tuple{arg_of(is_i32), arg_of(is_i64)}), "x", yes)); + BOOST_TEST(is_i64(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 3ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], subscript_of(var("x"), constant(0)), constant(0))); + BOOST_TEST(is_assign(f->value.body.stmts[1ul], subscript_of(var("x"), constant(1)), constant(1))); + BOOST_TEST(is_return_of(f->value.body.stmts[2ul], constant(0))); } } @@ -142,5 +155,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_005) BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(pass("0024_mutability/typecheck_error_006.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(pass("0024_mutability/typecheck_error_007.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(pass("0024_mutability/typecheck_error_008.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(pass("0024_mutability/typecheck_error_009.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index b7d220f0..8b14dbf3 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -20,8 +20,11 @@ #include "dep0/typecheck/beta_delta_reduction.hpp" #include "dep0/typecheck/list_initialization.hpp" +#include "dep0/typecheck/subscript_access.hpp" +#include "dep0/ast/find_member_field.hpp" #include "dep0/ast/mutable_place_expression.hpp" +#include "dep0/ast/occurs_in.hpp" #include "dep0/ast/pretty_print.hpp" #include "dep0/ast/views.hpp" @@ -332,23 +335,66 @@ check_stmt( { return error_t("target of assignment is not a mutable place expression", loc); }, - [&] (std::reference_wrapper const var) -> expected + [&] (expr_t::var_t const& var) -> expected { - return var.get(); + return var; }, - [&] (std::reference_wrapper const member) -> expected + [&] (expr_t::member_t const& member) -> expected { auto const root = root_var(member); if (not root) return error_t("target of assignment is not rooted in a variable", loc); - return *root; + + using enum ast::occurrence_style; + if (auto const ty = std::get_if(&member.object.get().properties.sort.get())) + if (auto const g = std::get_if(&ty->value)) + if (auto const type_def = std::get_if(env[*g])) + if (auto const s = std::get_if(&type_def->value)) + if (auto const it = ast::find_member_field(member.field, *s); it != s->fields.end()) + if (not ast::occurs_in(it->var, std::next(it), s->fields.end(), free)) + return *root; + else + return error_t("cannot mutate a member field that carries dependency to other fields", loc); + + return error_t("cannot verify that member field does not carry dependency to other fields", loc); }, - [&] (std::reference_wrapper const subscript) -> expected + [&] (expr_t::subscript_t const& x) -> expected { - auto const root = root_var(subscript); + auto const root = root_var(x); if (not root) return error_t("target of assignment is not rooted in a variable", loc); - return *root; + + if (auto const ty = std::get_if(&x.object.get().properties.sort.get())) + return match( + has_subscript_access(*ty), + [&] (has_subscript_access_result::no_t) + { + // this is really not possible but whatever + return error_t("cannot verify that subscript access does not carry dependency", loc); + }, + [&] (has_subscript_access_result::sigma_t const sigma) -> expected + { + using enum ast::occurrence_style; + if (auto const i = std::get_if(&x.index.get().value)) + if (auto const v = i->value.template convert_to(); v < sigma.args.size()) + if (not sigma.args[v].var or + not ast::occurs_in( + *sigma.args[v].var, + std::next(sigma.args.begin(), v+1), + sigma.args.end(), + free)) + return *root; + else + return error_t( + "cannot mutate a member field that carries dependency to other fields", + loc); + return error_t("cannot verify that subscript access does not carry dependency", loc); + }, + [&] (has_subscript_access_result::array_t) + { + return *root; + }); + return error_t("cannot verify that subscript access does not carry dependency", loc); }); if (not root) return root.error(); diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index c7c5ef85..8fff278f 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -16,9 +16,9 @@ BOOST_FIXTURE_TEST_SUITE(dep0_typecheck_tests_0024_mutability, TypecheckTestsFix BOOST_AUTO_TEST_CASE(pass_000) { BOOST_TEST_REQUIRE(pass("0024_mutability/pass_000.depc")); - BOOST_TEST_REQUIRE(pass_result->entries.size() == 4ul); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 5ul); auto constexpr yes = dep0::ast::is_mutable_t::yes; - BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("y", is_i32))); + BOOST_TEST(is_struct_def(pass_result->entries[0], "t", struct_field("a", is_i32), struct_field("b", is_i64))); { auto const f = std::get_if(&pass_result->entries[1]); BOOST_TEST_REQUIRE(f); @@ -52,9 +52,22 @@ BOOST_AUTO_TEST_CASE(pass_000) BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); BOOST_TEST(is_arg(f->value.args[0], global("t"), "x", yes)); BOOST_TEST(is_global(f->value.ret_type.get(), "t")); - BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); - BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "y"), constant(0))); - BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 3ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], member_of(var("x"), "a"), constant(0))); + BOOST_TEST(is_assign(f->value.body.stmts[1ul], member_of(var("x"), "b"), constant(1))); + BOOST_TEST(is_return_of(f->value.body.stmts[2ul], var("x"))); + } + { + auto const f = std::get_if(&pass_result->entries[4]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f3"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], sigma_of(std::tuple{arg_of(is_i32), arg_of(is_i64)}), "x", yes)); + BOOST_TEST(is_i64(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 3ul); + BOOST_TEST(is_assign(f->value.body.stmts[0ul], subscript_of(var("x"), constant(0)), constant(0))); + BOOST_TEST(is_assign(f->value.body.stmts[1ul], subscript_of(var("x"), constant(1)), constant(1))); + BOOST_TEST(is_return_of(f->value.body.stmts[2ul], constant(0))); } } @@ -87,5 +100,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_004) { BOOST_TEST(fail("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_005) { BOOST_TEST(fail("0024_mutability/typecheck_error_005.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(fail("0024_mutability/typecheck_error_006.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(fail("0024_mutability/typecheck_error_007.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(fail("0024_mutability/typecheck_error_008.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(fail("0024_mutability/typecheck_error_009.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/sigma.hpp b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/sigma.hpp index fe09267c..07a62949 100644 --- a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/sigma.hpp +++ b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/sigma.hpp @@ -39,6 +39,7 @@ is_sigma_of(ast::expr_t

const& type, std::tuple const& f_ar return result; } +// TODO no need for tuple here template >... ArgPredicates> constexpr auto sigma_of(std::tuple args) { diff --git a/testfiles/0024_mutability/pass_000.depc b/testfiles/0024_mutability/pass_000.depc index c3548b83..a144b5ff 100644 --- a/testfiles/0024_mutability/pass_000.depc +++ b/testfiles/0024_mutability/pass_000.depc @@ -6,7 +6,8 @@ */ struct t { - i32_t y; + i32_t a; + i64_t b; }; func f0(i32_t mutable x) -> i32_t @@ -23,6 +24,14 @@ func f1(array_t(i32_t, 3) mutable xs) -> array_t(i32_t, 3) func f2(t mutable x) -> t { - x.y = 0; + x.a = 0; + x.b = 1; return x; } + +func f3((i32_t, i64_t) mutable x) -> i64_t +{ + x[0] = 0; + x[1] = 1; + return 0; +} diff --git a/testfiles/0024_mutability/typecheck_error_008.depc b/testfiles/0024_mutability/typecheck_error_008.depc new file mode 100644 index 00000000..c2c1f93a --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_008.depc @@ -0,0 +1,11 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f((u64_t n, array_t(i32_t, n)) mutable x) -> i32_t +{ + x[0] = 1; // typecheck error: cannot mutate element 0 because element 1 depends on it + return 0; +} diff --git a/testfiles/0024_mutability/typecheck_error_009.depc b/testfiles/0024_mutability/typecheck_error_009.depc new file mode 100644 index 00000000..b4f5895c --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_009.depc @@ -0,0 +1,17 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + u64_t n; + array_t(i32_t, n) xs; +}; + +func f(t mutable x) -> t +{ + x.n = 0; // typecheck error: cannot mutate member `n` because member `xs` depends on it + return x; +} From 60eb04e877d34fe43530aacbff852a00270bd086 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Wed, 26 Nov 2025 20:18:45 +0000 Subject: [PATCH 03/11] cannot take ref to mutable --- .../dep0_parser_tests_0024_mutability.cpp | 4 +- dep0/lib/03_typecheck/src/type_assign.cpp | 34 ++++++++++++- .../dep0_typecheck_tests_0024_mutability.cpp | 2 + testfiles/0024_mutability/pass_003.depc | 18 +++++++ .../0024_mutability/typecheck_error_010.depc | 49 +++++++++++++++++++ 5 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 testfiles/0024_mutability/pass_003.depc create mode 100644 testfiles/0024_mutability/typecheck_error_010.depc diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 8348566b..2f656903 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -110,6 +110,8 @@ BOOST_AUTO_TEST_CASE(pass_002) } } +BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } + BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST_REQUIRE(pass("0024_mutability/typecheck_error_000.depc")); @@ -130,7 +132,6 @@ BOOST_AUTO_TEST_CASE(typecheck_error_002) { BOOST_TEST(pass("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_003) { BOOST_TEST(pass("0024_mutability/typecheck_error_003.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_004) { BOOST_TEST(pass("0024_mutability/typecheck_error_004.depc")); } - BOOST_AUTO_TEST_CASE(typecheck_error_005) { BOOST_TEST_REQUIRE(pass("0024_mutability/typecheck_error_005.depc")); @@ -157,5 +158,6 @@ BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(pass("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(pass("0024_mutability/typecheck_error_007.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(pass("0024_mutability/typecheck_error_008.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(pass("0024_mutability/typecheck_error_009.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(pass("0024_mutability/typecheck_error_010.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/src/type_assign.cpp b/dep0/lib/03_typecheck/src/type_assign.cpp index 873cbaa5..740db5f0 100644 --- a/dep0/lib/03_typecheck/src/type_assign.cpp +++ b/dep0/lib/03_typecheck/src/type_assign.cpp @@ -12,12 +12,14 @@ #include "private/max_scope.hpp" #include "private/proof_search.hpp" #include "private/returns_from_all_branches.hpp" +#include "private/root_var.hpp" #include "private/substitute.hpp" #include "dep0/typecheck/beta_delta_reduction.hpp" #include "dep0/typecheck/subscript_access.hpp" #include "dep0/ast/find_member_field.hpp" +#include "dep0/ast/mutable_place_expression.hpp" #include "dep0/ast/place_expression.hpp" #include "dep0/ast/pretty_print.hpp" #include "dep0/ast/unwrap_because.hpp" @@ -375,10 +377,40 @@ type_assign( [&] (parser::expr_t::scope_t) -> expected { return derivation_rules::make_scope_t(env, ctx); }, [&] (parser::expr_t::addressof_t const& x) -> expected { - // TODO can only take reference of immutable variables, needs a test auto expr = type_assign(env, ctx, x.expr.get(), is_mutable_allowed, usage, usage_multiplier); if (not expr) return expr; + auto const is_mutable_variable = [&] (expr_t::var_t const& var) + { + auto const decl = ctx[var]; + return decl and decl->is_mutable == ast::is_mutable_t::yes; + }; + auto const reference_to_immutable = + match( + ast::is_mutable_place_expression(*expr), + [] (ast::immutable_place_t) { return expected{}; }, + [&] (expr_t::var_t const& var) -> expected + { + if (is_mutable_variable(var)) + return error_t("cannot take reference to mutable variable", loc); + return {}; + }, + [&] (expr_t::member_t const& x) -> expected + { + if (auto const root = root_var(x)) + if (is_mutable_variable(*root)) + return error_t("cannot take reference to field rooted in a mutable variable", loc); + return {}; + }, + [&] (expr_t::subscript_t const& x) -> expected + { + if (auto const root = root_var(x)) + if (is_mutable_variable(*root)) + return error_t("cannot take reference to element rooted in a mutable variable", loc); + return {}; + }); + if (not reference_to_immutable) + return reference_to_immutable.error(); return match( ast::is_place_expression(x.expr.get()), [&] (T) -> expected diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index 8fff278f..6d7aa5a4 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -91,6 +91,7 @@ BOOST_AUTO_TEST_CASE(pass_001) } BOOST_AUTO_TEST_CASE(pass_002) { BOOST_TEST(pass("0024_mutability/pass_002.depc")); } +BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(fail("0024_mutability/typecheck_error_001.depc")); } @@ -102,5 +103,6 @@ BOOST_AUTO_TEST_CASE(typecheck_error_006) { BOOST_TEST(fail("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(fail("0024_mutability/typecheck_error_007.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(fail("0024_mutability/typecheck_error_008.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(fail("0024_mutability/typecheck_error_009.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(fail("0024_mutability/typecheck_error_010.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/testfiles/0024_mutability/pass_003.depc b/testfiles/0024_mutability/pass_003.depc new file mode 100644 index 00000000..f6bcea6d --- /dev/null +++ b/testfiles/0024_mutability/pass_003.depc @@ -0,0 +1,18 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + if (*p < 10) + x = 11; + return x; +} + +func g(i32_t a) -> i32_t +{ + // similar to `typecheck_error_010.depc` but `p` is a referencing to `a` which is immutable, which is ok + return f(a, &a); +} diff --git a/testfiles/0024_mutability/typecheck_error_010.depc b/testfiles/0024_mutability/typecheck_error_010.depc new file mode 100644 index 00000000..2e2d5f3d --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_010.depc @@ -0,0 +1,49 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +// struct node_t +// { +// u64_t n; +// array_t(node_t, n) children; +// }; +// +// func select(bool_t which, 0 scope_t a, ref_t(node_t, a) p, ref_t(node_t, a) q) -> ref_t(node_t, a) +// { +// if (which) +// return p; +// else +// return q; +// } +// +// func f(bool_t which, node_t mutable x, 0 true_t(0 < x.n), 0 true_t(1 < x.n)) -> node_t +// { +// // typecheck error: cannot take reference to an element rooted in a mutable variable +// x = *select(which, scopeof(x), &x.children[0], &x.children[1]); +// return x; +// } +// +// func f(i32_t mutable x, i32_t mutable y) -> i32_t +// { +// auto p = &y; // typecheck error: cannot take reference to a mutable variable +// if (*p < 10) +// y = 11; // because otherwise we might break proofs about those references +// +// x = *&y; // typecheck error: cannot take reference to a mutable variable +// return x; +// } + +func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + p = &x; // typecheck error: cannot take reference to a mutable variable + if (*p < 10) + x = 11; // because otherwise we might break proofs about those references + return x; +} + +func g(i32_t x) -> i32_t +{ + return f(x, &x); +} From fcd74abf5de57bc375c746fc1e7d9965759d20eb Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sat, 29 Nov 2025 22:36:01 +0000 Subject: [PATCH 04/11] handle mutation inside if-else --- dep0/lib/01_ast/test/ast_tests_fixture.cpp | 1 + .../dep0_parser_tests_0024_mutability.cpp | 2 + dep0/lib/03_typecheck/src/check.cpp | 87 ++++++++++++++----- dep0/lib/03_typecheck/src/private/check.hpp | 22 ++++- dep0/lib/03_typecheck/src/type_assign.cpp | 4 +- .../dep0_typecheck_tests_0024_mutability.cpp | 2 + .../0024_mutability/typecheck_error_011.depc | 12 +++ .../0024_mutability/typecheck_error_012.depc | 14 +++ 8 files changed, 119 insertions(+), 25 deletions(-) create mode 100644 testfiles/0024_mutability/typecheck_error_011.depc create mode 100644 testfiles/0024_mutability/typecheck_error_012.depc diff --git a/dep0/lib/01_ast/test/ast_tests_fixture.cpp b/dep0/lib/01_ast/test/ast_tests_fixture.cpp index d9bcab62..88dc37eb 100644 --- a/dep0/lib/01_ast/test/ast_tests_fixture.cpp +++ b/dep0/lib/01_ast/test/ast_tests_fixture.cpp @@ -79,6 +79,7 @@ AstTestsFixture::func_arg_t AstTestsFixture::arg(qty_t const qty, expr_t type, c return func_arg_t{ dummy_properties_t{}, qty, + is_mutable_t::no, std::move(type), var_name ? std::optional{var_t(var_name)} : std::nullopt }; diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 2f656903..2638e062 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -159,5 +159,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(pass("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(pass("0024_mutability/typecheck_error_008.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(pass("0024_mutability/typecheck_error_009.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(pass("0024_mutability/typecheck_error_010.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(pass("0024_mutability/typecheck_error_011.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(pass("0024_mutability/typecheck_error_012.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index 8b14dbf3..ad8fa87c 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -76,6 +76,19 @@ static bool has_attribute(func_decl_t const& x, std::string_view const attribute return x.attribute and x.attribute->value == attribute; } +expected mutation_log_t::combine(mutation_log_t&& a, mutation_log_t&& b) +{ + for (auto& [v, t]: b.mutations) + if (auto const [it, inserted] = a.mutations.try_emplace(v, t); not inserted) + if (auto eq = is_beta_delta_equivalent(it->second, t); not eq) + { + std::ostringstream err; + pretty_print(err << "found mutation of different types for variable `", v) << '`'; + return error_t(err.str(), std::vector{std::move(eq.error())}); + } + return std::move(a); +} + expected check(env_t const& base_env, parser::module_t const& x) noexcept { auto env = base_env.extend(); @@ -279,7 +292,7 @@ expected check_type(env_t const& env, ctx_t const& ctx, parser::expr_t c : std::vector{std::move(as_type.error()), std::move(as_kind.error())}); } -expected +expected> check_body( env_t const& env, proof_state_t state, @@ -288,20 +301,39 @@ check_body( usage_t& usage, ast::qty_t const usage_multiplier) { + mutation_log_t log; auto stmts = fmap_or_error( x.stmts, - [&] (parser::stmt_t const& s) + [&] (parser::stmt_t const& s) -> expected { - return check_stmt(env, state, s, is_mutable, usage, usage_multiplier); + auto stmt = check_stmt(env, state, s, is_mutable, usage, usage_multiplier); + if (not stmt) + return stmt.error(); + std::optional new_ctx; + auto const loc = s.properties; + for (auto& [var, type]: stmt->second.mutations) + { + if (auto const decl = state.context[var]) + { + if (not new_ctx) + new_ctx.emplace(state.context.extend()); + auto const v = new_ctx->try_emplace(var.name, loc, decl->qty, ast::is_mutable_t::yes, type); + if (not v) + return v.error(); // should not happen, but just in case + log.mutations.insert_or_assign(std::move(var), std::move(type)); + } + } + if (new_ctx) + state.context = std::move(*new_ctx); + return std::move(stmt->first); }); - if (stmts) - return make_legal_body(std::move(*stmts)); - else + if (not stmts) return std::move(stmts.error()); + return std::pair{make_legal_body(std::move(*stmts)), std::move(log)}; } -expected +expected> check_stmt( env_t const& env, proof_state_t& state, @@ -311,7 +343,8 @@ check_stmt( ast::qty_t const usage_multiplier) { auto const loc = s.properties; - return match( + mutation_log_t log; + auto stmt = match( s.value, [&] (parser::expr_t::app_t const& x) -> expected { @@ -325,8 +358,8 @@ check_stmt( auto lhs = type_assign(env, state.context, x.lhs, is_mutable, usage, usage_multiplier); if (not lhs) return lhs.error(); - auto rhs = - check_expr(env, state.context, x.rhs, lhs->properties.sort.get(), is_mutable, usage, usage_multiplier); + auto const& lhs_type = lhs->properties.sort.get(); + auto rhs = check_expr(env, state.context, x.rhs, lhs_type, is_mutable, usage, usage_multiplier); if (not rhs) return rhs.error(); auto const root = match( @@ -402,13 +435,7 @@ check_stmt( assert(decl and "root of assignment was a variable but it did not exist in current context"); if (decl->is_mutable == ast::is_mutable_t::no) return error_t("cannot mutate immutable variable"); - auto modified_context = state.context.extend(); - auto const new_var = - modified_context.try_emplace( - decl->var.name, decl->origin, decl->qty, ast::is_mutable_t::yes, decl->type); - if (not new_var) - return new_var.error(); // should not happen, but just in case - state.context = std::move(modified_context); + log.mutations.insert_or_assign(*root, decl->type); return make_legal_stmt(stmt_t::assign_t{std::move(*lhs), std::move(*rhs)}); }, [&] (parser::stmt_t::if_else_t const& x) -> expected @@ -469,11 +496,19 @@ check_stmt( { if (auto ok = combine_usages(); not ok) return std::move(ok.error()); + // combine mutations from both branches and add the result to the current context + auto new_log = + mutation_log_t::combine( + std::move(true_branch->second), + std::move(false_branch->second)); + if (not new_log) + return std::move(new_log.error()); + log = std::move(*new_log); return make_legal_stmt( stmt_t::if_else_t{ std::move(*cond), - std::move(*true_branch), - std::move(*false_branch) + std::move(true_branch->first), + std::move(false_branch->first) }); } else @@ -483,17 +518,24 @@ check_stmt( // but if the true branch returns from all its sub-branches, // then it means we are now in the implied else branch; // we can then rewrite `cond = false` inside the current proof state - if (returns_from_all_branches(*true_branch)) + if (returns_from_all_branches(true_branch->first)) { state.rewrite(*cond, derivation_rules::make_false(env, state.context)); add_true_not_cond(state.context); } if (auto ok = combine_usages(); not ok) return std::move(ok.error()); + // TODO currently mutations cannot change type, but when they can do, this code might be incorrect. + // Because if the true-branch of an if-without-else has changed the type of a variable of the parent scope, + // the type of that variable after the if-statement is potentially unspecified. + // Maybe a solution is to synthesize a log for the missing else-branch where each variable + // mutated by the true-branch is added to the synthetic log with the original type + // and then run the same combining logic as-if the else-branch was present. + log = std::move(true_branch->second); return make_legal_stmt( stmt_t::if_else_t{ std::move(*cond), - std::move(*true_branch), + std::move(true_branch->first), std::nullopt }); }, @@ -540,6 +582,9 @@ check_stmt( else return check_impossible_stmt(state.context, std::nullopt); }); + if (not stmt) + return std::move(stmt.error()); + return std::pair{std::move(*stmt), std::move(log)}; } expected diff --git a/dep0/lib/03_typecheck/src/private/check.hpp b/dep0/lib/03_typecheck/src/private/check.hpp index 38da3f48..6f529934 100644 --- a/dep0/lib/03_typecheck/src/private/check.hpp +++ b/dep0/lib/03_typecheck/src/private/check.hpp @@ -21,11 +21,29 @@ #include "dep0/error.hpp" +#include #include #include namespace dep0::typecheck { +/** @brief Helper type to keep track of which variables have been mutated by a body or statement. */ +struct mutation_log_t +{ + std::map mutations; /**< Store the new type of a variable after a mutation.*/ + + /** + * @brief Constructs a new log formed by the union of the two input logs. + * @remarks Currently this expects that, if a variable appears in both input logs, it has the same type. + * If not, this function will return an error. + * This is only a temporary limitation which will need to be removed. + * If a variable has been mutated to different types by different branches, + * the combined log will need to somehow contain the mutated variable but + * it is currently unclear what its type should be. + */ + static expected combine(mutation_log_t&&, mutation_log_t&&); +}; + /** * @brief Checks whether a type definition is legal; * if it is, the type is stored in the given environment. @@ -83,7 +101,7 @@ expected check_func_def(env_t&, parser::func_def_t const&); * * @return A legal body or an error. */ -expected +expected> check_body( env_t const&, proof_state_t, @@ -107,7 +125,7 @@ check_body( * * @return A legal statement or an error. */ -expected +expected> check_stmt( env_t const&, proof_state_t&, diff --git a/dep0/lib/03_typecheck/src/type_assign.cpp b/dep0/lib/03_typecheck/src/type_assign.cpp index 740db5f0..7fd08bd8 100644 --- a/dep0/lib/03_typecheck/src/type_assign.cpp +++ b/dep0/lib/03_typecheck/src/type_assign.cpp @@ -752,7 +752,7 @@ expected type_assign_abs( return std::move(body.error()); // so far so good, but we now need to make sure that all branches contain a return statement, // with the only exception of functions returning `unit_t` because the return statement is optional; - if (not returns_from_all_branches(*body)) + if (not returns_from_all_branches(body->first)) { if (not is_beta_delta_equivalent(ret_type.get(), derivation_rules::make_unit(env, ctx))) { @@ -770,7 +770,7 @@ expected type_assign_abs( is_mutable, std::move(arg_types), std::move(ret_type), - std::move(*body) + std::move(body->first) }); } diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index 6d7aa5a4..abadec05 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -104,5 +104,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_007) { BOOST_TEST(fail("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_008) { BOOST_TEST(fail("0024_mutability/typecheck_error_008.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(fail("0024_mutability/typecheck_error_009.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(fail("0024_mutability/typecheck_error_010.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(fail("0024_mutability/typecheck_error_011.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(fail("0024_mutability/typecheck_error_012.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/testfiles/0024_mutability/typecheck_error_011.depc b/testfiles/0024_mutability/typecheck_error_011.depc new file mode 100644 index 00000000..3eb1373c --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_011.depc @@ -0,0 +1,12 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable i, array_t(i32_t, 3) xs, 0 true_t(i < 3)) -> i32_t +{ + if (i == 0) + i = 10; + return xs[i]; // typecheck error: `i` may have been modified, so the proof `i < 3` may not be valid +} diff --git a/testfiles/0024_mutability/typecheck_error_012.depc b/testfiles/0024_mutability/typecheck_error_012.depc new file mode 100644 index 00000000..f5a90c26 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_012.depc @@ -0,0 +1,14 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable i, array_t(i32_t, 3) xs, 0 true_t(i < 3)) -> i32_t +{ + if (i == 0) + i = 10; + else + i = 1; + return xs[i]; // typecheck error: `i` may have been modified, so the proof `i < 3` may not be valid +} From 1404d4c179d231f5a40b013acce3810da7e0275f Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sat, 29 Nov 2025 22:43:21 +0000 Subject: [PATCH 05/11] add pass_004.depc --- .../test/dep0_parser_tests_0024_mutability.cpp | 1 + .../dep0_typecheck_tests_0024_mutability.cpp | 1 + testfiles/0024_mutability/pass_004.depc | 16 ++++++++++++++++ 3 files changed, 18 insertions(+) create mode 100644 testfiles/0024_mutability/pass_004.depc diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 2638e062..33af84f2 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -111,6 +111,7 @@ BOOST_AUTO_TEST_CASE(pass_002) } BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } +BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_000) { diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index abadec05..1530b1cd 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -92,6 +92,7 @@ BOOST_AUTO_TEST_CASE(pass_001) BOOST_AUTO_TEST_CASE(pass_002) { BOOST_TEST(pass("0024_mutability/pass_002.depc")); } BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } +BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(fail("0024_mutability/typecheck_error_001.depc")); } diff --git a/testfiles/0024_mutability/pass_004.depc b/testfiles/0024_mutability/pass_004.depc new file mode 100644 index 00000000..06b02496 --- /dev/null +++ b/testfiles/0024_mutability/pass_004.depc @@ -0,0 +1,16 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct node_t +{ + array_t(node_t, 2) children; +}; + +func f(node_t mutable x, node_t y) -> node_t +{ + x = y.children[0]; + return x; +} From dc838d239d6b1f776ea95fb21c34f666759b6321 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sat, 6 Dec 2025 14:15:43 +0000 Subject: [PATCH 06/11] more tests --- .../dep0_parser_tests_0024_mutability.cpp | 4 +++ .../dep0_typecheck_tests_0024_mutability.cpp | 4 +++ testfiles/0024_mutability/pass_003.depc | 2 +- testfiles/0024_mutability/pass_005.depc | 18 +++++++++++ testfiles/0024_mutability/pass_006.depc | 24 ++++++++++++++ .../0024_mutability/typecheck_error_010.depc | 31 ------------------- .../0024_mutability/typecheck_error_013.depc | 18 +++++++++++ .../0024_mutability/typecheck_error_014.depc | 24 ++++++++++++++ 8 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 testfiles/0024_mutability/pass_005.depc create mode 100644 testfiles/0024_mutability/pass_006.depc create mode 100644 testfiles/0024_mutability/typecheck_error_013.depc create mode 100644 testfiles/0024_mutability/typecheck_error_014.depc diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 33af84f2..9bb348c2 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -112,6 +112,8 @@ BOOST_AUTO_TEST_CASE(pass_002) BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc")); } +BOOST_AUTO_TEST_CASE(pass_005) { BOOST_TEST(pass("0024_mutability/pass_005.depc")); } +BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_000) { @@ -162,5 +164,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(pass("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(pass("0024_mutability/typecheck_error_010.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(pass("0024_mutability/typecheck_error_011.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(pass("0024_mutability/typecheck_error_012.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_013) { BOOST_TEST(pass("0024_mutability/typecheck_error_013.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_014) { BOOST_TEST(pass("0024_mutability/typecheck_error_014.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index 1530b1cd..14ecc48a 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -93,6 +93,8 @@ BOOST_AUTO_TEST_CASE(pass_001) BOOST_AUTO_TEST_CASE(pass_002) { BOOST_TEST(pass("0024_mutability/pass_002.depc")); } BOOST_AUTO_TEST_CASE(pass_003) { BOOST_TEST(pass("0024_mutability/pass_003.depc")); } BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc")); } +BOOST_AUTO_TEST_CASE(pass_005) { BOOST_TEST(pass("0024_mutability/pass_005.depc")); } +BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(fail("0024_mutability/typecheck_error_001.depc")); } @@ -107,5 +109,7 @@ BOOST_AUTO_TEST_CASE(typecheck_error_009) { BOOST_TEST(fail("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_010) { BOOST_TEST(fail("0024_mutability/typecheck_error_010.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(fail("0024_mutability/typecheck_error_011.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(fail("0024_mutability/typecheck_error_012.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_013) { BOOST_TEST(fail("0024_mutability/typecheck_error_013.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_014) { BOOST_TEST(fail("0024_mutability/typecheck_error_014.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/testfiles/0024_mutability/pass_003.depc b/testfiles/0024_mutability/pass_003.depc index f6bcea6d..9f550642 100644 --- a/testfiles/0024_mutability/pass_003.depc +++ b/testfiles/0024_mutability/pass_003.depc @@ -13,6 +13,6 @@ func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t func g(i32_t a) -> i32_t { - // similar to `typecheck_error_010.depc` but `p` is a referencing to `a` which is immutable, which is ok + // similar to `typecheck_error_010.depc` but `p` is a reference to `a` which is immutable, which is ok return f(a, &a); } diff --git a/testfiles/0024_mutability/pass_005.depc b/testfiles/0024_mutability/pass_005.depc new file mode 100644 index 00000000..f9aead7e --- /dev/null +++ b/testfiles/0024_mutability/pass_005.depc @@ -0,0 +1,18 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(array_t(i32_t, 2) mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + if (*p < 10) + x[0] = 11; + return x[0]; +} + +func g(array_t(i32_t, 2) x) -> i32_t +{ + // similar to `typecheck_error_013.depc` but `p` is a reference to `x[0]` which is immutable, which is ok + return f(x, &x[0]); +} diff --git a/testfiles/0024_mutability/pass_006.depc b/testfiles/0024_mutability/pass_006.depc new file mode 100644 index 00000000..3f5da3f2 --- /dev/null +++ b/testfiles/0024_mutability/pass_006.depc @@ -0,0 +1,24 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + i32_t x; + i32_t y; +}; + +func f(t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + if (*p < 10) + x.x = 11; + return x.x; +} + +func g(t x) -> i32_t +{ + // similar to `typecheck_error_014.depc` but `p` is a reference to `x.x` which is immutable, which is ok + return f(x, &x.x); +} diff --git a/testfiles/0024_mutability/typecheck_error_010.depc b/testfiles/0024_mutability/typecheck_error_010.depc index 2e2d5f3d..25aa8bea 100644 --- a/testfiles/0024_mutability/typecheck_error_010.depc +++ b/testfiles/0024_mutability/typecheck_error_010.depc @@ -4,37 +4,6 @@ * Distributed under the Boost Software License, Version 1.0. * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) */ -// struct node_t -// { -// u64_t n; -// array_t(node_t, n) children; -// }; -// -// func select(bool_t which, 0 scope_t a, ref_t(node_t, a) p, ref_t(node_t, a) q) -> ref_t(node_t, a) -// { -// if (which) -// return p; -// else -// return q; -// } -// -// func f(bool_t which, node_t mutable x, 0 true_t(0 < x.n), 0 true_t(1 < x.n)) -> node_t -// { -// // typecheck error: cannot take reference to an element rooted in a mutable variable -// x = *select(which, scopeof(x), &x.children[0], &x.children[1]); -// return x; -// } -// -// func f(i32_t mutable x, i32_t mutable y) -> i32_t -// { -// auto p = &y; // typecheck error: cannot take reference to a mutable variable -// if (*p < 10) -// y = 11; // because otherwise we might break proofs about those references -// -// x = *&y; // typecheck error: cannot take reference to a mutable variable -// return x; -// } - func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t { p = &x; // typecheck error: cannot take reference to a mutable variable diff --git a/testfiles/0024_mutability/typecheck_error_013.depc b/testfiles/0024_mutability/typecheck_error_013.depc new file mode 100644 index 00000000..ed8e2842 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_013.depc @@ -0,0 +1,18 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(array_t(i32_t, 2) mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + p = &x[0]; // typecheck error: cannot take reference to an element rooted in a mutable variable + if (*p < 10) + x[0] = 11; // because otherwise we might break proofs about those references + return x[0]; +} + +func g(array_t(i32_t, 2) x) -> i32_t +{ + return f(x, &x[0]); +} diff --git a/testfiles/0024_mutability/typecheck_error_014.depc b/testfiles/0024_mutability/typecheck_error_014.depc new file mode 100644 index 00000000..e189efbe --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_014.depc @@ -0,0 +1,24 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +struct t +{ + i32_t x; + i32_t y; +}; + +func f(t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + p = &x.x; // typecheck error: cannot take reference to field rooted in a mutable variable + if (*p < 10) + x[0] = 11; + return x[0]; +} + +func g(t x) -> i32_t +{ + return f(x, &x.x); +} From 924f50085282630ab1fb8c7dc597835e8e7b095b Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sat, 6 Dec 2025 18:00:13 +0000 Subject: [PATCH 07/11] immutable block --- antlr4/DepCLexer.g4 | 1 + antlr4/DepCParser.g4 | 3 +- .../dep0/ast/alpha_equivalence_impl.hpp | 7 +++ dep0/lib/01_ast/include/dep0/ast/ast.hpp | 10 +++- .../include/dep0/ast/hash_code_impl.hpp | 7 +++ .../include/dep0/ast/max_index_impl.hpp | 39 +++++++------ .../include/dep0/ast/occurs_in_impl.hpp | 4 ++ .../01_ast/include/dep0/ast/pretty_print.hpp | 3 + .../include/dep0/ast/pretty_print_impl.hpp | 10 ++++ .../01_ast/include/dep0/ast/replace_impl.hpp | 6 ++ .../lib/01_ast/include/dep0/ast/size_impl.hpp | 4 ++ dep0/lib/02_parser/src/parse.cpp | 13 +++++ .../dep0_parser_tests_0024_mutability.cpp | 56 +++++++++++++++++++ .../02_parser/test/parser_tests_fixture.hpp | 6 ++ dep0/lib/03_typecheck/src/beta_reduction.cpp | 11 ++++ dep0/lib/03_typecheck/src/check.cpp | 25 +++++++++ dep0/lib/03_typecheck/src/delta_unfold.cpp | 6 ++ dep0/lib/03_typecheck/src/is_impossible.cpp | 1 + dep0/lib/03_typecheck/src/is_terminator.cpp | 1 + dep0/lib/03_typecheck/src/max_scope.cpp | 4 ++ dep0/lib/03_typecheck/src/rewrite.cpp | 11 ++++ dep0/lib/03_typecheck/src/substitute.cpp | 5 ++ .../dep0_typecheck_tests_0024_mutability.cpp | 56 +++++++++++++++++++ .../test/typecheck_tests_fixture.hpp | 6 ++ dep0/lib/05_llvmgen/src/gen_body.cpp | 4 ++ .../dep0/testing/ast_predicates/stmt.hpp | 19 +++++++ testfiles/0024_mutability/pass_007.depc | 25 +++++++++ .../0024_mutability/typecheck_error_015.depc | 13 +++++ .../0024_mutability/typecheck_error_016.depc | 14 +++++ .../0024_mutability/typecheck_error_017.depc | 14 +++++ 30 files changed, 366 insertions(+), 18 deletions(-) create mode 100644 testfiles/0024_mutability/pass_007.depc create mode 100644 testfiles/0024_mutability/typecheck_error_015.depc create mode 100644 testfiles/0024_mutability/typecheck_error_016.depc create mode 100644 testfiles/0024_mutability/typecheck_error_017.depc diff --git a/antlr4/DepCLexer.g4 b/antlr4/DepCLexer.g4 index d5825ab9..122cf4b8 100644 --- a/antlr4/DepCLexer.g4 +++ b/antlr4/DepCLexer.g4 @@ -27,6 +27,7 @@ KW_I32_T: 'i32_t'; KW_I64_T: 'i64_t'; KW_I8_T: 'i8_t'; KW_IF: 'if'; +KW_IMMUTABLE: 'immutable'; KW_IMPOSSIBLE: 'impossible'; KW_MUTABLE: 'mutable'; KW_NOT: 'not'; diff --git a/antlr4/DepCParser.g4 b/antlr4/DepCParser.g4 index 6c652f80..04ce97bb 100644 --- a/antlr4/DepCParser.g4 +++ b/antlr4/DepCParser.g4 @@ -65,10 +65,11 @@ typeVar: name=ID; // Statements body: '{' stmt* '}'; -stmt: funcCallStmt | assignment | ifElse | returnStmt | impossibleStmt; +stmt: funcCallStmt | assignment | immutableBlock | ifElse | returnStmt | impossibleStmt; funcCallStmt: func=expr '(' (expr (',' expr)*)? ')' ';'; assignment: lhs=expr '=' rhs=expr ';'; +immutableBlock: 'immutable' '(' ID (',' ID)* ')' body; ifElse: 'if' '(' cond=expr ')' true_branch=bodyOrStmt ('else' false_branch=bodyOrStmt)?; bodyOrStmt: body | stmt; returnStmt: 'return' expr? ';'; diff --git a/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp index 32fa1bf6..82ae01cd 100644 --- a/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/alpha_equivalence_impl.hpp @@ -290,6 +290,13 @@ struct alpha_equivalence_visitor return eq; } + result_t operator()(typename stmt_t

::immutable_t& x, typename stmt_t

::immutable_t& y) const + { + if (x.vars != y.vars) + return dep0::error_t("immutable blocks must have the same set of variables"); + return is_alpha_equivalent_impl(x.body, y.body); + } + result_t operator()(typename stmt_t

::if_else_t& x, typename stmt_t

::if_else_t& y) const { auto eq = is_alpha_equivalent_impl(x.cond, y.cond); diff --git a/dep0/lib/01_ast/include/dep0/ast/ast.hpp b/dep0/lib/01_ast/include/dep0/ast/ast.hpp index a2ee897f..056a7b5b 100644 --- a/dep0/lib/01_ast/include/dep0/ast/ast.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/ast.hpp @@ -24,6 +24,7 @@ #include #include +#include #include #include #include @@ -448,6 +449,13 @@ struct stmt_t expr_t rhs; }; + /** @brief Represents an immutable block, for example `immutable(x, y) { ... }`. */ + struct immutable_t + { + std::set vars; + body_t body; + }; + /** @brief Represents an `if` or `if-else` statement, whose condition must be of type `%bool_t`. */ struct if_else_t { @@ -481,7 +489,7 @@ struct stmt_t std::optional reason; }; - using value_t = std::variant; + using value_t = std::variant; properties_t properties; value_t value; diff --git a/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp index d0c10571..ef3c94ec 100644 --- a/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/hash_code_impl.hpp @@ -105,6 +105,13 @@ std::size_t hash_code_impl(hash_code_state_t

& state, stmt_t

const& x) { return combine(hash_code_impl(state, assign.lhs), hash_code_impl(state, assign.rhs)); }, + [&] (stmt_t

::immutable_t const& immutable) + { + std::size_t result = 0ul; + for (auto const& var: immutable.vars) + boost::hash_combine(result, hash_code_impl(state, var)); + return combine(result, hash_code_impl(state, immutable.body)); + }, [&] (stmt_t

::if_else_t const& if_) { return combine( diff --git a/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp index e02128b6..833cc72d 100644 --- a/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/max_index_impl.hpp @@ -25,6 +25,22 @@ template std::size_t max_index(body_t

const&); template std::size_t max_index(expr_t

const&); template std::size_t max_index(typename expr_t

::app_t const&); +} // namespace impl + +namespace { +template +inline constexpr auto max_accumulator = + boost::hana::overload( + [] (std::size_t const x, func_arg_t

const& y) { return std::max(x, impl::max_index(y)); }, + [] (std::size_t const x, typename expr_t

::var_t const& y) { return std::max(x, y.idx); }, + [] (std::size_t const x, typename type_def_t

::struct_t::field_t const& y) + { + return std::max(x, std::max(impl::max_index(y.type), y.var.idx)); + }); +} // namespace + +namespace impl { + template std::size_t max_index(func_arg_t

const& x) { @@ -51,6 +67,10 @@ std::size_t max_index(body_t

const& x) { return std::max(max_index(x.lhs), max_index(x.rhs)); }, + [] (stmt_t

::immutable_t const& x) + { + return std::accumulate(x.vars.begin(), x.vars.end(), max_index(x.body), max_accumulator

); + }, [] (stmt_t

::if_else_t const& if_) { return std::max( @@ -194,13 +214,6 @@ std::size_t max_index(typename expr_t

::app_t const& x) } // namespace impl -namespace { -inline constexpr auto max_accumulator = [] (std::size_t const acc, func_arg_t

const& arg) -{ - return std::max(acc, impl::max_index(arg)); -}; -} - template std::size_t max_index( typename std::vector>::const_iterator const begin, @@ -209,7 +222,7 @@ std::size_t max_index( body_t

const* body) { auto const initial_value = std::max(impl::max_index(ret_type), body ? impl::max_index(*body) : 0ul); - return std::accumulate(begin, end, initial_value, max_accumulator); + return std::accumulate(begin, end, initial_value, max_accumulator

); } template @@ -217,7 +230,7 @@ std::size_t max_index( typename std::vector>::const_iterator const begin, typename std::vector>::const_iterator const end) { - return std::accumulate(begin, end, 0ul, max_accumulator); + return std::accumulate(begin, end, 0ul, max_accumulator

); } template @@ -225,13 +238,7 @@ std::size_t max_index( typename std::vector::struct_t::field_t>::const_iterator const begin, typename std::vector::struct_t::field_t>::const_iterator const end) { - return std::accumulate( - begin, end, - 0ul, - [] (std::size_t const acc, type_def_t

::struct_t::field_t const& field) - { - return std::max(acc, std::max(impl::max_index(field.type), field.var.idx)); - }); + return std::accumulate(begin, end, 0ul, max_accumulator

); } } // namespace dep0::ast diff --git a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp index e68ef911..8a2932ae 100644 --- a/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/occurs_in_impl.hpp @@ -43,6 +43,10 @@ bool occurs_in(typename expr_t

::var_t const& var, body_t

const& x, occurre { return occurs_in(var, assign.lhs, style) or occurs_in(var, assign.rhs, style); }, + [&] (stmt_t

::immutable_t const& immutable) + { + return immutable.vars.contains(var) or occurs_in(var, immutable.body, style); + }, [&] (stmt_t

::if_else_t const& if_) { return occurs_in(var, if_.cond, style) diff --git a/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp b/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp index 9d752fd9..b0b141df 100644 --- a/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/pretty_print.hpp @@ -56,6 +56,9 @@ std::ostream& pretty_print(std::ostream&, stmt_t

const&, std::size_t indent = template std::ostream& pretty_print(std::ostream&, typename stmt_t

::assign_t const&, std::size_t indent = 0ul); +template +std::ostream& pretty_print(std::ostream&, typename stmt_t

::immutable_t const&, std::size_t indent = 0ul); + template std::ostream& pretty_print(std::ostream&, typename stmt_t

::if_else_t const&, std::size_t indent = 0ul); diff --git a/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp index 8e6188d9..8fb8609a 100644 --- a/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/pretty_print_impl.hpp @@ -250,6 +250,16 @@ std::ostream& pretty_print(std::ostream& os, typename stmt_t

::assign_t const& return os; } +template +std::ostream& pretty_print(std::ostream& os, typename stmt_t

::immutable_t const& x, std::size_t const indent) +{ + os << "immutable("; + for (bool first = true; auto const& var: x.vars) + pretty_print

(std::exchange(first, false) ? os : os << ", ", var); + pretty_print(detail::new_line(os << ')', indent), x.body, indent); + return os; +} + template std::ostream& pretty_print(std::ostream& os, typename stmt_t

::if_else_t const& x, std::size_t const indent) { diff --git a/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp index f35015e3..b33039ad 100644 --- a/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/replace_impl.hpp @@ -44,6 +44,12 @@ void replace(typename expr_t

::var_t const& from, typename expr_t

::var_t co replace(from, to, assign.lhs); replace(from, to, assign.rhs); }, + [&] (typename stmt_t

::immutable_t& immutable) + { + immutable.vars.erase(from); + immutable.vars.insert(to); + replace(from, to, immutable.body); + }, [&] (typename stmt_t

::if_else_t& if_) { replace(from, to, if_.cond); diff --git a/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp b/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp index f6b9d120..82e90c93 100644 --- a/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp +++ b/dep0/lib/01_ast/include/dep0/ast/size_impl.hpp @@ -58,6 +58,10 @@ std::size_t size(stmt_t

const& x) { return 1ul + std::max(size(assign.lhs), size(assign.rhs)); }, + [] (stmt_t

::immutable_t const& immutable) + { + return 1ul + size(immutable.body); + }, [] (stmt_t

::if_else_t const& if_) { return 1ul + std::max( diff --git a/dep0/lib/02_parser/src/parse.cpp b/dep0/lib/02_parser/src/parse.cpp index 7a9ed9a2..dbbeee8c 100644 --- a/dep0/lib/02_parser/src/parse.cpp +++ b/dep0/lib/02_parser/src/parse.cpp @@ -304,6 +304,7 @@ struct parse_visitor_t : dep0::DepCParserVisitor assert(ctx); if (ctx->funcCallStmt()) return std::any_cast(visitFuncCallStmt(ctx->funcCallStmt())); if (ctx->assignment()) return std::any_cast(visitAssignment(ctx->assignment())); + if (ctx->immutableBlock()) return std::any_cast(visitImmutableBlock(ctx->immutableBlock())); if (ctx->ifElse()) return std::any_cast(visitIfElse(ctx->ifElse())); if (ctx->returnStmt()) return std::any_cast(visitReturnStmt(ctx->returnStmt())); if (ctx->impossibleStmt()) return std::any_cast(visitImpossibleStmt(ctx->impossibleStmt())); @@ -341,6 +342,18 @@ struct parse_visitor_t : dep0::DepCParserVisitor }}; } + virtual std::any visitImmutableBlock(DepCParser::ImmutableBlockContext* ctx) override + { + assert(ctx); + auto const vars = fmap(ctx->ID(), [this] (auto* x) { return expr_t::var_t{get_text(src, *x->getSymbol())}; }); + return stmt_t{ + get_loc(src, *ctx), + stmt_t::immutable_t{ + std::set{vars.begin(), vars.end()}, + std::any_cast(visitBody(ctx->body())) + }}; + } + virtual std::any visitIfElse(DepCParser::IfElseContext* ctx) override { assert(ctx); diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index 9bb348c2..a7637a2d 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -115,6 +115,59 @@ BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc" BOOST_AUTO_TEST_CASE(pass_005) { BOOST_TEST(pass("0024_mutability/pass_005.depc")); } BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc")); } +BOOST_AUTO_TEST_CASE(pass_007) +{ + BOOST_TEST(pass("0024_mutability/pass_007.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 3ul); + { + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f0"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_scope, "a", dep0::ast::qty_t::zero)); + BOOST_TEST(is_arg(f->value.args[1], ref_of(is_i32, var("a")), "p")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST(is_return_of(f->value.body.stmts[0ul], deref(var("p")))); + } + { + auto const f = std::get_if(&pass_result->entries[1]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f1"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x"}, + std::tuple{ + return_of(app_of(var("f0"), scopeof("x"), addressof("x"))) + })); + } + { + auto const f = std::get_if(&pass_result->entries[2]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f2"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x", "y"}, + std::tuple{ + return_of( + plus( + app_of(var("f0"), scopeof("x"), addressof("x")), + app_of(var("f0"), scopeof("y"), addressof("y")))) + })); + } +} + BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST_REQUIRE(pass("0024_mutability/typecheck_error_000.depc")); @@ -166,5 +219,8 @@ BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(pass("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(pass("0024_mutability/typecheck_error_012.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_013) { BOOST_TEST(pass("0024_mutability/typecheck_error_013.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_014) { BOOST_TEST(pass("0024_mutability/typecheck_error_014.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_015) { BOOST_TEST(pass("0024_mutability/typecheck_error_015.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_016) { BOOST_TEST(pass("0024_mutability/typecheck_error_016.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_017) { BOOST_TEST(pass("0024_mutability/typecheck_error_017.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/02_parser/test/parser_tests_fixture.hpp b/dep0/lib/02_parser/test/parser_tests_fixture.hpp index 53edd2d3..99270517 100644 --- a/dep0/lib/02_parser/test/parser_tests_fixture.hpp +++ b/dep0/lib/02_parser/test/parser_tests_fixture.hpp @@ -74,6 +74,12 @@ struct ParserTestsFixture return dep0::testing::init_list_of(std::forward(args)...); } + template + static constexpr auto assign_of(Args&&... args) + { + return dep0::testing::assign_of(std::forward(args)...); + } + template static constexpr auto if_else_of(Args&&... args) { diff --git a/dep0/lib/03_typecheck/src/beta_reduction.cpp b/dep0/lib/03_typecheck/src/beta_reduction.cpp index 87c88592..72635e2b 100644 --- a/dep0/lib/03_typecheck/src/beta_reduction.cpp +++ b/dep0/lib/03_typecheck/src/beta_reduction.cpp @@ -28,6 +28,7 @@ namespace impl { static bool beta_normalize(stmt_t&); static bool beta_normalize(stmt_t::assign_t&); +static bool beta_normalize(stmt_t::immutable_t&); static bool beta_normalize(stmt_t::if_else_t&); static bool beta_normalize(stmt_t::return_t&); static bool beta_normalize(stmt_t::impossible_t&); @@ -81,6 +82,11 @@ bool beta_normalize(stmt_t::assign_t& assign) return changed; } +bool beta_normalize(stmt_t::immutable_t& immutable) +{ + return beta_normalize(immutable.body); +} + bool beta_normalize(stmt_t::if_else_t& if_) { bool changed = beta_normalize(if_.cond); @@ -243,6 +249,11 @@ bool beta_normalize(body_t& body) // TODO could drop trivial self-assignments, eg `x = x`; return std::next(it); }, + [&] (stmt_t::immutable_t& immutable) + { + changed |= beta_normalize(immutable.body); + return std::next(it); + }, [&] (stmt_t::if_else_t& if_) { // this step is arguably iota-reduction diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index ad8fa87c..dce7258b 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -438,6 +438,31 @@ check_stmt( log.mutations.insert_or_assign(*root, decl->type); return make_legal_stmt(stmt_t::assign_t{std::move(*lhs), std::move(*rhs)}); }, + [&] (parser::stmt_t::immutable_t const& x) -> expected + { + auto new_state = proof_state_t(state.context.extend(), state.goal); + std::set vars; + for (auto const& v: x.vars) + { + auto const d = state.context[v.name]; + if (not d) + { + std::ostringstream err; + pretty_print(err << "unknown variable `", v) << '`'; + err << " inside immutable block declaration"; + return error_t(err.str(), loc); + } + auto var = new_state.context.try_emplace(v.name, d->origin, d->qty, ast::is_mutable_t::no, d->type); + if (not var) // should not happen but whatever + return var.error(); + vars.insert(std::move(*var)); + } + auto body = check_body(env, std::move(new_state), x.body, is_mutable, usage, usage_multiplier); + if (not body) + return body.error(); + log = std::move(body->second); + return make_legal_stmt(stmt_t::immutable_t{std::move(vars), std::move(body->first)}); + }, [&] (parser::stmt_t::if_else_t const& x) -> expected { auto cond = diff --git a/dep0/lib/03_typecheck/src/delta_unfold.cpp b/dep0/lib/03_typecheck/src/delta_unfold.cpp index ec9488f9..996b1d8d 100644 --- a/dep0/lib/03_typecheck/src/delta_unfold.cpp +++ b/dep0/lib/03_typecheck/src/delta_unfold.cpp @@ -36,6 +36,7 @@ namespace impl { static bool delta_unfold(stmt_t&); static bool delta_unfold(stmt_t::assign_t&); +static bool delta_unfold(stmt_t::immutable_t&); static bool delta_unfold(stmt_t::if_else_t&); static bool delta_unfold(stmt_t::return_t&); static bool delta_unfold(stmt_t::impossible_t&); @@ -129,6 +130,11 @@ bool delta_unfold(stmt_t::assign_t& assign) return delta_unfold(assign.rhs) or delta_unfold(assign.lhs); } +bool delta_unfold(stmt_t::immutable_t& immutable) +{ + return delta_unfold(immutable.body); +} + bool delta_unfold(stmt_t::if_else_t& if_) { return delta_unfold(if_.cond) diff --git a/dep0/lib/03_typecheck/src/is_impossible.cpp b/dep0/lib/03_typecheck/src/is_impossible.cpp index 41bcd6e6..9c3460de 100644 --- a/dep0/lib/03_typecheck/src/is_impossible.cpp +++ b/dep0/lib/03_typecheck/src/is_impossible.cpp @@ -67,6 +67,7 @@ bool is_impossible(stmt_t const& s) s.value, [] (expr_t::app_t const& x) { return impl::is_impossible(x); }, [] (stmt_t::assign_t const& x) { return impl::is_impossible(x.lhs) or impl::is_impossible(x.rhs); }, + [] (stmt_t::immutable_t const& x) { return is_impossible(x.body); }, [] (stmt_t::if_else_t const& x) { // an if-statement is impossible if the condition is impossible or diff --git a/dep0/lib/03_typecheck/src/is_terminator.cpp b/dep0/lib/03_typecheck/src/is_terminator.cpp index 014451af..e2351427 100644 --- a/dep0/lib/03_typecheck/src/is_terminator.cpp +++ b/dep0/lib/03_typecheck/src/is_terminator.cpp @@ -18,6 +18,7 @@ bool is_terminator(stmt_t const& s) s.value, [] (expr_t::app_t const&) { return false; }, [] (stmt_t::assign_t const&) { return false; }, + [] (stmt_t::immutable_t const& x) { return returns_from_all_branches(x.body); }, [] (stmt_t::if_else_t const& x) { return returns_from_all_branches(x); }, [] (stmt_t::return_t const&) { return true; }, [] (stmt_t::impossible_t const&) diff --git a/dep0/lib/03_typecheck/src/max_scope.cpp b/dep0/lib/03_typecheck/src/max_scope.cpp index 3933ceb5..22fc92e6 100644 --- a/dep0/lib/03_typecheck/src/max_scope.cpp +++ b/dep0/lib/03_typecheck/src/max_scope.cpp @@ -91,6 +91,10 @@ expected max_scope_stmt(ctx_t const& ctx, stmt_t const& stmt) { return max_scope_combine(max_scope_expr(ctx, assign.lhs), max_scope_expr(ctx, assign.rhs)); }, + [&] (stmt_t::immutable_t const& immutable) -> expected + { + return max_scope_body(ctx, immutable.body); + }, [&] (stmt_t::if_else_t const& if_stmt) -> expected { auto cond_result = max_scope_expr(ctx, if_stmt.cond); diff --git a/dep0/lib/03_typecheck/src/rewrite.cpp b/dep0/lib/03_typecheck/src/rewrite.cpp index 23c4570f..5d0d0f3d 100644 --- a/dep0/lib/03_typecheck/src/rewrite.cpp +++ b/dep0/lib/03_typecheck/src/rewrite.cpp @@ -94,6 +94,17 @@ std::optional rewrite(expr_t const& from, expr_t const& to, stmt_t const impl::choose(std::move(new_lhs), assign.lhs), impl::choose(std::move(new_rhs), assign.rhs)}); }, + [&] (stmt_t::immutable_t const& immutable) + { + auto new_body = rewrite(from, to, immutable.body); + if (new_body) + result.emplace( + old.properties, + stmt_t::immutable_t{ + immutable.vars, + std::move(*new_body) + }); + }, [&] (stmt_t::if_else_t const& if_else) { auto new_cond = rewrite(from, to, if_else.cond); diff --git a/dep0/lib/03_typecheck/src/substitute.cpp b/dep0/lib/03_typecheck/src/substitute.cpp index f3ddb301..c2aa5150 100644 --- a/dep0/lib/03_typecheck/src/substitute.cpp +++ b/dep0/lib/03_typecheck/src/substitute.cpp @@ -35,6 +35,11 @@ void substitute(expr_t::var_t const& var, expr_t const& expr, body_t& body) substitute(var, expr, assign.lhs); substitute(var, expr, assign.rhs); }, + [&] (stmt_t::immutable_t& immutable) + { + immutable.vars.erase(var); + substitute(var, expr, immutable.body); + }, [&] (stmt_t::if_else_t& if_) { substitute(var, expr, if_.cond); diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index 14ecc48a..f2f872e0 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -96,6 +96,59 @@ BOOST_AUTO_TEST_CASE(pass_004) { BOOST_TEST(pass("0024_mutability/pass_004.depc" BOOST_AUTO_TEST_CASE(pass_005) { BOOST_TEST(pass("0024_mutability/pass_005.depc")); } BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc")); } +BOOST_AUTO_TEST_CASE(pass_007) +{ + BOOST_TEST(pass("0024_mutability/pass_007.depc")); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 3ul); + { + auto const f = std::get_if(&pass_result->entries[0]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f0"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_scope, "a", dep0::ast::qty_t::zero)); + BOOST_TEST(is_arg(f->value.args[1], ref_of(is_i32, var("a")), "p")); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST(is_return_of(f->value.body.stmts[0ul], deref(var("p")))); + } + { + auto const f = std::get_if(&pass_result->entries[1]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f1"); + BOOST_TEST_REQUIRE(f->value.args.size() == 1ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x"}, + std::tuple{ + return_of(app_of(global("f0"), scopeof("x"), addressof("x"))) + })); + } + { + auto const f = std::get_if(&pass_result->entries[2]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f2"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x", "y"}, + std::tuple{ + return_of( + plus( + app_of(global("f0"), scopeof("x"), addressof("x")), + app_of(global("f0"), scopeof("y"), addressof("y")))) + })); + } +} + BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_001) { BOOST_TEST(fail("0024_mutability/typecheck_error_001.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_002) { BOOST_TEST(fail("0024_mutability/typecheck_error_002.depc")); } @@ -111,5 +164,8 @@ BOOST_AUTO_TEST_CASE(typecheck_error_011) { BOOST_TEST(fail("0024_mutability/typ BOOST_AUTO_TEST_CASE(typecheck_error_012) { BOOST_TEST(fail("0024_mutability/typecheck_error_012.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_013) { BOOST_TEST(fail("0024_mutability/typecheck_error_013.depc")); } BOOST_AUTO_TEST_CASE(typecheck_error_014) { BOOST_TEST(fail("0024_mutability/typecheck_error_014.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_015) { BOOST_TEST(fail("0024_mutability/typecheck_error_015.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_016) { BOOST_TEST(fail("0024_mutability/typecheck_error_016.depc")); } +BOOST_AUTO_TEST_CASE(typecheck_error_017) { BOOST_TEST(fail("0024_mutability/typecheck_error_017.depc")); } BOOST_AUTO_TEST_SUITE_END() diff --git a/dep0/lib/03_typecheck/test/typecheck_tests_fixture.hpp b/dep0/lib/03_typecheck/test/typecheck_tests_fixture.hpp index 4e6ab96c..7572360c 100644 --- a/dep0/lib/03_typecheck/test/typecheck_tests_fixture.hpp +++ b/dep0/lib/03_typecheck/test/typecheck_tests_fixture.hpp @@ -77,6 +77,12 @@ struct TypecheckTestsFixture return dep0::testing::init_list_of(std::forward(args)...); } + template + static constexpr auto assign_of(Args&&... args) + { + return dep0::testing::assign_of(std::forward(args)...); + } + template static constexpr auto if_else_of(Args&&... args) { diff --git a/dep0/lib/05_llvmgen/src/gen_body.cpp b/dep0/lib/05_llvmgen/src/gen_body.cpp index cf14caa8..2f119659 100644 --- a/dep0/lib/05_llvmgen/src/gen_body.cpp +++ b/dep0/lib/05_llvmgen/src/gen_body.cpp @@ -349,6 +349,10 @@ void gen_stmt( { assert(false and "assignment not implemented yet"); }, + [] (typecheck::stmt_t::immutable_t const&) + { + assert(false and "immutable block not implemented yet"); + }, [&] (typecheck::stmt_t::if_else_t const& x) { // Let's eliminate impossible branches. diff --git a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp index 23590dea..bf04f39b 100644 --- a/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp +++ b/dep0/lib/99_testing/include/dep0/testing/ast_predicates/stmt.hpp @@ -6,6 +6,8 @@ */ #pragma once +#include "dep0/testing/ast_predicates/details/check_name.hpp" + #include "dep0/testing/ast_predicates/app.hpp" #include "dep0/testing/failure.hpp" @@ -72,6 +74,23 @@ constexpr auto assign_of(F_lhs&& f_lhs, F_rhs&& f_rhs) }; } +template >... F_body> +boost::test_tools::predicate_result +is_immutable_block(ast::stmt_t

const& stmt, std::set vars, std::tuple f_body) +{ + auto const immutable = std::get_if::immutable_t>(&stmt.value); + if (not immutable) + return failure("statement is not an immutable block but ", pretty_name(stmt.value)); + if (immutable->vars.size() != vars.size()) + return failure("vars of immutable block have different length: ", immutable->vars.size(), " != ", vars.size()); + for (std::string_view const v: vars) + if (std::ranges::none_of(immutable->vars, [v] (auto const& var) { return details::check_name

(var, v); })) + return failure("immutable block does not declare variable `", v, '`'); + if (auto const result = check_body(immutable->body, std::move(f_body)); not result) + return failure("inside immutable block: ", result.message()); + return true; +} + template < ast::Properties P, Predicate> F_cond, diff --git a/testfiles/0024_mutability/pass_007.depc b/testfiles/0024_mutability/pass_007.depc new file mode 100644 index 00000000..a261a8ce --- /dev/null +++ b/testfiles/0024_mutability/pass_007.depc @@ -0,0 +1,25 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f0(0 scope_t a, ref_t(i32_t, a) p) -> i32_t +{ + return *p; +} + +func f1(i32_t mutable x) -> i32_t +{ + immutable(x) + { + return f0(scopeof(x), &x); + } +} +func f2(i32_t mutable x, i32_t mutable y) -> i32_t +{ + immutable(x, y) + { + return f0(scopeof(x), &x) + f0(scopeof(y), &y); + } +} diff --git a/testfiles/0024_mutability/typecheck_error_015.depc b/testfiles/0024_mutability/typecheck_error_015.depc new file mode 100644 index 00000000..4d31f4ec --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_015.depc @@ -0,0 +1,13 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f() -> i32_t +{ + immutable(x) // typecheck error: unknown variable x + { + return 0; + } +} diff --git a/testfiles/0024_mutability/typecheck_error_016.depc b/testfiles/0024_mutability/typecheck_error_016.depc new file mode 100644 index 00000000..df7144e8 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_016.depc @@ -0,0 +1,14 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t +{ + immutable(x) + { + p = &x; // typecheck error: the scope of the immutable x is smaller than the mutable one + } + return *p; // otherwise `p` could now refer to the mutable `x`, which must not be allowed +} diff --git a/testfiles/0024_mutability/typecheck_error_017.depc b/testfiles/0024_mutability/typecheck_error_017.depc new file mode 100644 index 00000000..380bac90 --- /dev/null +++ b/testfiles/0024_mutability/typecheck_error_017.depc @@ -0,0 +1,14 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +func f(u64_t mutable i, array_t(i32_t, 2) mutable xs, 0 true_t(i < 2)) -> i32_t +{ + immutable(xs) + { + i = 7; + } + return xs[i]; // typecheck error: i has now been mutated +} From 0c89983a995e33f3dc473db8f3883585ec2b9177 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sat, 6 Dec 2025 18:20:31 +0000 Subject: [PATCH 08/11] self review --- .../03_typecheck/include/dep0/typecheck/context.hpp | 12 ++++++------ dep0/lib/03_typecheck/src/check.cpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp index 92e8e4f8..bc20a19c 100644 --- a/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/context.hpp @@ -70,12 +70,12 @@ class ctx_t { } public: - std::optional origin; /**< Where in the source code the variable declaration was encountered. */ - std::optional scope_id; /**< Scope ID of the originating context or empty if unscoped. */ - expr_t::var_t var; /**< Copy of the variable to which this declaration was bound, eg `x` in `0 i32_t x`. */ - ast::qty_t qty; /**< Quantity of the variable declaration, eg `0` in `0 i32_t x`. */ - ast::is_mutable_t is_mutable; /**< Whether the variable was declared mutable or not. */ - expr_t type; /**< Type of the variable declaration, eg `i32_t` in `0 i32_t x`. */ + std::optional origin; /**< Where in the source code the variable declaration was found. */ + std::optional scope_id; /**< Scope ID of the originating context or empty if unscoped. */ + expr_t::var_t var; /**< Copy of the variable to which this declaration was bound. */ + ast::qty_t qty; /**< Quantity of the variable declaration, eg `0` in `0 i32_t x`. */ + ast::is_mutable_t is_mutable; /**< Whether the variable was declared mutable or not. */ + expr_t type; /**< Type of the variable declaration, eg `i32_t` in `0 i32_t x`. */ }; /** @brief The default context is unscoped, which reduces the risk of compiler bugs when typechecking references. */ diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index dce7258b..da90c884 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -434,7 +434,7 @@ check_stmt( auto const decl = state.context[*root]; assert(decl and "root of assignment was a variable but it did not exist in current context"); if (decl->is_mutable == ast::is_mutable_t::no) - return error_t("cannot mutate immutable variable"); + return error_t("cannot mutate immutable variable", loc); log.mutations.insert_or_assign(*root, decl->type); return make_legal_stmt(stmt_t::assign_t{std::move(*lhs), std::move(*rhs)}); }, From d5c8eb8f30292e05b450edc1e11c10f9f6b7a3cc Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sun, 18 Jan 2026 19:51:42 +0000 Subject: [PATCH 09/11] llvgen --- .../dep0_parser_tests_0024_mutability.cpp | 34 +++- dep0/lib/03_typecheck/CMakeLists.txt | 2 + .../include/dep0/typecheck/ast.hpp | 1 + .../include/dep0/typecheck/context_ref.hpp | 1 + .../include/dep0/typecheck/derivation.hpp | 30 ++-- .../include/dep0/typecheck/location_map.hpp | 30 ++++ dep0/lib/03_typecheck/src/check.cpp | 144 +++++++++-------- dep0/lib/03_typecheck/src/context_ref.cpp | 2 + dep0/lib/03_typecheck/src/delta_unfold.cpp | 6 +- dep0/lib/03_typecheck/src/location_map.cpp | 36 +++++ dep0/lib/03_typecheck/src/private/check.hpp | 21 +-- .../src/private/derivation_rules.hpp | 14 +- dep0/lib/03_typecheck/src/type_assign.cpp | 4 +- .../dep0_typecheck_tests_0024_mutability.cpp | 34 +++- dep0/lib/05_llvmgen/CMakeLists.txt | 4 + dep0/lib/05_llvmgen/src/context.cpp | 2 + dep0/lib/05_llvmgen/src/gen_address.cpp | 108 +++++++++++++ dep0/lib/05_llvmgen/src/gen_body.cpp | 125 ++++++++++++--- dep0/lib/05_llvmgen/src/gen_func.cpp | 89 +++++++++-- dep0/lib/05_llvmgen/src/gen_val.cpp | 147 ++++++------------ dep0/lib/05_llvmgen/src/private/context.hpp | 33 ++-- .../05_llvmgen/src/private/gen_address.hpp | 37 +++++ .../lib/05_llvmgen/src/private/gen_alloca.hpp | 3 - dep0/lib/05_llvmgen/src/private/gen_body.hpp | 31 ++-- dep0/lib/05_llvmgen/src/private/value.hpp | 39 +++++ dep0/lib/05_llvmgen/src/value.cpp | 18 +++ dep0/lib/05_llvmgen/test/CMakeLists.txt | 1 + .../dep0_llvmgen_tests_0024_mutability.cpp | 29 ++++ testfiles/0024_mutability/pass_004.depc | 1 + testfiles/0024_mutability/pass_007.depc | 18 +++ 30 files changed, 764 insertions(+), 280 deletions(-) create mode 100644 dep0/lib/03_typecheck/include/dep0/typecheck/location_map.hpp create mode 100644 dep0/lib/03_typecheck/src/location_map.cpp create mode 100644 dep0/lib/05_llvmgen/src/gen_address.cpp create mode 100644 dep0/lib/05_llvmgen/src/private/gen_address.hpp create mode 100644 dep0/lib/05_llvmgen/src/private/value.hpp create mode 100644 dep0/lib/05_llvmgen/src/value.cpp create mode 100644 dep0/lib/05_llvmgen/test/dep0_llvmgen_tests_0024_mutability.cpp diff --git a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp index a7637a2d..1a7e3ed3 100644 --- a/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp +++ b/dep0/lib/02_parser/test/dep0_parser_tests_0024_mutability.cpp @@ -118,7 +118,7 @@ BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc" BOOST_AUTO_TEST_CASE(pass_007) { BOOST_TEST(pass("0024_mutability/pass_007.depc")); - BOOST_TEST_REQUIRE(pass_result->entries.size() == 3ul); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 5ul); { auto const f = std::get_if(&pass_result->entries[0]); BOOST_TEST_REQUIRE(f); @@ -166,6 +166,38 @@ BOOST_AUTO_TEST_CASE(pass_007) app_of(var("f0"), scopeof("y"), addressof("y")))) })); } + { + auto const f = std::get_if(&pass_result->entries[3]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f3"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::no)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x", "y"}, + std::tuple{ + return_of( + plus( + app_of(var("f0"), scopeof("x"), addressof("x")), + var("y"))) + })); + } + { + auto const f = std::get_if(&pass_result->entries[4]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f4"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::no)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_immutable_block(f->value.body.stmts[0ul], {"y"}, std::tuple{assign_of(var("x"), constant(10))})); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } } BOOST_AUTO_TEST_CASE(typecheck_error_000) diff --git a/dep0/lib/03_typecheck/CMakeLists.txt b/dep0/lib/03_typecheck/CMakeLists.txt index 17bd3b60..fb40cd17 100644 --- a/dep0/lib/03_typecheck/CMakeLists.txt +++ b/dep0/lib/03_typecheck/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(dep0_typecheck_lib include/dep0/typecheck/is_impossible.hpp include/dep0/typecheck/is_mutable.hpp include/dep0/typecheck/list_initialization.hpp + include/dep0/typecheck/location_map.hpp include/dep0/typecheck/subscript_access.hpp # private headers src/private/beta_delta_equivalence.hpp @@ -75,6 +76,7 @@ add_library(dep0_typecheck_lib src/is_mutable.cpp src/is_terminator.cpp src/list_initialization.cpp + src/location_map.cpp src/max_scope.cpp src/prelude.cpp src/proof_search.cpp diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/ast.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/ast.hpp index 665ef183..4f6bb1b7 100644 --- a/dep0/lib/03_typecheck/include/dep0/typecheck/ast.hpp +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/ast.hpp @@ -139,3 +139,4 @@ std::ostream& pretty_print(std::ostream&, sort_t const&, std::size_t indent = 0u // we also need to include it here in order to make its destructor visible, // otherwise `boost::recursive_wrapper` does not compile #include "dep0/typecheck/environment.hpp" +#include "dep0/typecheck/location_map.hpp" diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/context_ref.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/context_ref.hpp index bd9f51ea..66db3b08 100644 --- a/dep0/lib/03_typecheck/include/dep0/typecheck/context_ref.hpp +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/context_ref.hpp @@ -29,6 +29,7 @@ class ctx_ref_t public: explicit ctx_ref_t(ctx_t const&); + explicit ctx_ref_t(ctx_t&&); ctx_t const& operator*() const { return *ctx; } ctx_t const* operator->() const { return ctx.get(); } diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/derivation.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/derivation.hpp index d5780e48..57d4a114 100644 --- a/dep0/lib/03_typecheck/include/dep0/typecheck/derivation.hpp +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/derivation.hpp @@ -18,6 +18,8 @@ namespace dep0::typecheck { +struct location_map_t; + template struct derivation_properties_t { }; @@ -29,6 +31,20 @@ struct derivation_properties_t> ctx_ref_t ctx; }; +template <> +struct derivation_properties_t> +{ + std::optional next_ctx; + boost::recursive_wrapper location_map; +}; + +template <> +struct derivation_properties_t> +{ + std::optional next_ctx; + boost::recursive_wrapper location_map; +}; + /** * @brief Proof that an AST node is legal because it has a valid derivation. * @@ -38,17 +54,11 @@ struct derivation_properties_t> * It can only be constructed using `derivation_rules`, which is private within the typecheck module; * therefore typechecking is the only way to construct a legal AST. * - * @remarks Currently this contains no fields so the only thing that it is proving is that - * the AST node was constructed via `derivation_rules`. - * Ideally each specialization should contain the real proof of why the node was legal but - * that requires dependent types in C++. - * It might still be useful to add some fields in some cases, but currently there are none. - * * @warning This type is copiable, so one could forge a derivation by copying from another one. * But we are trying to "guard against Murphy, not Machiavelli". */ template -struct derivation_t +struct derivation_t : derivation_properties_t { derivation_t(derivation_t const&) = default; derivation_t& operator=(derivation_t const&) = default; @@ -57,11 +67,11 @@ struct derivation_t bool operator==(derivation_t const&) const = default; - derivation_properties_t properties; - private: friend struct derivation_rules; - derivation_t(derivation_properties_t properties) : properties(std::move(properties)) {} + derivation_t(derivation_properties_t properties) + : derivation_properties_t(std::move(properties)) + { } }; } // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/include/dep0/typecheck/location_map.hpp b/dep0/lib/03_typecheck/include/dep0/typecheck/location_map.hpp new file mode 100644 index 00000000..86d074c0 --- /dev/null +++ b/dep0/lib/03_typecheck/include/dep0/typecheck/location_map.hpp @@ -0,0 +1,30 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +/** + * @file + * @brief Contains the definition of `dep0::typecheck::location_map_t`. + */ +#pragma once + +#include "dep0/typecheck/ast.hpp" + +#include "dep0/error.hpp" + +#include + +namespace dep0::typecheck { + +/** @brief Helper type to keep track of which variables refer to the same location as other variables. */ +struct location_map_t +{ + std::map map; + + /** @brief Constructs a new map formed by the union of the two input maps. */ + static expected combine(location_map_t const&, location_map_t const&); +}; + +} // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index da90c884..adc2fb17 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -20,6 +20,7 @@ #include "dep0/typecheck/beta_delta_reduction.hpp" #include "dep0/typecheck/list_initialization.hpp" +#include "dep0/typecheck/location_map.hpp" #include "dep0/typecheck/subscript_access.hpp" #include "dep0/ast/find_member_field.hpp" @@ -76,19 +77,6 @@ static bool has_attribute(func_decl_t const& x, std::string_view const attribute return x.attribute and x.attribute->value == attribute; } -expected mutation_log_t::combine(mutation_log_t&& a, mutation_log_t&& b) -{ - for (auto& [v, t]: b.mutations) - if (auto const [it, inserted] = a.mutations.try_emplace(v, t); not inserted) - if (auto eq = is_beta_delta_equivalent(it->second, t); not eq) - { - std::ostringstream err; - pretty_print(err << "found mutation of different types for variable `", v) << '`'; - return error_t(err.str(), std::vector{std::move(eq.error())}); - } - return std::move(a); -} - expected check(env_t const& base_env, parser::module_t const& x) noexcept { auto env = base_env.extend(); @@ -292,7 +280,7 @@ expected check_type(env_t const& env, ctx_t const& ctx, parser::expr_t c : std::vector{std::move(as_type.error()), std::move(as_kind.error())}); } -expected> +expected check_body( env_t const& env, proof_state_t state, @@ -301,7 +289,6 @@ check_body( usage_t& usage, ast::qty_t const usage_multiplier) { - mutation_log_t log; auto stmts = fmap_or_error( x.stmts, @@ -310,30 +297,21 @@ check_body( auto stmt = check_stmt(env, state, s, is_mutable, usage, usage_multiplier); if (not stmt) return stmt.error(); - std::optional new_ctx; - auto const loc = s.properties; - for (auto& [var, type]: stmt->second.mutations) - { - if (auto const decl = state.context[var]) - { - if (not new_ctx) - new_ctx.emplace(state.context.extend()); - auto const v = new_ctx->try_emplace(var.name, loc, decl->qty, ast::is_mutable_t::yes, type); - if (not v) - return v.error(); // should not happen, but just in case - log.mutations.insert_or_assign(std::move(var), std::move(type)); - } - } - if (new_ctx) - state.context = std::move(*new_ctx); - return std::move(stmt->first); + if (stmt->properties.derivation.next_ctx) + state.context = std::move(stmt->properties.derivation.next_ctx->operator->()->extend()); + return stmt; }); if (not stmts) return std::move(stmts.error()); - return std::pair{make_legal_body(std::move(*stmts)), std::move(log)}; + if (stmts->empty()) + return make_legal_body(std::nullopt, location_map_t{}, std::move(*stmts)); + auto next_ctx = stmts->back().properties.derivation.next_ctx; + // TODO this should be the union of all maps + auto location_map = stmts->back().properties.derivation.location_map.get(); + return make_legal_body(std::move(next_ctx), std::move(location_map), std::move(*stmts)); } -expected> +expected check_stmt( env_t const& env, proof_state_t& state, @@ -343,13 +321,12 @@ check_stmt( ast::qty_t const usage_multiplier) { auto const loc = s.properties; - mutation_log_t log; - auto stmt = match( + return match( s.value, [&] (parser::expr_t::app_t const& x) -> expected { if (auto app = type_assign_app(env, state.context, x, loc, is_mutable, usage, usage_multiplier)) - return make_legal_stmt(std::move(std::get(app->value))); + return make_legal_stmt(std::nullopt, location_map_t{}, std::move(std::get(app->value))); else return std::move(app.error()); }, @@ -435,13 +412,31 @@ check_stmt( assert(decl and "root of assignment was a variable but it did not exist in current context"); if (decl->is_mutable == ast::is_mutable_t::no) return error_t("cannot mutate immutable variable", loc); - log.mutations.insert_or_assign(*root, decl->type); - return make_legal_stmt(stmt_t::assign_t{std::move(*lhs), std::move(*rhs)}); + + auto next_ctx = state.context.extend(); + auto new_var = + next_ctx.try_emplace(root->name, x.lhs.properties, decl->qty, ast::is_mutable_t::yes, decl->type); + if (not new_var) + return std::move(new_var.error()); // should not happen, but whatever + return make_legal_stmt( + ctx_ref_t(std::move(next_ctx)), + location_map_t{{{*new_var, *root}}}, + stmt_t::assign_t{ + std::move(*lhs), + std::move(*rhs) + }); }, [&] (parser::stmt_t::immutable_t const& x) -> expected { auto new_state = proof_state_t(state.context.extend(), state.goal); std::set vars; + // This is a bit of a hack, hence the name. + // Variables declared in an immutable block refer to variables in the parent scope. + // We need a way to tell the llvmgen stage what is the memory location that these new variables refer to. + // A location map is exactly what we need and we can exploit the fact that all new variables introduced + // by mutations will have a different name from the ones introduced by the immutable block. + // So we can just combine a hacky location map with the real one obtained by typechecking the body. + location_map_t hacky_map; for (auto const& v: x.vars) { auto const d = state.context[v.name]; @@ -455,13 +450,24 @@ check_stmt( auto var = new_state.context.try_emplace(v.name, d->origin, d->qty, ast::is_mutable_t::no, d->type); if (not var) // should not happen but whatever return var.error(); + bool const inserted = hacky_map.map.try_emplace(*var, d->var).second; + assert(inserted and "variable already present in immutable block"); vars.insert(std::move(*var)); } auto body = check_body(env, std::move(new_state), x.body, is_mutable, usage, usage_multiplier); if (not body) return body.error(); - log = std::move(body->second); - return make_legal_stmt(stmt_t::immutable_t{std::move(vars), std::move(body->first)}); + auto next_ctx = body->properties.derivation.next_ctx; + auto location_map = location_map_t::combine(hacky_map, body->properties.derivation.location_map.get()); + if (not location_map) + return location_map.error(); + return make_legal_stmt( + std::move(next_ctx), + std::move(*location_map), + stmt_t::immutable_t{ + std::move(vars), + std::move(*body) + }); }, [&] (parser::stmt_t::if_else_t const& x) -> expected { @@ -521,19 +527,34 @@ check_stmt( { if (auto ok = combine_usages(); not ok) return std::move(ok.error()); - // combine mutations from both branches and add the result to the current context - auto new_log = - mutation_log_t::combine( - std::move(true_branch->second), - std::move(false_branch->second)); - if (not new_log) - return std::move(new_log.error()); - log = std::move(*new_log); + auto location_map = + location_map_t::combine( + true_branch->properties.derivation.location_map.get(), + false_branch->properties.derivation.location_map.get()); + if (not location_map) + { + location_map.error().location = loc; + return std::move(location_map.error()); + } + std::optional next_ctx; + for (auto const& [new_var, old_var]: location_map->map) + if (auto const decl = state.context[old_var]) + { + if (not next_ctx) + next_ctx.emplace(state.context.extend()); + auto const ok = + next_ctx->try_emplace( + new_var.name, loc, decl->qty, ast::is_mutable_t::yes, decl->type); + if (not ok) + return std::move(ok.error()); // should not happen but whatever + } return make_legal_stmt( + next_ctx ? std::optional{ctx_ref_t(std::move(*next_ctx))} : std::nullopt, + std::move(*location_map), stmt_t::if_else_t{ std::move(*cond), - std::move(true_branch->first), - std::move(false_branch->first) + std::move(*true_branch), + std::move(*false_branch) }); } else @@ -543,7 +564,7 @@ check_stmt( // but if the true branch returns from all its sub-branches, // then it means we are now in the implied else branch; // we can then rewrite `cond = false` inside the current proof state - if (returns_from_all_branches(true_branch->first)) + if (returns_from_all_branches(*true_branch)) { state.rewrite(*cond, derivation_rules::make_false(env, state.context)); add_true_not_cond(state.context); @@ -553,14 +574,14 @@ check_stmt( // TODO currently mutations cannot change type, but when they can do, this code might be incorrect. // Because if the true-branch of an if-without-else has changed the type of a variable of the parent scope, // the type of that variable after the if-statement is potentially unspecified. - // Maybe a solution is to synthesize a log for the missing else-branch where each variable - // mutated by the true-branch is added to the synthetic log with the original type - // and then run the same combining logic as-if the else-branch was present. - log = std::move(true_branch->second); + auto next_ctx = true_branch->properties.derivation.next_ctx; + auto location_map = true_branch->properties.derivation.location_map.get(); return make_legal_stmt( + std::move(next_ctx), + std::move(location_map), stmt_t::if_else_t{ std::move(*cond), - std::move(true_branch->first), + std::move(*true_branch), std::nullopt }); }, @@ -569,7 +590,7 @@ check_stmt( if (not x.expr) { if (is_beta_delta_equivalent(state.goal, derivation_rules::make_unit(env, state.context))) - return make_legal_stmt(stmt_t::return_t{}); + return make_legal_stmt(std::nullopt, location_map_t{}, stmt_t::return_t{}); else { std::ostringstream err; @@ -578,7 +599,7 @@ check_stmt( } } else if (auto expr = check_expr(env, state.context, *x.expr, state.goal, is_mutable, usage, usage_multiplier)) - return make_legal_stmt(stmt_t::return_t{std::move(*expr)}); + return make_legal_stmt(std::nullopt, location_map_t{}, stmt_t::return_t{std::move(*expr)}); else return std::move(expr.error()); }, @@ -590,7 +611,7 @@ check_stmt( sort_t{derivation_rules::make_true_t(env, ctx, derivation_rules::make_false(env, ctx))}; for (ctx_t::decl_t const& decl: ctx.decls()) if (is_beta_delta_equivalent(decl.type, false_type)) - return make_legal_stmt(stmt_t::impossible_t{std::move(reason)}); + return make_legal_stmt(std::nullopt, location_map_t{}, stmt_t::impossible_t{std::move(reason)}); return error_t("proof of false not found", loc); }; if (x.reason) @@ -607,9 +628,6 @@ check_stmt( else return check_impossible_stmt(state.context, std::nullopt); }); - if (not stmt) - return std::move(stmt.error()); - return std::pair{std::move(*stmt), std::move(log)}; } expected diff --git a/dep0/lib/03_typecheck/src/context_ref.cpp b/dep0/lib/03_typecheck/src/context_ref.cpp index 738615c0..78867f30 100644 --- a/dep0/lib/03_typecheck/src/context_ref.cpp +++ b/dep0/lib/03_typecheck/src/context_ref.cpp @@ -12,4 +12,6 @@ namespace dep0::typecheck { ctx_ref_t::ctx_ref_t(ctx_t const& ctx) : ctx(std::make_shared(std::move(ctx.extend()))) {} +ctx_ref_t::ctx_ref_t(ctx_t&& ctx) : ctx(std::make_shared(std::move(ctx))) {} + } // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/src/delta_unfold.cpp b/dep0/lib/03_typecheck/src/delta_unfold.cpp index 996b1d8d..cc5392bb 100644 --- a/dep0/lib/03_typecheck/src/delta_unfold.cpp +++ b/dep0/lib/03_typecheck/src/delta_unfold.cpp @@ -197,7 +197,7 @@ bool delta_unfold(expr_t::app_t& app) // Inside f3, first f1 is unfolded, which yields `return f2(0);` and then // f2 can also be unfolded, but note that the original `g(0)` // was type-checked in an environment where `f2` did not exist yet! - auto const& env = *app.func.get().properties.derivation.properties.env; + auto const& env = *app.func.get().properties.derivation.env; if (auto const func_def = std::get_if(env[*global])) { app.func.get().value = func_def->value; @@ -521,7 +521,7 @@ bool delta_unfold(expr_t& expr) auto const& ty = std::get(x.lhs.get().properties.sort.get()); expr.value = expr_t::numeric_constant_t{ impl::reduce( - *expr.properties.derivation.properties.env, + *expr.properties.derivation.env, hana::type_c, n->value, m->value, @@ -559,7 +559,7 @@ bool delta_unfold(expr_t& expr) if (auto const init_list = std::get_if(&member.object.get().value)) if (auto const type = std::get_if(&member.object.get().properties.sort.get())) if (auto const g = std::get_if(&type->value)) - if (auto const type_def = std::get_if((*expr.properties.derivation.properties.env)[*g])) + if (auto const type_def = std::get_if((*expr.properties.derivation.env)[*g])) if (auto const s = std::get_if(&type_def->value)) if (auto const i = ast::find_member_index(member.field, *s)) { diff --git a/dep0/lib/03_typecheck/src/location_map.cpp b/dep0/lib/03_typecheck/src/location_map.cpp new file mode 100644 index 00000000..94d87e73 --- /dev/null +++ b/dep0/lib/03_typecheck/src/location_map.cpp @@ -0,0 +1,36 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#include "dep0/typecheck/location_map.hpp" + +#include "dep0/ast/pretty_print.hpp" + +#include +#include + +namespace dep0::typecheck { +expected location_map_t::combine(location_map_t const& a, location_map_t const& b) +{ + auto result = a; + std::vector reasons; + for (auto const& [v1, v2]: b.map) + { + auto const [it, inserted] = result.map.try_emplace(v1, v2); + if (not inserted and it->second != v2) + { + std::ostringstream err; + pretty_print(err << '`', v1) << " ` canot refer both to "; + pretty_print(err << '`', it->second) << "` and "; + pretty_print(err << '`', v2) << '`'; + reasons.push_back(error_t(err.str())); + } + } + if (not reasons.empty()) + return error_t("failed to combine location maps", std::move(reasons)); + return std::move(result); +} + +} // namespace dep0::typecheck diff --git a/dep0/lib/03_typecheck/src/private/check.hpp b/dep0/lib/03_typecheck/src/private/check.hpp index 6f529934..9c3cbfae 100644 --- a/dep0/lib/03_typecheck/src/private/check.hpp +++ b/dep0/lib/03_typecheck/src/private/check.hpp @@ -27,23 +27,6 @@ namespace dep0::typecheck { -/** @brief Helper type to keep track of which variables have been mutated by a body or statement. */ -struct mutation_log_t -{ - std::map mutations; /**< Store the new type of a variable after a mutation.*/ - - /** - * @brief Constructs a new log formed by the union of the two input logs. - * @remarks Currently this expects that, if a variable appears in both input logs, it has the same type. - * If not, this function will return an error. - * This is only a temporary limitation which will need to be removed. - * If a variable has been mutated to different types by different branches, - * the combined log will need to somehow contain the mutated variable but - * it is currently unclear what its type should be. - */ - static expected combine(mutation_log_t&&, mutation_log_t&&); -}; - /** * @brief Checks whether a type definition is legal; * if it is, the type is stored in the given environment. @@ -101,7 +84,7 @@ expected check_func_def(env_t&, parser::func_def_t const&); * * @return A legal body or an error. */ -expected> +expected check_body( env_t const&, proof_state_t, @@ -125,7 +108,7 @@ check_body( * * @return A legal statement or an error. */ -expected> +expected check_stmt( env_t const&, proof_state_t&, diff --git a/dep0/lib/03_typecheck/src/private/derivation_rules.hpp b/dep0/lib/03_typecheck/src/private/derivation_rules.hpp index 0f0fb4c9..8d893026 100644 --- a/dep0/lib/03_typecheck/src/private/derivation_rules.hpp +++ b/dep0/lib/03_typecheck/src/private/derivation_rules.hpp @@ -184,15 +184,21 @@ func_arg_t make_legal_func_arg(Args&&... args) } template -body_t make_legal_body(Args&&... args) +body_t make_legal_body(std::optional next_ctx, location_map_t location_map, Args&&... args) { - return body_t{derivation_rules::make_derivation(), std::forward(args)...}; + return body_t{ + derivation_rules::make_derivation(std::move(next_ctx), std::move(location_map)), + std::forward(args)... + }; } template -stmt_t make_legal_stmt(Args&&... args) +stmt_t make_legal_stmt(std::optional next_ctx, location_map_t location_map, Args&&... args) { - return stmt_t{derivation_rules::make_derivation(), std::forward(args)...}; + return stmt_t{ + derivation_rules::make_derivation(std::move(next_ctx), std::move(location_map)), + std::forward(args)... + }; } template diff --git a/dep0/lib/03_typecheck/src/type_assign.cpp b/dep0/lib/03_typecheck/src/type_assign.cpp index 7fd08bd8..740db5f0 100644 --- a/dep0/lib/03_typecheck/src/type_assign.cpp +++ b/dep0/lib/03_typecheck/src/type_assign.cpp @@ -752,7 +752,7 @@ expected type_assign_abs( return std::move(body.error()); // so far so good, but we now need to make sure that all branches contain a return statement, // with the only exception of functions returning `unit_t` because the return statement is optional; - if (not returns_from_all_branches(body->first)) + if (not returns_from_all_branches(*body)) { if (not is_beta_delta_equivalent(ret_type.get(), derivation_rules::make_unit(env, ctx))) { @@ -770,7 +770,7 @@ expected type_assign_abs( is_mutable, std::move(arg_types), std::move(ret_type), - std::move(body->first) + std::move(*body) }); } diff --git a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp index f2f872e0..6269ca35 100644 --- a/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp +++ b/dep0/lib/03_typecheck/test/dep0_typecheck_tests_0024_mutability.cpp @@ -99,7 +99,7 @@ BOOST_AUTO_TEST_CASE(pass_006) { BOOST_TEST(pass("0024_mutability/pass_006.depc" BOOST_AUTO_TEST_CASE(pass_007) { BOOST_TEST(pass("0024_mutability/pass_007.depc")); - BOOST_TEST_REQUIRE(pass_result->entries.size() == 3ul); + BOOST_TEST_REQUIRE(pass_result->entries.size() == 5ul); { auto const f = std::get_if(&pass_result->entries[0]); BOOST_TEST_REQUIRE(f); @@ -147,6 +147,38 @@ BOOST_AUTO_TEST_CASE(pass_007) app_of(global("f0"), scopeof("y"), addressof("y")))) })); } + { + auto const f = std::get_if(&pass_result->entries[3]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f3"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::no)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 1ul); + BOOST_TEST( + is_immutable_block( + f->value.body.stmts[0ul], + {"x", "y"}, + std::tuple{ + return_of( + plus( + app_of(global("f0"), scopeof("x"), addressof("x")), + var("y"))) + })); + } + { + auto const f = std::get_if(&pass_result->entries[4]); + BOOST_TEST_REQUIRE(f); + BOOST_TEST(f->name == "f4"); + BOOST_TEST_REQUIRE(f->value.args.size() == 2ul); + BOOST_TEST(is_arg(f->value.args[0], is_i32, "x", dep0::ast::is_mutable_t::yes)); + BOOST_TEST(is_arg(f->value.args[1], is_i32, "y", dep0::ast::is_mutable_t::no)); + BOOST_TEST(is_i32(f->value.ret_type.get())); + BOOST_TEST_REQUIRE(f->value.body.stmts.size() == 2ul); + BOOST_TEST(is_immutable_block(f->value.body.stmts[0ul], {"y"}, std::tuple{assign_of(var("x"), constant(10))})); + BOOST_TEST(is_return_of(f->value.body.stmts[1ul], var("x"))); + } } BOOST_AUTO_TEST_CASE(typecheck_error_000) { BOOST_TEST(fail("0024_mutability/typecheck_error_000.depc")); } diff --git a/dep0/lib/05_llvmgen/CMakeLists.txt b/dep0/lib/05_llvmgen/CMakeLists.txt index 8205811e..4044cd83 100644 --- a/dep0/lib/05_llvmgen/CMakeLists.txt +++ b/dep0/lib/05_llvmgen/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(dep0_llvmgen_lib src/private/context.hpp src/private/first_order_types.hpp src/private/gen_alloca.hpp + src/private/gen_address.hpp src/private/gen_array.hpp src/private/gen_attrs.hpp src/private/gen_body.hpp @@ -23,10 +24,12 @@ add_library(dep0_llvmgen_lib src/private/gen_val.hpp src/private/llvm_func.hpp src/private/proto.hpp + src/private/value.hpp # src files src/context.cpp src/first_order_types.cpp src/gen.cpp + src/gen_address.cpp src/gen_alloca.cpp src/gen_array.cpp src/gen_attrs.cpp @@ -37,6 +40,7 @@ add_library(dep0_llvmgen_lib src/gen_type.cpp src/gen_val.cpp src/llvm_func.cpp + src/value.cpp src/proto.cpp ) add_library(DepC::Dep0::LLVMGen ALIAS dep0_llvmgen_lib) diff --git a/dep0/lib/05_llvmgen/src/context.cpp b/dep0/lib/05_llvmgen/src/context.cpp index d5cc6cdf..c638a657 100644 --- a/dep0/lib/05_llvmgen/src/context.cpp +++ b/dep0/lib/05_llvmgen/src/context.cpp @@ -145,6 +145,8 @@ llvm::Value* local_ctx_t::load_address(typecheck::expr_t::var_t const& k) const void local_ctx_t::save_address(typecheck::expr_t::var_t const& k, llvm::Value* const address) { + auto const p = entries[k]; + assert(p and "you must save a value before its address"); if (auto const p = entries[k]) p->address = address; } diff --git a/dep0/lib/05_llvmgen/src/gen_address.cpp b/dep0/lib/05_llvmgen/src/gen_address.cpp new file mode 100644 index 00000000..d71583ed --- /dev/null +++ b/dep0/lib/05_llvmgen/src/gen_address.cpp @@ -0,0 +1,108 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#include "private/gen_address.hpp" + +#include "private/gen_array.hpp" +#include "private/gen_type.hpp" +#include "private/gen_val.hpp" + +#include "dep0/typecheck/subscript_access.hpp" + +#include "dep0/ast/find_member_field.hpp" + +namespace dep0::llvmgen { + +namespace impl { + +static llvm::Value* +gen_array_element_address( + global_ctx_t& global, + local_ctx_t& local, + llvm::IRBuilder<>& builder, + typecheck::expr_t::subscript_t const& subscript) +{ + auto const& array = subscript.object.get(); + auto const properties = get_array_properties(std::get(array.properties.sort.get())); + auto const stride_size = gen_stride_size_if_needed(global, local, builder, properties); + auto const element_type = gen_type(global, properties.element_type); + auto const base = gen_temporary_val(global, local, builder, array); + auto const index_val = gen_temporary_val(global, local, builder, subscript.index.get()); + auto const offset = stride_size ? builder.CreateMul(stride_size, index_val) : index_val; + return builder.CreateGEP(element_type, base, offset); +} + +static llvm::Value* +gen_tuple_element_address( + global_ctx_t& global, + local_ctx_t& local, + llvm::IRBuilder<>& builder, + typecheck::expr_t::subscript_t const& subscript, + llvm::Type* const tuple_type) +{ + auto const base = gen_temporary_val(global, local, builder, subscript.object.get()); + auto const index_const = std::get_if(&subscript.index.get().value); + assert(index_const and "subscript operand on tuples must be a numeric literal"); + auto const int32 = llvm::Type::getInt32Ty(global.llvm_ctx); + auto const zero = llvm::ConstantInt::get(int32, 0); + auto const i = index_const->value.convert_to(); + auto const index_val = llvm::ConstantInt::get(int32, i); + return builder.CreateGEP(tuple_type, base, {zero, index_val}); +} + +} // namespace impl + +llvm::Value* +gen_address( + global_ctx_t& global, + local_ctx_t& local, + llvm::IRBuilder<>& builder, + typecheck::expr_t::subscript_t const& subscript) +{ + auto const& object_type = std::get(subscript.object.get().properties.sort.get()); + return match( + typecheck::has_subscript_access(object_type), + [] (typecheck::has_subscript_access_result::no_t) -> llvm::Value* + { + assert(false and "unexpected subscript expression; typechecking must be broken"); + __builtin_unreachable(); + }, + [&] (typecheck::has_subscript_access_result::sigma_t const& sigma) -> llvm::Value* + { + auto const tuple_type = gen_type(global, object_type); + return impl::gen_tuple_element_address(global, local, builder, subscript, tuple_type); + }, + [&] (typecheck::has_subscript_access_result::array_t const&) -> llvm::Value* + { + return impl::gen_array_element_address(global, local, builder, subscript); + }); +} + +std::pair +gen_field_address( + global_ctx_t& global, + local_ctx_t& local, + llvm::IRBuilder<>& builder, + typecheck::expr_t::member_t const& member) +{ + auto const& object_type = std::get(member.object.get().properties.sort.get()); + auto const& g = std::get(object_type.value); + auto const type_def = std::get_if(global[g]); + assert(type_def and "only global structs can have member access"); + auto const& s = std::get(type_def->def.value); + auto const i = ast::find_member_index(member.field, s); + assert(i.has_value()); + auto const struct_type = gen_type(global, object_type); + auto const base = gen_temporary_val(global, local, builder, member.object.get()); + auto const int32 = llvm::Type::getInt32Ty(global.llvm_ctx); + auto const zero = llvm::ConstantInt::get(int32, 0); + auto const index = llvm::ConstantInt::get(int32, *i); + auto const ptr = builder.CreateGEP(struct_type, base, {zero, index}); + auto const element_type = gen_type(global, s.fields[*i].type); + return {ptr, element_type}; +} + +} // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/src/gen_body.cpp b/dep0/lib/05_llvmgen/src/gen_body.cpp index 2f119659..c1c9e27f 100644 --- a/dep0/lib/05_llvmgen/src/gen_body.cpp +++ b/dep0/lib/05_llvmgen/src/gen_body.cpp @@ -6,6 +6,7 @@ */ #include "private/gen_body.hpp" +#include "private/gen_address.hpp" #include "private/gen_array.hpp" #include "private/gen_loop.hpp" #include "private/gen_type.hpp" @@ -15,6 +16,8 @@ #include "dep0/typecheck/is_impossible.hpp" #include "dep0/typecheck/is_mutable.hpp" +#include "dep0/ast/mutable_place_expression.hpp" + #include "dep0/match.hpp" #include @@ -66,30 +69,29 @@ static void gen_destructor_call(global_ctx_t&, local_ctx_t&, llvm::IRBuilder<>&, * store/memcpy/memset the resulting LLVM value at the runtime location * pointed by this LLVM value, which must be of pointer type. */ -static void gen_stmt( +static local_ctx_t +gen_stmt( global_ctx_t&, local_ctx_t&, - snippet_t& current_snippet, llvm::IRBuilder<>& builder, typecheck::stmt_t const&, + snippet_t& current_snippet, llvm::Function* parent_function, std::optional); static void gen_stmts( global_ctx_t& global, local_ctx_t& local, - snippet_t& snippet, llvm::IRBuilder<>& builder, typecheck::body_t const& body, + snippet_t& snippet, llvm::Function* const llvm_f, std::optional const inlined_result) { auto it = body.stmts.begin(); - if (it == body.stmts.end()) - snippet.open_blocks.push_back(snippet.entry_block); - else + if (it != body.stmts.end()) { - gen_stmt(global, local, snippet, builder, *it, llvm_f, inlined_result); + auto new_ctx = gen_stmt(global, local, builder, *it, snippet, llvm_f, inlined_result); while (++it != body.stmts.end()) { if (snippet.open_blocks.size()) @@ -98,7 +100,7 @@ static void gen_stmts( snippet.seal_open_blocks(builder, [next] (auto& builder) { builder.CreateBr(next); }); builder.SetInsertPoint(next); } - gen_stmt(global, local, snippet, builder, *it, llvm_f, inlined_result); + new_ctx = gen_stmt(global, new_ctx, builder, *it, snippet, llvm_f, inlined_result); } } } @@ -170,11 +172,17 @@ static llvm_func_t gen_destructor(global_ctx_t& global, typecheck::expr_t const& }; auto struct_ctx = local.extend(); for (auto const i: std::views::iota(0ul, s.fields.size())) + { + auto const is_pass_by_val = llvmgen::is_pass_by_val(global, s.fields[i].type); struct_ctx.try_emplace( s.fields[i].var, - is_boxed(s.fields[i].type) or is_pass_by_val(global, s.fields[i].type) - ? builder.CreateLoad(gen_type(global, s.fields[i].type), gep(i)) - : gep(i)); + std::in_place_type, + is_boxed(s.fields[i].type) or is_pass_by_val + ? builder.CreateLoad(gen_type(global, s.fields[i].type), gep(i)) + : gep(i), + ast::is_mutable_t::no, + not is_pass_by_val); + } for (auto const i: std::views::reverse(std::views::iota(0ul, s.fields.size()))) { auto const& ty = s.fields[i].type; @@ -224,11 +232,17 @@ static llvm_func_t gen_destructor(global_ctx_t& global, typecheck::expr_t const& auto sigma_ctx = local.extend(); for (auto const i: std::views::iota(0ul, x.args.size())) if (x.args[i].var) + { + auto const pass_by_val = is_pass_by_val(global, x.args[i].type); sigma_ctx.try_emplace( *x.args[i].var, - is_boxed(x.args[i].type) or is_pass_by_val(global, x.args[i].type) - ? builder.CreateLoad(gen_type(global, x.args[i].type), gep(i)) - : gep(i)); + std::in_place_type, + is_boxed(x.args[i].type) or pass_by_val + ? builder.CreateLoad(gen_type(global, x.args[i].type), gep(i)) + : gep(i), + ast::is_mutable_t::no, + not pass_by_val); + } for (auto const i: std::views::reverse(std::views::iota(0ul, x.args.size()))) { if (is_boxed(x.args[i].type)) @@ -307,6 +321,20 @@ static void gen_destructors(global_ctx_t& global, local_ctx_t& local, llvm::IRBu } } +void snippet_t::seal_open_blocks(llvm::IRBuilder<>& builder, std::function&)> f) +{ + for (auto& bb: open_blocks) + { + if (not bb->getTerminator()) + { + builder.SetInsertPoint(bb); + f(builder); + assert(bb->getTerminator() and "callback did not set a terminator"); + } + } + open_blocks.clear(); +} + snippet_t gen_body( global_ctx_t& global, local_ctx_t const& local, @@ -324,19 +352,22 @@ snippet_t gen_body( return snippet; } auto local_body = local.extend(); - gen_stmts(global, local_body, snippet, builder, body, llvm_f, inlined_result); + gen_stmts(global, local_body, builder, body, snippet, llvm_f, inlined_result); + if (not snippet.entry_block->getTerminator()) + snippet.open_blocks.push_back(snippet.entry_block); return snippet; } -void gen_stmt( +local_ctx_t gen_stmt( global_ctx_t& global, local_ctx_t& local, - snippet_t& snippet, llvm::IRBuilder<>& builder, typecheck::stmt_t const& stmt, + snippet_t& snippet, llvm::Function* const llvm_f, std::optional const inlined_result) { + auto new_ctx = local.extend(); auto local_stmt = local.extend(); match( stmt.value, @@ -345,16 +376,63 @@ void gen_stmt( gen_func_call(global, local_stmt, builder, x, value_category_t::temporary, nullptr); gen_destructors(global, local_stmt, builder); }, - [] (typecheck::stmt_t::assign_t const&) + [&] (typecheck::stmt_t::assign_t const& assign) { - assert(false and "assignment not implemented yet"); + assert(stmt.properties.derivation.location_map.get().map.size() == 1ul); + auto const& [new_var, old_var] = *stmt.properties.derivation.location_map.get().map.begin(); + auto const [old_val, old_address] = std::pair{local[old_var], local.load_address(old_var)}; + assert(old_val); + assert(old_address); + new_ctx.try_emplace(new_var, *old_val); + new_ctx.save_address(new_var, old_address); + match( + ast::is_mutable_place_expression(assign.lhs), + [] (ast::immutable_place_t) + { + assert(false and "unexpected assignment statement; typechecking must be broken"); + __builtin_unreachable(); + }, + [&] (typecheck::expr_t::var_t const& var) + { + gen_val(global, local_stmt, builder, assign.rhs, value_category_t::result, old_address); + }, + [&] (typecheck::expr_t::member_t const& x) + { + auto const dest = gen_field_address(global, local_stmt, builder, x).first; + gen_val(global, local_stmt, builder, assign.rhs, value_category_t::result, dest); + }, + [&] (typecheck::expr_t::subscript_t const& x) + { + auto const dest = gen_address(global, local_stmt, builder, x); + gen_val(global, local_stmt, builder, assign.rhs, value_category_t::result, dest); + }); }, - [] (typecheck::stmt_t::immutable_t const&) + [&] (typecheck::stmt_t::immutable_t const& x) { - assert(false and "immutable block not implemented yet"); + auto const& location_map = stmt.properties.derivation.location_map.get().map; + for (auto const& [new_var, old_var]: stmt.properties.derivation.location_map.get().map) + { + auto const [old_val, old_address] = std::pair{local[old_var], local.load_address(old_var)}; + // Any `new_var` was introduced either by the immutable block or by a mutation inside the body; + // this is what determines which scope it should be added to. + // This is because of the hacky location map described in `check.cpp`. + auto const it = x.vars.find(new_var); + auto& relevant_scope = it != x.vars.end() ? local_stmt : new_ctx; + relevant_scope.try_emplace(new_var, *old_val); + relevant_scope.save_address(new_var, old_address); + } + gen_stmts(global, local_stmt, builder, x.body, snippet, llvm_f, inlined_result); }, [&] (typecheck::stmt_t::if_else_t const& x) { + for (auto const& [new_var, old_var]: stmt.properties.derivation.location_map.get().map) + { + auto const [old_val, old_address] = std::pair{local[old_var], local.load_address(old_var)}; + assert(old_val); + assert(old_address); + new_ctx.try_emplace(new_var, *old_val); + new_ctx.save_address(new_var, old_address); + } // Let's eliminate impossible branches. // Note that at least one branch must be possible. // Because, if both branches were impossible, `gen_body` would emit `unreachable` and we wouldn't be here. @@ -368,7 +446,7 @@ void gen_stmt( gen_destructors(global, local_stmt, builder); } if (x.false_branch) - gen_stmts(global, local_stmt, snippet, builder, *x.false_branch, llvm_f, inlined_result); + gen_stmts(global, local_stmt, builder, *x.false_branch, snippet, llvm_f, inlined_result); return; } if (x.false_branch and is_impossible(*x.false_branch)) @@ -378,7 +456,7 @@ void gen_stmt( gen_temporary_val(global, local_stmt, builder, x.cond); gen_destructors(global, local_stmt, builder); } - gen_stmts(global, local_stmt, snippet, builder, x.true_branch, llvm_f, inlined_result); + gen_stmts(global, local_stmt, builder, x.true_branch, snippet, llvm_f, inlined_result); return; } // Otherwise both branches are possible and we need to emit each one in their own basic block. @@ -440,6 +518,7 @@ void gen_stmt( { builder.CreateUnreachable(); }); + return new_ctx; } } // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/src/gen_func.cpp b/dep0/lib/05_llvmgen/src/gen_func.cpp index 3d726da5..e66a511d 100644 --- a/dep0/lib/05_llvmgen/src/gen_func.cpp +++ b/dep0/lib/05_llvmgen/src/gen_func.cpp @@ -25,7 +25,7 @@ static void gen_func_args(global_ctx_t&, local_ctx_t&, llvm_func_proto_t const&, static void gen_func_attributes(global_ctx_t const&, llvm_func_proto_t const&, llvm::Function*); static void gen_func_body( global_ctx_t&, - local_ctx_t const&, + local_ctx_t&, llvm_func_proto_t const&, typecheck::body_t const&, llvm::Function*); @@ -69,18 +69,32 @@ void gen_func_args( { if (arg.var->idx == 0ul) llvm_arg.setName(arg.var->name.view()); - bool inserted = false; - if (auto const pi = std::get_if(&arg.type.value)) + // For immutable arguments, we store their value in the local context. + // Mutable arguments are instead handled in `gen_func_body` because we have to make a local copy. + if (arg.is_mutable == ast::is_mutable_t::no) { - assert(llvm_arg.getType()->isPointerTy()); - auto const proto = llvm_func_proto_t::from_pi(*pi); - assert(proto.has_value() and "function arguments must be of the 1st order"); - auto const function_type = gen_func_type(global, *proto); - inserted = local.try_emplace(*arg.var, llvm_func_t(function_type, &llvm_arg)).second; + if (auto const pi = std::get_if(&arg.type.value)) + { + assert(llvm_arg.getType()->isPointerTy()); + auto const proto = llvm_func_proto_t::from_pi(*pi); + assert(proto.has_value() and "function arguments must be of the 1st order"); + auto const function_type = gen_func_type(global, *proto); + bool const inserted = local.try_emplace(*arg.var, llvm_func_t(function_type, &llvm_arg)).second; + assert(inserted); + } + else + { + bool const inserted = + local.try_emplace( + *arg.var, + std::in_place_type, + &llvm_arg, + ast::is_mutable_t::no, + is_pass_by_ptr(global, arg.type) + ).second; + assert(inserted); + } } - else - inserted = local.try_emplace(*arg.var, &llvm_arg).second; - assert(inserted); } } } @@ -93,18 +107,63 @@ void gen_func_attributes(global_ctx_t const& global, llvm_func_proto_t const& pr void gen_func_body( global_ctx_t& global, - local_ctx_t const& local, + local_ctx_t& local, llvm_func_proto_t const& proto, typecheck::body_t const& body, llvm::Function* const llvm_f) { - auto snippet = gen_body(global, local, body, "entry", llvm_f, std::nullopt); - if (snippet.open_blocks.size() and std::holds_alternative(proto.ret_type().value)) + // If we have mutable arguments, first make a local copy of the incoming value and store the address of the copy. + // Note that it is physically impossible to mutate arguments that have no name, so those are ignored. + std::optional>> mutable_block; + auto constexpr yes = ast::is_mutable_t::yes; + if (std::ranges::any_of(proto.runtime_args(), [] (auto const& x) { return x.var and x.is_mutable == yes; })) + { + auto& [snippet, builder] = + mutable_block.emplace( + std::piecewise_construct, + std::forward_as_tuple(), + std::forward_as_tuple(global.llvm_ctx)); + snippet.entry_block = llvm::BasicBlock::Create(global.llvm_ctx, "mutables", llvm_f); + builder.SetInsertPoint(snippet.entry_block); + auto llvm_arg_it = llvm_f->arg_begin(); + if (is_pass_by_ptr(global, proto.ret_type())) + ++llvm_arg_it; // skip sret argument + for (auto const& arg: proto.runtime_args()) + { + auto& llvm_arg = *llvm_arg_it++; + if (arg.var and arg.is_mutable == yes) + { + auto const address = gen_alloca(global, local, builder, allocator_t::stack, arg.type); + gen_store(global, local, builder, value_category_t::temporary, &llvm_arg, address, arg.type); + bool const inserted = + local.try_emplace( + *arg.var, + std::in_place_type, + address, + ast::is_mutable_t::yes, + is_pass_by_ptr(global, arg.type) + ).second; + local.save_address(*arg.var, address); + assert(inserted); + } + } + // Note that at this point "mutables" is missing a terminator. + // We'll add a jump to "entry" once it has been generated. + snippet.open_blocks.push_back(snippet.entry_block); + } + // Here we generate the actual body. + auto entry = gen_body(global, local, body, "entry", llvm_f, std::nullopt); + if (entry.open_blocks.size() and std::holds_alternative(proto.ret_type().value)) { auto builder = llvm::IRBuilder<>(global.llvm_ctx); // Having open blocks means that the function has no return statement, // this implies its return type is `unit_t`, so just return `i8 0`. - snippet.seal_open_blocks(builder, [unit=gen_val_unit(global)] (auto& builder) { builder.CreateRet(unit); }); + entry.seal_open_blocks(builder, [unit=gen_val_unit(global)] (auto& builder) { builder.CreateRet(unit); }); + } + if (mutable_block) + { + auto& [snippet, builder] = *mutable_block; + snippet.seal_open_blocks(builder, [&] (auto& builder) { builder.CreateBr(entry.entry_block); }); } finalize_llvm_func(llvm_f); } diff --git a/dep0/lib/05_llvmgen/src/gen_val.cpp b/dep0/lib/05_llvmgen/src/gen_val.cpp index 6fdb52a0..7d6a06be 100644 --- a/dep0/lib/05_llvmgen/src/gen_val.cpp +++ b/dep0/lib/05_llvmgen/src/gen_val.cpp @@ -7,6 +7,7 @@ #include "private/gen_val.hpp" #include "private/first_order_types.hpp" +#include "private/gen_address.hpp" #include "private/gen_alloca.hpp" #include "private/gen_array.hpp" #include "private/gen_attrs.hpp" @@ -17,7 +18,6 @@ #include "private/gen_type.hpp" #include "private/proto.hpp" -#include "dep0/ast/find_member_field.hpp" #include "dep0/ast/place_expression.hpp" #include "dep0/ast/views.hpp" #include "dep0/typecheck/list_initialization.hpp" @@ -127,63 +127,6 @@ llvm::Value* gen_val(llvm::IntegerType* const type, boost::multiprecision::cpp_i return llvm::ConstantInt::get(type, x.str(), 10); } -static llvm::Value* gen_tuple_element_address( - global_ctx_t& global, - local_ctx_t& local, - llvm::IRBuilder<>& builder, - typecheck::expr_t::subscript_t const& subscript, - llvm::Type* const tuple_type) -{ - auto const base = gen_temporary_val(global, local, builder, subscript.object.get()); - auto const index_const = std::get_if(&subscript.index.get().value); - assert(index_const and "subscript operand on tuples must be a numeric literal"); - auto const int32 = llvm::Type::getInt32Ty(global.llvm_ctx); - auto const zero = llvm::ConstantInt::get(int32, 0); - auto const i = index_const->value.convert_to(); - auto const index_val = llvm::ConstantInt::get(int32, i); - return builder.CreateGEP(tuple_type, base, {zero, index_val}); -} - -static llvm::Value* gen_array_element_address( - global_ctx_t& global, - local_ctx_t& local, - llvm::IRBuilder<>& builder, - typecheck::expr_t::subscript_t const& subscript) -{ - auto const& array = subscript.object.get(); - auto const properties = get_array_properties(std::get(array.properties.sort.get())); - auto const stride_size = gen_stride_size_if_needed(global, local, builder, properties); - auto const element_type = gen_type(global, properties.element_type); - auto const base = gen_temporary_val(global, local, builder, array); - auto const index_val = gen_temporary_val(global, local, builder, subscript.index.get()); - auto const offset = stride_size ? builder.CreateMul(stride_size, index_val) : index_val; - return builder.CreateGEP(element_type, base, offset); -} - -static std::pair -gen_field_address( - global_ctx_t& global, - local_ctx_t& local, - llvm::IRBuilder<>& builder, - typecheck::expr_t::member_t const& member) -{ - auto const& object_type = std::get(member.object.get().properties.sort.get()); - auto const& g = std::get(object_type.value); - auto const type_def = std::get_if(global[g]); - assert(type_def and "only global structs can have member access"); - auto const& s = std::get(type_def->def.value); - auto const i = ast::find_member_index(member.field, s); - assert(i.has_value()); - auto const struct_type = gen_type(global, object_type); - auto const base = gen_temporary_val(global, local, builder, member.object.get()); - auto const int32 = llvm::Type::getInt32Ty(global.llvm_ctx); - auto const zero = llvm::ConstantInt::get(int32, 0); - auto const index = llvm::ConstantInt::get(int32, *i); - auto const ptr = builder.CreateGEP(struct_type, base, {zero, index}); - auto const element_type = gen_type(global, s.fields[*i].type); - return {ptr, element_type}; -} - llvm::Value* gen_val( global_ctx_t& global, local_ctx_t& local, @@ -399,7 +342,10 @@ llvm::Value* gen_val( assert(val and "unknown variable"); return maybe_store(match( *val, - [] (llvm::Value* const p) { return p; }, + [&] (value_t const& v) + { + return v.is_mutable() and v.is_pass_by_val() ? builder.CreateLoad(gen_type(global, type), v) : v; + }, [] (llvm_func_t const& c) { return c.func; })); }, [&] (typecheck::expr_t::global_t const& g) -> llvm::Value* @@ -516,7 +462,7 @@ llvm::Value* gen_val( { return match( ast::is_place_expression(x.expr.get()), - [&] (std::reference_wrapper const var) + [&] (typecheck::expr_t::var_t const& var) { auto const view = ast::get_if_ref(type); assert(view and "type of addressof is not a reference"); @@ -528,7 +474,7 @@ llvm::Value* gen_val( { // currently only arrays are boxed, so a variable must refer to an llvm::Value; // in particular, it cannot refer to an llvm_func_t - auto const val = std::get_if(local[var.get()]); + auto const val = std::get_if(local[var]); assert(val and "variable not found or not an llvm value"); address = builder.CreateAlloca(gen_type(global, el_type)); builder.CreateStore(*val, address); @@ -538,6 +484,7 @@ llvm::Value* gen_val( auto const value = gen_temporary_val(global, local, builder, x.expr.get()); if (is_pass_by_val(global, el_type)) { + // TODO isn't this the same as above, i.e. CreateAlloca()+CreateStore()? address = gen_alloca(global, local, builder, allocator_t::stack, el_type); gen_store(global, local, builder, value_category_t::temporary, value, address, el_type); } @@ -548,37 +495,18 @@ llvm::Value* gen_val( } return maybe_store(address); }, - [&] (std::reference_wrapper const deref) + [&] (typecheck::expr_t::deref_t const& deref) { - return gen_val(global, local, builder, deref.get().expr.get(), value_category, dest); + return gen_val(global, local, builder, deref.expr.get(), value_category, dest); }, - [&] (std::reference_wrapper const member) + [&] (typecheck::expr_t::member_t const& member) { - auto const ptr = gen_field_address(global, local, builder, member.get()).first; + auto const ptr = gen_field_address(global, local, builder, member).first; return maybe_store(ptr); }, - [&] (std::reference_wrapper const x) -> llvm::Value* + [&] (typecheck::expr_t::subscript_t const& x) -> llvm::Value* { - auto const& subscript = x.get(); - auto const& object_type = std::get(subscript.object.get().properties.sort.get()); - return match( - typecheck::has_subscript_access(object_type), - [] (typecheck::has_subscript_access_result::no_t) -> llvm::Value* - { - assert(false and "unexpected subscript expression; typechecking must be broken"); - __builtin_unreachable(); - }, - [&] (typecheck::has_subscript_access_result::sigma_t const& sigma) -> llvm::Value* - { - auto const tuple_type = gen_type(global, object_type); - auto const ptr = gen_tuple_element_address(global, local, builder, subscript, tuple_type); - return maybe_store(ptr); - }, - [&] (typecheck::has_subscript_access_result::array_t const&) -> llvm::Value* - { - auto const ptr = gen_array_element_address(global, local, builder, subscript); - return maybe_store(ptr); - }); + return gen_address(global, local, builder, x); }, [&] (ast::value_expression_t) { @@ -704,17 +632,19 @@ llvm::Value* gen_val( { auto const index = llvm::ConstantInt::get(int32, i); auto const element_ptr = builder.CreateGEP(llvm_type, dest2, {zero, index}); + bool const pass_by_ptr = is_pass_by_ptr(global, s.fields[i].type); + auto const immutable = ast::is_mutable_t::no; if (is_boxed(s.fields[i].type)) { auto const p = gen_alloca(global, ctx, builder, allocator, s.fields[i].type); gen_val(global, ctx, builder, x.values[i], value_category, p); builder.CreateStore(p, element_ptr); - ctx.try_emplace(s.fields[i].var, p); + ctx.try_emplace(s.fields[i].var, std::in_place_type, p, immutable, pass_by_ptr); } else { auto const val = gen_val(global, ctx, builder, x.values[i], value_category, element_ptr); - ctx.try_emplace(s.fields[i].var, val); + ctx.try_emplace(s.fields[i].var, std::in_place_type, val, immutable, pass_by_ptr); } } std::ranges::copy(ctx.destructors, std::back_inserter(local.destructors)); @@ -733,19 +663,23 @@ llvm::Value* gen_val( { auto const index = llvm::ConstantInt::get(int32, i); auto const element_ptr = builder.CreateGEP(llvm_type, dest2, {zero, index}); + bool const pass_by_ptr = is_pass_by_ptr(global, sigma.args[i].type); + auto const immutable = ast::is_mutable_t::no; if (is_boxed(sigma.args[i].type)) { auto const p = gen_alloca(global, sigma_ctx, builder, allocator, sigma.args[i].type); gen_val(global, sigma_ctx, builder, x.values[i], value_category, p); builder.CreateStore(p, element_ptr); if (sigma.args[i].var) - sigma_ctx.try_emplace(*sigma.args[i].var, p); + sigma_ctx.try_emplace( + *sigma.args[i].var, std::in_place_type, p, immutable, pass_by_ptr); } else { auto const val = gen_val(global, sigma_ctx, builder, x.values[i], value_category, element_ptr); if (sigma.args[i].var) - sigma_ctx.try_emplace(*sigma.args[i].var, val); + sigma_ctx.try_emplace( + *sigma.args[i].var, std::in_place_type, val, immutable, pass_by_ptr); } } std::ranges::copy(sigma_ctx.destructors, std::back_inserter(local.destructors)); @@ -792,8 +726,7 @@ llvm::Value* gen_val( }, [&] (typecheck::has_subscript_access_result::sigma_t const& sigma) -> llvm::Value* { - auto const tuple_type = gen_type(global, object_type); - auto const ptr = gen_tuple_element_address(global, local, builder, subscript, tuple_type); + auto const ptr = gen_address(global, local, builder, subscript); auto const index = std::get_if(&subscript.index.get().value); auto const i = index->value.convert_to(); auto const element_type = gen_type(global, sigma.args[i].type); @@ -804,8 +737,8 @@ llvm::Value* gen_val( }, [&] (typecheck::has_subscript_access_result::array_t const&) -> llvm::Value* { - auto const ptr = gen_array_element_address(global, local, builder, subscript); - auto const properties = get_array_properties(std::get(subscript.object.get().properties.sort.get())); + auto const ptr = gen_address(global, local, builder, subscript); + auto const properties = get_array_properties(object_type); auto const element_type = gen_type(global, properties.element_type); return maybe_store(is_pass_by_ptr(global, type) ? ptr : builder.CreateLoad(element_type, ptr)); }); @@ -852,11 +785,13 @@ void gen_store( return builder.CreateGEP(llvm_type, p, {zero, builder.getInt32(i)}); }; auto struct_ctx = local.extend(); + auto const immutable = ast::is_mutable_t::no; for (auto const i: std::views::iota(0ul, s.fields.size())) { auto const& element_type = s.fields[i].type; auto const element_ptr = gep(value, i); auto const dest_element_ptr = gep(dest, i); + bool const pass_by_ptr = is_pass_by_ptr(global, element_type); if (is_boxed(element_type)) { auto const allocator = select_allocator(value_category); @@ -864,18 +799,21 @@ void gen_store( builder.CreateStore(alloca, dest_element_ptr); auto const element_value = builder.CreateLoad(gen_type(global, element_type), element_ptr); gen_store(global, struct_ctx, builder, value_category, element_value, alloca, element_type); - struct_ctx.try_emplace(s.fields[i].var, alloca); + struct_ctx.try_emplace( + s.fields[i].var, std::in_place_type, alloca, immutable, pass_by_ptr); } - else if (is_pass_by_ptr(global, element_type)) + else if (pass_by_ptr) { gen_store(global, struct_ctx, builder, value_category, element_ptr, dest_element_ptr, element_type); - struct_ctx.try_emplace(s.fields[i].var, dest_element_ptr); + struct_ctx.try_emplace( + s.fields[i].var, std::in_place_type, dest_element_ptr, immutable, pass_by_ptr); } else { auto const element_value = builder.CreateLoad(gen_type(global, element_type), element_ptr); builder.CreateStore(element_value, dest_element_ptr); - struct_ctx.try_emplace(s.fields[i].var, element_value); + struct_ctx.try_emplace( + s.fields[i].var, std::in_place_type, element_value, immutable, pass_by_ptr); } } std::ranges::copy(struct_ctx.destructors, std::back_inserter(local.destructors)); @@ -906,6 +844,8 @@ void gen_store( auto const& element_type = sigma.args[i].type; auto const element_ptr = gep(value, i); auto const dest_element_ptr = gep(dest, i); + bool const pass_by_ptr = is_pass_by_ptr(global, element_type); + auto const immutable = ast::is_mutable_t::no; if (is_boxed(element_type)) { auto const allocator = select_allocator(value_category); @@ -914,20 +854,23 @@ void gen_store( auto const element_value = builder.CreateLoad(gen_type(global, element_type), element_ptr); gen_store(global, sigma_ctx, builder, value_category, element_value, alloca, element_type); if (sigma.args[i].var) - sigma_ctx.try_emplace(*sigma.args[i].var, alloca); + sigma_ctx.try_emplace( + *sigma.args[i].var, std::in_place_type, alloca, immutable, pass_by_ptr); } - else if (is_pass_by_ptr(global, element_type)) + else if (pass_by_ptr) { gen_store(global, sigma_ctx, builder, value_category, element_ptr, dest_element_ptr, element_type); if (sigma.args[i].var) - sigma_ctx.try_emplace(*sigma.args[i].var, dest_element_ptr); + sigma_ctx.try_emplace( + *sigma.args[i].var, std::in_place_type, dest_element_ptr, immutable, pass_by_ptr); } else { auto const element_value = builder.CreateLoad(gen_type(global, element_type), element_ptr); builder.CreateStore(element_value, dest_element_ptr); if (sigma.args[i].var) - sigma_ctx.try_emplace(*sigma.args[i].var, element_value); + sigma_ctx.try_emplace( + *sigma.args[i].var, std::in_place_type, element_value, immutable, pass_by_ptr); } } std::ranges::copy(sigma_ctx.destructors, std::back_inserter(local.destructors)); diff --git a/dep0/lib/05_llvmgen/src/private/context.hpp b/dep0/lib/05_llvmgen/src/private/context.hpp index a4fb19c5..b1b32f6b 100644 --- a/dep0/lib/05_llvmgen/src/private/context.hpp +++ b/dep0/lib/05_llvmgen/src/private/context.hpp @@ -11,6 +11,7 @@ #pragma once #include "private/llvm_func.hpp" +#include "private/value.hpp" #include "dep0/typecheck/ast.hpp" #include "dep0/typecheck/environment.hpp" @@ -149,15 +150,25 @@ struct global_ctx_t * * It also stores the values that need to be destructed before leaving the scope associated to this object. */ -struct local_ctx_t +class local_ctx_t { - using value_t = - std::variant< - llvm::Value*, - llvm_func_t - >; +public: + using value_t = std::variant; +private: + struct entry_t + { + value_t value; + llvm::Value* address = nullptr; + }; + scope_map entries; + + explicit local_ctx_t(scope_map); + +public: local_ctx_t() = default; + local_ctx_t(local_ctx_t&&) = default; + local_ctx_t& operator=(local_ctx_t&&) = default; #ifndef NDEBUG ~local_ctx_t() { assert(destructors.empty() and "local context would leak resources"); } @@ -191,16 +202,6 @@ struct local_ctx_t * If values need to be destroyed in reverse order it is the user responsibility to do so. */ std::vector> destructors; - -private: - struct entry_t - { - value_t value; - llvm::Value* address = nullptr; - }; - scope_map entries; - - explicit local_ctx_t(scope_map); }; } // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/src/private/gen_address.hpp b/dep0/lib/05_llvmgen/src/private/gen_address.hpp new file mode 100644 index 00000000..541c653b --- /dev/null +++ b/dep0/lib/05_llvmgen/src/private/gen_address.hpp @@ -0,0 +1,37 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +/** + * @file + * @brief Contains the declaration of `gen_address()`. + */ +#pragma once + +#include "private/context.hpp" + +#include "dep0/typecheck/ast.hpp" + +#include +#include + +namespace dep0::llvmgen { + +/** @brief Genreates the runtime memory address of the given subscript exprresion. */ +llvm::Value* +gen_address( + global_ctx_t&, + local_ctx_t&, + llvm::IRBuilder<>&, + typecheck::expr_t::subscript_t const&); + +std::pair +gen_field_address( + global_ctx_t&, + local_ctx_t&, + llvm::IRBuilder<>&, + typecheck::expr_t::member_t const&); + +} // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/src/private/gen_alloca.hpp b/dep0/lib/05_llvmgen/src/private/gen_alloca.hpp index 14113510..81db0c02 100644 --- a/dep0/lib/05_llvmgen/src/private/gen_alloca.hpp +++ b/dep0/lib/05_llvmgen/src/private/gen_alloca.hpp @@ -14,12 +14,9 @@ #include "dep0/typecheck/ast.hpp" -#include #include #include -#include - namespace dep0::llvmgen { /** diff --git a/dep0/lib/05_llvmgen/src/private/gen_body.hpp b/dep0/lib/05_llvmgen/src/private/gen_body.hpp index eb4a33b2..f543c875 100644 --- a/dep0/lib/05_llvmgen/src/private/gen_body.hpp +++ b/dep0/lib/05_llvmgen/src/private/gen_body.hpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -35,9 +36,9 @@ namespace dep0::llvmgen { * If the snippet is comprised of more basic blocks they will be linked * to one another and somehow reachable from the entry block. * - * It may also contain open blocks, i.e. blocks currently without a terminator. - * It is the caller responsibility to terminate all open blocks as necessary. - * Once an open block is terminated, it is removed from the list of open blocks. + * It may also contain currently/previously open blocks, i.e. blocks without a terminator. + * This allows the user to keep track of blocks that at some point were open and + * conveniently close them all by invoking `seal_open_blocks()`. */ struct snippet_t { @@ -45,27 +46,21 @@ struct snippet_t std::vector open_blocks; /** - * @brief Iterate over all currently open blocks and invoke the given function to close them all. + * @brief Iterate over all `open_blocks` that are still open and invoke the given function to close them all. + * + * The invoked function **must** produce a terminator using the supplied IRBuilder, + * for example to emit an unconditional jump to a continuation block. + * If `open_blocks` contains blocks that already have a terminator, they will be ignored and removed. + * Once this method returns, `open_blocks` will be empty. * * @param builder - * This builder will be passed to the callback function; - * before every function call, its insert point will be set to the next open block. + * This builder will be passed to the callback function and + * its insert point will be set to the next open block before every invocation of the callback function. * * @param f * This function will be called on each open block and must emit a terminator instruction. */ - template - void seal_open_blocks(llvm::IRBuilder<>& builder, F&& f) - { - for (auto& bb: open_blocks) - { - assert(not bb->getTerminator()); - builder.SetInsertPoint(bb); - f(builder); - assert(bb->getTerminator()); - } - open_blocks.clear(); - } + void seal_open_blocks(llvm::IRBuilder<>& builder, std::function&)>); }; /** diff --git a/dep0/lib/05_llvmgen/src/private/value.hpp b/dep0/lib/05_llvmgen/src/private/value.hpp new file mode 100644 index 00000000..36a53fdb --- /dev/null +++ b/dep0/lib/05_llvmgen/src/private/value.hpp @@ -0,0 +1,39 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +/** + * @file + * @brief Contains the definition of `dep0::llvmgen::value_t`. + */ +#pragma once + +#include + +#include "dep0/ast/mutable.hpp" + +namespace dep0::llvmgen { + +/** TODO */ +class value_t +{ + llvm::Value* m_value; + bool m_is_mutable; + bool m_is_pass_by_ptr; + +public: + value_t(llvm::Value*, ast::is_mutable_t, bool is_pass_by_ptr); + + value_t(value_t const&) = default; + value_t(value_t&&) = default; + + operator llvm::Value*() const { return m_value; } + + bool is_mutable() const { return m_is_mutable; } + bool is_pass_by_ptr() const { return m_is_pass_by_ptr; } + bool is_pass_by_val() const { return not m_is_pass_by_ptr; } +}; + +} // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/src/value.cpp b/dep0/lib/05_llvmgen/src/value.cpp new file mode 100644 index 00000000..fe68d022 --- /dev/null +++ b/dep0/lib/05_llvmgen/src/value.cpp @@ -0,0 +1,18 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#include "private/value.hpp" + +namespace dep0::llvmgen { + +value_t::value_t(llvm::Value* const value, ast::is_mutable_t const is_mutable, bool const is_pass_by_ptr) : + m_value(value), + m_is_mutable(is_mutable == ast::is_mutable_t::yes), + m_is_pass_by_ptr(is_pass_by_ptr) +{ +} + +} // namespace dep0::llvmgen diff --git a/dep0/lib/05_llvmgen/test/CMakeLists.txt b/dep0/lib/05_llvmgen/test/CMakeLists.txt index a9918ceb..e0363d30 100644 --- a/dep0/lib/05_llvmgen/test/CMakeLists.txt +++ b/dep0/lib/05_llvmgen/test/CMakeLists.txt @@ -119,3 +119,4 @@ add_dep0_llvmgen_test(0020_builtins) add_dep0_llvmgen_test(0021_tuples) add_dep0_llvmgen_test(0022_structs) add_dep0_llvmgen_test(0023_references) +add_dep0_llvmgen_test(0024_mutability) diff --git a/dep0/lib/05_llvmgen/test/dep0_llvmgen_tests_0024_mutability.cpp b/dep0/lib/05_llvmgen/test/dep0_llvmgen_tests_0024_mutability.cpp new file mode 100644 index 00000000..2a8b0ab2 --- /dev/null +++ b/dep0/lib/05_llvmgen/test/dep0_llvmgen_tests_0024_mutability.cpp @@ -0,0 +1,29 @@ +/* + * Copyright Raffaele Rossi 2025. + * + * Distributed under the Boost Software License, Version 1.0. + * (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt) + */ +#define BOOST_TEST_MODULE dep0_llvmgen_tests_0024_mutability +#include + +#include "llvmgen_tests_fixture.hpp" +#include "llvm_helpers.hpp" +#include "llvm_predicates.hpp" + +using namespace dep0::llvmgen::testing; + +static auto const nonnull = std::vector{llvm::Attribute::NonNull}; +static auto const sext = std::vector{llvm::Attribute::SExt}; +static auto const zext = std::vector{llvm::Attribute::ZExt}; + +BOOST_FIXTURE_TEST_SUITE(dep0_llvmgen_tests_0024_mutability, LLVMGenTestsFixture) + +BOOST_AUTO_TEST_CASE(pass_000) +{ + BOOST_TEST_REQUIRE(pass("0024_mutability/pass_000.depc")); + auto const t = get_struct("t"); + BOOST_TEST(is_struct(t, "t", is_i32, is_i64)); +} + +BOOST_AUTO_TEST_SUITE_END() diff --git a/testfiles/0024_mutability/pass_004.depc b/testfiles/0024_mutability/pass_004.depc index 06b02496..8647e3bd 100644 --- a/testfiles/0024_mutability/pass_004.depc +++ b/testfiles/0024_mutability/pass_004.depc @@ -9,6 +9,7 @@ struct node_t array_t(node_t, 2) children; }; +// NOTE: This test is fundamentally broken and it should not typecheck to begin with! func f(node_t mutable x, node_t y) -> node_t { x = y.children[0]; diff --git a/testfiles/0024_mutability/pass_007.depc b/testfiles/0024_mutability/pass_007.depc index a261a8ce..02096f15 100644 --- a/testfiles/0024_mutability/pass_007.depc +++ b/testfiles/0024_mutability/pass_007.depc @@ -16,6 +16,7 @@ func f1(i32_t mutable x) -> i32_t return f0(scopeof(x), &x); } } + func f2(i32_t mutable x, i32_t mutable y) -> i32_t { immutable(x, y) @@ -23,3 +24,20 @@ func f2(i32_t mutable x, i32_t mutable y) -> i32_t return f0(scopeof(x), &x) + f0(scopeof(y), &y); } } + +func f3(i32_t mutable x, i32_t y) -> i32_t +{ + immutable(x, y) + { + return f0(scopeof(x), &x) + y; + } +} + +func f4(i32_t mutable x, i32_t y) -> i32_t +{ + immutable(y) + { + x = 10; + } + return x; +} From 24fb06cf3242b1bb5852334b6c17efebc68e5f91 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sun, 18 Jan 2026 21:44:44 +0000 Subject: [PATCH 10/11] union of body location maps --- dep0/lib/03_typecheck/src/check.cpp | 24 +++++++++++++++++++++--- testfiles/0024_mutability/pass_003.depc | 10 ++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/dep0/lib/03_typecheck/src/check.cpp b/dep0/lib/03_typecheck/src/check.cpp index adc2fb17..5aacc37a 100644 --- a/dep0/lib/03_typecheck/src/check.cpp +++ b/dep0/lib/03_typecheck/src/check.cpp @@ -306,9 +306,27 @@ check_body( if (stmts->empty()) return make_legal_body(std::nullopt, location_map_t{}, std::move(*stmts)); auto next_ctx = stmts->back().properties.derivation.next_ctx; - // TODO this should be the union of all maps - auto location_map = stmts->back().properties.derivation.location_map.get(); - return make_legal_body(std::move(next_ctx), std::move(location_map), std::move(*stmts)); + if (stmts->size() == 1ul) + { + auto location_map = stmts->back().properties.derivation.location_map.get(); + return make_legal_body(std::move(next_ctx), std::move(location_map), std::move(*stmts)); + } + auto location_map = + location_map_t::combine( + stmts->operator[](0ul).properties.derivation.location_map.get(), + stmts->operator[](1ul).properties.derivation.location_map.get()); + if (not location_map) + return location_map.error(); + for (auto const i: std::views::iota(2ul, stmts->size())) + { + location_map = + location_map_t::combine( + *location_map, + stmts->operator[](i).properties.derivation.location_map.get()); + if (not location_map) + return location_map.error(); + } + return make_legal_body(std::move(next_ctx), std::move(*location_map), std::move(*stmts)); } expected diff --git a/testfiles/0024_mutability/pass_003.depc b/testfiles/0024_mutability/pass_003.depc index 9f550642..178f7eea 100644 --- a/testfiles/0024_mutability/pass_003.depc +++ b/testfiles/0024_mutability/pass_003.depc @@ -11,6 +11,16 @@ func f(i32_t mutable x, ref_t(i32_t, scopeof(x)) mutable p) -> i32_t return x; } +func f2(i32_t mutable x, i32_t mutable y) -> i32_t +{ + if (y == 0) + { + x = 10; + y = 11; + } + return x + y; +} + func g(i32_t a) -> i32_t { // similar to `typecheck_error_010.depc` but `p` is a reference to `a` which is immutable, which is ok From 9907c05f9f199411ce99c516999c44289527c570 Mon Sep 17 00:00:00 2001 From: Raffaele Rossi Date: Sun, 18 Jan 2026 21:51:14 +0000 Subject: [PATCH 11/11] gen_alloca(stack) is CreateAlloca() --- dep0/lib/05_llvmgen/src/gen_val.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dep0/lib/05_llvmgen/src/gen_val.cpp b/dep0/lib/05_llvmgen/src/gen_val.cpp index 7d6a06be..617ff38c 100644 --- a/dep0/lib/05_llvmgen/src/gen_val.cpp +++ b/dep0/lib/05_llvmgen/src/gen_val.cpp @@ -484,8 +484,7 @@ llvm::Value* gen_val( auto const value = gen_temporary_val(global, local, builder, x.expr.get()); if (is_pass_by_val(global, el_type)) { - // TODO isn't this the same as above, i.e. CreateAlloca()+CreateStore()? - address = gen_alloca(global, local, builder, allocator_t::stack, el_type); + address = builder.CreateAlloca(gen_type(global, el_type)); gen_store(global, local, builder, value_category_t::temporary, value, address, el_type); } else