diff --git a/docs/dev/lcp.md b/docs/dev/lcp.md index 6c3021782..83180b428 100644 --- a/docs/dev/lcp.md +++ b/docs/dev/lcp.md @@ -23,6 +23,7 @@ Contents - [Defining an RPC](#defining-an-rpc) - [Cap'n Proto Serialization](#capnp-serial) - [SaveLoadKit Serialization](#slk-serial) + - [Object Cloning](#object-cloning) ## Running LCP @@ -1014,3 +1015,237 @@ example: (:serialize (:slk))) ``` +### Object Cloning + +LCP supports automatic generation of cloning (deep copy) code for user-defined +classes. + +A textbook example of an object that would require a deep copy functionality is +a tree structure. The following class represents a node in the binary tree, +carrying an integer value and having pointers to its two children: + +```lisp +(lcp:define-class node () + ((value :int32_t) + (left "std::unique_ptr") + (right "std::unique_ptr")) + (:clone :return-type (lambda (typename) + #>cpp + std::unique_ptr<${typename}> + cpp<#) + :init-object (lambda (var typename) + #>cpp + auto ${var} = std::make_unique<${typename}>(); + cpp<#))) +``` + +The above will generate the following C++ class with a `Clone` function that +can be used for making a deep copy of the binary tree structure: + +```cpp +class Node { + public: + std::unique_ptr Clone() const { + auto object = std::make_unique(); + object->value_ = value_; + object->left_ = left_ ? left_->Clone() : nullptr; + object->right_ = right_ ? right_->Clone() : nullptr; + return object; + } + + private: + int32_t value_; + std::unique_ptr left_; + std::unique_ptr right_; +}; +``` + +To specify that a class is deep copyable, `:clone` class option must be passed. +We have already seen two options that `:clone` accepts: `:return-type` and +`:init-object`. + +`:return-type` expects a function that takes a single argument which is the C++ +type name of the class and produces C++ code, which is a valid C++ type +delcaration. Here we used it to specify that `Clone` function should return a +`std::unique_ptr` to the newly created `Node` to override the default behavior. +When `:return-type` option is not provided and class `T` is a member of an +inheritance hierarchy, `Clone` will return `std::unique_ptr`, where +`Base` is the root of that hierarchy. If `T` is not a member of inheritance +hierarchy, `Clone` will return `T` by default. + +`:init-object` expects a function that takes two arguments, first is a variable +name, and the second one is the C++ type name of the class. It must produce C++ +code that initializes an object with the given name of the same type that +`Clone` function returns. Here we had to use it since we are overriding the +default return value of `Clone`. Unless `:init-object` argument is provided, an +object of type `T` will be instantiated with `auto object = +std::make_unique();` if `T` is a member of inheritance hierarchy, and `T +object;` if it is not. As you can see, deep copyable objects must be default +constructible. + + +#### Single Inheritance + +LCP supports deep copying of classes with single inheritance. The root class +will have a virtual `Clone` function that child classes will override. For +example: + +```lisp +(lcp:define-class base () + ((member :int32_t)) + (:clone)) + +(lcp:define-class derived (base) + ((another-member :int32_t)) + (:clone)) +``` + +We get the following code: + +```cpp +class Base { + public: + virtual std::unique_ptr Clone() const { + auto object = std::make_unique(); + object->member_ = member_; + return object; + } + + private: + int32_t member_; +}; + +class Derived : public Base { + public: + std::unique_ptr Clone() const override { + auto object = std::make_unique(); + object->member_ = member_; + object->another_member_ = another_member_; + return object; + } + + private: + int32_t another_member_; +}; +``` + +Notice that the `Clone` function of derived class also returns +`std::unique_ptr`, because C++ doesn't support return type covariance +with smart pointers. + +#### Multiple Inheritance + +Deep copying of classes with multiple inheritance is *not* supported! + +Usually, multiple inheritance is used to satisfy some interface which doesn't +carry data. In such cases, you can ignore the multiple inheritance by +specifying `:ignore-other-base-classes` option. For example: + +```lisp +(lcp:define-class derived (primary-base some-interface ...) + ... + (:clone :ignore-other-base-classes t)) +``` + +The above will produce deep copying code as if `derived` is inheriting *only* +from `primary-base`. + +#### Templated Types + +Deep copying of templated types is *not* supported! + +#### Custom Clone Hooks + +In cases when default deep copying code is not adequate, you may wish to +provide your own. LCP provides `:clone` option that can be specified for each +member. + +These hooks for custom copying expect a function with two arguments, `source` +and `dest`, representing the member location in the cloned struct and member +location in the new struct. This allows to have a more generic function which +works with any member of some type. The return value of the function needs to +be C++ code. + +It is also possible to specify that a member is cloned by copying by passing +`:copy` instead of a function as an argument to `:clone`. + +```lisp +(lcp:define-class my-class () + ((callback "std::function" + :clone :copy) + (widget "Widget" + :clone (lambda (source dest) + #>cpp + ${dest} = WidgetFactory::Create(${source}.type()); + cpp<#))) + (:clone)) +``` + +#### Additional Arguments to Generated Clone Function + +By default, `Clone` function takes no argument. In some cases we would like to +accept additional arguments necessary to create a deep copy. Let's see how this +is done in LCP using the `:args` option. + +`:args` expects a list of pairs. Each pair designates one argument. The first +element of pair is the argument name and the second is the C++ type of that +argument. + +One case where we want to pass additional arguments to `Clone` is when there is +another object that owns all objects being cloned. For example, `AstStorage` +owns all Memgraph AST nodes. For that reason, `Clone` function of all AST node +types takes an `AstStorage \*` argument. Here's a snippet from the actual AST +code: + +```lisp +(lcp:define-class tree () + ((uid :int32_t)) + (:abstractp t) + ... + (:clone :return-type (lambda (typename) + (format nil "~A*" typename)) + :args '((storage "AstStorage *")) + :init-object (lambda (var typename) + (format nil "~A* ~A = storage->Create<~A>();" + typename var typename)))) + +(lcp:define-class expression (tree) + () + (:abstractp t) + ... + (:clone)) + +(lcp:define-class where (tree) + ((expression "Expression *" :initval "nullptr" :scope :public)) + (:clone)) +``` + +`:args` option is only passed to the root class in inheritance hierarchy. By +default, the same extra arguments will be passed to all class members that are +cloned using `Clone` metehod. The generated code is: + +```cpp +class Tree { + public: + virtual Tree *Clone(AstStorage *storage) const = 0; + private: + int32_t uid_; +}; + +class Expression : public Tree { + public: + Expression *Clone(AstStorage *storage) const override = 0; +}; + +class Where : public Tree { + public: + Expression *expression_{nullptr}; + + Where *Clone(AstStorage *storage) const override { + Where *object = storage->Create(); + object->uid_ = uid_; + object->expression_ = expression_ ? expression_->Clone(storage) : nullptr; + return object; + } +}; +``` diff --git a/src/lisp/CMakeLists.txt b/src/lisp/CMakeLists.txt index 5cd68ec53..4eb37a953 100644 --- a/src/lisp/CMakeLists.txt +++ b/src/lisp/CMakeLists.txt @@ -6,6 +6,7 @@ set(lcp_src_files ${CMAKE_SOURCE_DIR}/src/lisp/lcp-compile ${CMAKE_SOURCE_DIR}/src/lisp/package.lisp ${CMAKE_SOURCE_DIR}/src/lisp/types.lisp + ${CMAKE_SOURCE_DIR}/src/lisp/clone.lisp ${CMAKE_SOURCE_DIR}/src/lisp/code-gen.lisp ${CMAKE_SOURCE_DIR}/src/lisp/slk.lisp ${CMAKE_SOURCE_DIR}/src/lisp/lcp.lisp @@ -64,6 +65,7 @@ macro(define_add_lcp name main_src_files generated_lcp_files) ${CMAKE_SOURCE_DIR}/src/lisp/lcp-compile ${CMAKE_SOURCE_DIR}/src/lisp/package.lisp ${CMAKE_SOURCE_DIR}/src/lisp/types.lisp + ${CMAKE_SOURCE_DIR}/src/lisp/clone.lisp ${CMAKE_SOURCE_DIR}/src/lisp/code-gen.lisp ${CMAKE_SOURCE_DIR}/src/lisp/slk.lisp ${CMAKE_SOURCE_DIR}/src/lisp/lcp.lisp diff --git a/src/lisp/clone.lisp b/src/lisp/clone.lisp new file mode 100644 index 000000000..78e7597c6 --- /dev/null +++ b/src/lisp/clone.lisp @@ -0,0 +1,255 @@ +(in-package #:lcp.clone) + +(defvar *variable-idx* 0 "Used to generate unique variable names") + +(defmacro with-vars (vars &body body) + "Generates unique variable names for use in generated code by +appending an index to desired variable names. Useful when generating +loops which might reuse counter names. + +Usage example: + (with-vars ((loop-counter \"i\")) + (format nil \"for (auto ~A = 0; ~A < v.size(); ++~A) { + // do something + }\" + loop-counter loop-counter loop-counter))" + `(let* ((*variable-idx* (1+ *variable-idx*)) + ,@(loop for var in vars collecting + `(,(first var) + (format nil "~A~A" ,(second var) *variable-idx*)))) + ,@body)) + +(define-condition clone-error (error) + ((message :type string :initarg :message :reader clone-error-message) + (format-args :type list :initform nil :initarg :format-args :reader clone-error-format-args)) + (:report (lambda (condition stream) + (apply #'format stream + (clone-error-message condition) + (clone-error-format-args condition))))) + +(defun clone-error (message &rest format-args) + (error 'clone-error :message message :format-args format-args)) + +(defun cloning-parent (cpp-class) + (let ((supers (lcp::cpp-class-super-classes cpp-class)) + (opts (lcp::cpp-class-clone-opts cpp-class))) + (unless opts + (clone-error "Class ~A isn't cloneable" (lcp::cpp-type-base-name cpp-class))) + (cond + ((lcp::clone-opts-base opts) nil) + ((lcp::clone-opts-ignore-other-base-classes opts) (car supers)) + (t + (when (> (length supers) 1) + (clone-error "Cloning doesn't support multiple inheritance (class '~A', parents: '~A')" + (lcp::cpp-type-base-name cpp-class) supers)) + (car supers))))) + +(defun cloning-root (cpp-class) + (let ((parent-class (cloning-parent cpp-class))) + (if parent-class + (cloning-root (lcp::find-cpp-class parent-class)) + cpp-class))) + +(defun members-for-cloning (cpp-class) + (do ((current-class cpp-class) members) + ((not current-class) members) + (setf members (append (remove-if-not #'lcp::cpp-member-clone + (lcp::cpp-class-members current-class)) + members)) + (setf current-class (lcp::find-cpp-class (cloning-parent current-class))))) + +(defun copy-object (source-name dest-name) + (format nil "~A = ~A;" dest-name source-name)) + +(defun clone-by-copy-p (object-type) + (cond + ((string= "vector" (lcp::cpp-type-name object-type)) + (clone-by-copy-p (car (lcp::cpp-type-type-args object-type)))) + ((string= "optional" (lcp::cpp-type-name object-type)) + (clone-by-copy-p (car (lcp::cpp-type-type-args object-type)))) + ((string= "unordered_map" (lcp::cpp-type-name object-type)) + (and (clone-by-copy-p (first (lcp::cpp-type-type-args object-type))) + (clone-by-copy-p (second (lcp::cpp-type-type-args object-type))))) + ((string= "pair" (lcp::cpp-type-name object-type)) + (and (clone-by-copy-p (first (lcp::cpp-type-type-args object-type))) + (clone-by-copy-p (second (lcp::cpp-type-type-args object-type))))) + ((lcp::cpp-type-type-args object-type) nil) + (t (or + (lcp::find-cpp-enum (lcp::cpp-type-name object-type)) + (typep object-type 'lcp::cpp-primitive-type) + (string= "string" (lcp::cpp-type-name object-type)) + (not (lcp::find-cpp-class (lcp::cpp-type-name object-type))) + (not (lcp::cpp-class-clone-opts + (lcp::find-cpp-class (lcp::cpp-type-name object-type)))))))) + +(defun clone-object (object-type source-name dest-name &key args) + (let ((object-type + (ctypecase object-type + (lcp::cpp-type object-type) + (string (lcp::parse-cpp-type-declaration object-type)) + (symbol (lcp::cpp-type object-type)))) + (arg-list (format nil "~{~A~^, ~}" + (mapcar (lambda (name-and-type) + (lcp::cpp-variable-name (first name-and-type))) + args)))) + (cond + ((clone-by-copy-p object-type) + (copy-object source-name dest-name)) + ((lcp::cpp-pointer-type-p object-type) + (format nil "~A = ~A ? ~A->Clone(~A) : nullptr;" + dest-name source-name source-name arg-list)) + ((string= "optional" (lcp::cpp-type-name object-type)) + (let ((value-type (car (lcp::cpp-type-type-args object-type)))) + (clone-optional value-type source-name dest-name :args args))) + ((string= "vector" (lcp::cpp-type-name object-type)) + (let ((elem-type (car (lcp::cpp-type-type-args object-type)))) + (clone-vector elem-type source-name dest-name :args args))) + ((string= "unordered_map" (lcp::cpp-type-name object-type)) + (let ((key-type (first (lcp::cpp-type-type-args object-type))) + (value-type (second (lcp::cpp-type-type-args object-type)))) + (clone-map key-type value-type source-name dest-name :args args))) + ((string= "pair" (lcp::cpp-type-name object-type)) + (let ((first-type (first (lcp::cpp-type-type-args object-type))) + (second-type (second (lcp::cpp-type-type-args object-type)))) + (clone-pair first-type second-type source-name dest-name :args args))) + ((and (lcp::find-cpp-class (lcp::cpp-type-name object-type)) + (lcp::cpp-class-clone-opts (lcp::find-cpp-class (lcp::cpp-type-name object-type)))) + (format nil "~A = ~A.Clone(~A);" dest-name source-name arg-list)) + (t + (format nil "static_assert(false, \"Don't know how to clone object of type ~A\");" + (lcp::cpp-type-decl object-type)))))) + +(defun clone-vector (elem-type source-name dest-name &key args) + (with-vars ((loop-counter "i")) + (format nil + "~A.resize(~A.size()); + for (auto ~A = 0; ~A < ~A.size(); ++~A) { ~A }" + dest-name source-name + loop-counter loop-counter source-name loop-counter + (clone-object elem-type + (format nil "~A[~A]" source-name loop-counter) + (format nil "~A[~A]" dest-name loop-counter) + :args args)))) + +(defun clone-map (key-type value-type source-name dest-name &key args) + (with-vars ((loop-var "kv") (entry-var "entry")) + (let ((entry-type (lcp::make-cpp-type "pair" + :namespace '("std") + :type-args (list key-type value-type)))) + (format nil + "for (const auto &~A : ~A) { + ~A ~A; + ~A + ~A.emplace(std::move(~A)); + }" + loop-var source-name + (lcp::cpp-type-decl entry-type) entry-var + (clone-object entry-type loop-var entry-var :args args) + dest-name entry-var)))) + +(defun clone-optional (value-type source-name dest-name &key args) + (with-vars ((value-var "value")) + (format nil + "if (~A) { + ~A ~A; + ~A + ~A.emplace(std::move(~A)); + } else { + ~A = std::experimental::nullopt; + }" + source-name + (lcp::cpp-type-decl value-type) value-var + (clone-object value-type + (format nil "(*~A)" source-name) + value-var + :args args) + dest-name value-var + dest-name))) + +(defun clone-pair (first-type second-type source-name dest-name &key args) + (with-vars ((first-var "first") (second-var "second")) + (with-output-to-string (cpp-out) + (lcp::with-cpp-block-output (cpp-out) + (format cpp-out + "~A ~A; + ~A + ~A ~A; + ~A + ~A = std::make_pair(std::move(~A), std::move(~A));" + (lcp::cpp-type-decl first-type) first-var + (clone-object first-type + (format nil "~A.first" source-name) + first-var + :args args) + (lcp::cpp-type-decl second-type) second-var + (clone-object second-type + (format nil "~A.second" source-name) + second-var + :args args) + dest-name first-var second-var)) + cpp-out))) + +(defun clone-function-definition-for-class (cpp-class) + (check-type cpp-class lcp::cpp-class) + (when (lcp::cpp-type-type-params cpp-class) + (clone-error "Don't know how to clone templated class '~A'" + (lcp::cpp-type-base-name cpp-class))) + (let* ((cloning-root (cloning-root cpp-class)) + (root-opts (lcp::cpp-class-clone-opts cloning-root)) + (inheritancep (or (lcp::direct-subclasses-of cpp-class) + (cloning-parent cpp-class))) + (return-type (cond + ((lcp::clone-opts-return-type root-opts) + (lcp::cpp-code + (funcall (lcp::clone-opts-return-type root-opts) + (lcp::cpp-type-name cpp-class)))) + (inheritancep (format nil "std::unique_ptr<~A>" + (lcp::cpp-type-name (cloning-root cpp-class)))) + (t (lcp::cpp-type-name cpp-class)))) + (declaration + (lcp::cpp-method-declaration cpp-class "Clone" + :args (lcp::clone-opts-args root-opts) + :returns return-type + :virtual (and inheritancep + (eq cpp-class cloning-root)) + :inline t + :const t + :override (and inheritancep + (not (eq cpp-class cloning-root))) + :delete (lcp::cpp-class-abstractp cpp-class)))) + (if (lcp::cpp-class-abstractp cpp-class) + (return-from clone-function-definition-for-class (format nil "~A;" declaration))) + (with-output-to-string (cpp-out) + (lcp::with-cpp-block-output (cpp-out :name declaration :semicolonp nil) + (let (object-access) + (cond + ((lcp::clone-opts-init-object root-opts) + (setf object-access "object->") + (write-line + (lcp::cpp-code + (funcall (lcp::clone-opts-init-object root-opts) + "object" (lcp::cpp-type-name cpp-class))) + cpp-out)) + (inheritancep + (setf object-access "object->") + (format cpp-out "~&auto object = std::make_unique<~A>();" + (lcp::cpp-type-name cpp-class))) + (t + (setf object-access "object.") + (format cpp-out "~&~A object;" + (lcp::cpp-type-name cpp-class)))) + (dolist (member (members-for-cloning cpp-class)) + (let* ((source (lcp::cpp-member-name member :struct (lcp::cpp-class-structp cpp-class))) + (dest (format nil "~A~A" object-access source))) + (cond + ((eq (lcp::cpp-member-clone member) :copy) + (format cpp-out "~&~A" (copy-object source dest))) + ((functionp (lcp::cpp-member-clone member)) + (format cpp-out "~&~A" + (lcp::cpp-code (funcall (lcp::cpp-member-clone member) source dest)))) + (t + (format cpp-out "~&~A" + (clone-object (lcp::cpp-member-type member) + source dest + :args (lcp::clone-opts-args root-opts))))))) + (format cpp-out "~&return object;")))))) diff --git a/src/lisp/lcp-test.lisp b/src/lisp/lcp-test.lisp index cc22f92a2..f731500f7 100644 --- a/src/lisp/lcp-test.lisp +++ b/src/lisp/lcp-test.lisp @@ -564,3 +564,379 @@ ((protected-member :int64_t :scope :protected) (public-member :char)))) 'lcp.slk:slk-error))) + +(deftest "clone" + (subtest "no inheritance" + (undefine-cpp-types) + (let ((tree-class (lcp:define-class tree () + ((value :int32_t) + (left "std::unique_ptr") + (right "std::unique_ptr")) + (:clone :return-type (lambda (typename) + (format nil "std::unique_ptr<~A>" typename)) + :init-object (lambda (var typename) + (format nil "auto ~A = std::make_unique<~A>();" + var typename))))) + (forest-class (lcp:define-class forest () + ((name "std::string") + (small-tree "std::unique_ptr") + (big-tree "std::unique_ptr")) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class tree-class) + "std::unique_ptr Clone() const { + auto object = std::make_unique(); + object->value_ = value_; + object->left_ = left_ ? left_->Clone() : nullptr; + object->right_ = right_ ? right_->Clone() : nullptr; + return object; + }") + (is-generated (lcp.clone:clone-function-definition-for-class forest-class) + "Forest Clone() const { + Forest object; + object.name_ = name_; + object.small_tree_ = small_tree_ ? small_tree_->Clone() : nullptr; + object.big_tree_ = big_tree_ ? big_tree_->Clone() : nullptr; + return object; + }"))) + (subtest "single inheritance" + (undefine-cpp-types) + ;; Simple case + (let ((base-class (lcp:define-class base () + ((int-member :int32_t) + (string-member "std::string")) + (:clone))) + (child-class (lcp:define-class child (base) + ((another-int-member :int64_t)) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class base-class) + "virtual std::unique_ptr Clone() const { + auto object = std::make_unique(); + object->int_member_ = int_member_; + object->string_member_ = string_member_; + return object; + }") + (is-generated (lcp.clone:clone-function-definition-for-class child-class) + "std::unique_ptr Clone() const override { + auto object = std::make_unique(); + object->int_member_ = int_member_; + object->string_member_ = string_member_; + object->another_int_member_ = another_int_member_; + return object; + }")) + (undefine-cpp-types) + ;; Abstract base class + (let ((base-class (lcp:define-class base () + ((int-member :int32_t) + (string-member "std::string")) + (:abstractp t) + (:clone))) + (child-class (lcp:define-class child (base) + ((another-int-member :int64_t)) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class base-class) + "virtual std::unique_ptr Clone() const = 0;") + (is-generated (lcp.clone:clone-function-definition-for-class child-class) + "std::unique_ptr Clone() const override { + auto object = std::make_unique(); + object->int_member_ = int_member_; + object->string_member_ = string_member_; + object->another_int_member_ = another_int_member_; + return object; + }")) + (undefine-cpp-types) + ;; :return-type and :init-object propagation + (let ((base-class (lcp:define-class base () + ((int-member :int32_t) + (string-member "std::string")) + (:abstractp t) + (:clone :return-type (lambda (typename) + (format nil "~A*" typename)) + :init-object (lambda (var typename) + (format nil "~A* ~A = GlobalFactory::Create();" + typename var))))) + (child-class (lcp:define-class child (base) + ((another-int-member :int64_t)) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class base-class) + "virtual Base *Clone() const = 0;") + (is-generated (lcp.clone:clone-function-definition-for-class child-class) + "Child *Clone() const override { + Child *object = GlobalFactory::Create(); + object->int_member_ = int_member_; + object->string_member_ = string_member_; + object->another_int_member_ = another_int_member_; + return object; + }")) + (undefine-cpp-types) + ;; inheritance with :ignore-other-base-classes and :base + (let ((base-class (lcp:define-class base ("utils::TotalOrdering") + ((int-member :int32_t) + (string-member "std::string")) + (:abstractp t) + (:clone :base t + :return-type (lambda (typename) + (format nil "~A*" typename)) + :init-object (lambda (var typename) + (format nil "~A* ~A = GlobalFactory::Create();" + typename var))))) + (child-class (lcp:define-class child (base "utils::TotalOrdering" "utils::TotalOrdering") + ((another-int-member :int64_t)) + (:clone :ignore-other-base-classes t)))) + (is-generated (lcp.clone:clone-function-definition-for-class base-class) + "virtual Base *Clone() const = 0;") + (is-generated (lcp.clone:clone-function-definition-for-class child-class) + "Child *Clone() const override { + Child *object = GlobalFactory::Create(); + object->int_member_ = int_member_; + object->string_member_ = string_member_; + object->another_int_member_ = another_int_member_; + return object; + }"))) + (subtest "extra args" + (undefine-cpp-types) + ;; extra arguments are always passed when calling `Clone` function + (let ((expression-class (lcp:define-class expression () + ((lhs "Expression *") + (rhs "Expression *")) + (:abstractp t) + (:clone :return-type (lambda (typename) + (format nil "~A*" typename)) + :init-object (lambda (var typename) + (format nil "~A* ~A = storage->Create<~A>();" + typename var typename)) + :args '((storage "ExpressionStorage *"))))) + (and-class (lcp:define-class and (expression) + () + (:clone))) + (or-class (lcp:define-class or (expression) + () + (:clone))) + (filter-class (lcp:define-class filter () + ((expressions "std::vector")) + (:clone :args '((exp-storage "ExpressionStorage *")))))) + (is-generated (lcp.clone:clone-function-definition-for-class expression-class) + "virtual Expression *Clone(ExpressionStorage *storage) const = 0;") + (is-generated (lcp.clone:clone-function-definition-for-class and-class) + "And *Clone(ExpressionStorage *storage) const override { + And *object = storage->Create(); + object->lhs_ = lhs_ ? lhs_->Clone(storage) : nullptr; + object->rhs_ = rhs_ ? rhs_->Clone(storage) : nullptr; + return object; + }") + (is-generated (lcp.clone:clone-function-definition-for-class or-class) + "Or *Clone(ExpressionStorage *storage) const override { + Or *object = storage->Create(); + object->lhs_ = lhs_ ? lhs_->Clone(storage) : nullptr; + object->rhs_ = rhs_ ? rhs_->Clone(storage) : nullptr; + return object; + }") + (is-generated (lcp.clone:clone-function-definition-for-class filter-class) + "Filter Clone(ExpressionStorage *exp_storage) const { + Filter object; + object.expressions_.resize(expressions_.size()); + for (auto i1 = 0; i1 < expressions_.size(); ++i1) { + object.expressions_[i1] = + expressions_[i1] ? expressions_[i1]->Clone(exp_storage) : nullptr; + } + return object; + }"))) + (subtest "unsupported" + ;; multiple inheritance + (undefine-cpp-types) + (lcp:define-class first-base () + ((int-member :int32_t)) + (:clone)) + (lcp:define-class second-base () + ((private-member :int32_t :scope :private)) + (:clone)) + (let ((child-class (lcp:define-class child (first-base second-base) + ((name "std::string")) + (:clone)))) + (is-error (lcp.clone:clone-function-definition-for-class child-class) + 'lcp.clone:clone-error)) + ;; template classes + (undefine-cpp-types) + (let ((container-class (lcp:define-class (my-container t-element) () + ((data "TElement *") + (size "size_t"))))) + (is-error (lcp.clone:clone-function-definition-for-class container-class) + 'lcp.clone:clone-error))) + (subtest "custom clone" + (undefine-cpp-types) + (let ((my-class (lcp:define-class my-class () + ((callback "std::function" :clone :copy) + (click-counter :int32_t :clone nil) + (widget "Widget" + :clone (lambda (source dest) + #>cpp + ${dest} = WidgetFactory::Create(${source}.type()); + cpp<#))) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class my-class) + "MyClass Clone() const { + MyClass object; + object.callback_ = callback_; + object.widget_ = WidgetFactory::Create(widget_.type()); + return object; + }"))) + (subtest "types" + (undefine-cpp-types) + (macrolet ((single-member-test (member expected) + (let ((class-sym (gensym))) + `(let ((,class-sym (lcp:define-class my-class () + (,member) + (:clone)))) + (is-generated (lcp.clone:clone-function-definition-for-class ,class-sym) + (format nil "MyClass Clone() const { + MyClass object; + ~A + return object; + }" + ,expected)))))) + (lcp:define-class klondike () + () + (:clone)) + (subtest "vector" + (single-member-test (member "std::vector") + "object.member_ = member_;") + (single-member-test (member "std::vector>") + "object.member_ = member_;") + (single-member-test (member "std::vector") + "object.member_.resize(member_.size()); + for (auto i1 = 0; i1 < member_.size(); ++i1) { + object.member_[i1] = member_[i1].Clone(); + }") + (single-member-test (member "std::vector>") + "object.member_.resize(member_.size()); + for (auto i1 = 0; i1 < member_.size(); ++i1) { + object.member_[i1].resize(member_[i1].size()); + for (auto i2 = 0; i2 < member_[i1].size(); ++i2) { + object.member_[i1][i2] = member_[i1][i2].Clone(); + } + }")) + (subtest "optional" + (single-member-test (member "std::experimental::optional") + "object.member_ = member_;") + (single-member-test (member "std::experimental::optional") + "if (member_) { + Klondike value1; + value1 = (*member_).Clone(); + object.member_.emplace(std::move(value1)); + } else { + object.member_ = std::experimental::nullopt; + }")) + (subtest "unordered_map" + (single-member-test (member "std::unordered_map") + "object.member_ = member_;") + (single-member-test (member "std::unordered_map>") + "object.member_ = member_;") + (single-member-test (member "std::unordered_map") + "for (const auto &kv1 : member_) { + std::pair entry1; + { + int32_t first2; + first2 = kv1.first; + Klondike second2; + second2 = kv1.second.Clone(); + entry1 = std::make_pair(std::move(first2), std::move(second2)); + } + + object.member_.emplace(std::move(entry1)); + }") + (single-member-test (member "std::unordered_map>") + "for (const auto &kv1 : member_) { + std::pair> entry1; + { + int32_t first2; + first2 = kv1.first; + std::unordered_map second2; + for (const auto &kv3 : kv1.second) { + std::pair entry3; + { + int32_t first4; + first4 = kv3.first; + Klondike second4; + second4 = kv3.second.Clone(); + entry3 = std::make_pair(std::move(first4), std::move(second4)); + } + + second2.emplace(std::move(entry3)); + } + entry1 = std::make_pair(std::move(first2), std::move(second2)); + + } + + object.member_.emplace(std::move(entry1)); + }") + (single-member-test (member "std::unordered_map") + "for (const auto &kv1 : member_) { + std::pair entry1; + { + Klondike first2; + first2 = kv1.first.Clone(); + Klondike second2; + second2 = kv1.second.Clone(); + entry1 = std::make_pair(std::move(first2), std::move(second2)); + } + + object.member_.emplace(std::move(entry1)); + }")) + (subtest "pair" + (single-member-test (member "std::pair") + "object.member_ = member_;") + (single-member-test (member "std::pair") + "{ + int32_t first1; + first1 = member_.first; + Klondike second1; + second1 = member_.second.Clone(); + object.member_ = std::make_pair(std::move(first1), std::move(second1)); + }") + (single-member-test (member "std::pair") + "{ + Klondike first1; + first1 = member_.first.Clone(); + int32_t second1; + second1 = member_.second; + object.member_ = std::make_pair(std::move(first1), std::move(second1)); + }") + (single-member-test (member "std::pair") + "{ + Klondike first1; + first1 = member_.first.Clone(); + Klondike second1; + second1 = member_.second.Clone(); + object.member_ = std::make_pair(std::move(first1), std::move(second1)); + }") + (single-member-test (member "std::pair>") + "{ + std::string first1; + first1 = member_.first; + std::pair second1; + { + int32_t first2; + first2 = member_.second.first; + Klondike second2; + second2 = member_.second.second.Clone(); + second1 = std::make_pair(std::move(first2), std::move(second2)); + } + + object.member_ = std::make_pair(std::move(first1), std::move(second1)); + }") + ) + (subtest "pointers" + (single-member-test (member "Klondike *") + "object.member_ = member_ ? member_->Clone() : nullptr;") + (single-member-test (member "std::unique_ptr") + "object.member_ = member_ ? member_->Clone() : nullptr;") + (single-member-test (member "std::shared_ptr") + "object.member_ = member_ ? member_->Clone() : nullptr;")) + (subtest "enum" + (lcp:define-enum enum '(val1 val2 val3)) + (single-member-test (member "Enum") + "object.member_ = member_;")) + (subtest "builtin c++ types" + (single-member-test (member :int32_t) + "object.member_ = member_;") + (single-member-test (member :char) + "object.member_ = member_;"))))) diff --git a/src/lisp/lcp.asd b/src/lisp/lcp.asd index b99098507..16e951f82 100644 --- a/src/lisp/lcp.asd +++ b/src/lisp/lcp.asd @@ -8,6 +8,7 @@ (:file "types") (:file "code-gen") (:file "slk") + (:file "clone") (:file "lcp")) :in-order-to ((test-op (test-op "lcp/test")))) diff --git a/src/lisp/lcp.lisp b/src/lisp/lcp.lisp index 61141f3d4..d08286ac8 100644 --- a/src/lisp/lcp.lisp +++ b/src/lisp/lcp.lisp @@ -96,7 +96,9 @@ NIL, returns a string." (format s "~%~{~A~%~}" (mapcar #'cpp-code (cpp-class-public cpp-class))) (format s "~{~%~A~}~%" (mapcar #'cpp-member-reader-definition reader-members)) (format s "~{ ~%~A~}~%" - (mapcar #'member-declaration (cpp-class-members-scoped :public))))) + (mapcar #'member-declaration (cpp-class-members-scoped :public))) + (when (cpp-class-clone-opts cpp-class) + (format s "~%~A" (lcp.clone:clone-function-definition-for-class cpp-class))))) (when (or (cpp-class-protected cpp-class) (cpp-class-members-scoped :protected)) (write-line " protected:" s) (format s "~{~A~%~}" (mapcar #'cpp-code (cpp-class-protected cpp-class))) @@ -144,7 +146,7 @@ the function. TYPE-PARAMS is a list of names for template argments" (defun cpp-method-declaration (class method-name &key args (returns "void") (inline t) static - virtual const override) + virtual const override delete) "Generate a C++ method declaration as a string for the given METHOD-NAME on CLASS. ARGS is a list of (variable type) arguments to method. RETURNS is the return type of the function. When INLINE is set to NIL, generates a @@ -165,11 +167,12 @@ which generate the corresponding C++ keywords." (cpp-variable-name (first name-and-type)))) args))) (const (if const "const" "")) - (override (if (and override inline) "override" ""))) + (override (if (and override inline) "override" "")) + (delete (if delete "= 0" ""))) (raw-cpp-string #>cpp ${template} ${static/virtual} - ${returns} ${namespace}${method-name}(${args}) ${const} ${override} + ${returns} ${namespace}${method-name}(${args}) ${const} ${override} ${delete} cpp<#))) (defstruct cpp-list diff --git a/src/lisp/package.lisp b/src/lisp/package.lisp index 63e7e5c75..a270610dc 100644 --- a/src/lisp/package.lisp +++ b/src/lisp/package.lisp @@ -32,3 +32,8 @@ #:save-function-definition-for-enum #:load-function-declaration-for-enum #:load-function-definition-for-enum)) + +(defpackage #:lcp.clone + (:use #:cl) + (:export #:clone-error + #:clone-function-definition-for-class)) diff --git a/src/lisp/slk.lisp b/src/lisp/slk.lisp index 2e6afb8e3..5798569b1 100644 --- a/src/lisp/slk.lisp +++ b/src/lisp/slk.lisp @@ -105,18 +105,6 @@ generation expects the declarations and definitions to be in `slk` namespace." "Load" :args (cons self-arg (cons reader-arg (load-extra-args cpp-class))) :type-params (lcp::cpp-type-type-params cpp-class)))) -(defun cpp-type-pointer-p (cpp-type) - (check-type cpp-type (or lcp::cpp-type string lcp::cpp-primitive-type-keywords)) - (typecase cpp-type - (string (cpp-type-pointer-p (lcp::parse-cpp-type-declaration cpp-type))) - (lcp::cpp-type - (or - (string= "*" (lcp::cpp-type-name cpp-type)) - (string= "shared_ptr" (lcp::cpp-type-name cpp-type)) - ;; Note, we could forward to default slk::Load for unique_ptr and hope - ;; everything is alright w.r.t to inheritance. - (string= "unique_ptr" (lcp::cpp-type-name cpp-type)))))) - (defun save-members (cpp-class) "Generate code for saving members of CPP-CLASS. Raise `SLK-ERROR' if the serializable member has no public access." @@ -134,7 +122,7 @@ serializable member has no public access." member-name)) s))) ;; TODO: Maybe support saving (but not loading) unique_ptr. - ((cpp-type-pointer-p (lcp::cpp-member-type member)) + ((lcp::cpp-pointer-type-p (lcp::cpp-member-type member)) (slk-error "Don't know how to save pointer '~A' in '~A'" (lcp::cpp-member-type member) (lcp::cpp-type-base-name cpp-class))) @@ -164,7 +152,7 @@ serializable member has no public access." (write-line (lcp::cpp-code (funcall (lcp::cpp-member-slk-load member) member-name)) s))) - ((cpp-type-pointer-p (lcp::cpp-member-type member)) + ((lcp::cpp-pointer-type-p (lcp::cpp-member-type member)) (slk-error "Don't know how to load pointer '~A' in '~A'" (lcp::cpp-member-type member) (lcp::cpp-type-base-name cpp-class))) diff --git a/src/lisp/types.lisp b/src/lisp/types.lisp index 6d5121243..5066415ac 100644 --- a/src/lisp/types.lisp +++ b/src/lisp/types.lisp @@ -139,7 +139,8 @@ ;; May be a function which takes 1 argument, member-name. It needs to ;; return C++ code. (slk-save nil :type (or null function) :read-only t) - (slk-load nil :type (or null function) :read-only t)) + (slk-load nil :type (or null function) :read-only t) + (clone t :type (or boolean (eql :copy) function) :read-only t)) (defstruct capnp-opts "Cap'n Proto serialization options for C++ class." @@ -171,6 +172,15 @@ ;; In case of multiple inheritance, pretend we only inherit the 1st base class. (ignore-other-base-classes nil :type boolean :read-only t)) +(defstruct clone-opts + "Cloning options for C++ class." + ;; Extra arguments to the generated clone function. List of (name cpp-type). + (args nil :read-only t) + (return-type nil :type (or null function) :read-only t) + (base nil :read-only t) + (ignore-other-base-classes nil :read-only t) + (init-object nil :read-only t)) + (defclass cpp-class (cpp-type) ((structp :type boolean :initarg :structp :initform nil :reader cpp-class-structp) @@ -186,6 +196,8 @@ :reader cpp-class-capnp-opts) (slk-opts :type (or null slk-opts) :initarg :slk-opts :initform nil :reader cpp-class-slk-opts) + (clone-opts :type (or null clone-opts) :initarg :clone-opts :initform nil + :reader cpp-class-clone-opts) (inner-types :initarg :inner-types :initform nil :reader cpp-class-inner-types) (abstractp :initarg :abstractp :initform nil :reader cpp-class-abstractp)) (:documentation "Meta information on a C++ class (or struct).")) @@ -270,6 +282,17 @@ documentation on `CPP-TYPE' members for function arguments." "Whether the C++ type designated by TYPE-DECL is a primitive type." (typep (cpp-type type-decl) 'cpp-primitive-type)) +(defun cpp-pointer-type-p (type-decl) + "Whether the C++ type designated by TYPE-DECL is a smart or raw pointer type." + (check-type type-decl (or lcp::cpp-type string lcp::cpp-primitive-type-keywords)) + (typecase type-decl + (string (cpp-pointer-type-p (lcp::parse-cpp-type-declaration type-decl))) + (lcp::cpp-type + (or + (string= "*" (lcp::cpp-type-name type-decl)) + (string= "shared_ptr" (lcp::cpp-type-name type-decl)) + (string= "unique_ptr" (lcp::cpp-type-name type-decl)))))) + (defun parse-cpp-type-declaration (type-decl) "Parse C++ type from TYPE-DECL string and return CPP-TYPE. @@ -579,6 +602,8 @@ Generates C++: `(make-capnp-opts ,@(cdr (assoc :capnp serialize)))) :slk-opts ,(when (assoc :slk serialize) `(make-slk-opts ,@(cdr (assoc :slk serialize)))) + :clone-opts ,(when (assoc :clone options) + `(make-clone-opts ,@(cdr (assoc :clone options)))) :abstractp ,abstractp :namespace (reverse *cpp-namespaces*) ;; Set inner types at the end. This works