diff --git a/cheatsheet.md b/cheatsheet.md index 41b703f8..d2fb216b 100644 --- a/cheatsheet.md +++ b/cheatsheet.md @@ -665,6 +665,124 @@ copy.width = 99 print(original.width) // still 10 ``` +## Enums + +ChaiScript supports strongly-typed enums using `enum class` (or equivalently `enum struct`), +matching C++ scoped-enum semantics. Values are accessed via `::` syntax and are type-safe — +a plain integer cannot be passed where an enum type is expected. + +### Basic Definition + +``` +enum class Color { Red, Green, Blue } +``` + +Values are auto-numbered starting from 0. Access them with `Color::Red`, `Color::Green`, etc. + +### Explicit Values + +``` +enum class Priority { Low = 10, Medium = 20, High = 30 } +``` + +Auto-numbering continues from the last explicit value: + +``` +enum class Status { Pending, Active = 5, Done } +// Pending = 0, Active = 5, Done = 6 +``` + +### Specifying an Underlying Type + +By default the underlying type is `int`. Use `: type` to choose a different numeric type: + +``` +enum class Flags : char { Read = 1, Write = 2, Execute = 4 } +``` + +The underlying type must be a numeric type registered in ChaiScript. `string` and other +non-numeric types cannot be used. The available underlying types are: + +| Type | Description | +|------|-------------| +| `int` | (default) signed integer | +| `unsigned_int` | unsigned integer | +| `long` | signed long | +| `unsigned_long` | unsigned long | +| `long_long` | signed long long | +| `unsigned_long_long` | unsigned long long | +| `char` | character (8-bit) | +| `wchar_t` | wide character | +| `char16_t` | 16-bit character | +| `char32_t` | 32-bit character | +| `float` | single-precision float | +| `double` | double-precision float | +| `long_double` | extended-precision float | +| `size_t` | unsigned size type | +| `int8_t` | signed 8-bit | +| `int16_t` | signed 16-bit | +| `int32_t` | signed 32-bit | +| `int64_t` | signed 64-bit | +| `uint8_t` | unsigned 8-bit | +| `uint16_t` | unsigned 16-bit | +| `uint32_t` | unsigned 32-bit | + +### `enum struct` Syntax + +`enum struct` is accepted as a synonym for `enum class`, just like in C++: + +``` +enum struct Direction { North, East, South, West } +``` + +### Constructing from a Value + +Each enum type has a constructor that accepts the underlying type. It validates that the +value matches one of the defined enumerators: + +``` +auto c = Color::Color(1) // creates Color::Green +Color::Color(52) // throws: invalid value +``` + +### `to_underlying` + +Convert an enum value back to its underlying numeric type: + +``` +Color::Red.to_underlying() // 0 +Priority::High.to_underlying() // 30 +``` + +### Comparison + +`==` and `!=` are defined for values of the same enum type: + +``` +assert_true(Color::Red == Color::Red) +assert_true(Color::Red != Color::Green) +``` + +### Type-Safe Dispatch + +Functions declared with an enum parameter type reject plain integers: + +``` +def handle(Color c) { /* ... */ } +handle(Color::Red) // ok +handle(42) // throws: dispatch error +``` + +### Using with `switch` + +``` +switch(Color::Green) { + case (Color::Red) { print("red"); break } + case (Color::Green) { print("green"); break } + case (Color::Blue) { print("blue"); break } +} +``` + ## Dynamic Objects All ChaiScript defined types and generic Dynamic_Object support dynamic parameters diff --git a/grammar/chaiscript.ebnf b/grammar/chaiscript.ebnf index 8016f920..d95fe92b 100644 --- a/grammar/chaiscript.ebnf +++ b/grammar/chaiscript.ebnf @@ -18,8 +18,8 @@ /* ---- Top-level ---- */ -statements ::= ( def | try | if | while | class | for - | switch | return | break | continue +statements ::= ( def | try | if | while | class | enum + | for | switch | return | break | continue | equation | block | eol )+ /* ---- Functions ---- */ @@ -57,6 +57,17 @@ class ::= "class" id ( ":" id )? eol* class_block class_block ::= "{" class_statements* "}" class_statements ::= def | var_decl | eol +/* ---- Enums ---- */ + +enum ::= "enum" ( "class" | "struct" ) id ( ":" underlying_type )? + "{" enum_entries? "}" + +enum_entries ::= enum_entry ( "," enum_entry )* + +enum_entry ::= id ( "=" integer )? + +underlying_type ::= id + /* ---- Blocks & flow keywords ---- */ block ::= "{" statements* "}" diff --git a/include/chaiscript/language/chaiscript_common.hpp b/include/chaiscript/language/chaiscript_common.hpp index 9fca319c..36383731 100644 --- a/include/chaiscript/language/chaiscript_common.hpp +++ b/include/chaiscript/language/chaiscript_common.hpp @@ -33,7 +33,7 @@ namespace chaiscript { template static bool is_reserved_word(const T &s) noexcept { const static std::unordered_set - words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const")}; + words{utility::hash("def"), utility::hash("fun"), utility::hash("while"), utility::hash("for"), utility::hash("if"), utility::hash("else"), utility::hash("&&"), utility::hash("||"), utility::hash(","), utility::hash("auto"), utility::hash("return"), utility::hash("break"), utility::hash("true"), utility::hash("false"), utility::hash("class"), utility::hash("attr"), utility::hash("var"), utility::hash("global"), utility::hash("GLOBAL"), utility::hash("_"), utility::hash("__LINE__"), utility::hash("__FILE__"), utility::hash("__FUNC__"), utility::hash("__CLASS__"), utility::hash("const"), utility::hash("enum")}; return words.count(utility::hash(s)) == 1; } @@ -107,6 +107,7 @@ namespace chaiscript { Compiled, Const_Var_Decl, Const_Assign_Decl, + Enum, Namespace_Block }; @@ -128,7 +129,7 @@ namespace chaiscript { namespace { /// Helper lookup to get the name of each node type constexpr const char *ast_node_type_to_string(AST_Node_Type ast_node_type) noexcept { - constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Namespace_Block"}; + constexpr const char *const ast_node_types[] = {"Id", "Fun_Call", "Unused_Return_Fun_Call", "Arg_List", "Equation", "Var_Decl", "Assign_Decl", "Array_Call", "Dot_Access", "Lambda", "Block", "Scopeless_Block", "Def", "While", "If", "For", "Ranged_For", "Inline_Array", "Inline_Map", "Return", "File", "Prefix", "Break", "Continue", "Map_Pair", "Value_Range", "Inline_Range", "Try", "Catch", "Finally", "Method", "Attr_Decl", "Logical_And", "Logical_Or", "Reference", "Switch", "Case", "Default", "Noop", "Class", "Binary", "Arg", "Global_Decl", "Constant", "Compiled", "Const_Var_Decl", "Const_Assign_Decl", "Enum", "Namespace_Block"}; return ast_node_types[static_cast(ast_node_type)]; } diff --git a/include/chaiscript/language/chaiscript_eval.hpp b/include/chaiscript/language/chaiscript_eval.hpp index 6a9f3ca6..970cdd3a 100644 --- a/include/chaiscript/language/chaiscript_eval.hpp +++ b/include/chaiscript/language/chaiscript_eval.hpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -890,6 +891,75 @@ namespace chaiscript { } }; + template + struct Enum_AST_Node final : AST_Node_Impl { + Enum_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) + : AST_Node_Impl(std::move(t_ast_node_text), AST_Node_Type::Enum, std::move(t_loc), std::move(t_children)) { + } + + Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override { + const auto &enum_name = this->children[0]->text; + const auto &underlying_type_name = this->children[1]->text; + const auto underlying_ti = t_ss->get_type(underlying_type_name); + + dispatch::Dynamic_Object container(enum_name); + std::vector valid_values; + + for (size_t i = 2; i < this->children.size(); i += 2) { + const auto &val_name = this->children[i]->text; + const auto val_bv = Boxed_Number(this->children[i + 1]->eval(t_ss)).get_as(underlying_ti).bv; + valid_values.push_back(val_bv); + + dispatch::Dynamic_Object dobj(enum_name); + dobj.get_attr("value") = val_bv; + dobj.set_explicit(true); + container[val_name] = const_var(dobj); + } + + auto shared_valid = std::make_shared>(std::move(valid_values)); + + container[enum_name] = var( + fun([shared_valid, enum_name, underlying_ti](const Boxed_Number &t_val) -> Boxed_Value { + const auto converted = t_val.get_as(underlying_ti); + for (const auto &v : *shared_valid) { + if (Boxed_Number::equals(Boxed_Number(v), converted)) { + dispatch::Dynamic_Object dobj(enum_name); + dobj.get_attr("value") = converted.bv; + dobj.set_explicit(true); + return const_var(dobj); + } + } + throw exception::eval_error("Value is not valid for enum '" + enum_name + "'"); + })); + + t_ss->add_global_const(const_var(container), enum_name); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) { + return Boxed_Number::equals(Boxed_Number(lhs.get_attr("value")), Boxed_Number(rhs.get_attr("value"))); + })), + "=="); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) { + return !Boxed_Number::equals(Boxed_Number(lhs.get_attr("value")), Boxed_Number(rhs.get_attr("value"))); + })), + "!="); + + t_ss->add( + std::make_shared( + enum_name, + fun([](const dispatch::Dynamic_Object &obj) { return obj.get_attr("value"); })), + "to_underlying"); + + return void_var(); + } + }; + template struct Namespace_Block_AST_Node final : AST_Node_Impl { Namespace_Block_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector> t_children) diff --git a/include/chaiscript/language/chaiscript_parser.hpp b/include/chaiscript/language/chaiscript_parser.hpp index 91871ca2..0af66d4e 100644 --- a/include/chaiscript/language/chaiscript_parser.hpp +++ b/include/chaiscript/language/chaiscript_parser.hpp @@ -2069,6 +2069,97 @@ namespace chaiscript { return retval; } + bool Enum(const bool t_allowed) { + Depth_Counter dc{this}; + bool retval = false; + + const auto prev_stack_top = m_match_stack.size(); + + if (Keyword("enum")) { + if (!Keyword("class") && !Keyword("struct")) { + throw exception::eval_error("Expected 'class' or 'struct' after 'enum' (only 'enum class'/'enum struct' is supported)", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + if (!t_allowed) { + throw exception::eval_error("Enum definitions only allowed at top scope", + File_Position(m_position.line, m_position.col), + *m_filename); + } + + retval = true; + + if (!Id(true)) { + throw exception::eval_error("Missing enum class name in definition", File_Position(m_position.line, m_position.col), *m_filename); + } + + std::string underlying_type = "int"; + if (Char(':')) { + if (!Id(false)) { + throw exception::eval_error("Expected underlying type after ':'", + File_Position(m_position.line, m_position.col), + *m_filename); + } + underlying_type = m_match_stack.back()->text; + m_match_stack.pop_back(); + } + + m_match_stack.push_back( + make_node>(underlying_type, m_position.line, m_position.col, const_var(underlying_type))); + + if (!Char('{')) { + throw exception::eval_error("Expected '{' after enum class declaration", File_Position(m_position.line, m_position.col), *m_filename); + } + + int next_value = 0; + + while (Eol()) { + } + + if (!Char('}')) { + do { + while (Eol()) { + } + + if (!Id(true)) { + throw exception::eval_error("Expected enum value name", File_Position(m_position.line, m_position.col), *m_filename); + } + + if (Symbol("=")) { + if (!Num()) { + throw exception::eval_error("Expected integer after '=' in enum definition", + File_Position(m_position.line, m_position.col), + *m_filename); + } + next_value = static_cast(std::stoi(m_match_stack.back()->text)); + m_match_stack.pop_back(); + } + + m_match_stack.push_back( + make_node>(std::to_string(next_value), m_position.line, m_position.col, const_var(next_value))); + ++next_value; + + while (Eol()) { + } + } while (Char(',') && !Char('}')); + + while (Eol()) { + } + + if (!Char('}')) { + throw exception::eval_error("Expected '}' to close enum class definition", + File_Position(m_position.line, m_position.col), + *m_filename); + } + } + + build_match>(prev_stack_top); + } + + return retval; + } + /// Reads a while block from input bool While() { Depth_Counter dc{this}; @@ -2814,7 +2905,7 @@ namespace chaiscript { while (has_more) { const auto start = m_position; - if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || For() || Switch()) { + if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Enum(t_class_allowed) || For() || Switch()) { if (!saw_eol) { throw exception::eval_error("Two function definitions missing line separator", File_Position(start.line, start.col), diff --git a/unittests/enum.chai b/unittests/enum.chai new file mode 100644 index 00000000..1821af7a --- /dev/null +++ b/unittests/enum.chai @@ -0,0 +1,110 @@ +// Basic enum class definition (default underlying type: int) +enum class Color { Red, Green, Blue } + +// Access via :: syntax +auto r = Color::Red +auto g = Color::Green +auto b = Color::Blue + +// Equality and inequality +assert_true(Color::Red == Color::Red) +assert_false(Color::Red == Color::Green) +assert_true(Color::Red != Color::Green) +assert_false(Color::Red != Color::Red) + +// Constructor from valid underlying value +auto c = Color::Color(1) +assert_true(c == Color::Green) + +// Constructor from invalid value throws +try { + Color::Color(52) + assert_true(false) +} catch(e) { + // expected +} + +// Strong typing: function with typed parameter +def takes_color(Color val) { val } +takes_color(Color::Red) +takes_color(Color::Green) +takes_color(Color::Color(2)) + +// Cannot pass int where Color is expected +try { + takes_color(52) + assert_true(false) +} catch(e) { + // expected: dispatch error +} + +// to_underlying accessor +assert_equal(0, Color::Red.to_underlying()) +assert_equal(1, Color::Green.to_underlying()) +assert_equal(2, Color::Blue.to_underlying()) + +// Enum class with explicit values +enum class Priority { Low = 10, Medium = 20, High = 30 } +assert_equal(10, Priority::Low.to_underlying()) +assert_equal(20, Priority::Medium.to_underlying()) +assert_equal(30, Priority::High.to_underlying()) + +auto p = Priority::Priority(20) +assert_true(p == Priority::Medium) + +// Mixed auto and explicit values +enum class Status { Pending, Active = 5, Done } +assert_equal(0, Status::Pending.to_underlying()) +assert_equal(5, Status::Active.to_underlying()) +assert_equal(6, Status::Done.to_underlying()) + +// Switch on enum values +var result = "" +switch(Color::Green) { + case (Color::Red) { + result = "red" + break + } + case (Color::Green) { + result = "green" + break + } + case (Color::Blue) { + result = "blue" + break + } +} +assert_equal("green", result) + +// Switch on enum with explicit values +var prio_result = "" +switch(Priority::High) { + case (Priority::Low) { + prio_result = "low" + break + } + case (Priority::Medium) { + prio_result = "medium" + break + } + case (Priority::High) { + prio_result = "high" + break + } +} +assert_equal("high", prio_result) + +// Enum class with explicit underlying type +enum class Flags : char { Read = 1, Write = 2, Execute = 4 } +assert_equal(1, Flags::Read.to_underlying()) +assert_equal(2, Flags::Write.to_underlying()) +assert_equal(4, Flags::Execute.to_underlying()) + +auto f = Flags::Flags(2) +assert_true(f == Flags::Write) + +// enum struct syntax (equivalent to enum class, like C++) +enum struct Direction { North, East, South, West } +assert_equal(0, Direction::North.to_underlying()) +assert_equal(3, Direction::West.to_underlying()) +assert_true(Direction::East != Direction::South)