From f9a4d9c6f011ed30b379f0666c94dcbde716266d Mon Sep 17 00:00:00 2001 From: Roland Conybeare Date: Sat, 6 Jun 2026 22:14:57 -0400 Subject: [PATCH] squashed xo-object changes via xo-umbrella2 [GIT] --- xo-object/include/xo/object/Boolean.hpp | 62 ++ xo-object/include/xo/object/CVector.hpp | 45 ++ xo-object/include/xo/object/Collection.hpp | 23 + xo-object/include/xo/object/Float.hpp | 63 ++ xo-object/include/xo/object/Integer.hpp | 68 ++ xo-object/include/xo/object/List.hpp | 74 ++ xo-object/include/xo/object/Number.hpp | 22 + xo-object/include/xo/object/Numeric.hpp | 22 + .../include/xo/object/ObjectConversion.hpp | 30 + .../include/xo/object/ObjectConverter.hpp | 135 ++++ xo-object/include/xo/object/Primitive.hpp | 111 +++ xo-object/include/xo/object/Procedure.hpp | 37 + xo-object/include/xo/object/Scalar.hpp | 22 + xo-object/include/xo/object/Sequence.hpp | 22 + xo-object/include/xo/object/String.hpp | 171 +++++ xo-object/src/object/Boolean.cpp | 87 +++ xo-object/src/object/Float.cpp | 57 ++ xo-object/src/object/Integer.cpp | 59 ++ xo-object/src/object/List.cpp | 122 ++++ xo-object/src/object/ObjectConverter.cpp | 211 ++++++ xo-object/src/object/String.cpp | 181 +++++ xo-object/utest/Boolean.test.cpp | 63 ++ xo-object/utest/CMakeLists.txt | 15 + xo-object/utest/GC.test.cpp | 677 ++++++++++++++++++ xo-object/utest/Integer.test.cpp | 60 ++ xo-object/utest/List.test.cpp | 348 +++++++++ xo-object/utest/String.test.cpp | 236 ++++++ xo-object/utest/object_utest_main.cpp | 6 + 28 files changed, 3029 insertions(+) create mode 100644 xo-object/include/xo/object/Boolean.hpp create mode 100644 xo-object/include/xo/object/CVector.hpp create mode 100644 xo-object/include/xo/object/Collection.hpp create mode 100644 xo-object/include/xo/object/Float.hpp create mode 100644 xo-object/include/xo/object/Integer.hpp create mode 100644 xo-object/include/xo/object/List.hpp create mode 100644 xo-object/include/xo/object/Number.hpp create mode 100644 xo-object/include/xo/object/Numeric.hpp create mode 100644 xo-object/include/xo/object/ObjectConversion.hpp create mode 100644 xo-object/include/xo/object/ObjectConverter.hpp create mode 100644 xo-object/include/xo/object/Primitive.hpp create mode 100644 xo-object/include/xo/object/Procedure.hpp create mode 100644 xo-object/include/xo/object/Scalar.hpp create mode 100644 xo-object/include/xo/object/Sequence.hpp create mode 100644 xo-object/include/xo/object/String.hpp create mode 100644 xo-object/src/object/Boolean.cpp create mode 100644 xo-object/src/object/Float.cpp create mode 100644 xo-object/src/object/Integer.cpp create mode 100644 xo-object/src/object/List.cpp create mode 100644 xo-object/src/object/ObjectConverter.cpp create mode 100644 xo-object/src/object/String.cpp create mode 100644 xo-object/utest/Boolean.test.cpp create mode 100644 xo-object/utest/CMakeLists.txt create mode 100644 xo-object/utest/GC.test.cpp create mode 100644 xo-object/utest/Integer.test.cpp create mode 100644 xo-object/utest/List.test.cpp create mode 100644 xo-object/utest/String.test.cpp create mode 100644 xo-object/utest/object_utest_main.cpp diff --git a/xo-object/include/xo/object/Boolean.hpp b/xo-object/include/xo/object/Boolean.hpp new file mode 100644 index 00000000..354cc525 --- /dev/null +++ b/xo-object/include/xo/object/Boolean.hpp @@ -0,0 +1,62 @@ +/* @file Boolean.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "xo/alloc/Object.hpp" +#include "ObjectConversion.hpp" +#include "xo/indentlog/print/tag.hpp" + +namespace xo { + namespace obj { + /** @class Boolean + * @brief Boxed wrapper for a boolean value + **/ + class Boolean : public Object { + public: + /** @return instance representing boolean with truth-value @p x **/ + static gp boolean_obj(bool x); + static gp true_obj(); + static gp false_obj(); + static gp from(gp x) { return gp::from(x); } + + bool value() const { return value_; } + + // inherited from Object.. + + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * gc) const final override; + virtual std::size_t _forward_children(gc::IAlloc * gc) final override; + + private: + explicit Boolean(bool x) : value_{x} {} + + private: + bool value_; + }; + + template + struct ObjectConversion_Boolean { + static gp to_object(gc::IAlloc * /*mm*/, BoolType x) { + return Boolean::boolean_obj(x); + } + static BoolType from_object(gc::IAlloc *, gp x) { + gp x_bool = Boolean::from(x); + if (x_bool.get()) { + return x_bool->value(); + } else { + throw std::runtime_error(tostr("ObjectConversion_Boolean: x found where Boolean expected", xtag("x", x))); + } + } + }; + + template <> + struct ObjectConversion : public ObjectConversion_Boolean {}; + } +} + +/* end Boolean.hpp */ diff --git a/xo-object/include/xo/object/CVector.hpp b/xo-object/include/xo/object/CVector.hpp new file mode 100644 index 00000000..58828add --- /dev/null +++ b/xo-object/include/xo/object/CVector.hpp @@ -0,0 +1,45 @@ +/** @file CVector.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "xo/allocutil/IAlloc.hpp" +#include + +namespace xo { + namespace obj { + /** gc-only vector. + * Used in both LocalEnv and VsmStackFrame + **/ + template + class CVector { + public: + using value_type = ElementType; + + public: + CVector(gc::IAlloc * mm, std::size_t n) + : n_{n}, v_{nullptr} + { + if (n_ > 0) { + std::byte * mem = mm->alloc(n_ * sizeof(ElementType)); + this->v_ = new (mem) ElementType[n]; + } + } + + std::size_t size() const { return n_; } + + ElementType operator[](std::size_t i) const { return v_[i]; } + ElementType & operator[](std::size_t i) { return v_[i]; } + + public: + /** number of elements in @ref v_ **/ + std::size_t n_ = 0; + /** contiguous array of pointers **/ + ElementType * v_ = nullptr; + }; + } /*namespace obj*/ +} /*namespace xo*/ + +/* end CVector.hpp */ diff --git a/xo-object/include/xo/object/Collection.hpp b/xo-object/include/xo/object/Collection.hpp new file mode 100644 index 00000000..0d6216eb --- /dev/null +++ b/xo-object/include/xo/object/Collection.hpp @@ -0,0 +1,23 @@ +/* @file Collection.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "xo/alloc/Object.hpp" + +namespace xo { + namespace obj { + class Collection : public Object { + // inherited from Object.. + + //virtual std::size_t _shallow_size() const override; + //virtual Object * _shallow_copy() const override; + //virtual std::size_t _forward_children() override; + }; + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Collection.hpp */ diff --git a/xo-object/include/xo/object/Float.hpp b/xo-object/include/xo/object/Float.hpp new file mode 100644 index 00000000..00a63b7f --- /dev/null +++ b/xo-object/include/xo/object/Float.hpp @@ -0,0 +1,63 @@ +/** @file Float.hpp + * + * author: Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "Number.hpp" +#include "ObjectConversion.hpp" +#include "xo/indentlog/print/tag.hpp" + +namespace xo { + namespace obj { + class Float : public Number { + public: + using IAlloc = xo::gc::IAlloc; + using float_type = double; + + public: + Float() = default; + explicit Float(float_type x); + + /** create instance holding floating-point value @p x **/ + static gp make(IAlloc * mm, float_type x); + /** downcast from @p x iff x is actually a Float. Otherwise nullptr **/ + static gp from(gp x); + + float_type value() const { return value_; } + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * gc) const final override; + virtual std::size_t _forward_children(gc::IAlloc * gc) final override; + + private: + float_type value_ = 0.0; + }; + + template + struct ObjectConversion_Float { + static gp to_object(gc::IAlloc * mm, FloatType x) { + return new (MMPtr(mm)) Float(x); + } + static FloatType from_object(gc::IAlloc *, gp x) { + gp x_int = Float::from(x); + if (x_int.get()) { + return x_int->value(); + } else { + throw std::runtime_error(tostr("ObjectConversion_Float: x found where Float expected", xtag("x", x))); + } + } + }; + + template <> + struct ObjectConversion : public ObjectConversion_Float {}; + template <> + struct ObjectConversion : public ObjectConversion_Float {}; + } +} + +/* end Float.hpp */ diff --git a/xo-object/include/xo/object/Integer.hpp b/xo-object/include/xo/object/Integer.hpp new file mode 100644 index 00000000..ad88b0ad --- /dev/null +++ b/xo-object/include/xo/object/Integer.hpp @@ -0,0 +1,68 @@ +/* @file Integer.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "Number.hpp" +#include "ObjectConversion.hpp" +#include "xo/indentlog/print/tag.hpp" + +namespace xo { + namespace obj { + class Integer : public Number { + public: + using IAlloc = xo::gc::IAlloc; + using int_type = long long; + + public: + Integer() = default; + explicit Integer(int_type x); + + /** create instance holding integer value @p x **/ + static gp make(IAlloc * mm, int_type x); + /** downcast from @p x iff x is actually an Integer. Otherwise nullptr **/ + static gp from(gp x); + + int_type value() const { return value_; } + + void assign_value(int_type x) { value_ = x; } + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * gc) const final override; + virtual std::size_t _forward_children(gc::IAlloc * gc) final override; + + private: + int_type value_ = 0; + }; + + template + struct ObjectConversion_Integer { + static gp to_object(gc::IAlloc * mm, IntType x) { + return new (MMPtr(mm)) Integer(x); + } + static IntType from_object(gc::IAlloc *, gp x) { + gp x_int = Integer::from(x); + if (x_int.get()) { + return x_int->value(); + } else { + throw std::runtime_error(tostr("ObjectConversion_Integer: x found where Integer expected", xtag("x", x))); + } + } + }; + + template <> + struct ObjectConversion : public ObjectConversion_Integer {}; + template <> + struct ObjectConversion : public ObjectConversion_Integer {}; + template <> + struct ObjectConversion : public ObjectConversion_Integer {}; + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Integer.hpp */ diff --git a/xo-object/include/xo/object/List.hpp b/xo-object/include/xo/object/List.hpp new file mode 100644 index 00000000..71fe095f --- /dev/null +++ b/xo-object/include/xo/object/List.hpp @@ -0,0 +1,74 @@ +/** @file List.hpp + * + * @author: Roland Conybeare, Aug 2025 + **/ + +#pragma once + +#include "Sequence.hpp" + +namespace xo { + namespace obj { + /** @class List + * @brief A list element -- aka cons cell + **/ + class List : public Sequence { + public: + /** the empty list. unique sentinel object **/ + static gp nil; + + /** @return non-null iff @p x is actually a List cell (or nil) **/ + static gp from(gp x); + + /** @return list with first element @p car, and tail @p cdr **/ + static gp cons(gp car, gp cdr); + + /** @return list with single element @p x1 **/ + template + static gp list(T && x1) { + return List::cons(x1, nil); + } + + /** @return list with elements @p x1, ..., @p rest in argument order **/ + template + static gp list(T && x1, Rest &&... rest) { + return List::cons(x1, list(rest...)); + } + + /** @return true iff list is empty **/ + bool is_nil() const { return this == nil.ptr(); } + + gp head() const { return head_; } + gp rest() const { return rest_; } + + /** @return first element in list; synonym for @ref head **/ + gp car() const { return head_; } + /** @return remainder of list after first element; synonym for @ref rest **/ + gp cdr() const { return rest_; } + + /** @return number of top-level elements in this list **/ + std::size_t size() const; + gp list_ref(std::size_t i) const; + + void assign_head(gp head); + void assign_rest(gp rest); + + // inherited from Object.. + + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual IObject * _shallow_copy(gc::IAlloc * gc) const final override; + virtual std::size_t _forward_children(gc::IAlloc * gc) final override; + + private: + List(gp head, gp rest); + + private: + gp head_; + gp rest_; + }; + } +} /*namespace xo*/ + +/* end List.hpp */ diff --git a/xo-object/include/xo/object/Number.hpp b/xo-object/include/xo/object/Number.hpp new file mode 100644 index 00000000..c3513837 --- /dev/null +++ b/xo-object/include/xo/object/Number.hpp @@ -0,0 +1,22 @@ +/* @file Number.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "Scalar.hpp" + +namespace xo { + namespace obj { + class Number : public Scalar { + // inherited from Object.. + + //virtual std::size_t _shallow_size() const override; + //virtual Object * _shallow_copy() override; + //virtual std::size_t _forward_children() override; + }; + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Number.hpp */ diff --git a/xo-object/include/xo/object/Numeric.hpp b/xo-object/include/xo/object/Numeric.hpp new file mode 100644 index 00000000..85bc4a0c --- /dev/null +++ b/xo-object/include/xo/object/Numeric.hpp @@ -0,0 +1,22 @@ +/* @file Numeric.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "xo/alloc/Object.hpp" + +namespace xo { + namespace obj { + class Numeric : public Object { + // inherited from Object.. + + //virtual std::size_t _shallow_size() const override; + //virtual Object * _shallow_copy() override; + //virtual std::size_t _forward_children() override; + }; + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Numeric.hpp */ diff --git a/xo-object/include/xo/object/ObjectConversion.hpp b/xo-object/include/xo/object/ObjectConversion.hpp new file mode 100644 index 00000000..38b11a7d --- /dev/null +++ b/xo-object/include/xo/object/ObjectConversion.hpp @@ -0,0 +1,30 @@ +/** @file ObjectConversion.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "Object.hpp" + +namespace xo { + namespace obj { + template + struct ObjectConversion { + static gp to_object(gc::IAlloc * mm, const T & x) = delete; + static T from_object(gc::IAlloc * mm, gp x) = delete; + }; + + /** see specializations: + * ObjectConversion + * in object/Boolean.hpp + * + * ObjectConversion + * ObjectConversion + * ObjectConversion + * in object/Integer.hpp + **/ + } /*namespace obj*/ +} /*namespace xo*/ + +/* end ObjectConversion.hpp */ diff --git a/xo-object/include/xo/object/ObjectConverter.hpp b/xo-object/include/xo/object/ObjectConverter.hpp new file mode 100644 index 00000000..a7dd6a9b --- /dev/null +++ b/xo-object/include/xo/object/ObjectConverter.hpp @@ -0,0 +1,135 @@ +/** @file ObjectConverter.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "Object.hpp" +#include "xo/reflect/Reflect.hpp" +//#include "xo/reflect/TaggedPtr.hpp" +#include "xo/reflect/TypeDrivenMap.hpp" + +namespace xo { + namespace obj { + /* Convert between xo::reflect::TaggedPtr and xo::Object for + * a particular wrapped c++ type. + */ + struct Converter { + using TaggedPtr = xo::reflect::TaggedPtr; + using ConvertToObjectFn = gp (*)(gc::IAlloc *, const TaggedPtr &); + using ConvertFromObjectFn = TaggedPtr (*)(gc::IAlloc *, gp obj); + + public: + Converter() = default; + explicit Converter(ConvertToObjectFn to, + ConvertFromObjectFn from) + : cvt_to_object_{to}, cvt_from_object_{from} {} + + /** convert tagged pointer @p tp to new object, + * allocated via @p mm. + * + * Conversion will typically be for some specific type; + * see @ref ObjectConverter + **/ + ConvertToObjectFn cvt_to_object_ = nullptr; + + /** convert object to tagged pointer @p, + * allocated via @p mm. + * + * Conversion will typically be for some specific type; + * see @ref ObjectConverter + **/ + ConvertFromObjectFn cvt_from_object_ = nullptr; + }; + + /** @class ObjectConverter + * @brief Conversion to/from Object + * + * For some instance of type T: + * + * @code + * ObjectConverter & converters = ...; + * T x = ...; + * TaggedPtr tp = Reflect::make_tp(&x); + * TypeId tid = tp.td()->id(); + * + * const Converter * cvt = converters.cvt_.lookup(tid); + * + * if (cvt) { + * // cvt is a converter for T instances + * gp obj = (*(cvt->cvt_to_object_))(mm, + * @endcode + * + * ObjectConverter converts at run-time + * @see ObjectConversion for compile-time conversion + **/ + class ObjectConverter { + public: + using TaggedPtr = xo::reflect::TaggedPtr; + using TypeId = xo::reflect::TypeId; + using IAlloc = xo::gc::IAlloc; + + /** sets up standard conversions **/ + ObjectConverter(); + + /** establish conversion: use @p fn to convert values of type @tparam T. **/ + template + void establish_conversion(Converter::ConvertToObjectFn to, Converter::ConvertFromObjectFn from); + + /** convert tagged poitner @p tp to object. allocates memory only from @p mm. + * return nullptr if no converter available and @p throw_flag not set. + * throw exception if no converter available and @p throw_flag set. + **/ + gp tp_to_object(IAlloc * mm, const TaggedPtr & tp, bool throw_flag); + + /** convert @p x to object. + * return converted object; if allocated, using only memory from @p mm. + * return nullptr if no converter available, and @p throw_flag not set. + * throw exception if no converter available, and @p throw_flag set. + **/ + template + gp to_object(IAlloc * mm, const T & x, bool throw_flag); + + /** convert object @p obj to tagged pointer, with typeid @target_id. + * Allocates memory only from @p mm. + * return null TaggedPtr if no converter available and @p throw_flag not set. + * Throw exception if no converter available and @p throw_flag set. + **/ + TaggedPtr tp_from_object(IAlloc * mm, gp & obj, TypeId target_id, bool throw_flag); + + private: + /** expandable type-driven conversion table. + **/ + xo::reflect::TypeDrivenMap cvt_; + }; + + template + void + ObjectConverter::establish_conversion(Converter::ConvertToObjectFn to, + Converter::ConvertFromObjectFn from) + { + using xo::reflect::TypeDescrW; + using xo::reflect::Reflect; + + TypeDescrW td = Reflect::require(); + Converter * cvt = cvt_.require(td); + + *cvt = Converter(to, from); + } + + template + gp + ObjectConverter::to_object(IAlloc * mm, const T & x, bool throw_flag) + { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + TaggedPtr x_tp = Reflect::make_tp(&x); + + return tp_to_object(mm, x_tp, throw_flag); + } + } +} + +/* end ObjectConverter.hpp */ diff --git a/xo-object/include/xo/object/Primitive.hpp b/xo-object/include/xo/object/Primitive.hpp new file mode 100644 index 00000000..6ffe1d23 --- /dev/null +++ b/xo-object/include/xo/object/Primitive.hpp @@ -0,0 +1,111 @@ +/** @file Primitive.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "Procedure.hpp" +#include "Float.hpp" +#include "String.hpp" +#include "Boolean.hpp" +#include "ObjectConversion.hpp" +#include "xo/reflect/Reflect.hpp" + +namespace xo { + namespace obj { + // TODO: consider PrimitiveInterface here + + /** @class Primitive + * @brief Procedure implemented natively, i.e. in c++ + * + * @tparam FunctionType can be: + * - a C-style function signature such as double(*)(double) + * - a std::function, such as std::function + **/ + template + class PrimitiveBase : public Procedure { + public: + using function_type = Fn; + + public: + explicit PrimitiveBase(std::string_view name, Fn impl) : name_{name}, impl_{std::move(impl)} {} + + // inherited from Procedure.. + + virtual std::size_t n_args() const override = 0; + virtual gp apply_nocheck(gc::IAlloc * mm, const CVector> & args) override = 0; + + protected: + /** name for this primitive **/ + std::string_view name_; + /** implementation **/ + Fn impl_; + }; + + template + class Primitive : public PrimitiveBase { + }; + + template + class Primitive : public PrimitiveBase { + public: + using Super = PrimitiveBase; + using function_type = Ret (*)(Arg1, Arg2); + using TaggedPtr = xo::reflect::TaggedPtr; + + public: + explicit Primitive(std::string_view name, + function_type fn) : PrimitiveBase{name, fn} {} + + /** see also AdoptPrimitiveExpr::adopt() in xo-interpreter/AdoptPrimitiveExpr.hp **/ + + // inherited from Procedure.. + + virtual std::size_t n_args() const final override { return 2; } + + virtual gp apply_nocheck(gc::IAlloc * mm, + const CVector> & args) final override { + /* note: args[0] will be this procedure. + * actual i'th function argument in args[i+1] + */ + Arg1 arg1 = ObjectConversion::from_object(mm, args[1]); + Arg2 arg2 = ObjectConversion::from_object(mm, args[2]); + + Ret retval = (*Super::impl_)(arg1, arg2); + + return ObjectConversion::to_object(mm, retval); + } + + // inherited from Object.. + + virtual TaggedPtr self_tp() const final override { + using xo::reflect::Reflect; + + return Reflect::make_tp(const_cast(this)); + } + virtual void display(std::ostream & os) const final override { + os << ""; + } + virtual std::size_t _shallow_size() const final override { + return sizeof(*this); + } + virtual Object *_shallow_copy(gc::IAlloc * mm) const final override { + Cpof cpof(mm, this); + return new (cpof) Primitive(*this); + } + std::size_t _forward_children(gc::IAlloc *) final override { + return _shallow_size(); + } + }; + + template + gp> + make_primitive(gc::IAlloc * mm, std::string_view name, Fn fn) { + return new (MMPtr(mm)) Primitive(name, fn); + } + + } +} + +/* end Primitive.hpp */ diff --git a/xo-object/include/xo/object/Procedure.hpp b/xo-object/include/xo/object/Procedure.hpp new file mode 100644 index 00000000..1bd682f9 --- /dev/null +++ b/xo-object/include/xo/object/Procedure.hpp @@ -0,0 +1,37 @@ +/** @file Procedure.hpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#pragma once + +#include "Object.hpp" +#include "CVector.hpp" + +namespace xo { + namespace gc { class IAlloc; }; // see xo-alloc: xo/alloc/IAlloc.hpp + + namespace obj { + /** @class ProcedureInterface + * @brief Interface to a dynamically-typed procedure + **/ + class Procedure : public Object { + public: + /** downcast from @p x iff x is actually a Procedure. Otherwise nullptr **/ + static gp from(gp x) { return gp::from(x); } + + virtual std::size_t n_args() const = 0; + virtual gp apply_nocheck(gc::IAlloc * mm, const CVector> & args) = 0; + + // inherited from Object.. + + // virtual TaggedPtr self_tp() const override; + // virtual void display(std::ostream &) const override; + // virtual size_t _shallow_size() const override; + // virtual Object * _shallow_copy(gc::IAlloc *) const override; + // virtual size_t _forward_children(gc::IAlloc *) override; + }; + } +} + +/* end Procedure.hpp */ diff --git a/xo-object/include/xo/object/Scalar.hpp b/xo-object/include/xo/object/Scalar.hpp new file mode 100644 index 00000000..9bbc8aec --- /dev/null +++ b/xo-object/include/xo/object/Scalar.hpp @@ -0,0 +1,22 @@ +/* @file Scalar.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "Numeric.hpp" + +namespace xo { + namespace obj { + class Scalar : public Numeric { + // inherited from Object.. + + //virtual std::size_t _shallow_size() const override; + //virtual Object * _shallow_copy() override; + //virtual std::size_t _forward_children() override; + }; + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Scalar.hpp */ diff --git a/xo-object/include/xo/object/Sequence.hpp b/xo-object/include/xo/object/Sequence.hpp new file mode 100644 index 00000000..bbd2b0b7 --- /dev/null +++ b/xo-object/include/xo/object/Sequence.hpp @@ -0,0 +1,22 @@ +/* @file Sequence.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#pragma once + +#include "Collection.hpp" + +namespace xo { + namespace obj { + class Sequence : public Collection { + // inherited from Object.. + + //virtual std::size_t _shallow_size() const override; + //virtual Object * _shallow_copy() const override; + //virtual std::size_t _fixup_forwarded_children() override; + }; + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Sequence.hpp */ diff --git a/xo-object/include/xo/object/String.hpp b/xo-object/include/xo/object/String.hpp new file mode 100644 index 00000000..e608c2ab --- /dev/null +++ b/xo-object/include/xo/object/String.hpp @@ -0,0 +1,171 @@ +/* @file String.hpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/alloc/Object.hpp" +#include "ObjectConversion.hpp" +#include "xo/allocutil/IAlloc.hpp" +#include "xo/indentlog/print/tag.hpp" + +namespace xo { + namespace obj { + /** unicode terminology (via https://utf8everywhere.org) + * 1. code unit: + * bit combination that represents a unit of encoded text. + * 8-bits for utf-8 i.e. code-unit = char + * 2. code point: + * a numerical value in the unicode namespace, e.g. U+3243F + * one or more code units encode a code point. + * utf-8 uses 1-4 code units to encode each code point. + * 3. abstract character: + * inherently open, because includes characters that are not + * (yet) representable in unicode. + * 4. (en)coded character: + * mapping between code points and abstract character. + * for example U+1F428 is coded character for emoji named 'KOALA' + * caveats: + * - some code points do not have abstract characters assigned. + * - some code points are reserved for non-characters + * (e.g. null, newline ..) + * - some abstract characters require multiple code points: + * for example requiring a composition sequence + * - some abstract characters have multiple encodings + * 5. user-perceived character. whatever you think that means. + * May be language-dependent. + * 6. grapheme cluster. a sequence of coded characters that + * "belong together". for example backspace would erase + * a grapheme cluster atomically. + * 7. a shape within a font. A sequence of code points maps to + * a sequence of glyphs. + **/ + class String : public Object { + public: + enum class owner { unique, shared }; + + /** donwcast from @p x iff x is actually a String. Otherwise nullptr **/ + static gp from(gp x); + + /** create shared string @p s, using allocator @ref Object::mm **/ + static gp share(const char * s); + /** create shared string @p s, using allocator @p mm **/ + static gp share(gc::IAlloc * mm, const char * s); + /** create copy of string @p s, using allocator @ref Object::mm **/ + static gp copy(const char * s); + /** create copy of string @p s, using allocator @p mm **/ + static gp copy(gc::IAlloc * mm, const char * s); + + /** create empty string with @p z bytes of string space **/ + static gp allocate(std::size_t z); + /** create string containing contents of @p s1 follwed by contents of @p s2 **/ + static gp append(gp s1, gp s2); + + const char * c_str() const { return chars_; } + std::size_t length() const; + + /** Approximate number of columns (if using a fixed-width font) occupied + * by this string. Obtained by counting bytes up to null terminator, + * omitting utf-8 continuation bytes, i.e. bytes with high bit set + * and 2nd-highest bit clear. + * + * @text + * 1 byte + * <------> + * bits: 76543210 + * 10______ + * @endtext + **/ + std::size_t columns() const; + + std::strong_ordering operator<=>(const String & other) const { + size_t len1 = std::strlen(chars_); + size_t len2 = std::strlen(other.chars_); + +# if defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L + return std::lexicographical_compare_three_way(chars_, chars_ + len1, + other.chars_, other.chars_ + len2); +# else + /* lexicographical_compare_three_way n/avail in clang 16.0.6 */ + { + // Compare common prefix + size_t min_len = std::min(len1, len2); + int cmp = std::memcmp(chars_, other.chars_, min_len); + + if (cmp < 0) return std::strong_ordering::less; + if (cmp > 0) return std::strong_ordering::greater; + + // Common prefix is equal, compare lengths + if (len1 < len2) return std::strong_ordering::less; + if (len1 > len2) return std::strong_ordering::greater; + + return std::strong_ordering::equal; + } +# endif + } + + bool operator==(const String & other) const { + size_t len1 = std::strlen(chars_); + size_t len2 = std::strlen(other.chars_); + + if (len1 != len2) + return false; + + return std::memcmp(chars_, other.chars_, len1) == 0; + } + + // inherited from Object.. + virtual TaggedPtr self_tp() const final override; + virtual void display(std::ostream & os) const final override; + virtual std::size_t _shallow_size() const final override; + virtual Object * _shallow_copy(gc::IAlloc * gc) const final override; + virtual std::size_t _forward_children(gc::IAlloc * gc) final override; + + private: + String(owner owner, std::size_t z, char * s); + /** create instance, copying string contents (when @p copy_flag is true) using allocator @p mm **/ + String(gc::IAlloc * mm, owner owner, std::size_t z, char * s); + + private: + /** true iff storage in @ref chars_ is owned by this String. + **/ + owner owner_ = owner::shared; + /** length of @ref chars_ in bytes (storage allocated, not necessarily string length). + * Includes null terminator + **/ + std::size_t z_chars_ = 0; + /** utf-8 string contents. always null-terminated. + * Note that this is #of bytes + **/ + char * chars_ = nullptr; + }; + + struct ObjectConversion_String { + static gp to_object(gc::IAlloc * mm, std::string x) { + return String::copy(mm, x.c_str()); + } + static std::string from_object(gc::IAlloc *, gp x) { + gp x_str = String::from(x); + if (x_str.get()) { + /* note: ignores allocator, always uses heap. + * This will affect operation of primitives (if any) that + * expect std::string. Alternative would be use IAlloc*, + * with Blob wrapper (or without if/when need for iterable + * memory is dropped). + */ + return std::string(x_str->c_str()); + } else { + throw std::runtime_error + (tostr("ObjectConversion_String" + ": x found where string expected", xtag("x", x))); + } + + } + }; + + template <> + struct ObjectConversion : public ObjectConversion_String {}; + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end String.hpp */ diff --git a/xo-object/src/object/Boolean.cpp b/xo-object/src/object/Boolean.cpp new file mode 100644 index 00000000..424da920 --- /dev/null +++ b/xo-object/src/object/Boolean.cpp @@ -0,0 +1,87 @@ +/* @file Boolean.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "Boolean.hpp" +#include +#include +#include +#include +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + namespace obj { + gp + Boolean::boolean_obj(bool x) + { + static std::array, 2> s_boolean_v + = {{ new Boolean{false}, new Boolean{true} }}; + + return s_boolean_v[static_cast(x)]; + } + + gp + Boolean::true_obj() + { + return boolean_obj(true); + } + + gp + Boolean::false_obj() + { + return boolean_obj(false); + } + + TaggedPtr + Boolean::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + Boolean::display(std::ostream & os) const + { + os << (value_ ? "#t" : "#f"); + } + + std::size_t + Boolean::_shallow_size() const + { + return sizeof(Boolean); + } + + // LCOV_EXCL_START + Object * + Boolean::_shallow_copy(gc::IAlloc *) const + { + + /* Boolean instances not created in GC-owned space, + * so GC will not traverse them. + * + * If we wanted booleans in GC-owned space, would need + * to pad Boolean::value_ with enough space to hold a forwarding + * pointer + */ + + assert(false); + return nullptr; + + } + // LCOV_EXCL_STOP + + // LCOV_EXCL_START + std::size_t + Boolean::_forward_children(gc::IAlloc *) + { + assert(false); + return 0; + } + // LCOV_EXCL_STOP + } +} /*namespace xo*/ + +/* end Boolean.cpp */ diff --git a/xo-object/src/object/Float.cpp b/xo-object/src/object/Float.cpp new file mode 100644 index 00000000..57e0d995 --- /dev/null +++ b/xo-object/src/object/Float.cpp @@ -0,0 +1,57 @@ +/** @file Float.cpp + * + * @author: Roland Conybeare, Nov 2025 + **/ + +#include "Float.hpp" +#include "xo/reflect/Reflect.hpp" + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + namespace obj { + static_assert(sizeof(Float::float_type) == 8, "expected 64-bit representation for xo::obj::Float"); + + Float::Float(float_type x) : value_{x} {} + + gp + Float::make(IAlloc * mm, float_type x) + { + return new (MMPtr(mm)) Float(x); + } + + gp + Float::from(gp x) { + return dynamic_cast(x.ptr()); + } + + TaggedPtr + Float::self_tp() const { + return Reflect::make_tp(const_cast(this)); + } + + void + Float::display(std::ostream & os) const { + os << value_; + } + + std::size_t + Float::_shallow_size() const { + return sizeof(Float); + } + + Object * + Float::_shallow_copy(gc::IAlloc * mm) const { + Cpof cpof(mm, this); + return new (cpof) Float(*this); + } + + std::size_t + Float::_forward_children(gc::IAlloc * /*gc*/) { + return Float::_shallow_size(); + } + } +} + +/* end Float.cpp */ diff --git a/xo-object/src/object/Integer.cpp b/xo-object/src/object/Integer.cpp new file mode 100644 index 00000000..82e755c9 --- /dev/null +++ b/xo-object/src/object/Integer.cpp @@ -0,0 +1,59 @@ +/* @file Integer.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "Integer.hpp" +#include "xo/reflect/Reflect.hpp" +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + using xo::gc::IAlloc; + + namespace obj { + static_assert(sizeof(Integer::int_type) == 8, "expected 64-bit representation for xo::obj::Integer"); + + Integer::Integer(int_type x) : value_{x} {} + + gp + Integer::make(IAlloc * mm, int_type x) { + return new (MMPtr(mm)) Integer(x); + } + + gp + Integer::from(gp x) { + return gp::from(x); + } + + TaggedPtr + Integer::self_tp() const { + return Reflect::make_tp(const_cast(this)); + } + + void + Integer::display(std::ostream & os) const { + os << value_; + } + + std::size_t + Integer::_shallow_size() const { + return sizeof(Integer); + } + + Object * + Integer::_shallow_copy(gc::IAlloc * mm) const { + Cpof cpof(mm, this); + return new (cpof) Integer(*this); + } + + std::size_t + Integer::_forward_children(gc::IAlloc * /*gc*/) { + return Integer::_shallow_size(); + } + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end Integer.cpp */ diff --git a/xo-object/src/object/List.cpp b/xo-object/src/object/List.cpp new file mode 100644 index 00000000..e8e700f2 --- /dev/null +++ b/xo-object/src/object/List.cpp @@ -0,0 +1,122 @@ +/** @file List.cpp + * + * author: Roland Conybeare, Aug 2025 + **/ + +#include "List.hpp" +#include "xo/reflect/Reflect.hpp" +#include "xo/indentlog/scope.hpp" +#include +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + namespace obj { + List::List(gp head, gp rest) + : head_{head}, rest_{rest} {} + + gp + List::nil = new List(nullptr, nullptr); + + gp + List::from(gp x) { + return dynamic_cast(x.ptr()); + } + + gp + List::cons(gp car, gp cdr) { + return new (MMPtr(mm)) List(car, cdr); + } + + std::size_t + List::size() const { + std::size_t retval = 0; + + gp l(this); + while (!l->is_nil()) { + ++retval; + l = l->rest(); + } + + return retval; + } + + gp + List::list_ref(std::size_t i) const { + gp rem(this); + + while (i > 0) { + assert(!(rem->is_nil())); + + rem = rem->rest(); + --i; + } + + return rem->head(); + + } + + void + List::assign_head(gp head) + { + Object::assign_member(this, &(this->head_), head); + } + + void + List::assign_rest(gp tail) + { + Object::assign_member(this, &(this->rest_), tail); + } + + TaggedPtr + List::self_tp() const + { + return Reflect::make_tp(const_cast(this)); + } + + void + List::display(std::ostream & os) const + { + gp l = const_cast(this); + + os << "("; + size_t i = 0; + while (!l->is_nil()) { + if (i > 0) + os << " "; + os << l->head(); + l = l->rest(); + ++i; + } + os << ")"; + } + + std::size_t + List::_shallow_size() const + { + return sizeof(List); + } + + IObject * + List::_shallow_copy(gc::IAlloc * gc) const + { + assert(!(this->is_nil())); + + Cpof cpof(gc, this); + + return new (cpof) List(*this); + } + + std::size_t + List::_forward_children(gc::IAlloc * gc) + { + Object::_forward_inplace(head_, gc); + Object::_forward_inplace(rest_, gc); + return List::_shallow_size(); + } + } +} + +/* end List.cpp */ diff --git a/xo-object/src/object/ObjectConverter.cpp b/xo-object/src/object/ObjectConverter.cpp new file mode 100644 index 00000000..5ad36cb5 --- /dev/null +++ b/xo-object/src/object/ObjectConverter.cpp @@ -0,0 +1,211 @@ +/** @file ObjectConverter.cpp + * + * @author Roland Conybeare, Nov 2025 + **/ + +#include "ObjectConverter.hpp" +#include "Integer.hpp" +#include "Float.hpp" +#include "Boolean.hpp" +#include "String.hpp" +#include +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + using xo::reflect::TypeId; + using xo::gc::IAlloc; + + namespace obj { + namespace { + template + gp + int_to_object(IAlloc * mm, const TaggedPtr & src) + { + T * native = src.recover_native(); + + assert(native); + + return Integer::make(mm, *native); + } + + template + TaggedPtr + object_to_int(IAlloc * mm, gp obj) + { + /* mm cannot be GC allocator! + * That's Object-only + */ + + gp int_obj = Integer::from(obj); + + if (!int_obj.get()) { + throw std::runtime_error(tostr("Object obj found where Integer expected", + xtag("obj", obj))); + } + + /* allocate a Blob wrapper to satsify GC memory layout demands */ + + gp tmp = Blob::make(mm, sizeof(T)); + + T * p = reinterpret_cast(tmp->data()); + + *p = int_obj->value(); + + /* WARNING: retval invalidated when *mm cleared/recycled/collected */ + + return Reflect::make_tp(p); + } + + template + gp + float_to_object(IAlloc * mm, const TaggedPtr & src) + { + T * native = src.recover_native(); + + assert(native); + + return Float::make(mm, *native); + } + + template + TaggedPtr + object_to_float(IAlloc * mm, gp obj) + { + /* mm cannot be GC allocator! + * That's Object-only + */ + + gp float_obj = Float::from(obj); + + if (!float_obj.get()) { + throw std::runtime_error(tostr("Object obj found where Float expected", + xtag("obj", obj))); + } + + /* allocate a Blob wrapper to satsify GC memory layout demands */ + + gp tmp = Blob::make(mm, sizeof(T)); + + T * p = reinterpret_cast(tmp->data()); + + *p = float_obj->value(); + + /* WARNING: retval invalidated when *mm cleared/recycled/collected */ + + return Reflect::make_tp(p); + } + + gp + bool_to_object(IAlloc * /*mm*/, const TaggedPtr & src) + { + bool * native = src.recover_native(); + + assert(native); + + return Boolean::boolean_obj(*native); + } + + TaggedPtr + object_to_bool(IAlloc * /*mm*/, gp obj) + { + static bool s_true = true; + static bool s_false = false; + + gp bool_obj = Boolean::from(obj); + + if (!bool_obj.get()) { + throw std::runtime_error(tostr("Object obj found where Boolean expected", + xtag("obj", obj))); + } + + return Reflect::make_tp(bool_obj->value() ? &s_true : &s_false); + } + + gp + string_to_object(IAlloc * mm, const TaggedPtr & src) + { + std::string * native = src.recover_native(); + + assert(native); + + return String::copy(mm, native->c_str()); + } + + TaggedPtr + object_to_string(IAlloc * /*mm*/, gp obj) + { + gp string_obj = String::from(obj); + + if (!string_obj.get()) { + throw std::runtime_error(tostr("Object obj founcd where String expected", + xtag("obj", obj))); + } + + // still don't have good solver for this yet + assert(false); + + return TaggedPtr::universal_null(); + } + } + + ObjectConverter::ObjectConverter() + { + this->establish_conversion(&int_to_object, + &object_to_int); + this->establish_conversion(&int_to_object, + &object_to_int); + + this->establish_conversion(&float_to_object, + &object_to_float); + + this->establish_conversion(&bool_to_object, + &object_to_bool); + + this->establish_conversion(&string_to_object, + &object_to_string); + } + + gp + ObjectConverter::tp_to_object(IAlloc * mm, const TaggedPtr & x_tp, bool throw_flag) + { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + + const Converter * cvt = cvt_.lookup(x_tp.td()); + + if (cvt) { + return (cvt->cvt_to_object_)(mm, x_tp); + } else { + if (throw_flag) { + throw std::runtime_error + (tostr("no to-object-converter available for instance of type", + xtag("id", x_tp.td()->id()), + xtag("name", x_tp.td()->short_name()))); + } + + return nullptr; + } + } + + TaggedPtr + ObjectConverter::tp_from_object(IAlloc * mm, gp & obj, TypeId target_id, bool throw_flag) + { + const Converter * cvt = cvt_.lookup(target_id); + + if (cvt) { + return (cvt->cvt_from_object_)(mm, obj); + } else { + if (throw_flag) { + throw std::runtime_error(tostr("no from-object-converter available for instance of type", + xtag("id", target_id))); + } + + return TaggedPtr::universal_null(); + } + } + } +} + +/* end ObjectConverter.cpp */ diff --git a/xo-object/src/object/String.cpp b/xo-object/src/object/String.cpp new file mode 100644 index 00000000..4a457a36 --- /dev/null +++ b/xo-object/src/object/String.cpp @@ -0,0 +1,181 @@ +/* @file String.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "String.hpp" +#include "GC.hpp" +#include +#include +#include +#ifdef __linux__ +# include +#endif +#include +#include +#include + +namespace xo { + using xo::reflect::Reflect; + using xo::reflect::TaggedPtr; + using xo::print::quot; + + namespace obj { + String::String(owner owner, std::size_t z, char * s) + : owner_{owner}, z_chars_{z}, chars_{s} + {} + + String::String(gc::IAlloc * mm, owner owner, std::size_t z, char * s) + : owner_{owner}, z_chars_{z} + { + if (owner_ == owner::unique) { + chars_ = reinterpret_cast(mm->alloc(z)); + + assert(chars_); + + strlcpy(chars_, s, z); + } else { + chars_ = s; + } + } + + gp + String::from(gp x) { + return dynamic_cast(x.ptr()); + } + + gp + String::share(const char * s) { + return share(Object::mm, s); + } + + gp + String::share(gc::IAlloc * mm, const char * s) + { + const char * chars = s ? s : ""; + std::size_t z = 1 + ::strlen(chars); + + return new (MMPtr(mm)) String(mm, owner::shared, z, const_cast(chars)); + } + + gp + String::copy(const char * s) { + return copy(Object::mm, s); + } + + gp + String::copy(gc::IAlloc * mm, const char * s) + { + std::size_t z = 1 + (s ? ::strlen(s) : 0); + const char * chars = s ? s : ""; + + // const-cast ok since chars copied with Owner::unique + return new (MMPtr(mm)) String(mm, owner::unique, z, const_cast(chars)); + } + + gp + String::allocate(std::size_t z) + { + return new (MMPtr(Object::mm)) String(mm, owner::unique, z, const_cast("")); + } + + gp + String::append(gp s1, gp s2) + { + std::size_t z1 = s1->length(); + std::size_t z2 = s2->length(); + std::size_t z = z1 + z2; + + // +1 for null terminator + gp retval = allocate(z+1); + + strlcpy(retval->chars_, s1->chars_, z1+1); + strlcpy(retval->chars_ + z1, s2->chars_, z2+1); + + return retval; + } + + std::size_t + String::length() const + { + return ::strlen(chars_); + } + + std::size_t + String::columns() const + { + size_t retval = 0; + + for (const char * p = chars_, * e = chars_ + z_chars_; *p && (p < e); ++p) { + if ((*p & 0xc0) == 0x80) { + /* continuation byte -> ignore */ + } else { + ++retval; + } + } + + return retval; + } + + TaggedPtr + String::self_tp() const { + return Reflect::make_tp(const_cast(this)); + } + + void + String::display(std::ostream & os) const { + os << quot(c_str()); + } + + // ----- GC support ----- + + std::size_t + String::_shallow_size() const + { + /* no child Object* pointers to fixup, + * but must count for amount of storage used by _shallow_move() + */ + std::size_t retval = gc::IAlloc::with_padding(sizeof(String)); + + if (owner_ == owner::unique) + retval += gc::IAlloc::with_padding(z_chars_); + + return retval; + } + + Object * + String::_shallow_copy(gc::IAlloc * mm) const + { + // Reminder: String must come before secondary allocation, + + Cpof cpof(mm, this); + + // might expect to write: + // gp copy = new (gcm) String(Object::mm, owner_, z_chars_, chars_); + // but this would always put string contents in nursery to-space. + // + // We need to choose nursery/tenured based on location of this, + // achieved by calling GC::alloc_copy() instead of GC::alloc() + // + gp copy = new (cpof) String(owner_, z_chars_, chars_); + + if (owner_ == owner::unique) { + std::byte * mem = reinterpret_cast(chars_); + + copy->chars_ = reinterpret_cast(mm->alloc_gc_copy(z_chars_, mem)); + strlcpy(copy->chars_, chars_, z_chars_); + } + + return copy.ptr(); + } + + std::size_t + String::_forward_children(gc::IAlloc *) + { + return this->_shallow_size(); + } + + } /*namespace obj*/ +} /*namespace xo*/ + +/* end String.cpp */ diff --git a/xo-object/utest/Boolean.test.cpp b/xo-object/utest/Boolean.test.cpp new file mode 100644 index 00000000..83fe9b7c --- /dev/null +++ b/xo-object/utest/Boolean.test.cpp @@ -0,0 +1,63 @@ +/* @file Boolean.test.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/object/Boolean.hpp" +#include "xo/alloc/GC.hpp" +#include + +namespace xo { + using xo::obj::Boolean; + using xo::gc::GC; + using xo::gc::generation_result; + + namespace ut { + TEST_CASE("Boolean", "[Boolean]") + { + up gc = GC::make( + { .initial_nursery_z_ = 1024, + .initial_tenured_z_ = 2048, + .incr_gc_threshold_ = 512, + .full_gc_threshold_ = 512 + }); + + REQUIRE(gc.get()); + + /* use gc for "all" Object allocs. + * Not using, but want it to be available, verify conscious choice + */ + Object::mm = gc.get(); + + gp btrue = Boolean::true_obj(); + gp bfalse = Boolean::false_obj(); + + REQUIRE(btrue.ptr()); + REQUIRE(btrue->value() == true); + + REQUIRE(bfalse.ptr()); + REQUIRE(bfalse->value() == false); + + REQUIRE(btrue->_shallow_size() == sizeof(Boolean)); + + // booleans are global constants + REQUIRE(gc->tospace_generation_of(btrue.ptr()) == generation_result::not_found); + REQUIRE(gc->tospace_generation_of(bfalse.ptr()) == generation_result::not_found); + + REQUIRE(btrue->self_tp().td()->short_name() == "Boolean"); + REQUIRE(bfalse->self_tp().td()->short_name() == "Boolean"); + + { + std::stringstream ss; + ss << btrue; + REQUIRE(ss.str() == "#t"); + } + + { + std::stringstream ss; + ss << bfalse; + REQUIRE(ss.str() == "#f"); + } + } + } /*namespace ut*/ +} /*namespace xo*/ diff --git a/xo-object/utest/CMakeLists.txt b/xo-object/utest/CMakeLists.txt new file mode 100644 index 00000000..e98af8ad --- /dev/null +++ b/xo-object/utest/CMakeLists.txt @@ -0,0 +1,15 @@ +# build unittest object/utest + +set(UTEST_EXE utest.object) +set(UTEST_SRCS + object_utest_main.cpp + Boolean.test.cpp + Integer.test.cpp + String.test.cpp + List.test.cpp + GC.test.cpp) + +xo_add_utest_executable(${UTEST_EXE} ${UTEST_SRCS}) +xo_self_dependency(${UTEST_EXE} xo_object) +xo_dependency(${UTEST_EXE} randomgen) +xo_external_target_dependency(${UTEST_EXE} Catch2 Catch2::Catch2) diff --git a/xo-object/utest/GC.test.cpp b/xo-object/utest/GC.test.cpp new file mode 100644 index 00000000..015d7eb8 --- /dev/null +++ b/xo-object/utest/GC.test.cpp @@ -0,0 +1,677 @@ +/* @file GC.test.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/alloc/GC.hpp" +#include "xo/object/List.hpp" +#include "xo/object/Integer.hpp" +#include "xo/randomgen/random_seed.hpp" +#include "xo/randomgen/xoshiro256.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/tag.hpp" +#include +#include + +namespace xo { + using xo::obj::List; + using xo::obj::Integer; + using xo::gc::GC; + using xo::gc::generation_result; + using xo::gc::generation; + + using xo::rng::Seed; + using xo::rng::xoshiro256ss; + + namespace ut { + + // Also see GC unit tests in xo-alloc/utest + +#ifdef NOT_YET + namespace { + struct testcase_mlog { + testcase_mlog(std::size_t nz, std::size_t tz) : nursery_z_{nz}, tenured_z_{tz} {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + }; + } +#endif + + TEST_CASE("gc-mlog-1", "[alloc][gc][gc_mutation]") + { + up gc = GC::make( + { + .initial_nursery_z_ = 2024, + .initial_tenured_z_ = 4048, + .incr_gc_threshold_ = 512, + .full_gc_threshold_ = 1024, + .debug_flag_ = false + }); + gc->disable_gc(); + + REQUIRE(gc->native_gc_statistics().n_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + gp l = List::list(Integer::make(gc.get(), 1)); + gc->add_gc_root(reinterpret_cast(l.ptr_address())); + + gp l2 = List::list(Integer::make(gc.get(), 10)); + gc->add_gc_root(reinterpret_cast(l2.ptr_address())); + + { + REQUIRE(l->size() == 1); + REQUIRE(l2->size() == 1); + + REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery); + REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); + REQUIRE(gc->is_before_checkpoint(l.ptr()) == false); + REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false); + + REQUIRE(gc->tospace_generation_of(l2.ptr()) == generation_result::nursery); + REQUIRE(gc->tospace_generation_of(l2->head().ptr()) == generation_result::nursery); + REQUIRE(gc->is_before_checkpoint(l2.ptr()) == false); + REQUIRE(gc->is_before_checkpoint(l2->head().ptr()) == false); + + REQUIRE(gc->mlog_size() == 0); + } + + // mutation, but not {xgen, xckp} since parent,child both in N0 + + l->assign_head(Integer::make(gc.get(), 2)); + { + REQUIRE(gc->native_gc_statistics().n_mutation_ == 1); + REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 0); + REQUIRE(gc->mlog_size() == 0); + + REQUIRE(gc->is_gc_enabled() == false); + + } + + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + { + REQUIRE(gc->is_before_checkpoint(l.ptr()) == true); + REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == true); + + REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::nursery); + + REQUIRE(l->size() == 1); + REQUIRE(Integer::from(l->head()).ptr()); + REQUIRE(Integer::from(l->head())->value() == 2); + } + + // mutation, xckp since parent in N1, child in N0 + + l->assign_head(Integer::make(gc.get(), 3)); + { + REQUIRE(Integer::from(l->head())->value() == 3); + + REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); + REQUIRE(gc->is_before_checkpoint(l->head().ptr()) == false); + + REQUIRE(gc->native_gc_statistics().n_mutation_ == 2); + REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 1); + REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 1); + REQUIRE(gc->mlog_size() == 1); + } + + // gc promotes parent, still need mutation log for xgen ptr + + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + { + REQUIRE(l->size() == 1); + REQUIRE(Integer::from(l->head()).ptr()); + REQUIRE(Integer::from(l->head())->value() == 3); + + REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::tenured); + REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::nursery); + REQUIRE(gc->is_before_checkpoint(l->head().ptr())); + + REQUIRE(gc->native_gc_statistics().n_mutation_ == 2); + REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 1); + // counters recorded when mutation created. + // not modified by gc + REQUIRE(gc->native_gc_statistics().n_xgen_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_xckp_mutation_ == 1); + REQUIRE(gc->mlog_size() == 1); + } + + // gc promotes child, no longer need mutation log entry + + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + { + REQUIRE(l->size() == 1); + REQUIRE(Integer::from(l->head()).ptr()); + REQUIRE(Integer::from(l->head())->value() == 3); + + REQUIRE(gc->tospace_generation_of(l.ptr()) == generation_result::tenured); + REQUIRE(gc->tospace_generation_of(l->head().ptr()) == generation_result::tenured); + + REQUIRE(gc->mlog_size() == 0); + } + } + + namespace { + enum class object_type { + nil, + integer, + cons, + }; + + struct ObjectModel { + /* 1:1 with address */ + std::size_t index_; + /* nil|integer|cons */ + object_type type_; + /* value for model of Integer::value_, if type_ is object_type::integer */ + std::size_t int_value_; + /* index# for model of List::head_, if type_ is object_type::list */ + std::size_t head_ix_; + /* index# for model of List::rest_, if type_ is object_type::list */ + std::size_t rest_ix_; + }; + + struct ObjectGraphModel { + /** + * @param from_graph + * @param from_ix + * @param to_graph + * @param to_ix + * @param p_visited_set + **/ + static bool verify_equal_aux(ObjectGraphModel & from_graph, + std::size_t from_ix, + ObjectGraphModel & to_graph, + std::size_t to_ix, + std::unordered_set * p_visited_set); + + /** compare models for structural equivalence; will be comparing before/after a garbage collection cycle + * @param from_graph model before GC + * @param to_graph model after GC + * @return true iff models are equivalent + **/ + static bool verify_equal_models(ObjectGraphModel & from_model, ObjectGraphModel & to_model); + + /* build model for object graph from a vector of object pointers */ + void from_root_vector(const std::vector> & object_v); + /* include everything reachable from @p x in this object model */ + std::size_t traverse_from_object(gp x); + + /* one node per xo::Object instance. */ + std::vector nodes_; + /* map from root index to node index number */ + std::vector roots_; + /* map from (original) address to index number */ + std::unordered_map addr2node_map_; + }; + + bool + ObjectGraphModel::verify_equal_aux(ObjectGraphModel & from_graph, + std::size_t from_ix, + ObjectGraphModel & to_graph, + std::size_t to_ix, + std::unordered_set * p_visited_set) + { + if (p_visited_set->contains(from_ix)) + return true; + + const ObjectModel & from = from_graph.nodes_.at(from_ix); + const ObjectModel & to = from_graph.nodes_.at(to_ix); + + REQUIRE(from.type_ == to.type_); + + p_visited_set->insert(from.index_); + + if (from.type_ == object_type::nil) + return true; + + if (from.type_ == object_type::integer) { + REQUIRE(from.int_value_ == to.int_value_); + return true; + } + + if (from.type_ == object_type::cons) { + return (verify_equal_aux(from_graph, from.head_ix_, + to_graph, to.head_ix_, + p_visited_set) + && verify_equal_aux(from_graph, from.rest_ix_, + to_graph, to.rest_ix_, + p_visited_set)); + } + + return false; + } + + bool + ObjectGraphModel::verify_equal_models(ObjectGraphModel & from_model, + ObjectGraphModel & to_model) + { + REQUIRE(from_model.roots_.size() == to_model.roots_.size()); + REQUIRE(from_model.nodes_.size() == to_model.nodes_.size()); + + std::unordered_set visited_set; + + for (std::size_t i = 0, n = from_model.roots_.size(); i < n; ++i) { + INFO(tostr(xtag("i", i), xtag("n", n))); + + REQUIRE(verify_equal_aux(from_model, + from_model.roots_.at(i), + to_model, + to_model.roots_.at(i), + &visited_set)); + } + + return true; + } + + std::size_t + ObjectGraphModel::traverse_from_object(gp x) + { + std::uintptr_t x_addr = reinterpret_cast(x.ptr()); + + auto addr2node_ix = addr2node_map_.find(x_addr); + + if (addr2node_ix != addr2node_map_.end()) { + /* already imported (or import on call stack) */ + + return addr2node_ix->second; + } else { + ObjectModel new_model; + auto x_int = Integer::from(x); + auto x_list = List::from(x); + + std::size_t new_index = this->nodes_.size(); + { + if (x_int.is_null() && x_list.is_null()) + throw std::runtime_error(tostr("expecting object graph containing int|cons|nil only", xtag("x", x))); + + if (!x_int.is_null()) { + new_model.index_ = new_index; + new_model.type_ = object_type::integer; + new_model.int_value_ = x_int->value(); + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } + + if (!x_list.is_null()) { + + if (x_list->is_nil()) { + new_model.index_ = 0; + new_model.type_ = object_type::nil; + new_model.int_value_ = 0; + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } else { + new_model.index_ = new_index; + new_model.type_ = object_type::cons; + new_model.int_value_ = 0; + /* fill below */ + new_model.head_ix_ = 0; + new_model.rest_ix_ = 0; + } + } + } + + this->nodes_.push_back(new_model); + this->addr2node_map_[x_addr] = new_index; + + if (!x_list.is_null() && !(x_list->is_nil())) { + ObjectModel & model = this->nodes_.at(new_index); + + model.head_ix_ = traverse_from_object(x_list->head()); + model.rest_ix_ = traverse_from_object(x_list->rest()); + } + + return new_index; + } + } + + void + ObjectGraphModel::from_root_vector(const std::vector> & root_v) + { + assert(nodes_.empty()); + assert(addr2node_map_.empty()); + + /* sentinel = List::nil */ + { + ObjectModel sentinel; + sentinel.index_ = 0; + sentinel.type_ = object_type::nil; + sentinel.int_value_ = 0; + sentinel.head_ix_ = 0; + sentinel.rest_ix_ = 0; + + this->nodes_.push_back(sentinel); + } + + /* it's possible that object_v is complete. + * seed model by importing all the nodes in object_v[] + */ + for (gp x : root_v) + this->roots_.push_back(traverse_from_object(x)); + } + + /** Generate some random data + mutations to verify GC behavior + * + * To setup for first GC: + * RandomMutationModel model(m, n, r, k); + * model.generate_seed_values(); + * model.generate_random_roots(gc, &rgen); + * model.generate_random_mutations(&rgen); + * + * To prepare for next GC + * model.rejuvenate_seed_values(); + * model.alter_random_roots(&rgen); + * model.generate_random_mutations(&rgen); + **/ + struct RandomMutationModel { + RandomMutationModel(std::size_t m, std::size_t n, std::size_t r, std::size_t rr, std::size_t k) + : m_{m}, n_{n}, r_{r}, rr_{rr}, k_{k} {} + + void generate_seed_values(GC * gc); + void generate_random_roots(GC * gc, xoshiro256ss * p_rgen); + void generate_random_mutations(xoshiro256ss * p_rgen); + + void rejuvenate_seed_values(GC * gc); + void alter_random_roots(xoshiro256ss * p_rgen); + + /* create m random list cells */ + size_t m_ = 0; + /** create n random integers, starting with value @ref start_ **/ + size_t start_ = 0; + size_t n_ = 0; + /* #of roots */ + size_t r_ = 0; + size_t rr_ = 0; + /* #of random mutations */ + size_t k_ = 0; + + /* w1[] contains some random list cells */ + std::vector> w1_; + /* w2[] has all of w1[], also contains some integers */ + std::vector> w2_; + + /* create some random roots. always pick at least one list cell */ + std::vector> root_v_; + }; + + void RandomMutationModel::generate_seed_values(GC * gc) + { + w1_.clear(); + w2_.clear(); + + { + for (size_t i = 0; i < m_; ++i) { + w1_.push_back(List::cons(List::nil, List::nil)); + } + REQUIRE(w1_.size() == m_); + } + + { + std::copy(w1_.begin(), w1_.end(), std::back_inserter(w2_)); + for (size_t j = 0; j < n_; ++j) { + w2_.push_back(Integer::make(gc, (this->start_)++)); + } + REQUIRE(w2_.size() == m_ + n_); + } + } + + void RandomMutationModel::generate_random_roots(GC * gc, + xoshiro256ss * p_rgen) + { + if (n_ > 0) { + std::size_t w1_ix = (*p_rgen)() % n_; + + if (r_ > 0) + root_v_.push_back(w2_.at(w1_ix)); + } + + for (std::size_t i = 1; i < r_; ++i) { + std::size_t w2_ix = (*p_rgen)() % (m_ + n_); + + root_v_.push_back(w2_.at(w2_ix)); + } + + REQUIRE(root_v_.size() == r_); + + for (auto & root : root_v_) + gc->add_gc_root(reinterpret_cast(root.ptr_address())); + } + + void RandomMutationModel::generate_random_mutations(xoshiro256ss * p_rgen) + { + for (std::size_t i = 0; i < k_; ++i) { + /* pick a root list cell at random */ + gp l1 = w1_.at((*p_rgen)() % w1_.size()); + REQUIRE(l1.ptr()); + + if ((*p_rgen)() % 2 == 0) { + /* pick another root list cell at random, and link it to l1 */ + gp l2 = w1_.at((*p_rgen)() % w1_.size()); + REQUIRE(l2.ptr()); + + l1->assign_rest(l2); + } else { + /* pick a value at random (could be list or integer), + * assign to head + */ + gp x2 = w2_.at((*p_rgen)() % w2_.size()); + REQUIRE(x2.ptr()); + + l1->assign_head(x2); + } + } + } + + void RandomMutationModel::rejuvenate_seed_values(GC * gc) + { + for (std::size_t i = 0; i < w1_.size(); ++i) { + INFO(xtag("i", i)); + + if (w1_.at(i)->_is_forwarded()) { + /* w[i] survived GC */ + w1_[i] = dynamic_cast(w1_[i]->_destination()); + } else { + /* w[i] is garbage, replace */ + w1_[i] = List::cons(List::nil, List::nil); + } + REQUIRE(w1_[i].ptr()); + } + + for (std::size_t j = 0; j < w2_.size(); ++j) { + INFO(xtag("j", j)); + + if (w2_.at(j)->_is_forwarded()) { + /* w2[i] survived GC */ + w2_[j] = Object::from(w2_[j]->_destination()); + REQUIRE(w2_[j].ptr()); + } else { + /* w2[j] is garbage, replace */ + w2_[j] = Integer::make(gc, (this->start_)++); + REQUIRE(w2_[j].ptr()); + } + } + } + + void RandomMutationModel::alter_random_roots(xoshiro256ss * p_rgen) + { + /* replace a root value rr times */ + for (std::size_t i = 0; i < rr_; ++i) { + /* choose new root value at random */ + gp new_root; + { + std::size_t j = (*p_rgen)() % (w1_.size() + w2_.size()); + + if (j < w1_.size()) + new_root = w1_.at(j); + else + new_root = w2_.at(j - w1_.size()); + } + + /* choose a root to replace at random */ + std::size_t j = (*p_rgen)() % root_v_.size(); + + root_v_[j] = new_root; + } + } + + struct testcase_stresstest { + testcase_stresstest(std::size_t nz, std::size_t tz, + std::size_t ngct, std::size_t tgct, + std::size_t m, std::size_t n, + std::size_t r, std::size_t rr, std::size_t k, + bool gc_stats_flag, bool debug_flag) + : nursery_z_{nz}, tenured_z_{tz}, nursery_gc_threshold_{ngct}, tenured_gc_threshold_{tgct}, + m_{m}, n_{n}, r_{r}, rr_{rr}, k_{k}, + gc_stats_flag_{gc_stats_flag}, debug_flag_{debug_flag} + {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + std::size_t nursery_gc_threshold_; + std::size_t tenured_gc_threshold_; + + /* #of random list cells to create */ + std::size_t m_; + /* #of random integers to create */ + std::size_t n_; + /* #of gc roots to create */ + std::size_t r_; + /* #of gc roots to replace between cycles */ + std::size_t rr_; + /* #of random mutations */ + std::size_t k_; + + bool gc_stats_flag_ = false; + bool debug_flag_ = false; + }; + + std::vector s_testcase_v = + { + // segfault with + + /* nz: nursery size + * tz: tenured size + * ngct: nursery gc threshold + * tgct: tenured gc threshold + * m: #of random list cells to create + * n: #of random integers to create + * r: #of gc roots to create + * rr: #of gc roots to replace between iterations + * k: #of random mutations to apply + * + * nz tz ngct tgct m n r rr k stats, debug */ + testcase_stresstest(1024, 2048, 256, 1024, 2, 0, 0, 0, 0, false, false), + testcase_stresstest(1024, 2048, 256, 1024, 2, 1, 5, 0, 0, false, false), + testcase_stresstest(1024, 2048, 256, 1024, 5, 2, 5, 2, 10, false, false), + testcase_stresstest(1024, 2048, 256, 1024, 10, 10, 5, 2, 10, false, false) + }; + } /*namespace*/ + + TEST_CASE("gc-stresstest", "[alloc][gc][gc_mutation]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const testcase_stresstest & tc = s_testcase_v[i_tc]; + + scope log(XO_DEBUG(tc.gc_stats_flag_)); + + up gc = GC::make( + { + .initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.nursery_gc_threshold_, + .full_gc_threshold_ = tc.tenured_gc_threshold_, + .stats_flag_ = tc.gc_stats_flag_, + .debug_flag_ = tc.debug_flag_ + }); + gc->disable_gc(); + + REQUIRE(gc->native_gc_statistics().n_mutation_ == 0); + REQUIRE(gc->native_gc_statistics().n_logged_mutation_ == 0); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + // Plan: + // - create vector of m cons cells w1[]. + // - prepend w1[] to a vector of n integers; call this w2[]. + // - create vector root_v[] of r gc roots. Assign each root_v[j] to some random w2[i] + // - make some random mutations. + // - traverse root_v[] to construct model from_model for reachable objects + // - run gc + // - traverse root_v[] again, to construct to_model for eachable objects + // - verify from_model ~=~ to_model + + uint64_t seed = 8365237040761243362UL; + //Seed seed; // to seed from /dev/random + //std::cerr << "seed=" << seed << std::endl; + auto rgen = xoshiro256ss(seed); + + REQUIRE(tc.m_ > 0); + //REQUIRE(tc.n_ > 0); + //REQUIRE(tc.r_ > 0); + + // data_model: generate some random data, to exercise GC + RandomMutationModel data_model(tc.m_, tc.n_, tc.r_, tc.rr_, tc.k_); + + for (std::size_t cycle = 0; cycle < 3; ++cycle) { + INFO(xtag("cycle", cycle)); + + if (cycle == 0) { + data_model.generate_seed_values(gc.get()); + data_model.generate_random_roots(gc.get(), &rgen); + } else { + /* figure out values in {data_model_.w1_, data_model_.w2_} that + * survived GC; keep these. Discard the remainder. + * don't want these as roots, because that would alter the behavior of GC. + * + * (For example want to verify behavior of GC w.r.t. cells that are alive only + * because of a mutation) + */ + data_model.rejuvenate_seed_values(gc.get()); + data_model.alter_random_roots(&rgen); + } + + data_model.generate_random_mutations(&rgen); + + log && log(xtag("cycle", cycle), + xtag("stats.before", gc->get_gc_statistics())); + + /* make model for contents of w2[] - baseline for post-GC comparison */ + ObjectGraphModel from_model; + from_model.from_root_vector(data_model.root_v_); + + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + /* collector cycle changed object addresses. + * build a new object model, and verify consiste1ncy with from_model + */ + ObjectGraphModel to_model; + to_model.from_root_vector(data_model.root_v_); + + REQUIRE(ObjectGraphModel::verify_equal_models(from_model, to_model)); + + log && log(xtag("cycle", cycle), + xtag("stats.after", gc->get_gc_statistics())); + } + } + } + } /*namespace ut*/ +} /*namespace xo*/ + +/* end GC.test.cpp */ diff --git a/xo-object/utest/Integer.test.cpp b/xo-object/utest/Integer.test.cpp new file mode 100644 index 00000000..f68ddd48 --- /dev/null +++ b/xo-object/utest/Integer.test.cpp @@ -0,0 +1,60 @@ +/* @file Integer.test.cpp + * + * author Roland Conybeare, Aug 2025 + */ + +#include "xo/object/Integer.hpp" +#include "xo/alloc/GC.hpp" +#include + +namespace xo { + using xo::gc::GC; + using xo::gc::generation_result; + using xo::obj::Integer; + + namespace ut { + TEST_CASE("Integer", "[Integer]") + { + up gc = GC::make( + { .initial_nursery_z_ = 1024, + .initial_tenured_z_ = 2048, + .incr_gc_threshold_ = 512, + .full_gc_threshold_ = 512, + }); + + REQUIRE(gc.get()); + + Object::mm = gc.get(); + + gp i1 = Integer::make(gc.get(), 123); + gp i2 = Integer::make(gc.get(), -321); + + REQUIRE(i1->value() == 123); + REQUIRE(i2->value() == -321); + + REQUIRE(i1->_shallow_size() == sizeof(Integer)); + REQUIRE(i2->_shallow_size() == sizeof(Integer)); + + REQUIRE(gc->tospace_generation_of(i1.ptr()) == generation_result::nursery); + REQUIRE(gc->tospace_generation_of(i2.ptr()) == generation_result::nursery); + + REQUIRE(i1->self_tp().td()->short_name() == "Integer"); + REQUIRE(i2->self_tp().td()->short_name() == "Integer"); + + { + std::stringstream ss; + ss << i1; + REQUIRE(ss.str() == "123"); + } + + { + std::stringstream ss; + ss << i2; + REQUIRE(ss.str() == "-321"); + } + } + + } /*namespace ut*/ +} /*namespace xo*/ + +/* Integer.test.cpp */ diff --git a/xo-object/utest/List.test.cpp b/xo-object/utest/List.test.cpp new file mode 100644 index 00000000..f2c75de7 --- /dev/null +++ b/xo-object/utest/List.test.cpp @@ -0,0 +1,348 @@ +/* @file List.test.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/object/List.hpp" +#include "xo/object/String.hpp" +#include "xo/object/Integer.hpp" +#include "xo/alloc/GC.hpp" +#include "xo/alloc/ArenaAlloc.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/vector.hpp" +#include "xo/indentlog/print/tag.hpp" +#include +#include +#include +#include + +namespace xo { + namespace ut { + using xo::obj::List; + using xo::obj::String; + using xo::obj::Integer; + using xo::gc::GC; + using xo::gc::generation_result; + using xo::gc::generation; + using xo::gc::ArenaAlloc; + + namespace { + struct Testcase_List { + Testcase_List(std::size_t nz, std::size_t tz, std::size_t ngct, std::size_t tgct, + const std::vector> & v) + : nursery_z_{nz}, tenured_z_{tz}, incr_gc_threshold_{ngct}, full_gc_threshold_{tgct}, v_{v} + {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + std::size_t incr_gc_threshold_; + std::size_t full_gc_threshold_; + + std::vector> v_; + + std::string expect_display_; + }; + + std::vector + s_testcase_v = { + Testcase_List(1024, 2048, 512, 1024, {{}}), + Testcase_List(2048, 4096, 512, 1024, {{"hello", ", ", " world!"}}), + Testcase_List(2048, 4096, 512, 1024, {{"the", " quick", " brown", "fox", "jumps"}, + {"over", " the", " lazy", " dog!"}}) + }; + } + + TEST_CASE("List", "[List][gc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_List & tc = s_testcase_v[i_tc]; + + constexpr bool c_debug_flag = false; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.incr_gc_threshold_, + .full_gc_threshold_ = tc.full_gc_threshold_, + .debug_flag_ = c_debug_flag}); + gc->disable_gc(); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + { + scope log(XO_DEBUG(c_debug_flag)); + log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size())); + + std::vector> root_v(tc.v_.size()); + std::size_t i = 0; + + /* auditing List::_shallow_size(), String::_shallow_size() + * vs GC::allocated() + */ + std::size_t expected_alloc_z = 0; + + // TODO: consolidate: root setup shared with "List" unit test + + REQUIRE(gc->allocated() == expected_alloc_z); + + /* construct example Lists from testcase info */ + for (const std::vector & v : tc.v_) + { + INFO(xtag("v", v)); + + /* building l1 in reverse order */ + gp l1 = List::nil; + + for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) { + INFO(xtag("ip1", ip1)); + + const std::string & si = v.at(ip1 - 1); + + log && log(xtag("i", ip1-1), xtag("si", si)); + + gp sobj = String::copy(si.c_str()); + + std::size_t sobj_z = sobj->_shallow_size(); + expected_alloc_z += sobj_z; + + REQUIRE(gc->allocated() == expected_alloc_z); + + l1 = List::cons(sobj, l1); + + log && log(xtag("l1.size", l1->size())); + + std::size_t l1_z = l1->_shallow_size(); + expected_alloc_z += l1_z; + + REQUIRE(gc->allocated() == expected_alloc_z); + } + + REQUIRE(l1->is_nil() == (v.size() == 0)); + REQUIRE(l1->size() == v.size()); + + root_v[i] = l1; + gc->add_gc_root(reinterpret_cast(root_v[i].ptr_address())); + + REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + ++i; + } + + /* gc responsible for a bunch of list objects; + * all are roots and should be preserved + */ + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + /* verify GC preserved list structure and contents */ + for (std::size_t i = 0, n = root_v.size(); i < n; ++i) { + std::size_t nj = tc.v_.at(i).size(); + + REQUIRE(root_v.at(i)->size() == nj); + if (!(root_v.at(i)->is_nil())) + REQUIRE(gc->contains(reinterpret_cast(root_v.at(i).ptr()))); + + for (std::size_t j = 0; j < nj; ++j) { + gp s = String::from(root_v.at(i)->list_ref(j)); + REQUIRE(s.ptr()); + REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); + + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::nursery); + } + } + + /* every has survived one GC cycle. collect again should promote */ + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + /* verify GC preserved list structure and contents */ + for (std::size_t i = 0, n = root_v.size(); i < n; ++i) { + std::size_t nj = tc.v_.at(i).size(); + + REQUIRE(root_v.at(i)->size() == nj); + if (!(root_v.at(i)->is_nil())) + REQUIRE(gc->contains(reinterpret_cast(root_v.at(i).ptr()))); + + for (std::size_t j = 0; j < nj; ++j) { + gp s = String::from(root_v.at(i)->list_ref(j)); + REQUIRE(s.ptr()); + REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); + + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::tenured); + } + } + + REQUIRE(gc->native_gc_statistics().total_promoted_ == gc->allocated()); + + gc->request_gc(generation::tenured); + gc->enable_gc_once(); + + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 2); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 1); + REQUIRE(gc->allocated() == expected_alloc_z); + + /* verify GC preserved list structure and contents */ + for (std::size_t i = 0, n = root_v.size(); i < n; ++i) { + std::size_t nj = tc.v_.at(i).size(); + + REQUIRE(root_v.at(i)->size() == nj); + if (!(root_v.at(i)->is_nil())) + REQUIRE(gc->contains(reinterpret_cast(root_v.at(i).ptr()))); + + for (std::size_t j = 0; j < nj; ++j) { + gp s = String::from(root_v.at(i)->list_ref(j)); + REQUIRE(s.ptr()); + REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); + + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::tenured); + } + } + + log && log("stats", gc->native_gc_statistics()); + } + } + } /*TEST_CASE(List, ..)*/ + + TEST_CASE("List-cyclic", "[List][gc][cycles]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_List & tc = s_testcase_v[i_tc]; + + constexpr bool c_debug_flag = false; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.incr_gc_threshold_, + .full_gc_threshold_ = tc.full_gc_threshold_, + .debug_flag_ = c_debug_flag}); + + REQUIRE(gc.get()); + + gc->disable_gc(); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + { + scope log(XO_DEBUG(c_debug_flag)); + log && log(xtag("i_tc", i_tc), xtag("tc.v_.size", tc.v_.size())); + + std::vector> root_v(tc.v_.size()); + std::size_t i = 0; + + std::size_t expected_alloc_z = 0; + + // TODO: consolidate: root setup shared with "List" unit test + + /* construct example Lists from testcase info */ + for (const std::vector & v : tc.v_) + { + /* building l1 in reverse order */ + gp l1 = List::nil; + gp last = List::nil; + + for (std::size_t ip1 = v.size(); ip1 > 0; --ip1) { + const std::string & si = v.at(ip1 - 1); + + log && log(xtag("i", ip1-1), xtag("si", si)); + gp sobj = String::copy(si.c_str()); + + l1 = List::cons(sobj, l1); + + log && log(xtag("l1.size", l1->size())); + + if (ip1 == v.size()) { + // capture last + last = l1; + } + + std::size_t alloc_z = l1->_shallow_size() + l1->head()->_shallow_size(); + expected_alloc_z += alloc_z; + + // replace tail to make a cycle + if (last.ptr()) { + last->assign_rest(l1); + } + } + + REQUIRE(l1->is_nil() == (v.size() == 0)); + //REQUIRE(l1->size() == v.size()); // lwill loop forever + + root_v[i] = l1; + gc->add_gc_root(reinterpret_cast(root_v[i].ptr_address())); + + REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + ++i; + } + + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + /* verify GC preserved list structure and contents */ + for (std::size_t i = 0, n = root_v.size(); i < n; ++i) { + std::size_t nj = tc.v_.at(i).size(); + + // REQUIRE(root_v.at(i)->size() == nj); // will loop forever + if (!(root_v.at(i)->is_nil())) + REQUIRE(gc->contains(reinterpret_cast(root_v.at(i).ptr()))); + + for (std::size_t j = 0; j < nj; ++j) { + gp s = String::from(root_v.at(i)->list_ref(j)); + REQUIRE(s.ptr()); + REQUIRE(strcmp(s->c_str(), tc.v_.at(i).at(j).c_str()) == 0); + + REQUIRE(gc->tospace_generation_of(reinterpret_cast(s.ptr())) + == generation_result::nursery); + } + + REQUIRE(root_v.at(i)->list_ref(nj).ptr() == root_v.at(i)->list_ref(0).ptr()); + } + } + } + } + + TEST_CASE("List.display", "[List]") + { + constexpr bool c_debug_flag = false; + + up alloc = ArenaAlloc::make("arena", 1024, c_debug_flag); + + ArenaAlloc * mm = alloc.get(); + Object::mm = mm; + + gp l = List::list(Integer::make(mm, 1), Integer::make(mm, 2), Integer::make(mm, 3)); + + REQUIRE(l->size() == 3); + + std::stringstream ss; + ss << l; + + REQUIRE(ss.str() == "(1 2 3)"); + } + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end List.test.cpp */ diff --git a/xo-object/utest/String.test.cpp b/xo-object/utest/String.test.cpp new file mode 100644 index 00000000..15b90748 --- /dev/null +++ b/xo-object/utest/String.test.cpp @@ -0,0 +1,236 @@ +/* @file String.test.cpp + * + * author: Roland Conybeare, Aug 2025 + */ + +#include "xo/object/String.hpp" +#include "xo/alloc/GC.hpp" +#include "xo/alloc/ArenaAlloc.hpp" +#include "xo/indentlog/scope.hpp" +#include "xo/indentlog/print/quoted.hpp" +#include +#include +#include + +namespace xo { + using xo::gc::IAlloc; + using xo::gc::GC; + using xo::gc::ArenaAlloc; + using xo::gc::generation; + using xo::obj::String; + + namespace ut { + + namespace { + struct Testcase_String { + Testcase_String(std::size_t nz, + std::size_t tz, + std::size_t ngct, + std::size_t tgct, + const std::vector & v) : nursery_z_{nz}, + tenured_z_{tz}, + incr_gc_threshold_{ngct}, + full_gc_threshold_{tgct}, + v_{v} {} + + std::size_t nursery_z_; + std::size_t tenured_z_; + std::size_t incr_gc_threshold_; + std::size_t full_gc_threshold_; + + std::vector v_; + }; + + std::vector + s_testcase_v = { + Testcase_String(1024, 4096, 512, 512, {"hello"}), + // in emacs: C-x 8 RET lambda + // + Testcase_String(1024, 4096, 512, 512, {"λ"}), + Testcase_String(1024, 4096, 512, 512, {"hello", ", world!"}) + }; + } + + TEST_CASE("String", "[String][gc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_String & tc = s_testcase_v[i_tc]; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.incr_gc_threshold_, + .full_gc_threshold_ = tc.full_gc_threshold_, + .debug_flag_ = false}); + + REQUIRE(gc.get()); + + gc->disable_gc(); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + { + std::size_t n_string = 0; + std::size_t expected_alloc_z = 0; + + for (const std::string & s_str : tc.v_) + { + gp s1 = String::copy(s_str.c_str()); + + ++n_string; + /* 1+ for mandatory null terminator */ + expected_alloc_z += (IAlloc::with_padding(sizeof(String)) + + IAlloc::with_padding(1 + s_str.length())); + + REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + REQUIRE(s1->length() == s_str.length()); + REQUIRE(strcmp(s1->c_str(), s_str.c_str()) == 0); + } + + /* gc has n_string objects. Nothing refers to them, so gc going to kill all */ + gc->request_gc(generation::nursery); + gc->enable_gc_once(); + + REQUIRE(gc->gc_in_progress() == false); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + REQUIRE(gc->allocated() == 0); + REQUIRE(gc->native_gc_statistics().total_allocated_ == expected_alloc_z); + } + } + } + + TEST_CASE("String2", "[String][gc]") + { + for (std::size_t i_tc = 0, n_tc = s_testcase_v.size(); i_tc < n_tc; ++i_tc) { + const Testcase_String & tc = s_testcase_v[i_tc]; + + up gc = GC::make( + {.initial_nursery_z_ = tc.nursery_z_, + .initial_tenured_z_ = tc.tenured_z_, + .incr_gc_threshold_ = tc.incr_gc_threshold_, + .full_gc_threshold_ = tc.full_gc_threshold_, + .debug_flag_ = false}); + + REQUIRE(gc.get()); + + /* use gc for all Object allocs */ + Object::mm = gc.get(); + + { + scope log(XO_DEBUG(false)); + + std::size_t n_string = 0; + std::size_t expected_alloc_z = 0; + + std::vector> sv(tc.v_.size()); + + std::size_t i = 0; + + for (const std::string & s_str : tc.v_) + { + sv[i] = String::copy(s_str.c_str()); + + ++n_string; + /* 1+ for mandatory null terminator */ + std::size_t alloc_z = (IAlloc::with_padding(sizeof(String)) + + IAlloc::with_padding(1 + s_str.length())); + expected_alloc_z += alloc_z; + + log && log(xtag("s_str", xo::print::unq(s_str)), + xtag("s_str.length", s_str.length()), + xtag("alloc_z", alloc_z)); + log && log(xtag("expected_alloc_z", expected_alloc_z)); + + gc->add_gc_root(reinterpret_cast(sv[i].ptr_address())); + + REQUIRE(gc->allocated() % sizeof(std::uintptr_t) == 0); + REQUIRE(gc->allocated() == expected_alloc_z); + + REQUIRE(sv[i]->length() == s_str.length()); + REQUIRE(strcmp(sv[i]->c_str(), s_str.c_str()) == 0); + + ++i; + } + + /* gc has a bunch of string objects; all are roots + should be preserved */ + gc->request_gc(generation::nursery); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::nursery)].n_gc_ == 1); + REQUIRE(gc->native_gc_statistics().gen_v_[gen2int(generation::tenured)].n_gc_ == 0); + + REQUIRE(gc->allocated() == expected_alloc_z); + + for (std::size_t i = 0, n = sv.size(); i < n; ++i) { + REQUIRE(gc->contains(reinterpret_cast(sv.at(i).ptr()))); + REQUIRE(strcmp(sv.at(i)->c_str(), tc.v_[i].c_str()) == 0); + } + } + } + } + + TEST_CASE("String.columns", "[String][unicode]") + { + const bool c_debug_flag = false; + up arena = ArenaAlloc::make("testarena", + 16*1024, c_debug_flag); + + Object::mm = arena.get(); + + gp s0 = String::copy(""); + + REQUIRE(s0->columns() == 0); + REQUIRE(s0->length() == 0); + + gp s1 = String::copy("l"); + + REQUIRE(s1->columns() == 1); + REQUIRE(s1->length() == 1); + + gp s2 = String::copy("λ"); + + REQUIRE(s2->columns() == 1); + /* two code units in code point */ + REQUIRE(s2->length() == 2); + } + + TEST_CASE("String.append", "[String]") + { + const bool c_debug_flag = false; + up arena = ArenaAlloc::make("testarena", + 16*1024, c_debug_flag); + + Object::mm = arena.get(); + + gp s1 = String::share("the"); + gp s2 = String::share(" quick"); + + gp s3 = String::append(s1, s2); + + REQUIRE(::strcmp(s1->c_str(), "the") == 0); + REQUIRE(::strcmp(s2->c_str(), " quick") == 0); + REQUIRE(s3.ptr()); + REQUIRE(s3->length() == s1->length() + s2->length()); + REQUIRE(::strcmp(s1->c_str(), "the quick")); + + { + std::stringstream ss1; + ss1 << s1; + REQUIRE(ss1.str() == "\"the\""); + } + + /* on printing, escape embedded " chars */ + { + std::stringstream ss4; + ss4 << String::share("\"Allo!\", he said"); + REQUIRE(ss4.str() == "\"\\\"Allo!\\\", he said\""); + } + } + + + } /*namespace ut*/ +} /*namespace xo*/ + +/* end String.test.cpp */ diff --git a/xo-object/utest/object_utest_main.cpp b/xo-object/utest/object_utest_main.cpp new file mode 100644 index 00000000..54a9e38a --- /dev/null +++ b/xo-object/utest/object_utest_main.cpp @@ -0,0 +1,6 @@ +/* file object_utest_main.cpp */ + +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" + +/* end object_utest_main.cpp */