From 3eafc95961a16969e4552b5b3c5490c0b5876d7b Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 02:55:29 +0000 Subject: [PATCH 1/6] Implement Map collection type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add complete support for Map hash map collection: - Map() constructor for empty maps - Map literal syntax: {"key": value, ...} - Methods: set(), get() (returns optional), contains(), remove(), keys(), values(), items(), length(), is_empty(), clear() - Iteration support via items() - Maps to std::unordered_map in C++ Implements #36 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CMakeLists.txt | 3 + README.md | 72 ++++++++++ codegen/codegen.hpp | 5 + codegen/emit_expression.cpp | 8 ++ codegen/emit_map.cpp | 128 +++++++++++++++++ codegen/emit_method_call.cpp | 5 + codegen/emit_type.cpp | 45 ++++++ common/type_utils.hpp | 46 ++++++ lexer/lexer.cpp | 1 + lexer/token.hpp | 1 + parser/ast.hpp | 17 +++ parser/parse_primary.cpp | 52 +++++++ parser/parse_type.cpp | 11 ++ tests/test_maps.b | 229 ++++++++++++++++++++++++++++++ typechecker/check_expression.cpp | 8 ++ typechecker/check_field.cpp | 14 ++ typechecker/check_map.cpp | 137 ++++++++++++++++++ typechecker/check_method_call.cpp | 12 ++ typechecker/maps.cpp | 159 +++++++++++++++++++++ typechecker/maps.hpp | 24 ++++ typechecker/typechecker.hpp | 6 + 21 files changed, 983 insertions(+) create mode 100644 codegen/emit_map.cpp create mode 100644 tests/test_maps.b create mode 100644 typechecker/check_map.cpp create mode 100644 typechecker/maps.cpp create mode 100644 typechecker/maps.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b162c0b..a30923d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,10 +90,12 @@ add_library(bishop_lib typechecker/check_select_stmt.cpp typechecker/strings.cpp typechecker/lists.cpp + typechecker/maps.cpp typechecker/pairs.cpp typechecker/tuples.cpp typechecker/check_pair.cpp typechecker/check_tuple.cpp + typechecker/check_map.cpp typechecker/check_lambda.cpp codegen/codegen.cpp codegen/emit_type.cpp @@ -121,6 +123,7 @@ add_library(bishop_lib codegen/emit_lambda.cpp codegen/emit_channel.cpp codegen/emit_list.cpp + codegen/emit_map.cpp codegen/emit_string.cpp codegen/emit_pair.cpp codegen/emit_tuple.cpp diff --git a/README.md b/README.md index 5c1c19d..efed796 100644 --- a/README.md +++ b/README.md @@ -635,6 +635,78 @@ quotient := result.first; // 3 remainder := result.second; // 2 ``` +## Maps + +Maps are key-value collections that provide fast lookup by key. + +### Map Creation + +```bishop +ages := Map(); // empty map +config := Map(); // string to string map +codes := Map(); // int to string map +``` + +### Map Literals + +```bishop +ages := {"alice": 30, "bob": 25}; // inferred as Map +config := {"host": "localhost", "port": "8080"}; // inferred as Map +``` + +### Typed Declaration + +```bishop +Map ages = {"alice": 30}; +Map config = Map(); +``` + +### Map Methods + +```bishop +ages := {"alice": 30, "bob": 25}; + +// Query methods +ages.length(); // -> int: 2 +ages.is_empty(); // -> bool: false +ages.contains("alice"); // -> bool: true + +// Access methods (get returns optional) +age := ages.get("alice") default 0; // -> int: 30 +age := ages.get("unknown") default 0; // -> int: 0 (key not found) + +// Modification methods +ages.set("charlie", 35); // add or update key +ages.remove("bob"); // remove key +ages.clear(); // remove all entries + +// Iteration methods +keys := ages.keys(); // -> List: all keys +vals := ages.values(); // -> List: all values +items := ages.items(); // -> List>: all entries +``` + +### Iterating Maps + +```bishop +ages := {"alice": 30, "bob": 25}; + +// Iterate over keys +for key in ages.keys() { + print(key); +} + +// Iterate over values +for val in ages.values() { + print(val); +} + +// Iterate over key-value pairs +for item in ages.items() { + print(item.key + ": " + str(item.value)); +} +``` + ## Tuples Tuples hold 2-5 values of the same type. diff --git a/codegen/codegen.hpp b/codegen/codegen.hpp index 6653c2b..25dcf58 100644 --- a/codegen/codegen.hpp +++ b/codegen/codegen.hpp @@ -101,6 +101,11 @@ std::string emit_list_create(const ListCreate& list); std::string emit_list_literal(CodeGenState& state, const ListLiteral& list); std::string emit_list_method_call(CodeGenState& state, const MethodCall& call, const std::string& obj_str, const std::vector& args); +// Map (emit_map.cpp) +std::string emit_map_create(const MapCreate& map); +std::string emit_map_literal(CodeGenState& state, const MapLiteral& map); +std::string emit_map_method_call(CodeGenState& state, const MethodCall& call, const std::string& obj_str, const std::vector& args); + // String methods (emit_string.cpp) std::string emit_str_method_call(CodeGenState& state, const MethodCall& call, const std::string& obj_str, const std::vector& args); diff --git a/codegen/emit_expression.cpp b/codegen/emit_expression.cpp index f416932..cf1f687 100644 --- a/codegen/emit_expression.cpp +++ b/codegen/emit_expression.cpp @@ -98,6 +98,14 @@ string emit(CodeGenState& state, const ASTNode& node) { return emit_list_literal(state, *list); } + if (auto* map = dynamic_cast(&node)) { + return emit_map_create(*map); + } + + if (auto* map = dynamic_cast(&node)) { + return emit_map_literal(state, *map); + } + if (auto* pair = dynamic_cast(&node)) { return emit_pair_create(state, *pair); } diff --git a/codegen/emit_map.cpp b/codegen/emit_map.cpp new file mode 100644 index 0000000..4e1ac2c --- /dev/null +++ b/codegen/emit_map.cpp @@ -0,0 +1,128 @@ +/** + * @file emit_map.cpp + * @brief Map emission for the Bishop code generator. + */ + +#include "codegen.hpp" +#include + +using namespace std; + +namespace codegen { + +/** + * Emits a map creation: Map() -> std::unordered_map{}. + */ +string emit_map_create(const MapCreate& map) { + string cpp_key_type = map_type(map.key_type); + string cpp_value_type = map_type(map.value_type); + return "std::unordered_map<" + cpp_key_type + ", " + cpp_value_type + ">{}"; +} + +/** + * Emits a map literal: {"key": value, ...} -> std::unordered_map{{"key", value}, ...}. + */ +string emit_map_literal(CodeGenState& state, const MapLiteral& map) { + string entries; + + for (size_t i = 0; i < map.entries.size(); i++) { + if (i > 0) { + entries += ", "; + } + + string key = emit(state, *map.entries[i].first); + string value = emit(state, *map.entries[i].second); + entries += "{" + key + ", " + value + "}"; + } + + return "std::unordered_map{" + entries + "}"; +} + +/** + * Emits a map method call, mapping Bishop methods to std::unordered_map equivalents. + */ +string emit_map_method_call(CodeGenState& state, const MethodCall& call, const string& obj_str, const vector& args) { + if (call.method_name == "length") { + return obj_str + ".size()"; + } + + if (call.method_name == "is_empty") { + return obj_str + ".empty()"; + } + + if (call.method_name == "contains") { + return "(" + obj_str + ".find(" + args[0] + ") != " + obj_str + ".end())"; + } + + if (call.method_name == "get") { + // Returns optional - uses find and returns std::optional + return fmt::format( + "[](const auto& m, const auto& key) -> std::optionalsecond)>> {{ " + "auto it = m.find(key); " + "if (it != m.end()) return it->second; " + "return std::nullopt; " + "}}({}, {})", + obj_str, args[0] + ); + } + + if (call.method_name == "set") { + return obj_str + "[" + args[0] + "] = " + args[1]; + } + + if (call.method_name == "remove") { + return obj_str + ".erase(" + args[0] + ")"; + } + + if (call.method_name == "clear") { + return obj_str + ".clear()"; + } + + if (call.method_name == "keys") { + // Returns a vector of keys + return fmt::format( + "[](const auto& m) {{ " + "std::vectorfirst)>> keys; " + "keys.reserve(m.size()); " + "for (const auto& [k, v] : m) keys.push_back(k); " + "return keys; " + "}}({})", + obj_str + ); + } + + if (call.method_name == "values") { + // Returns a vector of values + return fmt::format( + "[](const auto& m) {{ " + "std::vectorsecond)>> values; " + "values.reserve(m.size()); " + "for (const auto& [k, v] : m) values.push_back(v); " + "return values; " + "}}({})", + obj_str + ); + } + + if (call.method_name == "items") { + // Returns a vector of MapItem structs with key and value fields + return fmt::format( + "[](const auto& m) {{ " + "struct MapItem {{ " + "std::decay_tfirst)> key; " + "std::decay_tsecond)> value; " + "}}; " + "std::vector items; " + "items.reserve(m.size()); " + "for (const auto& [k, v] : m) items.push_back({{k, v}}); " + "return items; " + "}}({})", + obj_str + ); + } + + // Unknown map method - fall back to generic method call + return method_call(obj_str, call.method_name, args); +} + +} // namespace codegen diff --git a/codegen/emit_method_call.cpp b/codegen/emit_method_call.cpp index cc2051e..5679955 100644 --- a/codegen/emit_method_call.cpp +++ b/codegen/emit_method_call.cpp @@ -92,6 +92,11 @@ string emit_method_call(CodeGenState& state, const MethodCall& call) { return emit_list_method_call(state, call, obj_str, args); } + // Handle Map methods - map to std::unordered_map equivalents + if (call.object_type.rfind("Map<", 0) == 0) { + return emit_map_method_call(state, call, obj_str, args); + } + // Handle extended string methods if (call.object_type == "str") { string result = emit_str_method_call(state, call, obj_str, args); diff --git a/codegen/emit_type.cpp b/codegen/emit_type.cpp index ff74b76..ad33969 100644 --- a/codegen/emit_type.cpp +++ b/codegen/emit_type.cpp @@ -62,6 +62,51 @@ string map_type(const string& t) { return "std::vector<" + map_type(element_type) + ">"; } + // Handle Map types: Map -> std::unordered_map + if (t.rfind("Map<", 0) == 0 && t.back() == '>') { + // Extract key and value types by finding the comma at depth 1 + size_t start = 4; // After "Map<" + int depth = 1; + size_t comma_pos = string::npos; + + for (size_t i = start; i < t.size(); i++) { + if (t[i] == '<') depth++; + else if (t[i] == '>') depth--; + else if (t[i] == ',' && depth == 1 && comma_pos == string::npos) { + comma_pos = i; + break; + } + } + + assert(comma_pos != string::npos && "malformed Map type passed typechecker"); + string key_type = t.substr(start, comma_pos - start); + string value_type = t.substr(comma_pos + 2, t.size() - comma_pos - 3); // Skip ", " and ">" + return "std::unordered_map<" + map_type(key_type) + ", " + map_type(value_type) + ">"; + } + + // Handle MapItem types for iteration + if (t.rfind("MapItem<", 0) == 0 && t.back() == '>') { + // MapItem is a struct with key and value fields - we use an anonymous struct + size_t start = 8; // After "MapItem<" + int depth = 1; + size_t comma_pos = string::npos; + + for (size_t i = start; i < t.size(); i++) { + if (t[i] == '<') depth++; + else if (t[i] == '>') depth--; + else if (t[i] == ',' && depth == 1 && comma_pos == string::npos) { + comma_pos = i; + break; + } + } + + assert(comma_pos != string::npos && "malformed MapItem type passed typechecker"); + string key_type = t.substr(start, comma_pos - start); + string value_type = t.substr(comma_pos + 2, t.size() - comma_pos - 3); + // Use auto for the struct since it's generated inline + return "auto"; + } + // Handle Pair types: Pair -> std::pair if (t.rfind("Pair<", 0) == 0 && t.back() == '>') { string element_type = extract_element_type(t, "Pair<"); diff --git a/common/type_utils.hpp b/common/type_utils.hpp index 173365d..f7805e7 100644 --- a/common/type_utils.hpp +++ b/common/type_utils.hpp @@ -46,4 +46,50 @@ inline std::string extract_element_type(const std::string& generic_type, const s return ""; } +/** + * Extracts both key and value types from a Map type string. + * + * For example: + * - extract_map_types("Map") returns {"str", "int"} + * - extract_map_types("Map>") returns {"str", "List"} + * + * @param map_type The full Map type string (e.g., "Map") + * @return A pair of {key_type, value_type}, or {"", ""} if invalid + */ +inline std::pair extract_map_types(const std::string& map_type) { + const std::string prefix = "Map<"; + + if (map_type.empty() || map_type.rfind(prefix, 0) != 0 || map_type.back() != '>') { + return {"", ""}; + } + + size_t start = prefix.length(); + int depth = 1; + size_t comma_pos = std::string::npos; + + // Find the comma at depth 1 that separates K and V + for (size_t i = start; i < map_type.size(); i++) { + if (map_type[i] == '<') { + depth++; + } else if (map_type[i] == '>') { + depth--; + + if (depth == 0) { + // Found the end, extract both types + if (comma_pos == std::string::npos) { + return {"", ""}; // No comma found + } + + std::string key_type = map_type.substr(start, comma_pos - start); + std::string value_type = map_type.substr(comma_pos + 2, i - comma_pos - 2); // +2 to skip ", " + return {key_type, value_type}; + } + } else if (map_type[i] == ',' && depth == 1 && comma_pos == std::string::npos) { + comma_pos = i; + } + } + + return {"", ""}; +} + } // namespace bishop diff --git a/lexer/lexer.cpp b/lexer/lexer.cpp index f771992..23be4b4 100644 --- a/lexer/lexer.cpp +++ b/lexer/lexer.cpp @@ -34,6 +34,7 @@ static unordered_map keywords = { {"private", TokenType::PRIVATE}, {"Channel", TokenType::CHANNEL}, {"List", TokenType::LIST}, + {"Map", TokenType::MAP}, {"Pair", TokenType::PAIR}, {"Tuple", TokenType::TUPLE}, {"select", TokenType::SELECT}, diff --git a/lexer/token.hpp b/lexer/token.hpp index e47aef8..a23d230 100644 --- a/lexer/token.hpp +++ b/lexer/token.hpp @@ -159,6 +159,7 @@ enum class TokenType { PRIVATE, CHANNEL, LIST, + MAP, PAIR, TUPLE, SELECT, diff --git a/parser/ast.hpp b/parser/ast.hpp index f6dcdff..f2a70cd 100644 --- a/parser/ast.hpp +++ b/parser/ast.hpp @@ -184,6 +184,23 @@ struct ListLiteral : ASTNode { vector> elements; ///< List element expressions }; +/** @brief Map creation: Map() */ +struct MapCreate : ASTNode { + string key_type; ///< Type of keys + string value_type; ///< Type of values +}; + +/** @brief Map literal: {"key": value, ...} */ +struct MapLiteral : ASTNode { + vector, unique_ptr>> entries; ///< Key-value pairs +}; + +/** @brief Map item for iteration: has key and value fields */ +struct MapItem : ASTNode { + string key_type; ///< Type of keys + string value_type; ///< Type of values +}; + /** @brief Pair creation: Pair(a, b) */ struct PairCreate : ASTNode { string element_type; ///< Type of elements (homogeneous) diff --git a/parser/parse_primary.cpp b/parser/parse_primary.cpp index 18ac2ae..dd938e4 100644 --- a/parser/parse_primary.cpp +++ b/parser/parse_primary.cpp @@ -159,6 +159,30 @@ unique_ptr parse_primary(ParserState& state) { return list; } + // Handle map creation: Map() or Map>() + if (check(state, TokenType::MAP)) { + int start_line = current(state).line; + advance(state); + consume(state, TokenType::LT); + + // Parse key type + string key_type = parse_type(state); + consume(state, TokenType::COMMA); + + // Parse value type + string value_type = parse_type(state); + + consume(state, TokenType::GT); + consume(state, TokenType::LPAREN); + consume(state, TokenType::RPAREN); + + auto map = make_unique(); + map->key_type = key_type; + map->value_type = value_type; + map->line = start_line; + return map; + } + // Handle pair creation: Pair(a, b) or Pair>(a, b) if (check(state, TokenType::PAIR)) { int start_line = current(state).line; @@ -244,6 +268,34 @@ unique_ptr parse_primary(ParserState& state) { return list; } + // Handle map literal: {"key": value, ...} + // Distinguished from struct literal by having STRING : at start instead of IDENT : + if (check(state, TokenType::LBRACE) && check_ahead(state, 1, TokenType::STRING) && check_ahead(state, 2, TokenType::COLON)) { + int start_line = current(state).line; + advance(state); // consume '{' + + auto map = make_unique(); + map->line = start_line; + + while (!check(state, TokenType::RBRACE) && !check(state, TokenType::EOF_TOKEN)) { + // Parse key expression + auto key = parse_expression(state); + consume(state, TokenType::COLON); + + // Parse value expression + auto value = parse_expression(state); + + map->entries.push_back({move(key), move(value)}); + + if (check(state, TokenType::COMMA)) { + advance(state); + } + } + + consume(state, TokenType::RBRACE); + return map; + } + if (check(state, TokenType::NUMBER)) { Token tok = current(state); advance(state); diff --git a/parser/parse_type.cpp b/parser/parse_type.cpp index d86fbb0..40a0b05 100644 --- a/parser/parse_type.cpp +++ b/parser/parse_type.cpp @@ -138,6 +138,17 @@ string parse_base_type(ParserState& state) { return "List<" + element_type + ">"; } + // Map type + if (check(state, TokenType::MAP)) { + advance(state); + consume(state, TokenType::LT); + string key_type = parse_type(state); + consume(state, TokenType::COMMA); + string value_type = parse_type(state); + consume(state, TokenType::GT); + return "Map<" + key_type + ", " + value_type + ">"; + } + // Pair type if (check(state, TokenType::PAIR)) { advance(state); diff --git a/tests/test_maps.b b/tests/test_maps.b new file mode 100644 index 0000000..0c6ba2c --- /dev/null +++ b/tests/test_maps.b @@ -0,0 +1,229 @@ +// ============================================ +// Map Creation +// ============================================ + +fn test_map_create_empty() { + ages := Map(); + assert_eq(ages.length(), 0); +} + +fn test_map_create_str_str() { + config := Map(); + assert_eq(config.is_empty(), true); +} + +// ============================================ +// Map Literal Syntax +// ============================================ + +fn test_map_literal_str_str() { + config := {"host": "localhost", "port": "8080"}; + assert_eq(config.length(), 2); +} + +fn test_map_literal_str_int() { + ages := {"alice": 30, "bob": 25}; + assert_eq(ages.length(), 2); +} + +fn test_map_literal_single() { + single := {"key": "value"}; + assert_eq(single.length(), 1); +} + +// ============================================ +// Map set() and get() +// ============================================ + +fn test_map_set_get() { + ages := Map(); + ages.set("alice", 30); + ages.set("bob", 25); + + alice_age := ages.get("alice") default 0; + assert_eq(alice_age, 30); + + bob_age := ages.get("bob") default 0; + assert_eq(bob_age, 25); +} + +fn test_map_get_missing_key() { + ages := Map(); + ages.set("alice", 30); + + charlie_age := ages.get("charlie") default 99; + assert_eq(charlie_age, 99); +} + +fn test_map_set_overwrite() { + ages := Map(); + ages.set("alice", 30); + ages.set("alice", 31); + + alice_age := ages.get("alice") default 0; + assert_eq(alice_age, 31); +} + +// ============================================ +// Map contains() +// ============================================ + +fn test_map_contains_true() { + ages := {"alice": 30, "bob": 25}; + assert_eq(ages.contains("alice"), true); + assert_eq(ages.contains("bob"), true); +} + +fn test_map_contains_false() { + ages := {"alice": 30}; + assert_eq(ages.contains("charlie"), false); +} + +// ============================================ +// Map remove() +// ============================================ + +fn test_map_remove() { + ages := {"alice": 30, "bob": 25}; + ages.remove("bob"); + + assert_eq(ages.length(), 1); + assert_eq(ages.contains("bob"), false); + assert_eq(ages.contains("alice"), true); +} + +fn test_map_remove_missing_key() { + ages := {"alice": 30}; + ages.remove("charlie"); + + assert_eq(ages.length(), 1); +} + +// ============================================ +// Map Query Methods +// ============================================ + +fn test_map_length() { + ages := {"alice": 30, "bob": 25, "charlie": 35}; + assert_eq(ages.length(), 3); +} + +fn test_map_is_empty_true() { + ages := Map(); + assert_eq(ages.is_empty(), true); +} + +fn test_map_is_empty_false() { + ages := {"alice": 30}; + assert_eq(ages.is_empty(), false); +} + +fn test_map_clear() { + ages := {"alice": 30, "bob": 25}; + ages.clear(); + + assert_eq(ages.length(), 0); + assert_eq(ages.is_empty(), true); +} + +// ============================================ +// Map keys(), values(), items() +// ============================================ + +fn test_map_keys() { + ages := {"alice": 30, "bob": 25}; + key_list := ages.keys(); + + assert_eq(key_list.length(), 2); + assert_eq(key_list.contains("alice"), true); + assert_eq(key_list.contains("bob"), true); +} + +fn test_map_values() { + ages := {"alice": 30, "bob": 25}; + val_list := ages.values(); + + assert_eq(val_list.length(), 2); + assert_eq(val_list.contains(30), true); + assert_eq(val_list.contains(25), true); +} + +fn test_map_items() { + ages := {"alice": 30, "bob": 25}; + item_list := ages.items(); + + assert_eq(item_list.length(), 2); +} + +// ============================================ +// Map Iteration +// ============================================ + +fn test_map_iterate_keys() { + ages := {"alice": 30, "bob": 25}; + count := 0; + + for key in ages.keys() { + count = count + 1; + } + + assert_eq(count, 2); +} + +fn test_map_iterate_values() { + ages := {"alice": 30, "bob": 25}; + total := 0; + + for val in ages.values() { + total = total + val; + } + + assert_eq(total, 55); +} + +fn test_map_iterate_items() { + ages := {"alice": 30, "bob": 25}; + total := 0; + + for item in ages.items() { + total = total + item.value; + } + + assert_eq(total, 55); +} + +// ============================================ +// Map with Different Types +// ============================================ + +fn test_map_int_str() { + codes := Map(); + codes.set(200, "OK"); + codes.set(404, "Not Found"); + + msg := codes.get(200) default "Unknown"; + assert_eq(msg, "OK"); +} + +fn test_map_str_bool() { + flags := Map(); + flags.set("debug", true); + flags.set("verbose", false); + + is_debug := flags.get("debug") default false; + assert_eq(is_debug, true); +} + +// ============================================ +// Typed Variable Declaration +// ============================================ + +fn test_map_typed_decl() { + Map ages = {"alice": 30}; + assert_eq(ages.length(), 1); +} + +fn test_map_typed_empty() { + Map config = Map(); + assert_eq(config.is_empty(), true); +} diff --git a/typechecker/check_expression.cpp b/typechecker/check_expression.cpp index 46c0bc7..ebdd91f 100644 --- a/typechecker/check_expression.cpp +++ b/typechecker/check_expression.cpp @@ -87,6 +87,14 @@ TypeInfo infer_type(TypeCheckerState& state, const ASTNode& expr) { return check_list_literal(state, *list); } + if (auto* map = dynamic_cast(&expr)) { + return check_map_create(state, *map); + } + + if (auto* map = dynamic_cast(&expr)) { + return check_map_literal(state, *map); + } + if (auto* pair = dynamic_cast(&expr)) { return check_pair_create(state, *pair); } diff --git a/typechecker/check_field.cpp b/typechecker/check_field.cpp index 9f23f6c..ab05149 100644 --- a/typechecker/check_field.cpp +++ b/typechecker/check_field.cpp @@ -4,6 +4,7 @@ */ #include "typechecker.hpp" +#include "common/type_utils.hpp" using namespace std; @@ -56,6 +57,19 @@ TypeInfo check_field_access(TypeCheckerState& state, const FieldAccess& access) return check_pair_field(state, access, element_type); } + // Handle MapItem field access (key, value) + if (struct_type.rfind("MapItem<", 0) == 0) { + auto [key_type, value_type] = bishop::extract_map_types( + "Map<" + struct_type.substr(8)); // Convert MapItem to Map for extraction + + if (key_type.empty() || value_type.empty()) { + error(state, "malformed MapItem type '" + struct_type + "'", access.line); + return {"unknown", false, false}; + } + + return check_map_item_field(state, access, key_type, value_type); + } + // Handle err type field access (message, cause, root_cause) if (struct_type == "err") { if (access.field_name == "message") { diff --git a/typechecker/check_map.cpp b/typechecker/check_map.cpp new file mode 100644 index 0000000..99fe39d --- /dev/null +++ b/typechecker/check_map.cpp @@ -0,0 +1,137 @@ +/** + * @file check_map.cpp + * @brief Map type inference for the Bishop type checker. + */ + +#include "typechecker.hpp" +#include "maps.hpp" + +using namespace std; + +namespace typechecker { + +/** + * Infers the type of a map creation expression. + */ +TypeInfo check_map_create(TypeCheckerState& state, const MapCreate& map) { + (void)state; + return {"Map<" + map.key_type + ", " + map.value_type + ">", false, false}; +} + +/** + * Infers the type of a map literal. + */ +TypeInfo check_map_literal(TypeCheckerState& state, const MapLiteral& map) { + if (map.entries.empty()) { + error(state, "cannot infer type of empty map literal, use Map() instead", map.line); + return {"unknown", false, false}; + } + + // Infer types from the first entry + TypeInfo first_key_type = infer_type(state, *map.entries[0].first); + TypeInfo first_value_type = infer_type(state, *map.entries[0].second); + + // Check all entries have consistent types + for (size_t i = 1; i < map.entries.size(); i++) { + TypeInfo key_type = infer_type(state, *map.entries[i].first); + TypeInfo value_type = infer_type(state, *map.entries[i].second); + + if (key_type.base_type != first_key_type.base_type) { + error(state, "map literal has mixed key types: '" + format_type(first_key_type) + + "' and '" + format_type(key_type) + "'", map.line); + } + + if (value_type.base_type != first_value_type.base_type) { + error(state, "map literal has mixed value types: '" + format_type(first_value_type) + + "' and '" + format_type(value_type) + "'", map.line); + } + } + + return {"Map<" + first_key_type.base_type + ", " + first_value_type.base_type + ">", false, false}; +} + +/** + * Type checks a method call on a map. + */ +TypeInfo check_map_method(TypeCheckerState& state, const MethodCall& mcall, + const string& key_type, const string& value_type) { + auto method_info = bishop::get_map_method_info(mcall.method_name); + + if (!method_info) { + error(state, "Map has no method '" + mcall.method_name + "'", mcall.line); + return {"unknown", false, false}; + } + + const auto& [param_types, return_type] = *method_info; + + if (mcall.args.size() != param_types.size()) { + error(state, "method '" + mcall.method_name + "' expects " + + to_string(param_types.size()) + " arguments, got " + + to_string(mcall.args.size()), mcall.line); + } + + // Check argument types + for (size_t i = 0; i < mcall.args.size() && i < param_types.size(); i++) { + TypeInfo arg_type = infer_type(state, *mcall.args[i]); + string expected_str = param_types[i]; + + // Replace K and V with actual types + if (expected_str == "K") { + expected_str = key_type; + } else if (expected_str == "V") { + expected_str = value_type; + } + + TypeInfo expected_type = {expected_str, false, false}; + + if (!types_compatible(expected_type, arg_type)) { + error(state, "argument " + to_string(i + 1) + " of method '" + + mcall.method_name + "' expects '" + expected_str + + "', got '" + format_type(arg_type) + "'", mcall.line); + } + } + + // Compute return type + string ret = return_type; + + // Replace K and V placeholders + if (ret == "K") { + ret = key_type; + } else if (ret == "V") { + ret = value_type; + } else if (ret == "V?") { + // Optional value type + return {value_type, true, false}; + } else if (ret == "List") { + ret = "List<" + key_type + ">"; + } else if (ret == "List") { + ret = "List<" + value_type + ">"; + } else if (ret == "List>") { + ret = "List>"; + } + + if (ret == "void") { + return {"void", false, true}; + } + + return {ret, false, false}; +} + +/** + * Type checks field access on a MapItem. + */ +TypeInfo check_map_item_field(TypeCheckerState& state, const FieldAccess& access, + const string& key_type, const string& value_type) { + if (access.field_name == "key") { + return {key_type, false, false}; + } + + if (access.field_name == "value") { + return {value_type, false, false}; + } + + error(state, "MapItem has no field '" + access.field_name + "'", access.line); + return {"unknown", false, false}; +} + +} // namespace typechecker diff --git a/typechecker/check_method_call.cpp b/typechecker/check_method_call.cpp index 8c66e89..578e909 100644 --- a/typechecker/check_method_call.cpp +++ b/typechecker/check_method_call.cpp @@ -5,6 +5,7 @@ #include "typechecker.hpp" #include "strings.hpp" +#include "common/type_utils.hpp" using namespace std; @@ -236,6 +237,17 @@ TypeInfo check_method_call(TypeCheckerState& state, const MethodCall& mcall) { return check_list_method(state, mcall, element_type); } + if (effective_type.base_type.rfind("Map<", 0) == 0) { + auto [key_type, value_type] = bishop::extract_map_types(effective_type.base_type); + + if (key_type.empty() || value_type.empty()) { + error(state, "malformed Map type '" + effective_type.base_type + "'", mcall.line); + return {"unknown", false, false}; + } + + return check_map_method(state, mcall, key_type, value_type); + } + if (effective_type.base_type.rfind("Pair<", 0) == 0) { string element_type = extract_element_type(effective_type.base_type, "Pair<"); diff --git a/typechecker/maps.cpp b/typechecker/maps.cpp new file mode 100644 index 0000000..b214c05 --- /dev/null +++ b/typechecker/maps.cpp @@ -0,0 +1,159 @@ +/** + * @file maps.cpp + * @brief Map method type definitions for the Bishop type checker. + * + * Defines type signatures for all built-in Map methods. + * Uses "K" as a placeholder for the key type and "V" for the value type, + * which are substituted with the actual types at type check time. + */ + +/** + * @bishop_method length + * @type Map + * @description Returns the number of entries in the map. + * @returns int - The map length + * @example + * ages := {"alice": 30, "bob": 25}; + * len := ages.length(); // 2 + */ + +/** + * @bishop_method is_empty + * @type Map + * @description Returns true if the map has no entries. + * @returns bool - True if empty, false otherwise + * @example + * ages := Map(); + * if ages.is_empty() { + * print("Map is empty"); + * } + */ + +/** + * @bishop_method contains + * @type Map + * @description Checks if the map contains the given key. + * @param key K - The key to search for + * @returns bool - True if found, false otherwise + * @example + * ages := {"alice": 30, "bob": 25}; + * if ages.contains("alice") { + * print("Found alice!"); + * } + */ + +/** + * @bishop_method get + * @type Map + * @description Returns the value for the given key, or none if not found. + * @param key K - The key to look up + * @returns V? - The value (optional) + * @example + * ages := {"alice": 30, "bob": 25}; + * age := ages.get("alice") default 0; // 30 + */ + +/** + * @bishop_method set + * @type Map + * @description Sets the value for the given key. + * @param key K - The key + * @param value V - The value to store + * @example + * ages := Map(); + * ages.set("alice", 30); + */ + +/** + * @bishop_method remove + * @type Map + * @description Removes the entry for the given key. + * @param key K - The key to remove + * @example + * ages := {"alice": 30, "bob": 25}; + * ages.remove("bob"); // ages now only has alice + */ + +/** + * @bishop_method clear + * @type Map + * @description Removes all entries from the map. + * @example + * ages := {"alice": 30, "bob": 25}; + * ages.clear(); // ages is now empty + */ + +/** + * @bishop_method keys + * @type Map + * @description Returns a list of all keys in the map. + * @returns List - List of keys + * @example + * ages := {"alice": 30, "bob": 25}; + * for key in ages.keys() { + * print(key); + * } + */ + +/** + * @bishop_method values + * @type Map + * @description Returns a list of all values in the map. + * @returns List - List of values + * @example + * ages := {"alice": 30, "bob": 25}; + * for val in ages.values() { + * print(val); + * } + */ + +/** + * @bishop_method items + * @type Map + * @description Returns a list of all key-value pairs in the map. + * @returns List> - List of entries with .key and .value fields + * @example + * ages := {"alice": 30, "bob": 25}; + * for item in ages.items() { + * print(item.key + ": " + str(item.value)); + * } + */ + +#include "maps.hpp" + +#include + +namespace bishop { + +std::optional get_map_method_info(const std::string& method_name) { + // "K" is placeholder for key type, "V" for value type + static const std::map map_methods = { + // Query methods + {"length", {{}, "int"}}, + {"is_empty", {{}, "bool"}}, + {"contains", {{"K"}, "bool"}}, + + // Access methods - get returns optional + {"get", {{"K"}, "V?"}}, + + // Modification methods + {"set", {{"K", "V"}, "void"}}, + {"remove", {{"K"}, "void"}}, + {"clear", {{}, "void"}}, + + // Iteration methods + {"keys", {{}, "List"}}, + {"values", {{}, "List"}}, + {"items", {{}, "List>"}}, + }; + + auto it = map_methods.find(method_name); + + if (it != map_methods.end()) { + return it->second; + } + + return std::nullopt; +} + +} // namespace bishop diff --git a/typechecker/maps.hpp b/typechecker/maps.hpp new file mode 100644 index 0000000..1b46274 --- /dev/null +++ b/typechecker/maps.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace bishop { + +/** + * Represents a map method signature with parameter types and return type. + * Uses "K" as a placeholder for the key type and "V" for the value type. + */ +struct MapMethodInfo { + std::vector param_types; + std::string return_type; +}; + +/** + * Returns type information for built-in Map methods. + * Returns nullopt if the method is not found. + */ +std::optional get_map_method_info(const std::string& method_name); + +} // namespace bishop diff --git a/typechecker/typechecker.hpp b/typechecker/typechecker.hpp index 02201e2..701373a 100644 --- a/typechecker/typechecker.hpp +++ b/typechecker/typechecker.hpp @@ -182,6 +182,12 @@ TypeInfo check_list_create(TypeCheckerState& state, const ListCreate& list); TypeInfo check_list_literal(TypeCheckerState& state, const ListLiteral& list); TypeInfo check_list_method(TypeCheckerState& state, const MethodCall& mcall, const std::string& element_type); +// Map type inference (check_map.cpp) +TypeInfo check_map_create(TypeCheckerState& state, const MapCreate& map); +TypeInfo check_map_literal(TypeCheckerState& state, const MapLiteral& map); +TypeInfo check_map_method(TypeCheckerState& state, const MethodCall& mcall, const std::string& key_type, const std::string& value_type); +TypeInfo check_map_item_field(TypeCheckerState& state, const FieldAccess& access, const std::string& key_type, const std::string& value_type); + // Pair type inference (check_pair.cpp) TypeInfo check_pair_create(TypeCheckerState& state, const PairCreate& pair); TypeInfo check_pair_method(TypeCheckerState& state, const MethodCall& mcall, const std::string& element_type); From 298918a226c82cbdce15e2033794f5710f8baf5b Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 03:01:42 +0000 Subject: [PATCH 2/6] Fix Map typed variable declaration parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add handler for Map typed variable declarations in parse_statement.cpp. This was missing, causing parse errors like "unexpected token ','" when parsing syntax like: Map ages = {"alice": 30}; The handler follows the same pattern as List, Pair, Tuple, and Channel but handles two type parameters separated by a comma. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- parser/parse_statement.cpp | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/parser/parse_statement.cpp b/parser/parse_statement.cpp index 1fd9b1f..e7766ba 100644 --- a/parser/parse_statement.cpp +++ b/parser/parse_statement.cpp @@ -231,6 +231,31 @@ unique_ptr parse_statement(ParserState& state) { return decl; } + // Map variable declaration: Map ages = {"alice": 30}; or Map> x = ...; + if (check(state, TokenType::MAP)) { + int start_line = current(state).line; + advance(state); + consume(state, TokenType::LT); + + // Parse key type (supports nested generics) + string key_type = parse_type(state); + consume(state, TokenType::COMMA); + + // Parse value type (supports nested generics) + string value_type = parse_type(state); + + consume(state, TokenType::GT); + + auto decl = make_unique(); + decl->type = "Map<" + key_type + ", " + value_type + ">"; + decl->line = start_line; + decl->name = consume(state, TokenType::IDENT).value; + consume(state, TokenType::ASSIGN); + decl->value = parse_expression(state); + consume(state, TokenType::SEMICOLON); + return decl; + } + // NOT expression statement: !expr or fail "msg"; !valid or continue; etc. if (check(state, TokenType::NOT)) { auto expr = parse_expression(state); From 5c7dfe886bb9e181810b9e499c9770ea7577d66e Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 03:06:06 +0000 Subject: [PATCH 3/6] Add Map type validation in typechecker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The is_valid_type() function was missing validation for Map types, causing type errors when declaring typed Map variables like: Map ages = {"alice": 30}; This adds the missing check that extracts and validates both key and value types using the existing bishop::extract_map_types() utility. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- typechecker/typechecker.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/typechecker/typechecker.cpp b/typechecker/typechecker.cpp index 4a63d0a..3fa1f79 100644 --- a/typechecker/typechecker.cpp +++ b/typechecker/typechecker.cpp @@ -267,6 +267,16 @@ bool is_valid_type(const TypeCheckerState& state, const string& type) { return is_valid_type(state, element_type); } + if (type.rfind("Map<", 0) == 0 && type.back() == '>') { + auto [key_type, value_type] = bishop::extract_map_types(type); + + if (key_type.empty() || value_type.empty()) { + return false; + } + + return is_valid_type(state, key_type) && is_valid_type(state, value_type); + } + // Pointer type: StructName* -> check that base is a valid struct if (!type.empty() && type.back() == '*') { string pointee = type.substr(0, type.length() - 1); From 280089b2bca0b926abf0735f710636350e5ee1e9 Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 03:10:02 +0000 Subject: [PATCH 4/6] Fix map literal codegen to use std::make_pair for CTAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous map literal codegen produced: std::unordered_map{{key, value}, {key, value}} This caused C++ class template argument deduction (CTAD) to fail because the compiler couldn't deduce the template parameters from the nested brace initializers. Changed to use std::make_pair() which provides explicit type info: std::unordered_map{std::make_pair(key, value), std::make_pair(key, value)} 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- codegen/emit_map.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/codegen/emit_map.cpp b/codegen/emit_map.cpp index 4e1ac2c..d57a4cc 100644 --- a/codegen/emit_map.cpp +++ b/codegen/emit_map.cpp @@ -20,7 +20,10 @@ string emit_map_create(const MapCreate& map) { } /** - * Emits a map literal: {"key": value, ...} -> std::unordered_map{{"key", value}, ...}. + * Emits a map literal: {"key": value, ...} -> std::unordered_map{std::make_pair(key, value), ...}. + * + * Uses std::make_pair for each entry to help class template argument deduction (CTAD) + * correctly infer the map types. */ string emit_map_literal(CodeGenState& state, const MapLiteral& map) { string entries; @@ -32,7 +35,7 @@ string emit_map_literal(CodeGenState& state, const MapLiteral& map) { string key = emit(state, *map.entries[i].first); string value = emit(state, *map.entries[i].second); - entries += "{" + key + ", " + value + "}"; + entries += "std::make_pair(" + key + ", " + value + ")"; } return "std::unordered_map{" + entries + "}"; From 7f5f5452c1f2b8db6e5bc5d22a7828312161163c Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 03:14:12 +0000 Subject: [PATCH 5/6] Add std::optional support to or_value() in runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The or_value() function is used by the `default` expression handler to unwrap values. It was correctly handling Result types by calling .value(), but for std::optional types (used by Map.get()), it was returning the optional as-is instead of unwrapping it. This adds: - Type trait detail::is_optional_v to detect std::optional types - Explicit handling in or_value() to call .value() on optionals This enables map.get("key") default fallback to work correctly: age := ages.get("alice") default 0; // Now returns int, not optional 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- runtime/std/std.hpp | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/runtime/std/std.hpp b/runtime/std/std.hpp index 9d5c96b..dd5b88a 100644 --- a/runtime/std/std.hpp +++ b/runtime/std/std.hpp @@ -210,6 +210,16 @@ namespace detail { template inline constexpr bool is_result_v = is_result::value; + + // Type trait to detect std::optional types + template + struct is_optional : std::false_type {}; + + template + struct is_optional> : std::true_type {}; + + template + inline constexpr bool is_optional_v = is_optional>::value; } /** @@ -231,16 +241,19 @@ inline bool is_or_falsy(const T& value) { } /** - * Get the actual value from either a Result or falsy type. + * Get the actual value from either a Result, optional, or falsy type. * Used by 'or' handlers to extract the value when condition passes. * * For Result types: returns .value() - * For falsy types: returns the value as-is + * For std::optional types: returns .value() (unwraps the optional) + * For other falsy types: returns the value as-is */ template inline decltype(auto) or_value(const T& value) { if constexpr (detail::is_result_v>) { return value.value(); + } else if constexpr (detail::is_optional_v) { + return value.value(); } else { return value; } @@ -250,6 +263,8 @@ template inline decltype(auto) or_value(T& value) { if constexpr (detail::is_result_v>) { return value.value(); + } else if constexpr (detail::is_optional_v) { + return value.value(); } else { return value; } From 24b61778c811fca6a27bcf603740dbc91d9b7b10 Mon Sep 17 00:00:00 2001 From: "clank-hayen[bot]" Date: Fri, 2 Jan 2026 04:49:26 +0000 Subject: [PATCH 6/6] Fix map literal parsing order after merge conflict resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move map literal parsing before set literal parsing so that {"key": value} syntax is correctly parsed as MapLiteral instead of SetLiteral. The merge conflict resolution accidentally placed the map literal check after the set literal check, causing map literals to be incorrectly parsed as set literals (since both start with LBRACE). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- parser/parse_primary.cpp | 57 ++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/parser/parse_primary.cpp b/parser/parse_primary.cpp index ff04f2d..bf1af1c 100644 --- a/parser/parse_primary.cpp +++ b/parser/parse_primary.cpp @@ -263,6 +263,35 @@ unique_ptr parse_primary(ParserState& state) { return set; } + // Handle map literal: {"key": value, ...} + // Must be checked BEFORE set literal since both start with LBRACE + // Distinguished from struct literal by having STRING : at start instead of IDENT : + if (check(state, TokenType::LBRACE) && check_ahead(state, 1, TokenType::STRING) && check_ahead(state, 2, TokenType::COLON)) { + int start_line = current(state).line; + advance(state); // consume '{' + + auto map = make_unique(); + map->line = start_line; + + while (!check(state, TokenType::RBRACE) && !check(state, TokenType::EOF_TOKEN)) { + // Parse key expression + auto key = parse_expression(state); + consume(state, TokenType::COLON); + + // Parse value expression + auto value = parse_expression(state); + + map->entries.push_back({move(key), move(value)}); + + if (check(state, TokenType::COMMA)) { + advance(state); + } + } + + consume(state, TokenType::RBRACE); + return map; + } + // Handle set literal: {expr, expr, ...} // Distinguished from struct literal by not being preceded by a type name if (check(state, TokenType::LBRACE)) { @@ -320,34 +349,6 @@ unique_ptr parse_primary(ParserState& state) { return list; } - // Handle map literal: {"key": value, ...} - // Distinguished from struct literal by having STRING : at start instead of IDENT : - if (check(state, TokenType::LBRACE) && check_ahead(state, 1, TokenType::STRING) && check_ahead(state, 2, TokenType::COLON)) { - int start_line = current(state).line; - advance(state); // consume '{' - - auto map = make_unique(); - map->line = start_line; - - while (!check(state, TokenType::RBRACE) && !check(state, TokenType::EOF_TOKEN)) { - // Parse key expression - auto key = parse_expression(state); - consume(state, TokenType::COLON); - - // Parse value expression - auto value = parse_expression(state); - - map->entries.push_back({move(key), move(value)}); - - if (check(state, TokenType::COMMA)) { - advance(state); - } - } - - consume(state, TokenType::RBRACE); - return map; - } - if (check(state, TokenType::NUMBER)) { Token tok = current(state); advance(state);