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:
leftibot 2026-04-13 17:45:47 -06:00
parent 362e93fb29
commit 20ed7e3410
4 changed files with 229 additions and 4 deletions

View File

@ -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)];
}

View File

@ -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)

View File

@ -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
View 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())