Merge: Derive all the things
authorJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:41:23 +0000 (12:41 +0700)
committerJean Privat <jean@pryen.org>
Wed, 18 Mar 2015 05:41:23 +0000 (12:41 +0700)
This PR is crazy and inspired by https://github.com/privat/nit/pull/1202#discussion_r26359435
Basically, you will find here some hackish user-level pseudo meta-programming with optional unsafe support from the execution engines trough injection of code in the AST.

The idea is a generalization (and a basic simplification) of the approach or @xymus for serialization.
If fact, there is 2 level of generalizations.

In the compiler, a new phase `deriving` offers a static deriving mechanism. For instance, the annotation `auto_inspect` will implements the `inspect` method with a simple recursive inspection of attributes.

In the standard library, a new module `deriving` offers a general mechanism with a new standard `derive_to_map` method that is expected to dump attributes in a simple HashMap.

This basic low-level method is used to provide user-defined deriving methods.
For instance, the module provide basic derived implementation of `==`, `to_s` and `hash` in pure Nit at the user-level.

Moreover, the compiler phase `deriving` is extended to provide `auto_derive` that statically implements `derive_to_map`

Here an example from the code:

~~~nit
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"
~~~

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

12 files changed:
lib/deriving.nit [new file with mode: 0644]
lib/standard/collection/abstract_collection.nit
src/frontend/check_annotation.nit
src/frontend/deriving.nit [new file with mode: 0644]
src/frontend/frontend.nit
tests/sav/nitg-g/fixme/test_deriving_alt1.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/test_deriving.nit [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 e1cb6a0..d51e710 100644 (file)
@@ -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 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
 
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/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
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