Moving the astvalidation module in astbuilder
[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
39 import explain_assert_api
40
41 redef class ToolContext
42
43 # Phase modifying the AST to explain assets when they fail
44 var explain_assert_phase: Phase = new ExplainAssertPhase(self, [modelize_class_phase, typing_phase, literal_phase])
45 end
46
47 private class ExplainAssertPhase
48 super Phase
49
50 redef fun process_nmodule(nmodule)
51 do
52 var mmodule = nmodule.mmodule
53 if mmodule == null then return
54
55 # Skip if `mmodule` doesn't have access to `String`
56 var string_class = toolcontext.modelbuilder.try_get_mclass_by_name(nmodule, mmodule, "String")
57 if string_class == null then return
58
59 # Launch a visitor on all elements of the AST
60 var visitor = new ExplainAssertVisitor(toolcontext, mmodule, string_class.mclass_type)
61 visitor.enter_visit nmodule
62 end
63 end
64
65 # Visitor to find and explain asserts
66 private class ExplainAssertVisitor
67 super Visitor
68
69 # The toolcontext is our entry point to most services
70 var toolcontext: ToolContext
71
72 # The visited module
73 var mmodule: MModule
74
75 # Type of `String` (the generated code does not work without a `String`)
76 var string_mtype: MType
77
78 # Tool to modify the AST
79 var builder = new ASTBuilder(mmodule) is lazy
80
81 redef fun visit(node)
82 do
83 # Recursively visit all sub-nodes
84 node.visit_all(self)
85
86 # Only work on asserts
87 if not node isa AAssertExpr then return
88 var expr = node.n_expr
89
90 # Skip assert on a single boolean var and asserts on false:
91 # ~~~
92 # assert false
93 # # or
94 # var x = false # Or any boolean expression
95 # assert x
96 # ~~~
97 if expr isa AVarExpr or expr isa AFalseExpr then return
98
99 # Build the superstring to explain the assert
100 var explain_str = new ASuperstringExpr
101
102 # Prepare attribute used by visited nodes
103 self.assert_node = node
104 self.explain_str = explain_str
105 expr.accept_explain_assert self
106
107 # Done! Store the superstring in the assert's node
108 if explain_str.n_exprs.not_empty then
109 node.explain_assert_str = explain_str
110 end
111 end
112
113 # Visited assert node
114 var assert_node: AAssertExpr is noinit
115
116 # Superstring in construction to explain the `assert_node`
117 var explain_str: ASuperstringExpr is noinit
118
119 # Build an `AStringExpr` containing `value`
120 #
121 # Add it to `explain_str` if `auto_add == true`, the default.
122 fun explain_string(value: String, auto_add: nullable Bool): AStringExpr
123 do
124 auto_add = auto_add or else true
125
126 var tk = new TString
127 tk.text = "\"{value}\""
128 var op = new AStringExpr
129 op.n_string = tk
130 op.mtype = string_mtype
131 op.value = value
132 op.location = assert_node.location
133
134 if auto_add then explain_str.n_exprs.add op
135 return op
136 end
137
138 # Add the value of `v_expr` to `explain_str` and protect null values
139 fun explain_expr(v_expr: AExpr)
140 do
141 var mtype = v_expr.mtype
142 if mtype == null then
143 explain_string "<unexpected error>"
144 return
145 end
146
147 # Set the expression value aside
148 var old_parent = v_expr.parent
149 var expr = v_expr.make_var_read
150 if old_parent != null then old_parent.validate
151
152 # Protect nullable types
153 if mtype isa MNullType then
154 explain_string "null"
155 return
156 else if mtype isa MNullableType then
157 var e = new AOrElseExpr
158 e.n_expr = expr
159 e.n_expr2 = explain_string("null", false)
160 e.location = assert_node.location
161 e.mtype = mmodule.object_type
162
163 explain_str.n_exprs.add e
164 return
165 end
166
167 explain_str.n_exprs.add expr
168 end
169
170 # Add all the arguments in `AExprs` to `explain_str`
171 fun explain_args(n_args: AExprs)
172 do
173 var first = true
174 for n_arg in n_args.to_a do
175 if not first then
176 explain_string ", "
177 else first = false
178
179 explain_expr n_arg
180 end
181 end
182 end
183
184 redef class AAssertExpr
185 redef var explain_assert_str = null
186 end
187
188 redef class AExpr
189 # Fill `v` to explain this node if the parent assert fails
190 private fun accept_explain_assert(v: ExplainAssertVisitor)
191 do if mtype != null then v.explain_expr self
192 end
193
194 redef class ABinopExpr
195 redef fun accept_explain_assert(v)
196 do
197 if n_expr.mtype == null or n_expr2.mtype == null then return
198
199 v.explain_expr n_expr
200 v.explain_string " {n_op.text} "
201 v.explain_expr n_expr2
202 end
203 end
204
205 redef class ACallExpr
206 redef fun accept_explain_assert(v)
207 do
208 if n_expr.mtype == null then return
209
210 v.explain_expr n_expr
211 v.explain_string ".{n_qid.n_id.text}"
212
213 if n_args.to_a.not_empty then
214 v.explain_string "("
215 v.explain_args n_args
216 v.explain_string ")"
217 end
218 end
219 end
220
221 redef class ABraExpr
222 redef fun accept_explain_assert(v)
223 do
224 if n_expr.mtype == null then return
225
226 v.explain_expr n_expr
227 v.explain_string "["
228 v.explain_args n_args
229 v.explain_string "]"
230 end
231 end
232
233 redef class AIsaExpr
234 redef fun accept_explain_assert(v)
235 do
236 if n_expr.mtype == null then return
237
238 v.explain_expr n_expr
239 v.explain_string " {n_kwisa.text} "
240 v.explain_string n_type.collect_text
241 end
242 end
243
244 redef class ANotExpr
245 redef fun accept_explain_assert(v)
246 do
247 v.explain_string "{n_kwnot.text} "
248 n_expr.accept_explain_assert v
249 end
250 end
251
252 redef class ABinBoolExpr
253 # Don't explain the conditions using `and`, `or`, etc.
254 redef fun accept_explain_assert(v) do end
255 end