mirror of
https://github.com/ChaiScript/ChaiScript.git
synced 2026-05-01 03:19:28 +08:00
Fix #19: Add strongly-typed enum support to ChaiScript
Adds the ability to define enums inside ChaiScript with syntax:
enum Color { Red, Green, Blue }
enum Priority { Low = 10, Medium = 20, High = 30 }
Enum values are strongly typed Dynamic_Objects accessed via :: syntax
(e.g. Color::Red). A validating constructor from int is registered that
rejects values outside the defined range. Functions declared with an enum
parameter type (e.g. def fun(Color val)) correctly reject plain integers,
enforcing type safety at the dispatch level.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
362e93fb29
commit
20ed7e3410
@ -33,7 +33,7 @@ namespace chaiscript {
|
||||
template<typename T>
|
||||
static bool is_reserved_word(const T &s) noexcept {
|
||||
const static std::unordered_set<std::uint32_t>
|
||||
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;
|
||||
}
|
||||
@ -106,7 +106,9 @@ namespace chaiscript {
|
||||
Constant,
|
||||
Compiled,
|
||||
Const_Var_Decl,
|
||||
Const_Assign_Decl
|
||||
Const_Assign_Decl,
|
||||
Enum,
|
||||
Enum_Access
|
||||
};
|
||||
|
||||
enum class Operator_Precedence {
|
||||
@ -127,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"};
|
||||
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", "Enum_Access"};
|
||||
|
||||
return ast_node_types[static_cast<int>(ast_node_type)];
|
||||
}
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <ostream>
|
||||
#include <set>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@ -887,6 +888,88 @@ namespace chaiscript {
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct Enum_AST_Node final : AST_Node_Impl<T> {
|
||||
Enum_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
|
||||
: AST_Node_Impl<T>(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;
|
||||
|
||||
std::set<int> valid_values;
|
||||
|
||||
for (size_t i = 1; i < this->children.size(); i += 2) {
|
||||
const auto &val_name = this->children[i]->text;
|
||||
const int val_int = Boxed_Number(this->children[i + 1]->eval(t_ss)).get_as<int>();
|
||||
valid_values.insert(val_int);
|
||||
|
||||
dispatch::Dynamic_Object dobj(enum_name);
|
||||
dobj.get_attr("value") = Boxed_Value(val_int);
|
||||
dobj.set_explicit(true);
|
||||
t_ss->add_global_const(const_var(dobj), enum_name + "::" + val_name);
|
||||
}
|
||||
|
||||
auto shared_valid = std::make_shared<const std::set<int>>(std::move(valid_values));
|
||||
|
||||
t_ss->add(
|
||||
std::make_shared<dispatch::detail::Dynamic_Object_Constructor>(
|
||||
enum_name,
|
||||
fun([shared_valid, enum_name](dispatch::Dynamic_Object &t_obj, int t_val) {
|
||||
if (shared_valid->count(t_val) == 0) {
|
||||
throw exception::eval_error("Value " + std::to_string(t_val) + " is not valid for enum '" + enum_name + "'");
|
||||
}
|
||||
t_obj.get_attr("value") = Boxed_Value(t_val);
|
||||
t_obj.set_explicit(true);
|
||||
})),
|
||||
enum_name);
|
||||
|
||||
t_ss->add(
|
||||
std::make_shared<dispatch::detail::Dynamic_Object_Function>(
|
||||
enum_name,
|
||||
fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) {
|
||||
return Boxed_Number(lhs.get_attr("value")).get_as<int>() == Boxed_Number(rhs.get_attr("value")).get_as<int>();
|
||||
})),
|
||||
"==");
|
||||
|
||||
t_ss->add(
|
||||
std::make_shared<dispatch::detail::Dynamic_Object_Function>(
|
||||
enum_name,
|
||||
fun([](const dispatch::Dynamic_Object &lhs, const dispatch::Dynamic_Object &rhs) {
|
||||
return Boxed_Number(lhs.get_attr("value")).get_as<int>() != Boxed_Number(rhs.get_attr("value")).get_as<int>();
|
||||
})),
|
||||
"!=");
|
||||
|
||||
t_ss->add(
|
||||
std::make_shared<dispatch::detail::Dynamic_Object_Function>(
|
||||
enum_name,
|
||||
fun([](const dispatch::Dynamic_Object &obj) { return Boxed_Number(obj.get_attr("value")).get_as<int>(); })),
|
||||
"to_int");
|
||||
|
||||
return void_var();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct Enum_Access_AST_Node final : AST_Node_Impl<T> {
|
||||
Enum_Access_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
|
||||
: AST_Node_Impl<T>(std::move(t_ast_node_text), AST_Node_Type::Enum_Access, std::move(t_loc), std::move(t_children))
|
||||
, m_key(this->children[0]->text + "::" + this->children[1]->text) {
|
||||
}
|
||||
|
||||
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
|
||||
try {
|
||||
return t_ss.get_object(m_key, m_loc);
|
||||
} catch (std::exception &) {
|
||||
throw exception::eval_error("Can not find enum value: " + m_key);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string m_key;
|
||||
mutable std::atomic_uint_fast32_t m_loc = {0};
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
struct If_AST_Node final : AST_Node_Impl<T> {
|
||||
If_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)
|
||||
|
||||
@ -2031,6 +2031,77 @@ 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 (!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 name in definition", File_Position(m_position.line, m_position.col), *m_filename);
|
||||
}
|
||||
|
||||
if (!Char('{')) {
|
||||
throw exception::eval_error("Expected '{' after enum name", 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<int>(std::stoi(m_match_stack.back()->text));
|
||||
m_match_stack.pop_back();
|
||||
}
|
||||
|
||||
m_match_stack.push_back(
|
||||
make_node<eval::Constant_AST_Node<Tracer>>(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 definition",
|
||||
File_Position(m_position.line, m_position.col),
|
||||
*m_filename);
|
||||
}
|
||||
}
|
||||
|
||||
build_match<eval::Enum_AST_Node<Tracer>>(prev_stack_top);
|
||||
}
|
||||
|
||||
return retval;
|
||||
}
|
||||
|
||||
/// Reads a while block from input
|
||||
bool While() {
|
||||
Depth_Counter dc{this};
|
||||
@ -2379,6 +2450,16 @@ namespace chaiscript {
|
||||
}
|
||||
|
||||
build_match<eval::Array_Call_AST_Node<Tracer>>(prev_stack_top);
|
||||
} else if (!m_match_stack.empty() && m_match_stack.back()->identifier == AST_Node_Type::Id && Symbol("::")) {
|
||||
has_more = true;
|
||||
if (!Id(true)) {
|
||||
throw exception::eval_error("Expected identifier after '::'", File_Position(m_position.line, m_position.col), *m_filename);
|
||||
}
|
||||
|
||||
if (std::distance(m_match_stack.begin() + static_cast<int>(prev_stack_top), m_match_stack.end()) != 2) {
|
||||
throw exception::eval_error("Incomplete enum access", File_Position(m_position.line, m_position.col), *m_filename);
|
||||
}
|
||||
build_match<eval::Enum_Access_AST_Node<Tracer>>(prev_stack_top);
|
||||
} else if (Symbol(".")) {
|
||||
has_more = true;
|
||||
if (!(Id(true))) {
|
||||
@ -2776,7 +2857,7 @@ namespace chaiscript {
|
||||
|
||||
while (has_more) {
|
||||
const auto start = m_position;
|
||||
if (Def() || Try() || If() || While() || Class(t_class_allowed) || For() || Switch()) {
|
||||
if (Def() || Try() || If() || While() || 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),
|
||||
|
||||
59
unittests/enum.chai
Normal file
59
unittests/enum.chai
Normal file
@ -0,0 +1,59 @@
|
||||
// Basic enum definition
|
||||
enum 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)
|
||||
|
||||
// Construction from valid int
|
||||
auto c = Color(1)
|
||||
assert_true(c == Color::Green)
|
||||
|
||||
// Construction from invalid int throws
|
||||
try {
|
||||
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(2))
|
||||
|
||||
// Cannot pass int where Color is expected
|
||||
try {
|
||||
takes_color(52)
|
||||
assert_true(false)
|
||||
} catch(e) {
|
||||
// expected: dispatch error
|
||||
}
|
||||
|
||||
// to_int accessor
|
||||
assert_equal(0, Color::Red.to_int())
|
||||
assert_equal(1, Color::Green.to_int())
|
||||
assert_equal(2, Color::Blue.to_int())
|
||||
|
||||
// Enum with explicit values
|
||||
enum Priority { Low = 10, Medium = 20, High = 30 }
|
||||
assert_equal(10, Priority::Low.to_int())
|
||||
assert_equal(20, Priority::Medium.to_int())
|
||||
assert_equal(30, Priority::High.to_int())
|
||||
|
||||
auto p = Priority(20)
|
||||
assert_true(p == Priority::Medium)
|
||||
|
||||
// Mixed auto and explicit values
|
||||
enum Status { Pending, Active = 5, Done }
|
||||
assert_equal(0, Status::Pending.to_int())
|
||||
assert_equal(5, Status::Active.to_int())
|
||||
assert_equal(6, Status::Done.to_int())
|
||||
Loading…
x
Reference in New Issue
Block a user