Browse Source

[analyzer] rework side-effect handling

closes #5477
closes #5520
closes #5544
closes #5545
closes #5327
Simon Krajewski 9 years ago
parent
commit
0558013888

+ 8 - 3
src/generators/codegen.ml

@@ -441,9 +441,14 @@ module AbstractCast = struct
 				end
 			| TCall(e1, el) ->
 				begin try
-					let rec find_abstract e = match follow e.etype,e.eexpr with
+					let rec find_abstract e t = match follow t,e.eexpr with
 						| TAbstract(a,pl),_ when Meta.has Meta.MultiType a.a_meta -> a,pl,e
-						| _,TCast(e1,None) -> find_abstract e1
+						| _,TCast(e1,None) -> find_abstract e1 e1.etype
+						| _,TLocal {v_extra = Some(_,Some e')} ->
+							begin match follow e'.etype with
+							| TAbstract(a,pl) when Meta.has Meta.MultiType a.a_meta -> a,pl,mk (TCast(e,None)) e'.etype e.epos
+							| _ -> raise Not_found
+							end
 						| _ -> raise Not_found
 					in
 					let rec find_field e1 =
@@ -451,7 +456,7 @@ module AbstractCast = struct
 						| TCast(e2,None) ->
 							{e1 with eexpr = TCast(find_field e2,None)}
 						| TField(e2,fa) ->
-							let a,pl,e2 = find_abstract e2 in
+							let a,pl,e2 = find_abstract e2 e2.etype in
 							let m = Abstract.get_underlying_type a pl in
 							let fname = field_name fa in
 							let el = List.map (loop ctx) el in

+ 1 - 1
src/optimization/analyzerConfig.ml

@@ -86,7 +86,7 @@ let get_base_config com =
 		copy_propagation = not (Common.raw_defined com "analyzer-no-copy-propagation");
 		code_motion = Common.raw_defined com "analyzer-code-motion";
 		local_dce = not (Common.raw_defined com "analyzer-no-local-dce");
-		fusion = not (Common.raw_defined com "analyzer-no-fusion") && (match com.platform with Flash | Java -> false | _ -> true);
+		fusion = not (Common.raw_defined com "analyzer-no-fusion") && (match com.platform with Flash | Java | Cs -> false | _ -> true);
 		purity_inference = not (Common.raw_defined com "analyzer-no-purity-inference");
 		debug_kind = DebugNone;
 		detail_times = Common.raw_defined com "analyzer-times";

+ 290 - 100
src/optimization/analyzerTexpr.ml

@@ -102,7 +102,10 @@ let target_handles_unops com = match com.platform with
 	| _ -> true
 
 let target_handles_assign_ops com = match com.platform with
-	| Lua -> false
+	(* Technically PHP can handle assign ops, but unfortunately x += y is not always
+	   equivalent to x = x + y in case y has side-effects. *)
+	| Lua | Php -> false
+	| Cpp when not (Common.defined com Define.Cppia) -> false
 	| _ -> true
 
 let rec can_be_used_as_value com e =
@@ -127,10 +130,6 @@ let rec can_be_used_as_value com e =
 	with Exit ->
 		false
 
-let has_pure_meta meta = Meta.has Meta.Pure meta
-
-let is_pure c cf = has_pure_meta c.cl_meta || has_pure_meta cf.cf_meta
-
 let wrap_meta s e =
 	mk (TMeta((Meta.Custom s,[],e.epos),e)) e.etype e.epos
 
@@ -308,46 +307,149 @@ module VarLazifier = struct
 		snd (loop PMap.empty e)
 end
 
-module Fusion = struct
+module InterferenceReport = struct
+	type interference_report = {
+		ir_var_reads : (int,bool) Hashtbl.t;
+		ir_var_writes : (int,bool) Hashtbl.t;
+		ir_field_reads : (string,bool) Hashtbl.t;
+		ir_field_writes : (string,bool) Hashtbl.t;
+		mutable ir_state_read : bool;
+		mutable ir_state_write : bool;
+	}
 
-	open AnalyzerConfig
+	let create () = {
+		ir_var_reads = Hashtbl.create 0;
+		ir_var_writes = Hashtbl.create 0;
+		ir_field_reads = Hashtbl.create 0;
+		ir_field_writes = Hashtbl.create 0;
+		ir_state_read = false;
+		ir_state_write = false;
+	}
 
-	let get_interference_kind e =
-		let vars = ref [] in
-		let has_side_effect = ref false in
+	let set_var_read ir v = Hashtbl.replace ir.ir_var_reads v.v_id true
+	let set_var_write ir v = Hashtbl.replace ir.ir_var_writes v.v_id true
+	let set_field_read ir s = Hashtbl.replace ir.ir_field_reads s true
+	let set_field_write ir s = Hashtbl.replace ir.ir_field_writes s true
+	let set_state_read ir = ir.ir_state_read <- true
+	let set_state_write ir = ir.ir_state_write <- true
+
+	let has_var_read ir v = Hashtbl.mem ir.ir_var_reads v.v_id
+	let has_var_write ir v = Hashtbl.mem ir.ir_var_writes v.v_id
+	let has_field_read ir s = Hashtbl.mem ir.ir_field_reads s
+	let has_field_write ir s = Hashtbl.mem ir.ir_field_writes s
+	let has_state_read ir = ir.ir_state_read
+	let has_state_write ir = ir.ir_state_write
+	let has_any_field_read ir = Hashtbl.length ir.ir_field_reads > 0
+	let has_any_field_write ir = Hashtbl.length ir.ir_field_writes > 0
+
+	let from_texpr e =
+		let ir = create () in
 		let rec loop e = match e.eexpr with
-			| TMeta((Meta.Pure,_,_),_) ->
-				()
-			| TUnop((Increment | Decrement),_,{eexpr = TLocal v}) ->
-				vars := v :: !vars
-			| TBinop((OpAssign | OpAssignOp _),{eexpr = TLocal v},e2) ->
-				vars := v :: !vars;
+			(* vars *)
+			| TLocal v ->
+				set_var_read ir v
+			| TBinop(OpAssign,{eexpr = TLocal v},e2) ->
+				set_var_write ir v;
 				loop e2
-			| TBinop((OpAssign | OpAssignOp _),e1,e2) ->
-				has_side_effect := true;
+			| TBinop(OpAssignOp _,{eexpr = TLocal v},e2) ->
+				set_var_read ir v;
+				set_var_write ir v;
+				loop e2
+			| TUnop((Increment | Decrement),_,{eexpr = TLocal v}) ->
+				set_var_read ir v;
+				set_var_write ir v;
+			(* fields *)
+			| TField(e1,fa) ->
+				loop e1;
+				if not (Optimizer.is_read_only_field_access fa) then set_field_read ir (field_name fa);
+			| TBinop(OpAssign,{eexpr = TField(e1,fa)},e2) ->
+				set_field_write ir (field_name fa);
 				loop e1;
 				loop e2;
-			| TUnop((Increment | Decrement),_,e1) ->
-				has_side_effect := true;
+			| TBinop(OpAssignOp _,{eexpr = TField(e1,fa)},e2) ->
+				let name = field_name fa in
+				set_field_read ir name;
+				set_field_write ir name;
+				loop e1;
+				loop e2;
+			| TUnop((Increment | Decrement),_,{eexpr = TField(e1,fa)}) ->
+				let name = field_name fa in
+				set_field_read ir name;
+				set_field_write ir name;
 				loop e1
+			(* array *)
+			| TArray(e1,e2) ->
+				set_state_read ir;
+				loop e1;
+				loop e2;
+			| TBinop(OpAssign,{eexpr = TArray(e1,e2)},e3) ->
+				set_state_write ir;
+				loop e1;
+				loop e2;
+				loop e3;
+			| TBinop(OpAssignOp _,{eexpr = TArray(e1,e2)},e3) ->
+				set_state_read ir;
+				set_state_write ir;
+				loop e1;
+				loop e2;
+				loop e3;
+			| TUnop((Increment | Decrement),_,{eexpr = TArray(e1,e2)}) ->
+				set_state_read ir;
+				set_state_write ir;
+				loop e1;
+				loop e2;
+			(* state *)
 			| TCall({eexpr = TLocal v},el) when not (is_unbound_call_that_might_have_side_effects v el) ->
 				List.iter loop el
-			| TCall({eexpr = TField(_,FStatic(c,cf))},el) when is_pure c cf ->
-				List.iter loop el
-			| TNew(c,_,el) when (match c.cl_constructor with Some cf when is_pure c cf -> true | _ -> false) ->
+			| TNew(c,_,el) when (match c.cl_constructor with Some cf when Optimizer.is_pure c cf -> true | _ -> false) ->
+				set_state_read ir;
 				List.iter loop el;
 			| TCall(e1,el) ->
-				has_side_effect := true;
+				set_state_read ir;
+				if Optimizer.has_side_effect e then set_state_write ir;
 				loop e1;
 				List.iter loop el
 			| TNew(_,_,el) ->
-				has_side_effect := true;
-				List.iter loop el;
+				set_state_read ir;
+				set_state_write ir;
+				List.iter loop el
+			| TBinop(OpAssign,e1,e2) ->
+				set_state_write ir;
+				loop e1;
+				loop e2;
+			| TBinop(OpAssignOp _,e1,e2) ->
+				set_state_read ir;
+				set_state_write ir;
+				loop e1;
+				loop e2;
+			| TUnop((Increment | Decrement),_,e1) ->
+				set_state_read ir;
+				set_state_write ir;
+				loop e1
 			| _ ->
 				Type.iter loop e
 		in
 		loop e;
-		!has_side_effect,!vars
+		ir
+
+	let to_string ir =
+		let s_hashtbl f h =
+			String.concat ", " (Hashtbl.fold (fun k _ acc -> (f k) :: acc) h [])
+		in
+		Type.Printer.s_record_fields "" [
+			"ir_var_reads",s_hashtbl string_of_int ir.ir_var_reads;
+			"ir_var_writes",s_hashtbl string_of_int ir.ir_var_writes;
+			"ir_field_reads",s_hashtbl (fun x -> x) ir.ir_field_reads;
+			"ir_field_writes",s_hashtbl (fun x -> x) ir.ir_field_writes;
+			"ir_state_read",string_of_bool ir.ir_state_read;
+			"ir_state_write",string_of_bool ir.ir_state_write;
+		]
+	end
+
+
+module Fusion = struct
+	open AnalyzerConfig
+	open InterferenceReport
 
 	let apply com config e =
 		let rec block_element acc el = match el with
@@ -412,15 +514,47 @@ module Fusion = struct
 			        can_be_used_as_value com e &&
 			        (Meta.has Meta.CompilerGenerated v.v_meta || config.optimize && config.fusion && config.user_var_fusion && v.v_extra = None)
 			in
-(* 			let st = s_type (print_context()) in
+ 			(*let st = s_type (print_context()) in
 			if e.epos.pfile = "src/Main.hx" then
 				print_endline (Printf.sprintf "%s(%s) -> %s: #uses=%i && #writes=%i && used_as_value=%b && (compiler-generated=%b || optimize=%b && fusion=%b && user_var_fusion=%b && type_change_ok=%b && v_extra=%b) -> %b"
 					v.v_name (st v.v_type) (st e.etype)
 					(get_num_uses v) (get_num_writes v) (can_be_used_as_value com e)
 					(Meta.has Meta.CompilerGenerated v.v_meta) config.optimize config.fusion
-					config.user_var_fusion (type_change_ok com v.v_type e.etype) (v.v_extra = None) b); *)
+					config.user_var_fusion (type_change_ok com v.v_type e.etype) (v.v_extra = None) b);*)
 			b
 		in
+		let is_assign_op = function
+			| OpAdd
+			| OpMult
+			| OpDiv
+			| OpSub
+			| OpAnd
+			| OpOr
+			| OpXor
+			| OpShl
+			| OpShr
+			| OpUShr
+			| OpMod ->
+				true
+			| OpAssign
+			| OpEq
+			| OpNotEq
+			| OpGt
+			| OpGte
+			| OpLt
+			| OpLte
+			| OpBoolAnd
+			| OpBoolOr
+			| OpAssignOp _
+			| OpInterval
+			| OpArrow ->
+				false
+		in
+		let use_assign_op op e1 e2 =
+			is_assign_op op && target_handles_assign_ops com && Texpr.equal e1 e2 && not (Optimizer.has_side_effect e1) && match com.platform with
+				| Cs when is_null e1.etype || is_null e2.etype -> false
+				| _ -> true
+		in
 		let rec fuse acc el = match el with
 			| ({eexpr = TVar(v1,None)} as e1) :: {eexpr = TBinop(OpAssign,{eexpr = TLocal v2},e2)} :: el when v1 == v2 ->
 				changed := true;
@@ -446,102 +580,146 @@ module Fusion = struct
 				with Exit ->
 					fuse (e1 :: acc) (e2 :: el)
 				end
-			| ({eexpr = TVar(v1,Some e1)} as ev) :: e2 :: el when can_be_fused v1 e1 ->
+			| ({eexpr = TVar(v1,Some e1)} as ev) :: el when can_be_fused v1 e1 ->
 				let found = ref false in
-				let affected = ref false in
-				let ik1 = get_interference_kind e1 in
-				let check_interference e2 =
-					let check (has_side_effect,modified_vars) e2 =
-						if has_side_effect then begin
-							let rec loop e = match e.eexpr with
-								| TMeta((Meta.Pure,_,_),_) ->
-									()
-								| TArray _ ->
-									raise Exit
-								| TField _ when Optimizer.is_affected_type e.etype ->
-									raise Exit
-								| TCall({eexpr = TField(_,FStatic(c,cf))},el) when is_pure c cf ->
-									List.iter loop el
-								| TNew(c,_,el) when (match c.cl_constructor with Some cf when is_pure c cf -> true | _ -> false) ->
-									List.iter loop el
-								| TCall _ | TNew _ | TBinop((OpAssign | OpAssignOp _),_,_) | TUnop((Increment | Decrement),_,_) ->
-									raise Exit
-								| _ ->
-									Type.iter loop e
-							in
-							loop e2
-						end;
-						if modified_vars <> [] then begin
-							let rec loop e = match e.eexpr with
-								| TLocal v when List.memq v modified_vars -> raise Exit
-								| _ -> Type.iter loop e
-							in
-							loop e2
-						end
-					in
-					try
-						check ik1 e2;
-						check (get_interference_kind e2) e1
-					with Exit -> match com.platform with
-						| Cpp when not (Common.defined com Define.Cppia) -> raise Exit
-						| Php -> raise Exit (* They don't define evaluation order, so let's exit *)
-						| _ -> affected := true;
-				in
+				let blocked = ref false in
+				let ir = InterferenceReport.from_texpr e1 in
+				(*if e.epos.pfile = "src/Main.hx" then print_endline (Printf.sprintf "FUSION %s<%i> = %s\n\t%s\n\t%s" v1.v_name v1.v_id (s_expr_pretty e1) (s_expr_pretty e2) (InterferenceReport.to_string ir));*)
 				let rec replace e =
-					let e = match e.eexpr with
+					let explore e =
+						let old = !blocked in
+						blocked := true;
+						let e = replace e in
+						blocked := old;
+						e
+					in
+					let handle_call e2 el = match com.platform with
+						| Neko ->
+							(* Neko has this reversed at the moment (issue #4787) *)
+							let el = List.map replace el in
+							let e2 = replace e2 in
+							e2,el
+						| Php | Cpp  when not (Common.defined com Define.Cppia) ->
+							let e2 = match e1.eexpr with
+								(* PHP doesn't like call()() expressions. *)
+								| TCall _ when com.platform = Php -> explore e2
+								| _ -> replace e2
+							in
+							let temp_found = false in
+							let really_found = ref !found in
+							let el = List.map (fun e ->
+								found := temp_found;
+								let e = replace e in
+								if !found then really_found := true;
+								e
+							) el in
+							found := !really_found;
+							e2,el
+						| _ ->
+							let e2 = replace e2 in
+							let el = List.map replace el in
+							e2,el
+						in
+					if !found then e else match e.eexpr with
 						| TWhile _ | TFunction _ ->
 							e
-						| TIf(e1,e2,eo) ->
-							let e1 = replace e1 in
-							{e with eexpr = TIf(e1,e2,eo)}
 						| TSwitch(e1,cases,edef) ->
 							let e1 = match com.platform with
-								| Lua | Python -> e1
+								| Lua | Python -> explore e1
 								| _ -> replace e1
 							in
 							{e with eexpr = TSwitch(e1,cases,edef)}
-						| TLocal v2 when v1 == v2 && not !affected ->
+						| TLocal v2 when v1 == v2 && not !blocked ->
 							found := true;
 							if type_change_ok com v1.v_type e1.etype then e1 else mk (TCast(e1,None)) v1.v_type e.epos
-						| TBinop((OpAssign | OpAssignOp _ as op),({eexpr = TArray(e1,e2)} as ea),e3) ->
-							let e1 = replace e1 in
+						| TLocal v when has_var_write ir v ->
+							raise Exit
+						| TBinop(OpAssign,({eexpr = TLocal v} as e1),e2) ->
 							let e2 = replace e2 in
-							let ea = {ea with eexpr = TArray(e1,e2)} in
-							let e3 = replace e3 in
-							{e with eexpr = TBinop(op,ea,e3)}
-						| TBinop((OpAssign | OpAssignOp _ as op),e1,e2) ->
+							if not !found && has_var_read ir v then raise Exit;
+							{e with eexpr = TBinop(OpAssign,e1,e2)}
+						| TBinop(OpAssignOp _ as op,({eexpr = TLocal v} as e1),e2) ->
 							let e2 = replace e2 in
-							let e1 = match e1.eexpr with TLocal _ -> e1 | _ -> replace e1 in
+							if not !found && (has_var_read ir v || has_var_write ir v) then raise Exit;
 							{e with eexpr = TBinop(op,e1,e2)}
-						| TUnop((Increment | Decrement),_,{eexpr = TLocal _}) ->
-							e
-						| TCall({eexpr = TLocal v},_) when is_really_unbound v ->
+						| TUnop((Increment | Decrement),_,{eexpr = TLocal v}) when has_var_read ir v || has_var_write ir v ->
+							raise Exit
+						(* fields *)
+						| TField(e1,fa) ->
+							let e1 = replace e1 in
+							if not !found && not (Optimizer.is_read_only_field_access fa) && (has_field_write ir (field_name fa) || has_state_write ir) then raise Exit;
+							{e with eexpr = TField(e1,fa)}
+						| TBinop(OpAssign,({eexpr = TField(e1,fa)} as ef),e2) ->
+							let e1 = replace e1 in
+							let e2 = replace e2 in
+							if not !found && (has_field_read ir (field_name fa) || has_state_read ir) then raise Exit;
+							{e with eexpr = TBinop(OpAssign,{ef with eexpr = TField(e1,fa)},e2)}
+						| TBinop(OpAssignOp _ as op,({eexpr = TField(e1,fa)} as ef),e2) ->
+							let e1 = replace e1 in
+							let s = field_name fa in
+							if not !found && (has_field_write ir s || has_state_write ir) then raise Exit;
+							let e2 = replace e2 in
+							if not !found && (has_field_read ir s || has_state_read ir) then raise Exit;
+							{e with eexpr = TBinop(op,{ef with eexpr = TField(e1,fa)},e2)}
+						| TUnop((Increment | Decrement),_,{eexpr = TField(e1,fa)}) when has_field_read ir (field_name fa) || has_state_read ir
+							|| has_field_write ir (field_name fa) || has_state_write ir ->
+							raise Exit
+						(* state *)
+						| TCall({eexpr = TLocal v},el) when not (is_unbound_call_that_might_have_side_effects v el) ->
 							e
-						(* TODO: this is a pretty outrageous hack for https://github.com/HaxeFoundation/haxe/issues/5366 *)
-						| TCall({eexpr = TField(_,FStatic({cl_path=["python"],"Syntax"},{cf_name="arraySet"}))} as ef,[e1;e2;e3]) ->
+						| TNew(c,tl,el) when (match c.cl_constructor with Some cf when Optimizer.is_pure c cf -> true | _ -> false) ->
+							let el = List.map replace el in
+							{e with eexpr = TNew(c,tl,el)}
+						| TCall(e1,el) ->
+							let e1,el = handle_call e1 el in
+							if not !found && ((Optimizer.has_side_effect e && (has_state_read ir || has_any_field_read ir)) || has_state_write ir || has_any_field_write ir) then raise Exit;
+							{e with eexpr = TCall(e1,el)}
+						| TNew(c,tl,el) ->
+							let el = List.map replace el in
+							if not !found && (has_state_write ir || has_state_read ir || has_any_field_read ir || has_any_field_write ir) then raise Exit;
+							{e with eexpr = TNew(c,tl,el)}
+						| TBinop(OpAssign,({eexpr = TArray(e1,e2)} as ea),e3) ->
+							let e1 = replace e1 in
+							let e2 = replace e2 in
 							let e3 = replace e3 in
+							if not !found && has_state_read ir then raise Exit;
+							{e with eexpr = TBinop(OpAssign,{ea with eexpr = TArray(e1,e2)},e3)}
+						| TBinop(op,e1,e2) when (match com.platform with Cpp | Php -> true | _ -> false) ->
 							let e1 = replace e1 in
+							let temp_found = !found in
+							found := false;
 							let e2 = replace e2 in
-							{e with eexpr = TCall(ef,[e1;e2;e3])}
-						| TCall(e1,el) when com.platform = Neko ->
-							(* Neko has this reversed at the moment (issue #4787) *)
-							let el = List.map replace el in
+							found := !found || temp_found;
+							{e with eexpr = TBinop(op,e1,e2)}
+						| TArray(e1,e2) ->
 							let e1 = replace e1 in
-							{e with eexpr = TCall(e1,el)}
+							let e2 = replace e2 in
+							if not !found && has_state_write ir then raise Exit;
+							{e with eexpr = TArray(e1,e2)}
 						| _ ->
 							Type.map_expr replace e
-					in
-					check_interference e;
-					e
 				in
 				begin try
-					let e = replace e2 in
+					let rec loop acc el = match el with
+						| e :: el ->
+							let e = replace e in
+							if !found then (List.rev (e :: acc)) @ el
+							else begin match e.eexpr with
+								| TWhile _ | TIf _ | TSwitch _ | TTry _ -> raise Exit
+								| _ -> loop (e :: acc) el
+							end
+						| [] ->
+							List.rev acc
+					in
+					let el = loop [] el in
 					if not !found then raise Exit;
 					changed := true;
 					change_num_uses v1 (-1);
-					fuse (e :: acc) el
+					(*if e.epos.pfile = "src/Main.hx" then print_endline (Printf.sprintf "OK: %s" (s_expr_pretty e));*)
+					fuse acc el
 				with Exit ->
-					fuse (ev :: acc) (e2 :: el)
+					(*if e.epos.pfile = "src/Main.hx" then print_endline (Printf.sprintf "NOPE: %s" (Printexc.get_backtrace()));*)
+					fuse (ev :: acc) el
 				end
 			| {eexpr = TUnop((Increment | Decrement as op,Prefix,({eexpr = TLocal v} as ev)))} as e1 :: e2 :: el ->
 				begin try
@@ -575,6 +753,17 @@ module Fusion = struct
 				with Exit ->
 					fuse (e1 :: acc) (e2 :: el)
 				end
+			| {eexpr = TBinop(OpAssign,e1,{eexpr = TBinop(op,e2,e3)})} as e :: el when use_assign_op op e1 e2 ->
+				let rec loop e = match e.eexpr with
+					| TLocal v -> change_num_uses v (-1)
+					| _ -> Type.iter loop e
+				in
+				loop e1;
+				changed := true;
+				fuse acc ({e with eexpr = TBinop(OpAssignOp op,e1,e3)} :: el)
+			| {eexpr = TBinop(OpAssignOp _,e1,_)} as eop :: ({eexpr = TVar(v,Some e2)} as evar) :: el when Texpr.equal e1 e2 ->
+				changed := true;
+				fuse ({evar with eexpr = TVar(v,Some eop)} :: acc) el
 			| e1 :: el ->
 				fuse (e1 :: acc) el
 			| [] ->
@@ -582,7 +771,8 @@ module Fusion = struct
 		in
 		let rec loop e = match e.eexpr with
 			| TBlock el ->
-				let el = List.map loop el in
+				let el = List.rev_map loop el in
+				let el = block_element [] el in
 				(* fuse flips element order, but block_element doesn't care and flips it back *)
 				let el = fuse [] el in
 				let el = block_element [] el in
@@ -752,7 +942,7 @@ module Purity = struct
 		| Some e ->
 			try
 				if (Meta.has (Meta.Custom ":impure")) cf.cf_meta then taint_raise node;
-				if is_pure c cf then raise Exit;
+				if Optimizer.is_pure c cf then raise Exit;
 				loop e;
 				node.pn_purity <- Pure;
 			with Exit ->

+ 53 - 23
src/optimization/analyzerTexprTransformer.ml

@@ -99,9 +99,13 @@ let rec func ctx bb tf t p =
 			bb,e
 		| TCall(e1,el) ->
 			call bb e e1 el
+		| TBinop(OpAssignOp op,({eexpr = TArray(e1,e2)} as ea),e3) ->
+			array_assign_op bb op e ea e1 e2 e3
+		| TBinop(OpAssignOp op,({eexpr = TField(e1,fa)} as ef),e2) ->
+			field_assign_op bb op e ef e1 fa e2
 		| TBinop((OpAssign | OpAssignOp _) as op,e1,e2) ->
-			let bb,e2 = value bb e2 in
 			let bb,e1 = value bb e1 in
+			let bb,e2 = value bb e2 in
 			bb,{e with eexpr = TBinop(op,e1,e2)}
 		| TBinop(op,e1,e2) ->
 			let bb,e1,e2 = match ordered_value_list bb [e1;e2] with
@@ -156,8 +160,8 @@ let rec func ctx bb tf t p =
 			close_node g bb;
 			add_cfg_edge bb_func_end bb_next CFGGoto;
 			bb_next,ec
-		| TTypeExpr(TClassDecl {cl_kind = KAbstractImpl a}) when not (Meta.has Meta.RuntimeValue a.a_meta) ->
-			error "Cannot use abstract as value" e.epos
+		(*| TTypeExpr(TClassDecl {cl_kind = KAbstractImpl a}) when not (Meta.has Meta.RuntimeValue a.a_meta) ->
+			error "Cannot use abstract as value" e.epos*)
 		| TTypeExpr(TClassDecl c) ->
 			List.iter (fun cf -> if not (Meta.has Meta.MaybeUsed cf.cf_meta) then cf.cf_meta <- (Meta.MaybeUsed,[],cf.cf_pos) :: cf.cf_meta;) c.cl_ordered_statics;
 			bb,e
@@ -195,8 +199,10 @@ let rec func ctx bb tf t p =
 		) (bb,[]) el in
 		bb,List.rev values
 	and bind_to_temp bb sequential e =
-		let is_probably_not_affected e e1 fa = match extract_field fa with
-			| Some {cf_kind = Method MethNormal} -> true
+		let is_probably_not_affected e e1 fa = match fa with
+			| FAnon cf | FInstance (_,_,cf) | FStatic (_,cf) | FClosure (_,cf) when cf.cf_kind = Method MethNormal -> true
+			| FEnum _ -> true
+			| FDynamic ("cca" | "__Index" | "__s") -> true (* This is quite retarded, but we have to deal with this somehow... *)
 			| _ -> match follow e.etype,follow e1.etype with
 				| TFun _,TInst _ -> false
 				| TFun _,_ -> true (* We don't know what's going on here, don't create a temp var (see #5082). *)
@@ -205,6 +211,12 @@ let rec func ctx bb tf t p =
 		let rec loop fl e = match e.eexpr with
 			| TField(e1,fa) when is_probably_not_affected e e1 fa ->
 				loop ((fun e' -> {e with eexpr = TField(e',fa)}) :: fl) e1
+			| TField(e1,fa) ->
+				let fa = match fa with
+					| FInstance(c,tl,({cf_kind = Method _ } as cf)) -> FClosure(Some(c,tl),cf)
+					| _ -> fa
+				in
+				fl,{e with eexpr = TField(e1,fa)}
 			| _ ->
 				fl,e
 		in
@@ -266,23 +278,35 @@ let rec func ctx bb tf t p =
 		let e,efinal = map_values f e in
 		block_element_plus bb (e,efinal) f
 	and call bb e e1 el =
-		begin match e1.eexpr with
-			| TConst TSuper when ctx.com.platform = Java || ctx.com.platform = Cs ->
-				bb,e
+		let check e t = match e.eexpr with
+			| TLocal v when is_ref_type t ->
+				v.v_capture <- true;
+				e
 			| _ ->
-				let check e t = match e.eexpr with
-					| TLocal v when is_ref_type t ->
-						v.v_capture <- true;
-						e
-					| _ ->
-						e
-				in
-				let el = Codegen.UnificationCallback.check_call check el e1.etype in
-					let bb,el = ordered_value_list bb (e1 :: el) in
-					match el with
-						| e1 :: el -> bb,{e with eexpr = TCall(e1,el)}
-						| _ -> assert false
-		end
+				e
+		in
+		let el = Codegen.UnificationCallback.check_call check el e1.etype in
+			let bb,el = ordered_value_list bb (e1 :: el) in
+			match el with
+				| e1 :: el -> bb,{e with eexpr = TCall(e1,el)}
+				| _ -> assert false
+	and array_assign_op bb op e ea e1 e2 e3 =
+		let bb,e1 = bind_to_temp bb false e1 in
+		let bb,e2 = bind_to_temp bb false e2 in
+		let ea = {ea with eexpr = TArray(e1,e2)} in
+		let bb,e4 = bind_to_temp bb false ea in
+		let bb,e3 = bind_to_temp bb false e3 in
+		let eop = {e with eexpr = TBinop(op,e4,e3)} in
+		add_texpr bb {e with eexpr = TBinop(OpAssign,ea,eop)};
+		bb,ea
+	and field_assign_op bb op e ef e1 fa e2 =
+		let bb,e1 = bind_to_temp bb false e1 in
+		let ef = {ef with eexpr = TField(e1,fa)} in
+		let bb,e3 = bind_to_temp bb false ef in
+		let bb,e2 = bind_to_temp bb false e2 in
+		let eop = {e with eexpr = TBinop(op,e3,e2)} in
+		add_texpr bb {e with eexpr = TBinop(OpAssign,ef,eop)};
+		bb,ef
 	and block_element bb e = match e.eexpr with
 		(* variables *)
 		| TVar(v,None) ->
@@ -518,12 +542,18 @@ let rec func ctx bb tf t p =
 			let b,e1 = value bb e1 in
 			add_texpr bb {e with eexpr = TCast(e1,Some mt)};
 			bb
-		| TBinop((OpAssign | OpAssignOp _) as op,({eexpr = TArray(e1,e2)} as ea),e3) ->
+		| TBinop(OpAssignOp op,({eexpr = TArray(e1,e2)} as ea),e3) ->
+			let bb,_ = array_assign_op bb op e ea e1 e2 e3 in
+			bb
+		| TBinop(OpAssignOp op,({eexpr = TField(e1,fa)} as ef),e2) ->
+			let bb,_ = field_assign_op bb op e ef e1 fa e2 in
+			bb
+		| TBinop(OpAssign,({eexpr = TArray(e1,e2)} as ea),e3) ->
 			let bb,e1,e2,e3 = match ordered_value_list bb [e1;e2;e3] with
 				| bb,[e1;e2;e3] -> bb,e1,e2,e3
 				| _ -> assert false
 			in
-			add_texpr bb {e with eexpr = TBinop(op,{ea with eexpr = TArray(e1,e2)},e3)};
+			add_texpr bb {e with eexpr = TBinop(OpAssign,{ea with eexpr = TArray(e1,e2)},e3)};
 			bb
 		| TBinop((OpAssign | OpAssignOp _ as op),e1,e2) ->
 			let bb,e1 = value bb e1 in

+ 25 - 12
src/optimization/optimizer.ml

@@ -25,10 +25,16 @@ open Typecore
 (* ---------------------------------------------------------------------- *)
 (* API OPTIMIZATIONS *)
 
+let has_pure_meta meta = Meta.has Meta.Pure meta
+
+let is_pure c cf = has_pure_meta c.cl_meta || has_pure_meta cf.cf_meta
+
 let field_call_has_side_effect f e1 fa el =
-	begin match extract_field fa with
-		| Some cf when Meta.has Meta.Pure cf.cf_meta -> ()
-		| _ -> raise Exit
+	begin match fa with
+	| FInstance(c,_,cf) | FStatic(c,cf) | FClosure(Some(c,_),cf) when is_pure c cf -> ()
+	| FAnon cf | FClosure(None,cf) when has_pure_meta cf.cf_meta -> ()
+	| FEnum _ -> ()
+	| _ -> raise Exit
 	end;
 	f e1;
 	List.iter f el
@@ -40,6 +46,7 @@ let has_side_effect e =
 		| TConst _ | TLocal _ | TTypeExpr _ | TFunction _ -> ()
 		| TCall ({ eexpr = TField(_,FStatic({ cl_path = ([],"Std") },{ cf_name = "string" })) },args) -> Type.iter loop e
 		| TCall({eexpr = TField(e1,fa)},el) -> field_call_has_side_effect loop e1 fa el
+		| TNew(c,_,el) when (match c.cl_constructor with Some cf when is_pure c cf -> true | _ -> false) -> List.iter loop el
 		| TNew _ | TCall _ | TBinop ((OpAssignOp _ | OpAssign),_,_) | TUnop ((Increment|Decrement),_,_) -> raise Exit
 		| TReturn _ | TBreak | TContinue | TThrow _ | TCast (_,Some _) -> raise Exit
 		| TArray _ | TEnumParameter _ | TCast (_,None) | TBinop _ | TUnop _ | TParenthesis _ | TMeta _ | TWhile _ | TFor _
@@ -231,12 +238,17 @@ let api_inline ctx c field params p = match c.cl_path, field, params with
 	| _ ->
 		api_inline2 ctx.com c field params p
 
-let rec is_affected_type t = match follow t with
-	| TAbstract({a_path = [],("Int" | "Float" | "Bool")},_) -> true
-	| TAbstract({a_path = ["haxe"],("Int64" | "Int32")},_) -> true
-	| TAbstract(a,tl) -> is_affected_type (Abstract.get_underlying_type a tl)
-	| TDynamic _ -> true (* sadly *)
-	| _ -> false
+let is_read_only_field_access fa = match fa with
+	| FEnum _ ->
+		true
+	| FDynamic _ ->
+		false
+	| FAnon cf | FInstance (_,_,cf) | FStatic (_,cf) | FClosure (_,cf) ->
+		match cf.cf_kind with
+			| Method MethDynamic -> false
+			| Method _ -> true
+			| Var {v_write = AccNever | AccNo} -> true
+			| _ -> false
 
 let create_affection_checker () =
 	let modified_locals = Hashtbl.create 0 in
@@ -244,7 +256,7 @@ let create_affection_checker () =
 		let rec loop e = match e.eexpr with
 			| TConst _ | TFunction _ | TTypeExpr _ -> ()
 			| TLocal v when Hashtbl.mem modified_locals v.v_id -> raise Exit
-			| TField _ when is_affected_type e.etype -> raise Exit
+			| TField(_,fa) when not (is_read_only_field_access fa) -> raise Exit
 			| _ -> Type.iter loop e
 		in
 		try
@@ -254,9 +266,9 @@ let create_affection_checker () =
 			true
 	in
 	let rec collect_modified_locals e = match e.eexpr with
-		| TUnop((Increment | Decrement),_,{eexpr = TLocal v}) when is_affected_type v.v_type ->
+		| TUnop((Increment | Decrement),_,{eexpr = TLocal v}) ->
 			Hashtbl.add modified_locals v.v_id true
-		| TBinop((OpAssign | OpAssignOp _),{eexpr = TLocal v},e2) when is_affected_type v.v_type ->
+		| TBinop((OpAssign | OpAssignOp _),{eexpr = TLocal v},e2) ->
 			collect_modified_locals e2;
 			Hashtbl.add modified_locals v.v_id true
 		| _ ->
@@ -400,6 +412,7 @@ let rec type_inline ctx cf f ethis params tret config p ?(self_calling_closure=f
 			had_side_effect := true;
 			l.i_force_temp <- true;
 		end;
+		if l.i_abstract_this then l.i_subst.v_extra <- Some ([],Some e);
 		l, e
 	) (ethis :: loop params f.tf_args true) ((vthis,None) :: f.tf_args) in
 	List.iter (fun (l,e) ->

+ 1 - 1
tests/optimization/run.hxml

@@ -1,5 +1,5 @@
 -cp src
--D analyzer
+-D analyzer-user-var-fusion
 --each
 
 -main TestAnalyzer

+ 1 - 1
tests/optimization/src/TestJs.hx

@@ -576,7 +576,7 @@ class TestJs {
 	static function use<T>(t:T) { return t; }
 
 	static var intField = 12;
-	static var stringField = "foo";
+	static var stringField(default, never) = "foo";
 
 	@:js('
 		var _g = Type["typeof"]("");

+ 1 - 1
tests/optimization/src/issues/Issue4690.hx

@@ -39,7 +39,7 @@ class Issue4690 {
 		console.log(c_y);
 		console.log(c_z);
 	')
-	@:analyzer(no_const_propagation)
+	@:analyzer(no_const_propagation, no_fusion)
 	static function test() {
 		var c = new Child(1, 2, 3);
 		trace(c.x);

+ 16 - 0
tests/optimization/src/issues/Issue5477.hx

@@ -0,0 +1,16 @@
+package issues;
+
+class Issue5477 {
+	@:js('
+		issues_Issue5477["use"](issues_Issue5477.pureUse(12) > 0.5?1:issues_Issue5477.pureUse(12));
+	')
+	static function testIssue5477() {
+		var v = pureUse(12);
+		use(pureUse(12) > 0.5 ? 1 : v);
+	}
+
+	@:impure
+	static function use<T>(t:T) { return t; }
+	@:pure
+	static function pureUse<T>(t:T) { return t; }
+}

+ 0 - 0
tests/unit/src/unit/issues/Issue4436.hx → tests/unit/src/unit/issues/Issue4436.hx.disabled


+ 16 - 0
tests/unit/src/unit/issues/Issue5358.hx

@@ -0,0 +1,16 @@
+package unit.issues;
+
+class Issue5358 extends unit.Test {
+
+	#if false
+	static var example = if (veryRandom() > 0.5) 1 else 2;
+
+	function test() {
+		eq(example, 1);
+	}
+
+	static function veryRandom() {
+		return 4;
+	}
+	#end
+}

+ 167 - 0
tests/unit/src/unit/issues/Issue5477.hx

@@ -0,0 +1,167 @@
+package unit.issues;
+
+private class C {
+	public var hash:Array<Int>;
+	public function new() {
+		hash = [1, 2, 3];
+	}
+
+	public function add(x) {
+		hash[0] = x;
+		return x;
+	}
+}
+
+private class D {
+	public var field:{value1: Int, value2: Int};
+	public function new() {
+		field = { value1: 1, value2: 2 };
+	}
+
+	public function set(x) {
+		field.value1 = x;
+		return x;
+	}
+}
+
+private class StupidStringBuf {
+	public var b:String;
+
+	public inline function new() {
+		b = "";
+	}
+
+	public inline function add(s:String) {
+		b += s;
+	}
+}
+
+class Issue5477 extends unit.Test  {
+	static var idCounter = 0;
+
+	function testArray() {
+		idCounter = 0;
+
+		var c = new C();
+		almostId(c).hash[0] = c.add(4);
+		eq(4, c.hash[0]);
+		eq(1, idCounter);
+
+		var c = new C();
+		c.hash[0] += c.add(4);
+		eq(5, c.hash[0]);
+
+		var c = new C();
+		c.hash[1] = almostId(c).hash[0] += {
+			var x = c.add(4);
+			x;
+		}
+		eq(5, c.hash[0]);
+		eq(5, c.hash[1]);
+		eq(2, idCounter);
+
+		var c = new C();
+		almostId(c).hash[1] += ({eq(3, idCounter); almostId(c);}).hash[0] += {
+			var x = c.add(4);
+			x;
+		}
+		eq(5, c.hash[0]);
+		eq(7, c.hash[1]);
+		eq(4, idCounter);
+
+		var c = new C();
+		c.hash[1] += c.hash[0] = {
+			var x = c.add(4);
+			x;
+		}
+		eq(4, c.hash[0]);
+		eq(6, c.hash[1]);
+
+		var c = new C();
+		var tmp = c.add(4);
+		c.hash[0] += tmp;
+		eq(8, c.hash[0]);
+	}
+
+	function testField() {
+		idCounter = 0;
+
+		var d = new D();
+		almostId(d).field.value1 = d.set(4);
+		eq(4, d.field.value1);
+		eq(1, idCounter);
+
+		var d = new D();
+		d.field.value1 += d.set(4);
+		eq(5, d.field.value1);
+
+		var d = new D();
+		d.field.value2 = almostId(d).field.value1 += {
+			var x = d.set(4);
+			x;
+		}
+		eq(5, d.field.value1);
+		eq(5, d.field.value2);
+		eq(2, idCounter);
+
+		var d = new D();
+		almostId(d).field.value2 += ({eq(3, idCounter); almostId(d);}).field.value1 += {
+			var x = d.set(4);
+			x;
+		}
+		eq(5, d.field.value1);
+		eq(7, d.field.value2);
+		eq(4, idCounter);
+
+		var d = new D();
+		d.field.value2 += d.field.value2 = {
+			var x = d.set(4);
+			x;
+		}
+		eq(4, d.field.value1);
+		eq(6, d.field.value2);
+
+		var d = new D();
+		var tmp = d.set(4);
+		d.field.value1 += tmp;
+		eq(8, d.field.value1);
+	}
+
+	function testStringBuf() {
+		var buf = new StupidStringBuf();
+		buf.add("1");
+		buf.add(messUp(buf));
+		eq("23", buf.b);
+
+		var buf = new StupidStringBuf();
+		buf.add("1");
+		buf.b += messUp(buf);
+		eq("13", buf.b);
+	}
+
+	function test() {
+        var a = g();
+        var b = ff() + a;
+        eq(3, b);
+    }
+
+    static var x = 1;
+
+    static function ff() {
+        return x = 2;
+    }
+
+    static function g() {
+        return x;
+    }
+
+	static function messUp(buf:StupidStringBuf) {
+		buf.b = "2";
+		return "3";
+	}
+
+	static function almostId<T>(c:T) {
+		++idCounter;
+		return c;
+	}
+}

+ 27 - 0
tests/unit/src/unit/issues/Issue5520.hx

@@ -0,0 +1,27 @@
+package unit.issues;
+
+private class A extends B {
+	public function new() {
+		super([
+			"sample" => C.E
+		]);
+	}
+}
+
+private class B {
+	public var map:Map<String, C>;
+	public function new(str:Map<String, C>) {
+		map = str;
+	}
+}
+
+private enum C {
+	E;
+}
+
+class Issue5520 extends unit.Test {
+	function test() {
+		var a = new A();
+		eq(C.E, a.map["sample"]);
+	}
+}

+ 31 - 0
tests/unit/src/unit/issues/Issue5544.hx

@@ -0,0 +1,31 @@
+package unit.issues;
+
+private extern class C {
+	public inline function inlineMe():String {
+		return "I'm inlined!";
+	}
+}
+
+private typedef T = {
+	function inlineMe():String;
+}
+
+@:multiType
+private abstract A(T) {
+
+	public function new();
+
+	@:to @:extern inline function toC():C {
+		return null;
+	}
+
+	public inline function get(s:String) {
+		return this.inlineMe();
+	}
+}
+
+class Issue5544 extends unit.Test {
+	function test() {
+		eq("I'm inlined!", new A().get("foo"));
+	}
+}