diff --git a/CMakeLists.txt b/CMakeLists.txt index d3dc34d..15be90a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -90,11 +90,13 @@ 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/sets.cpp typechecker/check_pair.cpp typechecker/check_tuple.cpp + typechecker/check_map.cpp typechecker/check_set.cpp typechecker/check_lambda.cpp codegen/codegen.cpp @@ -123,6 +125,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 76693f9..e2d9c14 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 65a28d2..cdf9efa 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 806116d..5153e2b 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..d57a4cc --- /dev/null +++ b/codegen/emit_map.cpp @@ -0,0 +1,131 @@ +/** + * @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{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; + + 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 += "std::make_pair(" + 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 f77e11e..363b882 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 4d6841f..bab0805 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 a78dd6a..2a9502b 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}, {"Set", TokenType::SET}, diff --git a/lexer/token.hpp b/lexer/token.hpp index af2e08c..3ef12c9 100644 --- a/lexer/token.hpp +++ b/lexer/token.hpp @@ -159,6 +159,7 @@ enum class TokenType { PRIVATE, CHANNEL, LIST, + MAP, PAIR, TUPLE, SET, diff --git a/parser/ast.hpp b/parser/ast.hpp index aa0fd2b..9b0d348 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 ff84b94..bf1af1c 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; @@ -239,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)) { diff --git a/parser/parse_statement.cpp b/parser/parse_statement.cpp index 6b78249..288bf78 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; + } + // Set variable declaration: Set nums = {1, 2, 3}; or Set names = Set(); if (check(state, TokenType::SET)) { int start_line = current(state).line; diff --git a/parser/parse_type.cpp b/parser/parse_type.cpp index 525bace..3b83f52 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/runtime/std/std.hpp b/runtime/std/std.hpp index 2e0a62f..73c6da9 100644 --- a/runtime/std/std.hpp +++ b/runtime/std/std.hpp @@ -212,6 +212,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; } /** @@ -233,16 +243,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; } @@ -252,6 +265,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; } 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 0299330..2f13b6b 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 a995cfe..8c7a5ed 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.cpp b/typechecker/typechecker.cpp index a838797..0802da7 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); + } + if (type.rfind("Set<", 0) == 0 && type.back() == '>') { string element_type = extract_element_type(type, "Set<"); diff --git a/typechecker/typechecker.hpp b/typechecker/typechecker.hpp index ad4097f..946a031 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);