Implement cloning in LCP

Summary:
Add automatic generating of cloning (deep-copy) functions for LCP
defined classes. This enables us to remove a bunch of manually written `Clone`
functions from AST and also to implement logical plan cloning properly (before
it was using serialization).

Reviewers: teon.banek, llugovic

Reviewed By: teon.banek

Subscribers: pullbot

Differential Revision: https://phabricator.memgraph.io/D1808
This commit is contained in:
Marin Tomic 2019-01-17 16:44:55 +01:00
parent 0111fa506a
commit f8e76efa3e
9 changed files with 909 additions and 19 deletions

View File

@ -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<Node>")
(right "std::unique_ptr<Node>"))
(: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<Node> Clone() const {
auto object = std::make_unique<Node>();
object->value_ = value_;
object->left_ = left_ ? left_->Clone() : nullptr;
object->right_ = right_ ? right_->Clone() : nullptr;
return object;
}
private:
int32_t value_;
std::unique_ptr<Node> left_;
std::unique_ptr<Node> 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<Base>`, 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<T>();` 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<Base> Clone() const {
auto object = std::make_unique<Base>();
object->member_ = member_;
return object;
}
private:
int32_t member_;
};
class Derived : public Base {
public:
std::unique_ptr<Base> Clone() const override {
auto object = std::make_unique<Derived>();
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<Base>`, 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<void(int, int)>"
: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<Where>();
object->uid_ = uid_;
object->expression_ = expression_ ? expression_->Clone(storage) : nullptr;
return object;
}
};
```

View File

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

255
src/lisp/clone.lisp Normal file
View File

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

View File

@ -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<Tree>")
(right "std::unique_ptr<Tree>"))
(: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<Tree>")
(big-tree "std::unique_ptr<Tree>"))
(:clone))))
(is-generated (lcp.clone:clone-function-definition-for-class tree-class)
"std::unique_ptr<Tree> Clone() const {
auto object = std::make_unique<Tree>();
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<Base> Clone() const {
auto object = std::make_unique<Base>();
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<Base> Clone() const override {
auto object = std::make_unique<Child>();
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<Base> Clone() const = 0;")
(is-generated (lcp.clone:clone-function-definition-for-class child-class)
"std::unique_ptr<Base> Clone() const override {
auto object = std::make_unique<Child>();
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<Expression *>"))
(: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<And>();
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<Or>();
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<void(int, int)>" :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<int32_t>")
"object.member_ = member_;")
(single-member-test (member "std::vector<std::vector<int32_t>>")
"object.member_ = member_;")
(single-member-test (member "std::vector<Klondike>")
"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<std::vector<Klondike>>")
"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<int32_t>")
"object.member_ = member_;")
(single-member-test (member "std::experimental::optional<Klondike>")
"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<int32_t, std::string>")
"object.member_ = member_;")
(single-member-test (member "std::unordered_map<int32_t, std::unordered_map<int32_t, std::string>>")
"object.member_ = member_;")
(single-member-test (member "std::unordered_map<int32_t, Klondike>")
"for (const auto &kv1 : member_) {
std::pair<int32_t, Klondike> 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<int32_t, std::unordered_map<int32_t, Klondike>>")
"for (const auto &kv1 : member_) {
std::pair<int32_t, std::unordered_map<int32_t, Klondike>> entry1;
{
int32_t first2;
first2 = kv1.first;
std::unordered_map<int32_t, Klondike> second2;
for (const auto &kv3 : kv1.second) {
std::pair<int32_t, Klondike> 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<Klondike, Klondike>")
"for (const auto &kv1 : member_) {
std::pair<Klondike, Klondike> 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<int32_t, int32_t>")
"object.member_ = member_;")
(single-member-test (member "std::pair<int32_t, Klondike>")
"{
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, int32_t>")
"{
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, Klondike>")
"{
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, std::pair<int32_t, Klondike>>")
"{
std::string first1;
first1 = member_.first;
std::pair<int32_t, Klondike> 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<Klondike>")
"object.member_ = member_ ? member_->Clone() : nullptr;")
(single-member-test (member "std::shared_ptr<Klondike>")
"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_;")))))

View File

@ -8,6 +8,7 @@
(:file "types")
(:file "code-gen")
(:file "slk")
(:file "clone")
(:file "lcp"))
:in-order-to ((test-op (test-op "lcp/test"))))

View File

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

View File

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

View File

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

View File

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