Merge upstream/develop into fix/issue-677-add-strong-typedefs

Carefully resolved merge conflicts to preserve both the strong typedef
(using) feature from this branch and the enum support (#679), nested
namespaces (#552), grammar diagram (#628), and WASM exception (#678)
features from upstream develop. All 376 tests pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-14 22:16:11 -06:00
commit ebcbd23bf8
6 changed files with 407 additions and 5 deletions

View File

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

View File

@ -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,18 @@ 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* "}"

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"), utility::hash("using")};
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("using"), utility::hash("enum")};
return words.count(utility::hash(s)) == 1;
}
@ -108,6 +108,7 @@ namespace chaiscript {
Const_Var_Decl,
Const_Assign_Decl,
Using,
Enum,
Namespace_Block
};
@ -129,7 +130,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", "Using", "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", "Using", "Enum", "Namespace_Block"};
return ast_node_types[static_cast<int>(ast_node_type)];
}

View File

@ -16,6 +16,7 @@
#include <map>
#include <memory>
#include <ostream>
#include <algorithm>
#include <stdexcept>
#include <string>
#include <vector>
@ -1166,6 +1167,75 @@ 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;
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<Boxed_Value> 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<const std::vector<Boxed_Value>>(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<dispatch::detail::Dynamic_Object_Function>(
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<dispatch::detail::Dynamic_Object_Function>(
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<dispatch::detail::Dynamic_Object_Function>(
enum_name,
fun([](const dispatch::Dynamic_Object &obj) { return obj.get_attr("value"); })),
"to_underlying");
return void_var();
}
};
template<typename T>
struct Namespace_Block_AST_Node final : AST_Node_Impl<T> {
Namespace_Block_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)

View File

@ -2106,6 +2106,97 @@ namespace chaiscript {
return false;
}
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<eval::Constant_AST_Node<Tracer>>(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<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 class 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};
@ -2851,7 +2942,7 @@ namespace chaiscript {
while (has_more) {
const auto start = m_position;
if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Using(t_class_allowed) || For() || Switch()) {
if (Def() || Try() || If() || While() || Namespace_Block() || Class(t_class_allowed) || Using(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),

110
unittests/enum.chai Normal file
View File

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