Merge: Enable tagging of primitive types
authorJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:41:34 +0000 (12:41 +0700)
committerJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:41:34 +0000 (12:41 +0700)
Another old optimization since it was present in PRM, the Australopithecus compiler.

This PR bring back tagging of the primitives types Int, Bool and Char.

Previously, all primitive types where boxed.
It means that when a Bool, or any primitive object, must be manipulated in a polymorphic way (i.e. as an Object in a `val*`), a small box is allocated that contain the value and the reference (the `val*`) points to the box.
The boxes use the layout of real objects (with a pointer to the class table and everything) so that boxes are compatible with the various implementation of OO mechanism, eg `obj->class->vtf[METHODID]` to implement a method invocation.

Basically boxes work like Java auxiliary classes (eg. `Integer`) except that they are fully transparent for the user and, more important, a implementation detail unrelated to the specification of the language.
Therefore, one can provide a different implementation, like tagging, without worrying about breaking the specification and existing programs.

The principle of tagging is that `val*` values are overloaded to store primitive value in addition to genuine pointers to allocated object.
The two low bits of the `val*` (so 4 combinations) is used to distinguish if the value is a real pointer (in this case, bits are 00) or one of the masqueraded common type (Int, Bool and Char).
If it is a pointer there is nothing to do and the value can be used as is.
If it is a primitive value, then the real value is stored in the remaining bits (but shifted).
The trick works because allocated objects are aligned so that pointer of genuine allocated object have always their last two bits at 00.

The advantage of tagging is that this reduces the cost of manipulating primitive values in a polymorphic way, especially this reduce the numerous allocations of short lived boxes that is slow to do and increase the workload of the GC.
By comparison, with tagging, masquerading a Bool as a `val*` is easily done with few bit-to-bit operations.

Unfortunately, tagging is not a panacea since `val*` is not always a real pointer and require specific and additional protection to avoid doing `obj->class` in the case of `obj` is in fact a tagged value.
Therefore tagging add a minimal but systematic overhead to OO mechanisms like calls, type tests and equality tests.

After quick tests, the numbers are encouraging.

For nitc/nitc/nitc:
before: 0m6.796s
after: 0m6.452s (-5%, not that bad)

Benches where run and tagging was either comparable or better than systematic boxing:

![](https://cloud.githubusercontent.com/assets/135828/6656784/b5a38698-cb67-11e4-96a4-c46f7df2331f.png)

Especially `lib/ai/examples/queens.nit` get the best of it with a -20% improvement.
That make sense because it use arrays of integers to model the states of the n-queen problem. And unfortunately arrays are implemented in a homogeneous way where elements are always polymorphic `val*` values.
Once generics and collections are implemented in an heterogeneous way for primitive types, the benefit of tagging should be reevaluated.

Note: funnily, the main commit of the series, the one that implements tagging, is only made of insertions of lines (no deletion or changes) and it only modifies a single file.

Pull-Request: #1206
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>

36 files changed:
lib/deriving.nit [new file with mode: 0644]
lib/json_serialization.nit
lib/serialization.nit [deleted file]
lib/serialization/README.md [new file with mode: 0644]
lib/serialization/serialization.nit [new file with mode: 0644]
lib/standard/collection/abstract_collection.nit
misc/vim/plugin/nit.vim
share/man/nitunit.md
src/doc/vim_autocomplete.nit
src/frontend/check_annotation.nit
src/frontend/deriving.nit [new file with mode: 0644]
src/frontend/frontend.nit
src/loader.nit
src/location.nit
src/model/mdoc.nit
src/model/model.nit
src/modelbuilder_base.nit
src/neo.nit
src/nitunit.nit
src/testing/testing_doc.nit
tests/nitunit.args
tests/sav/nitg-e/test_deserialization.res
tests/sav/nitg-e/test_deserialization_serial.res
tests/sav/nitg-g/fixme/test_deriving_alt1.res [new file with mode: 0644]
tests/sav/nitunit_args6.res [new file with mode: 0644]
tests/sav/nitunit_args7.res [new file with mode: 0644]
tests/sav/test_deriving.res [new file with mode: 0644]
tests/sav/test_deriving_alt1.res [new file with mode: 0644]
tests/sav/test_deriving_alt2.res [new file with mode: 0644]
tests/sav/test_deriving_alt3.res [new file with mode: 0644]
tests/sav/test_deriving_alt4.res [new file with mode: 0644]
tests/sav/test_deserialization.res
tests/test_deriving.nit [new file with mode: 0644]
tests/test_nitunit3/README.md [new file with mode: 0644]
tests/test_nitunit3/test_nitunit3.nit [new file with mode: 0644]
tests/test_nitunit_md.md [new file with mode: 0644]

diff --git a/lib/deriving.nit b/lib/deriving.nit
new file mode 100644 (file)
index 0000000..ae8394e
--- /dev/null
@@ -0,0 +1,117 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# This file is free software, which comes along with NIT.  This software is
+# distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+# without  even  the implied warranty of  MERCHANTABILITY or  FITNESS FOR A
+# PARTICULAR PURPOSE.  You can modify it is you want,  provided this header
+# is kept unaltered, and a notification of the changes is added.
+# You  are  allowed  to  redistribute it and sell it, alone or is a part of
+# another product.
+
+# Automatic derivable implementation of standard basic methods.
+#
+# This module introduce `Derivable` as the main interface to implement (or auto-implement) and
+# provides additional mixin-interfaces with specific default behavior of standard basic methods based
+# on the services of this interface.
+#
+# The name *deriving* is inspired from the deriving mechanism of Haskell.
+#
+# This module also introduce a new annotation `auto_derive`. See `Derivable` for details.
+module deriving is
+       new_annotation auto_derive
+end
+
+# Interface of objects that expose some kind of internal representation in a very unreliable way.
+#
+# The point of this interface is to allow objects to give a basic representation of
+# themselves within a simple key-value dictionary.
+# The specific semantic of each key and value is let unspecified.
+#
+# Moreover the class annotation `auto_derive` will automatically implements the
+# interface with the attributes locally defined in the class.
+#
+# ~~~
+# class A
+#    auto_derive
+#    var an_int: Int
+#    var a_string: String
+# end
+#
+# var a = new A(5, "five")
+# var map = a.derive_to_map
+# assert map.length == 2
+# assert map["an_int"] == 5
+# assert map["a_string"] == "five"
+# ~~~
+interface Derivable
+       # Returns a map that loosely represents the object `self`.
+       #
+       # Warning: by default the method returns an empty Map.
+       # It is done this way so that subclasses can just call `super` and add their own attributes.
+       #
+       # Forgetting to redefine `derive_to_map` will broke the expectation of the user of the class
+       # Since an empty map is not POLA.
+       #
+       # Note that the semantic of keys and values is let unspecified.
+       # Moreover, there is no specification nor mechanism to avoid key collision.
+       fun derive_to_map: Map[String, nullable Object]
+       do
+               return new HashMap[String, nullable Object]
+       end
+end
+
+# Implementation of `to_s` for `Derivable` objects.
+#
+# The implementation uses `to_s` for each value of `attributes_to_map`.
+#
+# ~~~
+# class A
+#    auto_derive
+#    super DeriveToS
+#    var an_int: Int
+#    var a_string: String
+# end
+#
+# var a = new A(5, "five")
+# assert a.to_s == "an_int:5; a_string:five"
+# ~~~
+#
+# Warning: the method may go in an infinite recursion if there is a circuit in
+# the implementation of `to_s`.
+interface DeriveToS
+       super Derivable
+       redef fun to_s do return derive_to_map.join("; ", ":")
+end
+
+# Implementation of `==` and `hash` for `Derivable` objects.
+#
+# The implementations just call `==` and `hash` on `derive_to_map`.
+#
+# ~~~
+# class A
+#    auto_derive
+#    super DeriveEqual
+#    var an_int: Int
+#    var a_string: String
+# end
+#
+# var a = new A(5, "five")
+# var b = new A(5, "five")
+# var c = new A(6, "six")
+# assert a == b
+# assert a.hash == b.hash
+# assert a != c
+# ~~~
+#
+# Warning: the method may go in an infinite recursion if there is a circuit in
+# the implementation of `==` or `hash`.
+interface DeriveEqual
+       super Derivable
+       redef fun ==(other) do
+               if not other isa Derivable then return false
+               return derive_to_map == other.derive_to_map
+       end
+       redef fun hash do
+               return derive_to_map.hash
+       end
+end
index 4c552f6..e8c76b4 100644 (file)
@@ -102,7 +102,7 @@ class JsonDeserializer
 
                assert current.keys.has(name)
                var value = current[name]
-               
+
                return convert_object(value)
        end
 
@@ -164,7 +164,7 @@ class JsonDeserializer
                                assert val isa String
 
                                if val.length != 1 then print "Error: expected a single char when deserializing '{val}'."
-                               
+
                                return val.chars.first
                        end
 
diff --git a/lib/serialization.nit b/lib/serialization.nit
deleted file mode 100644 (file)
index 8a1d1ee..0000000
+++ /dev/null
@@ -1,103 +0,0 @@
-# This file is part of NIT ( http://www.nitlanguage.org ).
-#
-# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-# Offers services to serialize a Nit objects to different persistent formats
-module serialization is
-       new_annotation auto_serializable
-end
-
-# Abstract serialization service to be sub-classed by specialized services.
-interface Serializer
-       # Main method of this service, serialize the `object`
-       fun serialize(object: nullable Serializable) is abstract
-
-       # Serialize an object as a "possible" reference, depending of the service
-       fun serialize_reference(object: Serializable) is abstract
-
-       # Serialize an attribute, used by `Serializable::core_serialize_to`
-       fun serialize_attribute(name: String, value: nullable Object)
-       do
-               if not try_to_serialize(value) then
-                       warn("argument {value.class_name}::{name} is not serializable.")
-               end
-       end
-
-       # Serialize `value` is possie, i.e. it is `Serializable` or `null`
-       fun try_to_serialize(value: nullable Object): Bool
-       do
-               if value isa Serializable then
-                       value.serialize_to_or_delay(self)
-               else if value == null then
-                       serialize value
-               else return false
-               return true
-       end
-
-       # Warn of problems and potential errors (such as if an attribute
-       # is not serializable)
-       fun warn(msg: String) do print "Serialization warning: {msg}"
-end
-
-# Abstract deserialization service
-#
-# After initialization of one of its sub-classes, call `deserialize`
-interface Deserializer
-       # Main method of this class, returns a Nit object
-       fun deserialize: nullable Object is abstract
-
-       # Internal method to be implemented by sub-classes
-       fun deserialize_attribute(name: String): nullable Object is abstract
-
-       # Internal method called by objects in creation,
-       # to be implemented by sub-classes
-       fun notify_of_creation(new_object: Object) is abstract
-
-       # Mainly generated method to return the next instance of the givent
-       # class by name
-       fun deserialize_class(class_name: String): Object do
-               print "Error: doesn't know how to deserialize class \"{class_name}\""
-               abort
-       end
-end
-
-# Instances of this class can be passed to `Serializer::serialize`
-interface Serializable
-       # Full or true serialization
-       fun serialize_to(v: Serializer) do v.serialize(self)
-
-       # Body of the serialization of this class
-       # Can be redefed in sub classes and refinements
-       fun core_serialize_to(v: Serializer) do end
-
-       # Whether full serialization (calls `serialize_to`) or place only references
-       fun serialize_to_or_delay(v: Serializer) do v.serialize_reference(self)
-end
-
-# Instances of this class are not delayed and instead serialized immediately
-# This applies mainly to `universal` types
-interface DirectSerializable
-       super Serializable
-
-       redef fun serialize_to_or_delay(v) do serialize_to(v)
-end
-
-redef class Bool super DirectSerializable end
-redef class Char super DirectSerializable end
-redef class Int super DirectSerializable end
-redef class Float super DirectSerializable end
-redef class NativeString super DirectSerializable end
-redef class String super DirectSerializable end
-redef class Array[E] super Serializable end
diff --git a/lib/serialization/README.md b/lib/serialization/README.md
new file mode 100644 (file)
index 0000000..4cbcf64
--- /dev/null
@@ -0,0 +1,254 @@
+# Abstract serialization services
+
+The serialization services are centered around the `auto_serializable` annotation,
+the `Serializable` interface and the implementations of `Serializer` and `Deserializer`.
+
+## The `auto_serializable` annotation
+
+A class annotated with `auto_serializable` identifies it as a subclass of Serializable and
+triggers the generation of customized serialization and deserialization services.
+
+~~~
+import serialization
+
+# Simple serializable class identifying a human
+class Person
+       auto_serializable
+
+       # First and last name
+       var name: String
+
+       # Year of birth (`null` if unknown)
+       var birth: nullable Int
+
+       redef fun ==(o) do return o isa SELF and name == o.name and birth == o.birth
+       redef fun hash do return name.hash
+end
+~~~
+
+The `Person` class also defines `==` and `hash`, this is optional but we will use it to make an important point.
+By definition of a serializable class, an instance can be serialized to a stream, then deserialized.
+The deserialized instance will not be the same instance, but they should be equal.
+So, in this case, we can compare both instances with `==` to test their equality.
+
+Some conditions applies to the classes that can be annotated as `auto_serializable`.
+All attributes of the class must be serializable, runtime errors will be
+raised when trying to serialize non-serializable attributes.
+
+In the class `Person`, all attributes are typed with classes the standards library.
+These common types are defined defined as serializable by this project.
+The attributes could also be typed with user-defined `auto_serializable`
+classes or any other subclass of `Serializable`.
+
+~~~
+# This `auto_serializable` class is composed of two `auto_serializable` attributes
+class Partnership
+       auto_serializable
+
+       var partner_a: Person
+       var partner_b: Person
+
+       redef fun ==(o) do return o isa SELF and partner_a == o.partner_a and partner_b == o.partner_b
+       redef fun hash do return partner_a.hash + 1024*partner_b.hash
+end
+~~~
+
+The `auto_serializable` applies only to the class definition,
+only attributes declared locally will be serialized.
+However, each definition of a class (a refinement or specialization)
+can declare `auto_serializable`.
+
+## Custom serializable classes
+
+The annotation `auto_serializable` should be enough for most cases,
+but in some cases you need more control over the serialization process.
+
+For more control, create a subclass to `Serializable` and redefine `core_serialize_to`.
+This method should use `Serializer::serialize_attribute` to serialize its components.
+`serialize_attribute` works as a dictionary and organize attributes with a key.
+
+You will also need to redefine `Deserializer::deserialize_class` to support this specific class.
+The method should only act on known class names, and call super otherwise.
+
+### Example: the User class
+
+The following example cannot use the `auto_serializable` annotations
+because some of the arguments to the `User` class need special treatment:
+
+* The `name` attribute is perfectly normal, it can be serialized and deserialized
+  directly.
+
+* The `password` attribute must be encrypted before being serialized,
+  and unencrypted on deserialization.
+
+* The `avatar` attributes is kept as ASCII art in memory.
+  It could be serialized as such but it is cleaner to only
+  serialize the path to its source on the file system.
+  The data is reloaded on deserialization.
+
+For this customization, the following code snippet implements
+two serialization services: `User::core_serialize_to` and
+`Deserializer::deserialize_class`.
+
+~~~
+module user_credentials
+
+# User credentials for a website
+class User
+       super Serializable
+
+       # User name
+       var name: String
+
+       # Clear text password
+       var password: String
+
+       # User's avatar image as data blob
+       var avatar: Image
+
+       redef fun core_serialize_to(serializer: Serializer)
+       do
+               # This is the normal serialization process
+               serializer.serialize_attribute("name", name)
+
+               # Serialized an encrypted version of the password
+               #
+               # Obviously, `rot(13)` is not a good encrption
+               serializer.serialize_attribute("pass", password.rot(13))
+
+               # Do not serialize the image, only its path
+               serializer.serialize_attribute("avatar_path", avatar.path)
+       end
+end
+
+redef class Deserializer
+       redef fun deserialize_class(name)
+       do
+               if name == "User" then
+                       # Deserialize normally
+                       var user = deserialize_attribute("name")
+
+                       # Decrypt password
+                       var pass = deserialize_attribute("pass").rot(-13)
+
+                       # Deserialize the path and load the avatar from the file system
+                       var avatar_path = deserialize_attribute("avatar_path")
+                       var avatar = new Image(avatar_path)
+
+                       return new User(user, pass, avatar)
+               end
+
+               return super
+       end
+end
+
+# An image loaded in memory as ASCII art
+#
+# Not really useful for this example, provided for consistency only.
+class Image
+       # Path on the filesystem for `self`
+       var path: String
+
+       # ASCII art composing this image
+       var ascii_art: String = path.read_all is lazy
+end
+
+~~~
+
+See the documentation of the module `serialization::serialization` for more
+information on the services to redefine.
+
+## Serialization services
+
+The `auto_serializable` annotation and the `Serializable` class are used on
+classes specific to the business domain.
+To write (and read) instances of these classes to a persistent format
+you must use implementations of `Serializer` and `Deserializer`.
+
+The main implementations of these services are `JsonSerializer` and `JsonDeserializer`,
+from the `json_serialization` module.
+
+~~~
+import json_serialization
+import user_credentials
+
+# Data to be serialized and deserialized
+var couple = new Partnership(
+       new Person("Alice", 1985, new Image("alice.png")),
+       new Person("Bob", null, new Image("bob.png")))
+
+var path = "serialized_data.json"
+var writer = new FileWriter(path)
+var serializer = new JsonSerializer(writer)
+serializer.serialize couple
+writer.close
+
+var reader = new FileReader(path)
+var deserializer = new JsonDeserializer(reader.to_s)
+var deserialized_couple = deserializer.deserialize
+reader.close
+
+assert couple == deserialize_couple
+~~~
+
+## Limitations and TODO
+
+The serialization has some limitations:
+
+* Not enough classes from the standard library are supported.
+  This only requires someone to actually code the support.
+  It should not be especially hard for most classes, some can
+  simply declare the `auto_serializable` annotation.
+
+* A limitation of the Json parser prevents deserializing from files
+  with more than one object.
+  This could be improved in the future, but for now you should
+  serialize a single object to each filesand use different instances of
+  serializer and deserializer each time.
+
+* The `auto_serializable` annotation does not handle very well
+  complex constructors. This could be improved in the compiler.
+  For now, you may prefer to use `auto_serializable` on simple classes,
+  of by using custom `Serializable`.
+
+* The serialization uses only the short name of a class, not its qualified name.
+  This will cause problem when different classes using the same name.
+  This could be solved partially in the compiler and the library.
+  A special attention must be given to the consistency of the name across
+  the different programs sharing the serialized data.
+
+* The serialization support in the compiler need some help to
+  deal with generic types. The solution is to use `nitserial`,
+  the next section explores this subject.
+
+## Dealing with generic types
+
+One limitation of the serialization support in the compiler is with generic types.
+For example, the `Array` class is generic and serializable.
+However, the runtime types of Array instances are parameterized and are unknown to the compiler.
+So the compiler won't support serializing instances of `Array[MySerializable]`.
+
+The tool `nitserial` solves this problem at the level of user modules.
+It does so by parsing a Nit module, group or project to find all known
+parameterized types of generic classes.
+It will then generating a Nit module to handle deserialization of these types.
+
+Usage steps to serialize parameterized types:
+
+* Write your program, let's call it `my_prog.nit`,
+  it must use some parameterized serializable types.
+  Let's say that you use `Array[MySerializable]`.
+
+* Run nitserial using `nitserial my_prog.nit` to
+  generate the file `my_prog_serial.nit`.
+
+* Compile your program by mixing in the generated module with:
+  `nitc my_prog.nit -m my_prog_serial.nit`
+
+This was a simple example, in practical cases you may need
+to use more than one generated file.
+For example, on a client/server system, an instance can be created
+server-side, serialized and the used client-side.
+In this case, two files will be generated by nitserial,
+one for the server and one for the client.
+Both the files should be compiled with both the client and the server.
diff --git a/lib/serialization/serialization.nit b/lib/serialization/serialization.nit
new file mode 100644 (file)
index 0000000..31f98f0
--- /dev/null
@@ -0,0 +1,153 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Copyright 2014 Alexis Laferrière <alexis.laf@xymus.net>
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Abstract services to serialize Nit objects to different formats
+#
+# This module declares the `auto_serializable` annotation to mark Nit classes as serializable.
+# For an introduction to this service, refer to the documentation of the `serialization` group.
+# This documentation provides more technical information on interesting entitie of this module.
+#
+# Interesting entities for end users of serializable classes:
+#
+# * Serialize an instance subclass of `Serializable` with either
+#   `Serializer::serializable` and `Serializable::serialize`.
+# * Deserialize an object using `Deserializer::deserialize`.
+#   The object type must the be checked with an `assert` or otherwise.
+#
+# Interesting entities to create custom serializable classes:
+#
+# * Subclass `Serializable` to declare a class as serializable and to customize
+#   the serialization and deserialization behavior.
+# * Redefine `Serializable::core_serialize_to` to customize the serialization
+#   of the receiver class.
+# * Redefine `Deserializer::deserialize_class` to customize the deserialization
+#   of a specific class by name.
+#
+# Interesting entities for serialization format:
+#
+# * Subclass `Serializer` and `Deserializer` with custom serices.
+# * In `Serializer`, `serialize` and `serialize_reference` must be redefined.
+# * In `Deserializer`; `deserialize`, `deserialize_attribute and
+#   `notify_of_creation` must be redefined.
+module serialization is
+       new_annotation auto_serializable
+end
+
+# Abstract serialization service to be sub-classed by specialized services.
+interface Serializer
+       # Entry point method of this service, serialize the `object`
+       #
+       # This method, and refinements, should handle `null` and probably
+       # use double dispatch to customize the bahavior per serializable objects.
+       fun serialize(object: nullable Serializable) is abstract
+
+       # Serialize an object, with full serialization or a simple reference
+       protected fun serialize_reference(object: Serializable) is abstract
+
+       # Serialize an attribute to compose a serializable object
+       #
+       # This method should be called from `Serializable::core_serialize_to`.
+       fun serialize_attribute(name: String, value: nullable Object)
+       do
+               if not try_to_serialize(value) then
+                       warn("argument {value.class_name}::{name} is not serializable.")
+               end
+       end
+
+       # Serialize `value` is possie, i.e. it is `Serializable` or `null`
+       fun try_to_serialize(value: nullable Object): Bool
+       do
+               if value isa Serializable then
+                       value.serialize_to_or_delay(self)
+               else if value == null then
+                       serialize value
+               else return false
+               return true
+       end
+
+       # Warn of problems and potential errors (such as if an attribute
+       # is not serializable)
+       fun warn(msg: String) do print "Serialization warning: {msg}"
+end
+
+# Abstract deserialization service
+#
+# After initialization of one of its sub-classes, call `deserialize`
+interface Deserializer
+       # Main method of this class, returns a Nit object
+       fun deserialize: nullable Object is abstract
+
+       # Internal method to be implemented by sub-classes
+       fun deserialize_attribute(name: String): nullable Object is abstract
+
+       # Internal method called by objects in creation,
+       # to be implemented by sub-classes
+       fun notify_of_creation(new_object: Object) is abstract
+
+       # Deserialize the next available object as an instance of `class_name`
+       #
+       # Returns the deserialized object on success, aborts on error.
+       #
+       # This method should be redefined for each custom subclass of `Serializable`.
+       # All refinement should look for a precise `class_name` and call super
+       # on unsupported classes.
+       fun deserialize_class(class_name: String): Object do
+               print "Error: doesn't know how to deserialize class \"{class_name}\""
+               abort
+       end
+end
+
+# Instances of this class can be passed to `Serializer::serialize`
+interface Serializable
+       # Serialize `self` to `serializer`
+       #
+       # This is a shortcut to `Serializer::serialize`.
+       fun serialize_to(serializer: Serializer) do serializer.serialize(self)
+
+       # Actual serialization of `self` to `serializer`
+       #
+       # This writes the full data of `self` to `serializer`.
+       #
+       # This method can be redefined in sub classes and refinements.
+       # It should use `Serializer::serialize_attribute` to to register real or
+       # logical attributes.
+       #
+       # Any refinement should have its equivalent refinement of
+       # `Deserializer::deserialize_class` to support this custom deserialization.
+       fun core_serialize_to(serializer: Serializer) do end
+
+       # Accept references or force direct serialization (using `serialize_to`)
+       #
+       # The subclass change the default behavior, which will accept references,
+       # to force to always serialize copies of `self`.
+       private fun serialize_to_or_delay(v: Serializer) do v.serialize_reference(self)
+end
+
+# Instances of this class are not delayed and instead serialized immediately
+# This applies mainly to `universal` types
+interface DirectSerializable
+       super Serializable
+
+       redef fun serialize_to_or_delay(v) do serialize_to(v)
+end
+
+redef class Bool super DirectSerializable end
+redef class Char super DirectSerializable end
+redef class Int super DirectSerializable end
+redef class Float super DirectSerializable end
+redef class NativeString super DirectSerializable end
+redef class String super DirectSerializable end
+redef class Array[E] super Serializable end
index d38e001..d51e710 100644 (file)
@@ -203,7 +203,7 @@ end
 #
 # Used to pass arguments by reference.
 #
-# Also used when one want to give asingle element when a full
+# Also used when one want to give a single element when a full
 # collection is expected
 class Container[E]
        super Collection[E]
@@ -497,6 +497,25 @@ interface MapRead[K, V]
                end
                return true
        end
+
+       # A hashcode based on the hashcode of the keys and the values.
+       #
+       # ~~~
+       # var a = new HashMap[String, Int]
+       # var b = new ArrayMap[Object, Numeric]
+       # a["one"] = 1
+       # b["one"] = 1
+       # assert a.hash == b.hash
+       # ~~~
+       redef fun hash
+       do
+               var res = length
+               for k, v in self do
+                       if k != null then res += k.hash * 7
+                       if v != null then res += v.hash * 11
+               end
+               return res
+       end
 end
 
 # Maps are associative collections: `key` -> `item`.
index 046ab9f..f19224a 100644 (file)
@@ -283,7 +283,7 @@ fun Nitdoc()
                for line in readfile(path)
                        let words = split(line, '#====#', 1)
                        let name = get(words, 0, '')
-                       if name =~ '^' . word
+                       if name =~ '^' . word . '\>'
                                " It fits our word, get long doc
                                let desc = get(words,3,'')
                                let desc = join(split(desc, '#nnnn#', 1), "\n")
index 4f45562..b43004f 100644 (file)
@@ -12,14 +12,16 @@ nitunit [*options*] FILE...
 
 Unit testing in Nit can be achieved in two ways:
 
-* using `DocUnits` in code comments
+* using `DocUnits` in code comments or in markdown files
 * using `TestSuites` with test unit files
 
-`DocUnits` are executable pieces of code found in the documentation of modules,
+`DocUnits` are executable pieces of code found in the documentation of groups, modules,
 classes and properties.
 They are used for documentation purpose, they should be kept simple and illustrative.
 More advanced unit testing can be done using TestSuites.
 
+`DocUnits` can also be used in any markdown files.
+
 `TestSuites` are test files coupled to a tested module.
 They contain a list of test methods called TestCase.
 
@@ -106,6 +108,14 @@ The `nitunit` command is used to test Nit files:
 
     $ nitunit foo.nit
 
+Groups (directories) can be given to test the documentation of the group and of all its Nit files:
+
+    $ nitunit lib/foo
+
+Finally, standard markdown documents can be checked with:
+
+    $ nitunit foo.md
+
 ## Working with `TestSuites`
 
 TestSuites are Nit files that define a set of TestCases for a particular module.
index b7edca0..edbc126 100644 (file)
@@ -52,7 +52,7 @@ redef class MEntity
        private fun field_separator: String do return "#====#"
        private fun line_separator: String do return "#nnnn#"
 
-       private fun write_to_stream(stream: Writer)
+       private fun write_doc(mainmodule: MModule, stream: Writer)
        do
                # 1. Short name for autocompletion
                stream.write complete_name
@@ -78,6 +78,7 @@ redef class MEntity
                        for i in 2.times do stream.write line_separator
                        stream.write mdoc.content.join(line_separator)
                end
+               write_extra_doc(mainmodule, stream)
 
                stream.write "\n"
        end
@@ -89,6 +90,9 @@ redef class MEntity
 
        # Doc to use in completion
        private fun complete_mdoc: nullable MDoc do return mdoc
+
+       # Extra auto documentation to append to the `stream`
+       private fun write_extra_doc(mainmodule: MModule, stream: Writer) do end
 end
 
 redef class MMethodDef
@@ -148,6 +152,84 @@ redef class MClassDef
        end
 end
 
+redef class MClassType
+       redef fun write_extra_doc(mainmodule, stream)
+       do
+               # Super classes
+               stream.write line_separator*2
+               stream.write "## Class hierarchy"
+
+               var direct_supers = [for s in mclass.in_hierarchy(mainmodule).direct_greaters do s.name]
+               if not direct_supers.is_empty then
+                       alpha_comparator.sort direct_supers
+                       stream.write line_separator
+                       stream.write "* Direct super classes: "
+                       stream.write direct_supers.join(", ")
+               end
+
+               var supers = [for s in mclass.in_hierarchy(mainmodule).greaters do s.name]
+               supers.remove mclass.name
+               if not supers.is_empty then
+                       alpha_comparator.sort supers
+                       stream.write line_separator
+                       stream.write "* All super classes: "
+                       stream.write supers.join(", ")
+               end
+
+               var direct_subs = [for s in mclass.in_hierarchy(mainmodule).direct_smallers do s.name]
+               if not direct_subs.is_empty then
+                       alpha_comparator.sort direct_subs
+                       stream.write line_separator
+                       stream.write "* Direct sub classes: "
+                       stream.write direct_subs.join(", ")
+               end
+
+               var subs = [for s in mclass.in_hierarchy(mainmodule).smallers do s.name]
+               subs.remove mclass.name
+               if not subs.is_empty then
+                       alpha_comparator.sort subs
+                       stream.write line_separator
+                       stream.write "* All sub classes: "
+                       stream.write subs.join(", ")
+               end
+
+               # List other properties
+               stream.write line_separator*2
+               stream.write "## Properties"
+               stream.write line_separator
+               var props = mclass.all_mproperties(mainmodule, protected_visibility).to_a
+               alpha_comparator.sort props
+               for prop in props do
+                       if mclass.name == "Object" or prop.intro.mclassdef.mclass.name != "Object" then
+
+                               if prop.visibility == public_visibility then
+                                       stream.write "+ "
+                               else stream.write "~ " # protected_visibility
+
+                               if prop isa MMethod then
+                                       if prop.is_init and prop.name != "init" then stream.write "init "
+                                       if prop.is_new and prop.name != "new" then stream.write "new "
+                               end
+
+                               stream.write prop.name
+
+                               if prop isa MMethod then
+                                       stream.write prop.intro.msignature.to_s
+                               end
+
+                               var mdoc = prop.intro.mdoc
+                               if mdoc != null then
+                                       stream.write "  # "
+                                       stream.write mdoc.content.first
+                               end
+                               stream.write line_separator
+                       end
+               end
+       end
+
+       redef fun complete_mdoc do return mclass.intro.mdoc
+end
+
 private class AutocompletePhase
        super Phase
 
@@ -168,7 +250,7 @@ private class AutocompletePhase
                # Got all known modules
                var model = mainmodule.model
                for mmodule in model.mmodules do
-                       mmodule.write_to_stream modules_stream
+                       mmodule.write_doc(mainmodule, modules_stream)
                end
 
                # TODO list other modules from the Nit lib
@@ -184,15 +266,15 @@ private class AutocompletePhase
                                for prop in mclass.all_mproperties(mainmodule, public_visibility) do
                                        if prop isa MMethod and prop.is_init then
                                                mclass_intro.target_constructor = prop.intro
-                                               mclass_intro.write_to_stream constructors_stream
+                                               mclass_intro.write_doc(mainmodule, constructors_stream)
                                        end
                                end
                                mclass_intro.target_constructor = null
                        end
 
                        # Always add to types and classes
-                       mclass.mclass_type.write_to_stream classes_stream
-                       mclass.mclass_type.write_to_stream types_stream
+                       mclass.mclass_type.write_doc(mainmodule, classes_stream)
+                       mclass.mclass_type.write_doc(mainmodule, types_stream)
                end
 
                # Get all known properties
@@ -202,7 +284,7 @@ private class AutocompletePhase
 
                        # Is it a virtual type?
                        if mproperty isa MVirtualTypeProp then
-                               mproperty.intro.write_to_stream types_stream
+                               mproperty.intro.write_doc(mainmodule, types_stream)
                                continue
                        end
 
@@ -210,7 +292,7 @@ private class AutocompletePhase
                        var first_letter = mproperty.name.chars.first
                        if first_letter == '@' or first_letter == '_' then continue
 
-                       mproperty.intro.write_to_stream properties_stream
+                       mproperty.intro.write_doc(mainmodule, properties_stream)
                end
 
                # Close streams
index 419c5ae..36244f6 100644 (file)
@@ -91,6 +91,8 @@ intern
 extern
 no_warning
 
+auto_inspect
+
 pkgconfig
 cflags
 ldflags
diff --git a/src/frontend/deriving.nit b/src/frontend/deriving.nit
new file mode 100644 (file)
index 0000000..955e24a
--- /dev/null
@@ -0,0 +1,102 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Injection of automatic method definitions for standard methods, based on the attributes of the classes
+#
+# This phase is only a proof of concept and is inherently fragile:
+#
+# * syntactic code injection without semantic checking
+# * ignorance of name conflicts
+# * attributes are syntactically and locally collected
+module deriving
+
+private import parser_util
+import modelize
+private import annotation
+
+redef class ToolContext
+       # Main phase of `deriving`
+       var deriving_phase: Phase = new DerivingPhase(self, null)
+end
+
+private class DerivingPhase
+       super Phase
+
+       redef fun process_annotated_node(nclassdef, nat)
+       do
+               if nat.name == "auto_inspect" then
+                       if not nclassdef isa AStdClassdef then
+                               toolcontext.error(nclassdef.location, "Syntax error: only a concrete class can be `{nat.name}`.")
+                       else
+                               generate_inspect_method(nclassdef)
+                       end
+               end
+
+               if nat.name == "auto_derive" then
+                       if not nclassdef isa AStdClassdef then
+                               toolcontext.error(nclassdef.location, "Syntax error: only a concrete class can be `{nat.name}`.")
+                       else
+                               generate_derive_to_map_method(nclassdef, nat)
+                       end
+               end
+       end
+
+       fun generate_inspect_method(nclassdef: AClassdef)
+       do
+               var npropdefs = nclassdef.n_propdefs
+
+               var code = new Array[String]
+               code.add "redef fun inspect"
+               code.add "do"
+               code.add "      var res = super"
+               code.add "      res = res.substring(0,res.length-1)"
+
+               for attribute in npropdefs do if attribute isa AAttrPropdef then
+                       var name = attribute.n_id2.text
+                       code.add """    res += " {{{name}}}: {self.{{{name}}}.inspect}""""
+               end
+
+               code.add "      res += \">\""
+               code.add "      return res"
+               code.add "end"
+
+               # Create method Node and add it to the AST
+               npropdefs.push(toolcontext.parse_propdef(code.join("\n")))
+       end
+
+       fun generate_derive_to_map_method(nclassdef: AClassdef, nat: AAnnotation)
+       do
+               var npropdefs = nclassdef.n_propdefs
+
+               var sc = toolcontext.parse_superclass("Derivable")
+               sc.location = nat.location
+               nclassdef.n_propdefs.add sc
+
+               var code = new Array[String]
+               code.add "redef fun derive_to_map"
+               code.add "do"
+               code.add "      var res = super"
+
+               for attribute in npropdefs do if attribute isa AAttrPropdef then
+                       var name = attribute.n_id2.text
+                       code.add """    res["{{{name}}}"] = self.{{{name}}}"""
+               end
+
+               code.add "      return res"
+               code.add "end"
+
+               # Create method Node and add it to the AST
+               npropdefs.push(toolcontext.parse_propdef(code.join("\n")))
+       end
+end
index 8c5079f..9d39890 100644 (file)
@@ -22,6 +22,7 @@ import modelize
 import semantize
 import div_by_zero
 import serialization_phase
+import deriving
 import check_annotation
 import glsl_validation
 
index 65e7c67..492584b 100644 (file)
@@ -385,11 +385,7 @@ redef class ModelBuilder
                var readme = dirpath2.join_path("README.md")
                if not readme.file_exists then readme = dirpath2.join_path("README")
                if readme.file_exists then
-                       var mdoc = new MDoc
-                       var s = new FileReader.open(readme)
-                       while not s.eof do
-                               mdoc.content.add(s.read_line)
-                       end
+                       var mdoc = load_markdown(readme)
                        mgroup.mdoc = mdoc
                        mdoc.original_mentity = mgroup
                end
@@ -398,6 +394,17 @@ redef class ModelBuilder
                return mgroup
        end
 
+       # Load a markdown file as a documentation object
+       fun load_markdown(filepath: String): MDoc
+       do
+               var mdoc = new MDoc(new Location(new SourceFile.from_string(filepath, ""),0,0,0,0))
+               var s = new FileReader.open(filepath)
+               while not s.eof do
+                       mdoc.content.add(s.read_line)
+               end
+               return mdoc
+       end
+
        # Force the identification of all ModulePath of the group and sub-groups.
        fun visit_group(mgroup: MGroup) do
                var p = mgroup.filepath
@@ -448,6 +455,26 @@ redef class ModelBuilder
                return nmodule
        end
 
+       # Remove Nit source files from a list of arguments.
+       #
+       # Items of `args` that can be loaded as a nit file will be removed from `args` and returned.
+       fun filter_nit_source(args: Array[String]): Array[String]
+       do
+               var keep = new Array[String]
+               var res = new Array[String]
+               for a in args do
+                       var l = identify_file(a)
+                       if l == null then
+                               keep.add a
+                       else
+                               res.add a
+                       end
+               end
+               args.clear
+               args.add_all(keep)
+               return res
+       end
+
        # Try to load a module using a path.
        # Display an error if there is a problem (IO / lexer / parser) and return null.
        # Note: usually, you do not need this method, use `get_mmodule_by_name` instead.
@@ -459,7 +486,11 @@ redef class ModelBuilder
                # Look for the module
                var file = identify_file(filename)
                if file == null then
-                       toolcontext.error(null, "Error: cannot find module `{filename}`.")
+                       if filename.file_exists then
+                               toolcontext.error(null, "Error: `{filename}` is not a Nit source file.")
+                       else
+                               toolcontext.error(null, "Error: cannot find module `{filename}`.")
+                       end
                        return null
                end
 
index 83ed72f..b65511d 100644 (file)
@@ -55,6 +55,8 @@ class Location
        var file: nullable SourceFile
 
        # The starting line number (starting from 1)
+       #
+       # If `line_start==0` then the whole file is considered
        var line_start: Int
 
        # The stopping line number (starting from 1)
@@ -129,9 +131,12 @@ class Location
                var file_part = ""
                if file != null then
                        file_part = file.filename
-                       if file.filename.length > 0 then file_part += ":"
                end
 
+               if line_start <= 0 then return file_part
+
+               if file != null and file.filename.length > 0 then file_part += ":"
+
                if line_start == line_end then
                        if column_start == column_end then
                                return "{file_part}{line_start},{column_start}"
@@ -181,6 +186,8 @@ class Location
 
                var l = self
                var i = l.line_start
+               if i <= 0 then return ""
+
                var line_start = l.file.line_starts[i-1]
                var line_end = line_start
                var string = l.file.string
index 2f8d045..4e8c565 100644 (file)
@@ -16,6 +16,7 @@
 module mdoc
 
 import model_base
+import location
 
 # Structured documentation of a `MEntity` object
 class MDoc
@@ -27,6 +28,9 @@ class MDoc
        # The entity where the documentation is originally attached to.
        # This gives some context to resolve identifiers or to run examples.
        var original_mentity: nullable MEntity = null is writable
+
+       # The original location of the doc for error messages
+       var location: Location
 end
 
 redef class MEntity
index f0f4ff9..b483543 100644 (file)
@@ -1191,7 +1191,7 @@ class MGenericType
                for t in arguments do
                        args.add t.full_name
                end
-               return "{mclass.full_name}[{args.join(", ")}]}"
+               return "{mclass.full_name}[{args.join(", ")}]"
        end
 
        redef var c_name is lazy do
index 6cbcbef..9995338 100644 (file)
@@ -375,7 +375,7 @@ redef class ADoc
        do
                var res = mdoc_cache
                if res != null then return res
-               res = new MDoc
+               res = new MDoc(location)
                for c in n_comment do
                        var text = c.text
                        if text.length < 2 then
index ce683de..32fab5d 100644 (file)
@@ -365,7 +365,10 @@ class NeoModel
                node.labels.add "MEntity"
                node.labels.add model_name
                node["name"] = mentity.name
-               if mentity.mdoc != null then node["mdoc"] = new JsonArray.from(mentity.mdoc.content)
+               if mentity.mdoc != null then
+                       node["mdoc"] = new JsonArray.from(mentity.mdoc.content)
+                       node["mdoc_location"] = mentity.mdoc.location.to_s
+               end
                return node
        end
 
@@ -867,6 +870,9 @@ class NeoModel
                #TODO filepath
                var parts = loc.split_with(":")
                var file = new SourceFile.from_string(parts[0], "")
+               if parts.length == 1 then
+                       return new Location(file, 0, 0, 0, 0)
+               end
                var pos = parts[1].split_with("--")
                var pos1 = pos[0].split_with(",")
                var pos2 = pos[1].split_with(",")
@@ -919,7 +925,8 @@ class NeoModel
                        for e in node["mdoc"].as(JsonArray) do
                                lines.add e.to_s#.replace("\n", "\\n")
                        end
-                       var mdoc = new MDoc
+                       var location = to_location(node["mdoc_location"].to_s)
+                       var mdoc = new MDoc(location)
                        mdoc.content.add_all(lines)
                        mdoc.original_mentity = mentity
                        mentity.mdoc = mdoc
index 7969f29..e86f92f 100644 (file)
@@ -53,7 +53,9 @@ end
 var model = new Model
 var modelbuilder = new ModelBuilder(model, toolcontext)
 
-var mmodules = modelbuilder.parse_full(args)
+var module_files = modelbuilder.filter_nit_source(args)
+
+var mmodules = modelbuilder.parse_full(module_files)
 modelbuilder.run_phases
 
 if toolcontext.opt_gen_unit.value then
@@ -65,6 +67,21 @@ var page = new HTMLTag("testsuites")
 
 if toolcontext.opt_full.value then mmodules = model.mmodules
 
+for a in args do
+       if not a.file_exists then
+               toolcontext.fatal_error(null, "Error: cannot load file or module `{a}`.")
+       end
+       # Try to load the file as a markdown document
+       var mdoc = modelbuilder.load_markdown(a)
+       page.add modelbuilder.test_mdoc(mdoc)
+end
+
+for a in module_files do
+       var g = modelbuilder.get_mgroup(a)
+       if g == null then continue
+       page.add modelbuilder.test_group(g)
+end
+
 for m in mmodules do
        page.add modelbuilder.test_markdown(m)
        page.add modelbuilder.test_unit(m)
index d6de328..563c418 100644 (file)
@@ -25,8 +25,8 @@ class NitUnitExecutor
        # The prefix of the generated Nit source-file
        var prefix: String
 
-       # The module to import
-       var mmodule: MModule
+       # The module to import, if any
+       var mmodule: nullable MModule
 
        # The XML node associated to the module
        var testsuite: HTMLTag
@@ -57,8 +57,8 @@ class NitUnitExecutor
                if not (ast isa AModule or ast isa ABlockExpr or ast isa AExpr) then
                        var message = ""
                        if ast isa AError then message = " At {ast.location}: {ast.message}."
-                       toolcontext.warning(ndoc.location, "invalid-block", "Error: There is a block of code that is not valid Nit, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
-                       failures.add("{ndoc.location}: Invalid block of code.{message}")
+                       toolcontext.warning(mdoc.location, "invalid-block", "Error: There is a block of code that is not valid Nit, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`).{message}")
+                       failures.add("{mdoc.location}: Invalid block of code.{message}")
                        return
                end
 
@@ -72,8 +72,8 @@ class NitUnitExecutor
                blocks.last.add(text)
        end
 
-       # The associated node to localize warnings
-       var ndoc: nullable ADoc = null
+       # The associated documentation object
+       var mdoc: nullable MDoc = null
 
        # used to generate distinct names
        var cpt = 0
@@ -82,14 +82,14 @@ class NitUnitExecutor
        # Fill `docunits` with new discovered unit of tests.
        #
        # `tc` (testcase) is the pre-filled XML node
-       fun extract(ndoc: ADoc, tc: HTMLTag)
+       fun extract(mdoc: MDoc, tc: HTMLTag)
        do
                blocks.clear
                failures.clear
 
-               self.ndoc = ndoc
+               self.mdoc = mdoc
 
-               work(ndoc.to_mdoc)
+               work(mdoc)
 
                toolcontext.check_errors
 
@@ -106,7 +106,7 @@ class NitUnitExecutor
                if blocks.is_empty then return
 
                for block in blocks do
-                       docunits.add new DocUnit(ndoc, tc, block.join(""))
+                       docunits.add new DocUnit(mdoc, tc, block.join(""))
                end
        end
 
@@ -144,11 +144,7 @@ class NitUnitExecutor
                var dir = file.dirname
                if dir != "" then dir.mkdir
                var f
-               f = new FileWriter.open(file)
-               f.write("# GENERATED FILE\n")
-               f.write("# Docunits extracted from comments\n")
-               f.write("import {mmodule.name}\n")
-               f.write("\n")
+               f = create_unitfile(file)
                var i = 0
                for du in dus do
 
@@ -166,14 +162,7 @@ class NitUnitExecutor
 
                if toolcontext.opt_noact.value then return
 
-               var nit_dir = toolcontext.nit_dir
-               var nitg = nit_dir/"bin/nitg"
-               if not nitg.file_exists then
-                       toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
-               var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
-               var res = sys.system(cmd)
+               var res = compile_unitfile(file)
 
                if res != 0 then
                        # Compilation error.
@@ -208,7 +197,7 @@ class NitUnitExecutor
                                var ne = new HTMLTag("error")
                                ne.attr("message", msg)
                                tc.add ne
-                               toolcontext.warning(du.ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                               toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
                                toolcontext.modelbuilder.failed_entities += 1
                        end
                        toolcontext.check_errors
@@ -229,27 +218,14 @@ class NitUnitExecutor
 
                toolcontext.info("Execute doc-unit {tc.attrs["name"]} in {file}", 1)
 
-               var dir = file.dirname
-               if dir != "" then dir.mkdir
                var f
-               f = new FileWriter.open(file)
-               f.write("# GENERATED FILE\n")
-               f.write("# Example extracted from a documentation\n")
-               f.write("import {mmodule.name}\n")
-               f.write("\n")
+               f = create_unitfile(file)
                f.write(du.block)
                f.close
 
                if toolcontext.opt_noact.value then return
 
-               var nit_dir = toolcontext.nit_dir
-               var nitg = nit_dir/"bin/nitg"
-               if not nitg.file_exists then
-                       toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
-                       toolcontext.check_errors
-               end
-               var cmd = "{nitg} --ignore-visibility --no-color '{file}' -I {mmodule.location.file.filename.dirname} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
-               var res = sys.system(cmd)
+               var res = compile_unitfile(file)
                var res2 = 0
                if res == 0 then
                        res2 = sys.system("{file.to_program_name}.bin >>'{file}.out1' 2>&1 </dev/null")
@@ -272,25 +248,68 @@ class NitUnitExecutor
                        var ne = new HTMLTag("failure")
                        ne.attr("message", msg)
                        tc.add ne
-                       toolcontext.warning(du.ndoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       toolcontext.warning(du.mdoc.location, "failure", "FAILURE: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                else if res2 != 0 then
                        var ne = new HTMLTag("error")
                        ne.attr("message", msg)
                        tc.add ne
-                       toolcontext.warning(du.ndoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
+                       toolcontext.warning(du.mdoc.location, "error", "ERROR: {tc.attrs["classname"]}.{tc.attrs["name"]} (in {file}): {msg}")
                        toolcontext.modelbuilder.failed_entities += 1
                end
                toolcontext.check_errors
 
                testsuite.add(tc)
        end
+
+       # Create and fill the header of a unit file `file`.
+       #
+       # A unit file is a Nit source file generated from one
+       # or more docunits that will be compiled and executed.
+       #
+       # The handled on the file is returned and must be completed and closed.
+       #
+       # `file` should be a valid filepath for a Nit source file.
+       private fun create_unitfile(file: String): Writer
+       do
+               var dir = file.dirname
+               if dir != "" then dir.mkdir
+               var f
+               f = new FileWriter.open(file)
+               f.write("# GENERATED FILE\n")
+               f.write("# Docunits extracted from comments\n")
+               if mmodule != null then
+                       f.write("import {mmodule.name}\n")
+               end
+               f.write("\n")
+               return f
+       end
+
+       # Compile an unit file and return the compiler return code
+       #
+       # Can terminate the program if the compiler is not found
+       private fun compile_unitfile(file: String): Int
+       do
+               var nit_dir = toolcontext.nit_dir
+               var nitg = nit_dir/"bin/nitg"
+               if not nitg.file_exists then
+                       toolcontext.error(null, "Cannot find nitg. Set envvar NIT_DIR.")
+                       toolcontext.check_errors
+               end
+               var opts = new Array[String]
+               if mmodule != null then
+                       opts.add "-I {mmodule.location.file.filename.dirname}"
+               end
+               var cmd = "{nitg} --ignore-visibility --no-color '{file}' {opts.join(" ")} >'{file}.out1' 2>&1 </dev/null -o '{file}.bin'"
+               var res = sys.system(cmd)
+               return res
+       end
 end
 
 # A unit-test to run
 class DocUnit
-       # The original comment node
-       var ndoc: ADoc
+       # The doc that contains self
+       var mdoc: MDoc
 
        # The XML node that contains the information about the execution
        var testcase: HTMLTag
@@ -350,7 +369,7 @@ redef class ModelBuilder
                        # NOTE: jenkins expects a '.' in the classname attr
                        tc.attr("classname", "nitunit." + mmodule.full_name + ".<module>")
                        tc.attr("name", "<module>")
-                       d2m.extract(ndoc, tc)
+                       d2m.extract(ndoc.to_mdoc, tc)
                end label x
                for nclassdef in nmodule.n_classdefs do
                        var mclassdef = nclassdef.mclassdef
@@ -363,7 +382,7 @@ redef class ModelBuilder
                                        tc = new HTMLTag("testcase")
                                        tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
                                        tc.attr("name", "<class>")
-                                       d2m.extract(ndoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, tc)
                                end
                        end
                        for npropdef in nclassdef.n_propdefs do
@@ -376,7 +395,7 @@ redef class ModelBuilder
                                        tc = new HTMLTag("testcase")
                                        tc.attr("classname", "nitunit." + mmodule.full_name + "." + mclassdef.mclass.full_name)
                                        tc.attr("name", mpropdef.mproperty.full_name)
-                                       d2m.extract(ndoc, tc)
+                                       d2m.extract(ndoc.to_mdoc, tc)
                                end
                        end
                end
@@ -385,4 +404,67 @@ redef class ModelBuilder
 
                return ts
        end
+
+       # Extracts and executes all the docunits in the readme of the `mgroup`
+       # Returns a JUnit-compatible `<testsuite>` XML element that contains the results of the executions.
+       fun test_group(mgroup: MGroup): HTMLTag
+       do
+               var ts = new HTMLTag("testsuite")
+               toolcontext.info("nitunit: doc-unit group {mgroup}", 2)
+
+               # usually, only the default module must be imported in the unit test.
+               var o = mgroup.default_mmodule
+
+               ts.attr("package", mgroup.full_name)
+
+               var prefix = toolcontext.test_dir
+               prefix = prefix.join_path(mgroup.to_s)
+               var d2m = new NitUnitExecutor(toolcontext, prefix, o, ts)
+
+               var tc
+
+               total_entities += 1
+               var mdoc = mgroup.mdoc
+               if mdoc == null then return ts
+
+               doc_entities += 1
+               tc = new HTMLTag("testcase")
+               # NOTE: jenkins expects a '.' in the classname attr
+               tc.attr("classname", "nitunit." + mgroup.full_name)
+               tc.attr("name", "<group>")
+               d2m.extract(mdoc, tc)
+
+               d2m.run_tests
+
+               return ts
+       end
+
+       # Test a document object unrelated to a Nit entity
+       fun test_mdoc(mdoc: MDoc): HTMLTag
+       do
+               var ts = new HTMLTag("testsuite")
+               var file = mdoc.location.to_s
+
+               toolcontext.info("nitunit: doc-unit file {file}", 2)
+
+               ts.attr("package", file)
+
+               var prefix = toolcontext.test_dir / "file"
+               var d2m = new NitUnitExecutor(toolcontext, prefix, null, ts)
+
+               var tc
+
+               total_entities += 1
+               doc_entities += 1
+
+               tc = new HTMLTag("testcase")
+               # NOTE: jenkins expects a '.' in the classname attr
+               tc.attr("classname", "nitunit.<file>")
+               tc.attr("name", file)
+
+               d2m.extract(mdoc, tc)
+               d2m.run_tests
+
+               return ts
+       end
 end
index f93e9d7..b3ac901 100644 (file)
@@ -3,3 +3,5 @@ test_nitunit.nit --gen-suite --only-show
 test_nitunit.nit --gen-suite --only-show --private
 test_nitunit2.nit -o $WRITE
 test_doc2.nit --no-color -o $WRITE
+test_nitunit3 --no-color -o $WRITE
+test_nitunit_md.md --no-color -o $WRITE
index 650bdaa..90e0d60 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>
 
index 650bdaa..90e0d60 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>
 
diff --git a/tests/sav/nitg-g/fixme/test_deriving_alt1.res b/tests/sav/nitg-g/fixme/test_deriving_alt1.res
new file mode 100644 (file)
index 0000000..7457500
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+<A>
+
+true
+true
+true
+
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+i=100 s=World a=<A>
+<B>
+
+true
+true
+true
diff --git a/tests/sav/nitunit_args6.res b/tests/sav/nitunit_args6.res
new file mode 100644 (file)
index 0000000..85c2831
--- /dev/null
@@ -0,0 +1,14 @@
+test_nitunit3/README.md: Error: There is a block of code that is not valid Nit, thus not considered a nitunit. To suppress this warning, enclose the block with a fence tagged `nitish` or `raw` (see `man nitdoc`). At 1,1: Syntax error: unknown token ;..
+test_nitunit3/README.md: ERROR: nitunit.test_nitunit3.<group> (in .nitunit/test_nitunit3-0.nit): Runtime error: Assert failed (.nitunit/test_nitunit3-0.nit:7)
+
+DocUnits:
+Entities: 2; Documented ones: 2; With nitunits: 2; Failures: 2
+
+TestSuites:
+No test cases found
+Class suites: 0; Test Cases: 0; Failures: 0
+<testsuites><testsuite package="test_nitunit3"><testcase classname="nitunit.test_nitunit3" name="&lt;group&gt;"><failure message="test_nitunit3&#47;README.md: Invalid block of code. At 1,1: Syntax error: unknown token ;.."></failure><system-err></system-err><system-out>assert false
+assert true
+</system-out><error message="Runtime error: Assert failed (.nitunit&#47;test_nitunit3-0.nit:7)
+"></error></testcase></testsuite><testsuite package="test_nitunit3"><testcase classname="nitunit.test_nitunit3.&lt;module&gt;" name="&lt;module&gt;"><system-err></system-err><system-out>assert true
+</system-out></testcase></testsuite><testsuite></testsuite></testsuites>
\ No newline at end of file
diff --git a/tests/sav/nitunit_args7.res b/tests/sav/nitunit_args7.res
new file mode 100644 (file)
index 0000000..ea6bab2
--- /dev/null
@@ -0,0 +1,13 @@
+test_nitunit_md.md: ERROR: nitunit.<file>.test_nitunit_md.md (in .nitunit/file-0.nit): Runtime error: Assert failed (.nitunit/file-0.nit:8)
+
+DocUnits:
+Entities: 1; Documented ones: 1; With nitunits: 1; Failures: 1
+
+TestSuites:
+No test cases found
+Class suites: 0; Test Cases: 0; Failures: 0
+<testsuites><testsuite package="test_nitunit_md.md"><testcase classname="nitunit.&lt;file&gt;" name="test_nitunit_md.md"><system-err></system-err><system-out>var a = 1
+assert 1 == 1
+assert false
+</system-out><error message="Runtime error: Assert failed (.nitunit&#47;file-0.nit:8)
+"></error></testcase></testsuite></testsuites>
\ No newline at end of file
diff --git a/tests/sav/test_deriving.res b/tests/sav/test_deriving.res
new file mode 100644 (file)
index 0000000..6020b5f
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+i:5; s:Hello
+
+true
+true
+true
+
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+i=100 s=World a=i:5; s:Hello
+i:100; s:World; a:i:5; s:Hello
+
+true
+true
+true
diff --git a/tests/sav/test_deriving_alt1.res b/tests/sav/test_deriving_alt1.res
new file mode 100644 (file)
index 0000000..88d1f35
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+<A i: <Int> s: <FlatString>>
+
+true
+true
+true
+
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+i=100 s=World a=<A i: <Int> s: <FlatString>>
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+
+true
+true
+true
diff --git a/tests/sav/test_deriving_alt2.res b/tests/sav/test_deriving_alt2.res
new file mode 100644 (file)
index 0000000..7af6a70
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+i:5; s:Hello
+
+false
+false
+true
+
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+i=100 s=World a=i:5; s:Hello
+i:100; s:World; a:i:5; s:Hello
+
+false
+false
+true
diff --git a/tests/sav/test_deriving_alt3.res b/tests/sav/test_deriving_alt3.res
new file mode 100644 (file)
index 0000000..9e385ed
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+i:5; s:Hello
+
+true
+true
+true
+
+<B i: <Int> s: <FlatString> a: <A i: <Int> s: <FlatString>>>
+i=100 s=World
+i:100; s:World
+
+true
+true
+false
diff --git a/tests/sav/test_deriving_alt4.res b/tests/sav/test_deriving_alt4.res
new file mode 100644 (file)
index 0000000..16cf4a0
--- /dev/null
@@ -0,0 +1,15 @@
+<A i: <Int> s: <FlatString>>
+i=5 s=Hello
+i:5; s:Hello
+
+true
+true
+true
+
+<B a: <A i: <Int> s: <FlatString>>>
+string=World a=i:5; s:Hello
+string:World; a:i:5; s:Hello
+
+true
+true
+true
index 33ac09b..13f9a72 100644 (file)
@@ -1,4 +1,4 @@
-Runtime error: Aborted (../lib/serialization.nit:72)
+Runtime error: Aborted (../lib/serialization/serialization.nit:109)
 # Nit:
 <A: true a 0.123 1234 asdf false>
 
diff --git a/tests/test_deriving.nit b/tests/test_deriving.nit
new file mode 100644 (file)
index 0000000..5f02241
--- /dev/null
@@ -0,0 +1,82 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import deriving
+
+redef class Object
+       # Redef to avoid unstable `object_id`
+       redef fun inspect_head do return "{class_name}"
+end
+
+class A
+       auto_inspect
+       auto_derive
+       super DeriveToS#alt1#
+       super DeriveEqual#alt2#
+
+       var i: Int
+       var s: String
+end
+
+class A2
+       super DeriveToS
+       super DeriveEqual
+
+       redef fun derive_to_map
+       do
+               var res = super
+               res["string"] = s # drop i
+               return res
+       end
+
+       var i: Int
+       var s: String
+end
+
+class B
+       super A#alt4#super A2
+
+       auto_inspect
+       auto_derive#alt3#
+
+       var a: A
+end
+
+var a = new A(5, "Hello")
+print a.inspect
+print a.derive_to_map.join(" ", "=")
+print a
+
+print ""
+
+var a2 = new A(5, "Hel" + "lo")
+var a3 = new A(6, "Hel" + "lo")
+print a == a2
+print a.hash == a2.hash
+print a != a3
+
+print ""
+
+var b = new B(100, "World", a)
+print b.inspect
+print b.derive_to_map.join(" ", "=")
+print b
+
+print ""
+
+var b2 = new B(100, "World", a2)
+var b3 = new B(100, "World", a3)
+print b == b2
+print b.hash == b2.hash
+print b != b3
diff --git a/tests/test_nitunit3/README.md b/tests/test_nitunit3/README.md
new file mode 100644 (file)
index 0000000..5c19d05
--- /dev/null
@@ -0,0 +1,13 @@
+Bla bla
+
+~~~
+assert false
+~~~
+
+~~~
+;'\][]
+~~~
+
+~~~
+assert true
+~~~
diff --git a/tests/test_nitunit3/test_nitunit3.nit b/tests/test_nitunit3/test_nitunit3.nit
new file mode 100644 (file)
index 0000000..033b187
--- /dev/null
@@ -0,0 +1,18 @@
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Test
+#
+#     assert true
+module test_nitunit3
diff --git a/tests/test_nitunit_md.md b/tests/test_nitunit_md.md
new file mode 100644 (file)
index 0000000..5bcfa88
--- /dev/null
@@ -0,0 +1,15 @@
+# Test
+
+~~~
+var a = 1
+~~~
+
+~~~raw
+ignored
+~~~
+
+~~~
+assert 1 == 1
+~~~
+
+    assert false