Browse Source

Null coalescing operator (#10428)

* Null coalescing operator

* use value_reference to avoid duplication

* Disable op overloading

* Allow ??=

* Option 2: better errors for static targets

* Type first operand as nullable

* Allow (rtl) ?? overloading

imagine rtl multiplication op with this, i'm not sure...

* Revert "Allow (rtl) ?? overloading"

This reverts commit 48320b6ed38b8940e9323611c9703b2bce8f5d15.

* Evaluation test
RblSb 3 years ago
parent
commit
1ec08562ec

+ 1 - 1
src/codegen/gencommon/dynamicOperators.ml

@@ -119,7 +119,7 @@ let init com handle_strings (should_change:texpr->bool) (equals_handler:texpr->t
 				{ e with eexpr = TBinop (op, mk_cast com.basic.tbool (run e1), mk_cast com.basic.tbool (run e2)) }
 			| OpAnd | OpOr | OpXor | OpShl | OpShr | OpUShr ->
 				{ e with eexpr = TBinop (op, mk_cast com.basic.tint (run e1), mk_cast com.basic.tint (run e2)) }
-			| OpAssign | OpAssignOp _ | OpInterval | OpArrow | OpIn ->
+			| OpAssign | OpAssignOp _ | OpInterval | OpArrow | OpIn | OpNullCoal ->
 				Globals.die "" __LOC__)
 
 		| TUnop (Increment as op, flag, e1)

+ 3 - 1
src/core/ast.ml

@@ -91,6 +91,7 @@ type binop =
 	| OpInterval
 	| OpArrow
 	| OpIn
+	| OpNullCoal
 
 type unop =
 	| Increment
@@ -560,6 +561,7 @@ let rec s_binop = function
 	| OpInterval -> "..."
 	| OpArrow -> "=>"
 	| OpIn -> " in "
+	| OpNullCoal -> "??"
 
 let s_unop = function
 	| Increment -> "++"
@@ -1239,4 +1241,4 @@ let get_meta_options metas meta =
 		| [] ->
 			[]
 	in
-	loop metas
+	loop metas

+ 2 - 1
src/core/json/genjson.ml

@@ -148,6 +148,7 @@ let rec generate_binop ctx op =
 	| OpInterval -> "OpInterval",None
 	| OpArrow -> "OpArrow",None
 	| OpIn -> "OpIn",None
+	| OpNullCoal -> "OpNullCoal",None
 	in
 	generate_adt ctx (Some (["haxe";"macro"],"Binop")) name args
 
@@ -733,4 +734,4 @@ let generate types file =
 	let ch = open_out_bin file in
 	Json.write_json (output_string ch) json;
 	close_out ch;
-	t()
+	t()

+ 3 - 0
src/generators/gencpp.ml

@@ -4233,6 +4233,7 @@ let gen_cpp_ast_expression_tree ctx class_name func_name function_args function_
       | OpInterval -> "..."
       | OpArrow -> "->"
       | OpIn -> " in "
+      | OpNullCoal -> "??"
       | OpAssign | OpAssignOp _ -> abort "Unprocessed OpAssign" pos
 
    and gen_closure closure =
@@ -7322,6 +7323,7 @@ let cppia_op_info = function
 	| IaBinOp OpInterval -> ("...", 121)
 	| IaBinOp OpArrow -> ("=>", 122)
 	| IaBinOp OpIn -> (" in ", 123)
+	| IaBinOp OpNullCoal -> ("??", 124)
 	| IaBinOp OpAssignOp OpAdd -> ("+=", 201)
 	| IaBinOp OpAssignOp OpMult -> ("*=", 202)
 	| IaBinOp OpAssignOp OpDiv -> ("/=", 203)
@@ -7339,6 +7341,7 @@ let cppia_op_info = function
 	| IaBinOp OpAssignOp OpMod -> ("%=", 220)
 
 	| IaBinOp OpAssignOp OpIn
+	| IaBinOp OpAssignOp OpNullCoal
 	| IaBinOp OpAssignOp OpInterval
 	| IaBinOp OpAssignOp OpAssign
 	| IaBinOp OpAssignOp OpEq

+ 1 - 1
src/generators/genhl.ml

@@ -2484,7 +2484,7 @@ and eval_expr ctx e =
 					free ctx r;
 					binop r r b;
 					r))
-		| OpInterval | OpArrow | OpIn ->
+		| OpInterval | OpArrow | OpIn | OpNullCoal ->
 			die "" __LOC__)
 	| TUnop (Not,_,v) ->
 		let tmp = alloc_tmp ctx HBool in

+ 1 - 1
src/generators/genpy.ml

@@ -1081,7 +1081,7 @@ module Printer = struct
 		| OpShr -> ">>"
 		| OpUShr -> ">>"
 		| OpMod -> "%"
-		| OpInterval | OpArrow | OpIn | OpAssignOp _ -> die "" __LOC__
+		| OpInterval | OpArrow | OpIn | OpNullCoal | OpAssignOp _ -> die "" __LOC__
 
 	let print_string s =
 		Printf.sprintf "\"%s\"" (StringHelper.s_escape s)

+ 1 - 1
src/generators/genswf9.ml

@@ -1829,7 +1829,7 @@ and gen_binop ctx retval op e1 e2 t p =
 		gen_op A3OLt
 	| OpLte ->
 		gen_op A3OLte
-	| OpInterval | OpArrow | OpIn ->
+	| OpInterval | OpArrow | OpIn | OpNullCoal ->
 		die "" __LOC__
 
 and gen_expr ctx retval e =

+ 2 - 2
src/macro/eval/evalDebugMisc.ml

@@ -266,7 +266,7 @@ let rec expr_to_value ctx env e =
 			| OpBoolOr ->
 				if is_true (loop e1) then VTrue
 				else loop e2
-			| OpInterval | OpArrow | OpIn ->
+			| OpInterval | OpArrow | OpIn | OpNullCoal ->
 				raise NoValueExpr
 			| _ ->
 				let v1 = loop e1 in
@@ -417,4 +417,4 @@ and write_expr ctx env expr value =
 
 let expr_to_value_safe ctx env e =
 	try expr_to_value ctx env e
-	with NoValueExpr -> VNull
+	with NoValueExpr -> VNull

+ 1 - 1
src/macro/eval/evalMisc.ml

@@ -284,7 +284,7 @@ let get_binop_fun op p = match op with
 	| OpShr -> op_shr p
 	| OpUShr -> op_ushr p
 	| OpMod -> op_mod p
-	| OpAssign | OpBoolAnd | OpBoolOr | OpAssignOp _ | OpInterval | OpArrow | OpIn -> die ~p "" __LOC__
+	| OpAssign | OpBoolAnd | OpBoolOr | OpAssignOp _ | OpInterval | OpArrow | OpIn | OpNullCoal -> die ~p "" __LOC__
 
 let prepare_callback v n =
 	match v with

+ 2 - 0
src/macro/macroApi.ml

@@ -260,6 +260,7 @@ let rec encode_binop op =
 	| OpInterval -> 21, []
 	| OpArrow -> 22, []
 	| OpIn -> 23, []
+	| OpNullCoal -> 24, []
 	in
 	encode_enum IBinop tag pl
 
@@ -594,6 +595,7 @@ let rec decode_op op =
 	| 21, [] -> OpInterval
 	| 22,[] -> OpArrow
 	| 23,[] -> OpIn
+	| 24,[] -> OpNullCoal
 	| _ -> raise Invalid_expr
 
 let decode_unop op =

+ 1 - 0
src/optimization/analyzerTexpr.ml

@@ -504,6 +504,7 @@ module Fusion = struct
 		| OpAssignOp _
 		| OpInterval
 		| OpIn
+		| OpNullCoal
 		| OpArrow ->
 			false
 

+ 2 - 2
src/optimization/analyzerTexprTransformer.ml

@@ -747,7 +747,7 @@ and func ctx i =
 				| OpAdd | OpMult | OpDiv | OpSub | OpAnd
 				| OpOr | OpXor | OpShl | OpShr | OpUShr | OpMod ->
 					true
-				| OpAssignOp _ | OpInterval | OpArrow | OpIn | OpAssign | OpEq
+				| OpAssignOp _ | OpInterval | OpArrow | OpIn | OpNullCoal | OpAssign | OpEq
 				| OpNotEq | OpGt | OpGte | OpLt | OpLte | OpBoolAnd | OpBoolOr ->
 					false
 			in
@@ -777,4 +777,4 @@ and func ctx i =
 	mk (TFunction {tf with tf_expr = e}) t p
 
 let to_texpr ctx =
-	func ctx ctx.entry.bb_id
+	func ctx ctx.entry.bb_id

+ 2 - 1
src/optimization/optimizer.ml

@@ -55,6 +55,7 @@ let standard_precedence op =
 	| OpBoolAnd -> 14, left
 	| OpBoolOr -> 15, left
 	| OpArrow -> 16, left
+	| OpNullCoal -> 17, right
 	| OpAssignOp OpAssign -> 18, right (* mimics ?: *)
 	| OpAssign | OpAssignOp _ -> 19, right
 
@@ -385,4 +386,4 @@ let rec make_constant_expression ctx ?(concat_strings=false) e =
 			| None -> None
 			| Some e -> make_constant_expression ctx e)
 		with Not_found -> None) *)
-	| _ -> None
+	| _ -> None

+ 3 - 1
src/syntax/lexer.ml

@@ -393,6 +393,7 @@ let rec token lexbuf =
 	| "<<=" -> mk lexbuf (Binop (OpAssignOp OpShl))
 	| "||=" -> mk lexbuf (Binop (OpAssignOp OpBoolOr))
 	| "&&=" -> mk lexbuf (Binop (OpAssignOp OpBoolAnd))
+	| "??=" -> mk lexbuf (Binop (OpAssignOp OpNullCoal))
 (*//| ">>=" -> mk lexbuf (Binop (OpAssignOp OpShr)) *)
 (*//| ">>>=" -> mk lexbuf (Binop (OpAssignOp OpUShr)) *)
 	| "==" -> mk lexbuf (Binop OpEq)
@@ -427,6 +428,7 @@ let rec token lexbuf =
 	| "}" -> mk lexbuf BrClose
 	| "(" -> mk lexbuf POpen
 	| ")" -> mk lexbuf PClose
+	| "??" -> mk lexbuf (Binop OpNullCoal)
 	| "?" -> mk lexbuf Question
 	| "@" -> mk lexbuf At
 
@@ -684,4 +686,4 @@ let lex_xml p lexbuf =
 	try
 		not_xml ctx 0 (name <> "") (* don't allow self-closing fragments *)
 	with Exit ->
-		error Unterminated_markup p
+		error Unterminated_markup p

+ 2 - 2
src/syntax/parser.ml

@@ -271,7 +271,7 @@ let precedence op =
 	| OpInterval -> 7, left
 	| OpBoolAnd -> 8, left
 	| OpBoolOr -> 9, left
-	| OpArrow -> 10, right
+	| OpArrow | OpNullCoal -> 10, right
 	| OpAssign | OpAssignOp _ -> 11, right
 
 let is_higher_than_ternary = function
@@ -425,4 +425,4 @@ let convert_abstract_flags flags =
 let no_keyword what s =
 	match Stream.peek s with
 	| Some (Kwd kwd,p) -> error (Custom ("Keyword " ^ (s_keyword kwd) ^ " cannot be used as " ^ what)) p
-	| _ -> raise Stream.Failure
+	| _ -> raise Stream.Failure

+ 1 - 0
src/syntax/reification.ml

@@ -70,6 +70,7 @@ let reify in_macro =
 		| OpInterval -> op "OpInterval"
 		| OpArrow -> op "OpArrow"
 		| OpIn -> op "OpIn"
+		| OpNullCoal -> op "OpNullCoal"
 	in
 	let to_string s p =
 		let len = String.length s in

+ 2 - 1
src/typing/operators.ml

@@ -444,6 +444,7 @@ let make_binop ctx op e1 e2 is_assign_op with_type p =
 		typing_error "Unexpected =>" p
 	| OpIn ->
 		typing_error "Unexpected in" p
+	| OpNullCoal
 	| OpAssign
 	| OpAssignOp _ ->
 		die "" __LOC__
@@ -952,4 +953,4 @@ let type_unop ctx op flag e with_type p =
 				find_overload_or_make e
 			end
 		| AKUsingField _ | AKResolve _ ->
-			typing_error "Invalid operation" p
+			typing_error "Invalid operation" p

+ 3 - 0
src/typing/typeloadFields.ml

@@ -1091,6 +1091,9 @@ let check_abstract (ctx,cctx,fctx) c cf fd t ret p =
 					allow_no_expr();
 				| (Meta.Op,[EBinop(OpAssign,_,_),_],_) :: _ ->
 					typing_error (cf.cf_name ^ ": Assignment overloading is not supported") p;
+				| (Meta.Op,[EBinop(OpAssignOp OpNullCoal,_,_),_],_) :: _
+				| (Meta.Op,[EBinop(OpNullCoal,_,_),_],_) :: _ ->
+					typing_error (cf.cf_name ^ ": Null coalescing overloading is not supported") p;
 				| (Meta.Op,[ETernary(_,_,_),_],_) :: _ ->
 					typing_error (cf.cf_name ^ ": Ternary overloading is not supported") p;
 				| (Meta.Op,[EBinop(op,_,_),_],_) :: _ ->

+ 29 - 12
src/typing/typer.ml

@@ -1459,6 +1459,19 @@ and type_cast ctx e t p =
 	let texpr = loop t in
 	mk (TCast (type_expr ctx e WithType.value,Some texpr)) t p
 
+and make_if_then_else ctx e0 e1 e2 with_type p =
+	let e1,e2,t = match with_type with
+	| WithType.NoValue -> e1,e2,ctx.t.tvoid
+	| WithType.Value _ -> e1,e2,unify_min ctx [e1; e2]
+	| WithType.WithType(t,src) when (match follow t with TMono _ -> true | t -> ExtType.is_void t) ->
+		e1,e2,unify_min_for_type_source ctx [e1; e2] src
+	| WithType.WithType(t,_) ->
+		let e1 = AbstractCast.cast_or_unify ctx t e1 e1.epos in
+		let e2 = AbstractCast.cast_or_unify ctx t e2 e2.epos in
+		e1,e2,t
+	in
+	mk (TIf (e0,e1,Some e2)) t p
+
 and type_if ctx e e1 e2 with_type is_ternary p =
 	let e = type_expr ctx e WithType.value in
 	if is_ternary then begin match e.eexpr with
@@ -1467,22 +1480,12 @@ and type_if ctx e e1 e2 with_type is_ternary p =
 	end;
 	let e = AbstractCast.cast_or_unify ctx ctx.t.tbool e p in
 	let e1 = type_expr ctx (Expr.ensure_block e1) with_type in
-	(match e2 with
+	match e2 with
 	| None ->
 		mk (TIf (e,e1,None)) ctx.t.tvoid p
 	| Some e2 ->
 		let e2 = type_expr ctx (Expr.ensure_block e2) with_type in
-		let e1,e2,t = match with_type with
-			| WithType.NoValue -> e1,e2,ctx.t.tvoid
-			| WithType.Value _ -> e1,e2,unify_min ctx [e1; e2]
-			| WithType.WithType(t,src) when (match follow t with TMono _ -> true | t -> ExtType.is_void t) ->
-				e1,e2,unify_min_for_type_source ctx [e1; e2] src
-			| WithType.WithType(t,_) ->
-				let e1 = AbstractCast.cast_or_unify ctx t e1 e1.epos in
-				let e2 = AbstractCast.cast_or_unify ctx t e2 e2.epos in
-				e1,e2,t
-		in
-		mk (TIf (e,e1,Some e2)) t p)
+		make_if_then_else ctx e e1 e2 with_type p
 
 and type_meta ?(mode=MGet) ctx m e1 with_type p =
 	if ctx.is_display_file then DisplayEmitter.check_display_metadata ctx [m];
@@ -1717,6 +1720,20 @@ and type_expr ?(mode=MGet) ctx (e,p) (with_type:WithType.t) =
 		| other -> typing_error (other ^ " is not a valid float suffix") p)
 	| EConst c ->
 		Texpr.type_constant ctx.com.basic c p
+	| EBinop (OpNullCoal,e1,e2) ->
+		let vr = new value_reference ctx in
+		let e1 = type_expr ctx (Expr.ensure_block e1) with_type in
+		let e2 = type_expr ctx (Expr.ensure_block e2) with_type in
+		let e1 = vr#as_var "tmp" {e1 with etype = ctx.t.tnull e1.etype} in
+		let e_null = Builder.make_null e1.etype e1.epos in
+		let e_cond = mk (TBinop(OpNotEq,e1,e_null)) ctx.t.tbool e1.epos in
+		let iftype = WithType.WithType(e2.etype,None) in
+		let e_if = make_if_then_else ctx e_cond e1 e2 iftype p in
+		vr#to_texpr e_if
+	| EBinop (OpAssignOp OpNullCoal,e1,e2) ->
+		let e_cond = EBinop(OpNotEq,e1,(EConst(Ident "null"), p)) in
+		let e_if = EIf ((e_cond, p),e1,Some e2) in
+		type_assign ctx e1 (e_if, p) with_type p
 	| EBinop (op,e1,e2) ->
 		type_binop ctx op e1 e2 false with_type p
 	| EBlock [] when (match with_type with

+ 1 - 0
std/haxe/display/JsonModuleTypes.hx

@@ -163,6 +163,7 @@ enum abstract JsonBinopKind<T>(String) {
 	var OpInterval;
 	var OpArrow;
 	var OpIn;
+	var OpNullCoal;
 }
 
 typedef JsonBinop<T> = {

+ 5 - 0
std/haxe/macro/Expr.hx

@@ -214,6 +214,11 @@ enum Binop {
 		`in`
 	**/
 	OpIn;
+
+	/**
+		`??`
+	**/
+	OpNullCoal;
 }
 
 /**

+ 1 - 0
std/haxe/macro/Printer.hx

@@ -75,6 +75,7 @@ class Printer {
 			case OpInterval: "...";
 			case OpArrow: "=>";
 			case OpIn: "in";
+			case OpNullCoal: "??";
 			case OpAssignOp(op):
 				printBinop(op) + "=";
 		}

+ 124 - 0
tests/unit/src/unit/TestNullCoalescing.hx

@@ -0,0 +1,124 @@
+package unit;
+
+@:nullSafety(StrictThreaded)
+class TestNullCoalescing extends Test {
+	final nullInt:Null<Int> = null;
+	final nullBool:Null<Bool> = null;
+	final nullString:Null<String> = null;
+
+	var count = 0;
+	function call() {
+		count++;
+		return "_";
+	}
+
+	function test() {
+		var a = call() ?? "default";
+		eq(count, 1);
+
+		eq(nullInt ?? nullInt, null);
+		eq(nullBool ?? nullBool, null);
+
+		final a:Dynamic = Std.random(0) + 1;
+		final b = Std.random(0) + 2;
+		eq(1 + a + 1 ?? 1 + b + 1, 3);
+
+		final nullableBool:Null<Bool> = false;
+		final testBool = nullBool ?? true;
+		final testNullBool = nullBool ?? nullableBool;
+		final s:Int = nullInt == null ? 2 : nullInt;
+		final s:Int = if (nullInt == null) 2; else nullInt;
+		final s = nullInt ?? 2;
+
+		// $type(testBool); // Bool
+		// $type(testNullBool); // Null<Bool>
+		// $type(s); // Int
+		final shouldBeBool:Bool = testBool;
+		if (testNullBool == null) {}
+		final shouldBeInt:Int = s;
+
+		eq(testBool, true);
+		eq(testNullBool, false);
+		eq(s, 2);
+
+		eq(nullInt == null ? 2 : nullInt, 2);
+		eq(nullInt ?? 2, 2);
+		eq(nullInt ?? (2 : Null<Int>) ?? 3 + 100, 2);
+		eq(nullInt ?? nullInt ?? 3, 3);
+
+		final i:Null<Int> = 1;
+		final arr:Array<Int> = [i ?? 2];
+		arr.push(i ?? 2);
+		arr.push((1 : Null<Int>) ?? 2);
+		eq(arr[0], 1);
+		eq(arr[1], 1);
+		eq(arr[2], 1);
+
+		final arr = [
+			nullInt ?? 2,
+			2
+		];
+		eq(arr[0], arr[1]);
+
+		var a = [0 => nullInt ?? 0 + 100];
+		eq(a[0], 100);
+
+		final di:Null<Dynamic> = null;
+		final di2:Null<Dynamic> = null;
+		final di3:Null<Dynamic> = 2;
+		eq(di ?? di2 ?? di3, 2);
+
+		var a:Null<Int> = null;
+		a ??= 5;
+		eq(a, 5);
+		var a:Null<Int> = null;
+		eq(a ??= 5, 5);
+		eq(a, 5);
+		var a = "default";
+		eq(a ??= "5", "default");
+
+		count = 0;
+		var a = call();
+		eq(count, 1);
+		a ??= call();
+		eq(count, 1);
+
+		var a:Null<String> = null;
+		final b = a ??= call();
+		final c = a ??= call();
+		eq(count, 2);
+		eq(a, "_");
+		eq(b, "_");
+		eq(c, "_");
+
+		final a:Null<Int> = ({} : Dynamic).x;
+		eq(a ?? 2, 2);
+
+		final a = nullInt;
+		eq(a ?? 2, 2);
+
+		final a = nullString;
+		eq(a ?? "2", "2");
+
+		eq(1 ?? 2, 1);
+		eq("1" ?? "2", "1");
+
+		final arr = [];
+		function item(n) {
+			arr.push(n);
+			return n;
+		}
+		eq(item(1) ?? item(2) ?? item(3), 1);
+		eq(arr.length, 1);
+		for (i => v in [1]) eq(arr[i], v);
+
+		final arr = [];
+		function item(n) {
+			arr.push(n);
+			return null;
+		}
+		eq(item(1) ?? item(2) ?? item(3), null);
+		eq(arr.length, 3);
+		for (i => v in [1, 2, 3]) eq(arr[i], v);
+	}
+}