Fix #17: Add const variables in ChaiScript (#643)

* Fix #17: Add const local variable support to ChaiScript

Adds `const var`, `const auto`, and `const` as variable declaration
syntax that creates immutable local variables. A const_override flag
on Boxed_Value enables script-level constness without changing the
C++ type system integration. The parser, optimizer, and evaluator
are extended with Const_Var_Decl and Const_Assign_Decl AST nodes
that mirror their non-const counterparts but mark the value as const
after initialization.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review: remove const_override, set const flag directly on Type_Info

Replace the m_const_override bool on Boxed_Value::Data with a
Type_Info::make_const() method that sets the const bit in m_flags
directly. This ensures constness is visible everywhere consistently,
including places that check get_type_info().is_const() directly.

Requested by @lefticus in PR #643 review.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: leftibot <leftibot@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
leftibot 2026-04-10 22:12:13 -06:00 committed by GitHub
parent 7d3c29085d
commit fc574c320b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 90 additions and 3 deletions

View File

@ -157,6 +157,12 @@ namespace chaiscript {
bool is_const() const noexcept { return m_data->m_type_info.is_const(); }
/// Mark this Boxed_Value as const (used for script-level const declarations)
void make_const() noexcept {
m_data->m_type_info.make_const();
m_data->m_data_ptr = nullptr;
}
bool is_type(const Type_Info &ti) const noexcept { return m_data->m_type_info.bare_equal(ti); }
template<typename T>

View File

@ -85,6 +85,8 @@ namespace chaiscript {
constexpr const std::type_info *bare_type_info() const noexcept { return m_bare_type_info; }
void make_const() noexcept { m_flags |= (1 << is_const_flag); }
private:
struct Unknown_Type {
};

View File

@ -37,7 +37,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__")};
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")};
return words.count(utility::hash(s)) == 1;
}
@ -108,7 +108,9 @@ namespace chaiscript {
Arg,
Global_Decl,
Constant,
Compiled
Compiled,
Const_Var_Decl,
Const_Assign_Decl
};
enum class Operator_Precedence {
@ -129,7 +131,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"};
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"};
return ast_node_types[static_cast<int>(ast_node_type)];
}

View File

@ -550,6 +550,41 @@ namespace chaiscript {
mutable std::atomic_uint_fast32_t m_loc = {0};
};
template<typename T>
struct Const_Var_Decl_AST_Node final : AST_Node_Impl<T> {
Const_Var_Decl_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::Const_Var_Decl, std::move(t_loc), std::move(t_children)) {
}
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &) const override {
throw exception::eval_error("const variables must be initialized at declaration");
}
};
template<typename T>
struct Const_Assign_Decl_AST_Node final : AST_Node_Impl<T> {
Const_Assign_Decl_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::Const_Assign_Decl, std::move(t_loc), std::move(t_children)) {
}
Boxed_Value eval_internal(const chaiscript::detail::Dispatch_State &t_ss) const override {
const std::string &idname = this->children[0]->text;
try {
Boxed_Value bv(detail::clone_if_necessary(this->children[1]->eval(t_ss), m_loc, t_ss));
bv.reset_return_value();
bv.make_const();
t_ss.add_object(idname, bv);
return bv;
} catch (const exception::name_conflict_error &e) {
throw exception::eval_error("Variable redefined '" + e.name() + "'");
}
}
private:
mutable std::atomic_uint_fast32_t m_loc = {0};
};
template<typename T>
struct Array_Call_AST_Node final : AST_Node_Impl<T> {
Array_Call_AST_Node(std::string t_ast_node_text, Parse_Location t_loc, std::vector<AST_Node_Impl_Ptr<T>> t_children)

View File

@ -89,6 +89,7 @@ namespace chaiscript {
template<typename T>
bool contains_var_decl_in_scope(const eval::AST_Node_Impl<T> &node) noexcept {
if (node.identifier == AST_Node_Type::Var_Decl || node.identifier == AST_Node_Type::Assign_Decl
|| node.identifier == AST_Node_Type::Const_Var_Decl || node.identifier == AST_Node_Type::Const_Assign_Decl
|| node.identifier == AST_Node_Type::Reference) {
return true;
}
@ -202,6 +203,14 @@ namespace chaiscript {
return chaiscript::make_unique<eval::AST_Node_Impl<T>, eval::Assign_Decl_AST_Node<T>>(node->text,
node->location,
std::move(new_children));
} else if ((node->identifier == AST_Node_Type::Equation) && node->text == "=" && node->children.size() == 2
&& node->children[0]->identifier == AST_Node_Type::Const_Var_Decl) {
std::vector<eval::AST_Node_Impl_Ptr<T>> new_children;
new_children.push_back(std::move(node->children[0]->children[0]));
new_children.push_back(std::move(node->children[1]));
return chaiscript::make_unique<eval::AST_Node_Impl<T>, eval::Const_Assign_Decl_AST_Node<T>>(node->text,
node->location,
std::move(new_children));
}
return node;

View File

@ -2434,6 +2434,18 @@ namespace chaiscript {
throw exception::eval_error("Incomplete variable declaration", File_Position(m_position.line, m_position.col), *m_filename);
}
} else if (Keyword("const")) {
retval = true;
// consume optional 'var' or 'auto' after 'const'
Keyword("var") || Keyword("auto");
if (Id(true)) {
build_match<eval::Const_Var_Decl_AST_Node<Tracer>>(prev_stack_top);
} else {
throw exception::eval_error("Incomplete const variable declaration", File_Position(m_position.line, m_position.col), *m_filename);
}
} else if (Keyword("global")) {
retval = true;

21
unittests/const_var.chai Normal file
View File

@ -0,0 +1,21 @@
// Test const variable declarations
const var x = 5
assert_equal(5, x)
// Test that const variables cannot be reassigned
assert_throws("Error: \"Error, cannot assign to constant value.\"", fun() { const var y = 10; y = 20 })
// Test const with auto keyword
const auto z = "hello"
assert_equal("hello", z)
// Test that const auto variables cannot be reassigned
assert_throws("Error: \"Error, cannot assign to constant value.\"", fun() { const auto w = 3; w = 4 })
// Test is_var_const on const var
const var c = 42
assert_true(c.is_var_const())
// Test that non-const var is not const
var nc = 42
assert_false(nc.is_var_const())