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>
--- /dev/null
+# 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
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`.
extern
no_warning
+auto_inspect
+
pkgconfig
cflags
ldflags
--- /dev/null
+# 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
import semantize
import div_by_zero
import serialization_phase
+import deriving
import check_annotation
import glsl_validation
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+<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
--- /dev/null
+# 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