326f58886e81b8ed33753a2de3c858db19233bc7
[nit.git] / src / frontend / explain_assert.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Explain failed assert to the console by modifying the AST.
16 #
17 # This module implements the service `AAssertExpr::explain_assert_str`,
18 # which should be used by the engines.
19 #
20 # Example assert:
21 #
22 # ~~~nitish
23 # var x = 0.0
24 # var y = 1.0
25 # assert x.is_approx(y, 0.5)
26 # ~~~
27 #
28 # Produces the following output on failure:
29 #
30 # ~~~raw
31 # Runtime assert: 0.0.is_approx(1.0, 0.5)
32 # ~~~
33 module explain_assert
34
35 import astbuilder
36 intrude import literal # for value=
37 intrude import typing # for mtype=
38 import astvalidation
39
40 import explain_assert_api
41
42 redef class ToolContext
43
44 # Phase modifying the AST to explain assets when they fail
45 var explain_assert_phase: Phase = new ExplainAssertPhase(self, [modelize_class_phase, typing_phase, literal_phase])
46 end
47
48 private class ExplainAssertPhase
49 super Phase
50
51 redef fun process_nmodule(nmodule)
52 do
53 var mmodule = nmodule.mmodule
54 if mmodule == null then return
55
56 # Skip if `mmodule` doesn't have access to `String`
57 var string_class = toolcontext.modelbuilder.try_get_mclass_by_name(nmodule, mmodule, "String")
58 if string_class == null then return
59
60 # Launch a visitor on all elements of the AST
61 var visitor = new ExplainAssertVisitor(toolcontext, mmodule, string_class.mclass_type)
62 visitor.enter_visit nmodule
63 end
64 end
65
66 # Visitor to find and explain asserts
67 private class ExplainAssertVisitor
68 super Visitor
69
70 # The toolcontext is our entry point to most services
71 var toolcontext: ToolContext
72
73 # The visited module
74 var mmodule: MModule
75
76 # Type of `String` (the generated code does not work without a `String`)
77 var string_mtype: MType
78
79 # Tool to modify the AST
80 var builder = new ASTBuilder(mmodule) is lazy
81
82 redef fun visit(node)
83 do
84 # Recursively visit all sub-nodes
85 node.visit_all(self)
86
87 # Only work on asserts
88 if not node isa AAssertExpr then return
89 var expr = node.n_expr
90
91 # Skip assert on a single boolean var and asserts on false:
92 # ~~~
93 # assert false
94 # # or
95 # var x = false # Or any boolean expression
96 # assert x
97 # ~~~
98 if expr isa AVarExpr or expr isa AFalseExpr then return
99
100 # Build the superstring to explain the assert
101 var explain_str = new ASuperstringExpr
102
103 # Prepare attribute used by visited nodes
104 self.assert_node = node
105 self.explain_str = explain_str
106 expr.accept_explain_assert self
107
108 # Done! Store the superstring in the assert's node
109 if explain_str.n_exprs.not_empty then
110 node.explain_assert_str = explain_str
111 end
112 end
113
114 # Visited assert node
115 var assert_node: AAssertExpr is noinit
116
117 # Superstring in construction to explain the `assert_node`
118 var explain_str: ASuperstringExpr is noinit
119
120 # Build an `AStringExpr` containing `value`
121 #
122 # Add it to `explain_str` if `auto_add == true`, the default.
123 fun explain_string(value: String, auto_add: nullable Bool): AStringExpr
124 do
125 auto_add = auto_add or else true
126
127 var tk = new TString
128 tk.text = "\"{value}\""
129 var op = new AStringExpr
130 op.n_string = tk
131 op.mtype = string_mtype
132 op.value = value
133 op.location = assert_node.location
134
135 if auto_add then explain_str.n_exprs.add op
136 return op
137 end
138
139 # Add the value of `v_expr` to `explain_str` and protect null values
140 fun explain_expr(v_expr: AExpr)
141 do
142 var mtype = v_expr.mtype
143 if mtype == null then
144 explain_string "<unexpected error>"
145 return
146 end
147
148 # Set the expression value aside
149 var old_parent = v_expr.parent
150 var expr = v_expr.make_var_read
151 if old_parent != null then old_parent.validate
152
153 # Protect nullable types
154 if mtype isa MNullType then
155 explain_string "null"
156 return
157 else if mtype isa MNullableType then
158 var e = new AOrElseExpr
159 e.n_expr = expr
160 e.n_expr2 = explain_string("null", false)
161 e.location = assert_node.location
162 e.mtype = mmodule.object_type
163
164 explain_str.n_exprs.add e
165 return
166 end
167
168 explain_str.n_exprs.add expr
169 end
170
171 # Add all the arguments in `AExprs` to `explain_str`
172 fun explain_args(n_args: AExprs)
173 do
174 var first = true
175 for n_arg in n_args.to_a do
176 if not first then
177 explain_string ", "
178 else first = false
179
180 explain_expr n_arg
181 end
182 end
183 end
184
185 redef class AAssertExpr
186 redef var explain_assert_str = null
187 end
188
189 redef class AExpr
190 # Fill `v` to explain this node if the parent assert fails
191 private fun accept_explain_assert(v: ExplainAssertVisitor)
192 do if mtype != null then v.explain_expr self
193 end
194
195 redef class ABinopExpr
196 redef fun accept_explain_assert(v)
197 do
198 if n_expr.mtype == null or n_expr2.mtype == null then return
199
200 v.explain_expr n_expr
201 v.explain_string " {n_op.text} "
202 v.explain_expr n_expr2
203 end
204 end
205
206 redef class ACallExpr
207 redef fun accept_explain_assert(v)
208 do
209 if n_expr.mtype == null then return
210
211 v.explain_expr n_expr
212 v.explain_string ".{n_qid.n_id.text}"
213
214 if n_args.to_a.not_empty then
215 v.explain_string "("
216 v.explain_args n_args
217 v.explain_string ")"
218 end
219 end
220 end
221
222 redef class ABraExpr
223 redef fun accept_explain_assert(v)
224 do
225 if n_expr.mtype == null then return
226
227 v.explain_expr n_expr
228 v.explain_string "["
229 v.explain_args n_args
230 v.explain_string "]"
231 end
232 end
233
234 redef class AIsaExpr
235 redef fun accept_explain_assert(v)
236 do
237 if n_expr.mtype == null then return
238
239 v.explain_expr n_expr
240 v.explain_string " {n_kwisa.text} "
241 v.explain_string n_type.collect_text
242 end
243 end
244
245 redef class ANotExpr
246 redef fun accept_explain_assert(v)
247 do
248 v.explain_string "{n_kwnot.text} "
249 n_expr.accept_explain_assert v
250 end
251 end
252
253 redef class ABinBoolExpr
254 # Don't explain the conditions using `and`, `or`, etc.
255 redef fun accept_explain_assert(v) do end
256 end