[m-rev.] for review: detect cycles in typeclass hierarchy

Mark Brown mark at cs.mu.OZ.AU
Wed Jan 19 05:42:48 AEDT 2005


This is for review by anyone.

Cheers,
Mark.

Estimated hours taken: 3
Branches: main

Check for cycles in the typeclass hierarchy.  This fixes a long-standing
bug whereby the compiler could go into an infinite loop in the polymorphism
stage if cycles were present.

compiler/check_typeclass.m:
	Add a pass to check through all visible typeclass declarations and
	report when a cycle is found.

	The interface to this module has been made more general, to reflect
	the fact that it checks the superclass relation as well as instance
	declarations.

compiler/mercury_compile.m:
	Use the new interface to the check_typeclass module.

compiler/error_util.m:
	Add a new format_component for sym_name_and_arity.

BUGS:
	Remove the bug report from this file.

tests/invalid/Mmakefile:
test/invalid/cyclic_typeclass.err_exp:
	Enable the cyclic_typeclass test, since we now pass it, and add an
	expected output file.  Also add a couple of new tests.

test/invalid/cyclic_typeclass_2.m:
test/invalid/cyclic_typeclass_2.err_exp:
test/invalid/cyclic_typeclass_3.m:
test/invalid/cyclic_typeclass_3.err_exp:
	New test cases.

Index: BUGS
===================================================================
RCS file: /home/mercury1/repository/mercury/BUGS,v
retrieving revision 1.19
diff -u -r1.19 BUGS
--- BUGS	5 Nov 2003 08:08:24 -0000	1.19
+++ BUGS	18 Jan 2005 18:33:54 -0000
@@ -152,18 +152,3 @@
 	list__map_foldl(MakeIndex, Args0, _, 0, _).
 
 -----------------------------------------------------------------------------
-
-Date: Wed, 1 Dec 1999 22:52:57 +1100
-Subject: compiler infinite loop for cyclic type classes
-
-According to the language reference manual:
-
-|  Typeclass constraints on type class declarations gives rise to a
-|  superclass relation.  This relation must be acyclic.  That is, it is an
-|  error if a type class is its own (direct or indirect) superclass.
-
-But if you try to compile modules containing cyclic typeclasses,
-the compiler goes into an infinite loop and eventually gets a
-stack overflow, rather than reporting a proper error message.
-
------------------------------------------------------------------------------
Index: compiler/check_typeclass.m
===================================================================
RCS file: /home/mercury1/repository/mercury/compiler/check_typeclass.m,v
retrieving revision 1.59
diff -u -r1.59 check_typeclass.m
--- compiler/check_typeclass.m	17 Jan 2005 05:01:33 -0000	1.59
+++ compiler/check_typeclass.m	18 Jan 2005 18:33:58 -0000
@@ -37,6 +37,8 @@
 % In addition, this pass checks that all superclass constraints are satisfied
 % by the instance declaration.
 %
+% This pass also checks for cycles in the typeclass hierarchy.
+%
 % This pass fills in the super class proofs and instance method pred/proc ids
 % in the instance table of the HLDS.
 %
@@ -53,7 +55,7 @@
 
 :- import_module bool, io.
 
-:- pred check_typeclass__check_instance_decls(qual_info::in, qual_info::out,
+:- pred check_typeclass__check_typeclasses(qual_info::in, qual_info::out,
 	module_info::in, module_info::out, bool::out, io::di, io::uo) is det.
 
 :- implementation.
@@ -76,12 +78,24 @@
 :- import_module parse_tree__prog_util.
 
 :- import_module int, string.
-:- import_module list, assoc_list, map, set, term, varset.
+:- import_module list, assoc_list, map, set, svset, term, varset.
 :- import_module std_util, require.
 
+check_typeclass__check_typeclasses(!QualInfo, !ModuleInfo, FoundError, !IO) :-
+	check_typeclass__check_instance_decls(!QualInfo, !ModuleInfo,
+		FoundInstanceError, !IO),
+	module_info_classes(!.ModuleInfo, ClassTable),
+	check_for_cyclic_classes(ClassTable, FoundCycleError, !IO),
+	FoundError = bool.or(FoundInstanceError, FoundCycleError).
+
+%---------------------------------------------------------------------------%
+
 :- type error_message == pair(prog_context, list(format_component)).
 :- type error_messages == list(error_message).
 
+:- pred check_typeclass__check_instance_decls(qual_info::in, qual_info::out,
+	module_info::in, module_info::out, bool::out, io::di, io::uo) is det.
+
 check_typeclass__check_instance_decls(!QualInfo, !ModuleInfo, FoundError,
 		!IO) :-
 	module_info_classes(!.ModuleInfo, ClassTable),
@@ -875,3 +889,111 @@
 	string__append_list([", `", String0, "'", String1], String).
 
 %---------------------------------------------------------------------------%
+
+:- pred check_for_cyclic_classes(class_table::in, bool::out, io::di, io::uo)
+	is det.
+
+check_for_cyclic_classes(ClassTable, Errors, !IO) :-
+	ClassIds = map__keys(ClassTable),
+	foldl2(find_cycles(ClassTable, []), ClassIds, set.init, _, [], Cycles),
+	(
+		Cycles = [],
+		Errors = no
+	;
+		Cycles = [_ | _],
+		Errors = yes,
+		foldl(report_cyclic_classes(ClassTable), Cycles, !IO)
+	).
+
+:- type class_path == list(class_id).
+
+	% find_cycles(ClassTable, Path, ClassId, !Visited, !Cycles)
+	%
+	% Perform a depth first traversal of the class hierarchy, starting
+	% from ClassId.  Path contains a list of nodes joining the current
+	% node to the root.  When we reach a node that has already been
+	% visited, check whether there is a cycle in the Path.
+	%
+:- pred find_cycles(class_table::in, class_path::in, class_id::in,
+	set(class_id)::in, set(class_id)::out,
+	list(class_path)::in, list(class_path)::out) is det.
+
+find_cycles(ClassTable, Path, ClassId, !Visited, !Cycles) :-
+	(
+		set.member(ClassId, !.Visited)
+	->
+		(
+			find_cycle(ClassId, Path, [ClassId], Cycle)
+		->
+			!:Cycles = [Cycle | !.Cycles]
+		;
+			true
+		)
+	;
+		svset.insert(ClassId, !Visited),
+		ClassIds = get_superclass_ids(ClassTable, ClassId),
+		foldl2(find_cycles(ClassTable, [ClassId | Path]), ClassIds,
+			!Visited, !Cycles)
+	).
+
+	% find_cycle(ClassId, PathRemaining, PathSoFar, Cycle)
+	%
+	% Check if ClassId is present in PathRemaining, and if so then make
+	% a cycle out of the front part of the path up to the point where
+	% the ClassId is found.  The part of the path checked so far is
+	% accumulated in PathSoFar.
+	%
+:- pred find_cycle(class_id::in, class_path::in, class_path::in,
+	class_path::out) is semidet.
+
+find_cycle(ClassId, [Head | Tail], Path0, Cycle) :-
+	Path = [Head | Path0],
+	(
+		ClassId = Head
+	->
+		Cycle = Path
+	;
+		find_cycle(ClassId, Tail, Path, Cycle)
+	).
+
+:- func get_superclass_ids(class_table, class_id) = list(class_id).
+
+get_superclass_ids(ClassTable, ClassId) = SuperclassIds :-
+	ClassDefn = map.lookup(ClassTable, ClassId),
+	SuperclassIds = list.map(get_constraint_id, ClassDefn ^ class_supers).
+
+:- func get_constraint_id(class_constraint) = class_id.
+
+get_constraint_id(constraint(Name, Args)) = class_id(Name, length(Args)).
+
+	% Report an error using the format
+	%
+	%	module.m:NNN: Error: cyclic superclass relation detected:
+	%	module.m:NNN:   `foo/N' <= `bar/N' <= `baz/N' <= `foo/N'
+	%
+:- pred report_cyclic_classes(class_table::in, class_path::in, io::di, io::uo)
+	is det.
+
+report_cyclic_classes(ClassTable, ClassPath, !IO) :-
+	(
+		ClassPath = [],
+		error("report_cyclic_classes: empty cycle found")
+	;
+		ClassPath = [ClassId | Tail],
+		Context = map.lookup(ClassTable, ClassId) ^ class_context,
+		ClassId = class_id(Name, Arity),
+		RevPieces0 = [
+			sym_name_and_arity(Name/Arity),
+			words("Error: cyclic superclass relation detected:")
+		],
+		RevPieces1 = foldl(add_path_element, Tail, RevPieces0),
+		Pieces = list.reverse(RevPieces1),
+		write_error_pieces(Context, 0, Pieces, !IO)
+	).
+
+:- func add_path_element(class_id, list(format_component))
+	= list(format_component).
+
+add_path_element(class_id(Name, Arity), RevPieces0) =
+	[sym_name_and_arity(Name/Arity), words("<=") | RevPieces0].
+
Index: compiler/error_util.m
===================================================================
RCS file: /home/mercury1/repository/mercury/compiler/error_util.m,v
retrieving revision 1.30
diff -u -r1.30 error_util.m
--- compiler/error_util.m	17 Jan 2005 05:01:33 -0000	1.30
+++ compiler/error_util.m	18 Jan 2005 18:34:01 -0000
@@ -57,6 +57,11 @@
 	;	sym_name(sym_name)
 				% The output should contain the string form of
 				% the sym_name, surrounded by `' quotes.
+	
+	;	sym_name_and_arity(sym_name_and_arity)
+				% The output should contain the string form of
+				% the sym_name, followed by '/' and the arity,
+				% all surrounded by `' quotes.
 
 	;	nl.		% Insert a line break if there has been text
 				% output since the last line break.
@@ -334,6 +339,14 @@
 			Str = Word ++ " " ++ TailStr
 		)
 	;
+		Component = sym_name_and_arity(SymNameAndArity),
+		Word = sym_name_and_arity_to_word(SymNameAndArity),
+		( TailStr = "" ->
+			Str = Word
+		;
+			Str = Word ++ " " ++ TailStr
+		)
+	;
 		Component = nl,
 		Str = "\n" ++ TailStr
 	).
@@ -370,6 +383,11 @@
 		RevWords1 = [word(sym_name_to_word(SymName)) | RevWords0],
 		Paras1 = Paras0
 	;
+		Component = sym_name_and_arity(SymNameAndArity),
+		Word = sym_name_and_arity_to_word(SymNameAndArity),
+		RevWords1 = [word(Word) | RevWords0],
+		Paras1 = Paras0
+	;
 		Component = nl,
 		Strings = rev_words_to_strings(RevWords0),
 		Paras1 = [Strings | Paras0],
@@ -410,6 +428,11 @@
 sym_name_to_word(SymName) = "`" ++ SymStr ++ "'" :-
 	sym_name_to_string(SymName, SymStr).
 
+:- func sym_name_and_arity_to_word(sym_name_and_arity) = string.
+
+sym_name_and_arity_to_word(SymNameAndArity) = "`" ++ SymStr ++ "'" :-
+	sym_name_and_arity_to_string(SymNameAndArity, SymStr).
+
 :- pred break_into_words(string::in, list(word)::in, list(word)::out) is det.
 
 break_into_words(String, Words0, Words) :-
@@ -560,6 +583,10 @@
 	;
 		Piece0 = sym_name(SymName),
 		String = sym_name_to_word(SymName),
+		Piece = fixed(string__append(String, char_to_string(Punc)))
+	;
+		Piece0 = sym_name_and_arity(SymNameAndArity),
+		String = sym_name_and_arity_to_word(SymNameAndArity),
 		Piece = fixed(string__append(String, char_to_string(Punc)))
 	;
 		Piece0 = nl,
Index: compiler/mercury_compile.m
===================================================================
RCS file: /home/mercury1/repository/mercury/compiler/mercury_compile.m,v
retrieving revision 1.318
diff -u -r1.318 mercury_compile.m
--- compiler/mercury_compile.m	23 Dec 2004 06:49:16 -0000	1.318
+++ compiler/mercury_compile.m	18 Jan 2005 18:34:11 -0000
@@ -1957,8 +1957,8 @@
 	globals__lookup_bool_option(Globals, verbose, Verbose),
 	globals__lookup_bool_option(Globals, statistics, Stats),
 	maybe_write_string(Verbose,
-		"% Checking typeclass instances...\n", !IO),
-	check_typeclass__check_instance_decls(QualInfo0, QualInfo, !HLDS,
+		"% Checking typeclasses...\n", !IO),
+	check_typeclass__check_typeclasses(QualInfo0, QualInfo, !HLDS,
 		FoundTypeclassError, !IO),
 	mercury_compile__maybe_dump_hlds(!.HLDS, 5, "typeclass", !IO),
 	make_hlds__set_module_recompilation_info(QualInfo, !HLDS),
Index: tests/invalid/Mmakefile
===================================================================
RCS file: /home/mercury1/repository/tests/invalid/Mmakefile,v
retrieving revision 1.156
diff -u -r1.156 Mmakefile
--- tests/invalid/Mmakefile	6 Jan 2005 04:30:55 -0000	1.156
+++ tests/invalid/Mmakefile	18 Jan 2005 18:34:28 -0000
@@ -56,6 +56,9 @@
 	conflicting_tabling_pragmas \
 	constrained_poly_insts \
 	constructor_warning \
+	cyclic_typeclass \
+	cyclic_typeclass_2 \
+	cyclic_typeclass_3 \
 	det_errors \
 	duplicate_modes \
 	duplicate_module_test \
@@ -188,7 +191,6 @@
 #	typeclass_test_8 (minor formatting error in the output --
 #			the type class name should be in quotes)
 #	typeclass_mode_{2,3,4} (compiler calls error/1)
-#	cyclic_typeclass (compiler goes into an infinite loop)
 #	ho_default_func_4 (due to a bug in the mode-checker ---
 #			see XXX comment in inst_match:inst_matches_final_3)
 #	inst_matches_final_bug (due to same bug as ho_default_func_4)
Index: tests/invalid/cyclic_typeclass.err_exp
===================================================================
RCS file: tests/invalid/cyclic_typeclass.err_exp
diff -N tests/invalid/cyclic_typeclass.err_exp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/invalid/cyclic_typeclass.err_exp	18 Jan 2005 18:34:28 -0000
@@ -0,0 +1,4 @@
+cyclic_typeclass.m:030: Error: cyclic superclass relation detected:
+cyclic_typeclass.m:030:   `cyclic_typeclass.bar/1' <= `cyclic_typeclass.foo/1'
+cyclic_typeclass.m:030:   <= `cyclic_typeclass.bar/1'
+For more information, try recompiling with `-E'.
Index: tests/invalid/cyclic_typeclass_2.err_exp
===================================================================
RCS file: tests/invalid/cyclic_typeclass_2.err_exp
diff -N tests/invalid/cyclic_typeclass_2.err_exp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/invalid/cyclic_typeclass_2.err_exp	18 Jan 2005 18:34:28 -0000
@@ -0,0 +1,5 @@
+cyclic_typeclass_2.m:014: Error: cyclic superclass relation detected:
+cyclic_typeclass_2.m:014:   `cyclic_typeclass_2.bar/1' <=
+cyclic_typeclass_2.m:014:   `cyclic_typeclass_2.foo/1' <=
+cyclic_typeclass_2.m:014:   `cyclic_typeclass_2.bar/1'
+For more information, try recompiling with `-E'.
Index: tests/invalid/cyclic_typeclass_2.m
===================================================================
RCS file: tests/invalid/cyclic_typeclass_2.m
diff -N tests/invalid/cyclic_typeclass_2.m
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/invalid/cyclic_typeclass_2.m	18 Jan 2005 18:34:28 -0000
@@ -0,0 +1,17 @@
+:- module cyclic_typeclass_2.
+
+% This test is a cut down version of cyclic_typeclass.  This one doesn't
+% cause the compiler to go into an infinite loop, but it still contains
+% an error that goes unreported, and may cause an infinite loop when
+% compiling other modules that import it.
+
+:- interface.
+
+:- typeclass foo(A) <= bar(A) where [
+	func foo(A) = int
+].
+
+:- typeclass bar(A) <= foo(A) where [
+	func bar(A) = int
+].
+
Index: tests/invalid/cyclic_typeclass_3.err_exp
===================================================================
RCS file: tests/invalid/cyclic_typeclass_3.err_exp
diff -N tests/invalid/cyclic_typeclass_3.err_exp
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/invalid/cyclic_typeclass_3.err_exp	18 Jan 2005 18:34:28 -0000
@@ -0,0 +1,11 @@
+cyclic_typeclass_3.m:014: Error: cyclic superclass relation detected:
+cyclic_typeclass_3.m:014:   `cyclic_typeclass_3.c/1' <=
+cyclic_typeclass_3.m:014:   `cyclic_typeclass_3.e/1' <=
+cyclic_typeclass_3.m:014:   `cyclic_typeclass_3.i/1' <=
+cyclic_typeclass_3.m:014:   `cyclic_typeclass_3.c/1'
+cyclic_typeclass_3.m:012: Error: cyclic superclass relation detected:
+cyclic_typeclass_3.m:012:   `cyclic_typeclass_3.a/1' <=
+cyclic_typeclass_3.m:012:   `cyclic_typeclass_3.b/1' <=
+cyclic_typeclass_3.m:012:   `cyclic_typeclass_3.g/1' <=
+cyclic_typeclass_3.m:012:   `cyclic_typeclass_3.a/1'
+For more information, try recompiling with `-E'.
Index: tests/invalid/cyclic_typeclass_3.m
===================================================================
RCS file: tests/invalid/cyclic_typeclass_3.m
diff -N tests/invalid/cyclic_typeclass_3.m
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ tests/invalid/cyclic_typeclass_3.m	18 Jan 2005 18:34:28 -0000
@@ -0,0 +1,21 @@
+:- module cyclic_typeclass_3.
+:- interface.
+
+% The cycles are:
+%	`a/1' <= `b/1' <= `g/1' <= `a/1'
+%	`a/1' <= `c/1' <= `e/1' <= `g/1' <= `a/1'
+%	`c/1' <= `e/1' <= `i/1' <= `c/1'
+%
+% The second of these is not reported, however, since a cycle for `a/1'
+% will have already been detected and reported.
+
+:- typeclass a(T) <= (b(T), c(T))	where [].
+:- typeclass b(T) <= g(T)		where [].
+:- typeclass c(T) <= (d(T), e(T), f(T))	where [].
+:- typeclass d(T) 			where [].
+:- typeclass e(T) <= (g(T), h(T), i(T))	where [].
+:- typeclass f(T) 			where [].
+:- typeclass g(T) <= a(T)		where [].
+:- typeclass h(T) <= f(T)		where [].
+:- typeclass i(T) <= c(T)		where [].
+
--------------------------------------------------------------------------
mercury-reviews mailing list
post:  mercury-reviews at cs.mu.oz.au
administrative address: owner-mercury-reviews at cs.mu.oz.au
unsubscribe: Address: mercury-reviews-request at cs.mu.oz.au Message: unsubscribe
subscribe:   Address: mercury-reviews-request at cs.mu.oz.au Message: subscribe
--------------------------------------------------------------------------



More information about the reviews mailing list