frontend: intro phase to explain assert failures
authorAlexis Laferrière <alexis.laf@xymus.net>
Tue, 1 Aug 2017 14:53:44 +0000 (10:53 -0400)
committerAlexis Laferrière <alexis.laf@xymus.net>
Thu, 28 Sep 2017 15:08:28 +0000 (11:08 -0400)
Signed-off-by: Alexis Laferrière <alexis.laf@xymus.net>

src/frontend/code_gen.nit
src/frontend/explain_assert.nit [new file with mode: 0644]
src/frontend/explain_assert_api.nit [new file with mode: 0644]

index ccbddde..48f1aa7 100644 (file)
@@ -18,3 +18,4 @@ module code_gen
 import frontend
 import actors_generation_phase
 import serialization_code_gen_phase
+import explain_assert
diff --git a/src/frontend/explain_assert.nit b/src/frontend/explain_assert.nit
new file mode 100644 (file)
index 0000000..b2350a4
--- /dev/null
@@ -0,0 +1,253 @@
+# 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.
+
+# Explain failed assert to the console by modifying the AST.
+#
+# This module implements the service `AAssertExpr::explain_assert_str`,
+# which should be used by the engines.
+#
+# Example assert:
+#
+# ~~~nitish
+# var x = 0.0
+# var y = 1.0
+# assert x.is_approx(y, 0.5)
+# ~~~
+#
+# Produces the following output on failure:
+#
+# ~~~raw
+# Runtime assert: 0.0.is_approx(1.0, 0.5)
+# ~~~
+module explain_assert
+
+import astbuilder
+intrude import literal # for value=
+intrude import typing # for mtype=
+
+import explain_assert_api
+
+redef class ToolContext
+
+       # Phase modifying the AST to explain assets when they fail
+       var explain_assert_phase: Phase = new ExplainAssertPhase(self, [modelize_class_phase, typing_phase, literal_phase])
+end
+
+private class ExplainAssertPhase
+       super Phase
+
+       redef fun process_nmodule(nmodule)
+       do
+               var mmodule = nmodule.mmodule
+               if mmodule == null then return
+
+               # Skip if `mmodule` doesn't have access to `String`
+               var string_class = toolcontext.modelbuilder.try_get_mclass_by_name(nmodule, mmodule, "String")
+               if string_class == null then return
+
+               # Launch a visitor on all elements of the AST
+               var visitor = new ExplainAssertVisitor(toolcontext, mmodule, string_class.mclass_type)
+               visitor.enter_visit nmodule
+       end
+end
+
+# Visitor to find and explain asserts
+private class ExplainAssertVisitor
+       super Visitor
+
+       # The toolcontext is our entry point to most services
+       var toolcontext: ToolContext
+
+       # The visited module
+       var mmodule: MModule
+
+       # Type of `String` (the generated code does not work without a `String`)
+       var string_mtype: MType
+
+       # Tool to modify the AST
+       var builder = new ASTBuilder(mmodule) is lazy
+
+       redef fun visit(node)
+       do
+               # Recursively visit all sub-nodes
+               node.visit_all(self)
+
+               # Only work on asserts
+               if not node isa AAssertExpr then return
+               var expr = node.n_expr
+
+               # Skip assert on a single boolean var and asserts on false:
+               # ~~~
+               # assert false
+               # # or
+               # var x = false # Or any boolean expression
+               # assert x
+               # ~~~
+               if expr isa AVarExpr or expr isa AFalseExpr then return
+
+               # Build the superstring to explain the assert
+               var explain_str = new ASuperstringExpr
+
+               # Prepare attribute used by visited nodes
+               self.assert_node = node
+               self.explain_str = explain_str
+               expr.accept_explain_assert self
+
+               # Done! Store the superstring in the assert's node
+               if explain_str.n_exprs.not_empty then
+                       node.explain_assert_str = explain_str
+               end
+       end
+
+       # Visited assert node
+       var assert_node: AAssertExpr is noinit
+
+       # Superstring in construction to explain the `assert_node`
+       var explain_str: ASuperstringExpr is noinit
+
+       # Build an `AStringExpr` containing `value`
+       #
+       # Add it to `explain_str` if `auto_add == true`, the default.
+       fun explain_string(value: String, auto_add: nullable Bool): AStringExpr
+       do
+               auto_add = auto_add or else true
+
+               var tk = new TString
+               tk.text = "\"{value}\""
+               var op = new AStringExpr
+               op.n_string = tk
+               op.mtype = string_mtype
+               op.value = value
+               op.location = assert_node.location
+
+               if auto_add then explain_str.n_exprs.add op
+               return op
+       end
+
+       # Add the value of `v_expr` to `explain_str` and protect null values
+       fun explain_expr(v_expr: AExpr)
+       do
+               var mtype = v_expr.mtype
+               if mtype == null then
+                       explain_string "<unexpected error>"
+                       return
+               end
+
+               # Set the expression value aside
+               var expr = v_expr.make_var_read
+
+               # Protect nullable types
+               if mtype isa MNullType then
+                       explain_string "null"
+                       return
+               else if mtype isa MNullableType then
+                       var e = new AOrElseExpr
+                       e.n_expr = expr
+                       e.n_expr2 = explain_string("null", false)
+                       e.location = assert_node.location
+                       e.mtype = mmodule.object_type
+
+                       explain_str.n_exprs.add e
+                       return
+               end
+
+               explain_str.n_exprs.add expr
+       end
+
+       # Add all the arguments in `AExprs` to `explain_str`
+       fun explain_args(n_args: AExprs)
+       do
+               var first = true
+               for n_arg in n_args.to_a do
+                       if not first then
+                               explain_string ", "
+                       else first = false
+
+                       explain_expr n_arg
+               end
+       end
+end
+
+redef class AAssertExpr
+       redef var explain_assert_str = null
+end
+
+redef class AExpr
+       # Fill `v` to explain this node if the parent assert fails
+       private fun accept_explain_assert(v: ExplainAssertVisitor)
+       do if mtype != null then v.explain_expr self
+end
+
+redef class ABinopExpr
+       redef fun accept_explain_assert(v)
+       do
+               if n_expr.mtype == null or n_expr2.mtype == null then return
+
+               v.explain_expr n_expr
+               v.explain_string " {n_op.text} "
+               v.explain_expr n_expr2
+       end
+end
+
+redef class ACallExpr
+       redef fun accept_explain_assert(v)
+       do
+               if n_expr.mtype == null then return
+
+               v.explain_expr n_expr
+               v.explain_string ".{n_qid.n_id.text}"
+
+               if n_args.to_a.not_empty then
+                       v.explain_string "("
+                       v.explain_args n_args
+                       v.explain_string ")"
+               end
+       end
+end
+
+redef class ABraExpr
+       redef fun accept_explain_assert(v)
+       do
+               if n_expr.mtype == null then return
+
+               v.explain_expr n_expr
+               v.explain_string "["
+               v.explain_args n_args
+               v.explain_string "]"
+       end
+end
+
+redef class AIsaExpr
+       redef fun accept_explain_assert(v)
+       do
+               if n_expr.mtype == null then return
+
+               v.explain_expr n_expr
+               v.explain_string " {n_kwisa.text} "
+               v.explain_string n_type.collect_text
+       end
+end
+
+redef class ANotExpr
+       redef fun accept_explain_assert(v)
+       do
+               v.explain_string "{n_kwnot.text} "
+               n_expr.accept_explain_assert v
+       end
+end
+
+redef class ABinBoolExpr
+       # Don't explain the conditions using `and`, `or`, etc.
+       redef fun accept_explain_assert(v) do end
+end
diff --git a/src/frontend/explain_assert_api.nit b/src/frontend/explain_assert_api.nit
new file mode 100644 (file)
index 0000000..6b36713
--- /dev/null
@@ -0,0 +1,28 @@
+# 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.
+
+# Explain failed assert to the console (service declaration only)
+#
+# The only service `assert_expr_str` is implemented by the
+# `explain_assert` module.
+module explain_assert_api
+
+import parser
+
+redef class AAssertExpr
+       # Superstring explaining `self` if the assert fails
+       #
+       # Engines should print out this superstring.
+       fun explain_assert_str: nullable ASuperstringExpr do return null
+end