diff --git a/.zed/debug.json b/.zed/debug.json new file mode 100644 index 0000000..72e67ce --- /dev/null +++ b/.zed/debug.json @@ -0,0 +1,17 @@ +// Project-local debug tasks +// +// For more documentation on how to configure debug tasks, +// see: https://zed.dev/docs/debugger +[ + { + "label": "Debug Test", + "build": { + "command": "make", + "args": ["-j8"], + "cwd": "$ZED_WORKTREE_ROOT/build", + }, + "program": "$ZED_WORKTREE_ROOT/build/camellya_tests", + "request": "launch", + "adapter": "GDB", + }, +] diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c7176f..2ce498c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,9 @@ if(CAMELLYA_BUILD_TESTS) tests/test_basic.cpp tests/test_utf8.cpp tests/test_vm.cpp + tests/test_list.cpp + tests/test_map_builtin.cpp + tests/test_generic_builtin.cpp ) target_include_directories(camellya_tests diff --git a/src/camellya.cpp b/src/camellya.cpp index 2c1ae8e..9b9521a 100644 --- a/src/camellya.cpp +++ b/src/camellya.cpp @@ -71,10 +71,13 @@ bool Camellya::do_file(const std::string &filename) { } void Camellya::register_function(const std::string &name, NativeFunction func) { - auto func_value = std::make_shared(name, func); d->vm->register_native_function(name, func); } +void Camellya::register_builtin_class(const std::string &type_name, std::shared_ptr klass) { + d->vm->register_builtin_class(type_name, klass); +} + int Camellya::get_top() const { return static_cast(d->stack.size()); } ValuePtr Camellya::get_global(const std::string &name) { return d->vm->get_global(name); diff --git a/src/camellya.h b/src/camellya.h index 000e52e..43ec679 100644 --- a/src/camellya.h +++ b/src/camellya.h @@ -23,6 +23,9 @@ public: // Register native function void register_function(const std::string &name, NativeFunction func); + // Register built-in class for any type (including user native types) + void register_builtin_class(const std::string &type_name, std::shared_ptr klass); + // Get global variable ValuePtr get_global(const std::string &name); diff --git a/src/value.cpp b/src/value.cpp index 42e29e4..f69027d 100644 --- a/src/value.cpp +++ b/src/value.cpp @@ -1,124 +1,125 @@ #include "value.h" -#include -#include #include +#include +#include namespace camellya { std::string NumberValue::to_string() const { - if (std::floor(value) == value) { - return std::to_string(static_cast(value)); - } - std::ostringstream oss; - oss << std::fixed << std::setprecision(6) << value; - std::string str = oss.str(); - str.erase(str.find_last_not_of('0') + 1, std::string::npos); - if (str.back() == '.') str.pop_back(); - return str; + if (std::floor(value) == value) { + return std::to_string(static_cast(value)); + } + std::ostringstream oss; + oss << std::fixed << std::setprecision(6) << value; + std::string str = oss.str(); + str.erase(str.find_last_not_of('0') + 1, std::string::npos); + if (str.back() == '.') + str.pop_back(); + return str; } std::string ListValue::to_string() const { - std::ostringstream oss; - oss << "["; - for (size_t i = 0; i < elements.size(); ++i) { - if (i > 0) oss << ", "; - oss << elements[i]->to_string(); - } - oss << "]"; - return oss.str(); + std::ostringstream oss; + oss << "["; + for (size_t i = 0; i < elements.size(); ++i) { + if (i > 0) + oss << ", "; + oss << elements[i]->to_string(); + } + oss << "]"; + return oss.str(); } ValuePtr ListValue::clone() const { - std::vector cloned_elements; - cloned_elements.reserve(elements.size()); - for (const auto& elem : elements) { - cloned_elements.push_back(elem->clone()); - } - return std::make_shared(std::move(cloned_elements)); + std::vector cloned_elements; + cloned_elements.reserve(elements.size()); + for (const auto &elem : elements) { + cloned_elements.push_back(elem->clone()); + } + return std::make_shared(std::move(cloned_elements)); } ValuePtr ListValue::get(size_t index) const { - if (index >= elements.size()) { - return std::make_shared(); - } - return elements[index]; + if (index >= elements.size()) { + return std::make_shared(); + } + return elements[index]; } void ListValue::set(size_t index, ValuePtr value) { - if (index >= elements.size()) { - elements.resize(index + 1, std::make_shared()); - } - elements[index] = std::move(value); + if (index >= elements.size()) { + elements.resize(index + 1, std::make_shared()); + } + elements[index] = std::move(value); } std::string MapValue::to_string() const { - std::ostringstream oss; - oss << "{"; - bool first = true; - for (const auto& [key, value] : pairs) { - if (!first) oss << ", "; - first = false; - oss << key << ": " << value->to_string(); - } - oss << "}"; - return oss.str(); + std::ostringstream oss; + oss << "{"; + bool first = true; + for (const auto &[key, value] : pairs) { + if (!first) + oss << ", "; + first = false; + oss << key << ": " << value->to_string(); + } + oss << "}"; + return oss.str(); } ValuePtr MapValue::clone() const { - std::map cloned_pairs; - for (const auto& [key, value] : pairs) { - cloned_pairs[key] = value->clone(); - } - return std::make_shared(std::move(cloned_pairs)); + std::map cloned_pairs; + for (const auto &[key, value] : pairs) { + cloned_pairs[key] = value->clone(); + } + return std::make_shared(std::move(cloned_pairs)); } -ValuePtr MapValue::get(const std::string& key) const { - auto it = pairs.find(key); - if (it == pairs.end()) { - return std::make_shared(); - } - return it->second; +ValuePtr MapValue::get(const std::string &key) const { + auto it = pairs.find(key); + if (it == pairs.end()) { + return std::make_shared(); + } + return it->second; } ValuePtr ClassValue::clone() const { - auto cloned = std::make_shared(name); - cloned->fields = fields; - cloned->methods = methods; - return cloned; + auto cloned = std::make_shared(name); + cloned->fields = fields; + cloned->methods = methods; + return cloned; } ValuePtr InstanceValue::clone() const { - auto cloned = std::make_shared(klass); - for (const auto& [key, value] : fields) { - cloned->fields[key] = value->clone(); - } - return cloned; + auto cloned = std::make_shared(klass); + for (const auto &[key, value] : fields) { + cloned->fields[key] = value->clone(); + } + return cloned; } -ValuePtr InstanceValue::get(const std::string& name) const { - // First check fields - auto field_it = fields.find(name); - if (field_it != fields.end()) { - return field_it->second; - } - - // Then check methods and bind 'this' - auto method_it = klass->methods.find(name); - if (method_it != klass->methods.end()) { - auto bound = std::make_shared(*method_it->second); - auto self = std::static_pointer_cast( - const_cast(this)->shared_from_this()); - bound->bound_instance = self; - return bound; - } - - return std::make_shared(); +ValuePtr InstanceValue::get(const std::string &name) const { + // First check fields + auto field_it = fields.find(name); + if (field_it != fields.end()) { + return field_it->second; + } + + // Then check methods and bind 'this' + auto method_it = klass->methods.find(name); + if (method_it != klass->methods.end()) { + auto bound = std::make_shared(*method_it->second); + bound->bound_instance = const_cast(this)->shared_from_this(); + return bound; + } + + return std::make_shared(); } -void InstanceValue::set(const std::string& name, ValuePtr value) { - if (fields.find(name) != fields.end()) { - fields[name] = std::move(value); - } +void InstanceValue::set(const std::string &name, ValuePtr value) { + if (fields.find(name) != fields.end()) { + fields[name] = std::move(value); + } } } // namespace camellya diff --git a/src/value.h b/src/value.h index 55ea2f6..48d22b6 100644 --- a/src/value.h +++ b/src/value.h @@ -24,13 +24,15 @@ enum class Type { MAP, FUNCTION, CLASS, - INSTANCE + INSTANCE, + NATIVE }; -class Value { +class Value : public std::enable_shared_from_this { public: virtual ~Value() = default; virtual Type type() const = 0; + virtual std::string type_name() const = 0; virtual std::string to_string() const = 0; virtual ValuePtr clone() const = 0; }; @@ -38,6 +40,7 @@ public: class NilValue : public Value { public: Type type() const override { return Type::NIL; } + std::string type_name() const override { return "nil"; } std::string to_string() const override { return "nil"; } ValuePtr clone() const override { return std::make_shared(); } }; @@ -48,6 +51,7 @@ public: explicit NumberValue(double value) : value(value) {} Type type() const override { return Type::NUMBER; } + std::string type_name() const override { return "number"; } std::string to_string() const override; ValuePtr clone() const override { return std::make_shared(value); } }; @@ -58,6 +62,7 @@ public: explicit StringValue(std::string value) : value(std::move(value)) {} Type type() const override { return Type::STRING; } + std::string type_name() const override { return "string"; } std::string to_string() const override { return value; } ValuePtr clone() const override { return std::make_shared(value); } }; @@ -68,6 +73,7 @@ public: explicit BoolValue(bool value) : value(value) {} Type type() const override { return Type::BOOL; } + std::string type_name() const override { return "bool"; } std::string to_string() const override { return value ? "true" : "false"; } ValuePtr clone() const override { return std::make_shared(value); } }; @@ -80,6 +86,7 @@ public: explicit ListValue(std::vector elements) : elements(std::move(elements)) {} Type type() const override { return Type::LIST; } + std::string type_name() const override { return "list"; } std::string to_string() const override; ValuePtr clone() const override; @@ -97,6 +104,7 @@ public: explicit MapValue(std::map pairs) : pairs(std::move(pairs)) {} Type type() const override { return Type::MAP; } + std::string type_name() const override { return "map"; } std::string to_string() const override; ValuePtr clone() const override; @@ -120,12 +128,12 @@ public: std::shared_ptr chunk; NativeFunction native_func; bool is_native; - std::shared_ptr bound_instance; + ValuePtr bound_instance; // Script function FunctionValue(std::string name, std::shared_ptr declaration, std::shared_ptr chunk = nullptr, - std::shared_ptr bound_instance = nullptr) + ValuePtr bound_instance = nullptr) : name(std::move(name)), declaration(std::move(declaration)), chunk(std::move(chunk)), is_native(false), bound_instance(std::move(bound_instance)) {} @@ -135,6 +143,7 @@ public: : name(std::move(name)), native_func(std::move(func)), is_native(true) {} Type type() const override { return Type::FUNCTION; } + std::string type_name() const override { return "function"; } std::string to_string() const override { return ""; } ValuePtr clone() const override { return std::make_shared(*this); } }; @@ -148,6 +157,7 @@ public: explicit ClassValue(std::string name) : name(std::move(name)) {} Type type() const override { return Type::CLASS; } + std::string type_name() const override { return "class"; } std::string to_string() const override { return ""; } ValuePtr clone() const override; @@ -160,7 +170,7 @@ public: } }; -class InstanceValue : public Value, public std::enable_shared_from_this { +class InstanceValue : public Value { public: std::shared_ptr klass; std::map fields; @@ -173,6 +183,7 @@ public: } Type type() const override { return Type::INSTANCE; } + std::string type_name() const override { return klass->name; } std::string to_string() const override { return "name + ">"; } ValuePtr clone() const override; @@ -180,6 +191,18 @@ public: void set(const std::string& name, ValuePtr value); }; +class NativeValue : public Value { +public: + std::string _type_name; + + explicit NativeValue(std::string type_name) : _type_name(std::move(type_name)) {} + + Type type() const override { return Type::NATIVE; } + std::string type_name() const override { return _type_name; } + std::string to_string() const override { return ""; } + ValuePtr clone() const override { return std::make_shared(_type_name); } +}; + // Helper functions inline bool is_truthy(const ValuePtr& value) { if (!value || value->type() == Type::NIL) return false; diff --git a/src/vm.cpp b/src/vm.cpp index a9f1908..45b61b4 100644 --- a/src/vm.cpp +++ b/src/vm.cpp @@ -1,5 +1,4 @@ #include "vm.h" -#include "interpreter.h" #include #include #include @@ -212,6 +211,9 @@ bool VM::run() { if (func->is_native) { // Native function call std::vector args; + if (func->bound_instance) { + args.push_back(func->bound_instance); + } for (int i = arg_count - 1; i >= 0; i--) { args.push_back(peek(i)); } @@ -424,7 +426,18 @@ bool VM::run() { auto instance = std::dynamic_pointer_cast(object); push(instance->get(name)); } else { - runtime_error("Only instances have properties."); + auto it = builtin_classes.find(object->type_name()); + if (it != builtin_classes.end()) { + auto klass = it->second; + auto method_it = klass->methods.find(name); + if (method_it != klass->methods.end()) { + auto bound = std::make_shared(*method_it->second); + bound->bound_instance = object; + push(bound); + break; + } + } + runtime_error("Only instances and types with registered built-in classes have properties/methods."); return false; } break; @@ -613,6 +626,67 @@ void VM::register_builtin_functions() { throw RuntimeError("len() expects list, string, or map."); }); globals["len"] = len_func; + + // Define list class + auto list_klass = std::make_shared("list"); + list_klass->add_method("push", std::make_shared("push", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 2) throw RuntimeError("list.push() expects 1 argument."); + auto list = std::dynamic_pointer_cast(args[0]); + list->push(args[1]); + return args[1]; + })); + list_klass->add_method("pop", std::make_shared("pop", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 1) throw RuntimeError("list.pop() expects 0 arguments."); + auto list = std::dynamic_pointer_cast(args[0]); + if (list->elements.empty()) return std::make_shared(); + ValuePtr val = list->elements.back(); + list->elements.pop_back(); + return val; + })); + list_klass->add_method("len", std::make_shared("len", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 1) throw RuntimeError("list.len() expects 0 arguments."); + auto list = std::dynamic_pointer_cast(args[0]); + return std::make_shared(static_cast(list->size())); + })); + register_builtin_class("list", list_klass); + + // Define map class + auto map_klass = std::make_shared("map"); + map_klass->add_method("set", std::make_shared("set", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 3) throw RuntimeError("map.set() expects 2 arguments."); + auto map = std::dynamic_pointer_cast(args[0]); + if (args[1]->type() != Type::STRING) throw RuntimeError("Map key must be a string."); + std::string key = std::dynamic_pointer_cast(args[1])->value; + map->set(key, args[2]); + return args[2]; + })); + map_klass->add_method("get", std::make_shared("get", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 2) throw RuntimeError("map.get() expects 1 argument."); + auto map = std::dynamic_pointer_cast(args[0]); + if (args[1]->type() != Type::STRING) throw RuntimeError("Map key must be a string."); + std::string key = std::dynamic_pointer_cast(args[1])->value; + return map->get(key); + })); + map_klass->add_method("has", std::make_shared("has", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 2) throw RuntimeError("map.has() expects 1 argument."); + auto map = std::dynamic_pointer_cast(args[0]); + if (args[1]->type() != Type::STRING) throw RuntimeError("Map key must be a string."); + std::string key = std::dynamic_pointer_cast(args[1])->value; + return std::make_shared(map->has(key)); + })); + map_klass->add_method("len", std::make_shared("len", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 1) throw RuntimeError("map.len() expects 0 arguments."); + auto map = std::dynamic_pointer_cast(args[0]); + return std::make_shared(static_cast(map->pairs.size())); + })); + register_builtin_class("map", map_klass); } void VM::register_native_function(const std::string& name, NativeFunction func) { @@ -620,6 +694,11 @@ void VM::register_native_function(const std::string& name, NativeFunction func) globals[name] = func_value; } +void VM::register_builtin_class(const std::string& type_name, std::shared_ptr klass) { + builtin_classes[type_name] = klass; + globals[klass->name] = klass; +} + void VM::set_global(const std::string& name, ValuePtr value) { globals[name] = value; } diff --git a/src/vm.h b/src/vm.h index 891f04f..23a1793 100644 --- a/src/vm.h +++ b/src/vm.h @@ -34,6 +34,9 @@ public: // Register native functions void register_native_function(const std::string& name, NativeFunction func); + // Register built-in classes for types (like list, map) + void register_builtin_class(const std::string& type_name, std::shared_ptr klass); + // Global variable access void set_global(const std::string& name, ValuePtr value); ValuePtr get_global(const std::string& name); @@ -48,6 +51,7 @@ private: std::vector frames; CallFrame* current_frame; std::map globals; + std::map> builtin_classes; std::string error_message; // Main execution loop diff --git a/tests/test_generic_builtin.cpp b/tests/test_generic_builtin.cpp new file mode 100644 index 0000000..c7352e3 --- /dev/null +++ b/tests/test_generic_builtin.cpp @@ -0,0 +1,59 @@ +#include "camellya.h" +#include "exceptions.h" +#include +#include + +using namespace camellya; + +// Simple Vector3D wrapper for testing +class Vector3DValue : public NativeValue { +public: + double x, y, z; + Vector3DValue(double x, double y, double z) : NativeValue("Vector3D"), x(x), y(y), z(z) {} + + std::string to_string() const override { + return "Vector3D(" + std::to_string(x) + ", " + std::to_string(y) + ", " + std::to_string(z) + ")"; + } + + ValuePtr clone() const override { + return std::make_shared(x, y, z); + } +}; + +TEST_CASE("Generic built-in class (Vector3D) test", "[generic]") { + Camellya c; + + // 1. Create the ClassValue for Vector3D + auto v3d_klass = std::make_shared("Vector3D"); + + // 2. Add a native method 'length' + v3d_klass->add_method("length", std::make_shared("length", + [](const std::vector& args) -> ValuePtr { + if (args.size() != 1) throw RuntimeError("Vector3D.length() expects 0 arguments."); + auto vec = std::dynamic_pointer_cast(args[0]); + double len = std::sqrt(vec->x * vec->x + vec->y * vec->y + vec->z * vec->z); + return std::make_shared(len); + })); + + // 3. Register it as a built-in class for type "Vector3D" + c.register_builtin_class("Vector3D", v3d_klass); + + // 4. Register a factory function to create Vector3D instances from script + c.register_function("Vector3D", [](const std::vector& args) -> ValuePtr { + if (args.size() != 3) throw RuntimeError("Vector3D() expects 3 arguments."); + double x = std::dynamic_pointer_cast(args[0])->value; + double y = std::dynamic_pointer_cast(args[1])->value; + double z = std::dynamic_pointer_cast(args[2])->value; + return std::make_shared(x, y, z); + }); + + SECTION("vector3d methods") { + REQUIRE(c.do_string(R"( + var v = Vector3D(3, 4, 0); + var len = v.length(); + )")); + + auto len = c.get_global("len"); + REQUIRE(std::dynamic_pointer_cast(len)->value == 5.0); + } +} diff --git a/tests/test_list.cpp b/tests/test_list.cpp new file mode 100644 index 0000000..084af77 --- /dev/null +++ b/tests/test_list.cpp @@ -0,0 +1,18 @@ +#include "camellya.h" +#include + +using namespace camellya; +TEST_CASE("Basic List test", "[list]") { + Camellya c; + + SECTION("len function") { + REQUIRE(c.do_string(R"( + var arr = [1, 2, 3, 4]; + var size = len(arr); + arr.push(5); + size = arr.len(); + )")); + auto size = c.get_global("size"); + REQUIRE(std::dynamic_pointer_cast(size)->value == 5.0); + } +} diff --git a/tests/test_map_builtin.cpp b/tests/test_map_builtin.cpp new file mode 100644 index 0000000..d937b69 --- /dev/null +++ b/tests/test_map_builtin.cpp @@ -0,0 +1,32 @@ +#include "camellya.h" +#include + +using namespace camellya; + +TEST_CASE("Map built-in class test", "[map]") { + Camellya c; + + SECTION("map methods") { + REQUIRE(c.do_string(R"( + var m = {"a": 1, "b": 2}; + var s1 = m.len(); + m.set("c", 3); + var s2 = m.len(); + var has_a = m.has("a"); + var has_z = m.has("z"); + var val_b = m.get("b"); + )")); + + auto s1 = c.get_global("s1"); + auto s2 = c.get_global("s2"); + auto has_a = c.get_global("has_a"); + auto has_z = c.get_global("has_z"); + auto val_b = c.get_global("val_b"); + + REQUIRE(std::dynamic_pointer_cast(s1)->value == 2.0); + REQUIRE(std::dynamic_pointer_cast(s2)->value == 3.0); + REQUIRE(std::dynamic_pointer_cast(has_a)->value == true); + REQUIRE(std::dynamic_pointer_cast(has_z)->value == false); + REQUIRE(std::dynamic_pointer_cast(val_b)->value == 2.0); + } +}