diff --git a/CHANGELOG.md b/CHANGELOG.md index 28a5f87..06a7f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ **Copyright © 2026 ViudiraTech · Code by JiTianYu391** +- Borrow checker: implicit reborrow when a `mutref` is passed to a `mutref` parameter, explicit `borrow [mut] deref r` reborrows (including nested and through reference parameters), and checking of writes through references (`deref r = v`) against live loans and moves. `consume(first); consume(first)` on a `mutref` is now accepted, as in Rust; `val second = first` still moves. +- Split `borrow.cpp`, `sema.cpp`, `codegen.cpp` and `mir.cpp` into focused translation units (no behavior change; LLVM IR for all examples and tests is byte-identical). +- SMP check: a local or parameter that shadows a mutable global is no longer reported as a global access (`E0363`), and the call-graph and shared-state walkers now share one AST traversal so they cannot drift apart. - Fixed a heap-use-after-free in `uinx::Type` copy assignment (`t = *t.pointee`) found by AddressSanitizer. - Repository hygiene: added `CMakePresets.json`, GitHub Actions CI (GCC, Clang, ASan/UBSan, clang-format), issue/PR templates, `CONTRIBUTING.md`, `.editorconfig` and `.gitattributes`; moved `ARCHITECTURE.md` and `BUILD.md` into `docs/`; moved test registration into `tests/CMakeLists.txt`; added `UINX_WARNINGS_AS_ERRORS` and `UINX_ENABLE_SANITIZERS` options; removed the stale `MANIFEST.sha256` and checked-in `verification/` transcripts. - Reworked borrow checking around backward liveness plus forward control-flow fixed points instead of block-local statement numbering. diff --git a/CMakeLists.txt b/CMakeLists.txt index 94dc470..7f65101 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,9 +40,23 @@ add_library(uinx_compiler src/parser.cpp src/types.cpp src/sema.cpp + src/sema_resolve.cpp + src/sema_smp.cpp + src/sema_stmt.cpp + src/sema_expr.cpp src/borrow.cpp + src/borrow_places.cpp + src/borrow_loans.cpp + src/borrow_liveness.cpp + src/borrow_flow.cpp src/mir.cpp + src/mir_stmt.cpp + src/mir_expr.cpp + src/mir_optimizer.cpp src/codegen.cpp + src/codegen_asm.cpp + src/codegen_async.cpp + src/codegen_function.cpp src/compiler.cpp ) target_include_directories(uinx_compiler PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) @@ -74,6 +88,7 @@ if(CLANG_FORMAT_EXECUTABLE) file(GLOB_RECURSE UINX_FORMAT_SOURCES CONFIGURE_DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/include/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.hpp ${CMAKE_CURRENT_SOURCE_DIR}/tools/*.cpp ${CMAKE_CURRENT_SOURCE_DIR}/runtime/*.c ${CMAKE_CURRENT_SOURCE_DIR}/runtime/*.h diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 10dd0f5..c1a18b8 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -31,25 +31,25 @@ The parser recognizes functions, structs, generic parameters and bounds, traits, ## Resolution and semantic model -`src/sema.cpp` collects declarations, resolves names and builds `SemanticModel`. Function signatures retain generic parameters, trait bounds, receiver types, safety and async properties. Trait implementations are checked for duplicate implementations, duplicate methods, missing methods and signature mismatch. +`src/sema.cpp` collects declarations and builds `SemanticModel`; name resolution and the trait solver live in `src/sema_resolve.cpp`, statement and expression checking in `src/sema_stmt.cpp` and `src/sema_expr.cpp`, and concurrency propagation plus the `E0363` shared-state check in `src/sema_smp.cpp`. Function signatures retain generic parameters, trait bounds, receiver types, safety and async properties. Trait implementations are checked for duplicate implementations, duplicate methods, missing methods and signature mismatch. Type inference is local and constraint-oriented: declared parameter/result types are fixed while local bindings infer from initializers. Generic function calls infer type arguments from actual argument types when explicit arguments are not supplied. ## Ownership and borrowing -`src/borrow.cpp` tracks places, moves and loans. A place may be a local, a field path or a conservatively normalized indexed path. Shared borrows may coexist; active mutable loans are exclusive. Mutable references are affine and cannot be implicitly copied. +`src/borrow.cpp` drives statement-level checking and tracks places, moves and loans (place helpers in `borrow_places.cpp`, loan creation/access/reborrows in `borrow_loans.cpp`, liveness in `borrow_liveness.cpp`, branch/loop state merging and expression inspection in `borrow_flow.cpp`). A place may be a local, a field path or a conservatively normalized indexed path. Shared borrows may coexist; active mutable loans are exclusive. Mutable references are affine and cannot be implicitly copied. Last-use information shortens loans before lexical scope end, providing NLL-style behavior. Field paths permit disjoint field borrowing. Indexed paths intentionally alias conservatively unless a stronger proof is available. ## MIR -`src/mir.cpp` lowers typed AST into control-flow blocks and typed instructions. MIR contains storage slots, loads/stores, arithmetic, comparisons, branch/jump, calls, casts, field and index addresses, struct construction, inline assembly, await operations and explicit scope drops. +`src/mir.cpp`, `mir_stmt.cpp` and `mir_expr.cpp` lower typed AST into control-flow blocks and typed instructions. MIR contains storage slots, loads/stores, arithmetic, comparisons, branch/jump, calls, casts, field and index addresses, struct construction, inline assembly, await operations and explicit scope drops. The MIR optimizer removes instructions unreachable after terminators and canonicalizes the emitted block stream. LLVM optimization levels are then delegated to Clang during object generation. ## LLVM backend -`src/codegen.cpp` serializes valid textual LLVM IR. It emits named struct layouts, specialized generic layouts, function definitions/declarations, control flow, GEPs, slice bounds checks, inline assembly and async state machines. +`src/codegen.cpp` serializes valid textual LLVM IR (functions in `codegen_function.cpp`, async state machines in `codegen_async.cpp`, inline assembly and syscalls in `codegen_asm.cpp`). It emits named struct layouts, specialized generic layouts, function definitions/declarations, control flow, GEPs, slice bounds checks, inline assembly and async state machines. The backend does not generate C or C++. The driver writes `.ll`, asks Clang to compile LLVM IR into an object, and performs linking as a separate process. This preserves the required `IR -> Object -> Link` boundary. diff --git a/docs/MEMORY_MODEL.md b/docs/MEMORY_MODEL.md index 9e14c09..333aaa0 100644 --- a/docs/MEMORY_MODEL.md +++ b/docs/MEMORY_MODEL.md @@ -45,6 +45,15 @@ MIR/code generation: shared-like, inspects args, then activates to exclusive. This accepts `v.push(v.len())`. Function `borrow mut` args and `x += x` compound assignment use the same reservation/activation protocol; +- passing a `mutref` place to a `mutref` parameter reborrows it for the call + instead of moving it (`inc(r); inc(r)`); a second use of the same reference in + the same argument list conflicts, and a call whose result may carry a + reference still moves the argument; +- `borrow deref r` / `borrow mut deref r` reborrow through `r`: the new + reference inherits `r`'s origins without conflicting with `r`'s own loans, + and `r` (and every ancestor of a nested reborrow) is frozen while the + reborrow is live. Writes through a reference (`deref r = v`) respect loans + on `r` and reject a moved `r`; - reference provenance through bindings, aggregates, calls, method receivers; - stack references cannot escape scope/function (`E0404/E0405`); - branch/loop fixed-point merging; NLL-style loan expiry via backward diff --git a/include/uinx/borrow.hpp b/include/uinx/borrow.hpp index 6dd0a4e..040284b 100644 --- a/include/uinx/borrow.hpp +++ b/include/uinx/borrow.hpp @@ -33,6 +33,10 @@ class BorrowChecker { std::string place; SourceRange range{}; bool mut{false}; + // Reference places this origin was reborrowed through (`borrow deref r`), + // nearest first. Loans held by those references do not conflict with the + // reborrow, and they stay frozen for as long as the reborrow lives. + std::vector via; }; struct State { std::unordered_set moved; @@ -99,13 +103,28 @@ class BorrowChecker { bool is_copy_type(const Type& type) const; bool states_equivalent(const State& a, const State& b) const; - void - access_place(const std::string& place, const SourceRange& range, Access access, State& state); + void access_place(const std::string& place, + const SourceRange& range, + Access access, + State& state, + const std::vector& via = {}); void begin_borrow(const std::string& place, std::string borrower, bool mut, const SourceRange& range, - State& state); + State& state, + const std::vector& via = {}); + // `borrow deref r` reborrows through the reference place `r`. + std::optional reborrow_parent(const ast::Expr& target) const; + // Writing through a reference (`deref r = v`) must respect loans on `r`. + void write_through_reference(const ast::Expr& target, State& state); + // Implicit reborrow of a `mutref` argument passed to a `mutref` parameter. + bool is_reborrow_argument(const ast::CallExpr& call, std::size_t index) const; + void inspect_call_argument(const ast::CallExpr& call, + std::size_t index, + std::vector& guards, + State& state); + void release_call_guards(const std::vector& guards, State& state); // Two-phase borrow protocol (rustc two-phase borrows): // reservation acts as shared, activation upgrades to exclusive. void begin_two_phase_reservation(const std::string& place, diff --git a/src/borrow.cpp b/src/borrow.cpp index feb3cbe..f6f5e91 100644 --- a/src/borrow.cpp +++ b/src/borrow.cpp @@ -4,1493 +4,13 @@ #include "uinx/borrow.hpp" +#include "borrow_internal.hpp" + #include #include namespace uinx { -namespace { -constexpr std::size_t kMaxDataflowIterations = 4096; - -std::string root_of(std::string_view place) { - const auto dot = place.find('.'); - return std::string(place.substr(0, dot)); -} - -bool is_strict_child(std::string_view parent, std::string_view child) { - return child.size() > parent.size() && child.substr(0, parent.size()) == parent && - child[parent.size()] == '.'; -} - -std::string symbol_key(std::string_view name, hir::SymbolId id) { - return std::string(name) + "#" + std::to_string(id); -} - -std::string display_place(std::string_view place) { - if (place.starts_with(" split_place(std::string_view place) { - std::vector parts; - std::size_t start = 0; - while (true) { - const auto dot = place.find('.', start); - if (dot == std::string_view::npos) { - parts.push_back(std::string(place.substr(start))); - break; - } - parts.push_back(std::string(place.substr(start, dot - start))); - start = dot + 1; - } - return parts; -} - -bool BorrowChecker::places_disjoint(std::string_view a, std::string_view b) const { - // Conservative external provenance always conflicts. - if (a.starts_with(" parent/child overlap. - return false; -} - -bool BorrowChecker::places_conflict(std::string_view a, std::string_view b) const { - return !places_disjoint(a, b); -} - -bool BorrowChecker::loan_live_at(const Borrow& loan, - const LiveSet& live_roots, - const LivePlaceSet& live_places) const { - if (loan.borrower == "") - return true; - const std::string borrower_root = root_of(loan.borrower); - if (model_.globals.contains(borrower_root)) - return true; - // Polonius-style: a loan is live only if its borrower may be used later. - // Check both root liveness (NLL) and precise place liveness. If either - // indicates a future use, the loan stays live. - if (live_roots.contains(borrower_root)) - return true; - if (live_places.contains(loan.borrower)) - return true; - // Subset edges: if any live place overlaps the borrower (e.g. a field of - // the borrower is live), the loan stays live. - for (const auto& live : live_places) { - if (places_conflict(live, loan.borrower)) - return true; - } - return false; -} - -bool BorrowChecker::same_origin(const RefOrigin& a, const RefOrigin& b) const { - return a.place == b.place && a.mut == b.mut && a.range.begin.line == b.range.begin.line && - a.range.begin.column == b.range.begin.column; -} - -bool BorrowChecker::same_borrow(const Borrow& a, const Borrow& b) const { - return a.place == b.place && a.borrower == b.borrower && a.mut == b.mut && a.kind == b.kind && - a.origin.begin.line == b.origin.begin.line && - a.origin.begin.column == b.origin.begin.column; -} - -std::optional BorrowChecker::place_of(const ast::Expr& expr) const { - if (expr.kind == ast::ExprKind::Name) { - const auto& name = static_cast(expr).name; - const auto resolved = model_.hir.expr_resolution.find(&expr); - if (resolved != model_.hir.expr_resolution.end() && - resolved->second < model_.hir.symbols.size()) { - const auto& symbol = model_.hir.symbols[resolved->second]; - if (symbol.kind == hir::SymbolKind::Local || symbol.kind == hir::SymbolKind::Parameter) - return symbol_key(name, resolved->second); - } - return name; - } - if (expr.kind == ast::ExprKind::Member) { - const auto& member = static_cast(expr); - auto base = place_of(*member.base); - if (base) - return *base + "." + member.member; - } - if (expr.kind == ast::ExprKind::Index) { - const auto& index = static_cast(expr); - auto base = place_of(*index.base); - if (base) - return *base + ".[*]"; - } - return std::nullopt; -} - -std::string BorrowChecker::binding_key(const ast::LetStmt& binding) const { - const auto found = model_.hir.bindings.find(&binding); - return found == model_.hir.bindings.end() ? binding.name - : symbol_key(binding.name, found->second); -} - -std::string BorrowChecker::parameter_key(const ast::Param& parameter) const { - const auto found = model_.hir.params.find(¶meter); - return found == model_.hir.params.end() ? parameter.name - : symbol_key(parameter.name, found->second); -} - -std::string BorrowChecker::for_binding_key(const ast::ForStmt& loop) const { - const auto found = model_.hir.for_bindings.find(&loop); - return found == model_.hir.for_bindings.end() ? loop.name - : symbol_key(loop.name, found->second); -} - -std::vector BorrowChecker::reference_origins(const ast::Expr& expr, - const State& state) const { - std::vector result; - auto append = [&](std::vector more) { - for (auto& origin : more) { - const bool duplicate = - std::any_of(result.begin(), result.end(), [&](const RefOrigin& x) { - return same_origin(x, origin); - }); - if (!duplicate) - result.push_back(std::move(origin)); - } - }; - - if (expr.kind == ast::ExprKind::Borrow) { - const auto& borrow = static_cast(expr); - if (auto place = place_of(*borrow.target)) { - result.push_back({*place, expr.range, borrow.mut}); - return result; - } - result = reference_origins(*borrow.target, state); - for (auto& origin : result) - origin.mut = borrow.mut; - return result; - } - - if (auto place = place_of(expr)) { - for (const auto& [stored_place, origins] : state.ref_origins) { - if (*place == stored_place || is_strict_child(*place, stored_place) || - is_strict_child(stored_place, *place)) { - append(origins); - } - } - if (!result.empty()) - return result; - } - - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - append(reference_origins(*field.value, state)); - break; - case ast::ExprKind::Unary: - append(reference_origins(*static_cast(expr).operand, state)); - break; - case ast::ExprKind::Call: { - bool result_can_contain_reference = false; - if (const auto type = model_.expr_types.find(&expr); type != model_.expr_types.end()) { - std::unordered_set visiting; - result_can_contain_reference = contains_reference(type->second, visiting); - } - if (result_can_contain_reference) { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - auto receiver_origins = reference_origins(*member.base, state); - if (!receiver_origins.empty()) { - append(std::move(receiver_origins)); - } else if (const auto receiver_place = place_of(*member.base)) { - Type receiver_type{}; - if (const auto found = model_.expr_types.find(member.base.get()); - found != model_.expr_types.end()) - receiver_type = found->second; - - Type owner = receiver_type; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = - model_.functions.find(owner.name + "::" + member.member); - if (method != model_.functions.end() && !method->second.params.empty() && - method->second.params.front().kind == TypeKind::Ref) { - if (receiver_type.kind == TypeKind::Ref) { - // A reference parameter can point outside the current frame. Keep - // that provenance external inside this function; the caller maps - // it back to its concrete argument when the call itself is bound. - result.push_back({"", - expr.range, - method->second.params.front().mut}); - } else { - result.push_back({*receiver_place, - expr.range, - method->second.params.front().mut}); - } - } - } - } - for (const auto& arg : call.args) - append(reference_origins(*arg, state)); - } - break; - } - case ast::ExprKind::Member: - append(reference_origins(*static_cast(expr).base, state)); - break; - case ast::ExprKind::Index: - append(reference_origins(*static_cast(expr).base, state)); - break; - case ast::ExprKind::Cast: - append(reference_origins(*static_cast(expr).value, state)); - break; - case ast::ExprKind::Await: - append(reference_origins(*static_cast(expr).value, state)); - break; - default: - break; - } - return result; -} - -bool BorrowChecker::contains_reference(const Type& type, - std::unordered_set& visiting) const { - if (type.kind == TypeKind::Ref) - return true; - if (type.kind == TypeKind::Generic) - return true; - if (type.kind != TypeKind::Named) - return false; - if (!visiting.insert(type.name).second) - return false; - - const auto structure = model_.structs.find(type.name); - if (structure == model_.structs.end()) { - visiting.erase(type.name); - return false; - } - - std::unordered_map substitutions; - for (std::size_t i = 0; i < structure->second.generic_names.size() && i < type.args.size(); - ++i) { - substitutions[structure->second.generic_names[i]] = type.args[i]; - } - - for (const auto& [name, field] : structure->second.fields) { - (void)name; - if (contains_reference(substitute_type(field, substitutions), visiting)) { - visiting.erase(type.name); - return true; - } - } - visiting.erase(type.name); - return false; -} - -Type BorrowChecker::type_of_place(std::string_view place, const State& state) const { - const std::string root = root_of(place); - Type type; - bool found = false; - for (auto it = state.scopes.rbegin(); it != state.scopes.rend(); ++it) { - const auto binding = it->find(root); - if (binding != it->end()) { - type = binding->second; - found = true; - break; - } - } - if (!found) { - const auto global = model_.globals.find(root); - if (global != model_.globals.end()) { - type = global->second.type; - found = true; - } - } - if (!found) - return {}; - - std::size_t pos = root.size(); - while (pos < place.size() && place[pos] == '.') { - ++pos; - const auto next = place.find('.', pos); - const std::string field( - place.substr(pos, next == std::string_view::npos ? place.size() - pos : next - pos)); - if (type.kind == TypeKind::Ref && type.pointee) - type = *type.pointee; - if (field == "[*]") { - if (type.kind == TypeKind::Named && (type.name == "Slice" || type.name == "SliceMut") && - !type.args.empty()) { - type = type.args[0]; - } else { - return {}; - } - } else { - const auto structure = model_.structs.find(type.name); - if (structure == model_.structs.end()) - return {}; - const auto member = structure->second.fields.find(field); - if (member == structure->second.fields.end()) - return {}; - std::unordered_map substitutions; - for (std::size_t i = 0; - i < structure->second.generic_names.size() && i < type.args.size(); - ++i) { - substitutions[structure->second.generic_names[i]] = type.args[i]; - } - type = substitute_type(member->second, substitutions); - } - if (next == std::string_view::npos) - break; - pos = next; - } - return type; -} - -bool BorrowChecker::unavailable(std::string_view place, const State& state) const { - return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { - return overlaps(moved, place); - }); -} - -bool BorrowChecker::has_moved_ancestor(std::string_view place, const State& state) const { - return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { - return moved != place && is_strict_child(moved, place); - }); -} - -bool BorrowChecker::is_stack_owned(std::string_view place, const State& state) const { - const std::string root = root_of(place); - for (const auto& scope : state.scopes) { - const auto binding = scope.find(root); - if (binding != scope.end()) { - // `ref T`/`mutref T` parameters and locals are stack bindings, but a field/index - // reached through them belongs to the referent, not to the reference slot itself. - // Borrowing the reference slot (`borrow r`) still correctly counts as stack-owned. - if (binding->second.kind == TypeKind::Ref && is_strict_child(root, place)) - return false; - return true; - } - } - return false; -} - -bool BorrowChecker::is_copy_type(const Type& type) const { - if (type.kind == TypeKind::Generic && copy_generics_.contains(type.name)) - return true; - if (type.is_copy()) - return true; - TraitSolver solver(model_); - return solver.satisfies(type, "Copy"); -} - -bool BorrowChecker::states_equivalent(const State& a, const State& b) const { - if (a.reachable != b.reachable || a.moved != b.moved || a.scopes.size() != b.scopes.size() || - a.borrows.size() != b.borrows.size() || a.ref_origins.size() != b.ref_origins.size()) { - return false; - } - for (const auto& borrow : a.borrows) { - if (std::none_of(b.borrows.begin(), b.borrows.end(), [&](const Borrow& other) { - return same_borrow(borrow, other); - })) { - return false; - } - } - for (const auto& [place, origins] : a.ref_origins) { - const auto found = b.ref_origins.find(place); - if (found == b.ref_origins.end() || found->second.size() != origins.size()) - return false; - for (const auto& origin : origins) { - if (std::none_of(found->second.begin(), - found->second.end(), - [&](const RefOrigin& other) { return same_origin(origin, other); })) { - return false; - } - } - } - return true; -} - -void BorrowChecker::mark_initialized(std::string_view place, State& state) { - for (auto it = state.moved.begin(); it != state.moved.end();) { - if (*it == place || is_strict_child(place, *it)) - it = state.moved.erase(it); - else - ++it; - } -} - -void BorrowChecker::kill_borrower(std::string_view borrower, State& state) { - state.borrows.erase( - std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { return loan.borrower == borrower; }), - state.borrows.end()); -} - -void BorrowChecker::kill_borrowers_under(std::string_view borrower, State& state) { - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - return loan.borrower == borrower || - is_strict_child(borrower, loan.borrower); - }), - state.borrows.end()); -} - -void BorrowChecker::set_reference_origins(const std::string& place, - std::vector origins, - State& state) { - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (it->first == place || is_strict_child(place, it->first)) - it = state.ref_origins.erase(it); - else - ++it; - } - if (origins.empty()) - return; - - std::vector unique; - for (auto& origin : origins) { - const bool duplicate = std::any_of(unique.begin(), unique.end(), [&](const RefOrigin& x) { - return same_origin(x, origin); - }); - if (!duplicate) - unique.push_back(std::move(origin)); - } - state.ref_origins[place] = std::move(unique); -} - -void BorrowChecker::promote_reference_value(const std::string& borrower, - const std::vector& origins, - State& state) { - if (origins.empty()) - return; - - state.borrows.erase( - std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - if (loan.borrower != "") - return false; - return std::any_of( - origins.begin(), origins.end(), [&](const RefOrigin& o) { - return overlaps(loan.place, o.place); - }); - }), - state.borrows.end()); - - for (const auto& origin : origins) - begin_borrow(origin.place, borrower, origin.mut, origin.range, state); -} - -void BorrowChecker::bind_reference_expression(const std::string& destination, - const ast::Expr& expression, - State& state) { - if (expression.kind == ast::ExprKind::StructLiteral) { - const auto& literal = static_cast(expression); - for (const auto& field : literal.fields) - bind_reference_expression(destination + "." + field.name, *field.value, state); - return; - } - - if (const auto source = place_of(expression)) { - std::vector>> mapped; - for (const auto& [stored_place, origins] : state.ref_origins) { - if (stored_place == *source || is_strict_child(*source, stored_place)) { - const std::string suffix = stored_place.substr(source->size()); - mapped.push_back({destination + suffix, origins}); - } - } - if (!mapped.empty()) { - const Type source_type = type_of_place(*source, state); - const bool transfer = !is_copy_type(source_type); - if (transfer) { - kill_borrowers_under(*source, state); - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (it->first == *source || is_strict_child(*source, it->first)) - it = state.ref_origins.erase(it); - else - ++it; - } - } - for (auto& [target, origins] : mapped) { - promote_reference_value(target, origins, state); - set_reference_origins(target, std::move(origins), state); - } - return; - } - } - - auto origins = reference_origins(expression, state); - if (!origins.empty()) { - promote_reference_value(destination, origins, state); - set_reference_origins(destination, std::move(origins), state); - } -} - -void BorrowChecker::access_place(const std::string& place, - const SourceRange& range, - Access access, - State& state) { - if (access == Access::Move) { - const Type type = type_of_place(place, state); - if (is_copy_type(type)) - access = Access::Read; - } - - if (access == Access::Write) { - if (has_moved_ancestor(place, state)) { - report_error(range, - "E0400", - "cannot initialize field/place '" + display_place(place) + - "' because an enclosing value was moved"); - return; - } - } else if (unavailable(place, state)) { - report_error(range, "E0400", "use of moved value/place '" + display_place(place) + "'"); - return; - } - - auto is_active_mut = [](const Borrow& loan) { - if (loan.kind == BorrowKind::TwoPhaseReserved) - return false; // reservation acts as shared - return loan.mut; - }; - auto is_active_exclusive = [](const Borrow& loan) { - if (loan.kind == BorrowKind::TwoPhaseReserved) - return false; - return loan.kind == BorrowKind::Mut || loan.kind == BorrowKind::TwoPhaseActivated; - }; - - if (access == Access::Read) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_mut(loan)) { - report_error(range, - "E0406", - "cannot read '" + display_place(place) + - "' while mutable borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::BorrowShared) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_mut(loan)) { - report_error(range, - "E0401", - "cannot immutably borrow '" + display_place(place) + - "' while mutable borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::TwoPhaseReserve) { - // Reservation phase: only conflicts with active exclusive borrows. - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place) && is_active_exclusive(loan)) { - report_error(range, - "E0402", - "cannot reserve mutably '" + display_place(place) + - "' while exclusive borrow of '" + display_place(loan.place) + - "' is active"); - } - } - return; - } - - if (access == Access::BorrowMut) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place)) { - // A two-phase reservation of the same place by the same - // statement is upgraded at activation, not a conflict here. - // Direct overlapping live loans are still errors. - report_error(range, - "E0402", - "cannot mutably borrow '" + display_place(place) + - "' while borrow of '" + display_place(loan.place) + "' is active"); - } - } - return; - } - - if (access == Access::Write || access == Access::Move) { - for (const auto& loan : state.borrows) { - if (places_conflict(place, loan.place)) { - report_error(range, - "E0403", - "cannot " + std::string(access == Access::Move ? "move" : "write") + - " '" + display_place(place) + "' while it is borrowed"); - } - } - } - - if (access == Access::Move) - state.moved.insert(place); -} - -void BorrowChecker::begin_borrow(const std::string& place, - std::string borrower, - bool mut, - const SourceRange& range, - State& state) { - access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state); - Borrow loan; - loan.place = place; - loan.borrower = std::move(borrower); - loan.mut = mut; - loan.kind = mut ? BorrowKind::Mut : BorrowKind::Shared; - loan.origin = range; - loan.reservation = range; - loan.activated = mut; - if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { - return same_borrow(existing, loan); - })) { - state.borrows.push_back(std::move(loan)); - } -} - -void BorrowChecker::begin_two_phase_reservation(const std::string& place, - std::string borrower, - const SourceRange& reservation, - State& state) { - access_place(place, reservation, Access::TwoPhaseReserve, state); - Borrow loan; - loan.place = place; - loan.borrower = std::move(borrower); - loan.mut = true; - loan.kind = BorrowKind::TwoPhaseReserved; - loan.origin = reservation; - loan.reservation = reservation; - loan.activated = false; - if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { - return same_borrow(existing, loan); - })) { - state.borrows.push_back(std::move(loan)); - } -} - -void BorrowChecker::activate_two_phase(const std::string& borrower, - const SourceRange& activation, - State& state) { - for (auto& loan : state.borrows) { - if (loan.borrower == borrower && loan.kind == BorrowKind::TwoPhaseReserved) { - // Activation: now acts as exclusive. Check against live shared. - for (const auto& other : state.borrows) { - if (&other == &loan) - continue; - if (places_conflict(loan.place, other.place)) { - report_error(activation, - "E0402", - "cannot activate mutable borrow of '" + display_place(loan.place) + - "' while borrow of '" + display_place(other.place) + - "' is active"); - } - } - loan.kind = BorrowKind::TwoPhaseActivated; - loan.origin = activation; - loan.activated = true; - } - } -} - -bool BorrowChecker::is_two_phase_method_receiver(const ast::Expr& base, - const ast::CallExpr& call) const { - if (call.callee->kind != ast::ExprKind::Member) - return false; - const auto& member = static_cast(*call.callee); - Type receiver{}; - if (const auto type = model_.expr_types.find(&base); type != model_.expr_types.end()) - receiver = type->second; - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = model_.functions.find(owner.name + "::" + member.member); - if (method == model_.functions.end() || method->second.params.empty()) - return false; - const Type& self_type = method->second.params.front(); - return self_type.kind == TypeKind::Ref && self_type.mut; -} - -void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State& state) { - // Two-phase protocol for `receiver.mut_method(args...)`: - // reserve receiver (shared-like), inspect args, then activate. - const auto& member = static_cast(*call.callee); - const auto receiver_place = place_of(*member.base); - const std::string reservation_id = ""; - bool reserved = false; - if (receiver_place) { - begin_two_phase_reservation(*receiver_place, reservation_id, call.range, state); - reserved = true; - } else { - inspect_expr(*member.base, state, Access::BorrowShared); - } - // Args are inspected while the receiver reservation is shared-like, so - // `v.push(v.len())` is accepted: `v.len()` shared-borrows while reserved. - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - if (reserved) { - activate_two_phase(reservation_id, call.range, state); - // Call-duration exclusive borrow ends with the call (unless the - // return value carries provenance, handled via reference_origins). - kill_borrower(reservation_id, state); - } - // Receiver provenance: method may return borrow of receiver. - // Keep existing reference_origins handling via caller. -} - -void BorrowChecker::collect_expr_uses(const ast::Expr& expr, - std::size_t index, - std::unordered_map& out) const { - switch (expr.kind) { - case ast::ExprKind::Name: - if (const auto place = place_of(expr)) - out[root_of(*place)] = index; - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - collect_expr_uses(*field.value, index, out); - break; - case ast::ExprKind::Unary: - collect_expr_uses(*static_cast(expr).operand, index, out); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - collect_expr_uses(*binary.lhs, index, out); - collect_expr_uses(*binary.rhs, index, out); - break; - } - case ast::ExprKind::Borrow: - collect_expr_uses(*static_cast(expr).target, index, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - collect_expr_uses(*call.callee, index, out); - for (const auto& arg : call.args) - collect_expr_uses(*arg, index, out); - break; - } - case ast::ExprKind::Member: - collect_expr_uses(*static_cast(expr).base, index, out); - break; - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - collect_expr_uses(*indexed.base, index, out); - collect_expr_uses(*indexed.index, index, out); - break; - } - case ast::ExprKind::Await: - collect_expr_uses(*static_cast(expr).value, index, out); - break; - case ast::ExprKind::Cast: - collect_expr_uses(*static_cast(expr).value, index, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - collect_expr_uses(*operand.value, index, out); - if (!operand.out_name.empty()) - out[operand.out_name] = index; - } - break; - default: - break; - } -} - -BorrowChecker::LiveSet BorrowChecker::expression_uses(const ast::Expr& expr) const { - std::unordered_map uses; - collect_expr_uses(expr, 0, uses); - LiveSet result; - for (const auto& [name, index] : uses) { - (void)index; - result.insert(name); - } - return result; -} - -void BorrowChecker::collect_place_uses(const ast::Expr& expr, LivePlaceSet& out) const { - if (auto place = place_of(expr)) { - out.insert(*place); - // Parent places are also considered used (shallow prefixes). - std::string cur = *place; - while (true) { - const auto dot = cur.rfind('.'); - if (dot == std::string::npos) - break; - cur = cur.substr(0, dot); - out.insert(cur); - } - } - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - collect_place_uses(*field.value, out); - break; - case ast::ExprKind::Unary: - collect_place_uses(*static_cast(expr).operand, out); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - collect_place_uses(*binary.lhs, out); - collect_place_uses(*binary.rhs, out); - break; - } - case ast::ExprKind::Borrow: - collect_place_uses(*static_cast(expr).target, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - collect_place_uses(*call.callee, out); - for (const auto& arg : call.args) - collect_place_uses(*arg, out); - break; - } - case ast::ExprKind::Member: - collect_place_uses(*static_cast(expr).base, out); - break; - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - collect_place_uses(*indexed.base, out); - collect_place_uses(*indexed.index, out); - break; - } - case ast::ExprKind::Await: - collect_place_uses(*static_cast(expr).value, out); - break; - case ast::ExprKind::Cast: - collect_place_uses(*static_cast(expr).value, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - collect_place_uses(*operand.value, out); - if (!operand.out_name.empty()) - out.insert(operand.out_name); - } - break; - default: - break; - } -} - -BorrowChecker::LivePlaceSet BorrowChecker::expression_place_uses(const ast::Expr& expr) const { - LivePlaceSet out; - collect_place_uses(expr, out); - return out; -} - -BorrowChecker::LiveSet BorrowChecker::liveness_block(const ast::BlockStmt& block, - const LiveSet& live_after, - const LiveSet& break_live, - const LiveSet& continue_live) { - LiveSet live = live_after; - for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) - live = liveness_stmt(**it, live, break_live, continue_live); - return live; -} - -BorrowChecker::LiveSet BorrowChecker::liveness_stmt(const ast::Stmt& stmt, - const LiveSet& live_after, - const LiveSet& break_live, - const LiveSet& continue_live) { - live_after_[&stmt] = live_after; - LiveSet before = live_after; - auto add = [&](const LiveSet& values) { before.insert(values.begin(), values.end()); }; - - if (const auto* let = dynamic_cast(&stmt)) { - before.erase(binding_key(*let)); - if (let->init) - add(expression_uses(*let->init)); - } else if (const auto* assign = dynamic_cast(&stmt)) { - const auto target = place_of(*assign->target); - const bool whole_binding = target && *target == root_of(*target); - if (assign->op == "=" && whole_binding) - before.erase(*target); - else - add(expression_uses(*assign->target)); - add(expression_uses(*assign->value)); - } else if (const auto* expr = dynamic_cast(&stmt)) { - add(expression_uses(*expr->expr)); - } else if (const auto* ret = dynamic_cast(&stmt)) { - before.clear(); - if (ret->value) - add(expression_uses(*ret->value)); - } else if (const auto* block = dynamic_cast(&stmt)) { - before = liveness_block(*block, live_after, break_live, continue_live); - } else if (const auto* branch = dynamic_cast(&stmt)) { - const LiveSet left = - liveness_block(*branch->then_block, live_after, break_live, continue_live); - const LiveSet right = - branch->else_block - ? liveness_block(*branch->else_block, live_after, break_live, continue_live) - : live_after; - before = left; - before.insert(right.begin(), right.end()); - add(expression_uses(*branch->condition)); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - const LiveSet body = liveness_block(*while_stmt->body, header, live_after, header); - LiveSet next = live_after; - next.insert(body.begin(), body.end()); - const LiveSet condition = expression_uses(*while_stmt->condition); - next.insert(condition.begin(), condition.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LiveSet body = liveness_block(*for_stmt->body, header, live_after, header); - body.erase(for_binding_key(*for_stmt)); - LiveSet next = live_after; - next.insert(body.begin(), body.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - before.erase(for_binding_key(*for_stmt)); - add(expression_uses(*for_stmt->begin)); - add(expression_uses(*for_stmt->end)); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - LiveSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LiveSet next = liveness_block(*loop_stmt->body, header, live_after, header); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (dynamic_cast(&stmt)) { - before = break_live; - } else if (dynamic_cast(&stmt)) { - before = continue_live; - } else if (const auto* unsafe = dynamic_cast(&stmt)) { - before = liveness_block(*unsafe->body, live_after, break_live, continue_live); - } - - live_before_[&stmt] = before; - return before; -} - -void BorrowChecker::prepare_liveness(const ast::FunctionDecl& fn) { - live_before_.clear(); - live_after_.clear(); - live_places_before_.clear(); - live_places_after_.clear(); - if (!fn.body) - return; - const LiveSet empty; - (void)liveness_block(*fn.body, empty, empty, empty); - // Polonius-style second dimension: precise place liveness. Mirror the - // root pass but with full `a#1.b` strings so sibling-field loans expire - // independently and two-phase reservations are tracked precisely. - const LivePlaceSet empty_places; - (void)liveness_place_block(*fn.body, empty_places, empty_places, empty_places); -} - -BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_block(const ast::BlockStmt& block, - const LivePlaceSet& live_after, - const LivePlaceSet& break_live, - const LivePlaceSet& continue_live) { - LivePlaceSet live = live_after; - for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) - live = liveness_place_stmt(**it, live, break_live, continue_live); - return live; -} - -BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_stmt(const ast::Stmt& stmt, - const LivePlaceSet& live_after, - const LivePlaceSet& break_live, - const LivePlaceSet& continue_live) { - live_places_after_[&stmt] = live_after; - LivePlaceSet before = live_after; - auto add = [&](const LivePlaceSet& values) { before.insert(values.begin(), values.end()); }; - - if (const auto* let = dynamic_cast(&stmt)) { - // Kill the defined place and all its children (strong update on - // whole-binding `=`), keep parent liveness for field updates. - std::vector to_erase; - const std::string key = binding_key(*let); - for (const auto& p : before) { - if (p == key || - (p.size() > key.size() && p.substr(0, key.size()) == key && p[key.size()] == '.')) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - if (let->init) - add(expression_place_uses(*let->init)); - } else if (const auto* assign = dynamic_cast(&stmt)) { - const auto target = place_of(*assign->target); - const bool whole_binding = target && *target == root_of(*target); - if (assign->op == "=" && whole_binding && target) { - std::vector to_erase; - for (const auto& p : before) { - if (p == *target || - (p.size() > target->size() && p.substr(0, target->size()) == *target && - (*target).size() < p.size() && p[target->size()] == '.')) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - } else { - if (assign->target) - add(expression_place_uses(*assign->target)); - } - add(expression_place_uses(*assign->value)); - } else if (const auto* expr = dynamic_cast(&stmt)) { - add(expression_place_uses(*expr->expr)); - } else if (const auto* ret = dynamic_cast(&stmt)) { - before.clear(); - if (ret->value) - add(expression_place_uses(*ret->value)); - } else if (const auto* block = dynamic_cast(&stmt)) { - before = liveness_place_block(*block, live_after, break_live, continue_live); - } else if (const auto* branch = dynamic_cast(&stmt)) { - const LivePlaceSet left = - liveness_place_block(*branch->then_block, live_after, break_live, continue_live); - const LivePlaceSet right = - branch->else_block - ? liveness_place_block(*branch->else_block, live_after, break_live, continue_live) - : live_after; - before = left; - before.insert(right.begin(), right.end()); - add(expression_place_uses(*branch->condition)); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - const LivePlaceSet body = - liveness_place_block(*while_stmt->body, header, live_after, header); - LivePlaceSet next = live_after; - next.insert(body.begin(), body.end()); - const LivePlaceSet condition = expression_place_uses(*while_stmt->condition); - next.insert(condition.begin(), condition.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LivePlaceSet body = liveness_place_block(*for_stmt->body, header, live_after, header); - // Kill loop variable places in body liveness. - const std::string loop_key = for_binding_key(*for_stmt); - std::vector to_erase; - for (const auto& p : body) { - if (p == loop_key || root_of(p) == loop_key) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - body.erase(p); - LivePlaceSet next = live_after; - next.insert(body.begin(), body.end()); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - { - const std::string loop_key = for_binding_key(*for_stmt); - std::vector to_erase; - for (const auto& p : before) { - if (p == loop_key || root_of(p) == loop_key) - to_erase.push_back(p); - } - for (const auto& p : to_erase) - before.erase(p); - } - add(expression_place_uses(*for_stmt->begin)); - add(expression_place_uses(*for_stmt->end)); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - LivePlaceSet header = live_after; - bool converged = false; - for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { - LivePlaceSet next = liveness_place_block(*loop_stmt->body, header, live_after, header); - if (next == header) { - converged = true; - break; - } - header = std::move(next); - } - if (!converged) - report_error(stmt.range, - "E0408", - "borrow liveness analysis did not converge; compilation stopped " - "conservatively"); - before = std::move(header); - } else if (dynamic_cast(&stmt)) { - before = break_live; - } else if (dynamic_cast(&stmt)) { - before = continue_live; - } else if (const auto* unsafe = dynamic_cast(&stmt)) { - before = liveness_place_block(*unsafe->body, live_after, break_live, continue_live); - } - - live_places_before_[&stmt] = before; - return before; -} - -void BorrowChecker::expire_dead_loans(const ast::Stmt& stmt, State& state) { - const auto live_it = live_before_.find(&stmt); - const auto live_places_it = live_places_before_.find(&stmt); - const LiveSet empty_roots; - const LivePlaceSet empty_places; - const LiveSet& live_roots = live_it == live_before_.end() ? empty_roots : live_it->second; - const LivePlaceSet& live_places = - live_places_it == live_places_before_.end() ? empty_places : live_places_it->second; - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - if (loan.borrower == "") - return true; - // Two-phase reservations with `` borrower - // are statement-local; keep until activation. - if (loan.borrower.starts_with("& accumulator, const State& state) const { - if (!state.reachable) - return; - if (!accumulator) { - accumulator = state; - return; - } - State merged; - merge_states(merged, *accumulator, state); - accumulator = std::move(merged); -} - -void BorrowChecker::truncate_scopes(State& state, std::size_t depth) { - while (state.scopes.size() > depth) { - std::unordered_set locals; - for (const auto& [name, type] : state.scopes.back()) { - (void)type; - locals.insert(name); - } - - for (const auto& [borrower, origins] : state.ref_origins) { - if (locals.contains(root_of(borrower))) - continue; - for (const auto& origin : origins) { - if (locals.contains(root_of(origin.place))) { - report_error(origin.range, - "E0405", - "reference stored in '" + display_place(borrower) + - "' outlives local place '" + display_place(origin.place) + - "'"); - } - } - } - - state.scopes.pop_back(); - state.borrows.erase(std::remove_if(state.borrows.begin(), - state.borrows.end(), - [&](const Borrow& loan) { - return locals.contains(root_of(loan.borrower)); - }), - state.borrows.end()); - for (auto it = state.moved.begin(); it != state.moved.end();) { - if (locals.contains(root_of(*it))) - it = state.moved.erase(it); - else - ++it; - } - for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { - if (locals.contains(root_of(it->first))) - it = state.ref_origins.erase(it); - else - ++it; - } - } -} - -void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access access) { - if (!state.reachable) - return; - if (auto place = place_of(expr)) { - access_place(*place, expr.range, access, state); - return; - } - - switch (expr.kind) { - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - inspect_expr(*field.value, state, Access::Move); - break; - case ast::ExprKind::Unary: { - const auto& unary = static_cast(expr); - inspect_expr(*unary.operand, - state, - unary.op == "*" ? Access::Read - : (unary.op == "move" ? Access::Move : access)); - break; - } - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - inspect_expr(*binary.lhs, state, Access::Move); - inspect_expr(*binary.rhs, state, Access::Move); - break; - } - case ast::ExprKind::Borrow: { - const auto& borrow = static_cast(expr); - if (auto place = place_of(*borrow.target)) { - begin_borrow(*place, "", borrow.mut, expr.range, state); - } else { - const auto origins = reference_origins(*borrow.target, state); - if (!origins.empty()) { - for (const auto& origin : origins) - begin_borrow(origin.place, "", borrow.mut, expr.range, state); - } else { - inspect_expr(*borrow.target, state, Access::Read); - } - } - break; - } - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - if (is_two_phase_method_receiver(*member.base, call)) { - inspect_call_with_two_phase(call, state); - break; - } - Type receiver{}; - if (const auto type = model_.expr_types.find(member.base.get()); - type != model_.expr_types.end()) { - receiver = type->second; - } - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto method = model_.functions.find(owner.name + "::" + member.member); - if (method != model_.functions.end() && !method->second.params.empty()) { - Access receiver_access = Access::Move; - const Type& self_type = method->second.params.front(); - if (self_type.kind == TypeKind::Ref) - receiver_access = self_type.mut ? Access::BorrowMut : Access::BorrowShared; - inspect_expr(*member.base, state, receiver_access); - } else { - inspect_expr(*member.base, state, Access::Move); - } - } - // Two-phase for function args with `borrow mut` reborrows - // (e.g. `replace(r, vec![r.len()])`): reserve all `borrow mut` - // args as shared-like, inspect remaining args, then activate. - // Detect `borrow mut ` args directly. - std::vector reserved_places; - std::vector reservation_ids; - for (std::size_t i = 0; i < call.args.size(); ++i) { - const auto& arg = call.args[i]; - if (arg->kind == ast::ExprKind::Borrow) { - const auto& b = static_cast(*arg); - if (b.mut) { - if (auto place = place_of(*b.target)) { - const std::string rid = - ""; - begin_two_phase_reservation(*place, rid, call.range, state); - reserved_places.push_back(*place); - reservation_ids.push_back(rid); - } - } - } - } - if (!reserved_places.empty()) { - for (const auto& arg : call.args) { - // Skip already-reserved `borrow mut` args (reservation - // covers them); inspect others while reserved. - if (arg->kind == ast::ExprKind::Borrow && - static_cast(*arg).mut) - continue; - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - for (const auto& rid : reservation_ids) - activate_two_phase(rid, call.range, state); - for (const auto& rid : reservation_ids) - kill_borrower(rid, state); - break; - } - for (const auto& arg : call.args) { - const auto type = model_.expr_types.find(arg.get()); - Access arg_access = Access::Move; - if (type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && - !type->second.mut) { - arg_access = Access::Read; - } - inspect_expr(*arg, state, arg_access); - } - break; - } - case ast::ExprKind::Index: { - const auto& indexed = static_cast(expr); - inspect_expr(*indexed.base, state, Access::Read); - inspect_expr(*indexed.index, state, Access::Move); - break; - } - case ast::ExprKind::Await: - if (state.async_context) { - for (const auto& loan : state.borrows) { - if (loan.borrower != "" && is_stack_owned(loan.place, state)) { - report_error(expr.range, - "E0407", - "safe reference to stack-owned place '" + - display_place(loan.place) + - "' cannot remain live across await"); - } - } - } - inspect_expr(*static_cast(expr).value, state, Access::Move); - break; - case ast::ExprKind::Cast: - inspect_expr(*static_cast(expr).value, state, Access::Move); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) { - if (operand.value) - inspect_expr(*operand.value, state, Access::Read); - if (!operand.out_name.empty()) - access_place(operand.out_name, operand.range, Access::Write, state); - } - break; - default: - break; - } -} +using namespace borrow_detail; void BorrowChecker::check_stmt(const ast::Stmt& stmt, State& state) { if (!state.reachable) @@ -1587,6 +107,8 @@ void BorrowChecker::check_stmt(const ast::Stmt& stmt, State& state) { kill_borrowers_under(*target, state); inspect_expr(*assign->value, state, Access::Move); + if (!target) + write_through_reference(*assign->target, state); if (target) { access_place(*target, assign->target->range, Access::Write, state); mark_initialized(*target, state); diff --git a/src/borrow_flow.cpp b/src/borrow_flow.cpp new file mode 100644 index 0000000..8bab647 --- /dev/null +++ b/src/borrow_flow.cpp @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::merge_states(State& out, const State& left, const State& right) const { + if (!left.reachable && !right.reachable) { + out = left; + out.reachable = false; + return; + } + if (!left.reachable) { + out = right; + return; + } + if (!right.reachable) { + out = left; + return; + } + + out = left; + out.reachable = true; + out.moved.insert(right.moved.begin(), right.moved.end()); + for (const auto& loan : right.borrows) { + if (std::none_of(out.borrows.begin(), out.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + out.borrows.push_back(loan); + } + } + for (const auto& [place, origins] : right.ref_origins) { + auto& merged = out.ref_origins[place]; + for (const auto& origin : origins) { + if (std::none_of(merged.begin(), merged.end(), [&](const RefOrigin& existing) { + return same_origin(existing, origin); + })) { + merged.push_back(origin); + } + } + } +} + +void BorrowChecker::merge_into(std::optional& accumulator, const State& state) const { + if (!state.reachable) + return; + if (!accumulator) { + accumulator = state; + return; + } + State merged; + merge_states(merged, *accumulator, state); + accumulator = std::move(merged); +} + +void BorrowChecker::truncate_scopes(State& state, std::size_t depth) { + while (state.scopes.size() > depth) { + std::unordered_set locals; + for (const auto& [name, type] : state.scopes.back()) { + (void)type; + locals.insert(name); + } + + for (const auto& [borrower, origins] : state.ref_origins) { + if (locals.contains(root_of(borrower))) + continue; + for (const auto& origin : origins) { + if (locals.contains(root_of(origin.place))) { + report_error(origin.range, + "E0405", + "reference stored in '" + display_place(borrower) + + "' outlives local place '" + display_place(origin.place) + + "'"); + } + } + } + + state.scopes.pop_back(); + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + return locals.contains(root_of(loan.borrower)); + }), + state.borrows.end()); + for (auto it = state.moved.begin(); it != state.moved.end();) { + if (locals.contains(root_of(*it))) + it = state.moved.erase(it); + else + ++it; + } + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (locals.contains(root_of(it->first))) + it = state.ref_origins.erase(it); + else + ++it; + } + } +} + +void BorrowChecker::inspect_expr(const ast::Expr& expr, State& state, Access access) { + if (!state.reachable) + return; + if (auto place = place_of(expr)) { + access_place(*place, expr.range, access, state); + return; + } + + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + inspect_expr(*field.value, state, Access::Move); + break; + case ast::ExprKind::Unary: { + const auto& unary = static_cast(expr); + inspect_expr(*unary.operand, + state, + unary.op == "*" ? Access::Read + : (unary.op == "move" ? Access::Move : access)); + break; + } + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + inspect_expr(*binary.lhs, state, Access::Move); + inspect_expr(*binary.rhs, state, Access::Move); + break; + } + case ast::ExprKind::Borrow: { + const auto& borrow = static_cast(expr); + if (auto place = place_of(*borrow.target)) { + begin_borrow(*place, "", borrow.mut, expr.range, state); + } else if (const auto parent = reborrow_parent(*borrow.target)) { + access_place(*parent, expr.range, Access::Read, state); + for (const auto& origin : reference_origins(expr, state)) { + std::vector exempt = origin.via; + exempt.push_back(""); + begin_borrow( + origin.place, "", borrow.mut, expr.range, state, exempt); + for (const auto& ancestor : origin.via) + begin_borrow( + ancestor, "", borrow.mut, expr.range, state, exempt); + } + } else { + const auto origins = reference_origins(*borrow.target, state); + if (!origins.empty()) { + for (const auto& origin : origins) + begin_borrow(origin.place, "", borrow.mut, expr.range, state); + } else { + inspect_expr(*borrow.target, state, Access::Read); + } + } + break; + } + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + if (is_two_phase_method_receiver(*member.base, call)) { + inspect_call_with_two_phase(call, state); + break; + } + Type receiver{}; + if (const auto type = model_.expr_types.find(member.base.get()); + type != model_.expr_types.end()) { + receiver = type->second; + } + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = model_.functions.find(owner.name + "::" + member.member); + if (method != model_.functions.end() && !method->second.params.empty()) { + Access receiver_access = Access::Move; + const Type& self_type = method->second.params.front(); + if (self_type.kind == TypeKind::Ref) + receiver_access = self_type.mut ? Access::BorrowMut : Access::BorrowShared; + inspect_expr(*member.base, state, receiver_access); + } else { + inspect_expr(*member.base, state, Access::Move); + } + } + // Two-phase for function args with `borrow mut` reborrows + // (e.g. `replace(r, vec![r.len()])`): reserve all `borrow mut` + // args as shared-like, inspect remaining args, then activate. + // Detect `borrow mut ` args directly. + std::vector reserved_places; + std::vector reservation_ids; + for (std::size_t i = 0; i < call.args.size(); ++i) { + const auto& arg = call.args[i]; + if (arg->kind == ast::ExprKind::Borrow) { + const auto& b = static_cast(*arg); + if (b.mut) { + if (auto place = place_of(*b.target)) { + const std::string rid = + ""; + begin_two_phase_reservation(*place, rid, call.range, state); + reserved_places.push_back(*place); + reservation_ids.push_back(rid); + } + } + } + } + std::vector guards; + if (!reserved_places.empty()) { + for (std::size_t i = 0; i < call.args.size(); ++i) { + // Skip already-reserved `borrow mut` args (reservation + // covers them); inspect others while reserved. + if (call.args[i]->kind == ast::ExprKind::Borrow && + static_cast(*call.args[i]).mut) + continue; + inspect_call_argument(call, i, guards, state); + } + release_call_guards(guards, state); + for (const auto& rid : reservation_ids) + activate_two_phase(rid, call.range, state); + for (const auto& rid : reservation_ids) + kill_borrower(rid, state); + break; + } + for (std::size_t i = 0; i < call.args.size(); ++i) + inspect_call_argument(call, i, guards, state); + release_call_guards(guards, state); + break; + } + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + inspect_expr(*indexed.base, state, Access::Read); + inspect_expr(*indexed.index, state, Access::Move); + break; + } + case ast::ExprKind::Await: + if (state.async_context) { + for (const auto& loan : state.borrows) { + if (loan.borrower != "" && is_stack_owned(loan.place, state)) { + report_error(expr.range, + "E0407", + "safe reference to stack-owned place '" + + display_place(loan.place) + + "' cannot remain live across await"); + } + } + } + inspect_expr(*static_cast(expr).value, state, Access::Move); + break; + case ast::ExprKind::Cast: + inspect_expr(*static_cast(expr).value, state, Access::Move); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + inspect_expr(*operand.value, state, Access::Read); + if (!operand.out_name.empty()) + access_place(operand.out_name, operand.range, Access::Write, state); + } + break; + default: + break; + } +} +} // namespace uinx diff --git a/src/borrow_internal.hpp b/src/borrow_internal.hpp new file mode 100644 index 0000000..88201c7 --- /dev/null +++ b/src/borrow_internal.hpp @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of borrow.cpp. + +#pragma once + +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx::borrow_detail { +inline constexpr std::size_t kMaxDataflowIterations = 4096; + +inline std::string root_of(std::string_view place) { + const auto dot = place.find('.'); + return std::string(place.substr(0, dot)); +} + +inline bool is_strict_child(std::string_view parent, std::string_view child) { + return child.size() > parent.size() && child.substr(0, parent.size()) == parent && + child[parent.size()] == '.'; +} + +inline std::string symbol_key(std::string_view name, hir::SymbolId id) { + return std::string(name) + "#" + std::to_string(id); +} + +inline std::string display_place(std::string_view place) { + if (place.starts_with(" split_place(std::string_view place) { + std::vector parts; + std::size_t start = 0; + while (true) { + const auto dot = place.find('.', start); + if (dot == std::string_view::npos) { + parts.push_back(std::string(place.substr(start))); + break; + } + parts.push_back(std::string(place.substr(start, dot - start))); + start = dot + 1; + } + return parts; +} +} // namespace uinx::borrow_detail diff --git a/src/borrow_liveness.cpp b/src/borrow_liveness.cpp new file mode 100644 index 0000000..a1c09d3 --- /dev/null +++ b/src/borrow_liveness.cpp @@ -0,0 +1,472 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::collect_expr_uses(const ast::Expr& expr, + std::size_t index, + std::unordered_map& out) const { + switch (expr.kind) { + case ast::ExprKind::Name: + if (const auto place = place_of(expr)) + out[root_of(*place)] = index; + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + collect_expr_uses(*field.value, index, out); + break; + case ast::ExprKind::Unary: + collect_expr_uses(*static_cast(expr).operand, index, out); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + collect_expr_uses(*binary.lhs, index, out); + collect_expr_uses(*binary.rhs, index, out); + break; + } + case ast::ExprKind::Borrow: + collect_expr_uses(*static_cast(expr).target, index, out); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + collect_expr_uses(*call.callee, index, out); + for (const auto& arg : call.args) + collect_expr_uses(*arg, index, out); + break; + } + case ast::ExprKind::Member: + collect_expr_uses(*static_cast(expr).base, index, out); + break; + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + collect_expr_uses(*indexed.base, index, out); + collect_expr_uses(*indexed.index, index, out); + break; + } + case ast::ExprKind::Await: + collect_expr_uses(*static_cast(expr).value, index, out); + break; + case ast::ExprKind::Cast: + collect_expr_uses(*static_cast(expr).value, index, out); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + collect_expr_uses(*operand.value, index, out); + if (!operand.out_name.empty()) + out[operand.out_name] = index; + } + break; + default: + break; + } +} + +BorrowChecker::LiveSet BorrowChecker::expression_uses(const ast::Expr& expr) const { + std::unordered_map uses; + collect_expr_uses(expr, 0, uses); + LiveSet result; + for (const auto& [name, index] : uses) { + (void)index; + result.insert(name); + } + return result; +} + +void BorrowChecker::collect_place_uses(const ast::Expr& expr, LivePlaceSet& out) const { + if (auto place = place_of(expr)) { + out.insert(*place); + // Parent places are also considered used (shallow prefixes). + std::string cur = *place; + while (true) { + const auto dot = cur.rfind('.'); + if (dot == std::string::npos) + break; + cur = cur.substr(0, dot); + out.insert(cur); + } + } + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + collect_place_uses(*field.value, out); + break; + case ast::ExprKind::Unary: + collect_place_uses(*static_cast(expr).operand, out); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + collect_place_uses(*binary.lhs, out); + collect_place_uses(*binary.rhs, out); + break; + } + case ast::ExprKind::Borrow: + collect_place_uses(*static_cast(expr).target, out); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + collect_place_uses(*call.callee, out); + for (const auto& arg : call.args) + collect_place_uses(*arg, out); + break; + } + case ast::ExprKind::Member: + collect_place_uses(*static_cast(expr).base, out); + break; + case ast::ExprKind::Index: { + const auto& indexed = static_cast(expr); + collect_place_uses(*indexed.base, out); + collect_place_uses(*indexed.index, out); + break; + } + case ast::ExprKind::Await: + collect_place_uses(*static_cast(expr).value, out); + break; + case ast::ExprKind::Cast: + collect_place_uses(*static_cast(expr).value, out); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) { + if (operand.value) + collect_place_uses(*operand.value, out); + if (!operand.out_name.empty()) + out.insert(operand.out_name); + } + break; + default: + break; + } +} + +BorrowChecker::LivePlaceSet BorrowChecker::expression_place_uses(const ast::Expr& expr) const { + LivePlaceSet out; + collect_place_uses(expr, out); + return out; +} + +BorrowChecker::LiveSet BorrowChecker::liveness_block(const ast::BlockStmt& block, + const LiveSet& live_after, + const LiveSet& break_live, + const LiveSet& continue_live) { + LiveSet live = live_after; + for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) + live = liveness_stmt(**it, live, break_live, continue_live); + return live; +} + +BorrowChecker::LiveSet BorrowChecker::liveness_stmt(const ast::Stmt& stmt, + const LiveSet& live_after, + const LiveSet& break_live, + const LiveSet& continue_live) { + live_after_[&stmt] = live_after; + LiveSet before = live_after; + auto add = [&](const LiveSet& values) { before.insert(values.begin(), values.end()); }; + + if (const auto* let = dynamic_cast(&stmt)) { + before.erase(binding_key(*let)); + if (let->init) + add(expression_uses(*let->init)); + } else if (const auto* assign = dynamic_cast(&stmt)) { + const auto target = place_of(*assign->target); + const bool whole_binding = target && *target == root_of(*target); + if (assign->op == "=" && whole_binding) + before.erase(*target); + else + add(expression_uses(*assign->target)); + add(expression_uses(*assign->value)); + } else if (const auto* expr = dynamic_cast(&stmt)) { + add(expression_uses(*expr->expr)); + } else if (const auto* ret = dynamic_cast(&stmt)) { + before.clear(); + if (ret->value) + add(expression_uses(*ret->value)); + } else if (const auto* block = dynamic_cast(&stmt)) { + before = liveness_block(*block, live_after, break_live, continue_live); + } else if (const auto* branch = dynamic_cast(&stmt)) { + const LiveSet left = + liveness_block(*branch->then_block, live_after, break_live, continue_live); + const LiveSet right = + branch->else_block + ? liveness_block(*branch->else_block, live_after, break_live, continue_live) + : live_after; + before = left; + before.insert(right.begin(), right.end()); + add(expression_uses(*branch->condition)); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + const LiveSet body = liveness_block(*while_stmt->body, header, live_after, header); + LiveSet next = live_after; + next.insert(body.begin(), body.end()); + const LiveSet condition = expression_uses(*while_stmt->condition); + next.insert(condition.begin(), condition.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LiveSet body = liveness_block(*for_stmt->body, header, live_after, header); + body.erase(for_binding_key(*for_stmt)); + LiveSet next = live_after; + next.insert(body.begin(), body.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + before.erase(for_binding_key(*for_stmt)); + add(expression_uses(*for_stmt->begin)); + add(expression_uses(*for_stmt->end)); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + LiveSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LiveSet next = liveness_block(*loop_stmt->body, header, live_after, header); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (dynamic_cast(&stmt)) { + before = break_live; + } else if (dynamic_cast(&stmt)) { + before = continue_live; + } else if (const auto* unsafe = dynamic_cast(&stmt)) { + before = liveness_block(*unsafe->body, live_after, break_live, continue_live); + } + + live_before_[&stmt] = before; + return before; +} + +void BorrowChecker::prepare_liveness(const ast::FunctionDecl& fn) { + live_before_.clear(); + live_after_.clear(); + live_places_before_.clear(); + live_places_after_.clear(); + if (!fn.body) + return; + const LiveSet empty; + (void)liveness_block(*fn.body, empty, empty, empty); + // Polonius-style second dimension: precise place liveness. Mirror the + // root pass but with full `a#1.b` strings so sibling-field loans expire + // independently and two-phase reservations are tracked precisely. + const LivePlaceSet empty_places; + (void)liveness_place_block(*fn.body, empty_places, empty_places, empty_places); +} + +BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_block(const ast::BlockStmt& block, + const LivePlaceSet& live_after, + const LivePlaceSet& break_live, + const LivePlaceSet& continue_live) { + LivePlaceSet live = live_after; + for (auto it = block.stmts.rbegin(); it != block.stmts.rend(); ++it) + live = liveness_place_stmt(**it, live, break_live, continue_live); + return live; +} + +BorrowChecker::LivePlaceSet BorrowChecker::liveness_place_stmt(const ast::Stmt& stmt, + const LivePlaceSet& live_after, + const LivePlaceSet& break_live, + const LivePlaceSet& continue_live) { + live_places_after_[&stmt] = live_after; + LivePlaceSet before = live_after; + auto add = [&](const LivePlaceSet& values) { before.insert(values.begin(), values.end()); }; + + if (const auto* let = dynamic_cast(&stmt)) { + // Kill the defined place and all its children (strong update on + // whole-binding `=`), keep parent liveness for field updates. + std::vector to_erase; + const std::string key = binding_key(*let); + for (const auto& p : before) { + if (p == key || + (p.size() > key.size() && p.substr(0, key.size()) == key && p[key.size()] == '.')) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + if (let->init) + add(expression_place_uses(*let->init)); + } else if (const auto* assign = dynamic_cast(&stmt)) { + const auto target = place_of(*assign->target); + const bool whole_binding = target && *target == root_of(*target); + if (assign->op == "=" && whole_binding && target) { + std::vector to_erase; + for (const auto& p : before) { + if (p == *target || + (p.size() > target->size() && p.substr(0, target->size()) == *target && + (*target).size() < p.size() && p[target->size()] == '.')) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + } else { + if (assign->target) + add(expression_place_uses(*assign->target)); + } + add(expression_place_uses(*assign->value)); + } else if (const auto* expr = dynamic_cast(&stmt)) { + add(expression_place_uses(*expr->expr)); + } else if (const auto* ret = dynamic_cast(&stmt)) { + before.clear(); + if (ret->value) + add(expression_place_uses(*ret->value)); + } else if (const auto* block = dynamic_cast(&stmt)) { + before = liveness_place_block(*block, live_after, break_live, continue_live); + } else if (const auto* branch = dynamic_cast(&stmt)) { + const LivePlaceSet left = + liveness_place_block(*branch->then_block, live_after, break_live, continue_live); + const LivePlaceSet right = + branch->else_block + ? liveness_place_block(*branch->else_block, live_after, break_live, continue_live) + : live_after; + before = left; + before.insert(right.begin(), right.end()); + add(expression_place_uses(*branch->condition)); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + const LivePlaceSet body = + liveness_place_block(*while_stmt->body, header, live_after, header); + LivePlaceSet next = live_after; + next.insert(body.begin(), body.end()); + const LivePlaceSet condition = expression_place_uses(*while_stmt->condition); + next.insert(condition.begin(), condition.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LivePlaceSet body = liveness_place_block(*for_stmt->body, header, live_after, header); + // Kill loop variable places in body liveness. + const std::string loop_key = for_binding_key(*for_stmt); + std::vector to_erase; + for (const auto& p : body) { + if (p == loop_key || root_of(p) == loop_key) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + body.erase(p); + LivePlaceSet next = live_after; + next.insert(body.begin(), body.end()); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + { + const std::string loop_key = for_binding_key(*for_stmt); + std::vector to_erase; + for (const auto& p : before) { + if (p == loop_key || root_of(p) == loop_key) + to_erase.push_back(p); + } + for (const auto& p : to_erase) + before.erase(p); + } + add(expression_place_uses(*for_stmt->begin)); + add(expression_place_uses(*for_stmt->end)); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + LivePlaceSet header = live_after; + bool converged = false; + for (std::size_t iteration = 0; iteration < kMaxDataflowIterations; ++iteration) { + LivePlaceSet next = liveness_place_block(*loop_stmt->body, header, live_after, header); + if (next == header) { + converged = true; + break; + } + header = std::move(next); + } + if (!converged) + report_error(stmt.range, + "E0408", + "borrow liveness analysis did not converge; compilation stopped " + "conservatively"); + before = std::move(header); + } else if (dynamic_cast(&stmt)) { + before = break_live; + } else if (dynamic_cast(&stmt)) { + before = continue_live; + } else if (const auto* unsafe = dynamic_cast(&stmt)) { + before = liveness_place_block(*unsafe->body, live_after, break_live, continue_live); + } + + live_places_before_[&stmt] = before; + return before; +} + +void BorrowChecker::expire_dead_loans(const ast::Stmt& stmt, State& state) { + const auto live_it = live_before_.find(&stmt); + const auto live_places_it = live_places_before_.find(&stmt); + const LiveSet empty_roots; + const LivePlaceSet empty_places; + const LiveSet& live_roots = live_it == live_before_.end() ? empty_roots : live_it->second; + const LivePlaceSet& live_places = + live_places_it == live_places_before_.end() ? empty_places : live_places_it->second; + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + if (loan.borrower == "") + return true; + // Two-phase reservations with `` borrower + // are statement-local; keep until activation. + if (loan.borrower.starts_with(" +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::set_reference_origins(const std::string& place, + std::vector origins, + State& state) { + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (it->first == place || is_strict_child(place, it->first)) + it = state.ref_origins.erase(it); + else + ++it; + } + if (origins.empty()) + return; + + std::vector unique; + for (auto& origin : origins) { + const bool duplicate = std::any_of(unique.begin(), unique.end(), [&](const RefOrigin& x) { + return same_origin(x, origin); + }); + if (!duplicate) + unique.push_back(std::move(origin)); + } + state.ref_origins[place] = std::move(unique); +} + +void BorrowChecker::promote_reference_value(const std::string& borrower, + const std::vector& origins, + State& state) { + if (origins.empty()) + return; + + state.borrows.erase( + std::remove_if( + state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + if (loan.borrower != "") + return false; + return std::any_of(origins.begin(), origins.end(), [&](const RefOrigin& o) { + return overlaps(loan.place, o.place) || + std::find(o.via.begin(), o.via.end(), loan.place) != o.via.end(); + }); + }), + state.borrows.end()); + + for (const auto& origin : origins) { + // Loans held by the references this one was reborrowed through, and by + // the borrower itself, are not conflicts. + std::vector exempt = origin.via; + exempt.push_back(borrower); + begin_borrow(origin.place, borrower, origin.mut, origin.range, state, exempt); + // A reborrow also freezes the reference it was taken through: `r` is + // unusable (or read-only) while the reborrow is live. + for (const auto& parent : origin.via) + begin_borrow(parent, borrower, origin.mut, origin.range, state, exempt); + } +} + +void BorrowChecker::bind_reference_expression(const std::string& destination, + const ast::Expr& expression, + State& state) { + if (expression.kind == ast::ExprKind::StructLiteral) { + const auto& literal = static_cast(expression); + for (const auto& field : literal.fields) + bind_reference_expression(destination + "." + field.name, *field.value, state); + return; + } + + if (const auto source = place_of(expression)) { + std::vector>> mapped; + for (const auto& [stored_place, origins] : state.ref_origins) { + if (stored_place == *source || is_strict_child(*source, stored_place)) { + const std::string suffix = stored_place.substr(source->size()); + mapped.push_back({destination + suffix, origins}); + } + } + if (!mapped.empty()) { + const Type source_type = type_of_place(*source, state); + const bool transfer = !is_copy_type(source_type); + if (transfer) { + kill_borrowers_under(*source, state); + for (auto it = state.ref_origins.begin(); it != state.ref_origins.end();) { + if (it->first == *source || is_strict_child(*source, it->first)) + it = state.ref_origins.erase(it); + else + ++it; + } + } + for (auto& [target, origins] : mapped) { + promote_reference_value(target, origins, state); + set_reference_origins(target, std::move(origins), state); + } + return; + } + } + + auto origins = reference_origins(expression, state); + if (!origins.empty()) { + promote_reference_value(destination, origins, state); + set_reference_origins(destination, std::move(origins), state); + } +} + +void BorrowChecker::access_place(const std::string& place, + const SourceRange& range, + Access access, + State& state, + const std::vector& via) { + const auto held_by_parent = [&](const Borrow& loan) { + return std::find(via.begin(), via.end(), loan.borrower) != via.end(); + }; + if (access == Access::Move) { + const Type type = type_of_place(place, state); + if (is_copy_type(type)) + access = Access::Read; + } + + if (access == Access::Write) { + if (has_moved_ancestor(place, state)) { + report_error(range, + "E0400", + "cannot initialize field/place '" + display_place(place) + + "' because an enclosing value was moved"); + return; + } + } else if (unavailable(place, state)) { + report_error(range, "E0400", "use of moved value/place '" + display_place(place) + "'"); + return; + } + + auto is_active_mut = [](const Borrow& loan) { + if (loan.kind == BorrowKind::TwoPhaseReserved) + return false; // reservation acts as shared + return loan.mut; + }; + auto is_active_exclusive = [](const Borrow& loan) { + if (loan.kind == BorrowKind::TwoPhaseReserved) + return false; + return loan.kind == BorrowKind::Mut || loan.kind == BorrowKind::TwoPhaseActivated; + }; + + if (access == Access::Read) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place) && is_active_mut(loan)) { + report_error(range, + "E0406", + "cannot read '" + display_place(place) + + "' while mutable borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::BorrowShared) { + for (const auto& loan : state.borrows) { + if (held_by_parent(loan)) + continue; + if (places_conflict(place, loan.place) && is_active_mut(loan)) { + report_error(range, + "E0401", + "cannot immutably borrow '" + display_place(place) + + "' while mutable borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::TwoPhaseReserve) { + // Reservation phase: only conflicts with active exclusive borrows. + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place) && is_active_exclusive(loan)) { + report_error(range, + "E0402", + "cannot reserve mutably '" + display_place(place) + + "' while exclusive borrow of '" + display_place(loan.place) + + "' is active"); + } + } + return; + } + + if (access == Access::BorrowMut) { + for (const auto& loan : state.borrows) { + if (held_by_parent(loan)) + continue; + if (places_conflict(place, loan.place)) { + // A two-phase reservation of the same place by the same + // statement is upgraded at activation, not a conflict here. + // Direct overlapping live loans are still errors. + report_error(range, + "E0402", + "cannot mutably borrow '" + display_place(place) + + "' while borrow of '" + display_place(loan.place) + "' is active"); + } + } + return; + } + + if (access == Access::Write || access == Access::Move) { + for (const auto& loan : state.borrows) { + if (places_conflict(place, loan.place)) { + report_error(range, + "E0403", + "cannot " + std::string(access == Access::Move ? "move" : "write") + + " '" + display_place(place) + "' while it is borrowed"); + } + } + } + + if (access == Access::Move) + state.moved.insert(place); +} + +void BorrowChecker::begin_borrow(const std::string& place, + std::string borrower, + bool mut, + const SourceRange& range, + State& state, + const std::vector& via) { + access_place(place, range, mut ? Access::BorrowMut : Access::BorrowShared, state, via); + Borrow loan; + loan.place = place; + loan.borrower = std::move(borrower); + loan.mut = mut; + loan.kind = mut ? BorrowKind::Mut : BorrowKind::Shared; + loan.origin = range; + loan.reservation = range; + loan.activated = mut; + if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + state.borrows.push_back(std::move(loan)); + } +} + +void BorrowChecker::begin_two_phase_reservation(const std::string& place, + std::string borrower, + const SourceRange& reservation, + State& state) { + access_place(place, reservation, Access::TwoPhaseReserve, state); + Borrow loan; + loan.place = place; + loan.borrower = std::move(borrower); + loan.mut = true; + loan.kind = BorrowKind::TwoPhaseReserved; + loan.origin = reservation; + loan.reservation = reservation; + loan.activated = false; + if (std::none_of(state.borrows.begin(), state.borrows.end(), [&](const Borrow& existing) { + return same_borrow(existing, loan); + })) { + state.borrows.push_back(std::move(loan)); + } +} + +void BorrowChecker::activate_two_phase(const std::string& borrower, + const SourceRange& activation, + State& state) { + for (auto& loan : state.borrows) { + if (loan.borrower == borrower && loan.kind == BorrowKind::TwoPhaseReserved) { + // Activation: now acts as exclusive. Check against live shared. + for (const auto& other : state.borrows) { + if (&other == &loan) + continue; + if (places_conflict(loan.place, other.place)) { + report_error(activation, + "E0402", + "cannot activate mutable borrow of '" + display_place(loan.place) + + "' while borrow of '" + display_place(other.place) + + "' is active"); + } + } + loan.kind = BorrowKind::TwoPhaseActivated; + loan.origin = activation; + loan.activated = true; + } + } +} + +bool BorrowChecker::is_two_phase_method_receiver(const ast::Expr& base, + const ast::CallExpr& call) const { + if (call.callee->kind != ast::ExprKind::Member) + return false; + const auto& member = static_cast(*call.callee); + Type receiver{}; + if (const auto type = model_.expr_types.find(&base); type != model_.expr_types.end()) + receiver = type->second; + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = model_.functions.find(owner.name + "::" + member.member); + if (method == model_.functions.end() || method->second.params.empty()) + return false; + const Type& self_type = method->second.params.front(); + return self_type.kind == TypeKind::Ref && self_type.mut; +} + +void BorrowChecker::inspect_call_with_two_phase(const ast::CallExpr& call, State& state) { + // Two-phase protocol for `receiver.mut_method(args...)`: + // reserve receiver (shared-like), inspect args, then activate. + const auto& member = static_cast(*call.callee); + const auto receiver_place = place_of(*member.base); + const std::string reservation_id = ""; + bool reserved = false; + if (receiver_place) { + begin_two_phase_reservation(*receiver_place, reservation_id, call.range, state); + reserved = true; + } else { + inspect_expr(*member.base, state, Access::BorrowShared); + } + // Args are inspected while the receiver reservation is shared-like, so + // `v.push(v.len())` is accepted: `v.len()` shared-borrows while reserved. + std::vector guards; + for (std::size_t i = 0; i < call.args.size(); ++i) + inspect_call_argument(call, i, guards, state); + release_call_guards(guards, state); + if (reserved) { + activate_two_phase(reservation_id, call.range, state); + // Call-duration exclusive borrow ends with the call (unless the + // return value carries provenance, handled via reference_origins). + kill_borrower(reservation_id, state); + } + // Receiver provenance: method may return borrow of receiver. + // Keep existing reference_origins handling via caller. +} + +std::optional BorrowChecker::reborrow_parent(const ast::Expr& target) const { + if (target.kind != ast::ExprKind::Unary) + return std::nullopt; + const auto& unary = static_cast(target); + if (unary.op != "*" || !unary.operand) + return std::nullopt; + const auto operand_type = model_.expr_types.find(unary.operand.get()); + if (operand_type == model_.expr_types.end() || operand_type->second.kind != TypeKind::Ref) + return std::nullopt; + return place_of(*unary.operand); +} + +void BorrowChecker::write_through_reference(const ast::Expr& target, State& state) { + const ast::Expr* base = ⌖ + while (base->kind == ast::ExprKind::Member || base->kind == ast::ExprKind::Index) { + base = base->kind == ast::ExprKind::Member + ? static_cast(*base).base.get() + : static_cast(*base).base.get(); + } + const auto parent = reborrow_parent(*base); + if (!parent) + return; + if (unavailable(*parent, state)) { + report_error( + target.range, "E0400", "use of moved value/place '" + display_place(*parent) + "'"); + return; + } + for (const auto& loan : state.borrows) { + // `` stands for the referent behind a reference, not for + // the reference itself. + if (loan.place.starts_with("second.kind != TypeKind::Ref || + !arg_type->second.mut) + return false; + // A result that may carry the reborrow keeps the old move semantics: its + // lifetime would have to be tied to the argument. + if (const auto call_type = model_.expr_types.find(&call); + call_type != model_.expr_types.end()) { + std::unordered_set visiting; + if (contains_reference(call_type->second, visiting)) + return false; + } + const FunctionSig* signature = nullptr; + std::size_t offset = 0; + if (call.callee->kind == ast::ExprKind::Name) { + const auto found = + model_.functions.find(static_cast(*call.callee).name); + if (found != model_.functions.end()) + signature = &found->second; + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + Type owner{}; + if (const auto type = model_.expr_types.find(member.base.get()); + type != model_.expr_types.end()) + owner = type->second; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto found = model_.functions.find(owner.name + "::" + member.member); + if (found != model_.functions.end()) { + signature = &found->second; + offset = 1; + } + } + if (!signature || index + offset >= signature->params.size()) + return false; + const Type& parameter = signature->params[index + offset]; + return parameter.kind == TypeKind::Ref && parameter.mut; +} + +void BorrowChecker::inspect_call_argument(const ast::CallExpr& call, + std::size_t index, + std::vector& guards, + State& state) { + const ast::Expr& arg = *call.args[index]; + if (is_reborrow_argument(call, index)) { + // `f(r)` with `r: mutref T` and `f(mutref T)` reborrows `*r` for the call + // instead of moving `r`. The guard makes a second use of `r` in the same + // argument list a conflict, matching rustc. + inspect_expr(arg, state, Access::Read); + const std::string id = ""; + begin_borrow(*place_of(arg), id, true, arg.range, state); + guards.push_back(id); + return; + } + const auto type = model_.expr_types.find(&arg); + const bool shared_ref = + type != model_.expr_types.end() && type->second.kind == TypeKind::Ref && !type->second.mut; + inspect_expr(arg, state, shared_ref ? Access::Read : Access::Move); +} + +void BorrowChecker::release_call_guards(const std::vector& guards, State& state) { + for (const auto& id : guards) + kill_borrower(id, state); +} +} // namespace uinx diff --git a/src/borrow_places.cpp b/src/borrow_places.cpp new file mode 100644 index 0000000..be5287d --- /dev/null +++ b/src/borrow_places.cpp @@ -0,0 +1,438 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "borrow_internal.hpp" +#include "uinx/borrow.hpp" + +#include +#include + +namespace uinx { +using namespace borrow_detail; + +void BorrowChecker::report_error(const SourceRange& range, std::string code, std::string message) { + std::ostringstream key; + key << code << '@' << range.begin.line << ':' << range.begin.column << '-' << range.end.line + << ':' << range.end.column << ':' << message; + if (emitted_errors_.insert(key.str()).second) + diags_.error(range, std::move(code), std::move(message)); +} + +bool BorrowChecker::overlaps(std::string_view a, std::string_view b) const { + return places_conflict(a, b); +} + +bool BorrowChecker::places_disjoint(std::string_view a, std::string_view b) const { + // Conservative external provenance always conflicts. + if (a.starts_with(" parent/child overlap. + return false; +} + +bool BorrowChecker::places_conflict(std::string_view a, std::string_view b) const { + return !places_disjoint(a, b); +} + +bool BorrowChecker::loan_live_at(const Borrow& loan, + const LiveSet& live_roots, + const LivePlaceSet& live_places) const { + if (loan.borrower == "") + return true; + const std::string borrower_root = root_of(loan.borrower); + if (model_.globals.contains(borrower_root)) + return true; + // Polonius-style: a loan is live only if its borrower may be used later. + // Check both root liveness (NLL) and precise place liveness. If either + // indicates a future use, the loan stays live. + if (live_roots.contains(borrower_root)) + return true; + if (live_places.contains(loan.borrower)) + return true; + // Subset edges: if any live place overlaps the borrower (e.g. a field of + // the borrower is live), the loan stays live. + for (const auto& live : live_places) { + if (places_conflict(live, loan.borrower)) + return true; + } + return false; +} + +bool BorrowChecker::same_origin(const RefOrigin& a, const RefOrigin& b) const { + return a.place == b.place && a.mut == b.mut && a.range.begin.line == b.range.begin.line && + a.range.begin.column == b.range.begin.column; +} + +bool BorrowChecker::same_borrow(const Borrow& a, const Borrow& b) const { + return a.place == b.place && a.borrower == b.borrower && a.mut == b.mut && a.kind == b.kind && + a.origin.begin.line == b.origin.begin.line && + a.origin.begin.column == b.origin.begin.column; +} + +std::optional BorrowChecker::place_of(const ast::Expr& expr) const { + if (expr.kind == ast::ExprKind::Name) { + const auto& name = static_cast(expr).name; + const auto resolved = model_.hir.expr_resolution.find(&expr); + if (resolved != model_.hir.expr_resolution.end() && + resolved->second < model_.hir.symbols.size()) { + const auto& symbol = model_.hir.symbols[resolved->second]; + if (symbol.kind == hir::SymbolKind::Local || symbol.kind == hir::SymbolKind::Parameter) + return symbol_key(name, resolved->second); + } + return name; + } + if (expr.kind == ast::ExprKind::Member) { + const auto& member = static_cast(expr); + auto base = place_of(*member.base); + if (base) + return *base + "." + member.member; + } + if (expr.kind == ast::ExprKind::Index) { + const auto& index = static_cast(expr); + auto base = place_of(*index.base); + if (base) + return *base + ".[*]"; + } + return std::nullopt; +} + +std::string BorrowChecker::binding_key(const ast::LetStmt& binding) const { + const auto found = model_.hir.bindings.find(&binding); + return found == model_.hir.bindings.end() ? binding.name + : symbol_key(binding.name, found->second); +} + +std::string BorrowChecker::parameter_key(const ast::Param& parameter) const { + const auto found = model_.hir.params.find(¶meter); + return found == model_.hir.params.end() ? parameter.name + : symbol_key(parameter.name, found->second); +} + +std::string BorrowChecker::for_binding_key(const ast::ForStmt& loop) const { + const auto found = model_.hir.for_bindings.find(&loop); + return found == model_.hir.for_bindings.end() ? loop.name + : symbol_key(loop.name, found->second); +} + +std::vector BorrowChecker::reference_origins(const ast::Expr& expr, + const State& state) const { + std::vector result; + auto append = [&](std::vector more) { + for (auto& origin : more) { + const bool duplicate = + std::any_of(result.begin(), result.end(), [&](const RefOrigin& x) { + return same_origin(x, origin); + }); + if (!duplicate) + result.push_back(std::move(origin)); + } + }; + + if (expr.kind == ast::ExprKind::Borrow) { + const auto& borrow = static_cast(expr); + if (auto place = place_of(*borrow.target)) { + result.push_back({*place, expr.range, borrow.mut, {}}); + return result; + } + result = reference_origins(*borrow.target, state); + for (auto& origin : result) + origin.mut = borrow.mut; + if (const auto parent = reborrow_parent(*borrow.target)) { + // Reference parameters have no recorded origin; track the reborrow + // against an external place so the parent is still frozen. + if (result.empty()) + result.push_back({"", expr.range, borrow.mut, {}}); + for (auto& origin : result) { + origin.range = expr.range; + origin.via.insert(origin.via.begin(), *parent); + } + } + return result; + } + + if (auto place = place_of(expr)) { + for (const auto& [stored_place, origins] : state.ref_origins) { + if (*place == stored_place || is_strict_child(*place, stored_place) || + is_strict_child(stored_place, *place)) { + append(origins); + } + } + if (!result.empty()) + return result; + } + + switch (expr.kind) { + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + append(reference_origins(*field.value, state)); + break; + case ast::ExprKind::Unary: + append(reference_origins(*static_cast(expr).operand, state)); + break; + case ast::ExprKind::Call: { + bool result_can_contain_reference = false; + if (const auto type = model_.expr_types.find(&expr); type != model_.expr_types.end()) { + std::unordered_set visiting; + result_can_contain_reference = contains_reference(type->second, visiting); + } + if (result_can_contain_reference) { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + auto receiver_origins = reference_origins(*member.base, state); + if (!receiver_origins.empty()) { + append(std::move(receiver_origins)); + } else if (const auto receiver_place = place_of(*member.base)) { + Type receiver_type{}; + if (const auto found = model_.expr_types.find(member.base.get()); + found != model_.expr_types.end()) + receiver_type = found->second; + + Type owner = receiver_type; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto method = + model_.functions.find(owner.name + "::" + member.member); + if (method != model_.functions.end() && !method->second.params.empty() && + method->second.params.front().kind == TypeKind::Ref) { + if (receiver_type.kind == TypeKind::Ref) { + // A reference parameter can point outside the current frame. Keep + // that provenance external inside this function; the caller maps + // it back to its concrete argument when the call itself is bound. + result.push_back({"", + expr.range, + method->second.params.front().mut, + {}}); + } else { + result.push_back({*receiver_place, + expr.range, + method->second.params.front().mut, + {}}); + } + } + } + } + for (const auto& arg : call.args) + append(reference_origins(*arg, state)); + } + break; + } + case ast::ExprKind::Member: + append(reference_origins(*static_cast(expr).base, state)); + break; + case ast::ExprKind::Index: + append(reference_origins(*static_cast(expr).base, state)); + break; + case ast::ExprKind::Cast: + append(reference_origins(*static_cast(expr).value, state)); + break; + case ast::ExprKind::Await: + append(reference_origins(*static_cast(expr).value, state)); + break; + default: + break; + } + return result; +} + +bool BorrowChecker::contains_reference(const Type& type, + std::unordered_set& visiting) const { + if (type.kind == TypeKind::Ref) + return true; + if (type.kind == TypeKind::Generic) + return true; + if (type.kind != TypeKind::Named) + return false; + if (!visiting.insert(type.name).second) + return false; + + const auto structure = model_.structs.find(type.name); + if (structure == model_.structs.end()) { + visiting.erase(type.name); + return false; + } + + std::unordered_map substitutions; + for (std::size_t i = 0; i < structure->second.generic_names.size() && i < type.args.size(); + ++i) { + substitutions[structure->second.generic_names[i]] = type.args[i]; + } + + for (const auto& [name, field] : structure->second.fields) { + (void)name; + if (contains_reference(substitute_type(field, substitutions), visiting)) { + visiting.erase(type.name); + return true; + } + } + visiting.erase(type.name); + return false; +} + +Type BorrowChecker::type_of_place(std::string_view place, const State& state) const { + const std::string root = root_of(place); + Type type; + bool found = false; + for (auto it = state.scopes.rbegin(); it != state.scopes.rend(); ++it) { + const auto binding = it->find(root); + if (binding != it->end()) { + type = binding->second; + found = true; + break; + } + } + if (!found) { + const auto global = model_.globals.find(root); + if (global != model_.globals.end()) { + type = global->second.type; + found = true; + } + } + if (!found) + return {}; + + std::size_t pos = root.size(); + while (pos < place.size() && place[pos] == '.') { + ++pos; + const auto next = place.find('.', pos); + const std::string field( + place.substr(pos, next == std::string_view::npos ? place.size() - pos : next - pos)); + if (type.kind == TypeKind::Ref && type.pointee) + type = *type.pointee; + if (field == "[*]") { + if (type.kind == TypeKind::Named && (type.name == "Slice" || type.name == "SliceMut") && + !type.args.empty()) { + type = type.args[0]; + } else { + return {}; + } + } else { + const auto structure = model_.structs.find(type.name); + if (structure == model_.structs.end()) + return {}; + const auto member = structure->second.fields.find(field); + if (member == structure->second.fields.end()) + return {}; + std::unordered_map substitutions; + for (std::size_t i = 0; + i < structure->second.generic_names.size() && i < type.args.size(); + ++i) { + substitutions[structure->second.generic_names[i]] = type.args[i]; + } + type = substitute_type(member->second, substitutions); + } + if (next == std::string_view::npos) + break; + pos = next; + } + return type; +} + +bool BorrowChecker::unavailable(std::string_view place, const State& state) const { + return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { + return overlaps(moved, place); + }); +} + +bool BorrowChecker::has_moved_ancestor(std::string_view place, const State& state) const { + return std::any_of(state.moved.begin(), state.moved.end(), [&](const std::string& moved) { + return moved != place && is_strict_child(moved, place); + }); +} + +bool BorrowChecker::is_stack_owned(std::string_view place, const State& state) const { + const std::string root = root_of(place); + for (const auto& scope : state.scopes) { + const auto binding = scope.find(root); + if (binding != scope.end()) { + // `ref T`/`mutref T` parameters and locals are stack bindings, but a field/index + // reached through them belongs to the referent, not to the reference slot itself. + // Borrowing the reference slot (`borrow r`) still correctly counts as stack-owned. + if (binding->second.kind == TypeKind::Ref && is_strict_child(root, place)) + return false; + return true; + } + } + return false; +} + +bool BorrowChecker::is_copy_type(const Type& type) const { + if (type.kind == TypeKind::Generic && copy_generics_.contains(type.name)) + return true; + if (type.is_copy()) + return true; + TraitSolver solver(model_); + return solver.satisfies(type, "Copy"); +} + +bool BorrowChecker::states_equivalent(const State& a, const State& b) const { + if (a.reachable != b.reachable || a.moved != b.moved || a.scopes.size() != b.scopes.size() || + a.borrows.size() != b.borrows.size() || a.ref_origins.size() != b.ref_origins.size()) { + return false; + } + for (const auto& borrow : a.borrows) { + if (std::none_of(b.borrows.begin(), b.borrows.end(), [&](const Borrow& other) { + return same_borrow(borrow, other); + })) { + return false; + } + } + for (const auto& [place, origins] : a.ref_origins) { + const auto found = b.ref_origins.find(place); + if (found == b.ref_origins.end() || found->second.size() != origins.size()) + return false; + for (const auto& origin : origins) { + if (std::none_of(found->second.begin(), + found->second.end(), + [&](const RefOrigin& other) { return same_origin(origin, other); })) { + return false; + } + } + } + return true; +} + +void BorrowChecker::mark_initialized(std::string_view place, State& state) { + for (auto it = state.moved.begin(); it != state.moved.end();) { + if (*it == place || is_strict_child(place, *it)) + it = state.moved.erase(it); + else + ++it; + } +} + +void BorrowChecker::kill_borrower(std::string_view borrower, State& state) { + state.borrows.erase( + std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { return loan.borrower == borrower; }), + state.borrows.end()); +} + +void BorrowChecker::kill_borrowers_under(std::string_view borrower, State& state) { + state.borrows.erase(std::remove_if(state.borrows.begin(), + state.borrows.end(), + [&](const Borrow& loan) { + return loan.borrower == borrower || + is_strict_child(borrower, loan.borrower); + }), + state.borrows.end()); +} +} // namespace uinx diff --git a/src/codegen.cpp b/src/codegen.cpp index 99a5544..65c997f 100644 --- a/src/codegen.cpp +++ b/src/codegen.cpp @@ -2,10 +2,10 @@ // Copyright (c) 2026 ViudiraTech // By JiTianYu391 -// Uinx Language - #include "uinx/codegen.hpp" +#include "codegen_internal.hpp" + #include #include #include @@ -14,351 +14,7 @@ #include namespace uinx { -namespace { - -bool unsigned_integer(TypeKind kind) { - return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || - kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; -} - -int integer_bits(const Type& type) { - switch (type.kind) { - case TypeKind::I8: - case TypeKind::U8: - return 8; - case TypeKind::I16: - case TypeKind::U16: - return 16; - case TypeKind::I32: - case TypeKind::U32: - case TypeKind::Char: - return 32; - case TypeKind::I128: - case TypeKind::U128: - return 128; - default: - return 64; - } -} - -std::string llvm_integer_constant(std::string_view literal) { - bool negative = false; - std::size_t offset = 0; - if (!literal.empty() && literal.front() == '-') { - negative = true; - offset = 1; - } - - unsigned base = 10; - if (literal.size() >= offset + 2 && literal[offset] == '0') { - switch (literal[offset + 1]) { - case 'x': - case 'X': - base = 16; - offset += 2; - break; - case 'b': - case 'B': - base = 2; - offset += 2; - break; - case 'o': - case 'O': - base = 8; - offset += 2; - break; - default: - break; - } - } - - // Store the magnitude in base 1,000,000,000 so this also handles u128 - // without narrowing through a host integer type. - std::vector digits{0}; - bool has_digit = false; - for (std::size_t i = offset; i < literal.size(); ++i) { - const char c = literal[i]; - if (c == '_') - continue; - unsigned value = 0; - if (c >= '0' && c <= '9') - value = static_cast(c - '0'); - else if (c >= 'a' && c <= 'f') - value = static_cast(c - 'a') + 10; - else if (c >= 'A' && c <= 'F') - value = static_cast(c - 'A') + 10; - else - return std::string(literal); - if (value >= base) - return std::string(literal); - - has_digit = true; - std::uint64_t carry = value; - for (auto& digit : digits) { - const std::uint64_t next = static_cast(digit) * base + carry; - digit = static_cast(next % 1000000000U); - carry = next / 1000000000U; - } - if (carry != 0) - digits.push_back(static_cast(carry)); - } - if (!has_digit) - return std::string(literal); - - while (digits.size() > 1 && digits.back() == 0) - digits.pop_back(); - if (digits.size() == 1 && digits.front() == 0) - return "0"; - - std::string result = std::to_string(digits.back()); - for (std::size_t i = digits.size() - 1; i > 0; --i) { - const std::string chunk = std::to_string(digits[i - 1]); - result.append(9 - chunk.size(), '0'); - result += chunk; - } - if (negative) - result.insert(result.begin(), '-'); - return result; -} - -std::string arithmetic_opcode(const Type& type, std::string_view op) { - if (op == "+") - return type.is_float() ? "fadd" : "add"; - if (op == "-") - return type.is_float() ? "fsub" : "sub"; - if (op == "*") - return type.is_float() ? "fmul" : "mul"; - if (op == "/") { - if (type.is_float()) - return "fdiv"; - return unsigned_integer(type.kind) ? "udiv" : "sdiv"; - } - if (op == "%") - return unsigned_integer(type.kind) ? "urem" : "srem"; - if (op == "&&" || op == "&") - return "and"; - if (op == "||" || op == "|") - return "or"; - if (op == "^") - return "xor"; - if (op == "<<") - return "shl"; - if (op == ">>") - return unsigned_integer(type.kind) ? "lshr" : "ashr"; - return "add"; -} - -unsigned type_alignment(const Type& type) { - switch (type.kind) { - case TypeKind::Bool: - case TypeKind::I8: - case TypeKind::U8: - return 1; - case TypeKind::I16: - case TypeKind::U16: - return 2; - case TypeKind::I32: - case TypeKind::U32: - case TypeKind::Char: - case TypeKind::F32: - return 4; - case TypeKind::I128: - case TypeKind::U128: - return 16; - default: - return 8; - } -} - -std::string atomic_rmw_opcode(std::string_view op) { - if (op == "+") - return "add"; - if (op == "-") - return "sub"; - if (op == "&") - return "and"; - if (op == "|") - return "or"; - if (op == "^") - return "xor"; - return "xchg"; -} - -std::optional atomic_order_from_abi(std::string_view value) { - if (value == "0") - return "monotonic"; - if (value == "1") - return "acquire"; - if (value == "2") - return "release"; - if (value == "3") - return "acq_rel"; - if (value == "4") - return "seq_cst"; - return std::nullopt; -} - -std::string cmpxchg_failure_order(std::string order) { - if (order == "release") - return "monotonic"; - if (order == "acq_rel") - return "acquire"; - return order; -} - -// Industrial C++20 ordering validation (Rust + C++20 + LKMM fusion). -// load: relaxed/acquire/seq_cst; store: relaxed/release/seq_cst; -// rmw/exchange: all five; cmpxchg failure: never release/acq_rel. -bool valid_atomic_load_order(std::string_view order) { - return order == "monotonic" || order == "acquire" || order == "seq_cst"; -} - -bool valid_atomic_store_order(std::string_view order) { - return order == "monotonic" || order == "release" || order == "seq_cst"; -} - -bool valid_atomic_rmw_order(std::string_view order) { - return order == "monotonic" || order == "acquire" || order == "release" || order == "acq_rel" || - order == "seq_cst"; -} - -int order_strength(std::string_view order) { - if (order == "monotonic") - return 0; - if (order == "acquire" || order == "release") - return 1; - if (order == "acq_rel") - return 2; - if (order == "seq_cst") - return 3; - return -1; -} - -std::string downgrade_cmpxchg_failure(std::string success, std::string failure) { - failure = cmpxchg_failure_order(std::move(failure)); - // Failure ordering must not exceed success ordering. - if (order_strength(failure) > order_strength(success)) { - if (success == "monotonic" || success == "release") - return "monotonic"; - return "acquire"; - } - return failure; -} - -struct AtomicIntrinsic { - std::string llvm_type; - unsigned align; - std::string rmw_op; // add/sub/and/or/xor/xchg/cmpxchg/load/store -}; - -std::optional parse_atomic_intrinsic(std::string_view name) { - // uinx_atomic_{load,store,fetch_add,fetch_sub,fetch_and,fetch_or,fetch_xor, - // exchange,compare_exchange}_{u8,u16,u32,u64} - static constexpr std::string_view prefix = "uinx_atomic_"; - if (!name.starts_with(prefix)) - return std::nullopt; - std::string_view rest = name.substr(prefix.size()); - std::string op; - for (std::string_view candidate : {"compare_exchange", - "fetch_add", - "fetch_sub", - "fetch_and", - "fetch_or", - "fetch_xor", - "exchange", - "load", - "store"}) { - if (rest.starts_with(candidate)) { - op = std::string(candidate); - rest = rest.substr(candidate.size()); - break; - } - } - if (op.empty() || !rest.starts_with("_")) - return std::nullopt; - std::string_view width = rest.substr(1); - std::string llvm_type; - unsigned align = 1; - if (width == "u8") { - llvm_type = "i8"; - align = 1; - } else if (width == "u16") { - llvm_type = "i16"; - align = 2; - } else if (width == "u32") { - llvm_type = "i32"; - align = 4; - } else if (width == "u64") { - llvm_type = "i64"; - align = 8; - } else { - return std::nullopt; - } - std::string rmw; - if (op == "load") - rmw = "load"; - else if (op == "store") - rmw = "store"; - else if (op == "fetch_add") - rmw = "add"; - else if (op == "fetch_sub") - rmw = "sub"; - else if (op == "fetch_and") - rmw = "and"; - else if (op == "fetch_or") - rmw = "or"; - else if (op == "fetch_xor") - rmw = "xor"; - else if (op == "exchange") - rmw = "xchg"; - else if (op == "compare_exchange") - rmw = "cmpxchg"; - return AtomicIntrinsic{llvm_type, align, rmw}; -} - -// Number of arguments encoded in a uinx_syscallN intrinsic name, or -// std::nullopt when the symbol is an ordinary runtime call. -std::optional syscall_arity_from_name(std::string_view name) { - static constexpr std::string_view prefix = "uinx_syscall"; - if (!name.starts_with(prefix)) - return std::nullopt; - const std::string_view suffix = name.substr(prefix.size()); - if (suffix.empty() || suffix.size() > 1) - return std::nullopt; - if (suffix.front() < '0' || suffix.front() > '6') - return std::nullopt; - return suffix.front() - '0'; -} - -std::string compare_predicate(const Type& type, std::string_view op) { - if (type.is_float()) { - if (op == "==") - return "oeq"; - if (op == "!=") - return "une"; - if (op == "<") - return "olt"; - if (op == "<=") - return "ole"; - if (op == ">") - return "ogt"; - return "oge"; - } - const bool u = unsigned_integer(type.kind); - if (op == "==") - return "eq"; - if (op == "!=") - return "ne"; - if (op == "<") - return u ? "ult" : "slt"; - if (op == "<=") - return u ? "ule" : "sle"; - if (op == ">") - return u ? "ugt" : "sgt"; - return u ? "uge" : "sge"; -} - -} // namespace +using namespace codegen_detail; TargetInfo TargetInfo::from_triple(std::string triple) { TargetInfo target; @@ -473,1271 +129,6 @@ std::string LLVMCodegen::escape_asm(std::string_view value) const { return out; } -bool LLVMCodegen::validate_constraint(std::string_view constraint, const SourceRange& range) const { - std::string raw(constraint); - while (!raw.empty() && std::string("=+&~").find(raw.front()) != std::string::npos) - raw.erase(raw.begin()); - if (raw == "memory" || raw == "cc" || raw == "r" || raw == "m" || raw == "i" || raw == "g") - return true; - if (raw.size() >= 2 && raw.front() == '{' && raw.back() == '}') - raw = raw.substr(1, raw.size() - 2); - - static const std::unordered_set x86 = { - "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", - "r9", "r10", "r11", "r12", "r13", "r14", "r15", "eax", "ebx", - "ecx", "edx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", - "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"}; - auto numbered = [&](char prefix, int max) { - if (raw.size() < 2 || raw.front() != prefix) - return false; - try { - const int n = std::stoi(raw.substr(1)); - return n >= 0 && n <= max; - } catch (...) { - return false; - } - }; - - bool valid = false; - if (target_.arch == "x86_64") { - valid = x86.contains(raw); - } else if (target_.arch == "aarch64") { - valid = numbered('x', 30) || numbered('w', 30) || numbered('v', 31) || raw == "sp"; - } else if (target_.arch == "riscv64") { - valid = numbered('x', 31) || raw == "ra" || raw == "sp" || raw == "gp" || raw == "tp" || - raw == "a0" || raw == "a1" || raw == "a2" || raw == "a3" || raw == "a4" || - raw == "a5" || raw == "a6" || raw == "a7" || raw == "s0" || raw == "s1" || - raw == "t0" || raw == "t1" || raw == "t2"; - } else { - valid = true; - } - if (!valid) { - diags_.error(range, - "E0600", - "asm constraint/register '" + std::string(constraint) + - "' is not valid for target architecture '" + target_.arch + "'"); - } - return valid; -} - -void LLVMCodegen::emit_inline_asm(std::ostream& os, - const mir::Instruction& in, - std::unordered_map& values, - std::unordered_map& value_types, - std::size_t& counter) const { - std::vector outputs; - std::vector inputs; - std::vector clobbers; - for (const auto& operand : in.asm_operands) { - validate_constraint(operand.constraint, in.range); - if (operand.kind == ast::AsmOperand::Kind::Out || - operand.kind == ast::AsmOperand::Kind::InOut) - outputs.push_back(&operand); - else if (operand.kind == ast::AsmOperand::Kind::In) - inputs.push_back(&operand); - else - clobbers.push_back(&operand); - } - - std::vector constraints; - std::vector> call_args; - for (const auto* output : outputs) { - std::string c = output->constraint; - while (!c.empty() && c.front() == '+') - c.erase(c.begin()); - if (c.empty() || c.front() != '=') - c = "=" + c; - constraints.push_back(std::move(c)); - } - for (std::size_t index = 0; index < outputs.size(); ++index) { - const auto* output = outputs[index]; - if (output->kind != ast::AsmOperand::Kind::InOut) - continue; - std::string initial = "undef"; - if (!output->out_slot.empty()) { - initial = "%asm.inout." + std::to_string(counter) + "." + std::to_string(index); - os << " " << initial << " = load " << llvm_type(output->type) << ", ptr " - << output->out_slot << "\n"; - } else if (!output->value.empty()) { - const auto it = values.find(output->value); - initial = it == values.end() ? output->value : it->second; - } - constraints.push_back(std::to_string(index)); - call_args.push_back({output->type, initial}); - } - for (const auto* input : inputs) { - constraints.push_back(input->constraint); - const auto it = values.find(input->value); - call_args.push_back({input->type, it == values.end() ? input->value : it->second}); - } - for (const auto* clobber : clobbers) { - if (clobber->constraint == "memory") - constraints.push_back("~{memory}"); - else if (clobber->constraint == "cc") - constraints.push_back("~{cc}"); - else { - std::string reg = clobber->constraint; - if (reg.front() == '{' && reg.back() == '}') - constraints.push_back("~" + reg); - else - constraints.push_back("~{" + reg + "}"); - } - } - - std::string return_type = "void"; - if (outputs.size() == 1) - return_type = llvm_type(outputs.front()->type); - else if (outputs.size() > 1) { - return_type = "{ "; - for (std::size_t i = 0; i < outputs.size(); ++i) { - if (i) - return_type += ", "; - return_type += llvm_type(outputs[i]->type); - } - return_type += " }"; - } - - std::string constraint_text; - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - - const std::string call_result = outputs.empty() ? "" : "%asm.result." + std::to_string(counter); - os << " "; - if (!call_result.empty()) - os << call_result << " = "; - os << "call " << return_type << " asm "; - if (in.flag) - os << "sideeffect "; - os << '"' << escape_asm(in.text) << "\", \"" << escape_asm(constraint_text) << "\"("; - for (std::size_t i = 0; i < call_args.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(call_args[i].first) << ' ' << call_args[i].second; - } - os << ")\n"; - - for (std::size_t i = 0; i < outputs.size(); ++i) { - const auto* output = outputs[i]; - if (output->out_slot.empty()) - continue; - std::string value = call_result; - if (outputs.size() > 1) { - value = "%asm.out." + std::to_string(counter) + "." + std::to_string(i); - os << " " << value << " = extractvalue " << return_type << ' ' << call_result << ", " - << i << "\n"; - } - os << " store " << llvm_type(output->type) << ' ' << value << ", ptr " << output->out_slot - << "\n"; - value_types[output->out_slot] = output->type; - } - ++counter; -} - -// Lowers the compiler-recognized `uinx_syscall0..6` ABI family into a raw -// kernel trap for the current target. Argument 0 is the syscall number, the -// remaining arguments map to the architecture's syscall argument registers. -// Returns false when the target has no known trap sequence so the caller can -// fall back to an ordinary call. -bool LLVMCodegen::emit_syscall(std::ostream& os, - const mir::Instruction& in, - std::unordered_map& values, - std::unordered_map& value_types, - std::size_t& counter) const { - const int arity = static_cast(in.args.size()); - if (arity < 1 || arity > 7) - return false; - - auto value_of = [&](const std::string& value) -> std::string { - const auto it = values.find(value); - return it == values.end() ? value : it->second; - }; - - // Zero-extend each argument into an i64 so register constraints are - // satisfied regardless of the source integer width. - std::vector args; - args.reserve(static_cast(arity)); - for (const auto& arg : in.args) { - const std::string raw = value_of(arg); - const auto type_it = value_types.find(arg); - const Type type = type_it == value_types.end() ? Type::builtin("i64") : type_it->second; - if (type.str() == "i64" || type.str() == "u64" || type.str() == "usize" || - type.str() == "isize") { - args.push_back(raw); - } else { - args.push_back("%syscall.arg." + std::to_string(counter) + "." + - std::to_string(args.size())); - os << " " << args.back() << " = zext " << llvm_type(type) << ' ' << raw << " to i64\n"; - } - } - - std::string constraint_text; - std::string asm_text; - if (target_.arch == "x86_64") { - // rax = number, rdi/rsi/rdx/r10/r8/r9 = args 1..6. Only the registers - // actually consumed are declared; the kernel additionally clobbers rcx - // (return RIP) and r11 (RFLAGS). - static const char* regs[7] = {"rax", "rdi", "rsi", "rdx", "r10", "r8", "r9"}; - std::vector constraints{"={rax}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{rcx}"); - constraints.push_back("~{r11}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "syscall"; - } else if (target_.arch == "aarch64") { - // x8 = number, x0..x5 = args 1..6. The kernel may clobber x16/x17. - static const char* regs[7] = {"x8", "x0", "x1", "x2", "x3", "x4", "x5"}; - std::vector constraints{"={x0}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{x16}"); - constraints.push_back("~{x17}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "svc #0"; - } else if (target_.arch == "riscv64") { - // a7 = number, a0..a5 = args 1..6. The kernel may clobber a6. - static const char* regs[7] = {"a7", "a0", "a1", "a2", "a3", "a4", "a5"}; - std::vector constraints{"={a0}"}; - for (int i = 0; i < arity; ++i) - constraints.push_back(std::string("{") + regs[i] + "}"); - constraints.push_back("~{a6}"); - constraints.push_back("~{memory}"); - constraints.push_back("~{cc}"); - for (std::size_t i = 0; i < constraints.size(); ++i) { - if (i) - constraint_text += ','; - constraint_text += constraints[i]; - } - asm_text = "ecall"; - } else { - diags_.error(in.range, - "E0601", - "no syscall trap sequence is defined for target architecture '" + - target_.arch + "'"); - return false; - } - - const std::string call_result = "%syscall.result." + std::to_string(counter); - os << " " << call_result << " = call i64 asm sideeffect \"" << asm_text << "\", \"" - << constraint_text << "\"("; - for (std::size_t i = 0; i < args.size(); ++i) { - if (i) - os << ", "; - os << "i64 " << args[i]; - } - os << ")\n"; - - if (!in.result.empty()) { - // The trap returns i64; narrow to the declared result type when needed. - const std::string result_type = llvm_type(in.type); - if (result_type == "i64") { - values[in.result] = call_result; - } else { - const std::string truncated = "%syscall.trunc." + std::to_string(counter); - os << " " << truncated << " = trunc i64 " << call_result << " to " << result_type - << "\n"; - values[in.result] = truncated; - } - value_types[in.result] = in.type; - } - ++counter; - return true; -} - -void LLVMCodegen::emit_async_function( - std::ostream& os, - const mir::Function& fn, - const std::unordered_map& functions) const { - struct FrameField { - Type type; - std::size_t index{}; - bool address{false}; - }; - - std::unordered_map fields; - std::vector> ordered; - std::size_t next_field = 1; - auto add_field = [&](const std::string& key, Type type, bool address) { - if (key.empty() || fields.contains(key)) - return; - FrameField field{std::move(type), next_field++, address}; - fields.emplace(key, field); - ordered.push_back({key, field}); - }; - - for (const auto& param : fn.params) - add_field("%arg." + param.name, param.type, false); - std::vector> awaits; - std::size_t await_count = 0; - for (const auto& block : fn.blocks) { - for (const auto& instruction : block.instructions) { - if (instruction.op == mir::Op::Await) - awaits.push_back({&instruction, ++await_count}); - if (instruction.result.empty()) - continue; - if (instruction.op == mir::Op::Alloca) - add_field(instruction.result, instruction.type, true); - else if (instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::IndexAddr) - add_field(instruction.result, Type::raw_ptr(instruction.type, true), false); - else - add_field(instruction.result, instruction.type, false); - } - } - - const std::string name = llvm_name(fn.name); - const std::string frame_type = "%uinx.async.frame." + name; - os << frame_type << " = type { i32"; - for (const auto& [key, field] : ordered) { - (void)key; - os << ", " << llvm_type(field.type); - } - os << " }\n"; - os << "@" << name << "__vtable = private constant %uinx.future.vtable { ptr @" << name - << "__poll, ptr @" << name << "__drop }\n"; - - os << "define %uinx.future @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); - } - os << ") {\nentry:\n"; - os << " %frame.size.ptr = getelementptr " << frame_type << ", ptr null, i32 1\n"; - os << " %frame.size = ptrtoint ptr %frame.size.ptr to i64\n"; - os << " %frame = call ptr @malloc(i64 %frame.size)\n"; - os << " %oom = icmp eq ptr %frame, null\n"; - os << " br i1 %oom, label %async.oom, label %async.init\n"; - os << "async.oom:\n call void @abort()\n unreachable\n"; - os << "async.init:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " store i32 0, ptr %state.addr\n"; - for (const auto& param : fn.params) { - const auto& field = fields.at("%arg." + param.name); - os << " %param.addr." << field.index << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << field.index << "\n"; - os << " store " << llvm_type(param.type) << " %arg." << llvm_name(param.name) - << ", ptr %param.addr." << field.index << "\n"; - } - os << " %future.0 = insertvalue %uinx.future poison, ptr %frame, 0\n"; - os << " %future.1 = insertvalue %uinx.future %future.0, ptr @" << name << "__vtable, 1\n"; - os << " ret %uinx.future %future.1\n}\n\n"; - - os << "define i32 @" << name << "__poll(ptr %frame, ptr %output) {\nentry:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " %state = load i32, ptr %state.addr\n"; - const std::string first_label = fn.blocks.empty() ? "entry" : fn.blocks.front().label; - os << " switch i32 %state, label %async.invalid [ i32 0, label %mir." - << llvm_name(first_label); - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << " i32 " << id << ", label %async.resume." << id; - } - os << " i32 -1, label %async.completed ]\n"; - os << "async.invalid:\n ret i32 -22\n"; - os << "async.completed:\n ret i32 1\n"; - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << "async.resume." << id << ":\n br label %async.await." << id << ".poll\n"; - } - - std::size_t temp_counter = 0; - std::size_t asm_counter = 0; - std::size_t bounds_counter = 0; - std::size_t current_await = 0; - std::unordered_map value_types; - for (const auto& [key, field] : ordered) - value_types[key] = field.type; - - auto frame_addr = [&](const std::string& key) -> std::string { - const auto it = fields.find(key); - if (it == fields.end()) - return key; - const std::string temp = "%af.addr." + std::to_string(temp_counter++); - os << " " << temp << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << it->second.index << "\n"; - return temp; - }; - auto value_of = [&](const std::string& value) -> std::string { - const auto it = fields.find(value); - if (it == fields.end()) - return value; - const std::string address = frame_addr(value); - if (it->second.address) - return address; - const std::string temp = "%af.load." + std::to_string(temp_counter++); - os << " " << temp << " = load " << llvm_type(it->second.type) << ", ptr " << address - << "\n"; - return temp; - }; - auto store_result = [&](const mir::Instruction& instruction, const std::string& value) { - if (instruction.result.empty()) - return; - const auto it = fields.find(instruction.result); - if (it == fields.end() || it->second.address) - return; - const std::string address = frame_addr(instruction.result); - os << " store " << llvm_type(it->second.type) << ' ' << value << ", ptr " << address - << "\n"; - }; - auto mir_label = [&](const std::string& label) { return "mir." + llvm_name(label); }; - - for (const auto& block : fn.blocks) { - os << mir_label(block.label) << ":\n"; - for (const auto& instruction : block.instructions) { - switch (instruction.op) { - case mir::Op::Alloca: - break; - case mir::Op::ConstInt: - store_result(instruction, llvm_integer_constant(instruction.text)); - break; - case mir::Op::ConstFloat: - case mir::Op::ConstBool: - case mir::Op::ConstChar: - store_result(instruction, instruction.text); - break; - case mir::Op::ConstString: { - store_result(instruction, - "getelementptr inbounds (i8, ptr @" + llvm_name(instruction.text) + - ", i64 0)"); - break; - } - case mir::Op::StructInit: { - if (instruction.args.empty()) { - store_result(instruction, "zeroinitializer"); - break; - } - std::string aggregate = "poison"; - for (std::size_t index = 0; index < instruction.args.size(); ++index) { - const std::string arg = value_of(instruction.args[index]); - const Type arg_type = value_types.contains(instruction.args[index]) - ? value_types[instruction.args[index]] - : Type::builtin("i32"); - const std::string next = "%af.insert." + std::to_string(temp_counter++); - os << " " << next << " = insertvalue " << llvm_type(instruction.type) - << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' << arg << ", " - << index << "\n"; - aggregate = next; - } - store_result(instruction, aggregate); - break; - } - case mir::Op::Load: { - const std::string address = value_of(instruction.args[0]); - const std::string result = "%af.op." + std::to_string(temp_counter++); - os << " " << result << " = load "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ", ptr " << address; - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Store: { - const std::string value = value_of(instruction.args[0]); - const std::string address = value_of(instruction.args[1]); - os << " store "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ' ' << value << ", ptr " << address; - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - break; - } - case mir::Op::AtomicRmw: { - const std::string address = value_of(instruction.args[0]); - const std::string value = value_of(instruction.args[1]); - const std::string result = "%af.atomic." + std::to_string(temp_counter++); - os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) - << " ptr " << address << ", " << llvm_type(instruction.type) << ' ' << value - << ' ' << instruction.args[2] << ", align " - << type_alignment(instruction.type) << "\n"; - break; - } - case mir::Op::Fence: - os << " fence " << instruction.text << "\n"; - break; - case mir::Op::CompilerFence: - os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; - break; - case mir::Op::FieldAddr: { - const std::string base = value_of(instruction.args[0]); - const std::string result = "%af.field." + std::to_string(temp_counter++); - os << " " << result << " = getelementptr inbounds " - << llvm_type(instruction.auxiliary_type) << ", ptr " << base - << ", i32 0, i32 " << instruction.text << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::IndexAddr: { - const std::string data = value_of(instruction.args[0]), - index = value_of(instruction.args[1]), - len = value_of(instruction.args[2]); - const int bits = integer_bits(instruction.auxiliary_type); - const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); - const std::string suffix = std::to_string(bounds_counter++); - std::string index64 = index, ok; - if (bits > 64) { - const std::string len_ext = "%af.bound.len." + suffix; - os << " " << len_ext << " = zext i64 " << len << " to " - << llvm_type(instruction.auxiliary_type) << "\n"; - const std::string lt = "%af.bound.lt." + suffix; - os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) - << " " << index << ", " << len_ext << "\n"; - ok = lt; - if (!is_unsigned) { - const std::string nonneg = "%af.bound.nonneg." + suffix; - os << " " << nonneg << " = icmp sge " - << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; - const std::string both = "%af.bound.ok." + suffix; - os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; - ok = both; - } - index64 = "%af.bound.index." + suffix; - os << " " << index64 << " = trunc " - << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; - } else { - if (bits < 64) { - index64 = "%af.bound.index." + suffix; - os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " - << llvm_type(instruction.auxiliary_type) << " " << index - << " to i64\n"; - } - ok = "%af.bound.ok." + suffix; - os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; - } - os << " br i1 " << ok << ", label %async.bounds.ok." << suffix - << ", label %async.bounds.fail." << suffix << "\n"; - os << "async.bounds.fail." << suffix - << ":\n call void @llvm.trap()\n unreachable\n"; - os << "async.bounds.ok." << suffix << ":\n"; - const std::string result = "%af.index." + suffix; - os << " " << result << " = getelementptr inbounds " - << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 - << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::AddressOf: - store_result(instruction, value_of(instruction.args[0])); - break; - case mir::Op::Binary: { - const std::string lhs = value_of(instruction.args[0]); - const std::string result = "%af.bin." + std::to_string(temp_counter++); - if (instruction.type.kind == TypeKind::RawPtr && - (instruction.text == "+" || instruction.text == "-") && - instruction.args.size() == 2) { - const std::string rhs = value_of(instruction.args[1]); - const Type index_type = value_types.contains(instruction.args[1]) - ? value_types.at(instruction.args[1]) - : Type::builtin("isize"); - std::string index = rhs; - if (instruction.text == "-") { - index = "%af.ptr.neg." + std::to_string(temp_counter++); - os << " " << index << " = sub " << llvm_type(index_type) << " 0, " - << rhs << "\n"; - } - const Type element = instruction.type.pointee ? *instruction.type.pointee - : Type::builtin("u8"); - os << " " << result << " = getelementptr " << llvm_type(element) - << ", ptr " << lhs << ", " << llvm_type(index_type) << ' ' << index - << "\n"; - } else if (instruction.text == "!" || instruction.text == "~") { - os << " " << result << " = xor " << llvm_type(instruction.type) << ' ' - << lhs << ", " - << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; - } else { - const std::string rhs = value_of(instruction.args[1]); - os << " " << result << " = " - << arithmetic_opcode(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; - } - store_result(instruction, result); - break; - } - case mir::Op::Compare: { - const std::string lhs = value_of(instruction.args[0]); - const std::string rhs = value_of(instruction.args[1]); - const std::string result = "%af.cmp." + std::to_string(temp_counter++); - os << " " << result << " = " - << (instruction.type.is_float() ? "fcmp " : "icmp ") - << compare_predicate(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Call: { - if (auto arity = syscall_arity_from_name(instruction.text); - arity && instruction.args.size() == static_cast(*arity) + 1) { - std::unordered_map syscall_values; - for (const auto& arg : instruction.args) - syscall_values[arg] = value_of(arg); - if (emit_syscall( - os, instruction, syscall_values, value_types, temp_counter)) { - if (!instruction.result.empty()) { - const auto it = syscall_values.find(instruction.result); - if (it != syscall_values.end()) - store_result(instruction, it->second); - } - break; - } - } - const auto fit = functions.find(instruction.text); - std::vector parameter_types; - if (fit != functions.end()) - for (const auto& param : fit->second->params) - parameter_types.push_back(param.type); - std::vector args; - for (const auto& arg : instruction.args) - args.push_back(value_of(arg)); - const std::string result = instruction.result.empty() - ? "" - : "%af.call." + std::to_string(temp_counter++); - os << " "; - if (!result.empty()) - os << result << " = "; - os << "call " << llvm_type(instruction.type) << " @" - << llvm_name(instruction.text) << '('; - for (std::size_t i = 0; i < args.size(); ++i) { - if (i) - os << ", "; - const Type arg_type = i < parameter_types.size() - ? parameter_types[i] - : (value_types.contains(instruction.args[i]) - ? value_types[instruction.args[i]] - : Type::builtin("i32")); - os << llvm_type(arg_type) << ' ' << args[i]; - } - os << ")\n"; - if (!result.empty()) - store_result(instruction, result); - break; - } - case mir::Op::Cast: { - const std::string value = value_of(instruction.args[0]); - const Type from = value_types.contains(instruction.args[0]) - ? value_types[instruction.args[0]] - : Type::builtin("i32"); - if (from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) { - store_result(instruction, value); - break; - } - std::string opcode; - if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) - opcode = "ptrtoint"; - else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) - opcode = "inttoptr"; - else if (from.is_integer() && instruction.type.is_integer()) { - const int from_bits = integer_bits(from), - to_bits = integer_bits(instruction.type); - opcode = from_bits < to_bits - ? (unsigned_integer(from.kind) ? "zext" : "sext") - : from_bits > to_bits ? "trunc" - : "bitcast"; - } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { - opcode = "zext"; - } else if (from.is_integer() && instruction.type.is_float()) - opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; - else if (from.is_float() && instruction.type.is_integer()) - opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; - else if (from.is_float() && instruction.type.is_float()) - opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; - else - opcode = "bitcast"; - const std::string result = "%af.cast." + std::to_string(temp_counter++); - os << " " << result << " = " << opcode << ' ' << llvm_type(from) << ' ' - << value << " to " << llvm_type(instruction.type) << "\n"; - store_result(instruction, result); - break; - } - case mir::Op::Await: { - const std::size_t id = ++current_await; - os << " br label %async.await." << id << ".poll\n"; - os << "async.await." << id << ".poll:\n"; - const std::string future = value_of(instruction.args[0]); - const std::string child_state = "%await.state." + std::to_string(id); - const std::string vtable = "%await.vtable." + std::to_string(id); - const std::string poll_addr = "%await.poll.addr." + std::to_string(id); - const std::string drop_addr = "%await.drop.addr." + std::to_string(id); - const std::string poll = "%await.pollfn." + std::to_string(id); - const std::string drop = "%await.dropfn." + std::to_string(id); - os << " " << child_state << " = extractvalue %uinx.future " << future - << ", 0\n"; - os << " " << vtable << " = extractvalue %uinx.future " << future << ", 1\n"; - os << " " << poll_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " - << vtable << ", i32 0, i32 0\n"; - os << " " << drop_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " - << vtable << ", i32 0, i32 1\n"; - os << " " << poll << " = load ptr, ptr " << poll_addr << "\n"; - os << " " << drop << " = load ptr, ptr " << drop_addr << "\n"; - const std::string output = "%await.out." + std::to_string(id); - os << " " << output << " = alloca " << llvm_type(instruction.type) << "\n"; - const std::string rc = "%await.rc." + std::to_string(id); - os << " " << rc << " = call i32 " << poll << "(ptr " << child_state << ", ptr " - << output << ")\n"; - const std::string pending = "%await.pending." + std::to_string(id); - os << " " << pending << " = icmp eq i32 " << rc << ", 0\n"; - os << " br i1 " << pending << ", label %async.await." << id - << ".pending, label %async.await." << id << ".nonpending\n"; - os << "async.await." << id << ".pending:\n"; - os << " store i32 " << id << ", ptr %state.addr\n"; - os << " ret i32 0\n"; - os << "async.await." << id << ".nonpending:\n"; - const std::string failed = "%await.failed." + std::to_string(id); - os << " " << failed << " = icmp slt i32 " << rc << ", 0\n"; - os << " br i1 " << failed << ", label %async.await." << id - << ".error, label %async.await." << id << ".ready\n"; - os << "async.await." << id << ".error:\n ret i32 " << rc << "\n"; - os << "async.await." << id << ".ready:\n"; - const std::string ready = "%await.value." + std::to_string(id); - os << " " << ready << " = load " << llvm_type(instruction.type) << ", ptr " - << output << "\n"; - os << " call void " << drop << "(ptr " << child_state << ")\n"; - os << " store i32 0, ptr %state.addr\n"; - store_result(instruction, ready); - os << " br label %async.await." << id << ".cont\n"; - os << "async.await." << id << ".cont:\n"; - break; - } - case mir::Op::InlineAsm: { - mir::Instruction copy = instruction; - std::unordered_map values; - for (auto& operand : copy.asm_operands) { - if (!operand.value.empty()) - values[operand.value] = value_of(operand.value); - if (!operand.out_slot.empty()) - operand.out_slot = value_of(operand.out_slot); - } - emit_inline_asm(os, copy, values, value_types, asm_counter); - break; - } - case mir::Op::Drop: { - const std::string address = value_of(instruction.args[0]); - os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " - << address << ")\n"; - break; - } - case mir::Op::Jump: - os << " br label %" << mir_label(instruction.args[0]) << "\n"; - break; - case mir::Op::CondJump: { - const std::string condition = value_of(instruction.args[0]); - os << " br i1 " << condition << ", label %" << mir_label(instruction.args[1]) - << ", label %" << mir_label(instruction.args[2]) << "\n"; - break; - } - case mir::Op::Return: { - if (!instruction.args.empty()) { - const std::string value = value_of(instruction.args[0]); - os << " store " << llvm_type(instruction.type) << ' ' << value - << ", ptr %output\n"; - } - os << " store i32 -1, ptr %state.addr\n"; - os << " ret i32 1\n"; - break; - } - } - } - } - os << "}\n\n"; - - os << "define void @" << name << "__drop(ptr %frame) {\nentry:\n"; - os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; - os << " %state = load i32, ptr %state.addr\n"; - if (awaits.empty()) { - os << " br label %free\n"; - } else { - os << " switch i32 %state, label %free ["; - for (const auto& [instruction, id] : awaits) { - (void)instruction; - os << " i32 " << id << ", label %cleanup." << id; - } - os << " ]\n"; - for (const auto& [instruction, id] : awaits) { - os << "cleanup." << id << ":\n"; - if (!instruction->args.empty()) { - const auto fit = fields.find(instruction->args[0]); - if (fit != fields.end()) { - os << " %c.addr." << id << " = getelementptr inbounds " << frame_type - << ", ptr %frame, i32 0, i32 " << fit->second.index << "\n"; - os << " %c.future." << id << " = load %uinx.future, ptr %c.addr." << id - << "\n"; - os << " %c.state." << id << " = extractvalue %uinx.future %c.future." << id - << ", 0\n"; - os << " %c.vtable." << id << " = extractvalue %uinx.future %c.future." << id - << ", 1\n"; - os << " %c.drop.addr." << id - << " = getelementptr inbounds %uinx.future.vtable, ptr %c.vtable." << id - << ", i32 0, i32 1\n"; - os << " %c.drop." << id << " = load ptr, ptr %c.drop.addr." << id << "\n"; - os << " call void %c.drop." << id << "(ptr %c.state." << id << ")\n"; - } - } - os << " br label %free\n"; - } - } - os << "free:\n call void @free(ptr %frame)\n ret void\n}\n\n"; -} - -void LLVMCodegen::emit_function( - std::ostream& os, - const mir::Function& fn, - const std::unordered_map& functions) const { - const std::string return_type = fn.is_async ? "%uinx.future" : llvm_type(fn.result); - const std::string name = llvm_name(fn.name); - if (fn.is_extern) { - os << "declare " << return_type << " @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type); - } - os << ")\n\n"; - return; - } - if (fn.is_async) { - emit_async_function(os, fn, functions); - return; - } - - os << "define " << return_type << " @" << name << '('; - for (std::size_t i = 0; i < fn.params.size(); ++i) { - if (i) - os << ", "; - os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); - } - os << ") {\n"; - - std::unordered_map values; - std::unordered_map value_types; - for (const auto& param : fn.params) - value_types["%arg." + param.name] = param.type; - std::size_t asm_counter = 0; - std::size_t bounds_counter = 0; - std::size_t atomic_counter = 0; - for (const auto& block : fn.blocks) { - os << llvm_name(block.label) << ":\n"; - for (const auto& instruction : block.instructions) { - auto value_of = [&](const std::string& value) { - const auto it = values.find(value); - return it == values.end() ? value : it->second; - }; - switch (instruction.op) { - case mir::Op::Alloca: - os << " " << instruction.result << " = alloca " << llvm_type(instruction.type) - << "\n"; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - case mir::Op::ConstInt: - values[instruction.result] = llvm_integer_constant(instruction.text); - value_types[instruction.result] = instruction.type; - break; - case mir::Op::ConstFloat: - case mir::Op::ConstBool: - case mir::Op::ConstChar: - values[instruction.result] = instruction.text; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::ConstString: - values[instruction.result] = "getelementptr inbounds (i8, ptr @" + - llvm_name(instruction.text) + ", i64 0)"; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::StructInit: { - if (instruction.args.empty()) { - values[instruction.result] = "zeroinitializer"; - value_types[instruction.result] = instruction.type; - break; - } - std::string aggregate = "poison"; - for (std::size_t index = 0; index < instruction.args.size(); ++index) { - const std::string next = - index + 1 == instruction.args.size() - ? instruction.result - : instruction.result + ".field." + std::to_string(index); - const Type arg_type = value_types.contains(instruction.args[index]) - ? value_types[instruction.args[index]] - : Type::builtin("i32"); - os << " " << next << " = insertvalue " << llvm_type(instruction.type) - << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' - << value_of(instruction.args[index]) << ", " << index << "\n"; - aggregate = next; - } - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Load: - os << " " << instruction.result << " = load "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ", ptr " << value_of(instruction.args[0]); - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - case mir::Op::Store: - os << " store "; - if (instruction.flag) - os << "atomic "; - os << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", ptr " << value_of(instruction.args[1]); - if (instruction.flag) - os << ' ' << instruction.text << ", align " - << type_alignment(instruction.type); - os << "\n"; - break; - case mir::Op::AtomicRmw: { - const std::string result = "%atomic." + std::to_string(atomic_counter++); - os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) - << " ptr " << value_of(instruction.args[0]) << ", " - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[1]) << ' ' - << instruction.args[2] << ", align " << type_alignment(instruction.type) - << "\n"; - break; - } - case mir::Op::Fence: - os << " fence " << instruction.text << "\n"; - break; - case mir::Op::CompilerFence: - os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; - break; - case mir::Op::FieldAddr: - os << " " << instruction.result << " = getelementptr inbounds " - << llvm_type(instruction.auxiliary_type) << ", ptr " - << value_of(instruction.args[0]) << ", i32 0, i32 " << instruction.text - << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - case mir::Op::IndexAddr: { - const std::string data = value_of(instruction.args[0]), - index = value_of(instruction.args[1]), - len = value_of(instruction.args[2]); - const int bits = integer_bits(instruction.auxiliary_type); - const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); - const std::string suffix = std::to_string(bounds_counter++); - std::string index64 = index, ok; - if (bits > 64) { - const std::string len_ext = "%bound.len." + suffix; - os << " " << len_ext << " = zext i64 " << len << " to " - << llvm_type(instruction.auxiliary_type) << "\n"; - const std::string lt = "%bound.lt." + suffix; - os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) - << " " << index << ", " << len_ext << "\n"; - ok = lt; - if (!is_unsigned) { - const std::string nonneg = "%bound.nonneg." + suffix; - os << " " << nonneg << " = icmp sge " - << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; - const std::string both = "%bound.ok." + suffix; - os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; - ok = both; - } - index64 = "%bound.index." + suffix; - os << " " << index64 << " = trunc " - << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; - } else { - if (bits < 64) { - index64 = "%bound.index." + suffix; - os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " - << llvm_type(instruction.auxiliary_type) << " " << index - << " to i64\n"; - } - ok = "%bound.ok." + suffix; - os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; - } - os << " br i1 " << ok << ", label %bounds.ok." << suffix - << ", label %bounds.fail." << suffix << "\n"; - os << "bounds.fail." << suffix - << ":\n call void @llvm.trap()\n unreachable\n"; - os << "bounds.ok." << suffix << ":\n"; - os << " " << instruction.result << " = getelementptr inbounds " - << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 - << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::raw_ptr(instruction.type, true); - break; - } - case mir::Op::AddressOf: - values[instruction.result] = value_of(instruction.args[0]); - value_types[instruction.result] = instruction.type; - break; - case mir::Op::Binary: { - if (instruction.type.kind == TypeKind::RawPtr && - (instruction.text == "+" || instruction.text == "-") && - instruction.args.size() == 2) { - const Type index_type = value_types.contains(instruction.args[1]) - ? value_types.at(instruction.args[1]) - : Type::builtin("isize"); - std::string index = value_of(instruction.args[1]); - if (instruction.text == "-") { - const std::string neg = instruction.result + ".neg"; - os << " " << neg << " = sub " << llvm_type(index_type) << " 0, " - << index << "\n"; - index = neg; - } - const Type element = instruction.type.pointee ? *instruction.type.pointee - : Type::builtin("u8"); - os << " " << instruction.result << " = getelementptr " - << llvm_type(element) << ", ptr " << value_of(instruction.args[0]) - << ", " << llvm_type(index_type) << ' ' << index << "\n"; - } else if (instruction.text == "!" || instruction.text == "~") { - os << " " << instruction.result << " = xor " << llvm_type(instruction.type) - << ' ' << value_of(instruction.args[0]) << ", " - << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; - } else { - os << " " << instruction.result << " = " - << arithmetic_opcode(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", " << value_of(instruction.args[1]) << "\n"; - } - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Compare: - os << " " << instruction.result << " = " - << (instruction.type.is_float() ? "fcmp " : "icmp ") - << compare_predicate(instruction.type, instruction.text) << ' ' - << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) - << ", " << value_of(instruction.args[1]) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = Type::builtin("bool"); - break; - case mir::Op::Call: { - if (auto arity = syscall_arity_from_name(instruction.text); - arity && instruction.args.size() == static_cast(*arity) + 1) { - if (emit_syscall(os, instruction, values, value_types, asm_counter)) - break; - } - const auto fit = functions.find(instruction.text); - std::vector params; - if (fit != functions.end()) - for (const auto& param : fit->second->params) - params.push_back(param.type); - - if ((instruction.text == "uinx_volatile_load_u8" || - instruction.text == "uinx_volatile_load_u32" || - instruction.text == "uinx_volatile_load_u64") && - instruction.args.size() == 1) { - os << " " << instruction.result << " = load volatile " - << llvm_type(instruction.type) << ", ptr " - << value_of(instruction.args[0]) << ", align " - << type_alignment(instruction.type) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - if ((instruction.text == "uinx_volatile_store_u8" || - instruction.text == "uinx_volatile_store_u32" || - instruction.text == "uinx_volatile_store_u64") && - instruction.args.size() == 2) { - const Type value_type = - params.size() > 1 ? params[1] : Type::builtin("u64"); - os << " store volatile " << llvm_type(value_type) << ' ' - << value_of(instruction.args[1]) << ", ptr " - << value_of(instruction.args[0]) << ", align " - << type_alignment(value_type) << "\n"; - break; - } - if (auto intrinsic = parse_atomic_intrinsic(instruction.text)) { - const std::string& llvm_ty = intrinsic->llvm_type; - const unsigned align = intrinsic->align; - const std::string& rmw = intrinsic->rmw_op; - if (rmw == "load" && instruction.args.size() == 2) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[1])); - order && valid_atomic_load_order(*order)) { - os << " " << instruction.result << " = load atomic " << llvm_ty - << ", ptr " << value_of(instruction.args[0]) << ' ' << *order - << ", align " << align << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - } - if (rmw == "store" && instruction.args.size() == 3) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); - order && valid_atomic_store_order(*order)) { - os << " store atomic " << llvm_ty << ' ' - << value_of(instruction.args[1]) << ", ptr " - << value_of(instruction.args[0]) << ' ' << *order << ", align " - << align << "\n"; - break; - } - } - if ((rmw == "add" || rmw == "sub" || rmw == "and" || rmw == "or" || - rmw == "xor" || rmw == "xchg") && - instruction.args.size() == 3) { - if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); - order && valid_atomic_rmw_order(*order)) { - os << " " << instruction.result << " = atomicrmw " << rmw - << " ptr " << value_of(instruction.args[0]) << ", " << llvm_ty - << ' ' << value_of(instruction.args[1]) << ' ' << *order - << ", align " << align << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - } - if (rmw == "cmpxchg" && instruction.args.size() == 5) { - auto success = atomic_order_from_abi(value_of(instruction.args[3])); - auto failure = atomic_order_from_abi(value_of(instruction.args[4])); - if (success && failure && valid_atomic_rmw_order(*success) && - (valid_atomic_load_order(*failure) || *failure == "monotonic")) { - const std::string failure_fixed = - downgrade_cmpxchg_failure(*success, *failure); - const std::string id = std::to_string(atomic_counter++); - const std::string expected = "%cas.expected." + id; - const std::string pair = "%cas.pair." + id; - const std::string old = "%cas.old." + id; - const std::string ok = "%cas.ok." + id; - const std::string result = instruction.result.empty() - ? "%cas.result." + id - : instruction.result; - os << " " << expected << " = load " << llvm_ty << ", ptr " - << value_of(instruction.args[1]) << ", align " << align << "\n"; - os << " " << pair << " = cmpxchg ptr " - << value_of(instruction.args[0]) << ", " << llvm_ty << ' ' - << expected << ", " << llvm_ty << ' ' - << value_of(instruction.args[2]) << ' ' << *success << ' ' - << failure_fixed << ", align " << align << "\n"; - os << " " << old << " = extractvalue { " << llvm_ty << ", i1 } " - << pair << ", 0\n"; - os << " " << ok << " = extractvalue { " << llvm_ty << ", i1 } " - << pair << ", 1\n"; - os << " store " << llvm_ty << ' ' << old << ", ptr " - << value_of(instruction.args[1]) << ", align " << align << "\n"; - os << " " << result << " = zext i1 " << ok << " to i32\n"; - if (!instruction.result.empty()) { - values[instruction.result] = result; - value_types[instruction.result] = instruction.type; - } - break; - } - } - // Invalid ordering falls through to runtime call (fail-closed - // is handled by sema/codegen validation elsewhere). - } - - os << " "; - if (!instruction.result.empty()) - os << instruction.result << " = "; - os << "call " << llvm_type(instruction.type) << " @" - << llvm_name(instruction.text) << '('; - for (std::size_t i = 0; i < instruction.args.size(); ++i) { - if (i) - os << ", "; - const Type arg_type = i < params.size() - ? params[i] - : (value_types.contains(instruction.args[i]) - ? value_types[instruction.args[i]] - : Type::builtin("i32")); - os << llvm_type(arg_type) << ' ' << value_of(instruction.args[i]); - } - os << ")\n"; - if (!instruction.result.empty()) { - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - } - break; - } - case mir::Op::Cast: { - const Type from = value_types.contains(instruction.args[0]) - ? value_types[instruction.args[0]] - : Type::builtin("i32"); - if ((from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) || - ((from.kind == TypeKind::Ref || from.kind == TypeKind::RawPtr) && - (instruction.type.kind == TypeKind::Ref || - instruction.type.kind == TypeKind::RawPtr))) { - values[instruction.result] = value_of(instruction.args[0]); - value_types[instruction.result] = instruction.type; - break; - } - std::string opcode; - if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) - opcode = "ptrtoint"; - else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) - opcode = "inttoptr"; - else if (from.is_integer() && instruction.type.is_integer()) { - const int from_bits = integer_bits(from), - to_bits = integer_bits(instruction.type); - opcode = from_bits < to_bits - ? (unsigned_integer(from.kind) ? "zext" : "sext") - : from_bits > to_bits ? "trunc" - : "bitcast"; - } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { - opcode = "zext"; - } else if (from.is_integer() && instruction.type.is_float()) - opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; - else if (from.is_float() && instruction.type.is_integer()) - opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; - else if (from.is_float() && instruction.type.is_float()) - opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; - else - opcode = "bitcast"; - os << " " << instruction.result << " = " << opcode << ' ' << llvm_type(from) - << ' ' << value_of(instruction.args[0]) << " to " - << llvm_type(instruction.type) << "\n"; - values[instruction.result] = instruction.result; - value_types[instruction.result] = instruction.type; - break; - } - case mir::Op::Await: - diags_.error(instruction.range, - "E0602", - "internal: await reached synchronous LLVM lowering"); - break; - case mir::Op::InlineAsm: - emit_inline_asm(os, instruction, values, value_types, asm_counter); - break; - case mir::Op::Drop: - os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " - << value_of(instruction.args[0]) << ")\n"; - break; - case mir::Op::Jump: - os << " br label %" << llvm_name(instruction.args[0]) << "\n"; - break; - case mir::Op::CondJump: - os << " br i1 " << value_of(instruction.args[0]) << ", label %" - << llvm_name(instruction.args[1]) << ", label %" - << llvm_name(instruction.args[2]) << "\n"; - break; - case mir::Op::Return: - if (instruction.args.empty()) - os << " ret void\n"; - else - os << " ret " << llvm_type(instruction.type) << ' ' - << value_of(instruction.args[0]) << "\n"; - break; - } - } - } - os << "}\n\n"; -} - std::string LLVMCodegen::emit(const mir::Module& module) { std::ostringstream os; os << "; Uinx LLVM IR - generated by JiTianYu391\n"; @@ -1877,5 +268,4 @@ std::string LLVMCodegen::emit(const mir::Module& module) { emit_function(os, function, functions); return os.str(); } - } // namespace uinx diff --git a/src/codegen_asm.cpp b/src/codegen_asm.cpp new file mode 100644 index 0000000..44bd5e1 --- /dev/null +++ b/src/codegen_asm.cpp @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +bool LLVMCodegen::validate_constraint(std::string_view constraint, const SourceRange& range) const { + std::string raw(constraint); + while (!raw.empty() && std::string("=+&~").find(raw.front()) != std::string::npos) + raw.erase(raw.begin()); + if (raw == "memory" || raw == "cc" || raw == "r" || raw == "m" || raw == "i" || raw == "g") + return true; + if (raw.size() >= 2 && raw.front() == '{' && raw.back() == '}') + raw = raw.substr(1, raw.size() - 2); + + static const std::unordered_set x86 = { + "rax", "rbx", "rcx", "rdx", "rsi", "rdi", "rbp", "rsp", "r8", + "r9", "r10", "r11", "r12", "r13", "r14", "r15", "eax", "ebx", + "ecx", "edx", "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", + "xmm7", "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"}; + auto numbered = [&](char prefix, int max) { + if (raw.size() < 2 || raw.front() != prefix) + return false; + try { + const int n = std::stoi(raw.substr(1)); + return n >= 0 && n <= max; + } catch (...) { + return false; + } + }; + + bool valid = false; + if (target_.arch == "x86_64") { + valid = x86.contains(raw); + } else if (target_.arch == "aarch64") { + valid = numbered('x', 30) || numbered('w', 30) || numbered('v', 31) || raw == "sp"; + } else if (target_.arch == "riscv64") { + valid = numbered('x', 31) || raw == "ra" || raw == "sp" || raw == "gp" || raw == "tp" || + raw == "a0" || raw == "a1" || raw == "a2" || raw == "a3" || raw == "a4" || + raw == "a5" || raw == "a6" || raw == "a7" || raw == "s0" || raw == "s1" || + raw == "t0" || raw == "t1" || raw == "t2"; + } else { + valid = true; + } + if (!valid) { + diags_.error(range, + "E0600", + "asm constraint/register '" + std::string(constraint) + + "' is not valid for target architecture '" + target_.arch + "'"); + } + return valid; +} + +void LLVMCodegen::emit_inline_asm(std::ostream& os, + const mir::Instruction& in, + std::unordered_map& values, + std::unordered_map& value_types, + std::size_t& counter) const { + std::vector outputs; + std::vector inputs; + std::vector clobbers; + for (const auto& operand : in.asm_operands) { + validate_constraint(operand.constraint, in.range); + if (operand.kind == ast::AsmOperand::Kind::Out || + operand.kind == ast::AsmOperand::Kind::InOut) + outputs.push_back(&operand); + else if (operand.kind == ast::AsmOperand::Kind::In) + inputs.push_back(&operand); + else + clobbers.push_back(&operand); + } + + std::vector constraints; + std::vector> call_args; + for (const auto* output : outputs) { + std::string c = output->constraint; + while (!c.empty() && c.front() == '+') + c.erase(c.begin()); + if (c.empty() || c.front() != '=') + c = "=" + c; + constraints.push_back(std::move(c)); + } + for (std::size_t index = 0; index < outputs.size(); ++index) { + const auto* output = outputs[index]; + if (output->kind != ast::AsmOperand::Kind::InOut) + continue; + std::string initial = "undef"; + if (!output->out_slot.empty()) { + initial = "%asm.inout." + std::to_string(counter) + "." + std::to_string(index); + os << " " << initial << " = load " << llvm_type(output->type) << ", ptr " + << output->out_slot << "\n"; + } else if (!output->value.empty()) { + const auto it = values.find(output->value); + initial = it == values.end() ? output->value : it->second; + } + constraints.push_back(std::to_string(index)); + call_args.push_back({output->type, initial}); + } + for (const auto* input : inputs) { + constraints.push_back(input->constraint); + const auto it = values.find(input->value); + call_args.push_back({input->type, it == values.end() ? input->value : it->second}); + } + for (const auto* clobber : clobbers) { + if (clobber->constraint == "memory") + constraints.push_back("~{memory}"); + else if (clobber->constraint == "cc") + constraints.push_back("~{cc}"); + else { + std::string reg = clobber->constraint; + if (reg.front() == '{' && reg.back() == '}') + constraints.push_back("~" + reg); + else + constraints.push_back("~{" + reg + "}"); + } + } + + std::string return_type = "void"; + if (outputs.size() == 1) + return_type = llvm_type(outputs.front()->type); + else if (outputs.size() > 1) { + return_type = "{ "; + for (std::size_t i = 0; i < outputs.size(); ++i) { + if (i) + return_type += ", "; + return_type += llvm_type(outputs[i]->type); + } + return_type += " }"; + } + + std::string constraint_text; + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + + const std::string call_result = outputs.empty() ? "" : "%asm.result." + std::to_string(counter); + os << " "; + if (!call_result.empty()) + os << call_result << " = "; + os << "call " << return_type << " asm "; + if (in.flag) + os << "sideeffect "; + os << '"' << escape_asm(in.text) << "\", \"" << escape_asm(constraint_text) << "\"("; + for (std::size_t i = 0; i < call_args.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(call_args[i].first) << ' ' << call_args[i].second; + } + os << ")\n"; + + for (std::size_t i = 0; i < outputs.size(); ++i) { + const auto* output = outputs[i]; + if (output->out_slot.empty()) + continue; + std::string value = call_result; + if (outputs.size() > 1) { + value = "%asm.out." + std::to_string(counter) + "." + std::to_string(i); + os << " " << value << " = extractvalue " << return_type << ' ' << call_result << ", " + << i << "\n"; + } + os << " store " << llvm_type(output->type) << ' ' << value << ", ptr " << output->out_slot + << "\n"; + value_types[output->out_slot] = output->type; + } + ++counter; +} + +// Lowers the compiler-recognized `uinx_syscall0..6` ABI family into a raw +// kernel trap for the current target. Argument 0 is the syscall number, the +// remaining arguments map to the architecture's syscall argument registers. +// Returns false when the target has no known trap sequence so the caller can +// fall back to an ordinary call. +bool LLVMCodegen::emit_syscall(std::ostream& os, + const mir::Instruction& in, + std::unordered_map& values, + std::unordered_map& value_types, + std::size_t& counter) const { + const int arity = static_cast(in.args.size()); + if (arity < 1 || arity > 7) + return false; + + auto value_of = [&](const std::string& value) -> std::string { + const auto it = values.find(value); + return it == values.end() ? value : it->second; + }; + + // Zero-extend each argument into an i64 so register constraints are + // satisfied regardless of the source integer width. + std::vector args; + args.reserve(static_cast(arity)); + for (const auto& arg : in.args) { + const std::string raw = value_of(arg); + const auto type_it = value_types.find(arg); + const Type type = type_it == value_types.end() ? Type::builtin("i64") : type_it->second; + if (type.str() == "i64" || type.str() == "u64" || type.str() == "usize" || + type.str() == "isize") { + args.push_back(raw); + } else { + args.push_back("%syscall.arg." + std::to_string(counter) + "." + + std::to_string(args.size())); + os << " " << args.back() << " = zext " << llvm_type(type) << ' ' << raw << " to i64\n"; + } + } + + std::string constraint_text; + std::string asm_text; + if (target_.arch == "x86_64") { + // rax = number, rdi/rsi/rdx/r10/r8/r9 = args 1..6. Only the registers + // actually consumed are declared; the kernel additionally clobbers rcx + // (return RIP) and r11 (RFLAGS). + static const char* regs[7] = {"rax", "rdi", "rsi", "rdx", "r10", "r8", "r9"}; + std::vector constraints{"={rax}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{rcx}"); + constraints.push_back("~{r11}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "syscall"; + } else if (target_.arch == "aarch64") { + // x8 = number, x0..x5 = args 1..6. The kernel may clobber x16/x17. + static const char* regs[7] = {"x8", "x0", "x1", "x2", "x3", "x4", "x5"}; + std::vector constraints{"={x0}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{x16}"); + constraints.push_back("~{x17}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "svc #0"; + } else if (target_.arch == "riscv64") { + // a7 = number, a0..a5 = args 1..6. The kernel may clobber a6. + static const char* regs[7] = {"a7", "a0", "a1", "a2", "a3", "a4", "a5"}; + std::vector constraints{"={a0}"}; + for (int i = 0; i < arity; ++i) + constraints.push_back(std::string("{") + regs[i] + "}"); + constraints.push_back("~{a6}"); + constraints.push_back("~{memory}"); + constraints.push_back("~{cc}"); + for (std::size_t i = 0; i < constraints.size(); ++i) { + if (i) + constraint_text += ','; + constraint_text += constraints[i]; + } + asm_text = "ecall"; + } else { + diags_.error(in.range, + "E0601", + "no syscall trap sequence is defined for target architecture '" + + target_.arch + "'"); + return false; + } + + const std::string call_result = "%syscall.result." + std::to_string(counter); + os << " " << call_result << " = call i64 asm sideeffect \"" << asm_text << "\", \"" + << constraint_text << "\"("; + for (std::size_t i = 0; i < args.size(); ++i) { + if (i) + os << ", "; + os << "i64 " << args[i]; + } + os << ")\n"; + + if (!in.result.empty()) { + // The trap returns i64; narrow to the declared result type when needed. + const std::string result_type = llvm_type(in.type); + if (result_type == "i64") { + values[in.result] = call_result; + } else { + const std::string truncated = "%syscall.trunc." + std::to_string(counter); + os << " " << truncated << " = trunc i64 " << call_result << " to " << result_type + << "\n"; + values[in.result] = truncated; + } + value_types[in.result] = in.type; + } + ++counter; + return true; +} +} // namespace uinx diff --git a/src/codegen_async.cpp b/src/codegen_async.cpp new file mode 100644 index 0000000..4e715fc --- /dev/null +++ b/src/codegen_async.cpp @@ -0,0 +1,556 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +void LLVMCodegen::emit_async_function( + std::ostream& os, + const mir::Function& fn, + const std::unordered_map& functions) const { + struct FrameField { + Type type; + std::size_t index{}; + bool address{false}; + }; + + std::unordered_map fields; + std::vector> ordered; + std::size_t next_field = 1; + auto add_field = [&](const std::string& key, Type type, bool address) { + if (key.empty() || fields.contains(key)) + return; + FrameField field{std::move(type), next_field++, address}; + fields.emplace(key, field); + ordered.push_back({key, field}); + }; + + for (const auto& param : fn.params) + add_field("%arg." + param.name, param.type, false); + std::vector> awaits; + std::size_t await_count = 0; + for (const auto& block : fn.blocks) { + for (const auto& instruction : block.instructions) { + if (instruction.op == mir::Op::Await) + awaits.push_back({&instruction, ++await_count}); + if (instruction.result.empty()) + continue; + if (instruction.op == mir::Op::Alloca) + add_field(instruction.result, instruction.type, true); + else if (instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::IndexAddr) + add_field(instruction.result, Type::raw_ptr(instruction.type, true), false); + else + add_field(instruction.result, instruction.type, false); + } + } + + const std::string name = llvm_name(fn.name); + const std::string frame_type = "%uinx.async.frame." + name; + os << frame_type << " = type { i32"; + for (const auto& [key, field] : ordered) { + (void)key; + os << ", " << llvm_type(field.type); + } + os << " }\n"; + os << "@" << name << "__vtable = private constant %uinx.future.vtable { ptr @" << name + << "__poll, ptr @" << name << "__drop }\n"; + + os << "define %uinx.future @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); + } + os << ") {\nentry:\n"; + os << " %frame.size.ptr = getelementptr " << frame_type << ", ptr null, i32 1\n"; + os << " %frame.size = ptrtoint ptr %frame.size.ptr to i64\n"; + os << " %frame = call ptr @malloc(i64 %frame.size)\n"; + os << " %oom = icmp eq ptr %frame, null\n"; + os << " br i1 %oom, label %async.oom, label %async.init\n"; + os << "async.oom:\n call void @abort()\n unreachable\n"; + os << "async.init:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " store i32 0, ptr %state.addr\n"; + for (const auto& param : fn.params) { + const auto& field = fields.at("%arg." + param.name); + os << " %param.addr." << field.index << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << field.index << "\n"; + os << " store " << llvm_type(param.type) << " %arg." << llvm_name(param.name) + << ", ptr %param.addr." << field.index << "\n"; + } + os << " %future.0 = insertvalue %uinx.future poison, ptr %frame, 0\n"; + os << " %future.1 = insertvalue %uinx.future %future.0, ptr @" << name << "__vtable, 1\n"; + os << " ret %uinx.future %future.1\n}\n\n"; + + os << "define i32 @" << name << "__poll(ptr %frame, ptr %output) {\nentry:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " %state = load i32, ptr %state.addr\n"; + const std::string first_label = fn.blocks.empty() ? "entry" : fn.blocks.front().label; + os << " switch i32 %state, label %async.invalid [ i32 0, label %mir." + << llvm_name(first_label); + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << " i32 " << id << ", label %async.resume." << id; + } + os << " i32 -1, label %async.completed ]\n"; + os << "async.invalid:\n ret i32 -22\n"; + os << "async.completed:\n ret i32 1\n"; + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << "async.resume." << id << ":\n br label %async.await." << id << ".poll\n"; + } + + std::size_t temp_counter = 0; + std::size_t asm_counter = 0; + std::size_t bounds_counter = 0; + std::size_t current_await = 0; + std::unordered_map value_types; + for (const auto& [key, field] : ordered) + value_types[key] = field.type; + + auto frame_addr = [&](const std::string& key) -> std::string { + const auto it = fields.find(key); + if (it == fields.end()) + return key; + const std::string temp = "%af.addr." + std::to_string(temp_counter++); + os << " " << temp << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << it->second.index << "\n"; + return temp; + }; + auto value_of = [&](const std::string& value) -> std::string { + const auto it = fields.find(value); + if (it == fields.end()) + return value; + const std::string address = frame_addr(value); + if (it->second.address) + return address; + const std::string temp = "%af.load." + std::to_string(temp_counter++); + os << " " << temp << " = load " << llvm_type(it->second.type) << ", ptr " << address + << "\n"; + return temp; + }; + auto store_result = [&](const mir::Instruction& instruction, const std::string& value) { + if (instruction.result.empty()) + return; + const auto it = fields.find(instruction.result); + if (it == fields.end() || it->second.address) + return; + const std::string address = frame_addr(instruction.result); + os << " store " << llvm_type(it->second.type) << ' ' << value << ", ptr " << address + << "\n"; + }; + auto mir_label = [&](const std::string& label) { return "mir." + llvm_name(label); }; + + for (const auto& block : fn.blocks) { + os << mir_label(block.label) << ":\n"; + for (const auto& instruction : block.instructions) { + switch (instruction.op) { + case mir::Op::Alloca: + break; + case mir::Op::ConstInt: + store_result(instruction, llvm_integer_constant(instruction.text)); + break; + case mir::Op::ConstFloat: + case mir::Op::ConstBool: + case mir::Op::ConstChar: + store_result(instruction, instruction.text); + break; + case mir::Op::ConstString: { + store_result(instruction, + "getelementptr inbounds (i8, ptr @" + llvm_name(instruction.text) + + ", i64 0)"); + break; + } + case mir::Op::StructInit: { + if (instruction.args.empty()) { + store_result(instruction, "zeroinitializer"); + break; + } + std::string aggregate = "poison"; + for (std::size_t index = 0; index < instruction.args.size(); ++index) { + const std::string arg = value_of(instruction.args[index]); + const Type arg_type = value_types.contains(instruction.args[index]) + ? value_types[instruction.args[index]] + : Type::builtin("i32"); + const std::string next = "%af.insert." + std::to_string(temp_counter++); + os << " " << next << " = insertvalue " << llvm_type(instruction.type) + << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' << arg << ", " + << index << "\n"; + aggregate = next; + } + store_result(instruction, aggregate); + break; + } + case mir::Op::Load: { + const std::string address = value_of(instruction.args[0]); + const std::string result = "%af.op." + std::to_string(temp_counter++); + os << " " << result << " = load "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ", ptr " << address; + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Store: { + const std::string value = value_of(instruction.args[0]); + const std::string address = value_of(instruction.args[1]); + os << " store "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ' ' << value << ", ptr " << address; + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + break; + } + case mir::Op::AtomicRmw: { + const std::string address = value_of(instruction.args[0]); + const std::string value = value_of(instruction.args[1]); + const std::string result = "%af.atomic." + std::to_string(temp_counter++); + os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) + << " ptr " << address << ", " << llvm_type(instruction.type) << ' ' << value + << ' ' << instruction.args[2] << ", align " + << type_alignment(instruction.type) << "\n"; + break; + } + case mir::Op::Fence: + os << " fence " << instruction.text << "\n"; + break; + case mir::Op::CompilerFence: + os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; + break; + case mir::Op::FieldAddr: { + const std::string base = value_of(instruction.args[0]); + const std::string result = "%af.field." + std::to_string(temp_counter++); + os << " " << result << " = getelementptr inbounds " + << llvm_type(instruction.auxiliary_type) << ", ptr " << base + << ", i32 0, i32 " << instruction.text << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::IndexAddr: { + const std::string data = value_of(instruction.args[0]), + index = value_of(instruction.args[1]), + len = value_of(instruction.args[2]); + const int bits = integer_bits(instruction.auxiliary_type); + const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); + const std::string suffix = std::to_string(bounds_counter++); + std::string index64 = index, ok; + if (bits > 64) { + const std::string len_ext = "%af.bound.len." + suffix; + os << " " << len_ext << " = zext i64 " << len << " to " + << llvm_type(instruction.auxiliary_type) << "\n"; + const std::string lt = "%af.bound.lt." + suffix; + os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) + << " " << index << ", " << len_ext << "\n"; + ok = lt; + if (!is_unsigned) { + const std::string nonneg = "%af.bound.nonneg." + suffix; + os << " " << nonneg << " = icmp sge " + << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; + const std::string both = "%af.bound.ok." + suffix; + os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; + ok = both; + } + index64 = "%af.bound.index." + suffix; + os << " " << index64 << " = trunc " + << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; + } else { + if (bits < 64) { + index64 = "%af.bound.index." + suffix; + os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " + << llvm_type(instruction.auxiliary_type) << " " << index + << " to i64\n"; + } + ok = "%af.bound.ok." + suffix; + os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; + } + os << " br i1 " << ok << ", label %async.bounds.ok." << suffix + << ", label %async.bounds.fail." << suffix << "\n"; + os << "async.bounds.fail." << suffix + << ":\n call void @llvm.trap()\n unreachable\n"; + os << "async.bounds.ok." << suffix << ":\n"; + const std::string result = "%af.index." + suffix; + os << " " << result << " = getelementptr inbounds " + << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 + << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::AddressOf: + store_result(instruction, value_of(instruction.args[0])); + break; + case mir::Op::Binary: { + const std::string lhs = value_of(instruction.args[0]); + const std::string result = "%af.bin." + std::to_string(temp_counter++); + if (instruction.type.kind == TypeKind::RawPtr && + (instruction.text == "+" || instruction.text == "-") && + instruction.args.size() == 2) { + const std::string rhs = value_of(instruction.args[1]); + const Type index_type = value_types.contains(instruction.args[1]) + ? value_types.at(instruction.args[1]) + : Type::builtin("isize"); + std::string index = rhs; + if (instruction.text == "-") { + index = "%af.ptr.neg." + std::to_string(temp_counter++); + os << " " << index << " = sub " << llvm_type(index_type) << " 0, " + << rhs << "\n"; + } + const Type element = instruction.type.pointee ? *instruction.type.pointee + : Type::builtin("u8"); + os << " " << result << " = getelementptr " << llvm_type(element) + << ", ptr " << lhs << ", " << llvm_type(index_type) << ' ' << index + << "\n"; + } else if (instruction.text == "!" || instruction.text == "~") { + os << " " << result << " = xor " << llvm_type(instruction.type) << ' ' + << lhs << ", " + << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; + } else { + const std::string rhs = value_of(instruction.args[1]); + os << " " << result << " = " + << arithmetic_opcode(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; + } + store_result(instruction, result); + break; + } + case mir::Op::Compare: { + const std::string lhs = value_of(instruction.args[0]); + const std::string rhs = value_of(instruction.args[1]); + const std::string result = "%af.cmp." + std::to_string(temp_counter++); + os << " " << result << " = " + << (instruction.type.is_float() ? "fcmp " : "icmp ") + << compare_predicate(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << lhs << ", " << rhs << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Call: { + if (auto arity = syscall_arity_from_name(instruction.text); + arity && instruction.args.size() == static_cast(*arity) + 1) { + std::unordered_map syscall_values; + for (const auto& arg : instruction.args) + syscall_values[arg] = value_of(arg); + if (emit_syscall( + os, instruction, syscall_values, value_types, temp_counter)) { + if (!instruction.result.empty()) { + const auto it = syscall_values.find(instruction.result); + if (it != syscall_values.end()) + store_result(instruction, it->second); + } + break; + } + } + const auto fit = functions.find(instruction.text); + std::vector parameter_types; + if (fit != functions.end()) + for (const auto& param : fit->second->params) + parameter_types.push_back(param.type); + std::vector args; + for (const auto& arg : instruction.args) + args.push_back(value_of(arg)); + const std::string result = instruction.result.empty() + ? "" + : "%af.call." + std::to_string(temp_counter++); + os << " "; + if (!result.empty()) + os << result << " = "; + os << "call " << llvm_type(instruction.type) << " @" + << llvm_name(instruction.text) << '('; + for (std::size_t i = 0; i < args.size(); ++i) { + if (i) + os << ", "; + const Type arg_type = i < parameter_types.size() + ? parameter_types[i] + : (value_types.contains(instruction.args[i]) + ? value_types[instruction.args[i]] + : Type::builtin("i32")); + os << llvm_type(arg_type) << ' ' << args[i]; + } + os << ")\n"; + if (!result.empty()) + store_result(instruction, result); + break; + } + case mir::Op::Cast: { + const std::string value = value_of(instruction.args[0]); + const Type from = value_types.contains(instruction.args[0]) + ? value_types[instruction.args[0]] + : Type::builtin("i32"); + if (from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) { + store_result(instruction, value); + break; + } + std::string opcode; + if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) + opcode = "ptrtoint"; + else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) + opcode = "inttoptr"; + else if (from.is_integer() && instruction.type.is_integer()) { + const int from_bits = integer_bits(from), + to_bits = integer_bits(instruction.type); + opcode = from_bits < to_bits + ? (unsigned_integer(from.kind) ? "zext" : "sext") + : from_bits > to_bits ? "trunc" + : "bitcast"; + } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { + opcode = "zext"; + } else if (from.is_integer() && instruction.type.is_float()) + opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; + else if (from.is_float() && instruction.type.is_integer()) + opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; + else if (from.is_float() && instruction.type.is_float()) + opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; + else + opcode = "bitcast"; + const std::string result = "%af.cast." + std::to_string(temp_counter++); + os << " " << result << " = " << opcode << ' ' << llvm_type(from) << ' ' + << value << " to " << llvm_type(instruction.type) << "\n"; + store_result(instruction, result); + break; + } + case mir::Op::Await: { + const std::size_t id = ++current_await; + os << " br label %async.await." << id << ".poll\n"; + os << "async.await." << id << ".poll:\n"; + const std::string future = value_of(instruction.args[0]); + const std::string child_state = "%await.state." + std::to_string(id); + const std::string vtable = "%await.vtable." + std::to_string(id); + const std::string poll_addr = "%await.poll.addr." + std::to_string(id); + const std::string drop_addr = "%await.drop.addr." + std::to_string(id); + const std::string poll = "%await.pollfn." + std::to_string(id); + const std::string drop = "%await.dropfn." + std::to_string(id); + os << " " << child_state << " = extractvalue %uinx.future " << future + << ", 0\n"; + os << " " << vtable << " = extractvalue %uinx.future " << future << ", 1\n"; + os << " " << poll_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " + << vtable << ", i32 0, i32 0\n"; + os << " " << drop_addr << " = getelementptr inbounds %uinx.future.vtable, ptr " + << vtable << ", i32 0, i32 1\n"; + os << " " << poll << " = load ptr, ptr " << poll_addr << "\n"; + os << " " << drop << " = load ptr, ptr " << drop_addr << "\n"; + const std::string output = "%await.out." + std::to_string(id); + os << " " << output << " = alloca " << llvm_type(instruction.type) << "\n"; + const std::string rc = "%await.rc." + std::to_string(id); + os << " " << rc << " = call i32 " << poll << "(ptr " << child_state << ", ptr " + << output << ")\n"; + const std::string pending = "%await.pending." + std::to_string(id); + os << " " << pending << " = icmp eq i32 " << rc << ", 0\n"; + os << " br i1 " << pending << ", label %async.await." << id + << ".pending, label %async.await." << id << ".nonpending\n"; + os << "async.await." << id << ".pending:\n"; + os << " store i32 " << id << ", ptr %state.addr\n"; + os << " ret i32 0\n"; + os << "async.await." << id << ".nonpending:\n"; + const std::string failed = "%await.failed." + std::to_string(id); + os << " " << failed << " = icmp slt i32 " << rc << ", 0\n"; + os << " br i1 " << failed << ", label %async.await." << id + << ".error, label %async.await." << id << ".ready\n"; + os << "async.await." << id << ".error:\n ret i32 " << rc << "\n"; + os << "async.await." << id << ".ready:\n"; + const std::string ready = "%await.value." + std::to_string(id); + os << " " << ready << " = load " << llvm_type(instruction.type) << ", ptr " + << output << "\n"; + os << " call void " << drop << "(ptr " << child_state << ")\n"; + os << " store i32 0, ptr %state.addr\n"; + store_result(instruction, ready); + os << " br label %async.await." << id << ".cont\n"; + os << "async.await." << id << ".cont:\n"; + break; + } + case mir::Op::InlineAsm: { + mir::Instruction copy = instruction; + std::unordered_map values; + for (auto& operand : copy.asm_operands) { + if (!operand.value.empty()) + values[operand.value] = value_of(operand.value); + if (!operand.out_slot.empty()) + operand.out_slot = value_of(operand.out_slot); + } + emit_inline_asm(os, copy, values, value_types, asm_counter); + break; + } + case mir::Op::Drop: { + const std::string address = value_of(instruction.args[0]); + os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " + << address << ")\n"; + break; + } + case mir::Op::Jump: + os << " br label %" << mir_label(instruction.args[0]) << "\n"; + break; + case mir::Op::CondJump: { + const std::string condition = value_of(instruction.args[0]); + os << " br i1 " << condition << ", label %" << mir_label(instruction.args[1]) + << ", label %" << mir_label(instruction.args[2]) << "\n"; + break; + } + case mir::Op::Return: { + if (!instruction.args.empty()) { + const std::string value = value_of(instruction.args[0]); + os << " store " << llvm_type(instruction.type) << ' ' << value + << ", ptr %output\n"; + } + os << " store i32 -1, ptr %state.addr\n"; + os << " ret i32 1\n"; + break; + } + } + } + } + os << "}\n\n"; + + os << "define void @" << name << "__drop(ptr %frame) {\nentry:\n"; + os << " %state.addr = getelementptr inbounds " << frame_type << ", ptr %frame, i32 0, i32 0\n"; + os << " %state = load i32, ptr %state.addr\n"; + if (awaits.empty()) { + os << " br label %free\n"; + } else { + os << " switch i32 %state, label %free ["; + for (const auto& [instruction, id] : awaits) { + (void)instruction; + os << " i32 " << id << ", label %cleanup." << id; + } + os << " ]\n"; + for (const auto& [instruction, id] : awaits) { + os << "cleanup." << id << ":\n"; + if (!instruction->args.empty()) { + const auto fit = fields.find(instruction->args[0]); + if (fit != fields.end()) { + os << " %c.addr." << id << " = getelementptr inbounds " << frame_type + << ", ptr %frame, i32 0, i32 " << fit->second.index << "\n"; + os << " %c.future." << id << " = load %uinx.future, ptr %c.addr." << id + << "\n"; + os << " %c.state." << id << " = extractvalue %uinx.future %c.future." << id + << ", 0\n"; + os << " %c.vtable." << id << " = extractvalue %uinx.future %c.future." << id + << ", 1\n"; + os << " %c.drop.addr." << id + << " = getelementptr inbounds %uinx.future.vtable, ptr %c.vtable." << id + << ", i32 0, i32 1\n"; + os << " %c.drop." << id << " = load ptr, ptr %c.drop.addr." << id << "\n"; + os << " call void %c.drop." << id << "(ptr %c.state." << id << ")\n"; + } + } + os << " br label %free\n"; + } + } + os << "free:\n call void @free(ptr %frame)\n ret void\n}\n\n"; +} +} // namespace uinx diff --git a/src/codegen_function.cpp b/src/codegen_function.cpp new file mode 100644 index 0000000..2786eea --- /dev/null +++ b/src/codegen_function.cpp @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "codegen_internal.hpp" +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace codegen_detail; + +void LLVMCodegen::emit_function( + std::ostream& os, + const mir::Function& fn, + const std::unordered_map& functions) const { + const std::string return_type = fn.is_async ? "%uinx.future" : llvm_type(fn.result); + const std::string name = llvm_name(fn.name); + if (fn.is_extern) { + os << "declare " << return_type << " @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type); + } + os << ")\n\n"; + return; + } + if (fn.is_async) { + emit_async_function(os, fn, functions); + return; + } + + os << "define " << return_type << " @" << name << '('; + for (std::size_t i = 0; i < fn.params.size(); ++i) { + if (i) + os << ", "; + os << llvm_type(fn.params[i].type) << " %arg." << llvm_name(fn.params[i].name); + } + os << ") {\n"; + + std::unordered_map values; + std::unordered_map value_types; + for (const auto& param : fn.params) + value_types["%arg." + param.name] = param.type; + std::size_t asm_counter = 0; + std::size_t bounds_counter = 0; + std::size_t atomic_counter = 0; + for (const auto& block : fn.blocks) { + os << llvm_name(block.label) << ":\n"; + for (const auto& instruction : block.instructions) { + auto value_of = [&](const std::string& value) { + const auto it = values.find(value); + return it == values.end() ? value : it->second; + }; + switch (instruction.op) { + case mir::Op::Alloca: + os << " " << instruction.result << " = alloca " << llvm_type(instruction.type) + << "\n"; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + case mir::Op::ConstInt: + values[instruction.result] = llvm_integer_constant(instruction.text); + value_types[instruction.result] = instruction.type; + break; + case mir::Op::ConstFloat: + case mir::Op::ConstBool: + case mir::Op::ConstChar: + values[instruction.result] = instruction.text; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::ConstString: + values[instruction.result] = "getelementptr inbounds (i8, ptr @" + + llvm_name(instruction.text) + ", i64 0)"; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::StructInit: { + if (instruction.args.empty()) { + values[instruction.result] = "zeroinitializer"; + value_types[instruction.result] = instruction.type; + break; + } + std::string aggregate = "poison"; + for (std::size_t index = 0; index < instruction.args.size(); ++index) { + const std::string next = + index + 1 == instruction.args.size() + ? instruction.result + : instruction.result + ".field." + std::to_string(index); + const Type arg_type = value_types.contains(instruction.args[index]) + ? value_types[instruction.args[index]] + : Type::builtin("i32"); + os << " " << next << " = insertvalue " << llvm_type(instruction.type) + << ' ' << aggregate << ", " << llvm_type(arg_type) << ' ' + << value_of(instruction.args[index]) << ", " << index << "\n"; + aggregate = next; + } + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Load: + os << " " << instruction.result << " = load "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ", ptr " << value_of(instruction.args[0]); + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + case mir::Op::Store: + os << " store "; + if (instruction.flag) + os << "atomic "; + os << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", ptr " << value_of(instruction.args[1]); + if (instruction.flag) + os << ' ' << instruction.text << ", align " + << type_alignment(instruction.type); + os << "\n"; + break; + case mir::Op::AtomicRmw: { + const std::string result = "%atomic." + std::to_string(atomic_counter++); + os << " " << result << " = atomicrmw " << atomic_rmw_opcode(instruction.text) + << " ptr " << value_of(instruction.args[0]) << ", " + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[1]) << ' ' + << instruction.args[2] << ", align " << type_alignment(instruction.type) + << "\n"; + break; + } + case mir::Op::Fence: + os << " fence " << instruction.text << "\n"; + break; + case mir::Op::CompilerFence: + os << " fence syncscope(\"singlethread\") " << instruction.text << "\n"; + break; + case mir::Op::FieldAddr: + os << " " << instruction.result << " = getelementptr inbounds " + << llvm_type(instruction.auxiliary_type) << ", ptr " + << value_of(instruction.args[0]) << ", i32 0, i32 " << instruction.text + << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + case mir::Op::IndexAddr: { + const std::string data = value_of(instruction.args[0]), + index = value_of(instruction.args[1]), + len = value_of(instruction.args[2]); + const int bits = integer_bits(instruction.auxiliary_type); + const bool is_unsigned = unsigned_integer(instruction.auxiliary_type.kind); + const std::string suffix = std::to_string(bounds_counter++); + std::string index64 = index, ok; + if (bits > 64) { + const std::string len_ext = "%bound.len." + suffix; + os << " " << len_ext << " = zext i64 " << len << " to " + << llvm_type(instruction.auxiliary_type) << "\n"; + const std::string lt = "%bound.lt." + suffix; + os << " " << lt << " = icmp ult " << llvm_type(instruction.auxiliary_type) + << " " << index << ", " << len_ext << "\n"; + ok = lt; + if (!is_unsigned) { + const std::string nonneg = "%bound.nonneg." + suffix; + os << " " << nonneg << " = icmp sge " + << llvm_type(instruction.auxiliary_type) << " " << index << ", 0\n"; + const std::string both = "%bound.ok." + suffix; + os << " " << both << " = and i1 " << nonneg << ", " << lt << "\n"; + ok = both; + } + index64 = "%bound.index." + suffix; + os << " " << index64 << " = trunc " + << llvm_type(instruction.auxiliary_type) << " " << index << " to i64\n"; + } else { + if (bits < 64) { + index64 = "%bound.index." + suffix; + os << " " << index64 << " = " << (is_unsigned ? "zext" : "sext") << " " + << llvm_type(instruction.auxiliary_type) << " " << index + << " to i64\n"; + } + ok = "%bound.ok." + suffix; + os << " " << ok << " = icmp ult i64 " << index64 << ", " << len << "\n"; + } + os << " br i1 " << ok << ", label %bounds.ok." << suffix + << ", label %bounds.fail." << suffix << "\n"; + os << "bounds.fail." << suffix + << ":\n call void @llvm.trap()\n unreachable\n"; + os << "bounds.ok." << suffix << ":\n"; + os << " " << instruction.result << " = getelementptr inbounds " + << llvm_type(instruction.type) << ", ptr " << data << ", i64 " << index64 + << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::raw_ptr(instruction.type, true); + break; + } + case mir::Op::AddressOf: + values[instruction.result] = value_of(instruction.args[0]); + value_types[instruction.result] = instruction.type; + break; + case mir::Op::Binary: { + if (instruction.type.kind == TypeKind::RawPtr && + (instruction.text == "+" || instruction.text == "-") && + instruction.args.size() == 2) { + const Type index_type = value_types.contains(instruction.args[1]) + ? value_types.at(instruction.args[1]) + : Type::builtin("isize"); + std::string index = value_of(instruction.args[1]); + if (instruction.text == "-") { + const std::string neg = instruction.result + ".neg"; + os << " " << neg << " = sub " << llvm_type(index_type) << " 0, " + << index << "\n"; + index = neg; + } + const Type element = instruction.type.pointee ? *instruction.type.pointee + : Type::builtin("u8"); + os << " " << instruction.result << " = getelementptr " + << llvm_type(element) << ", ptr " << value_of(instruction.args[0]) + << ", " << llvm_type(index_type) << ' ' << index << "\n"; + } else if (instruction.text == "!" || instruction.text == "~") { + os << " " << instruction.result << " = xor " << llvm_type(instruction.type) + << ' ' << value_of(instruction.args[0]) << ", " + << (instruction.type.kind == TypeKind::Bool ? "true" : "-1") << "\n"; + } else { + os << " " << instruction.result << " = " + << arithmetic_opcode(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", " << value_of(instruction.args[1]) << "\n"; + } + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Compare: + os << " " << instruction.result << " = " + << (instruction.type.is_float() ? "fcmp " : "icmp ") + << compare_predicate(instruction.type, instruction.text) << ' ' + << llvm_type(instruction.type) << ' ' << value_of(instruction.args[0]) + << ", " << value_of(instruction.args[1]) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = Type::builtin("bool"); + break; + case mir::Op::Call: { + if (auto arity = syscall_arity_from_name(instruction.text); + arity && instruction.args.size() == static_cast(*arity) + 1) { + if (emit_syscall(os, instruction, values, value_types, asm_counter)) + break; + } + const auto fit = functions.find(instruction.text); + std::vector params; + if (fit != functions.end()) + for (const auto& param : fit->second->params) + params.push_back(param.type); + + if ((instruction.text == "uinx_volatile_load_u8" || + instruction.text == "uinx_volatile_load_u32" || + instruction.text == "uinx_volatile_load_u64") && + instruction.args.size() == 1) { + os << " " << instruction.result << " = load volatile " + << llvm_type(instruction.type) << ", ptr " + << value_of(instruction.args[0]) << ", align " + << type_alignment(instruction.type) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + if ((instruction.text == "uinx_volatile_store_u8" || + instruction.text == "uinx_volatile_store_u32" || + instruction.text == "uinx_volatile_store_u64") && + instruction.args.size() == 2) { + const Type value_type = + params.size() > 1 ? params[1] : Type::builtin("u64"); + os << " store volatile " << llvm_type(value_type) << ' ' + << value_of(instruction.args[1]) << ", ptr " + << value_of(instruction.args[0]) << ", align " + << type_alignment(value_type) << "\n"; + break; + } + if (auto intrinsic = parse_atomic_intrinsic(instruction.text)) { + const std::string& llvm_ty = intrinsic->llvm_type; + const unsigned align = intrinsic->align; + const std::string& rmw = intrinsic->rmw_op; + if (rmw == "load" && instruction.args.size() == 2) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[1])); + order && valid_atomic_load_order(*order)) { + os << " " << instruction.result << " = load atomic " << llvm_ty + << ", ptr " << value_of(instruction.args[0]) << ' ' << *order + << ", align " << align << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + } + if (rmw == "store" && instruction.args.size() == 3) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); + order && valid_atomic_store_order(*order)) { + os << " store atomic " << llvm_ty << ' ' + << value_of(instruction.args[1]) << ", ptr " + << value_of(instruction.args[0]) << ' ' << *order << ", align " + << align << "\n"; + break; + } + } + if ((rmw == "add" || rmw == "sub" || rmw == "and" || rmw == "or" || + rmw == "xor" || rmw == "xchg") && + instruction.args.size() == 3) { + if (auto order = atomic_order_from_abi(value_of(instruction.args[2])); + order && valid_atomic_rmw_order(*order)) { + os << " " << instruction.result << " = atomicrmw " << rmw + << " ptr " << value_of(instruction.args[0]) << ", " << llvm_ty + << ' ' << value_of(instruction.args[1]) << ' ' << *order + << ", align " << align << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + } + if (rmw == "cmpxchg" && instruction.args.size() == 5) { + auto success = atomic_order_from_abi(value_of(instruction.args[3])); + auto failure = atomic_order_from_abi(value_of(instruction.args[4])); + if (success && failure && valid_atomic_rmw_order(*success) && + (valid_atomic_load_order(*failure) || *failure == "monotonic")) { + const std::string failure_fixed = + downgrade_cmpxchg_failure(*success, *failure); + const std::string id = std::to_string(atomic_counter++); + const std::string expected = "%cas.expected." + id; + const std::string pair = "%cas.pair." + id; + const std::string old = "%cas.old." + id; + const std::string ok = "%cas.ok." + id; + const std::string result = instruction.result.empty() + ? "%cas.result." + id + : instruction.result; + os << " " << expected << " = load " << llvm_ty << ", ptr " + << value_of(instruction.args[1]) << ", align " << align << "\n"; + os << " " << pair << " = cmpxchg ptr " + << value_of(instruction.args[0]) << ", " << llvm_ty << ' ' + << expected << ", " << llvm_ty << ' ' + << value_of(instruction.args[2]) << ' ' << *success << ' ' + << failure_fixed << ", align " << align << "\n"; + os << " " << old << " = extractvalue { " << llvm_ty << ", i1 } " + << pair << ", 0\n"; + os << " " << ok << " = extractvalue { " << llvm_ty << ", i1 } " + << pair << ", 1\n"; + os << " store " << llvm_ty << ' ' << old << ", ptr " + << value_of(instruction.args[1]) << ", align " << align << "\n"; + os << " " << result << " = zext i1 " << ok << " to i32\n"; + if (!instruction.result.empty()) { + values[instruction.result] = result; + value_types[instruction.result] = instruction.type; + } + break; + } + } + // Invalid ordering falls through to runtime call (fail-closed + // is handled by sema/codegen validation elsewhere). + } + + os << " "; + if (!instruction.result.empty()) + os << instruction.result << " = "; + os << "call " << llvm_type(instruction.type) << " @" + << llvm_name(instruction.text) << '('; + for (std::size_t i = 0; i < instruction.args.size(); ++i) { + if (i) + os << ", "; + const Type arg_type = i < params.size() + ? params[i] + : (value_types.contains(instruction.args[i]) + ? value_types[instruction.args[i]] + : Type::builtin("i32")); + os << llvm_type(arg_type) << ' ' << value_of(instruction.args[i]); + } + os << ")\n"; + if (!instruction.result.empty()) { + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + } + break; + } + case mir::Op::Cast: { + const Type from = value_types.contains(instruction.args[0]) + ? value_types[instruction.args[0]] + : Type::builtin("i32"); + if ((from.kind == TypeKind::Str && instruction.type.kind == TypeKind::RawPtr) || + ((from.kind == TypeKind::Ref || from.kind == TypeKind::RawPtr) && + (instruction.type.kind == TypeKind::Ref || + instruction.type.kind == TypeKind::RawPtr))) { + values[instruction.result] = value_of(instruction.args[0]); + value_types[instruction.result] = instruction.type; + break; + } + std::string opcode; + if (from.kind == TypeKind::RawPtr && instruction.type.is_integer()) + opcode = "ptrtoint"; + else if (from.is_integer() && instruction.type.kind == TypeKind::RawPtr) + opcode = "inttoptr"; + else if (from.is_integer() && instruction.type.is_integer()) { + const int from_bits = integer_bits(from), + to_bits = integer_bits(instruction.type); + opcode = from_bits < to_bits + ? (unsigned_integer(from.kind) ? "zext" : "sext") + : from_bits > to_bits ? "trunc" + : "bitcast"; + } else if (from.kind == TypeKind::Bool && instruction.type.is_integer()) { + opcode = "zext"; + } else if (from.is_integer() && instruction.type.is_float()) + opcode = unsigned_integer(from.kind) ? "uitofp" : "sitofp"; + else if (from.is_float() && instruction.type.is_integer()) + opcode = unsigned_integer(instruction.type.kind) ? "fptoui" : "fptosi"; + else if (from.is_float() && instruction.type.is_float()) + opcode = from.kind == TypeKind::F32 ? "fpext" : "fptrunc"; + else + opcode = "bitcast"; + os << " " << instruction.result << " = " << opcode << ' ' << llvm_type(from) + << ' ' << value_of(instruction.args[0]) << " to " + << llvm_type(instruction.type) << "\n"; + values[instruction.result] = instruction.result; + value_types[instruction.result] = instruction.type; + break; + } + case mir::Op::Await: + diags_.error(instruction.range, + "E0602", + "internal: await reached synchronous LLVM lowering"); + break; + case mir::Op::InlineAsm: + emit_inline_asm(os, instruction, values, value_types, asm_counter); + break; + case mir::Op::Drop: + os << " call void @" << llvm_name(instruction.type.name + "__drop") << "(ptr " + << value_of(instruction.args[0]) << ")\n"; + break; + case mir::Op::Jump: + os << " br label %" << llvm_name(instruction.args[0]) << "\n"; + break; + case mir::Op::CondJump: + os << " br i1 " << value_of(instruction.args[0]) << ", label %" + << llvm_name(instruction.args[1]) << ", label %" + << llvm_name(instruction.args[2]) << "\n"; + break; + case mir::Op::Return: + if (instruction.args.empty()) + os << " ret void\n"; + else + os << " ret " << llvm_type(instruction.type) << ' ' + << value_of(instruction.args[0]) << "\n"; + break; + } + } + } + os << "}\n\n"; +} +} // namespace uinx diff --git a/src/codegen_internal.hpp b/src/codegen_internal.hpp new file mode 100644 index 0000000..07bdb75 --- /dev/null +++ b/src/codegen_internal.hpp @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of codegen.cpp. + +#pragma once + +#include "uinx/codegen.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx::codegen_detail { + +inline bool unsigned_integer(TypeKind kind) { + return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || + kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; +} + +inline int integer_bits(const Type& type) { + switch (type.kind) { + case TypeKind::I8: + case TypeKind::U8: + return 8; + case TypeKind::I16: + case TypeKind::U16: + return 16; + case TypeKind::I32: + case TypeKind::U32: + case TypeKind::Char: + return 32; + case TypeKind::I128: + case TypeKind::U128: + return 128; + default: + return 64; + } +} + +inline std::string llvm_integer_constant(std::string_view literal) { + bool negative = false; + std::size_t offset = 0; + if (!literal.empty() && literal.front() == '-') { + negative = true; + offset = 1; + } + + unsigned base = 10; + if (literal.size() >= offset + 2 && literal[offset] == '0') { + switch (literal[offset + 1]) { + case 'x': + case 'X': + base = 16; + offset += 2; + break; + case 'b': + case 'B': + base = 2; + offset += 2; + break; + case 'o': + case 'O': + base = 8; + offset += 2; + break; + default: + break; + } + } + + // Store the magnitude in base 1,000,000,000 so this also handles u128 + // without narrowing through a host integer type. + std::vector digits{0}; + bool has_digit = false; + for (std::size_t i = offset; i < literal.size(); ++i) { + const char c = literal[i]; + if (c == '_') + continue; + unsigned value = 0; + if (c >= '0' && c <= '9') + value = static_cast(c - '0'); + else if (c >= 'a' && c <= 'f') + value = static_cast(c - 'a') + 10; + else if (c >= 'A' && c <= 'F') + value = static_cast(c - 'A') + 10; + else + return std::string(literal); + if (value >= base) + return std::string(literal); + + has_digit = true; + std::uint64_t carry = value; + for (auto& digit : digits) { + const std::uint64_t next = static_cast(digit) * base + carry; + digit = static_cast(next % 1000000000U); + carry = next / 1000000000U; + } + if (carry != 0) + digits.push_back(static_cast(carry)); + } + if (!has_digit) + return std::string(literal); + + while (digits.size() > 1 && digits.back() == 0) + digits.pop_back(); + if (digits.size() == 1 && digits.front() == 0) + return "0"; + + std::string result = std::to_string(digits.back()); + for (std::size_t i = digits.size() - 1; i > 0; --i) { + const std::string chunk = std::to_string(digits[i - 1]); + result.append(9 - chunk.size(), '0'); + result += chunk; + } + if (negative) + result.insert(result.begin(), '-'); + return result; +} + +inline std::string arithmetic_opcode(const Type& type, std::string_view op) { + if (op == "+") + return type.is_float() ? "fadd" : "add"; + if (op == "-") + return type.is_float() ? "fsub" : "sub"; + if (op == "*") + return type.is_float() ? "fmul" : "mul"; + if (op == "/") { + if (type.is_float()) + return "fdiv"; + return unsigned_integer(type.kind) ? "udiv" : "sdiv"; + } + if (op == "%") + return unsigned_integer(type.kind) ? "urem" : "srem"; + if (op == "&&" || op == "&") + return "and"; + if (op == "||" || op == "|") + return "or"; + if (op == "^") + return "xor"; + if (op == "<<") + return "shl"; + if (op == ">>") + return unsigned_integer(type.kind) ? "lshr" : "ashr"; + return "add"; +} + +inline unsigned type_alignment(const Type& type) { + switch (type.kind) { + case TypeKind::Bool: + case TypeKind::I8: + case TypeKind::U8: + return 1; + case TypeKind::I16: + case TypeKind::U16: + return 2; + case TypeKind::I32: + case TypeKind::U32: + case TypeKind::Char: + case TypeKind::F32: + return 4; + case TypeKind::I128: + case TypeKind::U128: + return 16; + default: + return 8; + } +} + +inline std::string atomic_rmw_opcode(std::string_view op) { + if (op == "+") + return "add"; + if (op == "-") + return "sub"; + if (op == "&") + return "and"; + if (op == "|") + return "or"; + if (op == "^") + return "xor"; + return "xchg"; +} + +inline std::optional atomic_order_from_abi(std::string_view value) { + if (value == "0") + return "monotonic"; + if (value == "1") + return "acquire"; + if (value == "2") + return "release"; + if (value == "3") + return "acq_rel"; + if (value == "4") + return "seq_cst"; + return std::nullopt; +} + +inline std::string cmpxchg_failure_order(std::string order) { + if (order == "release") + return "monotonic"; + if (order == "acq_rel") + return "acquire"; + return order; +} + +// Industrial C++20 ordering validation (Rust + C++20 + LKMM fusion). +// load: relaxed/acquire/seq_cst; store: relaxed/release/seq_cst; +// rmw/exchange: all five; cmpxchg failure: never release/acq_rel. +inline bool valid_atomic_load_order(std::string_view order) { + return order == "monotonic" || order == "acquire" || order == "seq_cst"; +} + +inline bool valid_atomic_store_order(std::string_view order) { + return order == "monotonic" || order == "release" || order == "seq_cst"; +} + +inline bool valid_atomic_rmw_order(std::string_view order) { + return order == "monotonic" || order == "acquire" || order == "release" || order == "acq_rel" || + order == "seq_cst"; +} + +inline int order_strength(std::string_view order) { + if (order == "monotonic") + return 0; + if (order == "acquire" || order == "release") + return 1; + if (order == "acq_rel") + return 2; + if (order == "seq_cst") + return 3; + return -1; +} + +inline std::string downgrade_cmpxchg_failure(std::string success, std::string failure) { + failure = cmpxchg_failure_order(std::move(failure)); + // Failure ordering must not exceed success ordering. + if (order_strength(failure) > order_strength(success)) { + if (success == "monotonic" || success == "release") + return "monotonic"; + return "acquire"; + } + return failure; +} + +struct AtomicIntrinsic { + std::string llvm_type; + unsigned align; + std::string rmw_op; // add/sub/and/or/xor/xchg/cmpxchg/load/store +}; + +inline std::optional parse_atomic_intrinsic(std::string_view name) { + // uinx_atomic_{load,store,fetch_add,fetch_sub,fetch_and,fetch_or,fetch_xor, + // exchange,compare_exchange}_{u8,u16,u32,u64} + static constexpr std::string_view prefix = "uinx_atomic_"; + if (!name.starts_with(prefix)) + return std::nullopt; + std::string_view rest = name.substr(prefix.size()); + std::string op; + for (std::string_view candidate : {"compare_exchange", + "fetch_add", + "fetch_sub", + "fetch_and", + "fetch_or", + "fetch_xor", + "exchange", + "load", + "store"}) { + if (rest.starts_with(candidate)) { + op = std::string(candidate); + rest = rest.substr(candidate.size()); + break; + } + } + if (op.empty() || !rest.starts_with("_")) + return std::nullopt; + std::string_view width = rest.substr(1); + std::string llvm_type; + unsigned align = 1; + if (width == "u8") { + llvm_type = "i8"; + align = 1; + } else if (width == "u16") { + llvm_type = "i16"; + align = 2; + } else if (width == "u32") { + llvm_type = "i32"; + align = 4; + } else if (width == "u64") { + llvm_type = "i64"; + align = 8; + } else { + return std::nullopt; + } + std::string rmw; + if (op == "load") + rmw = "load"; + else if (op == "store") + rmw = "store"; + else if (op == "fetch_add") + rmw = "add"; + else if (op == "fetch_sub") + rmw = "sub"; + else if (op == "fetch_and") + rmw = "and"; + else if (op == "fetch_or") + rmw = "or"; + else if (op == "fetch_xor") + rmw = "xor"; + else if (op == "exchange") + rmw = "xchg"; + else if (op == "compare_exchange") + rmw = "cmpxchg"; + return AtomicIntrinsic{llvm_type, align, rmw}; +} + +// Number of arguments encoded in a uinx_syscallN intrinsic name, or +// std::nullopt when the symbol is an ordinary runtime call. +inline std::optional syscall_arity_from_name(std::string_view name) { + static constexpr std::string_view prefix = "uinx_syscall"; + if (!name.starts_with(prefix)) + return std::nullopt; + const std::string_view suffix = name.substr(prefix.size()); + if (suffix.empty() || suffix.size() > 1) + return std::nullopt; + if (suffix.front() < '0' || suffix.front() > '6') + return std::nullopt; + return suffix.front() - '0'; +} + +inline std::string compare_predicate(const Type& type, std::string_view op) { + if (type.is_float()) { + if (op == "==") + return "oeq"; + if (op == "!=") + return "une"; + if (op == "<") + return "olt"; + if (op == "<=") + return "ole"; + if (op == ">") + return "ogt"; + return "oge"; + } + const bool u = unsigned_integer(type.kind); + if (op == "==") + return "eq"; + if (op == "!=") + return "ne"; + if (op == "<") + return u ? "ult" : "slt"; + if (op == "<=") + return u ? "ule" : "sle"; + if (op == ">") + return u ? "ugt" : "sgt"; + return u ? "uge" : "sge"; +} +} // namespace uinx::codegen_detail diff --git a/src/mir.cpp b/src/mir.cpp index 233f0ee..c2faaf2 100644 --- a/src/mir.cpp +++ b/src/mir.cpp @@ -4,6 +4,8 @@ #include "uinx/mir.hpp" +#include "mir_internal.hpp" + #include #include #include @@ -12,18 +14,8 @@ #include namespace uinx { -namespace { -bool unsigned_integer_kind(TypeKind kind) { - return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || - kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; -} +using namespace mir_detail; -bool builtin_function(std::string_view name) { - static const std::unordered_set names{ - "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; - return names.contains(name); -} -} // namespace std::string MIRLowerer::temp(FnState& s) { return "%t" + std::to_string(s.temp_counter++); } @@ -245,1226 +237,4 @@ void MIRLowerer::lower_block(const ast::BlockStmt& b, FnState& s) { emit_scope_drops(s); s.scope_locals.pop_back(); } -bool MIRLowerer::place_is_atomic(const ast::Expr& e, const FnState& s) const { - const Type ty = expr_type(e, s); - // Bool excluded: LLVM i1 atomics are invalid (byte-sized required). - // Use AtomicBool (u8-backed) instead. - const bool compatible = ty.is_integer() || ty.kind == TypeKind::RawPtr; - if (!compatible) - return false; - (void)s; - // Explicit-shared only: smp auto no longer promotes. Only `shared` - // globals/fields lower to LLVM atomics. percpu is never atomic. - if (e.kind == ast::ExprKind::Name) { - const auto& n = static_cast(e); - auto g = model_.globals.find(n.name); - if (g == model_.globals.end() || g->second.is_percpu) - return false; - return g->second.is_shared; - } - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base = expr_type(*m.base, s); - if (base.kind == TypeKind::Ref && base.pointee) - base = *base.pointee; - auto it = model_.structs.find(base.name); - return it != model_.structs.end() && it->second.shared_fields.contains(m.member); - } - return false; -} -std::string MIRLowerer::load_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acquire"; -} -std::string MIRLowerer::store_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "release"; -} -std::string MIRLowerer::rmw_order(const ast::Expr& e, const FnState& s) const { - (void)e; - return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acq_rel"; -} - -void MIRLowerer::lower_stmt(const ast::Stmt& st, FnState& s) { - if (auto l = dynamic_cast(&st)) { - auto [v, init_ty] = lower_expr(*l->init, s); - Type t = init_ty; - if (auto it = model_.binding_types.find(l); it != model_.binding_types.end()) - t = subst_type(it->second, s); - v = coerce_value(s, v, init_ty, t, l->range); - std::string sl = slot(s, l->name); - s.slots[l->name] = sl; - s.local_types[l->name] = t; - s.scope_locals.back().push_back(l->name); - emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, l->range}); - emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, l->range}); - return; - } - if (auto a = dynamic_cast(&st)) { - if (auto binding = model_.hir.assignment_bindings.find(a); - binding != model_.hir.assignment_bindings.end()) { - auto [v, init_ty] = lower_expr(*a->value, s); - Type t = init_ty; - if (auto it = model_.assignment_binding_types.find(a); - it != model_.assignment_binding_types.end()) - t = subst_type(it->second, s); - v = coerce_value(s, v, init_ty, t, a->range); - const auto& name = static_cast(*a->target).name; - std::string sl = slot(s, name); - s.slots[name] = sl; - s.local_types[name] = t; - s.scope_locals.back().push_back(name); - emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, a->range}); - emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, a->range}); - return; - } - std::string addr = lower_place_address(*a->target, s); - Type target_ty = expr_type(*a->target, s); - auto [v, vt] = lower_expr(*a->value, s); - v = coerce_value(s, v, vt, target_ty, a->range); - const bool atomic = place_is_atomic(*a->target, s); - if (atomic && a->op != "=") { - const std::string op = a->op.substr(0, a->op.size() - 1); - if (op == "+" || op == "-" || op == "&" || op == "|" || op == "^") { - emit(s, - {mir::Op::AtomicRmw, - "", - target_ty, - {addr, v, rmw_order(*a->target, s)}, - op, - {}, - true, - a->range}); - return; - } - diags_.error(a->range, - "E0509", - "atomic compound assignment '" + a->op + - "' is not supported; use an explicit compare/exchange loop"); - return; - } - if (a->op != "=") { - std::string old = temp(s); - emit(s, {mir::Op::Load, old, target_ty, {addr}, "", {}, false, a->range}); - std::string combined = temp(s); - std::string op = a->op.substr(0, a->op.size() - 1); - emit(s, {mir::Op::Binary, combined, target_ty, {old, v}, op, {}, false, a->range}); - v = combined; - } - emit(s, - {mir::Op::Store, - "", - target_ty, - {v, addr}, - atomic ? store_order(*a->target, s) : "", - {}, - atomic, - a->range}); - return; - } - if (auto e = dynamic_cast(&st)) { - (void)lower_expr(*e->expr, s); - return; - } - if (auto r = dynamic_cast(&st)) { - emit_scope_drops(s); - if (r->value) { - auto [v, t] = lower_expr(*r->value, s); - v = coerce_value(s, v, t, s.fn.result, r->range); - emit(s, {mir::Op::Return, "", s.fn.result, {v}, "", {}, false, r->range}); - } else - emit(s, {mir::Op::Return, "", Type::builtin("unit"), {}, "", {}, false, r->range}); - return; - } - if (auto f = dynamic_cast(&st)) { - emit(s, - {f->compiler_only ? mir::Op::CompilerFence : mir::Op::Fence, - "", - Type::builtin("unit"), - {}, - f->order, - {}, - false, - f->range}); - return; - } - if (auto b = dynamic_cast(&st)) { - lower_block(*b, s); - return; - } - if (auto u = dynamic_cast(&st)) { - lower_block(*u->body, s); - return; - } - if (auto i = dynamic_cast(&st)) { - auto [c, ct] = lower_expr(*i->condition, s); - (void)ct; - std::size_t then_i = new_block(s, "if.then." + std::to_string(s.fn.blocks.size())); - std::size_t else_i = new_block(s, "if.else." + std::to_string(s.fn.blocks.size())); - std::size_t merge_i = new_block(s, "if.end." + std::to_string(s.fn.blocks.size())); - std::string then_l = s.fn.blocks[then_i].label, else_l = s.fn.blocks[else_i].label, - merge_l = s.fn.blocks[merge_i].label; - emit(s, - {mir::Op::CondJump, - "", - Type::builtin("bool"), - {c, then_l, else_l}, - "", - {}, - false, - i->range}); - s.block = then_i; - lower_block(*i->then_block, s); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); - s.block = else_i; - if (i->else_block) - lower_block(*i->else_block, s); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); - s.block = merge_i; - return; - } - if (auto w = dynamic_cast(&st)) { - std::size_t cond_i = new_block(s, "while.cond." + std::to_string(s.fn.blocks.size())); - std::size_t body_i = new_block(s, "while.body." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "while.end." + std::to_string(s.fn.blocks.size())); - std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, - el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); - s.block = cond_i; - auto [c, t] = lower_expr(*w->condition, s); - (void)t; - emit(s, - {mir::Op::CondJump, "", Type::builtin("bool"), {c, bl, el}, "", {}, false, w->range}); - s.block = body_i; - s.loop_targets.push_back({cond_i, end_i}); - lower_block(*w->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); - s.block = end_i; - return; - } - if (auto f = dynamic_cast(&st)) { - auto [begin_value, iter_ty] = lower_expr(*f->begin, s); - auto [end_value, end_ty] = lower_expr(*f->end, s); - (void)end_ty; - std::string iter_slot = slot(s, f->name); - s.slots[f->name] = iter_slot; - s.local_types[f->name] = iter_ty; - s.scope_locals.back().push_back(f->name); - emit(s, {mir::Op::Alloca, iter_slot, iter_ty, {}, "", {}, false, f->range}); - emit(s, {mir::Op::Store, "", iter_ty, {begin_value, iter_slot}, "", {}, false, f->range}); - std::size_t cond_i = new_block(s, "for.cond." + std::to_string(s.fn.blocks.size())); - std::size_t body_i = new_block(s, "for.body." + std::to_string(s.fn.blocks.size())); - std::size_t step_i = new_block(s, "for.step." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "for.end." + std::to_string(s.fn.blocks.size())); - const std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, - sl = s.fn.blocks[step_i].label, el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); - s.block = cond_i; - std::string current = temp(s); - emit(s, {mir::Op::Load, current, iter_ty, {iter_slot}, "", {}, false, f->range}); - std::string cond = temp(s); - emit(s, - {mir::Op::Compare, - cond, - iter_ty, - {current, end_value}, - f->inclusive ? "<=" : "<", - {}, - false, - f->range}); - emit(s, - {mir::Op::CondJump, - "", - Type::builtin("bool"), - {cond, bl, el}, - "", - {}, - false, - f->range}); - s.block = body_i; - s.loop_targets.push_back({step_i, end_i}); - lower_block(*f->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {sl}, "", {}, false, f->range}); - s.block = step_i; - std::string old = temp(s); - emit(s, {mir::Op::Load, old, iter_ty, {iter_slot}, "", {}, false, f->range}); - std::string one = temp(s); - emit(s, {mir::Op::ConstInt, one, iter_ty, {}, "1", {}, false, f->range}); - std::string next = temp(s); - emit(s, {mir::Op::Binary, next, iter_ty, {old, one}, "+", {}, false, f->range}); - emit(s, {mir::Op::Store, "", iter_ty, {next, iter_slot}, "", {}, false, f->range}); - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); - s.block = end_i; - return; - } - if (auto l = dynamic_cast(&st)) { - std::size_t body_i = new_block(s, "loop.body." + std::to_string(s.fn.blocks.size())); - std::size_t end_i = new_block(s, "loop.end." + std::to_string(s.fn.blocks.size())); - const std::string bl = s.fn.blocks[body_i].label, el = s.fn.blocks[end_i].label; - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); - s.block = body_i; - s.loop_targets.push_back({body_i, end_i}); - lower_block(*l->body, s); - s.loop_targets.pop_back(); - if (!block(s).terminated) - emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); - s.block = end_i; - return; - } - if (dynamic_cast(&st)) { - if (!s.loop_targets.empty()) { - emit_scope_drops(s); - emit(s, - {mir::Op::Jump, - "", - Type::builtin("unit"), - {s.fn.blocks[s.loop_targets.back().second].label}, - "", - {}, - false, - st.range}); - } - return; - } - if (dynamic_cast(&st)) { - if (!s.loop_targets.empty()) { - emit_scope_drops(s); - emit(s, - {mir::Op::Jump, - "", - Type::builtin("unit"), - {s.fn.blocks[s.loop_targets.back().first].label}, - "", - {}, - false, - st.range}); - } - return; - } -} -std::string MIRLowerer::lower_place_address(const ast::Expr& e, FnState& s) { - if (e.kind == ast::ExprKind::Unary) { - const auto& u = static_cast(e); - if (u.op == "*") - return lower_expr(*u.operand, s).first; - } - if (e.kind == ast::ExprKind::Name) { - auto& n = static_cast(e); - auto it = s.slots.find(n.name); - if (it != s.slots.end()) - return it->second; - if (model_.globals.contains(n.name)) - return "@" + n.name; - diags_.error(e.range, "E0502", "internal: no storage for '" + n.name + "'"); - return "null"; - } - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base_ty = expr_type(*m.base, s); - std::string base_addr; - if (base_ty.kind == TypeKind::Ref && base_ty.pointee) { - auto lowered = lower_expr(*m.base, s); - base_addr = lowered.first; - base_ty = *base_ty.pointee; - } else - base_addr = lower_place_address(*m.base, s); - if (base_ty.kind != TypeKind::Named) { - diags_.error(e.range, "E0503", "field address base is not a struct"); - return "null"; - } - auto si = model_.structs.find(base_ty.name); - if (si == model_.structs.end() || !si->second.decl) { - diags_.error(e.range, "E0503", "unknown struct layout for '" + base_ty.name + "'"); - return "null"; - } - std::size_t index = 0; - bool found = false; - for (const auto& field : si->second.decl->fields) { - if (field.name == m.member) { - found = true; - break; - } - ++index; - } - if (!found) { - diags_.error(e.range, "E0503", "unknown field '" + m.member + "'"); - return "null"; - } - Type field_ty = expr_type(e, s); - std::string result = temp(s); - mir::Instruction in; - in.op = mir::Op::FieldAddr; - in.result = result; - in.type = field_ty; - in.args = {base_addr}; - in.text = std::to_string(index); - in.range = e.range; - in.auxiliary_type = base_ty; - emit(s, std::move(in)); - return result; - } - if (e.kind == ast::ExprKind::Index) { - const auto& index_expr = static_cast(e); - Type container_ty = expr_type(*index_expr.base, s); - std::string container_addr; - if (container_ty.kind == TypeKind::Ref && container_ty.pointee) { - container_addr = lower_expr(*index_expr.base, s).first; - container_ty = *container_ty.pointee; - } else - container_addr = lower_place_address(*index_expr.base, s); - if (container_ty.kind != TypeKind::Named || - (container_ty.name != "Slice" && container_ty.name != "SliceMut") || - container_ty.args.empty()) { - diags_.error(e.range, "E0505", "MIR indexing requires Slice or SliceMut"); - return "null"; - } - const Type elem = container_ty.args[0]; - const bool mut = container_ty.name == "SliceMut"; - const Type data_ty = Type::raw_ptr(elem, mut); - const Type usize_ty = Type::builtin("usize"); - std::string data_addr = temp(s); - mir::Instruction data_field; - data_field.op = mir::Op::FieldAddr; - data_field.result = data_addr; - data_field.type = data_ty; - data_field.args = {container_addr}; - data_field.text = "0"; - data_field.auxiliary_type = container_ty; - data_field.range = e.range; - emit(s, std::move(data_field)); - std::string data = temp(s); - emit(s, {mir::Op::Load, data, data_ty, {data_addr}, "", {}, false, e.range}); - std::string len_addr = temp(s); - mir::Instruction len_field; - len_field.op = mir::Op::FieldAddr; - len_field.result = len_addr; - len_field.type = usize_ty; - len_field.args = {container_addr}; - len_field.text = "1"; - len_field.auxiliary_type = container_ty; - len_field.range = e.range; - emit(s, std::move(len_field)); - std::string len = temp(s); - emit(s, {mir::Op::Load, len, usize_ty, {len_addr}, "", {}, false, e.range}); - auto [index_value, index_ty] = lower_expr(*index_expr.index, s); - std::string result = temp(s); - mir::Instruction indexed; - indexed.op = mir::Op::IndexAddr; - indexed.result = result; - indexed.type = elem; - indexed.args = {data, index_value, len}; - indexed.auxiliary_type = index_ty; - indexed.range = e.range; - emit(s, std::move(indexed)); - return result; - } - diags_.error(e.range, "E0504", "expression is not an addressable place"); - return "null"; -} -std::string -MIRLowerer::specialize_call(const ast::CallExpr& c, const FunctionSig& sig, FnState& s) { - if (sig.generic_names.empty()) - return sig.name; - std::unordered_map subst; - if (!c.generic_args.empty()) { - for (std::size_t i = 0; i < std::min(c.generic_args.size(), sig.generic_names.size()); ++i) - subst[sig.generic_names[i]] = type_from_ast(c.generic_args[i]); - } else { - for (std::size_t i = 0; i < c.args.size() && i < sig.params.size(); ++i) { - Type formal = subst_type(sig.params[i], s), actual = expr_type(*c.args[i], s); - if (formal.kind == TypeKind::Generic) - subst[formal.name] = actual; - } - } - std::ostringstream name; - name << sig.name; - for (const auto& g : sig.generic_names) { - auto it = subst.find(g); - if (it == subst.end()) - continue; - name << '$'; - for (char ch : it->second.str()) - name << (std::isalnum(static_cast(ch)) ? ch : '_'); - } - std::string n = name.str(); - if (!emitted_.contains(n)) { - pending_.push_back({sig.decl, subst, n}); - } - return n; -} -std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& s) { - Type ty = expr_type(e, s); - switch (e.kind) { - case ast::ExprKind::Integer: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstInt, r, ty, {}, x.value, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Float: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstFloat, r, ty, {}, x.value, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Bool: { - auto& x = static_cast(e); - std::string r = temp(s); - emit(s, {mir::Op::ConstBool, r, ty, {}, x.value ? "1" : "0", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Char: { - auto& x = static_cast(e); - std::string r = temp(s); - std::uint32_t v = x.value.empty() ? 0 : static_cast(x.value[0]); - emit(s, {mir::Op::ConstChar, r, ty, {}, std::to_string(v), {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::String: { - auto& x = static_cast(e); - std::string r = temp(s), sym = intern_string(x.value); - emit(s, {mir::Op::ConstString, r, ty, {}, sym, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Name: { - auto addr = lower_place_address(e, s); - std::string r = temp(s); - const bool atomic = place_is_atomic(e, s); - emit(s, - {mir::Op::Load, - r, - ty, - {addr}, - atomic ? load_order(e, s) : "", - {}, - atomic, - e.range}); - return {r, ty}; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto si = model_.structs.find(st.name); - if (si == model_.structs.end() || !si->second.decl) { - diags_.error(e.range, "E0506", "missing struct layout for '" + st.name + "'"); - return {"undef", ty}; - } - std::unordered_map values; - for (const auto& f : st.fields) - values[f.name] = f.value.get(); - // Expected field types under the call-site substitution, so - // integer literals coerce across widths exactly like call - // arguments and `let` bindings do (sema permits them via - // `can_coerce_expr`; the backend must never see mismatched - // operand types, e.g. i32 `7` into a u64 generic field). - std::unordered_set field_generics(si->second.generic_names.begin(), - si->second.generic_names.end()); - std::unordered_map field_subst; - for (std::size_t i = 0; i < si->second.generic_names.size() && i < ty.args.size(); ++i) - field_subst[si->second.generic_names[i]] = ty.args[i]; - std::vector args; - for (const auto& decl_field : si->second.decl->fields) { - auto it = values.find(decl_field.name); - if (it == values.end()) { - args.push_back("undef"); - continue; - } - auto [field_value, field_actual] = lower_expr(*it->second, s); - const Type field_expected = - substitute_type(type_from_ast(decl_field.type, field_generics), field_subst); - args.push_back( - coerce_value(s, field_value, field_actual, field_expected, it->second->range)); - } - std::string r = temp(s); - emit(s, {mir::Op::StructInit, r, ty, std::move(args), "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Borrow: { - auto& b = static_cast(e); - std::string a = lower_place_address(*b.target, s); - std::string r = temp(s); - emit(s, {mir::Op::AddressOf, r, ty, {a}, "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Unary: { - auto& u = static_cast(e); - auto [v, t] = lower_expr(*u.operand, s); - if (u.op == "move") - return {v, ty}; - if (u.op == "*") { - std::string r = temp(s); - emit(s, {mir::Op::Load, r, ty, {v}, "", {}, false, e.range}); - return {r, ty}; - } - std::string r = temp(s); - std::string zero = temp(s); - if (u.op == "-") { - // `ty` is the (possibly coerced) result type from sema, while `t` - // is the inner operand's own type. For `-literal` coerced to a - // wider integer (e.g. `result >= -4095` with result: i64), using - // `t` here emits `sub i32` whose result is later used as i64 and - // produces invalid `icmp sge i64 %x, %y(i32)` IR at -O0 - // (-O2 hides it via constant folding). Lower in `ty` instead, - // casting the operand up when needed. - std::string operand = v; - if (t != ty) { - std::string casted = temp(s); - emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); - operand = casted; - } - if (ty.is_float()) { - emit(s, {mir::Op::ConstFloat, zero, ty, {}, "0.0", {}, false, e.range}); - } else { - emit(s, {mir::Op::ConstInt, zero, ty, {}, "0", {}, false, e.range}); - } - emit(s, {mir::Op::Binary, r, ty, {zero, operand}, "-", {}, false, e.range}); - } else { - std::string operand = v; - if (t != ty) { - std::string casted = temp(s); - emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); - operand = casted; - } - emit(s, {mir::Op::Binary, r, ty, {operand}, u.op, {}, false, e.range}); - } - return {r, ty}; - } - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - auto [l, lt] = lower_expr(*b.lhs, s); - auto [r, rt] = lower_expr(*b.rhs, s); - std::string out = temp(s); - bool cmp = b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || - b.op == ">="; - if (cmp) { - // Comparison operands must share one LLVM integer type. - l = coerce_value(s, l, lt, lt, b.lhs->range); - r = coerce_value(s, r, rt, lt, b.rhs->range); - } else { - l = coerce_value(s, l, lt, ty, b.lhs->range); - r = coerce_value(s, r, rt, ty, b.rhs->range); - } - emit(s, - {cmp ? mir::Op::Compare : mir::Op::Binary, - out, - cmp ? lt : ty, - {l, r}, - b.op, - {}, - false, - e.range}); - return {out, ty}; - } - case ast::ExprKind::Call: { - auto& c = static_cast(e); - if (c.callee->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*c.callee).name; - if (builtin_function(name)) - return lower_builtin_call(c, s); - } - std::string callee; - std::vector args; - if (c.callee->kind == ast::ExprKind::Name) { - auto& n = static_cast(*c.callee); - auto fi = model_.functions.find(n.name); - callee = n.name; - if (fi != model_.functions.end()) - callee = specialize_call(c, fi->second, s); - for (std::size_t i = 0; i < c.args.size(); ++i) { - auto [v, actual] = lower_expr(*c.args[i], s); - if (fi != model_.functions.end() && i < fi->second.params.size()) { - Type expected = subst_type(fi->second.params[i], s); - // For generic callees the formal may still be generic; - // coerce_value is a no-op unless it is a concrete - // int/bool/float mismatch (e.g. i32 literal -> i64 param). - v = coerce_value(s, v, actual, expected, c.args[i]->range); - } - args.push_back(v); - } - } else if (c.callee->kind == ast::ExprKind::Member) { - auto& member = static_cast(*c.callee); - Type receiver_ty = expr_type(*member.base, s); - Type owner = receiver_ty; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const auto fi = model_.functions.find(owner.name + "::" + member.member); - if (fi == model_.functions.end()) { - diags_.error( - e.range, "E0507", "internal: method signature missing during MIR lowering"); - return {"undef", ty}; - } - callee = fi->second.name; - if (!fi->second.params.empty() && fi->second.params.front().kind == TypeKind::Ref) { - if (receiver_ty.kind == TypeKind::Ref) - args.push_back(lower_expr(*member.base, s).first); - else - args.push_back(lower_place_address(*member.base, s)); - } else - args.push_back(lower_expr(*member.base, s).first); - // Method params[0] is the receiver already handled above. - const std::size_t param_offset = - fi->second.params.empty() ? 0 : args.size() - c.args.size(); - for (std::size_t i = 0; i < c.args.size(); ++i) { - auto [v, actual] = lower_expr(*c.args[i], s); - if (param_offset + i < fi->second.params.size()) { - Type expected = subst_type(fi->second.params[param_offset + i], s); - v = coerce_value(s, v, actual, expected, c.args[i]->range); - } - args.push_back(v); - } - } else { - diags_.error( - e.range, "E0507", "internal: unsupported call target during MIR lowering"); - return {"undef", ty}; - } - std::string r = ty.is_unit() ? "" : temp(s); - emit(s, {mir::Op::Call, r, ty, std::move(args), callee, {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Cast: { - auto& c = static_cast(e); - auto [v, from] = lower_expr(*c.value, s); - std::string r = temp(s); - emit(s, {mir::Op::Cast, r, ty, {v}, from.str(), {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - mir::Instruction in; - in.op = mir::Op::InlineAsm; - in.type = Type::builtin("unit"); - in.text = a.assembly; - in.flag = a.is_volatile; - in.range = e.range; - for (const auto& o : a.operands) { - mir::AsmOperand mo; - mo.kind = o.kind; - mo.constraint = o.constraint; - if (o.value) { - auto [v, t] = lower_expr(*o.value, s); - mo.value = v; - mo.type = t; - } - if (!o.out_name.empty()) { - auto it = s.slots.find(o.out_name); - if (it != s.slots.end()) { - mo.out_slot = it->second; - mo.type = s.local_types[o.out_name]; - } - } - in.asm_operands.push_back(std::move(mo)); - } - emit(s, std::move(in)); - return {"", ty}; - } - case ast::ExprKind::Await: { - auto& a = static_cast(e); - auto [v, future_ty] = lower_expr(*a.value, s); - (void)future_ty; - std::string r = temp(s); - emit(s, {mir::Op::Await, r, ty, {v}, "", {}, false, e.range}); - return {r, ty}; - } - case ast::ExprKind::Member: { - std::string addr = lower_place_address(e, s); - std::string r = temp(s); - const bool atomic = place_is_atomic(e, s); - emit(s, - {mir::Op::Load, - r, - ty, - {addr}, - atomic ? load_order(e, s) : "", - {}, - atomic, - e.range}); - return {r, ty}; - } - case ast::ExprKind::Index: { - std::string addr = lower_place_address(e, s); - std::string r = temp(s); - emit(s, {mir::Op::Load, r, ty, {addr}, "", {}, false, e.range}); - return {r, ty}; - } - } - return {"undef", ty}; -} - -std::pair MIRLowerer::lower_builtin_call(const ast::CallExpr& call, FnState& s) { - const auto& name = static_cast(*call.callee).name; - const Type result = expr_type(call, s); - auto declare_call = [&](const std::string& runtime_name, - const Type& runtime_result, - const std::vector& parameter_types) { - std::vector parameters; - for (std::size_t i = 0; i < parameter_types.size(); ++i) - parameters.push_back({"p" + std::to_string(i), parameter_types[i]}); - declare_runtime_function(runtime_name, runtime_result, std::move(parameters)); - }; - auto runtime_call = [&](const std::string& runtime_name, - const Type& runtime_result, - const std::vector& parameter_types, - std::vector arguments, - const SourceRange& range) { - declare_call(runtime_name, runtime_result, parameter_types); - const std::string output = runtime_result.is_unit() ? "" : temp(s); - emit(s, - {mir::Op::Call, - output, - runtime_result, - std::move(arguments), - runtime_name, - {}, - false, - range}); - return output; - }; - auto cast_value = - [&](const std::string& value, const Type& from, const Type& to, const SourceRange& range) { - if (from == to) - return value; - const std::string output = temp(s); - emit(s, {mir::Op::Cast, output, to, {value}, from.str(), {}, false, range}); - return output; - }; - - if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { - const bool stderr_output = name == "eprint" || name == "eprintln"; - const Type i32 = Type::builtin("i32"); - const Type fd_type = i32; - const std::string fd = temp(s); - emit( - s, - {mir::Op::ConstInt, fd, fd_type, {}, stderr_output ? "2" : "1", {}, false, call.range}); - - for (std::size_t i = 0; i < call.args.size(); ++i) { - if (i != 0) { - runtime_call( - "uinx_print_space", Type::builtin("unit"), {fd_type}, {fd}, call.range); - } - auto [value, type] = lower_expr(*call.args[i], s); - if (type.is_integer() && type.kind != TypeKind::I128 && type.kind != TypeKind::U128) { - const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); - value = cast_value(value, type, target, call.args[i]->range); - runtime_call(unsigned_integer_kind(type.kind) ? "uinx_print_u64" : "uinx_print_i64", - Type::builtin("unit"), - {target, fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.is_float()) { - value = cast_value(value, type, Type::builtin("f64"), call.args[i]->range); - runtime_call("uinx_print_f64", - Type::builtin("unit"), - {Type::builtin("f64"), fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.kind == TypeKind::Bool) { - value = cast_value(value, type, Type::builtin("i64"), call.args[i]->range); - runtime_call("uinx_print_bool", - Type::builtin("unit"), - {Type::builtin("i64"), fd_type}, - {value, fd}, - call.args[i]->range); - } else if (type.kind == TypeKind::Char) { - runtime_call("uinx_print_char", - Type::builtin("unit"), - {Type::builtin("i32"), fd_type}, - {value, fd}, - call.args[i]->range); - } else { - runtime_call("uinx_print_cstr", - Type::builtin("unit"), - {Type::ref(Type::builtin("str"), false), fd_type}, - {value, fd}, - call.args[i]->range); - } - } - runtime_call("uinx_print_newline", Type::builtin("unit"), {fd_type}, {fd}, call.range); - return {"", Type::builtin("unit")}; - } - - if (name == "input") { - const Type str_ref = Type::ref(Type::builtin("str"), false); - std::string prompt = "null"; - if (!call.args.empty()) - prompt = lower_expr(*call.args[0], s).first; - const std::string fd = temp(s); - emit(s, {mir::Op::ConstInt, fd, Type::builtin("i32"), {}, "1", {}, false, call.range}); - return {runtime_call("uinx_input_cstr", - str_ref, - {str_ref, Type::builtin("i32")}, - {prompt, fd}, - call.range), - str_ref}; - } - - auto [value, type] = lower_expr(*call.args[0], s); - const Type str_ref = Type::ref(Type::builtin("str"), false); - const auto is_string = [](const Type& checked) { - return checked.kind == TypeKind::Str || (checked.kind == TypeKind::Ref && checked.pointee && - checked.pointee->kind == TypeKind::Str); - }; - if (name == "str") { - if (is_string(type)) - return {value, str_ref}; - if (type.is_integer()) { - const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); - value = cast_value(value, type, target, call.range); - return {runtime_call(unsigned_integer_kind(type.kind) ? "uinx_string_from_u64" - : "uinx_string_from_i64", - str_ref, - {target}, - {value}, - call.range), - str_ref}; - } - if (type.is_float()) { - value = cast_value(value, type, Type::builtin("f64"), call.range); - return { - runtime_call( - "uinx_string_from_f64", str_ref, {Type::builtin("f64")}, {value}, call.range), - str_ref}; - } - if (type.kind == TypeKind::Bool) { - value = cast_value(value, type, Type::builtin("i64"), call.range); - return { - runtime_call( - "uinx_string_from_bool", str_ref, {Type::builtin("i64")}, {value}, call.range), - str_ref}; - } - if (type.kind == TypeKind::Char) - return { - runtime_call( - "uinx_string_from_char", str_ref, {Type::builtin("i32")}, {value}, call.range), - str_ref}; - } - if (name == "int" || name == "float") { - const Type target = Type::builtin(name == "int" ? "i64" : "f64"); - if (is_string(type)) { - return {runtime_call(name == "int" ? "uinx_parse_i64_cstr" : "uinx_parse_f64_cstr", - target, - {str_ref}, - {value}, - call.range), - target}; - } - return {cast_value(value, type, target, call.range), target}; - } - if (name == "bool") { - if (type.kind == TypeKind::Bool) - return {value, type}; - if (is_string(type)) { - const Type i64 = Type::builtin("i64"); - value = runtime_call("uinx_parse_i64_cstr", i64, {str_ref}, {value}, call.range); - type = i64; - } - const std::string zero = temp(s); - emit(s, {mir::Op::ConstInt, zero, type, {}, "0", {}, false, call.range}); - const std::string output = temp(s); - emit(s, - {mir::Op::Compare, - output, - Type::builtin("bool"), - {value, zero}, - "!=", - {}, - false, - call.range}); - return {output, Type::builtin("bool")}; - } - if (name == "len") { - if (is_string(type)) - return {runtime_call( - "uinx_cstr_len", Type::builtin("usize"), {str_ref}, {value}, call.range), - Type::builtin("usize")}; - } - diags_.error(call.range, "E0507", "internal: unsupported builtin lowering"); - return {"undef", result}; -} -void MIROptimizer::remove_unreachable(mir::Function& f) const { - std::unordered_set reachable; - if (f.blocks.empty()) - return; - std::unordered_map labels; - for (std::size_t i = 0; i < f.blocks.size(); ++i) - labels[f.blocks[i].label] = i; - std::vector work{0}; - while (!work.empty()) { - const std::size_t index = work.back(); - work.pop_back(); - if (index >= f.blocks.size() || !reachable.insert(f.blocks[index].label).second) - continue; - const auto& block = f.blocks[index]; - if (block.instructions.empty()) { - if (index + 1 < f.blocks.size()) - work.push_back(index + 1); - continue; - } - const auto& tail = block.instructions.back(); - if (tail.op == mir::Op::Jump && !tail.args.empty()) { - if (auto it = labels.find(tail.args[0]); it != labels.end()) - work.push_back(it->second); - } else if (tail.op == mir::Op::CondJump && tail.args.size() >= 3) { - if (auto it = labels.find(tail.args[1]); it != labels.end()) - work.push_back(it->second); - if (auto it = labels.find(tail.args[2]); it != labels.end()) - work.push_back(it->second); - } else if (tail.op != mir::Op::Return && index + 1 < f.blocks.size()) { - work.push_back(index + 1); - } - } - f.blocks.erase(std::remove_if(f.blocks.begin(), - f.blocks.end(), - [&](const mir::BasicBlock& block) { - return !reachable.contains(block.label); - }), - f.blocks.end()); -} - -void MIROptimizer::forward_local_loads(mir::Function& fn) const { - for (auto& block : fn.blocks) { - std::unordered_map slots; - std::unordered_map aliases; - auto resolve = [&](std::string value) { - for (int depth = 0; depth < 32; ++depth) { - auto it = aliases.find(value); - if (it == aliases.end() || it->second == value) - break; - value = it->second; - } - return value; - }; - std::vector output; - output.reserve(block.instructions.size()); - for (auto instruction : block.instructions) { - for (auto& arg : instruction.args) - arg = resolve(arg); - for (auto& operand : instruction.asm_operands) { - if (!operand.value.empty()) - operand.value = resolve(operand.value); - } - const auto is_local_slot = [](const std::string& address) { - return !address.empty() && address.front() == '%' && - address.find(".slot.") != std::string::npos; - }; - if (instruction.op == mir::Op::Load && !instruction.flag && !instruction.args.empty()) { - const auto it = slots.find(instruction.args[0]); - if (it != slots.end()) { - aliases[instruction.result] = resolve(it->second); - continue; - } - } - if (instruction.op == mir::Op::Store && !instruction.flag && - instruction.args.size() >= 2) { - const std::string& address = instruction.args[1]; - if (is_local_slot(address)) - slots[address] = instruction.args[0]; - else - slots.clear(); - } else if (instruction.op == mir::Op::AddressOf) { - if (!instruction.args.empty()) - slots.erase(instruction.args[0]); - } else if (instruction.op == mir::Op::Call || instruction.op == mir::Op::InlineAsm || - instruction.op == mir::Op::AtomicRmw || instruction.op == mir::Op::Fence || - instruction.op == mir::Op::CompilerFence || - (instruction.op == mir::Op::Load && instruction.flag) || - (instruction.op == mir::Op::Store && instruction.flag)) { - slots.clear(); - } - output.push_back(std::move(instruction)); - } - block.instructions = std::move(output); - } -} - -void MIROptimizer::constant_fold(mir::Function& fn) const { - for (auto& block : fn.blocks) { - std::unordered_map constants; - auto get = [&](const std::string& value, std::int64_t& out) { - if (auto it = constants.find(value); it != constants.end()) { - out = it->second; - return true; - } - char* end = nullptr; - const long long parsed = std::strtoll(value.c_str(), &end, 0); - if (end && *end == '\0' && end != value.c_str()) { - out = static_cast(parsed); - return true; - } - return false; - }; - for (auto& instruction : block.instructions) { - if ((instruction.op == mir::Op::ConstInt || instruction.op == mir::Op::ConstBool || - instruction.op == mir::Op::ConstChar) && - !instruction.result.empty()) { - std::int64_t value{}; - if (get(instruction.text, value)) - constants[instruction.result] = value; - continue; - } - if (instruction.type.kind == TypeKind::I128 || instruction.type.kind == TypeKind::U128) - continue; - if (instruction.op == mir::Op::Binary && !instruction.result.empty()) { - std::int64_t a{}, b{}; - if (instruction.text == "!" && instruction.args.size() == 1 && - get(instruction.args[0], a)) { - const std::int64_t result = instruction.type.kind == TypeKind::Bool ? !a : ~a; - instruction.op = instruction.type.kind == TypeKind::Bool ? mir::Op::ConstBool - : mir::Op::ConstInt; - instruction.args.clear(); - instruction.text = std::to_string(result); - constants[instruction.result] = result; - continue; - } - if (instruction.args.size() != 2 || !get(instruction.args[0], a) || - !get(instruction.args[1], b)) - continue; - std::int64_t result{}; - bool ok = true; - if (instruction.text == "+") - result = static_cast(static_cast(a) + - static_cast(b)); - else if (instruction.text == "-") - result = static_cast(static_cast(a) - - static_cast(b)); - else if (instruction.text == "*") - result = static_cast(static_cast(a) * - static_cast(b)); - else if (instruction.text == "/") { - if (b == 0) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) / - static_cast(b)) - : a / b; - } else if (instruction.text == "%") { - if (b == 0) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) % - static_cast(b)) - : a % b; - } else if (instruction.text == "&" || instruction.text == "&&") - result = a & b; - else if (instruction.text == "|" || instruction.text == "||") - result = a | b; - else if (instruction.text == "^") - result = a ^ b; - else if (instruction.text == "<<") { - if (b < 0 || b >= 64) - ok = false; - else - result = static_cast(static_cast(a) << b); - } else if (instruction.text == ">>") { - if (b < 0 || b >= 64) - ok = false; - else - result = unsigned_integer_kind(instruction.type.kind) - ? static_cast(static_cast(a) >> b) - : (a >> b); - } else - ok = false; - if (ok) { - instruction.op = mir::Op::ConstInt; - instruction.args.clear(); - instruction.text = std::to_string(result); - constants[instruction.result] = result; - } - } else if (instruction.op == mir::Op::Compare && instruction.args.size() == 2 && - !instruction.result.empty()) { - std::int64_t a{}, b{}; - if (!get(instruction.args[0], a) || !get(instruction.args[1], b)) - continue; - const bool u = unsigned_integer_kind(instruction.type.kind); - bool result = false; - if (instruction.text == "==") - result = a == b; - else if (instruction.text == "!=") - result = a != b; - else if (instruction.text == "<") - result = - u ? static_cast(a) < static_cast(b) : a < b; - else if (instruction.text == "<=") - result = - u ? static_cast(a) <= static_cast(b) : a <= b; - else if (instruction.text == ">") - result = - u ? static_cast(a) > static_cast(b) : a > b; - else if (instruction.text == ">=") - result = - u ? static_cast(a) >= static_cast(b) : a >= b; - instruction.op = mir::Op::ConstBool; - instruction.type = Type::builtin("bool"); - instruction.args.clear(); - instruction.text = result ? "1" : "0"; - constants[instruction.result] = result ? 1 : 0; - } - } - } -} - -void MIROptimizer::eliminate_dead_values(mir::Function& fn) const { - bool changed = true; - while (changed) { - changed = false; - std::unordered_map uses; - for (const auto& block : fn.blocks) - for (const auto& instruction : block.instructions) { - for (const auto& arg : instruction.args) - ++uses[arg]; - for (const auto& operand : instruction.asm_operands) - if (!operand.value.empty()) - ++uses[operand.value]; - } - for (auto& block : fn.blocks) { - auto it = std::remove_if( - block.instructions.begin(), - block.instructions.end(), - [&](const mir::Instruction& instruction) { - if (instruction.result.empty() || uses[instruction.result] != 0) - return false; - bool pure = - instruction.op == mir::Op::Alloca || instruction.op == mir::Op::ConstInt || - instruction.op == mir::Op::ConstFloat || - instruction.op == mir::Op::ConstBool || - instruction.op == mir::Op::ConstChar || - instruction.op == mir::Op::ConstString || - instruction.op == mir::Op::StructInit || - instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::Binary || - instruction.op == mir::Op::Compare || instruction.op == mir::Op::Cast || - instruction.op == mir::Op::AddressOf || - (instruction.op == mir::Op::Load && !instruction.flag); - if (pure) - changed = true; - return pure; - }); - block.instructions.erase(it, block.instructions.end()); - } - } -} - -void MIROptimizer::run(mir::Module& m) const { - if (level_ <= 0) - return; - for (auto& f : m.functions) { - if (f.is_extern) - continue; - remove_unreachable(f); - forward_local_loads(f); - constant_fold(f); - eliminate_dead_values(f); - if (level_ >= 2) { - forward_local_loads(f); - eliminate_dead_values(f); - } - } -} - } // namespace uinx diff --git a/src/mir_expr.cpp b/src/mir_expr.cpp new file mode 100644 index 0000000..ccb85fe --- /dev/null +++ b/src/mir_expr.cpp @@ -0,0 +1,656 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +std::string MIRLowerer::lower_place_address(const ast::Expr& e, FnState& s) { + if (e.kind == ast::ExprKind::Unary) { + const auto& u = static_cast(e); + if (u.op == "*") + return lower_expr(*u.operand, s).first; + } + if (e.kind == ast::ExprKind::Name) { + auto& n = static_cast(e); + auto it = s.slots.find(n.name); + if (it != s.slots.end()) + return it->second; + if (model_.globals.contains(n.name)) + return "@" + n.name; + diags_.error(e.range, "E0502", "internal: no storage for '" + n.name + "'"); + return "null"; + } + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base_ty = expr_type(*m.base, s); + std::string base_addr; + if (base_ty.kind == TypeKind::Ref && base_ty.pointee) { + auto lowered = lower_expr(*m.base, s); + base_addr = lowered.first; + base_ty = *base_ty.pointee; + } else + base_addr = lower_place_address(*m.base, s); + if (base_ty.kind != TypeKind::Named) { + diags_.error(e.range, "E0503", "field address base is not a struct"); + return "null"; + } + auto si = model_.structs.find(base_ty.name); + if (si == model_.structs.end() || !si->second.decl) { + diags_.error(e.range, "E0503", "unknown struct layout for '" + base_ty.name + "'"); + return "null"; + } + std::size_t index = 0; + bool found = false; + for (const auto& field : si->second.decl->fields) { + if (field.name == m.member) { + found = true; + break; + } + ++index; + } + if (!found) { + diags_.error(e.range, "E0503", "unknown field '" + m.member + "'"); + return "null"; + } + Type field_ty = expr_type(e, s); + std::string result = temp(s); + mir::Instruction in; + in.op = mir::Op::FieldAddr; + in.result = result; + in.type = field_ty; + in.args = {base_addr}; + in.text = std::to_string(index); + in.range = e.range; + in.auxiliary_type = base_ty; + emit(s, std::move(in)); + return result; + } + if (e.kind == ast::ExprKind::Index) { + const auto& index_expr = static_cast(e); + Type container_ty = expr_type(*index_expr.base, s); + std::string container_addr; + if (container_ty.kind == TypeKind::Ref && container_ty.pointee) { + container_addr = lower_expr(*index_expr.base, s).first; + container_ty = *container_ty.pointee; + } else + container_addr = lower_place_address(*index_expr.base, s); + if (container_ty.kind != TypeKind::Named || + (container_ty.name != "Slice" && container_ty.name != "SliceMut") || + container_ty.args.empty()) { + diags_.error(e.range, "E0505", "MIR indexing requires Slice or SliceMut"); + return "null"; + } + const Type elem = container_ty.args[0]; + const bool mut = container_ty.name == "SliceMut"; + const Type data_ty = Type::raw_ptr(elem, mut); + const Type usize_ty = Type::builtin("usize"); + std::string data_addr = temp(s); + mir::Instruction data_field; + data_field.op = mir::Op::FieldAddr; + data_field.result = data_addr; + data_field.type = data_ty; + data_field.args = {container_addr}; + data_field.text = "0"; + data_field.auxiliary_type = container_ty; + data_field.range = e.range; + emit(s, std::move(data_field)); + std::string data = temp(s); + emit(s, {mir::Op::Load, data, data_ty, {data_addr}, "", {}, false, e.range}); + std::string len_addr = temp(s); + mir::Instruction len_field; + len_field.op = mir::Op::FieldAddr; + len_field.result = len_addr; + len_field.type = usize_ty; + len_field.args = {container_addr}; + len_field.text = "1"; + len_field.auxiliary_type = container_ty; + len_field.range = e.range; + emit(s, std::move(len_field)); + std::string len = temp(s); + emit(s, {mir::Op::Load, len, usize_ty, {len_addr}, "", {}, false, e.range}); + auto [index_value, index_ty] = lower_expr(*index_expr.index, s); + std::string result = temp(s); + mir::Instruction indexed; + indexed.op = mir::Op::IndexAddr; + indexed.result = result; + indexed.type = elem; + indexed.args = {data, index_value, len}; + indexed.auxiliary_type = index_ty; + indexed.range = e.range; + emit(s, std::move(indexed)); + return result; + } + diags_.error(e.range, "E0504", "expression is not an addressable place"); + return "null"; +} +std::string +MIRLowerer::specialize_call(const ast::CallExpr& c, const FunctionSig& sig, FnState& s) { + if (sig.generic_names.empty()) + return sig.name; + std::unordered_map subst; + if (!c.generic_args.empty()) { + for (std::size_t i = 0; i < std::min(c.generic_args.size(), sig.generic_names.size()); ++i) + subst[sig.generic_names[i]] = type_from_ast(c.generic_args[i]); + } else { + for (std::size_t i = 0; i < c.args.size() && i < sig.params.size(); ++i) { + Type formal = subst_type(sig.params[i], s), actual = expr_type(*c.args[i], s); + if (formal.kind == TypeKind::Generic) + subst[formal.name] = actual; + } + } + std::ostringstream name; + name << sig.name; + for (const auto& g : sig.generic_names) { + auto it = subst.find(g); + if (it == subst.end()) + continue; + name << '$'; + for (char ch : it->second.str()) + name << (std::isalnum(static_cast(ch)) ? ch : '_'); + } + std::string n = name.str(); + if (!emitted_.contains(n)) { + pending_.push_back({sig.decl, subst, n}); + } + return n; +} +std::pair MIRLowerer::lower_expr(const ast::Expr& e, FnState& s) { + Type ty = expr_type(e, s); + switch (e.kind) { + case ast::ExprKind::Integer: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstInt, r, ty, {}, x.value, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Float: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstFloat, r, ty, {}, x.value, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Bool: { + auto& x = static_cast(e); + std::string r = temp(s); + emit(s, {mir::Op::ConstBool, r, ty, {}, x.value ? "1" : "0", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Char: { + auto& x = static_cast(e); + std::string r = temp(s); + std::uint32_t v = x.value.empty() ? 0 : static_cast(x.value[0]); + emit(s, {mir::Op::ConstChar, r, ty, {}, std::to_string(v), {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::String: { + auto& x = static_cast(e); + std::string r = temp(s), sym = intern_string(x.value); + emit(s, {mir::Op::ConstString, r, ty, {}, sym, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Name: { + auto addr = lower_place_address(e, s); + std::string r = temp(s); + const bool atomic = place_is_atomic(e, s); + emit(s, + {mir::Op::Load, + r, + ty, + {addr}, + atomic ? load_order(e, s) : "", + {}, + atomic, + e.range}); + return {r, ty}; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto si = model_.structs.find(st.name); + if (si == model_.structs.end() || !si->second.decl) { + diags_.error(e.range, "E0506", "missing struct layout for '" + st.name + "'"); + return {"undef", ty}; + } + std::unordered_map values; + for (const auto& f : st.fields) + values[f.name] = f.value.get(); + // Expected field types under the call-site substitution, so + // integer literals coerce across widths exactly like call + // arguments and `let` bindings do (sema permits them via + // `can_coerce_expr`; the backend must never see mismatched + // operand types, e.g. i32 `7` into a u64 generic field). + std::unordered_set field_generics(si->second.generic_names.begin(), + si->second.generic_names.end()); + std::unordered_map field_subst; + for (std::size_t i = 0; i < si->second.generic_names.size() && i < ty.args.size(); ++i) + field_subst[si->second.generic_names[i]] = ty.args[i]; + std::vector args; + for (const auto& decl_field : si->second.decl->fields) { + auto it = values.find(decl_field.name); + if (it == values.end()) { + args.push_back("undef"); + continue; + } + auto [field_value, field_actual] = lower_expr(*it->second, s); + const Type field_expected = + substitute_type(type_from_ast(decl_field.type, field_generics), field_subst); + args.push_back( + coerce_value(s, field_value, field_actual, field_expected, it->second->range)); + } + std::string r = temp(s); + emit(s, {mir::Op::StructInit, r, ty, std::move(args), "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Borrow: { + auto& b = static_cast(e); + std::string a = lower_place_address(*b.target, s); + std::string r = temp(s); + emit(s, {mir::Op::AddressOf, r, ty, {a}, "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Unary: { + auto& u = static_cast(e); + auto [v, t] = lower_expr(*u.operand, s); + if (u.op == "move") + return {v, ty}; + if (u.op == "*") { + std::string r = temp(s); + emit(s, {mir::Op::Load, r, ty, {v}, "", {}, false, e.range}); + return {r, ty}; + } + std::string r = temp(s); + std::string zero = temp(s); + if (u.op == "-") { + // `ty` is the (possibly coerced) result type from sema, while `t` + // is the inner operand's own type. For `-literal` coerced to a + // wider integer (e.g. `result >= -4095` with result: i64), using + // `t` here emits `sub i32` whose result is later used as i64 and + // produces invalid `icmp sge i64 %x, %y(i32)` IR at -O0 + // (-O2 hides it via constant folding). Lower in `ty` instead, + // casting the operand up when needed. + std::string operand = v; + if (t != ty) { + std::string casted = temp(s); + emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); + operand = casted; + } + if (ty.is_float()) { + emit(s, {mir::Op::ConstFloat, zero, ty, {}, "0.0", {}, false, e.range}); + } else { + emit(s, {mir::Op::ConstInt, zero, ty, {}, "0", {}, false, e.range}); + } + emit(s, {mir::Op::Binary, r, ty, {zero, operand}, "-", {}, false, e.range}); + } else { + std::string operand = v; + if (t != ty) { + std::string casted = temp(s); + emit(s, {mir::Op::Cast, casted, ty, {v}, t.str(), {}, false, e.range}); + operand = casted; + } + emit(s, {mir::Op::Binary, r, ty, {operand}, u.op, {}, false, e.range}); + } + return {r, ty}; + } + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + auto [l, lt] = lower_expr(*b.lhs, s); + auto [r, rt] = lower_expr(*b.rhs, s); + std::string out = temp(s); + bool cmp = b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || + b.op == ">="; + if (cmp) { + // Comparison operands must share one LLVM integer type. + l = coerce_value(s, l, lt, lt, b.lhs->range); + r = coerce_value(s, r, rt, lt, b.rhs->range); + } else { + l = coerce_value(s, l, lt, ty, b.lhs->range); + r = coerce_value(s, r, rt, ty, b.rhs->range); + } + emit(s, + {cmp ? mir::Op::Compare : mir::Op::Binary, + out, + cmp ? lt : ty, + {l, r}, + b.op, + {}, + false, + e.range}); + return {out, ty}; + } + case ast::ExprKind::Call: { + auto& c = static_cast(e); + if (c.callee->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*c.callee).name; + if (builtin_function(name)) + return lower_builtin_call(c, s); + } + std::string callee; + std::vector args; + if (c.callee->kind == ast::ExprKind::Name) { + auto& n = static_cast(*c.callee); + auto fi = model_.functions.find(n.name); + callee = n.name; + if (fi != model_.functions.end()) + callee = specialize_call(c, fi->second, s); + for (std::size_t i = 0; i < c.args.size(); ++i) { + auto [v, actual] = lower_expr(*c.args[i], s); + if (fi != model_.functions.end() && i < fi->second.params.size()) { + Type expected = subst_type(fi->second.params[i], s); + // For generic callees the formal may still be generic; + // coerce_value is a no-op unless it is a concrete + // int/bool/float mismatch (e.g. i32 literal -> i64 param). + v = coerce_value(s, v, actual, expected, c.args[i]->range); + } + args.push_back(v); + } + } else if (c.callee->kind == ast::ExprKind::Member) { + auto& member = static_cast(*c.callee); + Type receiver_ty = expr_type(*member.base, s); + Type owner = receiver_ty; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const auto fi = model_.functions.find(owner.name + "::" + member.member); + if (fi == model_.functions.end()) { + diags_.error( + e.range, "E0507", "internal: method signature missing during MIR lowering"); + return {"undef", ty}; + } + callee = fi->second.name; + if (!fi->second.params.empty() && fi->second.params.front().kind == TypeKind::Ref) { + if (receiver_ty.kind == TypeKind::Ref) + args.push_back(lower_expr(*member.base, s).first); + else + args.push_back(lower_place_address(*member.base, s)); + } else + args.push_back(lower_expr(*member.base, s).first); + // Method params[0] is the receiver already handled above. + const std::size_t param_offset = + fi->second.params.empty() ? 0 : args.size() - c.args.size(); + for (std::size_t i = 0; i < c.args.size(); ++i) { + auto [v, actual] = lower_expr(*c.args[i], s); + if (param_offset + i < fi->second.params.size()) { + Type expected = subst_type(fi->second.params[param_offset + i], s); + v = coerce_value(s, v, actual, expected, c.args[i]->range); + } + args.push_back(v); + } + } else { + diags_.error( + e.range, "E0507", "internal: unsupported call target during MIR lowering"); + return {"undef", ty}; + } + std::string r = ty.is_unit() ? "" : temp(s); + emit(s, {mir::Op::Call, r, ty, std::move(args), callee, {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Cast: { + auto& c = static_cast(e); + auto [v, from] = lower_expr(*c.value, s); + std::string r = temp(s); + emit(s, {mir::Op::Cast, r, ty, {v}, from.str(), {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + mir::Instruction in; + in.op = mir::Op::InlineAsm; + in.type = Type::builtin("unit"); + in.text = a.assembly; + in.flag = a.is_volatile; + in.range = e.range; + for (const auto& o : a.operands) { + mir::AsmOperand mo; + mo.kind = o.kind; + mo.constraint = o.constraint; + if (o.value) { + auto [v, t] = lower_expr(*o.value, s); + mo.value = v; + mo.type = t; + } + if (!o.out_name.empty()) { + auto it = s.slots.find(o.out_name); + if (it != s.slots.end()) { + mo.out_slot = it->second; + mo.type = s.local_types[o.out_name]; + } + } + in.asm_operands.push_back(std::move(mo)); + } + emit(s, std::move(in)); + return {"", ty}; + } + case ast::ExprKind::Await: { + auto& a = static_cast(e); + auto [v, future_ty] = lower_expr(*a.value, s); + (void)future_ty; + std::string r = temp(s); + emit(s, {mir::Op::Await, r, ty, {v}, "", {}, false, e.range}); + return {r, ty}; + } + case ast::ExprKind::Member: { + std::string addr = lower_place_address(e, s); + std::string r = temp(s); + const bool atomic = place_is_atomic(e, s); + emit(s, + {mir::Op::Load, + r, + ty, + {addr}, + atomic ? load_order(e, s) : "", + {}, + atomic, + e.range}); + return {r, ty}; + } + case ast::ExprKind::Index: { + std::string addr = lower_place_address(e, s); + std::string r = temp(s); + emit(s, {mir::Op::Load, r, ty, {addr}, "", {}, false, e.range}); + return {r, ty}; + } + } + return {"undef", ty}; +} + +std::pair MIRLowerer::lower_builtin_call(const ast::CallExpr& call, FnState& s) { + const auto& name = static_cast(*call.callee).name; + const Type result = expr_type(call, s); + auto declare_call = [&](const std::string& runtime_name, + const Type& runtime_result, + const std::vector& parameter_types) { + std::vector parameters; + for (std::size_t i = 0; i < parameter_types.size(); ++i) + parameters.push_back({"p" + std::to_string(i), parameter_types[i]}); + declare_runtime_function(runtime_name, runtime_result, std::move(parameters)); + }; + auto runtime_call = [&](const std::string& runtime_name, + const Type& runtime_result, + const std::vector& parameter_types, + std::vector arguments, + const SourceRange& range) { + declare_call(runtime_name, runtime_result, parameter_types); + const std::string output = runtime_result.is_unit() ? "" : temp(s); + emit(s, + {mir::Op::Call, + output, + runtime_result, + std::move(arguments), + runtime_name, + {}, + false, + range}); + return output; + }; + auto cast_value = + [&](const std::string& value, const Type& from, const Type& to, const SourceRange& range) { + if (from == to) + return value; + const std::string output = temp(s); + emit(s, {mir::Op::Cast, output, to, {value}, from.str(), {}, false, range}); + return output; + }; + + if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { + const bool stderr_output = name == "eprint" || name == "eprintln"; + const Type i32 = Type::builtin("i32"); + const Type fd_type = i32; + const std::string fd = temp(s); + emit( + s, + {mir::Op::ConstInt, fd, fd_type, {}, stderr_output ? "2" : "1", {}, false, call.range}); + + for (std::size_t i = 0; i < call.args.size(); ++i) { + if (i != 0) { + runtime_call( + "uinx_print_space", Type::builtin("unit"), {fd_type}, {fd}, call.range); + } + auto [value, type] = lower_expr(*call.args[i], s); + if (type.is_integer() && type.kind != TypeKind::I128 && type.kind != TypeKind::U128) { + const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); + value = cast_value(value, type, target, call.args[i]->range); + runtime_call(unsigned_integer_kind(type.kind) ? "uinx_print_u64" : "uinx_print_i64", + Type::builtin("unit"), + {target, fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.is_float()) { + value = cast_value(value, type, Type::builtin("f64"), call.args[i]->range); + runtime_call("uinx_print_f64", + Type::builtin("unit"), + {Type::builtin("f64"), fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.kind == TypeKind::Bool) { + value = cast_value(value, type, Type::builtin("i64"), call.args[i]->range); + runtime_call("uinx_print_bool", + Type::builtin("unit"), + {Type::builtin("i64"), fd_type}, + {value, fd}, + call.args[i]->range); + } else if (type.kind == TypeKind::Char) { + runtime_call("uinx_print_char", + Type::builtin("unit"), + {Type::builtin("i32"), fd_type}, + {value, fd}, + call.args[i]->range); + } else { + runtime_call("uinx_print_cstr", + Type::builtin("unit"), + {Type::ref(Type::builtin("str"), false), fd_type}, + {value, fd}, + call.args[i]->range); + } + } + runtime_call("uinx_print_newline", Type::builtin("unit"), {fd_type}, {fd}, call.range); + return {"", Type::builtin("unit")}; + } + + if (name == "input") { + const Type str_ref = Type::ref(Type::builtin("str"), false); + std::string prompt = "null"; + if (!call.args.empty()) + prompt = lower_expr(*call.args[0], s).first; + const std::string fd = temp(s); + emit(s, {mir::Op::ConstInt, fd, Type::builtin("i32"), {}, "1", {}, false, call.range}); + return {runtime_call("uinx_input_cstr", + str_ref, + {str_ref, Type::builtin("i32")}, + {prompt, fd}, + call.range), + str_ref}; + } + + auto [value, type] = lower_expr(*call.args[0], s); + const Type str_ref = Type::ref(Type::builtin("str"), false); + const auto is_string = [](const Type& checked) { + return checked.kind == TypeKind::Str || (checked.kind == TypeKind::Ref && checked.pointee && + checked.pointee->kind == TypeKind::Str); + }; + if (name == "str") { + if (is_string(type)) + return {value, str_ref}; + if (type.is_integer()) { + const Type target = Type::builtin(unsigned_integer_kind(type.kind) ? "u64" : "i64"); + value = cast_value(value, type, target, call.range); + return {runtime_call(unsigned_integer_kind(type.kind) ? "uinx_string_from_u64" + : "uinx_string_from_i64", + str_ref, + {target}, + {value}, + call.range), + str_ref}; + } + if (type.is_float()) { + value = cast_value(value, type, Type::builtin("f64"), call.range); + return { + runtime_call( + "uinx_string_from_f64", str_ref, {Type::builtin("f64")}, {value}, call.range), + str_ref}; + } + if (type.kind == TypeKind::Bool) { + value = cast_value(value, type, Type::builtin("i64"), call.range); + return { + runtime_call( + "uinx_string_from_bool", str_ref, {Type::builtin("i64")}, {value}, call.range), + str_ref}; + } + if (type.kind == TypeKind::Char) + return { + runtime_call( + "uinx_string_from_char", str_ref, {Type::builtin("i32")}, {value}, call.range), + str_ref}; + } + if (name == "int" || name == "float") { + const Type target = Type::builtin(name == "int" ? "i64" : "f64"); + if (is_string(type)) { + return {runtime_call(name == "int" ? "uinx_parse_i64_cstr" : "uinx_parse_f64_cstr", + target, + {str_ref}, + {value}, + call.range), + target}; + } + return {cast_value(value, type, target, call.range), target}; + } + if (name == "bool") { + if (type.kind == TypeKind::Bool) + return {value, type}; + if (is_string(type)) { + const Type i64 = Type::builtin("i64"); + value = runtime_call("uinx_parse_i64_cstr", i64, {str_ref}, {value}, call.range); + type = i64; + } + const std::string zero = temp(s); + emit(s, {mir::Op::ConstInt, zero, type, {}, "0", {}, false, call.range}); + const std::string output = temp(s); + emit(s, + {mir::Op::Compare, + output, + Type::builtin("bool"), + {value, zero}, + "!=", + {}, + false, + call.range}); + return {output, Type::builtin("bool")}; + } + if (name == "len") { + if (is_string(type)) + return {runtime_call( + "uinx_cstr_len", Type::builtin("usize"), {str_ref}, {value}, call.range), + Type::builtin("usize")}; + } + diags_.error(call.range, "E0507", "internal: unsupported builtin lowering"); + return {"undef", result}; +} +} // namespace uinx diff --git a/src/mir_internal.hpp b/src/mir_internal.hpp new file mode 100644 index 0000000..31d413b --- /dev/null +++ b/src/mir_internal.hpp @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of mir.cpp. + +#pragma once + +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx::mir_detail { +inline bool unsigned_integer_kind(TypeKind kind) { + return kind == TypeKind::U8 || kind == TypeKind::U16 || kind == TypeKind::U32 || + kind == TypeKind::U64 || kind == TypeKind::U128 || kind == TypeKind::Usize; +} + +inline bool builtin_function(std::string_view name) { + static const std::unordered_set names{ + "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; + return names.contains(name); +} +} // namespace uinx::mir_detail diff --git a/src/mir_optimizer.cpp b/src/mir_optimizer.cpp new file mode 100644 index 0000000..f3f4d9b --- /dev/null +++ b/src/mir_optimizer.cpp @@ -0,0 +1,298 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +void MIROptimizer::remove_unreachable(mir::Function& f) const { + std::unordered_set reachable; + if (f.blocks.empty()) + return; + std::unordered_map labels; + for (std::size_t i = 0; i < f.blocks.size(); ++i) + labels[f.blocks[i].label] = i; + std::vector work{0}; + while (!work.empty()) { + const std::size_t index = work.back(); + work.pop_back(); + if (index >= f.blocks.size() || !reachable.insert(f.blocks[index].label).second) + continue; + const auto& block = f.blocks[index]; + if (block.instructions.empty()) { + if (index + 1 < f.blocks.size()) + work.push_back(index + 1); + continue; + } + const auto& tail = block.instructions.back(); + if (tail.op == mir::Op::Jump && !tail.args.empty()) { + if (auto it = labels.find(tail.args[0]); it != labels.end()) + work.push_back(it->second); + } else if (tail.op == mir::Op::CondJump && tail.args.size() >= 3) { + if (auto it = labels.find(tail.args[1]); it != labels.end()) + work.push_back(it->second); + if (auto it = labels.find(tail.args[2]); it != labels.end()) + work.push_back(it->second); + } else if (tail.op != mir::Op::Return && index + 1 < f.blocks.size()) { + work.push_back(index + 1); + } + } + f.blocks.erase(std::remove_if(f.blocks.begin(), + f.blocks.end(), + [&](const mir::BasicBlock& block) { + return !reachable.contains(block.label); + }), + f.blocks.end()); +} + +void MIROptimizer::forward_local_loads(mir::Function& fn) const { + for (auto& block : fn.blocks) { + std::unordered_map slots; + std::unordered_map aliases; + auto resolve = [&](std::string value) { + for (int depth = 0; depth < 32; ++depth) { + auto it = aliases.find(value); + if (it == aliases.end() || it->second == value) + break; + value = it->second; + } + return value; + }; + std::vector output; + output.reserve(block.instructions.size()); + for (auto instruction : block.instructions) { + for (auto& arg : instruction.args) + arg = resolve(arg); + for (auto& operand : instruction.asm_operands) { + if (!operand.value.empty()) + operand.value = resolve(operand.value); + } + const auto is_local_slot = [](const std::string& address) { + return !address.empty() && address.front() == '%' && + address.find(".slot.") != std::string::npos; + }; + if (instruction.op == mir::Op::Load && !instruction.flag && !instruction.args.empty()) { + const auto it = slots.find(instruction.args[0]); + if (it != slots.end()) { + aliases[instruction.result] = resolve(it->second); + continue; + } + } + if (instruction.op == mir::Op::Store && !instruction.flag && + instruction.args.size() >= 2) { + const std::string& address = instruction.args[1]; + if (is_local_slot(address)) + slots[address] = instruction.args[0]; + else + slots.clear(); + } else if (instruction.op == mir::Op::AddressOf) { + if (!instruction.args.empty()) + slots.erase(instruction.args[0]); + } else if (instruction.op == mir::Op::Call || instruction.op == mir::Op::InlineAsm || + instruction.op == mir::Op::AtomicRmw || instruction.op == mir::Op::Fence || + instruction.op == mir::Op::CompilerFence || + (instruction.op == mir::Op::Load && instruction.flag) || + (instruction.op == mir::Op::Store && instruction.flag)) { + slots.clear(); + } + output.push_back(std::move(instruction)); + } + block.instructions = std::move(output); + } +} + +void MIROptimizer::constant_fold(mir::Function& fn) const { + for (auto& block : fn.blocks) { + std::unordered_map constants; + auto get = [&](const std::string& value, std::int64_t& out) { + if (auto it = constants.find(value); it != constants.end()) { + out = it->second; + return true; + } + char* end = nullptr; + const long long parsed = std::strtoll(value.c_str(), &end, 0); + if (end && *end == '\0' && end != value.c_str()) { + out = static_cast(parsed); + return true; + } + return false; + }; + for (auto& instruction : block.instructions) { + if ((instruction.op == mir::Op::ConstInt || instruction.op == mir::Op::ConstBool || + instruction.op == mir::Op::ConstChar) && + !instruction.result.empty()) { + std::int64_t value{}; + if (get(instruction.text, value)) + constants[instruction.result] = value; + continue; + } + if (instruction.type.kind == TypeKind::I128 || instruction.type.kind == TypeKind::U128) + continue; + if (instruction.op == mir::Op::Binary && !instruction.result.empty()) { + std::int64_t a{}, b{}; + if (instruction.text == "!" && instruction.args.size() == 1 && + get(instruction.args[0], a)) { + const std::int64_t result = instruction.type.kind == TypeKind::Bool ? !a : ~a; + instruction.op = instruction.type.kind == TypeKind::Bool ? mir::Op::ConstBool + : mir::Op::ConstInt; + instruction.args.clear(); + instruction.text = std::to_string(result); + constants[instruction.result] = result; + continue; + } + if (instruction.args.size() != 2 || !get(instruction.args[0], a) || + !get(instruction.args[1], b)) + continue; + std::int64_t result{}; + bool ok = true; + if (instruction.text == "+") + result = static_cast(static_cast(a) + + static_cast(b)); + else if (instruction.text == "-") + result = static_cast(static_cast(a) - + static_cast(b)); + else if (instruction.text == "*") + result = static_cast(static_cast(a) * + static_cast(b)); + else if (instruction.text == "/") { + if (b == 0) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) / + static_cast(b)) + : a / b; + } else if (instruction.text == "%") { + if (b == 0) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) % + static_cast(b)) + : a % b; + } else if (instruction.text == "&" || instruction.text == "&&") + result = a & b; + else if (instruction.text == "|" || instruction.text == "||") + result = a | b; + else if (instruction.text == "^") + result = a ^ b; + else if (instruction.text == "<<") { + if (b < 0 || b >= 64) + ok = false; + else + result = static_cast(static_cast(a) << b); + } else if (instruction.text == ">>") { + if (b < 0 || b >= 64) + ok = false; + else + result = unsigned_integer_kind(instruction.type.kind) + ? static_cast(static_cast(a) >> b) + : (a >> b); + } else + ok = false; + if (ok) { + instruction.op = mir::Op::ConstInt; + instruction.args.clear(); + instruction.text = std::to_string(result); + constants[instruction.result] = result; + } + } else if (instruction.op == mir::Op::Compare && instruction.args.size() == 2 && + !instruction.result.empty()) { + std::int64_t a{}, b{}; + if (!get(instruction.args[0], a) || !get(instruction.args[1], b)) + continue; + const bool u = unsigned_integer_kind(instruction.type.kind); + bool result = false; + if (instruction.text == "==") + result = a == b; + else if (instruction.text == "!=") + result = a != b; + else if (instruction.text == "<") + result = + u ? static_cast(a) < static_cast(b) : a < b; + else if (instruction.text == "<=") + result = + u ? static_cast(a) <= static_cast(b) : a <= b; + else if (instruction.text == ">") + result = + u ? static_cast(a) > static_cast(b) : a > b; + else if (instruction.text == ">=") + result = + u ? static_cast(a) >= static_cast(b) : a >= b; + instruction.op = mir::Op::ConstBool; + instruction.type = Type::builtin("bool"); + instruction.args.clear(); + instruction.text = result ? "1" : "0"; + constants[instruction.result] = result ? 1 : 0; + } + } + } +} + +void MIROptimizer::eliminate_dead_values(mir::Function& fn) const { + bool changed = true; + while (changed) { + changed = false; + std::unordered_map uses; + for (const auto& block : fn.blocks) + for (const auto& instruction : block.instructions) { + for (const auto& arg : instruction.args) + ++uses[arg]; + for (const auto& operand : instruction.asm_operands) + if (!operand.value.empty()) + ++uses[operand.value]; + } + for (auto& block : fn.blocks) { + auto it = std::remove_if( + block.instructions.begin(), + block.instructions.end(), + [&](const mir::Instruction& instruction) { + if (instruction.result.empty() || uses[instruction.result] != 0) + return false; + bool pure = + instruction.op == mir::Op::Alloca || instruction.op == mir::Op::ConstInt || + instruction.op == mir::Op::ConstFloat || + instruction.op == mir::Op::ConstBool || + instruction.op == mir::Op::ConstChar || + instruction.op == mir::Op::ConstString || + instruction.op == mir::Op::StructInit || + instruction.op == mir::Op::FieldAddr || instruction.op == mir::Op::Binary || + instruction.op == mir::Op::Compare || instruction.op == mir::Op::Cast || + instruction.op == mir::Op::AddressOf || + (instruction.op == mir::Op::Load && !instruction.flag); + if (pure) + changed = true; + return pure; + }); + block.instructions.erase(it, block.instructions.end()); + } + } +} + +void MIROptimizer::run(mir::Module& m) const { + if (level_ <= 0) + return; + for (auto& f : m.functions) { + if (f.is_extern) + continue; + remove_unreachable(f); + forward_local_loads(f); + constant_fold(f); + eliminate_dead_values(f); + if (level_ >= 2) { + forward_local_loads(f); + eliminate_dead_values(f); + } + } +} +} // namespace uinx diff --git a/src/mir_stmt.cpp b/src/mir_stmt.cpp new file mode 100644 index 0000000..09f8a8e --- /dev/null +++ b/src/mir_stmt.cpp @@ -0,0 +1,321 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "mir_internal.hpp" +#include "uinx/mir.hpp" + +#include +#include +#include +#include +#include +#include + +namespace uinx { +using namespace mir_detail; + +bool MIRLowerer::place_is_atomic(const ast::Expr& e, const FnState& s) const { + const Type ty = expr_type(e, s); + // Bool excluded: LLVM i1 atomics are invalid (byte-sized required). + // Use AtomicBool (u8-backed) instead. + const bool compatible = ty.is_integer() || ty.kind == TypeKind::RawPtr; + if (!compatible) + return false; + (void)s; + // Explicit-shared only: smp auto no longer promotes. Only `shared` + // globals/fields lower to LLVM atomics. percpu is never atomic. + if (e.kind == ast::ExprKind::Name) { + const auto& n = static_cast(e); + auto g = model_.globals.find(n.name); + if (g == model_.globals.end() || g->second.is_percpu) + return false; + return g->second.is_shared; + } + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base = expr_type(*m.base, s); + if (base.kind == TypeKind::Ref && base.pointee) + base = *base.pointee; + auto it = model_.structs.find(base.name); + return it != model_.structs.end() && it->second.shared_fields.contains(m.member); + } + return false; +} +std::string MIRLowerer::load_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acquire"; +} +std::string MIRLowerer::store_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "release"; +} +std::string MIRLowerer::rmw_order(const ast::Expr& e, const FnState& s) const { + (void)e; + return s.smp_mode == ast::SmpMode::Strict ? "seq_cst" : "acq_rel"; +} + +void MIRLowerer::lower_stmt(const ast::Stmt& st, FnState& s) { + if (auto l = dynamic_cast(&st)) { + auto [v, init_ty] = lower_expr(*l->init, s); + Type t = init_ty; + if (auto it = model_.binding_types.find(l); it != model_.binding_types.end()) + t = subst_type(it->second, s); + v = coerce_value(s, v, init_ty, t, l->range); + std::string sl = slot(s, l->name); + s.slots[l->name] = sl; + s.local_types[l->name] = t; + s.scope_locals.back().push_back(l->name); + emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, l->range}); + emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, l->range}); + return; + } + if (auto a = dynamic_cast(&st)) { + if (auto binding = model_.hir.assignment_bindings.find(a); + binding != model_.hir.assignment_bindings.end()) { + auto [v, init_ty] = lower_expr(*a->value, s); + Type t = init_ty; + if (auto it = model_.assignment_binding_types.find(a); + it != model_.assignment_binding_types.end()) + t = subst_type(it->second, s); + v = coerce_value(s, v, init_ty, t, a->range); + const auto& name = static_cast(*a->target).name; + std::string sl = slot(s, name); + s.slots[name] = sl; + s.local_types[name] = t; + s.scope_locals.back().push_back(name); + emit(s, {mir::Op::Alloca, sl, t, {}, "", {}, false, a->range}); + emit(s, {mir::Op::Store, "", t, {v, sl}, "", {}, false, a->range}); + return; + } + std::string addr = lower_place_address(*a->target, s); + Type target_ty = expr_type(*a->target, s); + auto [v, vt] = lower_expr(*a->value, s); + v = coerce_value(s, v, vt, target_ty, a->range); + const bool atomic = place_is_atomic(*a->target, s); + if (atomic && a->op != "=") { + const std::string op = a->op.substr(0, a->op.size() - 1); + if (op == "+" || op == "-" || op == "&" || op == "|" || op == "^") { + emit(s, + {mir::Op::AtomicRmw, + "", + target_ty, + {addr, v, rmw_order(*a->target, s)}, + op, + {}, + true, + a->range}); + return; + } + diags_.error(a->range, + "E0509", + "atomic compound assignment '" + a->op + + "' is not supported; use an explicit compare/exchange loop"); + return; + } + if (a->op != "=") { + std::string old = temp(s); + emit(s, {mir::Op::Load, old, target_ty, {addr}, "", {}, false, a->range}); + std::string combined = temp(s); + std::string op = a->op.substr(0, a->op.size() - 1); + emit(s, {mir::Op::Binary, combined, target_ty, {old, v}, op, {}, false, a->range}); + v = combined; + } + emit(s, + {mir::Op::Store, + "", + target_ty, + {v, addr}, + atomic ? store_order(*a->target, s) : "", + {}, + atomic, + a->range}); + return; + } + if (auto e = dynamic_cast(&st)) { + (void)lower_expr(*e->expr, s); + return; + } + if (auto r = dynamic_cast(&st)) { + emit_scope_drops(s); + if (r->value) { + auto [v, t] = lower_expr(*r->value, s); + v = coerce_value(s, v, t, s.fn.result, r->range); + emit(s, {mir::Op::Return, "", s.fn.result, {v}, "", {}, false, r->range}); + } else + emit(s, {mir::Op::Return, "", Type::builtin("unit"), {}, "", {}, false, r->range}); + return; + } + if (auto f = dynamic_cast(&st)) { + emit(s, + {f->compiler_only ? mir::Op::CompilerFence : mir::Op::Fence, + "", + Type::builtin("unit"), + {}, + f->order, + {}, + false, + f->range}); + return; + } + if (auto b = dynamic_cast(&st)) { + lower_block(*b, s); + return; + } + if (auto u = dynamic_cast(&st)) { + lower_block(*u->body, s); + return; + } + if (auto i = dynamic_cast(&st)) { + auto [c, ct] = lower_expr(*i->condition, s); + (void)ct; + std::size_t then_i = new_block(s, "if.then." + std::to_string(s.fn.blocks.size())); + std::size_t else_i = new_block(s, "if.else." + std::to_string(s.fn.blocks.size())); + std::size_t merge_i = new_block(s, "if.end." + std::to_string(s.fn.blocks.size())); + std::string then_l = s.fn.blocks[then_i].label, else_l = s.fn.blocks[else_i].label, + merge_l = s.fn.blocks[merge_i].label; + emit(s, + {mir::Op::CondJump, + "", + Type::builtin("bool"), + {c, then_l, else_l}, + "", + {}, + false, + i->range}); + s.block = then_i; + lower_block(*i->then_block, s); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); + s.block = else_i; + if (i->else_block) + lower_block(*i->else_block, s); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {merge_l}, "", {}, false, i->range}); + s.block = merge_i; + return; + } + if (auto w = dynamic_cast(&st)) { + std::size_t cond_i = new_block(s, "while.cond." + std::to_string(s.fn.blocks.size())); + std::size_t body_i = new_block(s, "while.body." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "while.end." + std::to_string(s.fn.blocks.size())); + std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, + el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); + s.block = cond_i; + auto [c, t] = lower_expr(*w->condition, s); + (void)t; + emit(s, + {mir::Op::CondJump, "", Type::builtin("bool"), {c, bl, el}, "", {}, false, w->range}); + s.block = body_i; + s.loop_targets.push_back({cond_i, end_i}); + lower_block(*w->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, w->range}); + s.block = end_i; + return; + } + if (auto f = dynamic_cast(&st)) { + auto [begin_value, iter_ty] = lower_expr(*f->begin, s); + auto [end_value, end_ty] = lower_expr(*f->end, s); + (void)end_ty; + std::string iter_slot = slot(s, f->name); + s.slots[f->name] = iter_slot; + s.local_types[f->name] = iter_ty; + s.scope_locals.back().push_back(f->name); + emit(s, {mir::Op::Alloca, iter_slot, iter_ty, {}, "", {}, false, f->range}); + emit(s, {mir::Op::Store, "", iter_ty, {begin_value, iter_slot}, "", {}, false, f->range}); + std::size_t cond_i = new_block(s, "for.cond." + std::to_string(s.fn.blocks.size())); + std::size_t body_i = new_block(s, "for.body." + std::to_string(s.fn.blocks.size())); + std::size_t step_i = new_block(s, "for.step." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "for.end." + std::to_string(s.fn.blocks.size())); + const std::string cl = s.fn.blocks[cond_i].label, bl = s.fn.blocks[body_i].label, + sl = s.fn.blocks[step_i].label, el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); + s.block = cond_i; + std::string current = temp(s); + emit(s, {mir::Op::Load, current, iter_ty, {iter_slot}, "", {}, false, f->range}); + std::string cond = temp(s); + emit(s, + {mir::Op::Compare, + cond, + iter_ty, + {current, end_value}, + f->inclusive ? "<=" : "<", + {}, + false, + f->range}); + emit(s, + {mir::Op::CondJump, + "", + Type::builtin("bool"), + {cond, bl, el}, + "", + {}, + false, + f->range}); + s.block = body_i; + s.loop_targets.push_back({step_i, end_i}); + lower_block(*f->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {sl}, "", {}, false, f->range}); + s.block = step_i; + std::string old = temp(s); + emit(s, {mir::Op::Load, old, iter_ty, {iter_slot}, "", {}, false, f->range}); + std::string one = temp(s); + emit(s, {mir::Op::ConstInt, one, iter_ty, {}, "1", {}, false, f->range}); + std::string next = temp(s); + emit(s, {mir::Op::Binary, next, iter_ty, {old, one}, "+", {}, false, f->range}); + emit(s, {mir::Op::Store, "", iter_ty, {next, iter_slot}, "", {}, false, f->range}); + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {cl}, "", {}, false, f->range}); + s.block = end_i; + return; + } + if (auto l = dynamic_cast(&st)) { + std::size_t body_i = new_block(s, "loop.body." + std::to_string(s.fn.blocks.size())); + std::size_t end_i = new_block(s, "loop.end." + std::to_string(s.fn.blocks.size())); + const std::string bl = s.fn.blocks[body_i].label, el = s.fn.blocks[end_i].label; + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); + s.block = body_i; + s.loop_targets.push_back({body_i, end_i}); + lower_block(*l->body, s); + s.loop_targets.pop_back(); + if (!block(s).terminated) + emit(s, {mir::Op::Jump, "", Type::builtin("unit"), {bl}, "", {}, false, l->range}); + s.block = end_i; + return; + } + if (dynamic_cast(&st)) { + if (!s.loop_targets.empty()) { + emit_scope_drops(s); + emit(s, + {mir::Op::Jump, + "", + Type::builtin("unit"), + {s.fn.blocks[s.loop_targets.back().second].label}, + "", + {}, + false, + st.range}); + } + return; + } + if (dynamic_cast(&st)) { + if (!s.loop_targets.empty()) { + emit_scope_drops(s); + emit(s, + {mir::Op::Jump, + "", + Type::builtin("unit"), + {s.fn.blocks[s.loop_targets.back().first].label}, + "", + {}, + false, + st.range}); + } + return; + } +} +} // namespace uinx diff --git a/src/sema.cpp b/src/sema.cpp index 5df0701..d3128a1 100644 --- a/src/sema.cpp +++ b/src/sema.cpp @@ -4,438 +4,15 @@ #include "uinx/sema.hpp" +#include "sema_internal.hpp" + #include #include #include namespace uinx { -namespace { -Type replace_self(Type type, const Type& owner) { - if (type.kind == TypeKind::Named && type.name == "Self") - return owner; - if (type.pointee) - type.pointee = std::make_shared(replace_self(*type.pointee, owner)); - for (auto& arg : type.args) - arg = replace_self(std::move(arg), owner); - return type; -} - -bool integer_literal(const ast::Expr* expr) { - if (!expr) - return false; - if (expr->kind == ast::ExprKind::Integer) - return true; - if (expr->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*expr); - return unary.op == "-" && unary.operand && unary.operand->kind == ast::ExprKind::Integer; - } - return false; -} - -bool can_coerce_expr(const ast::Expr* expr, const Type& from, const Type& to) { - if (can_coerce(from, to)) - return true; - if (expr && expr->kind == ast::ExprKind::String && from.kind == TypeKind::Ref && from.pointee && - from.pointee->kind == TypeKind::Str && to.kind == TypeKind::Str) - return true; - return integer_literal(expr) && from.is_integer() && to.is_integer(); -} - -bool static_initializer_expr(const ast::Expr& expr) { - if (expr.kind == ast::ExprKind::Integer || expr.kind == ast::ExprKind::Float || - expr.kind == ast::ExprKind::Bool || expr.kind == ast::ExprKind::Char) - return true; - if (expr.kind == ast::ExprKind::StructLiteral) { - const auto& structure = static_cast(expr); - return std::all_of( - structure.fields.begin(), structure.fields.end(), [](const ast::FieldInit& field) { - if (!field.value) - return false; - if (field.value->kind == ast::ExprKind::Integer) - return static_cast(*field.value).value == "0"; - if (field.value->kind == ast::ExprKind::Bool) - return !static_cast(*field.value).value; - return false; - }); - } - return false; -} - -bool builtin_function(std::string_view name) { - static const std::unordered_set names{ - "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; - return names.contains(name); -} -} // namespace -hir::SymbolId NameResolver::add(hir::Module& out, - hir::SymbolKind kind, - std::string name, - SourceRange range, - std::optional parent) { - hir::SymbolId id = static_cast(out.symbols.size()); - out.symbols.push_back({id, kind, std::move(name), std::move(range), parent}); - return id; -} -hir::Module NameResolver::resolve(const ast::Module& module) { - hir::Module out; - out.ast = &module; - for (const auto& item : module.items) { - std::visit( - [&](const auto& x) { - using T = std::decay_t; - if constexpr (std::is_same_v || - std::is_same_v || - std::is_same_v || - std::is_same_v) { - hir::SymbolKind k = hir::SymbolKind::Function; - if constexpr (std::is_same_v) - k = hir::SymbolKind::Struct; - else if constexpr (std::is_same_v) - k = hir::SymbolKind::Trait; - else if constexpr (std::is_same_v) - k = hir::SymbolKind::Global; - if (out.globals.contains(x.name)) { - if constexpr (std::is_same_v) { - if (!x.is_extern) - diags_.error( - x.range, "E0200", "duplicate global symbol '" + x.name + "'"); - } else - diags_.error( - x.range, "E0200", "duplicate global symbol '" + x.name + "'"); - } else - out.globals[x.name] = add(out, k, x.name, x.range); - } - }, - item); - } - for (const auto& item : module.items) - if (auto f = std::get_if(&item)) - resolve_function(out, *f); - else if (auto im = std::get_if(&item)) - for (const auto& m : im->methods) - resolve_function(out, m); - return out; -} -void NameResolver::resolve_function(hir::Module& out, const ast::FunctionDecl& fn) { - std::optional owner; - auto gi = out.globals.find(fn.name); - if (gi != out.globals.end()) - owner = gi->second; - std::vector> scopes(1); - for (const auto& g : fn.generics) { - auto id = add(out, hir::SymbolKind::Generic, g.name, g.range, owner); - if (scopes.back().contains(g.name)) - diags_.error(g.range, "E0201", "duplicate generic parameter '" + g.name + "'"); - else - scopes.back()[g.name] = id; - } - for (const auto& p : fn.params) { - auto id = add(out, hir::SymbolKind::Parameter, p.name, p.range, owner); - if (scopes.back().contains(p.name)) - diags_.error(p.range, "E0202", "duplicate parameter '" + p.name + "'"); - else - scopes.back()[p.name] = id; - out.params[&p] = id; - } - if (fn.body) - resolve_block(out, *fn.body, scopes, owner); -} -void NameResolver::resolve_block( - hir::Module& out, - const ast::BlockStmt& block, - std::vector>& scopes, - std::optional owner) { - scopes.emplace_back(); - for (const auto& sp : block.stmts) { - const auto& s = *sp; - if (auto l = dynamic_cast(&s)) { - if (l->init) - resolve_expr(out, *l->init, scopes); - auto id = add(out, hir::SymbolKind::Local, l->name, l->range, owner); - if (scopes.back().contains(l->name)) - diags_.error( - l->range, "E0203", "binding '" + l->name + "' already exists in this scope"); - else - scopes.back()[l->name] = id; - out.bindings[l] = id; - } else if (auto a = dynamic_cast(&s)) { - if (a->op == "=" && a->target->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*a->target).name; - bool resolved = out.globals.contains(name); - for (auto it = scopes.rbegin(); !resolved && it != scopes.rend(); ++it) - resolved = it->contains(name); - if (!resolved) { - resolve_expr(out, *a->value, scopes); - auto id = add(out, hir::SymbolKind::Local, name, a->target->range, owner); - scopes.back()[name] = id; - out.assignment_bindings[a] = id; - out.expr_resolution[a->target.get()] = id; - continue; - } - } - resolve_expr(out, *a->target, scopes); - resolve_expr(out, *a->value, scopes); - } else if (auto e = dynamic_cast(&s)) { - resolve_expr(out, *e->expr, scopes); - } else if (auto r = dynamic_cast(&s)) { - if (r->value) - resolve_expr(out, *r->value, scopes); - } else if (auto b = dynamic_cast(&s)) { - resolve_block(out, *b, scopes, owner); - } else if (auto i = dynamic_cast(&s)) { - resolve_expr(out, *i->condition, scopes); - resolve_block(out, *i->then_block, scopes, owner); - if (i->else_block) - resolve_block(out, *i->else_block, scopes, owner); - } else if (auto w = dynamic_cast(&s)) { - resolve_expr(out, *w->condition, scopes); - resolve_block(out, *w->body, scopes, owner); - } else if (auto f = dynamic_cast(&s)) { - resolve_expr(out, *f->begin, scopes); - resolve_expr(out, *f->end, scopes); - scopes.emplace_back(); - auto id = add(out, hir::SymbolKind::Local, f->name, f->range, owner); - scopes.back()[f->name] = id; - out.for_bindings[f] = id; - resolve_block(out, *f->body, scopes, owner); - scopes.pop_back(); - } else if (auto loop = dynamic_cast(&s)) { - resolve_block(out, *loop->body, scopes, owner); - } else if (auto u = dynamic_cast(&s)) { - resolve_block(out, *u->body, scopes, owner); - } - } - scopes.pop_back(); -} -void NameResolver::resolve_expr( - hir::Module& out, - const ast::Expr& e, - std::vector>& scopes) { - switch (e.kind) { - case ast::ExprKind::Name: { - auto& n = static_cast(e); - for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { - auto f = it->find(n.name); - if (f != it->end()) { - out.expr_resolution[&e] = f->second; - return; - } - } - auto g = out.globals.find(n.name); - if (g != out.globals.end()) { - out.expr_resolution[&e] = g->second; - return; - } - if (builtin_function(n.name)) - return; - diags_.error(e.range, "E0204", "unresolved name '" + n.name + "'"); - break; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto g = out.globals.find(st.name); - if (g == out.globals.end()) - diags_.error(e.range, "E0206", "unknown struct '" + st.name + "'"); - else - out.expr_resolution[&e] = g->second; - for (auto& f : st.fields) - resolve_expr(out, *f.value, scopes); - break; - } - case ast::ExprKind::Unary: - resolve_expr(out, *static_cast(e).operand, scopes); - break; - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - resolve_expr(out, *b.lhs, scopes); - resolve_expr(out, *b.rhs, scopes); - break; - } - case ast::ExprKind::Borrow: - resolve_expr(out, *static_cast(e).target, scopes); - break; - case ast::ExprKind::Call: { - auto& c = static_cast(e); - resolve_expr(out, *c.callee, scopes); - for (auto& a : c.args) - resolve_expr(out, *a, scopes); - break; - } - case ast::ExprKind::Member: - resolve_expr(out, *static_cast(e).base, scopes); - break; - case ast::ExprKind::Index: { - auto& i = static_cast(e); - resolve_expr(out, *i.base, scopes); - resolve_expr(out, *i.index, scopes); - break; - } - case ast::ExprKind::Await: - resolve_expr(out, *static_cast(e).value, scopes); - break; - case ast::ExprKind::Cast: - resolve_expr(out, *static_cast(e).value, scopes); - break; - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - for (auto& o : a.operands) - if (o.value) - resolve_expr(out, *o.value, scopes); - for (auto& o : a.operands) - if (!o.out_name.empty()) { - bool found = false; - for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) - if (auto f = it->find(o.out_name); f != it->end()) { - found = true; - break; - } - if (!found) - diags_.error( - o.range, "E0205", "unknown asm output binding '" + o.out_name + "'"); - } - break; - } - default: - break; - } -} - -bool TraitSolver::satisfies(const Type& type, std::string_view trait) const { - return satisfies(type, trait, {}); -} - -bool TraitSolver::satisfies( - const Type& type, - std::string_view trait, - const std::unordered_map>& generic_bounds) const { - if (trait == "Copy") - return type.is_copy(); - if (trait != "Send" && trait != "Sync") - return false; - - std::function&)> - unify_pattern; - unify_pattern = [&](const Type& pattern, - const Type& actual, - std::unordered_map& substitutions) -> bool { - if (pattern.kind == TypeKind::Generic) { - auto [it, inserted] = substitutions.emplace(pattern.name, actual); - return inserted || it->second == actual; - } - if (pattern.kind != actual.kind || pattern.name != actual.name || - pattern.mut != actual.mut || pattern.args.size() != actual.args.size()) - return false; - if (static_cast(pattern.pointee) != static_cast(actual.pointee)) - return false; - if (pattern.pointee && !unify_pattern(*pattern.pointee, *actual.pointee, substitutions)) - return false; - for (std::size_t i = 0; i < pattern.args.size(); ++i) - if (!unify_pattern(pattern.args[i], actual.args[i], substitutions)) - return false; - return true; - }; +using namespace sema_detail; - std::function solve; - solve = [&](const Type& current, std::string_view current_trait, std::size_t depth) -> bool { - if (depth > 64) - return false; - - for (const auto& impl : model_.impls) - if (impl.trait == current_trait && impl.for_type == current) - return true; - - for (const auto& marker : model_.markers) { - if (marker.trait != current_trait) - continue; - std::unordered_map substitutions; - if (!unify_pattern(marker.type, current, substitutions)) - continue; - bool valid = true; - for (const auto& [generic, bounds] : marker.bounds) { - auto actual = substitutions.find(generic); - if (actual == substitutions.end()) { - valid = false; - break; - } - for (const auto& bound : bounds) { - if (!solve(actual->second, bound, depth + 1)) { - valid = false; - break; - } - } - if (!valid) - break; - } - if (valid) - return true; - } - - if (current_trait != "Send" && current_trait != "Sync") - return false; - - std::unordered_set visiting; - std::function&, - std::unordered_set)> - auto_trait; - std::unordered_set assumed; - for (const auto& [generic, bounds] : generic_bounds) - for (const auto& bound : bounds) - assumed.insert(bound + ":" + generic); - auto_trait = [&](const Type& value, - std::string_view value_trait, - std::unordered_set& active, - std::unordered_set assumptions) -> bool { - const std::string key = std::string(value_trait) + ":" + value.str(); - if (!active.insert(key).second) - return true; - const auto finish = [&](bool result) { - active.erase(key); - return result; - }; - - if (value.kind == TypeKind::RawPtr) - return finish(false); - if (value.kind == TypeKind::Ref && value.pointee) { - if (value_trait == "Send" && value.mut) - return finish( - auto_trait(*value.pointee, "Send", active, std::move(assumptions))); - return finish(auto_trait(*value.pointee, "Sync", active, std::move(assumptions))); - } - if (value.kind == TypeKind::Generic) - return finish(assumptions.contains(std::string(value_trait) + ":" + value.name)); - if (value.kind == TypeKind::Str) - return finish(true); - if (value.kind != TypeKind::Named) - return finish(value.is_copy()); - - auto structure = model_.structs.find(value.name); - if (structure == model_.structs.end()) - return finish(false); - for (const auto& [generic, bounds] : structure->second.bounds) - if (std::find(bounds.begin(), bounds.end(), value_trait) != bounds.end()) - assumptions.insert(std::string(value_trait) + ":" + generic); - - std::unordered_map substitutions; - for (std::size_t i = 0; - i < structure->second.generic_names.size() && i < value.args.size(); - ++i) - substitutions[structure->second.generic_names[i]] = value.args[i]; - for (const auto& [field_name, field_type] : structure->second.fields) { - (void)field_name; - if (!auto_trait(substitute_type(field_type, substitutions), - value_trait, - active, - assumptions)) - return finish(false); - } - return finish(true); - }; - return auto_trait(type, trait, visiting, assumed); - }; - return solve(type, trait, 0); -} void TypeChecker::collect_items(const ast::Module& module) { for (const auto& item : module.items) { if (auto f = std::get_if(&item)) { @@ -692,38 +269,6 @@ void TypeChecker::validate_special_traits() { } } -void TypeChecker::validate_concurrency_boundaries() { - TraitSolver solver(model_); - for (const auto& [key, signature] : model_.functions) { - (void)key; - if (!signature.is_concurrent || !signature.decl) - continue; - // `extern` declarations are FFI trust boundaries without bodies: the - // `unsafe` block at the Uinx call site is the audit point, and raw - // pointers are the ABI mechanism itself (e.g. `uinx_atomic_*` - // intrinsics lowered directly to LLVM atomics). Enforcing `Send` on - // their parameters would make compare-exchange/fetch-add unusable - // from exactly the concurrent paths they exist for, while checking - // nothing about an actual transfer. Uinx-level callers stay checked. - if (signature.is_extern) - continue; - for (std::size_t i = 0; i < signature.params.size(); ++i) { - const Type& parameter = signature.params[i]; - if (solver.satisfies(parameter, "Send", signature.bounds)) - continue; - - const SourceRange range = i < signature.decl->params.size() - ? signature.decl->params[i].range - : signature.decl->range; - diags_.error(range, - "E0362", - "concurrent function parameter " + std::to_string(i + 1) + " of type '" + - parameter.str() + "' is not Send; use an owning Send type, a shared " + - "reference whose pointee is Sync, or add the required generic bound"); - } - } -} - SemanticModel TypeChecker::check(const ast::Module& module) { collect_items(module); validate_special_traits(); @@ -772,1024 +317,4 @@ SemanticModel TypeChecker::check(const ast::Module& module) { validate_concurrency_boundaries(); return std::move(model_); } -void TypeChecker::propagate_concurrency(ast::SmpMode mode) { - std::unordered_map> edges; - - std::function&)> visit_expr; - std::function&)> visit_stmt; - visit_expr = [&](const ast::Expr& expr, std::unordered_set& out) { - switch (expr.kind) { - case ast::ExprKind::Unary: - visit_expr(*static_cast(expr).operand, out); - break; - case ast::ExprKind::Binary: { - const auto& b = static_cast(expr); - visit_expr(*b.lhs, out); - visit_expr(*b.rhs, out); - break; - } - case ast::ExprKind::Borrow: - visit_expr(*static_cast(expr).target, out); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Name) { - const auto& name = static_cast(*call.callee).name; - if (model_.functions.contains(name)) - out.insert(name); - } else if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - auto type_it = model_.expr_types.find(member.base.get()); - if (type_it != model_.expr_types.end()) { - Type owner = type_it->second; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - const std::string key = owner.name + "::" + member.member; - if (model_.functions.contains(key)) - out.insert(key); - } - visit_expr(*member.base, out); - } - for (const auto& arg : call.args) - visit_expr(*arg, out); - break; - } - case ast::ExprKind::Member: - visit_expr(*static_cast(expr).base, out); - break; - case ast::ExprKind::Index: { - const auto& i = static_cast(expr); - visit_expr(*i.base, out); - visit_expr(*i.index, out); - break; - } - case ast::ExprKind::Await: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::Cast: - visit_expr(*static_cast(expr).value, out); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - visit_expr(*field.value, out); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - visit_expr(*operand.value, out); - break; - default: - break; - } - }; - visit_stmt = [&](const ast::Stmt& stmt, std::unordered_set& out) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - visit_expr(*let_stmt->init, out); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - visit_expr(*assign_stmt->target, out); - visit_expr(*assign_stmt->value, out); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - visit_expr(*expr_stmt->expr, out); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - visit_expr(*return_stmt->value, out); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - visit_expr(*if_stmt->condition, out); - for (const auto& child : if_stmt->then_block->stmts) - visit_stmt(*child, out); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - visit_stmt(*child, out); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - visit_expr(*while_stmt->condition, out); - for (const auto& child : while_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - visit_expr(*for_stmt->begin, out); - visit_expr(*for_stmt->end, out); - for (const auto& child : for_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - visit_stmt(*child, out); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - visit_stmt(*child, out); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - visit_stmt(*child, out); - } - }; - - for (const auto& [key, sig] : model_.functions) { - if (!sig.decl || !sig.decl->body) - continue; - auto& callees = edges[key]; - for (const auto& stmt : sig.decl->body->stmts) - visit_stmt(*stmt, callees); - } - - std::vector work; - for (const auto& [key, sig] : model_.functions) - if (sig.is_concurrent) - work.push_back(key); - std::unordered_set seen; - while (!work.empty()) { - std::string key = std::move(work.back()); - work.pop_back(); - if (!seen.insert(key).second) - continue; - auto fit = model_.functions.find(key); - if (fit != model_.functions.end()) - fit->second.is_concurrent = true; - auto edge = edges.find(key); - if (edge == edges.end()) - continue; - for (const auto& callee : edge->second) - work.push_back(callee); - } - - // All SMP modes are explicit-shared now: `auto`, `manual` and `strict` - // differ only in ordering of explicitly atomic accesses, never in - // promotion. See docs/MEMORY_MODEL.md. - (void)mode; - // Industrial-grade explicit-shared model (Rust + C++20 + LKMM fusion): - // `smp auto/manual/strict` no longer implicitly promotes mutable globals. - // Only explicitly `shared` globals/fields lower to LLVM atomics. Any - // concurrent access to a mutable non-shared, non-percpu global is E0363. - // Ordering: auto/manual => acquire/release/acq_rel for explicit atomics, - // strict => seq_cst. See docs/MEMORY_MODEL.md. - // Note: `shared bool` is rejected at definition (use AtomicBool over u8) - // because LLVM atomics must be byte-sized (i1 is not valid). - const auto atomic_compatible = [](const Type& type) { - return type.is_integer() || type.kind == TypeKind::RawPtr; - }; - - std::unordered_set reported; - std::function check_expr; - std::function check_stmt; - check_expr = [&](const ast::Expr& expr) { - if (expr.kind == ast::ExprKind::Name) { - const auto& name = static_cast(expr).name; - auto global = model_.globals.find(name); - if (global != model_.globals.end() && global->second.is_mut && - !global->second.is_percpu && !global->second.is_shared) { - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + name; - if (reported.insert(key).second) { - if (atomic_compatible(global->second.type)) { - diags_.error(expr.range, - "E0363", - "concurrent access to mutable global '" + name + - "' requires explicit `shared`, a lock, or `percpu` " - "storage; smp auto no longer promotes implicitly " - "(Rust+C++20+LKMM explicit-shared model)"); - } else { - diags_.error(expr.range, - "E0363", - "concurrent access to aggregate global '" + name + - "' cannot be strengthened as one atomic object; mark " - "atomic fields shared, use a lock, or use percpu " - "storage"); - } - } - } - return; - } - if (expr.kind == ast::ExprKind::Member) { - const auto& member = static_cast(expr); - if (member.base->kind == ast::ExprKind::Name) { - const auto& base_name = static_cast(*member.base).name; - auto global = model_.globals.find(base_name); - if (global != model_.globals.end() && global->second.is_mut && - !global->second.is_percpu && global->second.type.kind == TypeKind::Named) { - auto structure = model_.structs.find(global->second.type.name); - if (structure != model_.structs.end()) { - auto field = structure->second.fields.find(member.member); - if (field != structure->second.fields.end()) { - if (structure->second.shared_fields.contains(member.member)) - return; - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + - base_name + "." + member.member; - if (reported.insert(key).second) { - diags_.error( - expr.range, - "E0363", - "concurrent access to field '" + base_name + "." + - member.member + - "' requires explicit `shared` field, a lock, or " - "`percpu` storage; smp auto no longer promotes implicitly"); - } - return; - } - } - // Aggregate global itself accessed concurrently without per-field - // sharing: reject. - const std::string key = std::to_string(expr.range.begin.line) + ":" + - std::to_string(expr.range.begin.column) + ":" + - base_name; - if (reported.insert(key).second) { - diags_.error(expr.range, - "E0363", - "concurrent access to aggregate global '" + base_name + - "' cannot be strengthened as one atomic object; mark " - "atomic fields shared, use a lock, or use percpu " - "storage"); - } - return; - } - } - check_expr(*member.base); - return; - } - switch (expr.kind) { - case ast::ExprKind::Unary: - check_expr(*static_cast(expr).operand); - break; - case ast::ExprKind::Binary: { - const auto& binary = static_cast(expr); - check_expr(*binary.lhs); - check_expr(*binary.rhs); - break; - } - case ast::ExprKind::Borrow: - check_expr(*static_cast(expr).target); - break; - case ast::ExprKind::Call: { - const auto& call = static_cast(expr); - if (call.callee->kind == ast::ExprKind::Member) - check_expr(*static_cast(*call.callee).base); - for (const auto& arg : call.args) - check_expr(*arg); - break; - } - case ast::ExprKind::Index: { - const auto& index = static_cast(expr); - check_expr(*index.base); - check_expr(*index.index); - break; - } - case ast::ExprKind::Await: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::Cast: - check_expr(*static_cast(expr).value); - break; - case ast::ExprKind::StructLiteral: - for (const auto& field : static_cast(expr).fields) - check_expr(*field.value); - break; - case ast::ExprKind::Asm: - for (const auto& operand : static_cast(expr).operands) - if (operand.value) - check_expr(*operand.value); - break; - default: - break; - } - }; - check_stmt = [&](const ast::Stmt& stmt) { - if (const auto* let_stmt = dynamic_cast(&stmt)) { - if (let_stmt->init) - check_expr(*let_stmt->init); - } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { - check_expr(*assign_stmt->target); - check_expr(*assign_stmt->value); - } else if (const auto* expr_stmt = dynamic_cast(&stmt)) - check_expr(*expr_stmt->expr); - else if (const auto* return_stmt = dynamic_cast(&stmt)) { - if (return_stmt->value) - check_expr(*return_stmt->value); - } else if (const auto* if_stmt = dynamic_cast(&stmt)) { - check_expr(*if_stmt->condition); - for (const auto& child : if_stmt->then_block->stmts) - check_stmt(*child); - if (if_stmt->else_block) - for (const auto& child : if_stmt->else_block->stmts) - check_stmt(*child); - } else if (const auto* while_stmt = dynamic_cast(&stmt)) { - check_expr(*while_stmt->condition); - for (const auto& child : while_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* for_stmt = dynamic_cast(&stmt)) { - check_expr(*for_stmt->begin); - check_expr(*for_stmt->end); - for (const auto& child : for_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { - for (const auto& child : loop_stmt->body->stmts) - check_stmt(*child); - } else if (const auto* block_stmt = dynamic_cast(&stmt)) { - for (const auto& child : block_stmt->stmts) - check_stmt(*child); - } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { - for (const auto& child : unsafe_stmt->body->stmts) - check_stmt(*child); - } - }; - - for (const auto& [key, signature] : model_.functions) { - if (!signature.is_concurrent || !signature.decl || !signature.decl->body) - continue; - for (const auto& statement : signature.decl->body->stmts) - check_stmt(*statement); - } -} - -void TypeChecker::check_function(const ast::FunctionDecl& fn, const FunctionSig& sig) { - if (!fn.body) - return; - FnContext c; - c.sig = &sig; - c.unsafe_context = fn.is_unsafe; - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - for (std::size_t i = 0; i < fn.params.size(); ++i) { - c.scopes.back()[fn.params[i].name] = sig.params[i]; - if (fn.params[i].mut) - c.mutable_scopes.back().insert(fn.params[i].name); - } - check_block(*fn.body, c); -} -void TypeChecker::check_block(const ast::BlockStmt& b, FnContext& c) { - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - for (const auto& s : b.stmts) - check_stmt(*s, c); - c.mutable_scopes.pop_back(); - c.scopes.pop_back(); -} -void TypeChecker::check_stmt(const ast::Stmt& s, FnContext& c) { - if (auto l = dynamic_cast(&s)) { - Type init = l->init ? check_expr(*l->init, c) : Type{}; - Type ty = l->type - ? type_from_ast(*l->type, - std::unordered_set(c.sig->generic_names.begin(), - c.sig->generic_names.end())) - : init; - if (l->type && !can_coerce_expr(l->init.get(), init, ty)) - diags_.error(l->range, - "E0301", - "initializer has type '" + init.str() + "' but binding expects '" + - ty.str() + "'"); - model_.binding_types[l] = ty; - c.scopes.back()[l->name] = ty; - if (l->mut) - c.mutable_scopes.back().insert(l->name); - return; - } - if (auto a = dynamic_cast(&s)) { - if (auto binding = model_.hir.assignment_bindings.find(a); - binding != model_.hir.assignment_bindings.end()) { - Type rhs = check_expr(*a->value, c); - model_.assignment_binding_types[a] = rhs; - model_.expr_types[a->target.get()] = rhs; - const auto& name = static_cast(*a->target).name; - c.scopes.back()[name] = rhs; - c.mutable_scopes.back().insert(name); - return; - } - Type lhs = check_expr(*a->target, c, false), rhs = check_expr(*a->value, c); - if (!is_mutable_place(*a->target, c)) - diags_.error(a->target->range, "E0333", "assignment requires a mutable place"); - if (!can_coerce_expr(a->value.get(), rhs, lhs)) - diags_.error( - a->range, "E0334", "cannot assign '" + rhs.str() + "' to '" + lhs.str() + "'"); - if (a->op != "=") { - const bool bitwise = a->op == "&=" || a->op == "|=" || a->op == "^=" || - a->op == "<<=" || a->op == ">>=" || a->op == "%="; - if (bitwise ? (!lhs.is_integer() || !rhs.is_integer()) - : (!lhs.is_numeric() || !rhs.is_numeric())) - diags_.error(a->range, - "E0335", - "compound assignment operator '" + a->op + - "' is not valid for these operands"); - } - return; - } - if (auto e = dynamic_cast(&s)) { - check_expr(*e->expr, c); - return; - } - if (auto r = dynamic_cast(&s)) { - Type got = r->value ? check_expr(*r->value, c) : Type::builtin("unit"); - if (!can_coerce_expr(r->value.get(), got, c.sig->result)) - diags_.error(r->range, - "E0302", - "return type '" + got.str() + "' does not match '" + c.sig->result.str() + - "'"); - return; - } - if (auto b = dynamic_cast(&s)) { - check_block(*b, c); - return; - } - if (auto i = dynamic_cast(&s)) { - Type t = check_expr(*i->condition, c); - if (t.kind != TypeKind::Bool) - diags_.error( - i->condition->range, "E0303", "if condition must be bool, found '" + t.str() + "'"); - check_block(*i->then_block, c); - if (i->else_block) - check_block(*i->else_block, c); - return; - } - if (auto w = dynamic_cast(&s)) { - Type t = check_expr(*w->condition, c); - if (t.kind != TypeKind::Bool) - diags_.error(w->condition->range, "E0304", "while condition must be bool"); - ++c.loop_depth; - check_block(*w->body, c); - --c.loop_depth; - return; - } - if (auto f = dynamic_cast(&s)) { - Type begin = check_expr(*f->begin, c), end = check_expr(*f->end, c); - if (!begin.is_integer() || !end.is_integer() || begin != end) - diags_.error(f->range, "E0356", "for ranges require matching integer endpoint types"); - c.scopes.emplace_back(); - c.mutable_scopes.emplace_back(); - c.scopes.back()[f->name] = begin; - ++c.loop_depth; - check_block(*f->body, c); - --c.loop_depth; - c.mutable_scopes.pop_back(); - c.scopes.pop_back(); - return; - } - if (auto l = dynamic_cast(&s)) { - ++c.loop_depth; - check_block(*l->body, c); - --c.loop_depth; - return; - } - if (dynamic_cast(&s) || dynamic_cast(&s)) { - if (c.loop_depth == 0) - diags_.error(s.range, "E0357", "break/continue is only valid inside a loop"); - return; - } - if (auto u = dynamic_cast(&s)) { - bool old = c.unsafe_context; - c.unsafe_context = true; - check_block(*u->body, c); - c.unsafe_context = old; - return; - } -} -Type TypeChecker::lookup_local(std::string_view n, const FnContext& c, const SourceRange& r) { - for (auto it = c.scopes.rbegin(); it != c.scopes.rend(); ++it) { - auto f = it->find(std::string(n)); - if (f != it->end()) - return f->second; - } - auto g = model_.globals.find(std::string(n)); - if (g != model_.globals.end()) - return g->second.type; - diags_.error(r, "E0305", "name '" + std::string(n) + "' is not a value in this scope"); - return {}; -} -bool TypeChecker::is_mutable_binding(std::string_view n, const FnContext& c) const { - for (auto it = c.mutable_scopes.rbegin(); it != c.mutable_scopes.rend(); ++it) - if (it->contains(std::string(n))) - return true; - auto g = model_.globals.find(std::string(n)); - return g != model_.globals.end() && g->second.is_mut && !g->second.is_const; -} -bool TypeChecker::is_mutable_place(const ast::Expr& e, const FnContext& c) { - if (e.kind == ast::ExprKind::Name) - return is_mutable_binding(static_cast(e).name, c); - if (e.kind == ast::ExprKind::Member) { - const auto& m = static_cast(e); - Type base{}; - if (auto it = model_.expr_types.find(m.base.get()); it != model_.expr_types.end()) - base = it->second; - if (base.kind == TypeKind::Ref) - return base.mut; - return is_mutable_place(*m.base, c); - } - if (e.kind == ast::ExprKind::Index) { - const auto& i = static_cast(e); - Type base{}; - if (auto it = model_.expr_types.find(i.base.get()); it != model_.expr_types.end()) - base = it->second; - if (base.kind == TypeKind::Ref) - return base.mut; - if (base.kind == TypeKind::Named && base.name == "SliceMut") - return is_mutable_place(*i.base, c); - return false; - } - if (e.kind == ast::ExprKind::Unary) { - const auto& u = static_cast(e); - if (u.op == "*") { - Type p{}; - if (auto it = model_.expr_types.find(u.operand.get()); it != model_.expr_types.end()) - p = it->second; - return p.kind == TypeKind::Ref ? p.mut : (p.kind == TypeKind::RawPtr ? p.mut : false); - } - } - return false; -} -bool TypeChecker::unify_generic(const Type& f, - const Type& a, - std::unordered_map& s) { - if (f.kind == TypeKind::Generic) { - auto it = s.find(f.name); - if (it == s.end()) { - s[f.name] = a; - return true; - } - return it->second == a; - } - if (f.kind != a.kind) - return false; - if (f.pointee && a.pointee) - return f.mut == a.mut && unify_generic(*f.pointee, *a.pointee, s); - if (f.args.size() != a.args.size()) - return false; - for (std::size_t i = 0; i < f.args.size(); ++i) - if (!unify_generic(f.args[i], a.args[i], s)) - return false; - return f.name == a.name || f.name.empty() || a.name.empty(); -} -Type TypeChecker::check_call(const ast::CallExpr& call, FnContext& c) { - const FunctionSig* sig = nullptr; - std::string display_name; - std::vector> actuals; - - if (call.callee->kind == ast::ExprKind::Name) { - const auto& n = static_cast(*call.callee); - if (builtin_function(n.name)) - return check_builtin_call(call, c); - auto it = model_.functions.find(n.name); - if (it == model_.functions.end()) { - diags_.error(call.range, "E0307", "unknown function '" + n.name + "'"); - return {}; - } - sig = &it->second; - display_name = n.name; - for (auto& arg : call.args) - actuals.push_back({arg.get(), check_expr(*arg, c)}); - } else if (call.callee->kind == ast::ExprKind::Member) { - const auto& member = static_cast(*call.callee); - Type receiver = check_expr(*member.base, c, false); - Type owner = receiver; - if (owner.kind == TypeKind::Ref && owner.pointee) - owner = *owner.pointee; - if (owner.kind != TypeKind::Named) { - diags_.error(call.range, "E0306", "method receiver must be a named type"); - return {}; - } - const std::string key = owner.name + "::" + member.member; - auto it = model_.functions.find(key); - if (it == model_.functions.end()) { - diags_.error(call.range, - "E0307", - "type '" + owner.str() + "' has no method '" + member.member + "'"); - return {}; - } - sig = &it->second; - display_name = owner.name + "." + member.member; - if (sig->params.empty()) { - diags_.error(call.range, "E0345", "method '" + display_name + "' has no self receiver"); - return sig->result; - } - Type receiver_actual = owner; - const Type& expected_receiver = sig->params.front(); - if (expected_receiver.kind == TypeKind::Ref) { - receiver_actual = Type::ref(owner, expected_receiver.mut); - if (expected_receiver.mut && !is_mutable_place(*member.base, c) && - !(receiver.kind == TypeKind::Ref && receiver.mut)) - diags_.error(member.base->range, - "E0346", - "method '" + display_name + "' requires a mutable receiver"); - } - actuals.push_back({member.base.get(), receiver_actual}); - for (auto& arg : call.args) - actuals.push_back({arg.get(), check_expr(*arg, c)}); - } else { - diags_.error(call.range, "E0306", "call target must be a function or method"); - return {}; - } - - if (sig->is_unsafe && !c.unsafe_context) - diags_.error(call.range, - "E0308", - "call to unsafe function/method '" + display_name + - "' requires unsafe context"); - if (actuals.size() != sig->params.size()) { - const std::size_t explicit_expected = - sig->params.size() - (call.callee->kind == ast::ExprKind::Member ? 1u : 0u); - diags_.error(call.range, - "E0309", - "function/method '" + display_name + "' expects " + - std::to_string(explicit_expected) + " arguments, got " + - std::to_string(call.args.size())); - return sig->result; - } - std::unordered_map subst; - if (!call.generic_args.empty()) { - if (call.generic_args.size() != sig->generic_names.size()) - diags_.error(call.range, "E0310", "wrong number of explicit generic arguments"); - else - for (std::size_t i = 0; i < call.generic_args.size(); ++i) - subst[sig->generic_names[i]] = type_from_ast(call.generic_args[i]); - } - for (std::size_t i = 0; i < actuals.size(); ++i) { - const Type actual = actuals[i].second; - const Type expected = substitute_type(sig->params[i], subst); - if (!unify_generic(sig->params[i], actual, subst) && - !can_coerce_expr(actuals[i].first, actual, expected)) - diags_.error(actuals[i].first->range, - "E0311", - "argument " + std::to_string(i + 1) + " has type '" + actual.str() + - "', expected '" + expected.str() + "'"); - } - TraitSolver solver(model_); - for (const auto& [g, bounds] : sig->bounds) { - auto si = subst.find(g); - if (si == subst.end()) { - diags_.error(call.range, "E0312", "cannot infer generic parameter '" + g + "'"); - continue; - } - for (const auto& bound : bounds) - if (!solver.satisfies(si->second, bound)) - diags_.error(call.range, - "E0313", - "type '" + si->second.str() + "' does not satisfy trait '" + bound + - "'"); - } - Type result = substitute_type(sig->result, subst); - return sig->is_async ? Type::named("Future", {result}) : result; -} - -Type TypeChecker::check_builtin_call(const ast::CallExpr& call, FnContext& c) { - const auto& name = static_cast(*call.callee).name; - const auto string_like = [](const Type& type) { - return type.kind == TypeKind::Str || - (type.kind == TypeKind::Ref && type.pointee && type.pointee->kind == TypeKind::Str); - }; - const auto printable = [&](const Type& type) { - return type.is_integer() || type.is_float() || type.kind == TypeKind::Bool || - type.kind == TypeKind::Char || string_like(type); - }; - std::vector arguments; - for (const auto& argument : call.args) - arguments.push_back(check_expr(*argument, c)); - - if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { - for (std::size_t i = 0; i < arguments.size(); ++i) - if (!printable(arguments[i])) - diags_.error(call.args[i]->range, - "E0367", - "print argument has unsupported type '" + arguments[i].str() + "'"); - return Type::builtin("unit"); - } - if (name == "input") { - if (arguments.size() > 1) - diags_.error(call.range, "E0368", "input expects zero or one prompt argument"); - if (!arguments.empty() && !string_like(arguments[0])) - diags_.error(call.args[0]->range, "E0369", "input prompt must be a string"); - return Type::ref(Type::builtin("str"), false); - } - if (name == "str") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "str expects exactly one argument"); - else if (!printable(arguments[0])) - diags_.error(call.args[0]->range, - "E0371", - "str argument has unsupported type '" + arguments[0].str() + "'"); - return Type::ref(Type::builtin("str"), false); - } - if (name == "int" || name == "float") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", name + " expects exactly one argument"); - else if (!arguments[0].is_numeric() && !string_like(arguments[0])) - diags_.error( - call.args[0]->range, "E0372", name + " argument must be numeric or string-like"); - return Type::builtin(name == "int" ? "i64" : "f64"); - } - if (name == "bool") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "bool expects exactly one argument"); - else if (arguments[0].kind != TypeKind::Bool && !arguments[0].is_numeric() && - !string_like(arguments[0])) - diags_.error( - call.args[0]->range, "E0373", "bool argument must be scalar or string-like"); - return Type::builtin("bool"); - } - if (name == "len") { - if (arguments.size() != 1) - diags_.error(call.range, "E0370", "len expects exactly one argument"); - else { - const Type& type = arguments[0]; - if (!string_like(type)) - diags_.error(call.args[0]->range, - "E0374", - "len is not implemented for type '" + type.str() + "'"); - } - return Type::builtin("usize"); - } - return {}; -} -void TypeChecker::validate_asm(const ast::AsmExpr& a, FnContext& c) { - if (!c.unsafe_context) - diags_.error(a.range, "E0314", "asm() is only permitted inside unsafe code"); - if (a.assembly.empty()) - diags_.warning(a.range, "W0315", "empty asm template"); - for (const auto& o : a.operands) { - if (o.constraint.empty()) - diags_.error(o.range, "E0316", "asm register/clobber constraint cannot be empty"); - for (char ch : o.constraint) - if (!(std::isalnum(static_cast(ch)) || - std::string("=+&{},~*").find(ch) != std::string::npos)) - diags_.error( - o.range, "E0317", "invalid character in asm constraint '" + o.constraint + "'"); - if (o.value) - check_expr(*o.value, c); - } -} -Type TypeChecker::check_expr(const ast::Expr& e, FnContext& c, bool) { - Type t; - switch (e.kind) { - case ast::ExprKind::Integer: - t = Type::builtin("i32"); - break; - case ast::ExprKind::Float: - t = Type::builtin("f64"); - break; - case ast::ExprKind::String: - t = Type::ref(Type::builtin("str"), false); - break; - case ast::ExprKind::Char: - t = Type::builtin("char"); - break; - case ast::ExprKind::Bool: - t = Type::builtin("bool"); - break; - case ast::ExprKind::Name: { - auto& n = static_cast(e); - auto fi = model_.functions.find(n.name); - t = fi != model_.functions.end() ? Type::named("fn") : lookup_local(n.name, c, e.range); - break; - } - case ast::ExprKind::StructLiteral: { - auto& st = static_cast(e); - auto si = model_.structs.find(st.name); - if (si == model_.structs.end()) { - diags_.error(e.range, "E0337", "unknown struct '" + st.name + "'"); - t = {}; - break; - } - std::vector type_args; - for (const auto& arg : st.generic_args) - type_args.push_back(type_from_ast(arg)); - if (type_args.size() != si->second.generic_names.size()) { - diags_.error(e.range, - "E0338", - "struct '" + st.name + "' expects " + - std::to_string(si->second.generic_names.size()) + - " type arguments, got " + std::to_string(type_args.size())); - t = {}; - break; - } - std::unordered_map subst; - for (std::size_t i = 0; i < type_args.size(); ++i) - subst[si->second.generic_names[i]] = type_args[i]; - TraitSolver solver(model_); - for (const auto& [generic, bounds] : si->second.bounds) { - const auto actual = subst.find(generic); - if (actual == subst.end()) - continue; - for (const auto& bound : bounds) - if (!solver.satisfies(actual->second, bound)) - diags_.error(e.range, - "E0362", - "type '" + actual->second.str() + - "' does not satisfy struct bound '" + generic + ": " + - bound + "'"); - } - std::unordered_set seen; - for (const auto& f : st.fields) { - if (!seen.insert(f.name).second) - diags_.error( - f.range, "E0339", "duplicate field '" + f.name + "' in struct literal"); - auto fi = si->second.fields.find(f.name); - Type actual = check_expr(*f.value, c); - if (fi == si->second.fields.end()) - diags_.error( - f.range, "E0340", "struct '" + st.name + "' has no field '" + f.name + "'"); - else { - Type expected = substitute_type(fi->second, subst); - if (!can_coerce_expr(f.value.get(), actual, expected)) - diags_.error(f.range, - "E0341", - "field '" + f.name + "' expects '" + expected.str() + - "', found '" + actual.str() + "'"); - } - } - for (const auto& [name, ft] : si->second.fields) { - (void)ft; - if (!seen.contains(name)) - diags_.error(e.range, - "E0342", - "missing field '" + name + "' in struct literal '" + st.name + - "'"); - } - t = Type::named(st.name, std::move(type_args)); - break; - } - case ast::ExprKind::Unary: { - auto& u = static_cast(e); - Type x = check_expr(*u.operand, c); - if (u.op == "!") { - if (x.kind != TypeKind::Bool && !x.is_integer()) - diags_.error(e.range, "E0318", "not/! requires bool or integer"); - t = x; - } else if (u.op == "~") { - if (!x.is_integer()) - diags_.error(e.range, "E0318", "~ requires an integer operand"); - t = x; - } else if (u.op == "-") { - if (!x.is_numeric()) - diags_.error(e.range, "E0319", "unary - requires numeric operand"); - t = x; - } else if (u.op == "move") { - t = x; - } else if (u.op == "*") { - if (x.kind != TypeKind::Ref && x.kind != TypeKind::RawPtr) { - diags_.error(e.range, "E0320", "dereference requires reference or raw pointer"); - t = {}; - } else { - if (x.kind == TypeKind::RawPtr && !c.unsafe_context) - diags_.error(e.range, - "E0321", - "dereferencing a raw pointer requires unsafe context"); - t = x.pointee ? *x.pointee : Type{}; - } - } - break; - } - case ast::ExprKind::Binary: { - auto& b = static_cast(e); - Type l = check_expr(*b.lhs, c), r = check_expr(*b.rhs, c); - if (l.is_integer() && r.is_integer() && l != r) { - if (integer_literal(b.lhs.get()) && can_coerce_expr(b.lhs.get(), l, r)) { - l = r; - model_.expr_types[b.lhs.get()] = r; - // `-literal` is a Unary node wrapping the Integer token; the - // MIR unary lowering reads the inner operand type, so keep it - // in sync or `sub i32` feeds an `i64` compare. - if (b.lhs->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*b.lhs); - if (unary.operand) - model_.expr_types[unary.operand.get()] = r; - } - } else if (integer_literal(b.rhs.get()) && can_coerce_expr(b.rhs.get(), r, l)) { - r = l; - model_.expr_types[b.rhs.get()] = l; - if (b.rhs->kind == ast::ExprKind::Unary) { - const auto& unary = static_cast(*b.rhs); - if (unary.operand) - model_.expr_types[unary.operand.get()] = l; - } - } - } - if ((l.kind == TypeKind::RawPtr || r.kind == TypeKind::RawPtr) && - (b.op == "+" || b.op == "-") && !c.unsafe_context) - diags_.error(e.range, "E0322", "raw pointer arithmetic requires unsafe context"); - if (b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || - b.op == ">=") { - if (!can_coerce(l, r) && !can_coerce(r, l)) - diags_.error(e.range, "E0323", "comparison operands have incompatible types"); - t = Type::builtin("bool"); - } else if (b.op == "&&" || b.op == "||") { - if (l.kind != TypeKind::Bool || r.kind != TypeKind::Bool) - diags_.error(e.range, "E0324", "logical operators require bool operands"); - t = Type::builtin("bool"); - } else if (b.op == "&" || b.op == "|" || b.op == "^" || b.op == "<<" || b.op == ">>") { - if (l != r || !l.is_integer()) - diags_.error(e.range, - "E0325", - "bitwise/shift operators require matching integer operands"); - t = l; - } else if (l.kind == TypeKind::RawPtr && (b.op == "+" || b.op == "-") && r.is_integer()) - t = l; - else if (l == r && l.is_numeric()) - t = l; - else { - diags_.error(e.range, - "E0325", - "binary operator '" + b.op + "' cannot combine '" + l.str() + - "' and '" + r.str() + "'"); - t = {}; - } - break; - } - case ast::ExprKind::Borrow: { - auto& b = static_cast(e); - Type target = check_expr(*b.target, c, false); - if (b.mut && !is_mutable_place(*b.target, c)) - diags_.error(e.range, "E0336", "mutable borrow requires a mutable place"); - t = Type::ref(target, b.mut); - break; - } - case ast::ExprKind::Call: - t = check_call(static_cast(e), c); - break; - case ast::ExprKind::Member: { - auto& m = static_cast(e); - Type base = check_expr(*m.base, c, false); - if (base.kind == TypeKind::Ref && base.pointee) - base = *base.pointee; - if (base.kind != TypeKind::Named || !model_.structs.contains(base.name)) { - diags_.error(e.range, "E0326", "member access requires struct type"); - t = {}; - } else { - auto& si = model_.structs.at(base.name); - auto f = si.fields.find(m.member); - if (f == si.fields.end()) { - diags_.error(e.range, - "E0327", - "struct '" + base.name + "' has no field '" + m.member + "'"); - t = {}; - } else { - std::unordered_map subst; - for (std::size_t gi = 0; gi < si.generic_names.size() && gi < base.args.size(); - ++gi) - subst[si.generic_names[gi]] = base.args[gi]; - t = substitute_type(f->second, subst); - } - } - break; - } - case ast::ExprKind::Index: { - auto& i = static_cast(e); - Type b = check_expr(*i.base, c, false), idx = check_expr(*i.index, c); - if (!idx.is_integer()) - diags_.error(i.index->range, "E0328", "index must be integer"); - Type container = b; - if (container.kind == TypeKind::Ref && container.pointee) - container = *container.pointee; - if (container.kind == TypeKind::Named && - (container.name == "Slice" || container.name == "SliceMut") && - !container.args.empty()) - t = container.args[0]; - else if (container.kind == TypeKind::Str) - t = Type::builtin("u8"); - else { - diags_.error(e.range, "E0329", "type '" + b.str() + "' is not safely indexable"); - t = {}; - } - break; - } - case ast::ExprKind::Await: { - if (!c.sig->is_async) - diags_.error(e.range, "E0343", "await is only valid inside an async function"); - Type f = check_expr(*static_cast(e).value, c); - if (f.kind != TypeKind::Named || f.name != "Future" || f.args.size() != 1) { - diags_.error(e.range, "E0330", "await requires Future"); - t = {}; - } else - t = f.args[0]; - break; - } - case ast::ExprKind::Cast: { - auto& x = static_cast(e); - Type from = check_expr(*x.value, c), to = type_from_ast(x.to); - bool ref_to_raw = from.kind == TypeKind::Ref && to.kind == TypeKind::RawPtr && - from.pointee && to.pointee; - if (ref_to_raw) { - if (!can_coerce(*from.pointee, *to.pointee)) - diags_.error( - e.range, "E0332", "reference/raw-pointer pointee types are incompatible"); - if (to.mut && !from.mut) - diags_.error(e.range, - "E0332", - "cannot create mutable raw pointer from shared reference"); - } else if ((from.kind == TypeKind::RawPtr || to.kind == TypeKind::RawPtr) && - !(from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr) && - !c.unsafe_context) - diags_.error(e.range, "E0331", "this raw pointer cast requires unsafe context"); - const bool str_to_raw = from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr && - to.pointee && to.pointee->kind == TypeKind::U8; - if (!(from.is_numeric() && to.is_numeric()) && !ref_to_raw && !str_to_raw && - from.kind != TypeKind::RawPtr && to.kind != TypeKind::RawPtr) - diags_.error(e.range, - "E0332", - "unsupported cast from '" + from.str() + "' to '" + to.str() + "'"); - t = to; - break; - } - case ast::ExprKind::Asm: { - auto& a = static_cast(e); - validate_asm(a, c); - t = Type::builtin("unit"); - break; - } - } - model_.expr_types[&e] = t; - return t; -} } // namespace uinx diff --git a/src/sema_expr.cpp b/src/sema_expr.cpp new file mode 100644 index 0000000..47c06bd --- /dev/null +++ b/src/sema_expr.cpp @@ -0,0 +1,494 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +Type TypeChecker::check_call(const ast::CallExpr& call, FnContext& c) { + const FunctionSig* sig = nullptr; + std::string display_name; + std::vector> actuals; + + if (call.callee->kind == ast::ExprKind::Name) { + const auto& n = static_cast(*call.callee); + if (builtin_function(n.name)) + return check_builtin_call(call, c); + auto it = model_.functions.find(n.name); + if (it == model_.functions.end()) { + diags_.error(call.range, "E0307", "unknown function '" + n.name + "'"); + return {}; + } + sig = &it->second; + display_name = n.name; + for (auto& arg : call.args) + actuals.push_back({arg.get(), check_expr(*arg, c)}); + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + Type receiver = check_expr(*member.base, c, false); + Type owner = receiver; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + if (owner.kind != TypeKind::Named) { + diags_.error(call.range, "E0306", "method receiver must be a named type"); + return {}; + } + const std::string key = owner.name + "::" + member.member; + auto it = model_.functions.find(key); + if (it == model_.functions.end()) { + diags_.error(call.range, + "E0307", + "type '" + owner.str() + "' has no method '" + member.member + "'"); + return {}; + } + sig = &it->second; + display_name = owner.name + "." + member.member; + if (sig->params.empty()) { + diags_.error(call.range, "E0345", "method '" + display_name + "' has no self receiver"); + return sig->result; + } + Type receiver_actual = owner; + const Type& expected_receiver = sig->params.front(); + if (expected_receiver.kind == TypeKind::Ref) { + receiver_actual = Type::ref(owner, expected_receiver.mut); + if (expected_receiver.mut && !is_mutable_place(*member.base, c) && + !(receiver.kind == TypeKind::Ref && receiver.mut)) + diags_.error(member.base->range, + "E0346", + "method '" + display_name + "' requires a mutable receiver"); + } + actuals.push_back({member.base.get(), receiver_actual}); + for (auto& arg : call.args) + actuals.push_back({arg.get(), check_expr(*arg, c)}); + } else { + diags_.error(call.range, "E0306", "call target must be a function or method"); + return {}; + } + + if (sig->is_unsafe && !c.unsafe_context) + diags_.error(call.range, + "E0308", + "call to unsafe function/method '" + display_name + + "' requires unsafe context"); + if (actuals.size() != sig->params.size()) { + const std::size_t explicit_expected = + sig->params.size() - (call.callee->kind == ast::ExprKind::Member ? 1u : 0u); + diags_.error(call.range, + "E0309", + "function/method '" + display_name + "' expects " + + std::to_string(explicit_expected) + " arguments, got " + + std::to_string(call.args.size())); + return sig->result; + } + std::unordered_map subst; + if (!call.generic_args.empty()) { + if (call.generic_args.size() != sig->generic_names.size()) + diags_.error(call.range, "E0310", "wrong number of explicit generic arguments"); + else + for (std::size_t i = 0; i < call.generic_args.size(); ++i) + subst[sig->generic_names[i]] = type_from_ast(call.generic_args[i]); + } + for (std::size_t i = 0; i < actuals.size(); ++i) { + const Type actual = actuals[i].second; + const Type expected = substitute_type(sig->params[i], subst); + if (!unify_generic(sig->params[i], actual, subst) && + !can_coerce_expr(actuals[i].first, actual, expected)) + diags_.error(actuals[i].first->range, + "E0311", + "argument " + std::to_string(i + 1) + " has type '" + actual.str() + + "', expected '" + expected.str() + "'"); + } + TraitSolver solver(model_); + for (const auto& [g, bounds] : sig->bounds) { + auto si = subst.find(g); + if (si == subst.end()) { + diags_.error(call.range, "E0312", "cannot infer generic parameter '" + g + "'"); + continue; + } + for (const auto& bound : bounds) + if (!solver.satisfies(si->second, bound)) + diags_.error(call.range, + "E0313", + "type '" + si->second.str() + "' does not satisfy trait '" + bound + + "'"); + } + Type result = substitute_type(sig->result, subst); + return sig->is_async ? Type::named("Future", {result}) : result; +} + +Type TypeChecker::check_builtin_call(const ast::CallExpr& call, FnContext& c) { + const auto& name = static_cast(*call.callee).name; + const auto string_like = [](const Type& type) { + return type.kind == TypeKind::Str || + (type.kind == TypeKind::Ref && type.pointee && type.pointee->kind == TypeKind::Str); + }; + const auto printable = [&](const Type& type) { + return type.is_integer() || type.is_float() || type.kind == TypeKind::Bool || + type.kind == TypeKind::Char || string_like(type); + }; + std::vector arguments; + for (const auto& argument : call.args) + arguments.push_back(check_expr(*argument, c)); + + if (name == "print" || name == "println" || name == "eprint" || name == "eprintln") { + for (std::size_t i = 0; i < arguments.size(); ++i) + if (!printable(arguments[i])) + diags_.error(call.args[i]->range, + "E0367", + "print argument has unsupported type '" + arguments[i].str() + "'"); + return Type::builtin("unit"); + } + if (name == "input") { + if (arguments.size() > 1) + diags_.error(call.range, "E0368", "input expects zero or one prompt argument"); + if (!arguments.empty() && !string_like(arguments[0])) + diags_.error(call.args[0]->range, "E0369", "input prompt must be a string"); + return Type::ref(Type::builtin("str"), false); + } + if (name == "str") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "str expects exactly one argument"); + else if (!printable(arguments[0])) + diags_.error(call.args[0]->range, + "E0371", + "str argument has unsupported type '" + arguments[0].str() + "'"); + return Type::ref(Type::builtin("str"), false); + } + if (name == "int" || name == "float") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", name + " expects exactly one argument"); + else if (!arguments[0].is_numeric() && !string_like(arguments[0])) + diags_.error( + call.args[0]->range, "E0372", name + " argument must be numeric or string-like"); + return Type::builtin(name == "int" ? "i64" : "f64"); + } + if (name == "bool") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "bool expects exactly one argument"); + else if (arguments[0].kind != TypeKind::Bool && !arguments[0].is_numeric() && + !string_like(arguments[0])) + diags_.error( + call.args[0]->range, "E0373", "bool argument must be scalar or string-like"); + return Type::builtin("bool"); + } + if (name == "len") { + if (arguments.size() != 1) + diags_.error(call.range, "E0370", "len expects exactly one argument"); + else { + const Type& type = arguments[0]; + if (!string_like(type)) + diags_.error(call.args[0]->range, + "E0374", + "len is not implemented for type '" + type.str() + "'"); + } + return Type::builtin("usize"); + } + return {}; +} +void TypeChecker::validate_asm(const ast::AsmExpr& a, FnContext& c) { + if (!c.unsafe_context) + diags_.error(a.range, "E0314", "asm() is only permitted inside unsafe code"); + if (a.assembly.empty()) + diags_.warning(a.range, "W0315", "empty asm template"); + for (const auto& o : a.operands) { + if (o.constraint.empty()) + diags_.error(o.range, "E0316", "asm register/clobber constraint cannot be empty"); + for (char ch : o.constraint) + if (!(std::isalnum(static_cast(ch)) || + std::string("=+&{},~*").find(ch) != std::string::npos)) + diags_.error( + o.range, "E0317", "invalid character in asm constraint '" + o.constraint + "'"); + if (o.value) + check_expr(*o.value, c); + } +} +Type TypeChecker::check_expr(const ast::Expr& e, FnContext& c, bool) { + Type t; + switch (e.kind) { + case ast::ExprKind::Integer: + t = Type::builtin("i32"); + break; + case ast::ExprKind::Float: + t = Type::builtin("f64"); + break; + case ast::ExprKind::String: + t = Type::ref(Type::builtin("str"), false); + break; + case ast::ExprKind::Char: + t = Type::builtin("char"); + break; + case ast::ExprKind::Bool: + t = Type::builtin("bool"); + break; + case ast::ExprKind::Name: { + auto& n = static_cast(e); + auto fi = model_.functions.find(n.name); + t = fi != model_.functions.end() ? Type::named("fn") : lookup_local(n.name, c, e.range); + break; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto si = model_.structs.find(st.name); + if (si == model_.structs.end()) { + diags_.error(e.range, "E0337", "unknown struct '" + st.name + "'"); + t = {}; + break; + } + std::vector type_args; + for (const auto& arg : st.generic_args) + type_args.push_back(type_from_ast(arg)); + if (type_args.size() != si->second.generic_names.size()) { + diags_.error(e.range, + "E0338", + "struct '" + st.name + "' expects " + + std::to_string(si->second.generic_names.size()) + + " type arguments, got " + std::to_string(type_args.size())); + t = {}; + break; + } + std::unordered_map subst; + for (std::size_t i = 0; i < type_args.size(); ++i) + subst[si->second.generic_names[i]] = type_args[i]; + TraitSolver solver(model_); + for (const auto& [generic, bounds] : si->second.bounds) { + const auto actual = subst.find(generic); + if (actual == subst.end()) + continue; + for (const auto& bound : bounds) + if (!solver.satisfies(actual->second, bound)) + diags_.error(e.range, + "E0362", + "type '" + actual->second.str() + + "' does not satisfy struct bound '" + generic + ": " + + bound + "'"); + } + std::unordered_set seen; + for (const auto& f : st.fields) { + if (!seen.insert(f.name).second) + diags_.error( + f.range, "E0339", "duplicate field '" + f.name + "' in struct literal"); + auto fi = si->second.fields.find(f.name); + Type actual = check_expr(*f.value, c); + if (fi == si->second.fields.end()) + diags_.error( + f.range, "E0340", "struct '" + st.name + "' has no field '" + f.name + "'"); + else { + Type expected = substitute_type(fi->second, subst); + if (!can_coerce_expr(f.value.get(), actual, expected)) + diags_.error(f.range, + "E0341", + "field '" + f.name + "' expects '" + expected.str() + + "', found '" + actual.str() + "'"); + } + } + for (const auto& [name, ft] : si->second.fields) { + (void)ft; + if (!seen.contains(name)) + diags_.error(e.range, + "E0342", + "missing field '" + name + "' in struct literal '" + st.name + + "'"); + } + t = Type::named(st.name, std::move(type_args)); + break; + } + case ast::ExprKind::Unary: { + auto& u = static_cast(e); + Type x = check_expr(*u.operand, c); + if (u.op == "!") { + if (x.kind != TypeKind::Bool && !x.is_integer()) + diags_.error(e.range, "E0318", "not/! requires bool or integer"); + t = x; + } else if (u.op == "~") { + if (!x.is_integer()) + diags_.error(e.range, "E0318", "~ requires an integer operand"); + t = x; + } else if (u.op == "-") { + if (!x.is_numeric()) + diags_.error(e.range, "E0319", "unary - requires numeric operand"); + t = x; + } else if (u.op == "move") { + t = x; + } else if (u.op == "*") { + if (x.kind != TypeKind::Ref && x.kind != TypeKind::RawPtr) { + diags_.error(e.range, "E0320", "dereference requires reference or raw pointer"); + t = {}; + } else { + if (x.kind == TypeKind::RawPtr && !c.unsafe_context) + diags_.error(e.range, + "E0321", + "dereferencing a raw pointer requires unsafe context"); + t = x.pointee ? *x.pointee : Type{}; + } + } + break; + } + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + Type l = check_expr(*b.lhs, c), r = check_expr(*b.rhs, c); + if (l.is_integer() && r.is_integer() && l != r) { + if (integer_literal(b.lhs.get()) && can_coerce_expr(b.lhs.get(), l, r)) { + l = r; + model_.expr_types[b.lhs.get()] = r; + // `-literal` is a Unary node wrapping the Integer token; the + // MIR unary lowering reads the inner operand type, so keep it + // in sync or `sub i32` feeds an `i64` compare. + if (b.lhs->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*b.lhs); + if (unary.operand) + model_.expr_types[unary.operand.get()] = r; + } + } else if (integer_literal(b.rhs.get()) && can_coerce_expr(b.rhs.get(), r, l)) { + r = l; + model_.expr_types[b.rhs.get()] = l; + if (b.rhs->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*b.rhs); + if (unary.operand) + model_.expr_types[unary.operand.get()] = l; + } + } + } + if ((l.kind == TypeKind::RawPtr || r.kind == TypeKind::RawPtr) && + (b.op == "+" || b.op == "-") && !c.unsafe_context) + diags_.error(e.range, "E0322", "raw pointer arithmetic requires unsafe context"); + if (b.op == "==" || b.op == "!=" || b.op == "<" || b.op == "<=" || b.op == ">" || + b.op == ">=") { + if (!can_coerce(l, r) && !can_coerce(r, l)) + diags_.error(e.range, "E0323", "comparison operands have incompatible types"); + t = Type::builtin("bool"); + } else if (b.op == "&&" || b.op == "||") { + if (l.kind != TypeKind::Bool || r.kind != TypeKind::Bool) + diags_.error(e.range, "E0324", "logical operators require bool operands"); + t = Type::builtin("bool"); + } else if (b.op == "&" || b.op == "|" || b.op == "^" || b.op == "<<" || b.op == ">>") { + if (l != r || !l.is_integer()) + diags_.error(e.range, + "E0325", + "bitwise/shift operators require matching integer operands"); + t = l; + } else if (l.kind == TypeKind::RawPtr && (b.op == "+" || b.op == "-") && r.is_integer()) + t = l; + else if (l == r && l.is_numeric()) + t = l; + else { + diags_.error(e.range, + "E0325", + "binary operator '" + b.op + "' cannot combine '" + l.str() + + "' and '" + r.str() + "'"); + t = {}; + } + break; + } + case ast::ExprKind::Borrow: { + auto& b = static_cast(e); + Type target = check_expr(*b.target, c, false); + if (b.mut && !is_mutable_place(*b.target, c)) + diags_.error(e.range, "E0336", "mutable borrow requires a mutable place"); + t = Type::ref(target, b.mut); + break; + } + case ast::ExprKind::Call: + t = check_call(static_cast(e), c); + break; + case ast::ExprKind::Member: { + auto& m = static_cast(e); + Type base = check_expr(*m.base, c, false); + if (base.kind == TypeKind::Ref && base.pointee) + base = *base.pointee; + if (base.kind != TypeKind::Named || !model_.structs.contains(base.name)) { + diags_.error(e.range, "E0326", "member access requires struct type"); + t = {}; + } else { + auto& si = model_.structs.at(base.name); + auto f = si.fields.find(m.member); + if (f == si.fields.end()) { + diags_.error(e.range, + "E0327", + "struct '" + base.name + "' has no field '" + m.member + "'"); + t = {}; + } else { + std::unordered_map subst; + for (std::size_t gi = 0; gi < si.generic_names.size() && gi < base.args.size(); + ++gi) + subst[si.generic_names[gi]] = base.args[gi]; + t = substitute_type(f->second, subst); + } + } + break; + } + case ast::ExprKind::Index: { + auto& i = static_cast(e); + Type b = check_expr(*i.base, c, false), idx = check_expr(*i.index, c); + if (!idx.is_integer()) + diags_.error(i.index->range, "E0328", "index must be integer"); + Type container = b; + if (container.kind == TypeKind::Ref && container.pointee) + container = *container.pointee; + if (container.kind == TypeKind::Named && + (container.name == "Slice" || container.name == "SliceMut") && + !container.args.empty()) + t = container.args[0]; + else if (container.kind == TypeKind::Str) + t = Type::builtin("u8"); + else { + diags_.error(e.range, "E0329", "type '" + b.str() + "' is not safely indexable"); + t = {}; + } + break; + } + case ast::ExprKind::Await: { + if (!c.sig->is_async) + diags_.error(e.range, "E0343", "await is only valid inside an async function"); + Type f = check_expr(*static_cast(e).value, c); + if (f.kind != TypeKind::Named || f.name != "Future" || f.args.size() != 1) { + diags_.error(e.range, "E0330", "await requires Future"); + t = {}; + } else + t = f.args[0]; + break; + } + case ast::ExprKind::Cast: { + auto& x = static_cast(e); + Type from = check_expr(*x.value, c), to = type_from_ast(x.to); + bool ref_to_raw = from.kind == TypeKind::Ref && to.kind == TypeKind::RawPtr && + from.pointee && to.pointee; + if (ref_to_raw) { + if (!can_coerce(*from.pointee, *to.pointee)) + diags_.error( + e.range, "E0332", "reference/raw-pointer pointee types are incompatible"); + if (to.mut && !from.mut) + diags_.error(e.range, + "E0332", + "cannot create mutable raw pointer from shared reference"); + } else if ((from.kind == TypeKind::RawPtr || to.kind == TypeKind::RawPtr) && + !(from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr) && + !c.unsafe_context) + diags_.error(e.range, "E0331", "this raw pointer cast requires unsafe context"); + const bool str_to_raw = from.kind == TypeKind::Str && to.kind == TypeKind::RawPtr && + to.pointee && to.pointee->kind == TypeKind::U8; + if (!(from.is_numeric() && to.is_numeric()) && !ref_to_raw && !str_to_raw && + from.kind != TypeKind::RawPtr && to.kind != TypeKind::RawPtr) + diags_.error(e.range, + "E0332", + "unsupported cast from '" + from.str() + "' to '" + to.str() + "'"); + t = to; + break; + } + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + validate_asm(a, c); + t = Type::builtin("unit"); + break; + } + } + model_.expr_types[&e] = t; + return t; +} +} // namespace uinx diff --git a/src/sema_internal.hpp b/src/sema_internal.hpp new file mode 100644 index 0000000..2ff1c9d --- /dev/null +++ b/src/sema_internal.hpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +// Internal helpers shared by the translation units split out of sema.cpp. + +#pragma once + +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx::sema_detail { +inline Type replace_self(Type type, const Type& owner) { + if (type.kind == TypeKind::Named && type.name == "Self") + return owner; + if (type.pointee) + type.pointee = std::make_shared(replace_self(*type.pointee, owner)); + for (auto& arg : type.args) + arg = replace_self(std::move(arg), owner); + return type; +} + +inline bool integer_literal(const ast::Expr* expr) { + if (!expr) + return false; + if (expr->kind == ast::ExprKind::Integer) + return true; + if (expr->kind == ast::ExprKind::Unary) { + const auto& unary = static_cast(*expr); + return unary.op == "-" && unary.operand && unary.operand->kind == ast::ExprKind::Integer; + } + return false; +} + +inline bool can_coerce_expr(const ast::Expr* expr, const Type& from, const Type& to) { + if (can_coerce(from, to)) + return true; + if (expr && expr->kind == ast::ExprKind::String && from.kind == TypeKind::Ref && from.pointee && + from.pointee->kind == TypeKind::Str && to.kind == TypeKind::Str) + return true; + return integer_literal(expr) && from.is_integer() && to.is_integer(); +} + +inline bool static_initializer_expr(const ast::Expr& expr) { + if (expr.kind == ast::ExprKind::Integer || expr.kind == ast::ExprKind::Float || + expr.kind == ast::ExprKind::Bool || expr.kind == ast::ExprKind::Char) + return true; + if (expr.kind == ast::ExprKind::StructLiteral) { + const auto& structure = static_cast(expr); + return std::all_of( + structure.fields.begin(), structure.fields.end(), [](const ast::FieldInit& field) { + if (!field.value) + return false; + if (field.value->kind == ast::ExprKind::Integer) + return static_cast(*field.value).value == "0"; + if (field.value->kind == ast::ExprKind::Bool) + return !static_cast(*field.value).value; + return false; + }); + } + return false; +} + +inline bool builtin_function(std::string_view name) { + static const std::unordered_set names{ + "print", "println", "eprint", "eprintln", "input", "str", "int", "float", "bool", "len"}; + return names.contains(name); +} + +// Calls `f` on every direct child expression of `expr`. A method call visits the +// receiver (`callee` base) and the arguments; a plain call only its arguments. +inline void for_each_child_expr(const ast::Expr& expr, + const std::function& f) { + switch (expr.kind) { + case ast::ExprKind::Unary: + f(*static_cast(expr).operand); + break; + case ast::ExprKind::Binary: { + const auto& binary = static_cast(expr); + f(*binary.lhs); + f(*binary.rhs); + break; + } + case ast::ExprKind::Borrow: + f(*static_cast(expr).target); + break; + case ast::ExprKind::Call: { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Member) + f(*static_cast(*call.callee).base); + for (const auto& arg : call.args) + f(*arg); + break; + } + case ast::ExprKind::Member: + f(*static_cast(expr).base); + break; + case ast::ExprKind::Index: { + const auto& index = static_cast(expr); + f(*index.base); + f(*index.index); + break; + } + case ast::ExprKind::Await: + f(*static_cast(expr).value); + break; + case ast::ExprKind::Cast: + f(*static_cast(expr).value); + break; + case ast::ExprKind::StructLiteral: + for (const auto& field : static_cast(expr).fields) + f(*field.value); + break; + case ast::ExprKind::Asm: + for (const auto& operand : static_cast(expr).operands) + if (operand.value) + f(*operand.value); + break; + default: + break; + } +} + +// Calls `f` on every statement-level expression reachable from `stmt`, +// descending into nested blocks. +inline void walk_stmt_exprs(const ast::Stmt& stmt, const std::function& f) { + const auto block = [&](const ast::BlockStmt* body) { + if (body) + for (const auto& child : body->stmts) + walk_stmt_exprs(*child, f); + }; + if (const auto* let_stmt = dynamic_cast(&stmt)) { + if (let_stmt->init) + f(*let_stmt->init); + } else if (const auto* assign_stmt = dynamic_cast(&stmt)) { + f(*assign_stmt->target); + f(*assign_stmt->value); + } else if (const auto* expr_stmt = dynamic_cast(&stmt)) { + f(*expr_stmt->expr); + } else if (const auto* return_stmt = dynamic_cast(&stmt)) { + if (return_stmt->value) + f(*return_stmt->value); + } else if (const auto* if_stmt = dynamic_cast(&stmt)) { + f(*if_stmt->condition); + block(if_stmt->then_block.get()); + block(if_stmt->else_block.get()); + } else if (const auto* while_stmt = dynamic_cast(&stmt)) { + f(*while_stmt->condition); + block(while_stmt->body.get()); + } else if (const auto* for_stmt = dynamic_cast(&stmt)) { + f(*for_stmt->begin); + f(*for_stmt->end); + block(for_stmt->body.get()); + } else if (const auto* loop_stmt = dynamic_cast(&stmt)) { + block(loop_stmt->body.get()); + } else if (const auto* block_stmt = dynamic_cast(&stmt)) { + block(block_stmt); + } else if (const auto* unsafe_stmt = dynamic_cast(&stmt)) { + block(unsafe_stmt->body.get()); + } +} +} // namespace uinx::sema_detail diff --git a/src/sema_resolve.cpp b/src/sema_resolve.cpp new file mode 100644 index 0000000..271143f --- /dev/null +++ b/src/sema_resolve.cpp @@ -0,0 +1,384 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +hir::SymbolId NameResolver::add(hir::Module& out, + hir::SymbolKind kind, + std::string name, + SourceRange range, + std::optional parent) { + hir::SymbolId id = static_cast(out.symbols.size()); + out.symbols.push_back({id, kind, std::move(name), std::move(range), parent}); + return id; +} +hir::Module NameResolver::resolve(const ast::Module& module) { + hir::Module out; + out.ast = &module; + for (const auto& item : module.items) { + std::visit( + [&](const auto& x) { + using T = std::decay_t; + if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v || + std::is_same_v) { + hir::SymbolKind k = hir::SymbolKind::Function; + if constexpr (std::is_same_v) + k = hir::SymbolKind::Struct; + else if constexpr (std::is_same_v) + k = hir::SymbolKind::Trait; + else if constexpr (std::is_same_v) + k = hir::SymbolKind::Global; + if (out.globals.contains(x.name)) { + if constexpr (std::is_same_v) { + if (!x.is_extern) + diags_.error( + x.range, "E0200", "duplicate global symbol '" + x.name + "'"); + } else + diags_.error( + x.range, "E0200", "duplicate global symbol '" + x.name + "'"); + } else + out.globals[x.name] = add(out, k, x.name, x.range); + } + }, + item); + } + for (const auto& item : module.items) + if (auto f = std::get_if(&item)) + resolve_function(out, *f); + else if (auto im = std::get_if(&item)) + for (const auto& m : im->methods) + resolve_function(out, m); + return out; +} +void NameResolver::resolve_function(hir::Module& out, const ast::FunctionDecl& fn) { + std::optional owner; + auto gi = out.globals.find(fn.name); + if (gi != out.globals.end()) + owner = gi->second; + std::vector> scopes(1); + for (const auto& g : fn.generics) { + auto id = add(out, hir::SymbolKind::Generic, g.name, g.range, owner); + if (scopes.back().contains(g.name)) + diags_.error(g.range, "E0201", "duplicate generic parameter '" + g.name + "'"); + else + scopes.back()[g.name] = id; + } + for (const auto& p : fn.params) { + auto id = add(out, hir::SymbolKind::Parameter, p.name, p.range, owner); + if (scopes.back().contains(p.name)) + diags_.error(p.range, "E0202", "duplicate parameter '" + p.name + "'"); + else + scopes.back()[p.name] = id; + out.params[&p] = id; + } + if (fn.body) + resolve_block(out, *fn.body, scopes, owner); +} +void NameResolver::resolve_block( + hir::Module& out, + const ast::BlockStmt& block, + std::vector>& scopes, + std::optional owner) { + scopes.emplace_back(); + for (const auto& sp : block.stmts) { + const auto& s = *sp; + if (auto l = dynamic_cast(&s)) { + if (l->init) + resolve_expr(out, *l->init, scopes); + auto id = add(out, hir::SymbolKind::Local, l->name, l->range, owner); + if (scopes.back().contains(l->name)) + diags_.error( + l->range, "E0203", "binding '" + l->name + "' already exists in this scope"); + else + scopes.back()[l->name] = id; + out.bindings[l] = id; + } else if (auto a = dynamic_cast(&s)) { + if (a->op == "=" && a->target->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*a->target).name; + bool resolved = out.globals.contains(name); + for (auto it = scopes.rbegin(); !resolved && it != scopes.rend(); ++it) + resolved = it->contains(name); + if (!resolved) { + resolve_expr(out, *a->value, scopes); + auto id = add(out, hir::SymbolKind::Local, name, a->target->range, owner); + scopes.back()[name] = id; + out.assignment_bindings[a] = id; + out.expr_resolution[a->target.get()] = id; + continue; + } + } + resolve_expr(out, *a->target, scopes); + resolve_expr(out, *a->value, scopes); + } else if (auto e = dynamic_cast(&s)) { + resolve_expr(out, *e->expr, scopes); + } else if (auto r = dynamic_cast(&s)) { + if (r->value) + resolve_expr(out, *r->value, scopes); + } else if (auto b = dynamic_cast(&s)) { + resolve_block(out, *b, scopes, owner); + } else if (auto i = dynamic_cast(&s)) { + resolve_expr(out, *i->condition, scopes); + resolve_block(out, *i->then_block, scopes, owner); + if (i->else_block) + resolve_block(out, *i->else_block, scopes, owner); + } else if (auto w = dynamic_cast(&s)) { + resolve_expr(out, *w->condition, scopes); + resolve_block(out, *w->body, scopes, owner); + } else if (auto f = dynamic_cast(&s)) { + resolve_expr(out, *f->begin, scopes); + resolve_expr(out, *f->end, scopes); + scopes.emplace_back(); + auto id = add(out, hir::SymbolKind::Local, f->name, f->range, owner); + scopes.back()[f->name] = id; + out.for_bindings[f] = id; + resolve_block(out, *f->body, scopes, owner); + scopes.pop_back(); + } else if (auto loop = dynamic_cast(&s)) { + resolve_block(out, *loop->body, scopes, owner); + } else if (auto u = dynamic_cast(&s)) { + resolve_block(out, *u->body, scopes, owner); + } + } + scopes.pop_back(); +} +void NameResolver::resolve_expr( + hir::Module& out, + const ast::Expr& e, + std::vector>& scopes) { + switch (e.kind) { + case ast::ExprKind::Name: { + auto& n = static_cast(e); + for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { + auto f = it->find(n.name); + if (f != it->end()) { + out.expr_resolution[&e] = f->second; + return; + } + } + auto g = out.globals.find(n.name); + if (g != out.globals.end()) { + out.expr_resolution[&e] = g->second; + return; + } + if (builtin_function(n.name)) + return; + diags_.error(e.range, "E0204", "unresolved name '" + n.name + "'"); + break; + } + case ast::ExprKind::StructLiteral: { + auto& st = static_cast(e); + auto g = out.globals.find(st.name); + if (g == out.globals.end()) + diags_.error(e.range, "E0206", "unknown struct '" + st.name + "'"); + else + out.expr_resolution[&e] = g->second; + for (auto& f : st.fields) + resolve_expr(out, *f.value, scopes); + break; + } + case ast::ExprKind::Unary: + resolve_expr(out, *static_cast(e).operand, scopes); + break; + case ast::ExprKind::Binary: { + auto& b = static_cast(e); + resolve_expr(out, *b.lhs, scopes); + resolve_expr(out, *b.rhs, scopes); + break; + } + case ast::ExprKind::Borrow: + resolve_expr(out, *static_cast(e).target, scopes); + break; + case ast::ExprKind::Call: { + auto& c = static_cast(e); + resolve_expr(out, *c.callee, scopes); + for (auto& a : c.args) + resolve_expr(out, *a, scopes); + break; + } + case ast::ExprKind::Member: + resolve_expr(out, *static_cast(e).base, scopes); + break; + case ast::ExprKind::Index: { + auto& i = static_cast(e); + resolve_expr(out, *i.base, scopes); + resolve_expr(out, *i.index, scopes); + break; + } + case ast::ExprKind::Await: + resolve_expr(out, *static_cast(e).value, scopes); + break; + case ast::ExprKind::Cast: + resolve_expr(out, *static_cast(e).value, scopes); + break; + case ast::ExprKind::Asm: { + auto& a = static_cast(e); + for (auto& o : a.operands) + if (o.value) + resolve_expr(out, *o.value, scopes); + for (auto& o : a.operands) + if (!o.out_name.empty()) { + bool found = false; + for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) + if (auto f = it->find(o.out_name); f != it->end()) { + found = true; + break; + } + if (!found) + diags_.error( + o.range, "E0205", "unknown asm output binding '" + o.out_name + "'"); + } + break; + } + default: + break; + } +} + +bool TraitSolver::satisfies(const Type& type, std::string_view trait) const { + return satisfies(type, trait, {}); +} + +bool TraitSolver::satisfies( + const Type& type, + std::string_view trait, + const std::unordered_map>& generic_bounds) const { + if (trait == "Copy") + return type.is_copy(); + if (trait != "Send" && trait != "Sync") + return false; + + std::function&)> + unify_pattern; + unify_pattern = [&](const Type& pattern, + const Type& actual, + std::unordered_map& substitutions) -> bool { + if (pattern.kind == TypeKind::Generic) { + auto [it, inserted] = substitutions.emplace(pattern.name, actual); + return inserted || it->second == actual; + } + if (pattern.kind != actual.kind || pattern.name != actual.name || + pattern.mut != actual.mut || pattern.args.size() != actual.args.size()) + return false; + if (static_cast(pattern.pointee) != static_cast(actual.pointee)) + return false; + if (pattern.pointee && !unify_pattern(*pattern.pointee, *actual.pointee, substitutions)) + return false; + for (std::size_t i = 0; i < pattern.args.size(); ++i) + if (!unify_pattern(pattern.args[i], actual.args[i], substitutions)) + return false; + return true; + }; + + std::function solve; + solve = [&](const Type& current, std::string_view current_trait, std::size_t depth) -> bool { + if (depth > 64) + return false; + + for (const auto& impl : model_.impls) + if (impl.trait == current_trait && impl.for_type == current) + return true; + + for (const auto& marker : model_.markers) { + if (marker.trait != current_trait) + continue; + std::unordered_map substitutions; + if (!unify_pattern(marker.type, current, substitutions)) + continue; + bool valid = true; + for (const auto& [generic, bounds] : marker.bounds) { + auto actual = substitutions.find(generic); + if (actual == substitutions.end()) { + valid = false; + break; + } + for (const auto& bound : bounds) { + if (!solve(actual->second, bound, depth + 1)) { + valid = false; + break; + } + } + if (!valid) + break; + } + if (valid) + return true; + } + + if (current_trait != "Send" && current_trait != "Sync") + return false; + + std::unordered_set visiting; + std::function&, + std::unordered_set)> + auto_trait; + std::unordered_set assumed; + for (const auto& [generic, bounds] : generic_bounds) + for (const auto& bound : bounds) + assumed.insert(bound + ":" + generic); + auto_trait = [&](const Type& value, + std::string_view value_trait, + std::unordered_set& active, + std::unordered_set assumptions) -> bool { + const std::string key = std::string(value_trait) + ":" + value.str(); + if (!active.insert(key).second) + return true; + const auto finish = [&](bool result) { + active.erase(key); + return result; + }; + + if (value.kind == TypeKind::RawPtr) + return finish(false); + if (value.kind == TypeKind::Ref && value.pointee) { + if (value_trait == "Send" && value.mut) + return finish( + auto_trait(*value.pointee, "Send", active, std::move(assumptions))); + return finish(auto_trait(*value.pointee, "Sync", active, std::move(assumptions))); + } + if (value.kind == TypeKind::Generic) + return finish(assumptions.contains(std::string(value_trait) + ":" + value.name)); + if (value.kind == TypeKind::Str) + return finish(true); + if (value.kind != TypeKind::Named) + return finish(value.is_copy()); + + auto structure = model_.structs.find(value.name); + if (structure == model_.structs.end()) + return finish(false); + for (const auto& [generic, bounds] : structure->second.bounds) + if (std::find(bounds.begin(), bounds.end(), value_trait) != bounds.end()) + assumptions.insert(std::string(value_trait) + ":" + generic); + + std::unordered_map substitutions; + for (std::size_t i = 0; + i < structure->second.generic_names.size() && i < value.args.size(); + ++i) + substitutions[structure->second.generic_names[i]] = value.args[i]; + for (const auto& [field_name, field_type] : structure->second.fields) { + (void)field_name; + if (!auto_trait(substitute_type(field_type, substitutions), + value_trait, + active, + assumptions)) + return finish(false); + } + return finish(true); + }; + return auto_trait(type, trait, visiting, assumed); + }; + return solve(type, trait, 0); +} +} // namespace uinx diff --git a/src/sema_smp.cpp b/src/sema_smp.cpp new file mode 100644 index 0000000..aa2ea18 --- /dev/null +++ b/src/sema_smp.cpp @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +void TypeChecker::validate_concurrency_boundaries() { + TraitSolver solver(model_); + for (const auto& [key, signature] : model_.functions) { + (void)key; + if (!signature.is_concurrent || !signature.decl) + continue; + // `extern` declarations are FFI trust boundaries without bodies: the + // `unsafe` block at the Uinx call site is the audit point, and raw + // pointers are the ABI mechanism itself (e.g. `uinx_atomic_*` + // intrinsics lowered directly to LLVM atomics). Enforcing `Send` on + // their parameters would make compare-exchange/fetch-add unusable + // from exactly the concurrent paths they exist for, while checking + // nothing about an actual transfer. Uinx-level callers stay checked. + if (signature.is_extern) + continue; + for (std::size_t i = 0; i < signature.params.size(); ++i) { + const Type& parameter = signature.params[i]; + if (solver.satisfies(parameter, "Send", signature.bounds)) + continue; + + const SourceRange range = i < signature.decl->params.size() + ? signature.decl->params[i].range + : signature.decl->range; + diags_.error(range, + "E0362", + "concurrent function parameter " + std::to_string(i + 1) + " of type '" + + parameter.str() + "' is not Send; use an owning Send type, a shared " + + "reference whose pointee is Sync, or add the required generic bound"); + } + } +} + +void TypeChecker::propagate_concurrency(ast::SmpMode mode) { + std::unordered_map> edges; + + std::function&)> collect_callees; + collect_callees = [&](const ast::Expr& expr, std::unordered_set& out) { + if (expr.kind == ast::ExprKind::Call) { + const auto& call = static_cast(expr); + if (call.callee->kind == ast::ExprKind::Name) { + const auto& name = static_cast(*call.callee).name; + if (model_.functions.contains(name)) + out.insert(name); + } else if (call.callee->kind == ast::ExprKind::Member) { + const auto& member = static_cast(*call.callee); + auto type_it = model_.expr_types.find(member.base.get()); + if (type_it != model_.expr_types.end()) { + Type owner = type_it->second; + if (owner.kind == TypeKind::Ref && owner.pointee) + owner = *owner.pointee; + const std::string key = owner.name + "::" + member.member; + if (model_.functions.contains(key)) + out.insert(key); + } + } + } + for_each_child_expr(expr, [&](const ast::Expr& child) { collect_callees(child, out); }); + }; + + for (const auto& [key, sig] : model_.functions) { + if (!sig.decl || !sig.decl->body) + continue; + auto& callees = edges[key]; + for (const auto& stmt : sig.decl->body->stmts) + walk_stmt_exprs(*stmt, [&](const ast::Expr& expr) { collect_callees(expr, callees); }); + } + + std::vector work; + for (const auto& [key, sig] : model_.functions) + if (sig.is_concurrent) + work.push_back(key); + std::unordered_set seen; + while (!work.empty()) { + std::string key = std::move(work.back()); + work.pop_back(); + if (!seen.insert(key).second) + continue; + auto fit = model_.functions.find(key); + if (fit != model_.functions.end()) + fit->second.is_concurrent = true; + auto edge = edges.find(key); + if (edge == edges.end()) + continue; + for (const auto& callee : edge->second) + work.push_back(callee); + } + + // All SMP modes are explicit-shared now: `auto`, `manual` and `strict` + // differ only in ordering of explicitly atomic accesses, never in + // promotion. See docs/MEMORY_MODEL.md. + (void)mode; + // Industrial-grade explicit-shared model (Rust + C++20 + LKMM fusion): + // `smp auto/manual/strict` no longer implicitly promotes mutable globals. + // Only explicitly `shared` globals/fields lower to LLVM atomics. Any + // concurrent access to a mutable non-shared, non-percpu global is E0363. + // Ordering: auto/manual => acquire/release/acq_rel for explicit atomics, + // strict => seq_cst. See docs/MEMORY_MODEL.md. + // Note: `shared bool` is rejected at definition (use AtomicBool over u8) + // because LLVM atomics must be byte-sized (i1 is not valid). + const auto atomic_compatible = [](const Type& type) { + return type.is_integer() || type.kind == TypeKind::RawPtr; + }; + + // A local or parameter that shadows a global is not a global access. + const auto is_local = [&](const ast::Expr& name_expr) { + const auto resolved = model_.hir.expr_resolution.find(&name_expr); + if (resolved == model_.hir.expr_resolution.end() || + resolved->second >= model_.hir.symbols.size()) + return false; + const auto kind = model_.hir.symbols[resolved->second].kind; + return kind == hir::SymbolKind::Local || kind == hir::SymbolKind::Parameter; + }; + + std::unordered_set reported; + std::function check_expr; + check_expr = [&](const ast::Expr& expr) { + if (expr.kind == ast::ExprKind::Name) { + if (is_local(expr)) + return; + const auto& name = static_cast(expr).name; + auto global = model_.globals.find(name); + if (global != model_.globals.end() && global->second.is_mut && + !global->second.is_percpu && !global->second.is_shared) { + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + name; + if (reported.insert(key).second) { + if (atomic_compatible(global->second.type)) { + diags_.error(expr.range, + "E0363", + "concurrent access to mutable global '" + name + + "' requires explicit `shared`, a lock, or `percpu` " + "storage; smp auto no longer promotes implicitly " + "(Rust+C++20+LKMM explicit-shared model)"); + } else { + diags_.error(expr.range, + "E0363", + "concurrent access to aggregate global '" + name + + "' cannot be strengthened as one atomic object; mark " + "atomic fields shared, use a lock, or use percpu " + "storage"); + } + } + } + return; + } + if (expr.kind == ast::ExprKind::Member) { + const auto& member = static_cast(expr); + if (member.base->kind == ast::ExprKind::Name && !is_local(*member.base)) { + const auto& base_name = static_cast(*member.base).name; + auto global = model_.globals.find(base_name); + if (global != model_.globals.end() && global->second.is_mut && + !global->second.is_percpu && global->second.type.kind == TypeKind::Named) { + auto structure = model_.structs.find(global->second.type.name); + if (structure != model_.structs.end()) { + auto field = structure->second.fields.find(member.member); + if (field != structure->second.fields.end()) { + if (structure->second.shared_fields.contains(member.member)) + return; + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + + base_name + "." + member.member; + if (reported.insert(key).second) { + diags_.error( + expr.range, + "E0363", + "concurrent access to field '" + base_name + "." + + member.member + + "' requires explicit `shared` field, a lock, or " + "`percpu` storage; smp auto no longer promotes implicitly"); + } + return; + } + } + // Aggregate global itself accessed concurrently without per-field + // sharing: reject. + const std::string key = std::to_string(expr.range.begin.line) + ":" + + std::to_string(expr.range.begin.column) + ":" + + base_name; + if (reported.insert(key).second) { + diags_.error(expr.range, + "E0363", + "concurrent access to aggregate global '" + base_name + + "' cannot be strengthened as one atomic object; mark " + "atomic fields shared, use a lock, or use percpu " + "storage"); + } + return; + } + } + check_expr(*member.base); + return; + } + for_each_child_expr(expr, check_expr); + }; + + for (const auto& [key, signature] : model_.functions) { + if (!signature.is_concurrent || !signature.decl || !signature.decl->body) + continue; + for (const auto& statement : signature.decl->body->stmts) + walk_stmt_exprs(*statement, check_expr); + } +} +} // namespace uinx diff --git a/src/sema_stmt.cpp b/src/sema_stmt.cpp new file mode 100644 index 0000000..ed6be29 --- /dev/null +++ b/src/sema_stmt.cpp @@ -0,0 +1,230 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 ViudiraTech +// By JiTianYu391 + +#include "sema_internal.hpp" +#include "uinx/sema.hpp" + +#include +#include +#include + +namespace uinx { +using namespace sema_detail; + +void TypeChecker::check_function(const ast::FunctionDecl& fn, const FunctionSig& sig) { + if (!fn.body) + return; + FnContext c; + c.sig = &sig; + c.unsafe_context = fn.is_unsafe; + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + for (std::size_t i = 0; i < fn.params.size(); ++i) { + c.scopes.back()[fn.params[i].name] = sig.params[i]; + if (fn.params[i].mut) + c.mutable_scopes.back().insert(fn.params[i].name); + } + check_block(*fn.body, c); +} +void TypeChecker::check_block(const ast::BlockStmt& b, FnContext& c) { + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + for (const auto& s : b.stmts) + check_stmt(*s, c); + c.mutable_scopes.pop_back(); + c.scopes.pop_back(); +} +void TypeChecker::check_stmt(const ast::Stmt& s, FnContext& c) { + if (auto l = dynamic_cast(&s)) { + Type init = l->init ? check_expr(*l->init, c) : Type{}; + Type ty = l->type + ? type_from_ast(*l->type, + std::unordered_set(c.sig->generic_names.begin(), + c.sig->generic_names.end())) + : init; + if (l->type && !can_coerce_expr(l->init.get(), init, ty)) + diags_.error(l->range, + "E0301", + "initializer has type '" + init.str() + "' but binding expects '" + + ty.str() + "'"); + model_.binding_types[l] = ty; + c.scopes.back()[l->name] = ty; + if (l->mut) + c.mutable_scopes.back().insert(l->name); + return; + } + if (auto a = dynamic_cast(&s)) { + if (auto binding = model_.hir.assignment_bindings.find(a); + binding != model_.hir.assignment_bindings.end()) { + Type rhs = check_expr(*a->value, c); + model_.assignment_binding_types[a] = rhs; + model_.expr_types[a->target.get()] = rhs; + const auto& name = static_cast(*a->target).name; + c.scopes.back()[name] = rhs; + c.mutable_scopes.back().insert(name); + return; + } + Type lhs = check_expr(*a->target, c, false), rhs = check_expr(*a->value, c); + if (!is_mutable_place(*a->target, c)) + diags_.error(a->target->range, "E0333", "assignment requires a mutable place"); + if (!can_coerce_expr(a->value.get(), rhs, lhs)) + diags_.error( + a->range, "E0334", "cannot assign '" + rhs.str() + "' to '" + lhs.str() + "'"); + if (a->op != "=") { + const bool bitwise = a->op == "&=" || a->op == "|=" || a->op == "^=" || + a->op == "<<=" || a->op == ">>=" || a->op == "%="; + if (bitwise ? (!lhs.is_integer() || !rhs.is_integer()) + : (!lhs.is_numeric() || !rhs.is_numeric())) + diags_.error(a->range, + "E0335", + "compound assignment operator '" + a->op + + "' is not valid for these operands"); + } + return; + } + if (auto e = dynamic_cast(&s)) { + check_expr(*e->expr, c); + return; + } + if (auto r = dynamic_cast(&s)) { + Type got = r->value ? check_expr(*r->value, c) : Type::builtin("unit"); + if (!can_coerce_expr(r->value.get(), got, c.sig->result)) + diags_.error(r->range, + "E0302", + "return type '" + got.str() + "' does not match '" + c.sig->result.str() + + "'"); + return; + } + if (auto b = dynamic_cast(&s)) { + check_block(*b, c); + return; + } + if (auto i = dynamic_cast(&s)) { + Type t = check_expr(*i->condition, c); + if (t.kind != TypeKind::Bool) + diags_.error( + i->condition->range, "E0303", "if condition must be bool, found '" + t.str() + "'"); + check_block(*i->then_block, c); + if (i->else_block) + check_block(*i->else_block, c); + return; + } + if (auto w = dynamic_cast(&s)) { + Type t = check_expr(*w->condition, c); + if (t.kind != TypeKind::Bool) + diags_.error(w->condition->range, "E0304", "while condition must be bool"); + ++c.loop_depth; + check_block(*w->body, c); + --c.loop_depth; + return; + } + if (auto f = dynamic_cast(&s)) { + Type begin = check_expr(*f->begin, c), end = check_expr(*f->end, c); + if (!begin.is_integer() || !end.is_integer() || begin != end) + diags_.error(f->range, "E0356", "for ranges require matching integer endpoint types"); + c.scopes.emplace_back(); + c.mutable_scopes.emplace_back(); + c.scopes.back()[f->name] = begin; + ++c.loop_depth; + check_block(*f->body, c); + --c.loop_depth; + c.mutable_scopes.pop_back(); + c.scopes.pop_back(); + return; + } + if (auto l = dynamic_cast(&s)) { + ++c.loop_depth; + check_block(*l->body, c); + --c.loop_depth; + return; + } + if (dynamic_cast(&s) || dynamic_cast(&s)) { + if (c.loop_depth == 0) + diags_.error(s.range, "E0357", "break/continue is only valid inside a loop"); + return; + } + if (auto u = dynamic_cast(&s)) { + bool old = c.unsafe_context; + c.unsafe_context = true; + check_block(*u->body, c); + c.unsafe_context = old; + return; + } +} +Type TypeChecker::lookup_local(std::string_view n, const FnContext& c, const SourceRange& r) { + for (auto it = c.scopes.rbegin(); it != c.scopes.rend(); ++it) { + auto f = it->find(std::string(n)); + if (f != it->end()) + return f->second; + } + auto g = model_.globals.find(std::string(n)); + if (g != model_.globals.end()) + return g->second.type; + diags_.error(r, "E0305", "name '" + std::string(n) + "' is not a value in this scope"); + return {}; +} +bool TypeChecker::is_mutable_binding(std::string_view n, const FnContext& c) const { + for (auto it = c.mutable_scopes.rbegin(); it != c.mutable_scopes.rend(); ++it) + if (it->contains(std::string(n))) + return true; + auto g = model_.globals.find(std::string(n)); + return g != model_.globals.end() && g->second.is_mut && !g->second.is_const; +} +bool TypeChecker::is_mutable_place(const ast::Expr& e, const FnContext& c) { + if (e.kind == ast::ExprKind::Name) + return is_mutable_binding(static_cast(e).name, c); + if (e.kind == ast::ExprKind::Member) { + const auto& m = static_cast(e); + Type base{}; + if (auto it = model_.expr_types.find(m.base.get()); it != model_.expr_types.end()) + base = it->second; + if (base.kind == TypeKind::Ref) + return base.mut; + return is_mutable_place(*m.base, c); + } + if (e.kind == ast::ExprKind::Index) { + const auto& i = static_cast(e); + Type base{}; + if (auto it = model_.expr_types.find(i.base.get()); it != model_.expr_types.end()) + base = it->second; + if (base.kind == TypeKind::Ref) + return base.mut; + if (base.kind == TypeKind::Named && base.name == "SliceMut") + return is_mutable_place(*i.base, c); + return false; + } + if (e.kind == ast::ExprKind::Unary) { + const auto& u = static_cast(e); + if (u.op == "*") { + Type p{}; + if (auto it = model_.expr_types.find(u.operand.get()); it != model_.expr_types.end()) + p = it->second; + return p.kind == TypeKind::Ref ? p.mut : (p.kind == TypeKind::RawPtr ? p.mut : false); + } + } + return false; +} +bool TypeChecker::unify_generic(const Type& f, + const Type& a, + std::unordered_map& s) { + if (f.kind == TypeKind::Generic) { + auto it = s.find(f.name); + if (it == s.end()) { + s[f.name] = a; + return true; + } + return it->second == a; + } + if (f.kind != a.kind) + return false; + if (f.pointee && a.pointee) + return f.mut == a.mut && unify_generic(*f.pointee, *a.pointee, s); + if (f.args.size() != a.args.size()) + return false; + for (std::size_t i = 0; i < f.args.size(); ++i) + if (!unify_generic(f.args[i], a.args[i], s)) + return false; + return f.name == a.name || f.name.empty() || a.name.empty(); +} +} // namespace uinx diff --git a/tests/conformance/pass/reborrow_call.ux b/tests/conformance/pass/reborrow_call.ux new file mode 100644 index 0000000..ef5d332 --- /dev/null +++ b/tests/conformance/pass/reborrow_call.ux @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-call +# EXPECT: compile-pass +# Passing a mutref to a mutref parameter reborrows instead of moving it. + +func inc(r: mutref i32) -> unit: + deref r += 1 + return +func twice(r: mutref i32) -> unit: + inc(r) + inc(r) + return +func main() -> i32: + var x = 0 + twice(borrow mut x) + val r = borrow mut x + var i = 0 + loop: + if i >= 3: + break + inc(r) + i += 1 + return deref r - 5 diff --git a/tests/conformance/pass/reborrow_explicit.ux b/tests/conformance/pass/reborrow_explicit.ux new file mode 100644 index 0000000..1080eac --- /dev/null +++ b/tests/conformance/pass/reborrow_explicit.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-explicit +# EXPECT: compile-pass +# borrow [mut] deref r reborrows; the parent is usable again after the last use. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + deref r2 = 5 + deref r = 6 + val s = borrow deref r + val t = borrow deref r + return deref s + deref t - 12 diff --git a/tests/conformance/pass/reborrow_nested.ux b/tests/conformance/pass/reborrow_nested.ux new file mode 100644 index 0000000..e06b681 --- /dev/null +++ b/tests/conformance/pass/reborrow_nested.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-nested +# EXPECT: compile-pass +# Reborrows of reborrows keep every ancestor frozen while the innermost is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + val r3 = borrow mut deref r2 + deref r3 = 3 + deref r2 = 4 + deref r = 5 + return x - 5 diff --git a/tests/conformance/pass/reborrow_param.ux b/tests/conformance/pass/reborrow_param.ux new file mode 100644 index 0000000..6b55750 --- /dev/null +++ b/tests/conformance/pass/reborrow_param.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.reborrow-param +# EXPECT: compile-pass +# Reborrowing through a reference parameter. + +func f(p: mutref i32) -> i32: + val a = borrow mut deref p + deref a = 4 + deref p = 5 + return deref p +func main() -> i32: + var x = 0 + return f(borrow mut x) - 5 diff --git a/tests/conformance/pass/smp_local_shadows_global.ux b/tests/conformance/pass/smp_local_shadows_global.ux new file mode 100644 index 0000000..9982c1f --- /dev/null +++ b/tests/conformance/pass/smp_local_shadows_global.ux @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: conformance.positive.smp-local-shadows-global +# EXPECT: compile-pass +# A local or parameter named like a mutable global is not a global access. + +dontneed std +smp auto + +static var counter: u64 = 0 +public unsafe concurrent func f() -> u64: + var counter: u64 = 1 + counter += 1 + return counter + +public unsafe concurrent func g(counter: u64) -> u64: + return counter + 1 diff --git a/tests/safety/borrow-fail/mutable_ref_move.ux b/tests/safety/borrow-fail/mutable_ref_move.ux index f81ad54..41f533d 100644 --- a/tests/safety/borrow-fail/mutable_ref_move.ux +++ b/tests/safety/borrow-fail/mutable_ref_move.ux @@ -12,6 +12,7 @@ func consume(value: mutref i32) -> unit: func main() -> i32: var x = 1 val first = borrow mut x + val second = first consume(first) - consume(first) + consume(second) return 0 diff --git a/tests/safety/borrow-fail/reborrow_after_move.ux b/tests/safety/borrow-fail/reborrow_after_move.ux new file mode 100644 index 0000000..3c9c542 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_after_move.ux @@ -0,0 +1,19 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-after-move +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0400 +# A moved mutref cannot be reborrowed by a later call. + +func inc(r: mutref i32) -> unit: + deref r += 1 + return +func main() -> i32: + var x = 1 + val r = borrow mut x + val moved = r + inc(r) + inc(moved) + return 0 diff --git a/tests/safety/borrow-fail/reborrow_double_mut.ux b/tests/safety/borrow-fail/reborrow_double_mut.ux new file mode 100644 index 0000000..34fab84 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_double_mut.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-double-mut +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0402 +# Two live mutable reborrows of the same reference conflict. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val a = borrow mut deref r + val b = borrow mut deref r + deref a = 1 + deref b = 2 + return 0 diff --git a/tests/safety/borrow-fail/reborrow_param_write.ux b/tests/safety/borrow-fail/reborrow_param_write.ux new file mode 100644 index 0000000..220d78f --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_param_write.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-param-write +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through a reference parameter while its reborrow is live. + +func f(p: mutref i32) -> unit: + val a = borrow mut deref p + deref p = 5 + deref a = 4 + return +func main() -> i32: + return 0 diff --git a/tests/safety/borrow-fail/reborrow_read_parent.ux b/tests/safety/borrow-fail/reborrow_read_parent.ux new file mode 100644 index 0000000..47516c7 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_read_parent.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-read-parent +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0406 +# Reading the parent while a mutable reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + val v = deref r + deref r2 = v + return 0 diff --git a/tests/safety/borrow-fail/reborrow_shared_then_write.ux b/tests/safety/borrow-fail/reborrow_shared_then_write.ux new file mode 100644 index 0000000..9162199 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_shared_then_write.ux @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-shared-then-write +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through the parent while a shared reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val s = borrow deref r + deref r = 2 + return deref s diff --git a/tests/safety/borrow-fail/reborrow_two_args.ux b/tests/safety/borrow-fail/reborrow_two_args.ux new file mode 100644 index 0000000..d799ba0 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_two_args.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-two-args +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0406 +# The same mutref cannot be reborrowed twice in one call. + +func two(a: mutref i32, b: mutref i32) -> unit: + return +func main() -> i32: + var x = 1 + val r = borrow mut x + two(r, r) + return 0 diff --git a/tests/safety/borrow-fail/reborrow_write_parent.ux b/tests/safety/borrow-fail/reborrow_write_parent.ux new file mode 100644 index 0000000..3e3d275 --- /dev/null +++ b/tests/safety/borrow-fail/reborrow_write_parent.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.reborrow-write-parent +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0403 +# Writing through the parent while a mutable reborrow is live. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val r2 = borrow mut deref r + deref r = 6 + deref r2 = 5 + return 0 diff --git a/tests/safety/borrow-fail/write_through_moved_ref.ux b/tests/safety/borrow-fail/write_through_moved_ref.ux new file mode 100644 index 0000000..b14e924 --- /dev/null +++ b/tests/safety/borrow-fail/write_through_moved_ref.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.borrow.write-through-moved-ref +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0400 +# Writing through a moved mutref is a use after move. + +func main() -> i32: + var x = 1 + val r = borrow mut x + val moved = r + deref r = 2 + deref moved = 3 + return 0 diff --git a/tests/safety/smp-fail/concurrent_global_in_cast.ux b/tests/safety/smp-fail/concurrent_global_in_cast.ux new file mode 100644 index 0000000..bea3ecc --- /dev/null +++ b/tests/safety/smp-fail/concurrent_global_in_cast.ux @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.smp.global-in-cast +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0363 +# Walker coverage: a mutable global under a cast in a let initializer. + +dontneed std +smp auto + +static var n: u64 = 3 +public unsafe concurrent func f() -> u32: + val a = n as u32 + return a diff --git a/tests/safety/smp-fail/concurrent_global_in_condition.ux b/tests/safety/smp-fail/concurrent_global_in_condition.ux new file mode 100644 index 0000000..81e81ca --- /dev/null +++ b/tests/safety/smp-fail/concurrent_global_in_condition.ux @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: BSD-3-Clause +# Copyright (c) 2026 ViudiraTech +# By JiTianYu391 + +# TEST: safety.smp.global-in-condition +# EXPECT: compile-fail +# EXPECT-DIAGNOSTIC: E0363 +# Walker coverage: a mutable global in a loop condition. + +dontneed std +smp auto + +static var counter: u64 = 0 +public unsafe concurrent func f() -> u64: + while counter < 3: + return 1 + return 0