Basically one of the oldest bug of Nit.
The point of adaptive typing is that the static types of variables follows the static types of the assigned values, type tests and comparison to null.
This is a great feature of the language but was buggy because loops where not taken in account (old TODO), until the present PR.
The basic idea is just to track if a type adaptation occurs, if yes replay the whole typing phase.
The immediate drawback is that now typing require 2 passes most of the time, and sometime more.
This PR was a roller-coaster of emotions to develop since a lot a surprise issues were discovered.
* bug in flow that give me hours of despair.
* duplication of error messages; because multiple passes.
* premature warning/error messages; because some error conditions on types are detected at the first pass but a second pass find it is fine in fact... but too late the error message was printed.
* almost work at the first time (modulo the previous issues)
* only two bugs found in the whole code, quite impressive, and one of them is not a real bug but an autocast issue.
The issue about error duplication and premature errors is only workadounded (or fixme) in most places.
But proper solutions will be required in future PR.
close #647 and close #86, then reopen it again, then reclose it.
Pull-Request: #1257
Reviewed-by: Alexis Laferrière <alexis.laf@xymus.net>
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>
Reviewed-by: Alexandre Terrasa <alexandre@moz-code.org>
while frontier_node != self do
path.nodes.unshift(frontier_node)
- frontier_node = frontier_node.best_source.as(not null)
+ frontier_node = frontier_node.best_source
+ assert frontier_node != null
end
return path
problem.backtrack(state, a)
node = node.parent
+ assert node != null
continue
end
do
super
if mtype == null and not is_typed then
- debug "TYPING: untyped expression"
+ #debug "TYPING: untyped expression"
end
end
end
fun stmt(nexpr: nullable AExpr)
do
if nexpr == null then return
+ if nexpr.mtype == null and not nexpr.is_typed then
+ # Untyped expression.
+ # Might mean dead code
+ # So just return
+ return
+ end
var narray = nexpr.comprehension
if narray != null then
# `mtype` is the expected return type, pass null if no specific type is expected.
fun expr(nexpr: AExpr, mtype: nullable MType): RuntimeVariable
do
+ if nexpr.mtype == null then
+ # Untyped expression.
+ # Might mean dead code
+ # so return a placebo result
+ if mtype == null then mtype = compiler.mainmodule.object_type
+ return new_var(mtype)
+ end
var old = self.current_node
self.current_node = nexpr
var res = nexpr.expr(self).as(not null)
fun printflow
do
var file = new FileWriter.open("flow.dot")
- file.write("digraph \{\n")
+ file.write("digraph \{\nnode[shape=box];")
for f in flows do
var s = ""
if f.node isa AExpr then
end
file.write "F{f.object_id} [label=\"{f.object_id}\\n{f.node.location}\\n{f.node.class_name}\\n{f.name}{s}\"];\n"
for p in f.previous do
- file.write "F{p.object_id} -> F{f.object_id};\n"
+ s = ""
+ if f.when_true == p then s = "[label=TRUE, style=dotted]"
+ if f.when_false == p then s = "[label=FALSE, style=dotted]"
+ if f.when_true == p and f.when_false == p then s = "[label=TRUE_FALSE, style=dotted]"
+ file.write "F{p.object_id} -> F{f.object_id}{s};\n"
end
- if f.when_true != f then
- file.write "F{f.object_id} -> F{f.when_true.object_id}[label=TRUE, style=dotted];\n"
- end
- if f.when_false != f then
- file.write "F{f.object_id} -> F{f.when_false.object_id}[label=FALSE,style=dotted];\n"
+ for p in f.loops do
+ file.write "F{p.object_id} -> F{f.object_id}[label=LOOP, style=dashed, constraint=false];\n"
end
end
file.write("\}\n")
var after_block = v.current_flow_context
before_loop.add_loop(after_block)
- v.merge_continues_to(after_block, self.continue_mark)
+ v.merge_continues_to(before_loop, self.continue_mark)
v.current_flow_context = after_expr.when_false
v.merge_breaks(self.break_mark)
var after_block = v.current_flow_context
before_loop.add_loop(after_block)
- v.merge_continues_to(after_block, self.continue_mark)
+ v.merge_continues_to(before_loop, self.continue_mark)
v.make_unreachable_flow
v.merge_breaks(self.break_mark)
var after_block = v.current_flow_context
before_loop.add_loop(after_block)
- v.merge_continues_to(after_block, self.continue_mark)
+ v.merge_continues_to(before_loop, self.continue_mark)
v.make_merge_flow(v.current_flow_context, before_loop)
v.merge_breaks(self.break_mark)
if not mtype2 isa MNullType then return
- if mtype isa MNullType then return
-
# Check of useless null
if not check_can_be_null(anode.n_expr, mtype) then return
- mtype = mtype.as_notnull
+ if mtype isa MNullType then
+ # Because of type adaptation, we cannot just stop here
+ # so return use `null` as a bottom type that will be merged easily (cf) `merge_types`
+ mtype = null
+ else
+ mtype = mtype.as_notnull
+ end
# Check for type adaptation
var variable = anode.n_expr.its_variable
# One is null (mtype2 see above) the other is not null
if anode isa AEqExpr then
- anode.after_flow_context.when_true.set_var(variable, mtype2)
- anode.after_flow_context.when_false.set_var(variable, mtype)
+ anode.after_flow_context.when_true.set_var(self, variable, mtype2)
+ anode.after_flow_context.when_false.set_var(self, variable, mtype)
else if anode isa ANeExpr then
- anode.after_flow_context.when_false.set_var(variable, mtype2)
- anode.after_flow_context.when_true.set_var(variable, mtype)
+ anode.after_flow_context.when_false.set_var(self, variable, mtype2)
+ anode.after_flow_context.when_true.set_var(self, variable, mtype)
else
abort
end
fun get_variable(node: AExpr, variable: Variable): nullable MType
do
+ if not variable.is_adapted then return variable.declared_type
+
var flow = node.after_flow_context
if flow == null then
self.error(node, "No context!")
#node.debug("*** START Collected for {variable}")
var mtypes = flow.collect_types(variable)
#node.debug("**** END Collected for {variable}")
- if mtypes == null or mtypes.length == 0 then
+ if mtypes.length == 0 then
return variable.declared_type
else if mtypes.length == 1 then
return mtypes.first
end
end
+ # Some variables where type-adapted during the visit
+ var dirty = false
+
+ # Some loops had been visited during the visit
+ var has_loop = false
+
fun set_variable(node: AExpr, variable: Variable, mtype: nullable MType)
do
var flow = node.after_flow_context
assert flow != null
- flow.set_var(variable, mtype)
+ flow.set_var(self, variable, mtype)
end
fun merge_types(node: ANode, col: Array[nullable MType]): nullable MType
redef class Variable
# The declared type of the variable
var declared_type: nullable MType
+
+ # Was the variable type-adapted?
+ # This is used to speedup type retrieval while it remains `false`
+ private var is_adapted = false
end
redef class FlowContext
# Store changes of types because of type evolution
private var vars = new HashMap[Variable, nullable MType]
- private var cache = new HashMap[Variable, nullable Array[nullable MType]]
# Adapt the variable to a static type
# Warning1: do not modify vars directly.
# Warning2: sub-flow may have cached a unadapted variable
- private fun set_var(variable: Variable, mtype: nullable MType)
+ private fun set_var(v: TypeVisitor, variable: Variable, mtype: nullable MType)
do
+ if variable.declared_type == mtype and not variable.is_adapted then return
+ if vars.has_key(variable) and vars[variable] == mtype then return
self.vars[variable] = mtype
- self.cache.keys.remove(variable)
+ v.dirty = true
+ variable.is_adapted = true
+ #node.debug "set {variable} to {mtype or else "X"}"
end
- private fun collect_types(variable: Variable): nullable Array[nullable MType]
+ # Look in the flow and previous flow and collect all first reachable type adaptation of a local variable
+ private fun collect_types(variable: Variable): Array[nullable MType]
do
- if cache.has_key(variable) then
- return cache[variable]
- end
- var res: nullable Array[nullable MType] = null
- if vars.has_key(variable) then
- var mtype = vars[variable]
- res = [mtype]
- else if self.previous.is_empty then
- # Root flow
- res = [variable.declared_type]
- else
- for flow in self.previous do
- if flow.is_unreachable then continue
- var r2 = flow.collect_types(variable)
- if r2 == null then continue
- if res == null then
- res = r2.to_a
- else
- for t in r2 do
- if not res.has(t) then res.add(t)
- end
+ #node.debug "flow for {variable}"
+ var res = new Array[nullable MType]
+
+ var todo = [self]
+ var seen = new HashSet[FlowContext]
+ while not todo.is_empty do
+ var f = todo.pop
+ if f.is_unreachable then continue
+ if seen.has(f) then continue
+ seen.add f
+
+ if f.vars.has_key(variable) then
+ # Found something. Collect it and do not process further on this path
+ res.add f.vars[variable]
+ #f.node.debug "process {variable}: got {f.vars[variable] or else "X"}"
+ else
+ todo.add_all f.previous
+ todo.add_all f.loops
+ if f.previous.is_empty then
+ # Root flowcontext mean a parameter or something related
+ res.add variable.declared_type
+ #f.node.debug "root process {variable}: got {variable.declared_type or else "X"}"
end
end
end
- cache[variable] = res
+ #self.node.debug "##### end flow for {variable}: {res.join(" ")}"
return res
end
end
assert variable != null
variable.declared_type = mtype
end
- v.visit_stmt(nblock)
+
+ loop
+ v.dirty = false
+ v.visit_stmt(nblock)
+ if not v.has_loop or not v.dirty then break
+ end
if not nblock.after_flow_context.is_unreachable and msignature.return_mtype != null then
# We reach the end of the function without having a return, it is bad
redef class AWhileExpr
redef fun accept_typing(v)
do
+ v.has_loop = true
v.visit_expr_bool(n_expr)
-
v.visit_stmt(n_block)
self.is_typed = true
end
redef class ALoopExpr
redef fun accept_typing(v)
do
+ v.has_loop = true
v.visit_stmt(n_block)
self.is_typed = true
end
redef fun accept_typing(v)
do
+ v.has_loop = true
var mtype = v.visit_expr(n_expr)
if mtype == null then return
self.do_type_iterator(v, mtype)
v.visit_stmt(n_block)
+
self.mtype = n_block.mtype
self.is_typed = true
end
end
end
if mtype == null then
+ # Ensure monotony for type adaptation on loops
+ if self.element_mtype != null then mtypes.add self.element_mtype
mtype = v.merge_types(self, mtypes)
end
if mtype == null or mtype isa MNullType then
#var from = if orig != null then orig.to_s else "invalid"
#var to = if mtype != null then mtype.to_s else "invalid"
#debug("adapt {variable}: {from} -> {to}")
- self.after_flow_context.when_true.set_var(variable, mtype)
+ self.after_flow_context.when_true.set_var(v, variable, mtype)
end
self.mtype = v.type_bool(self)
return location.as(not null) < other.location.as(not null)
end
+ redef fun ==(other): Bool do
+ if not other isa Message then return false
+ return location == other.location and tag == other.tag and text == other.text
+ end
+
redef fun to_s: String
do
var l = location
fun error(l: nullable Location, s: String): Message
do
var m = new Message(l,null,s)
+ if messages.has(m) then return m
if l != null then l.add_message m
messages.add m
error_count = error_count + 1
if not opt_warning.value.has(tag) and opt_warn.value == 0 then return null
if is_warning_blacklisted(l, tag) then return null
var m = new Message(l, tag, text)
+ if messages.has(m) then return null
if l != null then l.add_message m
messages.add m
warning_count = warning_count + 1
if not opt_warning.value.has(tag) and opt_warn.value <= 1 then return null
if is_warning_blacklisted(l, tag) then return null
var m = new Message(l, tag, text)
+ if messages.has(m) then return null
if l != null then l.add_message m
messages.add m
warning_count = warning_count + 1
--- /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 standard::kernel
+
+fun foo(i: Int): nullable Int do return i
+fun bar(i: Int) do i.output
+
+var i = 0
+var c = 0
+while i < 10 do
+ i += 1
+
+ var b = foo(i)
+ if b == null then
+ continue#alt1#
+ end
+
+ bar(b)
+ c = b
+end
+bar(c)
--- /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 standard::kernel
+
+class A
+ fun foo do 1.output
+end
+
+class B
+ super A
+ fun bar do 2.output
+end
+
+var x: A #alt3# var x
+x = new B
+
+var i = 0
+while i < 3 do
+ i += 1
+ x.bar
+ x = new A
+ x.foo
+ break#alt1#
+end
+x.foo
+#alt2#x.bar
--- /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 standard::kernel
+
+redef class Int
+ fun next: nullable Int do if self < 20 then return self + 1 else return null
+end
+
+var t2: nullable Int = 10
+t2.output
+while t2 != null do
+ t2 = t2.next #alt1# t2 = null
+ while t2 != null do
+ t2.output
+ t2 = t2.next #alt2# t2 = null
+ end
+ #alt3#t2 = t2.next
+end
--- /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 standard::collection::array
+
+var a #1alt1#var a: Array[Object]
+a = [1]#1alt2#
+#1alt2#a = 1
+var i = 0
+while i < 3 do
+ i += 1
+
+ a = [a]
+ a.length.output
+ #alt1#a.first.length.output
+end
+#alt1#a.length.output
--- /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 standard::kernel
+
+class O
+ fun foo: O do
+ 0.output
+ return new B
+ end
+end
+
+class A
+ super O
+ redef fun foo: B do
+ 1.output
+ return new B
+ end
+ fun bar: B do
+ 10.output
+ return new B
+ end
+end
+
+class B
+ super O
+ redef fun foo: C do
+ 2.output
+ return new C
+ end
+ fun bar: C do
+ 20.output
+ return new C
+ end
+end
+
+class C
+ super O
+end
+
+var a: O #alt1# var a
+
+a = new A
+var i = 0
+while i < 4 do
+ a = a.foo #alt2# a = a.bar
+ i += 1
+end
--- /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 standard::kernel
+
+fun foo(i: Int): nullable Int do return i
+fun bar(i: Int) do i.output
+fun baz: nullable Int do return null
+
+bar(0)
+var i = null
+while i == null do
+ i = foo(1) #alt1# exit(1)
+end
+bar(i)
+
+i = foo(2)
+while i != null do
+ bar(i)
+ i = baz
+end
+#alt2#bar(i)
+
+i = null
+loop
+ i = foo(3)
+ if i == null then continue #alt3#
+ bar(i)
+ break
+end
+bar(i)
+
+i = 4
+loop
+ bar(i)
+ i = baz
+ if i != null then
+ bar(i)
+ else
+ break #alt4#
+ end
+end
--- /dev/null
+1
+2
+3
+4
+5
+6
+7
+8
+9
+10
+10
--- /dev/null
+alt/base_adaptive_loop2_alt1.nit:32,2--6: Error: Method 'bar' doesn't exists in A.
--- /dev/null
+alt/base_adaptive_loop2_alt2.nit:38,1--5: Error: Method 'bar' doesn't exists in A.
--- /dev/null
+10
+11
+12
+13
+14
+15
+16
+17
+18
+19
+20
--- /dev/null
+alt/base_adaptive_loop3_alt3.nit:29,7--13: Error: Method 'next' call on 'null'.
--- /dev/null
+alt/base_adaptive_loop_alt1.nit:30,6: Type error: expected Int, got nullable Int
+alt/base_adaptive_loop_alt1.nit:31,6: Type error: expected Int, got nullable Int
--- /dev/null
+1
+1
+1
+1
+1
+1
+1
--- /dev/null
+alt/base_adaptive_loop_array_1alt2_alt1.nit:26,2--15: Error: Method 'length' doesn't exists in Int.
+alt/base_adaptive_loop_array_1alt2_alt1.nit:26,2--15: Error: Method 'length' doesn't exists in nullable Object.
+alt/base_adaptive_loop_array_1alt2_alt1.nit:28,1--8: Error: Method 'length' doesn't exists in nullable Object.
--- /dev/null
+alt/base_adaptive_loop_array_alt1.nit:26,2--15: Error: Method 'length' doesn't exists in nullable Object.
+alt/base_adaptive_loop_array_alt1.nit:28,1--8: Error: Method 'length' doesn't exists in nullable Object.
--- /dev/null
+1
+2
+0
+2
--- /dev/null
+alt/base_adaptive_loop_call_alt1.nit:57,6--10: Error: Method 'foo' doesn't exists in nullable Object.
--- /dev/null
+alt/base_adaptive_loop_call_alt2.nit:57,6--10: Error: Method 'bar' doesn't exists in O.
--- /dev/null
+0
+1
+2
+3
+3
+4
--- /dev/null
+alt/base_adaptive_loop_null_alt2.nit:33,5: Type error: expected Int, got null
--- /dev/null
+alt/base_adaptive_loop_null_alt3.nit:39,6: Type error: expected Int, got nullable Int
+alt/base_adaptive_loop_null_alt3.nit:42,5: Type error: expected Int, got nullable Int
--- /dev/null
+alt/base_adaptive_loop_null_alt4.nit:46,6: Type error: expected Int, got nullable Int
alt/base_no_object_alt1.nit:11,9--13: Error: Method 'init' doesn't exists in A.
alt/base_no_object_alt1.nit:13,4--12: Type Error: missing primitive class `Bool'.
-alt/base_no_object_alt1.nit:13,4--12: Type Error: missing primitive class `Bool'.
alt/base_notnull_1alt1_alt3.nit:23,7: Type error: expected Object, got null
alt/base_notnull_1alt1_alt3.nit:24,7: Type error: expected Object, got null
-alt/base_notnull_1alt1_alt3.nit:26,8: Type error: expected Object, got null
alt/base_notnull_1alt1_alt3.nit:27,12: Type error: expected Object, got null
-alt/base_notnull_1alt1_alt3.nit:29,8: Type error: expected Object, got null
alt/base_notnull_1alt1_alt3.nit:30,12: Type error: expected Object, got null
alt/base_notnull_1alt1_alt3.nit:31,7--20: Type error: as(not null) on null
alt/base_notnull_1alt1_alt3.nit:32,7--20: Type error: as(not null) on null
alt/base_notnull_alt3.nit:23,7: Type error: expected Object, got null
alt/base_notnull_alt3.nit:24,7: Type error: expected Object, got null
-alt/base_notnull_alt3.nit:26,8: Type error: expected Object, got null
alt/base_notnull_alt3.nit:27,12: Type error: expected Object, got null
-alt/base_notnull_alt3.nit:29,8: Type error: expected Object, got null
alt/base_notnull_alt3.nit:30,12: Type error: expected Object, got null
alt/base_notnull_alt3.nit:31,7--20: Type error: as(not null) on null
alt/base_notnull_alt3.nit:32,7--20: Type error: as(not null) on null
-1
-2
-3
-4
-4
-5
-6
-6
-7
-7
-8
-8
+alt/base_isa_cast4_alt5.nit:89,1--8: Error: Method 'foo' doesn't exists in A.