Browse Source

[typer] clean up AKNo errors

closes #10880

Also casually add a very partial diff port that I found in my git directory. That should probably be a haxelib but I hate haxelib.
Simon Krajewski 2 years ago
parent
commit
a3859970b7

+ 5 - 4
src/typing/calls.ml

@@ -190,7 +190,7 @@ let rec acc_get ctx g p =
 	in
 	in
 	let dispatcher () = new call_dispatcher ctx MGet WithType.value p in
 	let dispatcher () = new call_dispatcher ctx MGet WithType.value p in
 	match g with
 	match g with
-	| AKNo f -> typing_error ("Field " ^ f ^ " cannot be accessed for reading") p
+	| AKNo(name,p) -> typing_error (name ^ " cannot be accessed for reading") p
 	| AKExpr e -> e
 	| AKExpr e -> e
 	| AKSafeNav sn ->
 	| AKSafeNav sn ->
 		(* generate null-check branching for the safe navigation chain *)
 		(* generate null-check branching for the safe navigation chain *)
@@ -302,9 +302,10 @@ let rec build_call_access ctx acc el mode with_type p =
 		AKExpr e
 		AKExpr e
 	| AKResolve(sea,name) ->
 	| AKResolve(sea,name) ->
 		AKExpr (dispatch#expr_call (dispatch#resolve_call sea name) [] el)
 		AKExpr (dispatch#expr_call (dispatch#resolve_call sea name) [] el)
-	| AKNo _ | AKAccess _ ->
-		ignore(acc_get ctx acc p);
-		typing_error ("Unexpected access mode, please report this: " ^ (s_access_kind acc)) p
+	| AKNo(s,p) ->
+		typing_error (s ^ " cannot be called") p
+	| AKAccess _ ->
+		typing_error "This expression cannot be called" p
 	| AKAccessor fa ->
 	| AKAccessor fa ->
 		let e = get_accessor_to_call fa [] in
 		let e = get_accessor_to_call fa [] in
 		AKExpr (dispatch#expr_call e [] el)
 		AKExpr (dispatch#expr_call e [] el)

+ 4 - 4
src/typing/fields.ml

@@ -195,9 +195,9 @@ let field_access ctx mode f fh e pfield =
 			| TAnon a ->
 			| TAnon a ->
 				(match !(a.a_status) with
 				(match !(a.a_status) with
 				| Statics c2 when ctx.curclass == c2 || can_access ctx c2 { f with cf_flags = unset_flag f.cf_flags (int_of_class_field_flag CfPublic) } true -> normal false
 				| Statics c2 when ctx.curclass == c2 || can_access ctx c2 { f with cf_flags = unset_flag f.cf_flags (int_of_class_field_flag CfPublic) } true -> normal false
-				| _ -> if ctx.untyped then normal false else AKNo f.cf_name)
+				| _ -> if ctx.untyped then normal false else AKNo(f.cf_name, pfield))
 			| _ ->
 			| _ ->
-				if ctx.untyped then normal false else AKNo f.cf_name)
+				if ctx.untyped then normal false else AKNo(f.cf_name,pfield))
 		| AccNormal | AccNo ->
 		| AccNormal | AccNo ->
 			normal false
 			normal false
 		| AccCall when (not ctx.allow_transform) || (ctx.in_display && DisplayPosition.display_position#enclosed_in pfull) ->
 		| AccCall when (not ctx.allow_transform) || (ctx.in_display && DisplayPosition.display_position#enclosed_in pfull) ->
@@ -233,7 +233,7 @@ let field_access ctx mode f fh e pfield =
 				AKAccessor (make_access false)
 				AKAccessor (make_access false)
 			end
 			end
 		| AccNever ->
 		| AccNever ->
-			if ctx.untyped then normal false else AKNo f.cf_name
+			if ctx.untyped then normal false else AKNo(f.cf_name,pfield)
 		| AccInline ->
 		| AccInline ->
 			normal true
 			normal true
 		| AccCtor ->
 		| AccCtor ->
@@ -242,7 +242,7 @@ let field_access ctx mode f fh e pfield =
 			in
 			in
 			(match ctx.curfun, fh with
 			(match ctx.curfun, fh with
 				| FunConstructor, FHInstance(c,_) when c == ctx.curclass || is_child_of_abstract c -> normal false
 				| FunConstructor, FHInstance(c,_) when c == ctx.curclass || is_child_of_abstract c -> normal false
-				| _ -> AKNo f.cf_name
+				| _ -> AKNo(f.cf_name,pfield)
 			)
 			)
 		| AccRequire (r,msg) ->
 		| AccRequire (r,msg) ->
 			match msg with
 			match msg with

+ 5 - 5
src/typing/operators.ml

@@ -556,7 +556,7 @@ let type_assign ctx e1 e2 with_type p =
 		mk (TBinop (OpAssign,e1,e2)) e1.etype p
 		mk (TBinop (OpAssign,e1,e2)) e1.etype p
 	in
 	in
 	match e1 with
 	match e1 with
-	| AKNo s -> typing_error ("Cannot access field or identifier " ^ s ^ " for writing") p
+	| AKNo(s,p) -> typing_error ("Cannot access " ^ s ^ " for writing") p
 	| AKUsingField _ | AKSafeNav _ ->
 	| AKUsingField _ | AKSafeNav _ ->
 		typing_error "Invalid operation" p
 		typing_error "Invalid operation" p
 	| AKExpr { eexpr = TLocal { v_kind = VUser TVOLocalFunction; v_name = name } } ->
 	| AKExpr { eexpr = TLocal { v_kind = VUser TVOLocalFunction; v_name = name } } ->
@@ -649,10 +649,10 @@ let type_assign_op ctx op e1 e2 with_type p =
 		vr#to_texpr e
 		vr#to_texpr e
 	in
 	in
 	(match !type_access_ref ctx (fst e1) (snd e1) (MSet (Some e2)) with_type with
 	(match !type_access_ref ctx (fst e1) (snd e1) (MSet (Some e2)) with_type with
-	| AKNo s ->
+	| AKNo(s,p) ->
 		(* try abstract operator overloading *)
 		(* try abstract operator overloading *)
 		(try type_non_assign_op ctx op e1 e2 true true with_type p
 		(try type_non_assign_op ctx op e1 e2 true true with_type p
-		with Not_found -> typing_error ("Cannot access field or identifier " ^ s ^ " for writing") p
+		with Not_found -> typing_error ("Cannot access " ^ s ^ " for writing") p
 		)
 		)
 	| AKUsingField _ | AKSafeNav _ ->
 	| AKUsingField _ | AKSafeNav _ ->
 		typing_error "Invalid operation" p
 		typing_error "Invalid operation" p
@@ -839,8 +839,8 @@ let type_unop ctx op flag e with_type p =
 		in
 		in
 		let access_set = !type_access_ref ctx (fst e) (snd e) (MSet None) WithType.value (* WITHTYPETODO *) in
 		let access_set = !type_access_ref ctx (fst e) (snd e) (MSet None) WithType.value (* WITHTYPETODO *) in
 		match access_set with
 		match access_set with
-		| AKNo name ->
-			typing_error ("The field or identifier " ^ name ^ " is not accessible for writing") p
+		| AKNo(s,p) ->
+			typing_error ("Cannot access " ^ s ^ " for writing") p
 		| AKExpr e ->
 		| AKExpr e ->
 			find_overload_or_make e
 			find_overload_or_make e
 		| AKField fa ->
 		| AKField fa ->

+ 5 - 5
src/typing/typer.ml

@@ -272,12 +272,12 @@ let rec type_ident_raise ctx i p mode with_type =
 		if mode = MGet then
 		if mode = MGet then
 			AKExpr (mk (TConst (TBool true)) ctx.t.tbool p)
 			AKExpr (mk (TConst (TBool true)) ctx.t.tbool p)
 		else
 		else
-			AKNo i
+			AKNo(i,p)
 	| "false" ->
 	| "false" ->
 		if mode = MGet then
 		if mode = MGet then
 			AKExpr (mk (TConst (TBool false)) ctx.t.tbool p)
 			AKExpr (mk (TConst (TBool false)) ctx.t.tbool p)
 		else
 		else
-			AKNo i
+			AKNo(i,p)
 	| "this" ->
 	| "this" ->
 		if is_set then add_class_field_flag ctx.curfield CfModifiesThis;
 		if is_set then add_class_field_flag ctx.curfield CfModifiesThis;
 		(match mode, ctx.curclass.cl_kind with
 		(match mode, ctx.curclass.cl_kind with
@@ -286,7 +286,7 @@ let rec type_ident_raise ctx i p mode with_type =
 				typing_error "Abstract 'this' value can only be modified inside an inline function" p;
 				typing_error "Abstract 'this' value can only be modified inside an inline function" p;
 			AKExpr (get_this ctx p)
 			AKExpr (get_this ctx p)
 		| (MCall _, KAbstractImpl _) | (MGet, _)-> AKExpr(get_this ctx p)
 		| (MCall _, KAbstractImpl _) | (MGet, _)-> AKExpr(get_this ctx p)
-		| _ -> AKNo i)
+		| _ -> AKNo(i,p))
 	| "abstract" ->
 	| "abstract" ->
 		begin match mode, ctx.curclass.cl_kind with
 		begin match mode, ctx.curclass.cl_kind with
 			| MSet _, KAbstractImpl ab -> typing_error "Property 'abstract' is read-only" p;
 			| MSet _, KAbstractImpl ab -> typing_error "Property 'abstract' is read-only" p;
@@ -335,7 +335,7 @@ let rec type_ident_raise ctx i p mode with_type =
 				AKExpr (null t p)
 				AKExpr (null t p)
 			end
 			end
 		end else
 		end else
-			AKNo i
+			AKNo(i,p)
 	| _ ->
 	| _ ->
 	try
 	try
 		let v = PMap.find i ctx.locals in
 		let v = PMap.find i ctx.locals in
@@ -395,7 +395,7 @@ let rec type_ident_raise ctx i p mode with_type =
 		)
 		)
 	with Not_found -> try
 	with Not_found -> try
 		let wrap e = if is_set then
 		let wrap e = if is_set then
-				AKNo i
+				AKNo(i,p)
 			else
 			else
 				AKExpr e
 				AKExpr e
 		in
 		in

+ 2 - 2
src/typing/typerBase.ml

@@ -6,7 +6,7 @@ open Error
 
 
 type access_kind =
 type access_kind =
 	(* Access is not possible or allowed. *)
 	(* Access is not possible or allowed. *)
-	| AKNo of string
+	| AKNo of placed_name
 	(* Access on arbitrary expression. *)
 	(* Access on arbitrary expression. *)
 	| AKExpr of texpr
 	| AKExpr of texpr
 	(* Safe navigation access chain *)
 	(* Safe navigation access chain *)
@@ -209,7 +209,7 @@ let rec s_access_kind acc =
 	let st = s_type (print_context()) in
 	let st = s_type (print_context()) in
 	let se = s_expr_pretty true "" false st in
 	let se = s_expr_pretty true "" false st in
 	match acc with
 	match acc with
-	| AKNo s -> "AKNo " ^ s
+	| AKNo(s,_) -> "AKNo " ^ s
 	| AKExpr e -> "AKExpr " ^ (se e)
 	| AKExpr e -> "AKExpr " ^ (se e)
 	| AKSafeNav sn -> Printf.sprintf  "AKSafeNav(%s)" (s_safe_nav_access sn)
 	| AKSafeNav sn -> Printf.sprintf  "AKSafeNav(%s)" (s_safe_nav_access sn)
 	| AKField fa -> Printf.sprintf "AKField(%s)" (s_field_access "" fa)
 	| AKField fa -> Printf.sprintf "AKField(%s)" (s_field_access "" fa)

+ 1 - 1
tests/misc/projects/Issue10052/compile-fail.hxml.stderr

@@ -1 +1 @@
-Main.hx:20: characters 3-37 : Cannot access field or identifier position for writing
+Main.hx:20: characters 10-18 : Cannot access position for writing

+ 4 - 4
tests/misc/projects/Issue10845/compile-fail.hxml.stderr

@@ -1,10 +1,10 @@
-Main.hx:21: characters 3-10 : The field or identifier field is not accessible for writing
+Main.hx:21: characters 3-8 : Cannot access field for writing
 Main.hx:22: characters 3-10 : Cannot modify abstract value of final local
 Main.hx:22: characters 3-10 : Cannot modify abstract value of final local
-Main.hx:24: characters 3-13 : Cannot modify abstract value of final field
+Main.hx:24: characters 3-8 : Cannot modify abstract value of final field
 Main.hx:25: characters 3-13 : Cannot modify abstract value of final local
 Main.hx:25: characters 3-13 : Cannot modify abstract value of final local
-Main.hx:29: characters 3-10 : The field or identifier field is not accessible for writing
+Main.hx:29: characters 3-8 : Cannot access field for writing
 Main.hx:30: characters 3-8 : Cannot assign to final
 Main.hx:30: characters 3-8 : Cannot assign to final
-Main.hx:32: characters 3-13 : Cannot access field or identifier field for writing
+Main.hx:32: characters 3-8 : Cannot access field for writing
 Main.hx:33: characters 3-8 : Cannot assign to final
 Main.hx:33: characters 3-8 : Cannot assign to final
 Main.hx:37: characters 3-17 : Cannot modify abstract value of final field
 Main.hx:37: characters 3-17 : Cannot modify abstract value of final field
 Main.hx:38: characters 3-17 : Cannot modify abstract value of final local
 Main.hx:38: characters 3-17 : Cannot modify abstract value of final local

+ 5 - 0
tests/misc/projects/Issue10880/Main.hx

@@ -0,0 +1,5 @@
+class Main {
+	static function main() {
+		trace(null());
+	}
+}

+ 2 - 0
tests/misc/projects/Issue10880/compile-fail.hxml

@@ -0,0 +1,2 @@
+--main Main
+--interp

+ 2 - 0
tests/misc/projects/Issue10880/compile-fail.hxml.stderr

@@ -0,0 +1,2 @@
+Main.hx:3: characters 9-13 : null cannot be called
+Main.hx:3: characters 9-13 : ... For function argument 'v'

+ 1 - 1
tests/misc/projects/Issue6584/compile2-fail.hxml.stderr

@@ -1 +1 @@
-Main2.hx:5: characters 3-8 : Cannot access field or identifier v for writing
+Main2.hx:5: characters 3-4 : Cannot access v for writing

+ 1 - 1
tests/misc/projects/Issue6584/compile3-fail.hxml.stderr

@@ -1 +1 @@
-Main3.hx:9: characters 3-8 : Cannot access field or identifier v for writing
+Main3.hx:9: characters 3-4 : Cannot access v for writing

+ 1 - 1
tests/misc/projects/Issue9378/compile-fail.hxml.stderr

@@ -1 +1 @@
-Main.hx:10: characters 3-17 : Cannot access field or identifier x for writing
+Main.hx:10: characters 11-12 : Cannot access x for writing

+ 12 - 3
tests/misc/src/Main.hx

@@ -1,3 +1,4 @@
+import haxe.io.Bytes;
 import haxe.macro.Compiler;
 import haxe.macro.Compiler;
 import sys.FileSystem;
 import sys.FileSystem;
 import sys.io.File;
 import sys.io.File;
@@ -147,9 +148,17 @@ class Main {
 				.join('\n');
 				.join('\n');
 
 
 			if (content != expected) {
 			if (content != expected) {
-				println('Expected:\n$expected');
-				println('Actual:\n$content');
-				println('');
+				final a = new diff.FileData(Bytes.ofString(expected), "expected", Date.now());
+				final b = new diff.FileData(Bytes.ofString(content), "actual", Date.now());
+				var ctx:diff.Context = {
+					file1: a,
+					file2: b,
+					context: 10
+				}
+				final script = diff.Analyze.diff2Files(ctx);
+				var diff = diff.Printer.printUnidiff(ctx, script);
+				diff = diff.split("\n").slice(3).join("\n");
+				println(diff);
 				return false;
 				return false;
 			}
 			}
 		}
 		}

+ 132 - 0
tests/misc/src/diff/Analyze.hx

@@ -0,0 +1,132 @@
+package diff;
+
+import haxe.ds.Vector;
+import diff.Diffseq;
+
+class Analyze {
+	static function discardConfusingLines(file1:FileData, file2:FileData) {}
+
+	static function analyzeHunk(ctx:Context, script:Array<Change>) {
+		var showFrom = 0;
+		var showTo = 0;
+
+		final first0 = script[0].line0;
+		final first1 = script[0].line1;
+
+		var l0 = 0;
+		var l1 = 0;
+		for (next in script) {
+			l0 = next.line0 + next.deleted - 1;
+			l1 = next.line1 + next.inserted - 1;
+			showFrom += next.deleted;
+			showTo += next.inserted;
+		}
+
+		return {
+			first0: first0,
+			first1: first1,
+			last0: l0,
+			last1: l1,
+			showFrom: showFrom > 0,
+			showTo: showTo > 0
+		}
+	}
+
+	static function buildScript(ctx:Context) {
+		var currentChange = null;
+		var firstChange = null;
+		function addChange(line0:Int, line1:Int, deleted:Int, inserted:Int) {
+			var change:Change = {
+				line0: line0,
+				line1: line1,
+				deleted: deleted,
+				inserted: inserted
+			};
+			if (currentChange == null) {
+				currentChange = change;
+				firstChange = change;
+			} else {
+				currentChange.next = change;
+				currentChange = change;
+			}
+		};
+		final changed0 = ctx.file1.changed;
+		final changed1 = ctx.file2.changed;
+		final len0 = ctx.file1.lines.length;
+		final len1 = ctx.file2.lines.length;
+		var i0 = 0;
+		var i1 = 0;
+		while (i0 < len0 || i1 < len1) {
+			if (changed0[i0] | changed1[i1] != 0) {
+				final line0 = i0;
+				final line1 = i1;
+				while (changed0[i0] != 0) {
+					++i0;
+				}
+				while (changed1[i1] != 0) {
+					++i1;
+				}
+				addChange(line0, line1, i0 - line0, i1 - line1);
+			}
+			i0++;
+			i1++;
+		}
+		return firstChange;
+	}
+
+	static function printNormalHunk(ctx:Context, script:Array<Change>) {
+		// final changes = analyzeHunk(ctx, script);
+		var buf = new StringBuf();
+		for (change in script) {
+			if (change.deleted > 0) {
+				for (i in change.line0...change.line0 + change.deleted) {
+					final line = ctx.file1.lines[i];
+					buf.add("< ");
+					buf.add(ctx.file1.data.sub(line.pos, line.length));
+					buf.addChar("\n".code);
+				}
+			}
+			buf.add("---\n");
+			if (change.inserted > 0) {
+				for (i in change.line1...change.line1 + change.inserted) {
+					final line = ctx.file2.lines[i];
+					buf.add("> ");
+					buf.add(ctx.file2.data.sub(line.pos, line.length).toString());
+					buf.addChar("\n".code);
+				}
+			}
+		}
+		return buf.toString();
+	}
+
+	static public function diff2Files(ctx:Context) {
+		Io.readFiles(ctx);
+		final flagSpace = new Vector(ctx.file1.lines.length + ctx.file2.lines.length + 4);
+		for (i in 0...flagSpace.length) {
+			flagSpace[i] = 0;
+		}
+		ctx.file1.changed = new IndexVector(flagSpace, 1);
+		ctx.file2.changed = new IndexVector(flagSpace, ctx.file1.lines.length + 3);
+		// TODO: discardConfusingLines
+		final diags = ctx.file1.equivs.length + ctx.file2.equivs.length + 3;
+		var fdiag = new IndexVector(new Vector(diags * 2), 0);
+		var bdiag = fdiag + diags;
+		fdiag += ctx.file2.equivs.length + 1;
+		bdiag += ctx.file2.equivs.length + 1;
+		var dCtx:DiffseqContext = {
+			xvec: ctx.file1.equivs,
+			yvec: ctx.file2.equivs,
+			fdiag: fdiag,
+			bdiag: bdiag,
+			NOTE_INSERT: (d) -> {
+				ctx.file2.markChange(d);
+			},
+			NOTE_DELETE: (d) -> {
+				ctx.file1.markChange(d);
+			}
+		}
+		Diffseq.compareseq(0, ctx.file1.equivs.length, 0, ctx.file2.equivs.length, dCtx);
+		// TODO: shiftBoundaries
+		return buildScript(ctx);
+	}
+}

+ 15 - 0
tests/misc/src/diff/Change.hx

@@ -0,0 +1,15 @@
+package diff;
+
+@:structInit
+class Change {
+	@:optional
+	public var next:Null<Change>;
+	public var inserted:Int;
+	public var deleted:Int;
+	public var line0:Int;
+	public var line1:Int;
+
+	public function toString() {
+		return '[CHANGE inserted: $inserted, deleted: $deleted, line0: $line0, line1: $line1]';
+	}
+}

+ 8 - 0
tests/misc/src/diff/Context.hx

@@ -0,0 +1,8 @@
+package diff;
+
+@:structInit
+class Context {
+	public final file1:FileData;
+	public final file2:FileData;
+	public var context:Int;
+}

+ 176 - 0
tests/misc/src/diff/Diffseq.hx

@@ -0,0 +1,176 @@
+package diff;
+
+import haxe.ds.Vector;
+
+@:structInit
+private class Parition {
+	public var xmid:Int;
+	public var ymid:Int;
+
+	function toString() {
+		return '[Partition xmid: $xmid, ymid: $ymid]';
+	}
+}
+
+final OFFSET_MAX = 1 << 30;
+
+@:structInit
+class DiffseqContext {
+	public final xvec:Vector<Int>;
+	public final yvec:Vector<Int>;
+	public final fdiag:IndexVector;
+	public final bdiag:IndexVector;
+	public final NOTE_DELETE:(Int) -> Void;
+	public final NOTE_INSERT:(Int) -> Void;
+}
+
+class Diffseq {
+	static public function diag(xoff:Int, xlim:Int, yoff:Int, ylim:Int, ctxt:DiffseqContext):Parition {
+		var fd = ctxt.fdiag;
+		var bd = ctxt.bdiag;
+		var xv = ctxt.xvec;
+		var yv = ctxt.yvec;
+		inline function XREF_YREF_EQUAL(x, y) {
+			return xv[x] == yv[y];
+		}
+		final dmin = xoff - ylim;
+		final dmax = xlim - yoff;
+		final fmid = xoff - yoff;
+		final bmid = xlim - ylim;
+		var fmin = fmid;
+		var fmax = fmid;
+		var bmin = bmid;
+		var bmax = bmid;
+		var odd = ((fmid - bmid) & 1) != 0;
+		fd[fmid] = xoff;
+		bd[bmid] = xlim;
+		while (true) {
+			var d;
+			if (fmin > dmin) {
+				fd[--fmin - 1] = -1;
+			} else {
+				++fmin;
+			}
+			if (fmax < dmax) {
+				fd[++fmax + 1] = -1;
+			} else {
+				--fmax;
+			}
+			d = fmax;
+			while (d >= fmin) {
+				var tlo = fd[d - 1];
+				var thi = fd[d + 1];
+				var x0 = tlo < thi ? thi : tlo + 1;
+				var x = x0;
+				var y = x0 - d;
+				while (x < xlim && y < ylim && XREF_YREF_EQUAL(x, y)) {
+					x++;
+					y++;
+				}
+				fd[d] = x;
+				if (odd && bmin <= d && d <= bmax && bd[d] <= x) {
+					return {
+						xmid: x,
+						ymid: y
+					}
+				}
+				d -= 2;
+			}
+
+			if (bmin > dmin) {
+				bd[--bmin - 1] = OFFSET_MAX;
+			} else {
+				++bmin;
+			}
+			if (bmax < dmax) {
+				bd[++bmax + 1] = OFFSET_MAX;
+			} else {
+				--bmax;
+			}
+			d = bmax;
+			while (d >= bmin) {
+				var tlo = bd[d - 1];
+				var thi = bd[d + 1];
+				var x0 = tlo < thi ? tlo : thi - 1;
+				var x = x0;
+				var y = x0 - d;
+				while (xoff < x && yoff < y && XREF_YREF_EQUAL(x - 1, y - 1)) {
+					x--;
+					y--;
+				}
+				bd[d] = x;
+				if (!odd && fmin <= d && d <= fmax && x <= fd[d]) {
+					return {
+						xmid: x,
+						ymid: y
+					}
+				}
+				d -= 2;
+			}
+		}
+	}
+
+	static public function compareseq(xoff:Int, xlim:Int, yoff:Int, ylim:Int, ctxt:DiffseqContext) {
+		final xv = ctxt.xvec;
+		final yv = ctxt.yvec;
+		inline function XREF_YREF_EQUAL(x, y) {
+			return xv[x] == yv[y];
+		}
+		while (true) {
+			while (xoff < xlim && yoff < ylim && XREF_YREF_EQUAL(xoff, yoff)) {
+				xoff++;
+				yoff++;
+			}
+
+			while (xoff < xlim && yoff < ylim && XREF_YREF_EQUAL(xlim - 1, ylim - 1)) {
+				xlim--;
+				ylim--;
+			}
+
+			if (xoff == xlim) {
+				while (yoff < ylim) {
+					ctxt.NOTE_INSERT(yoff);
+					yoff++;
+				}
+				break;
+			}
+			if (yoff == ylim) {
+				while (xoff < xlim) {
+					ctxt.NOTE_DELETE(xoff);
+					xoff++;
+				}
+				break;
+			}
+
+			var part = diag(xoff, xlim, yoff, ylim, ctxt);
+
+			var xoff1, xlim1, yoff1, ylim1, xoff2, xlim2, yoff2, ylim2;
+			if ((xlim + ylim) - (part.xmid + part.ymid) < (part.xmid + part.ymid) - (xoff + yoff)) {
+				xoff1 = part.xmid;
+				xlim1 = xlim;
+				yoff1 = part.ymid;
+				ylim1 = ylim;
+				xoff2 = xoff;
+				xlim2 = part.xmid;
+				yoff2 = yoff;
+				ylim2 = part.ymid;
+			} else {
+				xoff1 = xoff;
+				xlim1 = part.xmid;
+				yoff1 = yoff;
+				ylim1 = part.ymid;
+				xoff2 = part.xmid;
+				xlim2 = xlim;
+				yoff2 = part.ymid;
+				ylim2 = ylim;
+			}
+			compareseq(xoff1, xlim1, yoff1, ylim1, ctxt);
+
+			xoff = xoff2;
+			xlim = xlim2;
+			yoff = yoff2;
+			ylim = ylim2;
+		}
+		return false;
+	}
+}

+ 73 - 0
tests/misc/src/diff/FileData.hx

@@ -0,0 +1,73 @@
+package diff;
+
+import haxe.io.Bytes;
+import haxe.ds.Vector;
+
+@:structInit
+private class Line {
+	public final pos:Int;
+	public final length:Int;
+}
+
+class FileData {
+	public final data:Bytes;
+	public final label:String;
+	public final mtime:Date;
+	public var prefixEnd:Int;
+	public var prefixNewlines:Int;
+	public var suffixBegin:Int;
+	public var equivs:Vector<Int>;
+	public final equivCount:Array<Null<Int>>;
+	public final lines:Array<Line>;
+
+	public var changed:IndexVector;
+
+	public function new(data:Bytes, label:String, mtime:Date) {
+		this.label = label;
+		this.mtime = mtime;
+		this.data = data;
+		prefixEnd = 0;
+		suffixBegin = data.length;
+		equivCount = [];
+		lines = [];
+	}
+
+	#if sys
+	static public function loadFile(path:String) {
+		final stat = sys.FileSystem.stat(path);
+		return new FileData(sys.io.File.getBytes(path), path, stat.mtime);
+	}
+	#end
+
+	public function addLine(pos:Int, length:Int) {
+		lines.push({pos: pos, length: length});
+	}
+
+	public function markChange(line:Int) {
+		changed[line] = 1;
+	}
+
+	public function finishLineProcessing(equivs:Array<Int>) {
+		this.equivs = Vector.fromArrayCopy(equivs);
+	}
+
+	public function increaseEquivCount(index:Int) {
+		if (equivCount[index] == null) {
+			equivCount[index] = 1;
+		} else {
+			equivCount[index]++;
+		}
+	}
+
+	public function dump() {
+		var buf = new StringBuf();
+		inline function add(s:String) {
+			buf.add(s);
+		}
+		add('  prefixEnd: ${prefixEnd}\n');
+		add('suffixBegin: ${suffixBegin}\n');
+		add('     equivs: ${equivs}\n');
+		add(' equivCount: ${equivCount}\n');
+		return buf.toString();
+	}
+}

+ 42 - 0
tests/misc/src/diff/IndexVector.hx

@@ -0,0 +1,42 @@
+package diff;
+
+import haxe.ds.Vector;
+
+@:structInit
+private class IV {
+	public final v:Vector<Int>;
+	public var index:Int;
+}
+
+abstract IndexVector(IV) {
+	public inline function new(v:Vector<Int>, index:Int) {
+		this = {
+			v: v,
+			index: index
+		}
+	}
+
+	@:op(A += B)
+	inline function addAssign(rhs:Int) {
+		this.index += rhs;
+	}
+
+	@:op(A + B)
+	inline function add(delta:Int) {
+		return new IndexVector(this.v, this.index + delta);
+	}
+
+	@:op([])
+	inline function read(delta:Int) {
+		return this.v[this.index + delta];
+	}
+
+	@:op([])
+	inline function write(delta:Int, v:Int) {
+		return this.v[this.index + delta] = v;
+	}
+
+	public inline function toString() {
+		return '[IV +${this.index}]';
+	}
+}

+ 159 - 0
tests/misc/src/diff/Io.hx

@@ -0,0 +1,159 @@
+package diff;
+
+import haxe.io.Bytes;
+import haxe.ds.IntMap;
+import haxe.ds.Vector;
+
+using diff.Io.BytesTools;
+
+private typedef Hash = Int;
+
+class BytesTools {
+	static public function compareSub(bytes1:Bytes, pos1:Int, bytes2:Bytes, pos2:Int, length:Int) {
+		for (i in 0...length) {
+			if (bytes1.get(pos1 + i) != bytes2.get(pos2 + i)) {
+				return false;
+			}
+		}
+		return true;
+	}
+}
+
+@:structInit
+private class Equivclass {
+	public final hash:Hash;
+	public final bytes:Bytes; // This is the complete bytes, not the sub-range
+	public final pos:Int;
+	public final length:Int;
+}
+
+private class IoContext {
+	final equivLut:IntMap<Array<Int>>;
+	final equivs:Array<Equivclass>;
+
+	public function new() {
+		equivLut = new IntMap();
+		equivs = [];
+	}
+
+	public function addEquiv(equiv:Equivclass) {
+		return equivs.push(equiv) - 1;
+	}
+
+	public function getEquiv(i:Int) {
+		return equivs[i];
+	}
+
+	public function lookup(hash:Hash) {
+		var a = equivLut.get(hash);
+		if (a == null) {
+			a = [];
+			equivLut.set(hash, a);
+		}
+		return a;
+	}
+}
+
+class Io {
+	static inline function ROL(v, n)
+		return ((v) << (n) | (v) >> (32 - (n)));
+
+	static inline function HASH(h, c)
+		return ((c) + ROL(h, 7));
+
+	static public function findIdenticalEnds(ctx:Context) {
+		var w1 = 0;
+		final file1 = ctx.file1;
+		final file2 = ctx.file2;
+		final len1 = file1.data.length;
+		final len2 = file2.data.length;
+		final min = len1 < len2 ? len1 : len2;
+		var newlineCount = 0;
+		final ringMod = ctx.context + 1;
+		final newlineRing = new Vector(ringMod);
+		var c;
+		while (w1 < min && (c = file1.data.get(w1)) == file2.data.get(w1)) {
+			if (c == '\n'.code) {
+				newlineRing[newlineCount++ % ringMod] = w1;
+			}
+			++w1;
+		}
+		if (newlineCount < ringMod) {
+			file1.prefixEnd = 0;
+			file2.prefixEnd = 0;
+			newlineCount = 0;
+		} else {
+			file1.prefixEnd = newlineRing[newlineCount % ringMod] + 1;
+			file2.prefixEnd = file1.prefixEnd;
+			newlineCount -= ringMod - 1;
+		}
+		file1.prefixNewlines = file2.prefixNewlines = newlineCount;
+
+		w1 = len1 - 1;
+		var w2 = len2 - 1;
+		newlineCount = 0;
+		while (w1 > file1.prefixEnd && w2 > file2.prefixEnd && (c = file1.data.get(w1)) == file2.data.get(w2)) {
+			if (c == '\n'.code) {
+				newlineRing[newlineCount++ % ringMod] = w1;
+			}
+			w1--;
+			w2--;
+		}
+		if (newlineCount < ringMod) {
+			file1.suffixBegin = len1 - 1;
+			file2.suffixBegin = len2 - 1;
+		} else {
+			file1.suffixBegin = newlineRing[newlineCount % ringMod] + 1;
+			file2.suffixBegin = file1.suffixBegin + (len2 - len1);
+		}
+	}
+
+	static public function findAndHashEachLine(ctx:IoContext, file:FileData) {
+		var p = file.prefixEnd;
+		var c = 0;
+		final cureqs = [];
+		while (p < file.suffixBegin) {
+			final ip = p;
+			var h = 0;
+			// TODO: This has a big switch in io.c that depends on the whitespace policy
+			while ((c = file.data.get(p++)) != '\n'.code) {
+				h = HASH(h, c);
+				if (p == file.data.length) {
+					p++;
+					break;
+				}
+			}
+			final length = p - ip - 1;
+			final a = ctx.lookup(h);
+			var equivIndex = -1;
+			for (i in a) {
+				final equiv = ctx.getEquiv(i);
+				if (equiv.hash == h && equiv.length == length && file.data.compareSub(ip, equiv.bytes, equiv.pos, length)) {
+					equivIndex = i;
+					break;
+				}
+			}
+			if (equivIndex < 0) {
+				final equiv:Equivclass = {
+					hash: h,
+					bytes: file.data,
+					pos: ip,
+					length: length
+				}
+				equivIndex = ctx.addEquiv(equiv);
+				a.push(equivIndex);
+			}
+			cureqs.push(equivIndex);
+			file.increaseEquivCount(equivIndex);
+			file.addLine(ip, length);
+		}
+		file.finishLineProcessing(cureqs);
+	}
+
+	static public function readFiles(ctx:Context) {
+		findIdenticalEnds(ctx);
+		var ioctx = new IoContext();
+		findAndHashEachLine(ioctx, ctx.file1);
+		findAndHashEachLine(ioctx, ctx.file2);
+	}
+}

+ 122 - 0
tests/misc/src/diff/Printer.hx

@@ -0,0 +1,122 @@
+package diff;
+
+class Printer {
+	static function printContextLabel(mark:String, file:FileData) {
+		var buf = new StringBuf();
+		buf.add(mark);
+		buf.add(" ");
+		buf.add(file.label);
+		buf.add("\t");
+		buf.add(file.mtime.toString());
+		buf.add("\n");
+		return buf.toString();
+	}
+
+	static function printContextHeader(ctx:Context) {
+		var buf = new StringBuf();
+		buf.add(printContextLabel("---", ctx.file1));
+		buf.add(printContextLabel("+++", ctx.file2));
+		return buf.toString();
+	}
+
+	static function translateLineNumber(file:FileData, a:Int) {
+		return file.prefixNewlines + a + 1;
+	}
+
+	static function printUnidiffNumberRange(file:FileData, a:Int, b:Int) {
+		var transA = translateLineNumber(file, a - 1) + 1;
+		var transB = translateLineNumber(file, b + 1) - 1;
+
+		if (transB == transA) {
+			return '$transB,0';
+		} else if (transB - transA == 1) {
+			return '1';
+		} else {
+			return '$transA,${transB - transA}';
+		}
+	}
+
+	static function printUnidiffHunk(ctx:Context, change:Change) {
+		var buf = new StringBuf();
+		var from0 = change.line0 - ctx.context;
+		if (from0 < 0) {
+			from0 = 0;
+		}
+		var from1 = change.line1 - ctx.context;
+		if (from1 < 0) {
+			from1 = 0;
+		}
+		final last = {
+			var next = change;
+			while (next.next != null) {
+				next = next.next;
+			}
+			next;
+		}
+		var to0 = last.line0 + last.deleted + ctx.context;
+		if (to0 >= ctx.file1.lines.length) {
+			to0 = ctx.file1.lines.length;
+		}
+		var to1 = last.line1 + last.inserted + ctx.context;
+		if (to1 >= ctx.file2.lines.length) {
+			to1 = ctx.file2.lines.length;
+		}
+		buf.add("@@ -");
+		buf.add(printUnidiffNumberRange(ctx.file1, from0, to0));
+		buf.add(" +");
+		buf.add(printUnidiffNumberRange(ctx.file2, from1, to1));
+		buf.add(" @@\n");
+
+		function printLine(file:FileData, prefix:String, i:Int) {
+			final line = file.lines[i];
+			buf.add(prefix);
+			buf.add(file.data.sub(line.pos, line.length).toString());
+			buf.addChar("\n".code);
+		}
+		var i = from0;
+		var j = from1;
+		while (i < to0 || j < to1) {
+			if (change == null || i < change.line0) {
+				printLine(ctx.file1, " ", i);
+				++i;
+				++j;
+			} else {
+				for (_ in 0...change.deleted) {
+					printLine(ctx.file1, "-", i++);
+				}
+				for (_ in 0...change.inserted) {
+					printLine(ctx.file2, "+", j++);
+				}
+				change = change.next;
+			}
+		}
+		return buf.toString();
+	}
+
+	static public function findHunk(ctx:Context, change:Change) {
+		final threshold = ctx.context * 2 + 1;
+		var previousChange = change;
+		while ((change = change.next) != null) {
+			if (change.line0 - (previousChange.line0 + previousChange.deleted) < threshold) {
+				previousChange = change;
+				continue;
+			}
+			break;
+		}
+		return previousChange;
+	}
+
+	static public function printUnidiff(ctx:Context, change:Change) {
+		var buf = new StringBuf();
+		buf.add(printContextHeader(ctx));
+		var next = change;
+		while (next != null) {
+			var end = findHunk(ctx, next);
+			final cur = next;
+			next = end.next;
+			end.next = null;
+			buf.add(printUnidiffHunk(ctx, cur));
+		}
+		return buf.toString();
+	}
+}