浏览代码

Sourcemap support for php7 (#6052)

* sourcemap generator draft

* trying to update Makefile for sourcemaps

* fix sourcemap_file#new_line

* added sourcemap generator to genphp7 as proof-of-concept

* sourcemap_builder draft

* [php7] refactor write_args

* [php7] use parent_expr instead of expr_hierarchy for cleaner code

* [php7] all output through type_builder#write

* fix line numbers in sourcemap

* fix accumulating sourcemaps of all previousely generated files

* fix line mapping if stack trace does not provide coumn information

* [php7] correctly build sourcemap for closures

* fix typo

* source_map define

* [php7] haxe.CallStack.mapPosition for pretty call stacks
Alexander Kuzmenko 8 年之前
父节点
当前提交
a020136776
共有 6 个文件被更改,包括 403 次插入47 次删除
  1. 6 2
      Makefile
  2. 2 0
      src/context/common.ml
  3. 76 44
      src/generators/genphp7.ml
  4. 302 0
      src/sourcemaps.ml
  5. 2 0
      std/php7/_std/StringBuf.hx
  6. 15 1
      std/php7/_std/haxe/CallStack.hx

+ 6 - 2
Makefile

@@ -67,7 +67,7 @@ RELDIR=../../..
 MODULES=json version globals path context/meta syntax/ast display/displayTypes typing/type typing/error \
 	syntax/lexer context/common generators/genxml \
 	syntax/parser typing/abstract typing/typecore display/display optimization/optimizerTexpr \
-	optimization/optimizer typing/overloads typing/typeload generators/codegen generators/gencommon generators/genas3 \
+	optimization/optimizer typing/overloads typing/typeload sourcemaps generators/codegen generators/gencommon generators/genas3 \
 	generators/gencpp generators/genjs generators/genneko generators/genphp generators/genphp7 generators/genswf9 \
 	generators/genswf generators/genjava generators/gencs generators/genpy macro/macroApi macro/interp generators/hlcode generators/hlopt generators/hlinterp generators/hl2c \
 	generators/genlua \
@@ -164,6 +164,10 @@ src/display/displayTypes.$(MODULE_EXT) : src/globals.$(MODULE_EXT) src/syntax/as
 
 src/display/displayOutput.$(MODULE_EXT): src/globals.$(MODULE_EXT) src/typing/type.$(MODULE_EXT) src/typing/typer.$(MODULE_EXT) src/context/common.$(MODULE_EXT) src/display/display.$(MODULE_EXT)
 
+# sourcemaps
+
+src/sourcemaps.$(MODULE_EXT): src/globals.$(MODULE_EXT) src/context/common.$(MODULE_EXT) src/syntax/lexer.$(MODULE_EXT) src/syntax/ast.$(MODULE_EXT)
+
 # generators
 
 src/generators/codegen.$(MODULE_EXT): src/globals.$(MODULE_EXT) src/typing/type.$(MODULE_EXT) src/generators/genxml.$(MODULE_EXT) src/context/common.$(MODULE_EXT) src/syntax/ast.$(MODULE_EXT)
@@ -192,7 +196,7 @@ src/generators/genswf.$(MODULE_EXT): src/globals.$(MODULE_EXT) src/typing/type.$
 
 src/generators/hlinterp.$(MODULE_EXT): src/context/common.$(MODULE_EXT) src/generators/hlcode.$(MODULE_EXT) src/macro/interp.$(MODULE_EXT) src/generators/hlopt.$(MODULE_EXT) src/macro/macroApi.$(MODULE_EXT)
 
-src/generators/genphp7.$(MODULE_EXT): src/typing/abstract.$(MODULE_EXT) src/globals.$(MODULE_EXT) src/context/meta.$(MODULE_EXT) src/path.$(MODULE_EXT) src/typing/type.$(MODULE_EXT) src/syntax/lexer.$(MODULE_EXT) src/context/common.$(MODULE_EXT) src/generators/codegen.$(MODULE_EXT) src/syntax/ast.$(MODULE_EXT)
+src/generators/genphp7.$(MODULE_EXT): src/typing/abstract.$(MODULE_EXT) src/globals.$(MODULE_EXT) src/context/meta.$(MODULE_EXT) src/path.$(MODULE_EXT) src/typing/type.$(MODULE_EXT) src/syntax/lexer.$(MODULE_EXT) src/context/common.$(MODULE_EXT) src/generators/codegen.$(MODULE_EXT) src/syntax/ast.$(MODULE_EXT) src/sourcemaps.$(MODULE_EXT)
 
 src/generators/hl2c.$(MODULE_EXT): src/generators/hlcode.$(MODULE_EXT) src/context/common.$(MODULE_EXT)
 

+ 2 - 0
src/context/common.ml

@@ -487,6 +487,7 @@ module Define = struct
 		| JsEs
 		| JsUnflatten
 		| JsSourceMap
+		| SourceMap
 		| KeepOldOutput
 		| LoopUnrollMaxCost
 		| LuaVer
@@ -582,6 +583,7 @@ module Define = struct
 		| JsEs -> ("js_es","Generate JS compilant with given ES standard version (default 5)")
 		| JsUnflatten -> ("js_unflatten","Generate nested objects for packages and types")
 		| JsSourceMap -> ("js_source_map","Generate JavaScript source map even in non-debug mode")
+		| SourceMap -> ("source_map","Generate source map for compiled files (Currently supported for php7 only)")
 		| KeepOldOutput -> ("keep_old_output","Keep old source files in the output directory (for C#/Java)")
 		| LoopUnrollMaxCost -> ("loop_unroll_max_cost","Maximum cost (number of expressions * iterations) before loop unrolling is canceled (default 250)")
 		| LuaJit -> ("lua_jit","Enable the jit compiler for lua (version 5.2 only")

+ 76 - 44
src/generators/genphp7.ml

@@ -7,6 +7,7 @@ open Type
 open Common
 open Meta
 open Globals
+open Sourcemaps
 
 let debug = ref false
 (**
@@ -578,14 +579,14 @@ let get_visibility (meta:metadata) = if Meta.has Meta.Protected meta then "prote
 (**
 	Writes arguments list to output buffer
 *)
-let rec write_args buffer arg_writer (args:'a) =
+let rec write_args (str_writer:string->unit) arg_writer (args:'a list) =
 	match args with
 		| [] -> ()
 		| [arg] -> arg_writer arg
 		| arg :: rest ->
 			arg_writer arg;
-			Buffer.add_string buffer ", ";
-			write_args buffer arg_writer rest
+			str_writer ", ";
+			write_args str_writer arg_writer rest
 
 (**
 	Check if specified field is a var with non-constant expression
@@ -1228,6 +1229,16 @@ class virtual type_builder ctx wrapper =
 		val mutable expr_hierarchy : texpr list = []
 		(** Object to collect local vars declarations and usage as we iterate through methods' expressions *)
 		val vars = new local_vars
+		(** Sourcemap generator *)
+		val mutable sourcemap : sourcemap_builder option = None
+		(**
+			Set sourcemap generator
+		*)
+		method set_sourcemap_generator generator = sourcemap <- Some generator
+		(**
+			Get sourcemap generator
+		*)
+		method get_sourcemap_generator = sourcemap
 		(**
 			Get PHP namespace path
 		*)
@@ -1315,7 +1326,6 @@ class virtual type_builder ctx wrapper =
 				(* Boot initialization *)
 				if boot_type_path = self#get_type_path then
 					self#write_statement (boot_class ^ "::__hx__init()");
-				(*let php_class = get_full_type_name ~escape:true ~omit_first_slash:true (add_php_prefix ctx self#get_type_path)*)
 				let haxe_class = match wrapper#get_type_path with (path, name) -> String.concat "." (path @ [name]) in
 				self#write_statement (boot_class ^ "::registerClass(" ^ (self#get_name) ^ "::class, '" ^ haxe_class ^ "')");
 				self#write_rtti_meta;
@@ -1324,6 +1334,7 @@ class virtual type_builder ctx wrapper =
 				if wrapper#needs_initialization && boot_type_path <> self#get_type_path then
 					self#write_statement (self#get_name ^ "::__hx__init()");
 				let body = Buffer.contents buffer in
+				Option.may (fun smap -> smap#rewind) sourcemap;
 				Buffer.clear buffer;
 				self#write_header;
 				self#write "\n";
@@ -1551,7 +1562,13 @@ class virtual type_builder ctx wrapper =
 			Writes specified string to output buffer
 		*)
 		method private write str =
-			Buffer.add_string buffer str
+			Buffer.add_string buffer str;
+			Option.may (fun smap -> smap#insert (SMStr str)) sourcemap;
+		(**
+			Writes specified string to output buffer without affecting sourcemap generator
+		*)
+		method private write_bypassing_sourcemap str =
+			Buffer.add_string buffer str;
 		(**
 			Writes constant double-quoted string to output buffer
 		*)
@@ -1561,23 +1578,22 @@ class virtual type_builder ctx wrapper =
 			Writes fixed amount of empty lines (E.g. between methods)
 		*)
 		method private write_empty_lines =
-			self#write "\n";
-			self#write "\n"
+			self#write "\n\n"
 		(**
 			Writes current indentation to output buffer
 		*)
 		method private write_indentation =
-			Buffer.add_string buffer indentation
+			self#write indentation
 		(**
 			Writes specified line to output buffer and appends \n
 		*)
 		method private write_line line =
-			Buffer.add_string buffer (indentation ^ line ^ "\n")
+			self#write (indentation ^ line ^ "\n")
 		(**
 			Writes specified statement to output buffer and appends ";\n"
 		*)
 		method private write_statement statement =
-			Buffer.add_string buffer (indentation ^ statement ^ ";\n")
+			self#write (indentation ^ statement ^ ";\n")
 		(**
 			Build file header (<?php, namespace and file doc block)
 		*)
@@ -1694,6 +1710,7 @@ class virtual type_builder ctx wrapper =
 		*)
 		method private write_expr (expr:texpr) =
 			expr_hierarchy <- expr :: expr_hierarchy;
+			Option.may (fun smap -> smap#insert (SMPos expr.epos)) sourcemap;
 			(match expr.eexpr with
 				| TConst const -> self#write_expr_const const
 				| TLocal var ->
@@ -1720,7 +1737,7 @@ class virtual type_builder ctx wrapper =
 				| TCall (expr, args) when is_lang_extern expr -> self#write_expr_call_lang_extern expr args
 				| TCall (target, args) when is_sure_var_field_access target -> self#write_expr_call (parenthesis target) args
 				| TCall (target, args) -> self#write_expr_call target args
-				| TNew (_, _, args) when is_string expr -> write_args buffer self#write_expr args
+				| TNew (_, _, args) when is_string expr -> write_args self#write self#write_expr args
 				| TNew (tcls, _, args) -> self#write_expr_new tcls args
 				| TUnop (operation, flag, target_expr) when needs_dereferencing (is_modifying_unop operation) target_expr ->
 					self#write_expr { expr with eexpr = TUnop (operation, flag, self#dereference target_expr) }
@@ -1893,7 +1910,7 @@ class virtual type_builder ctx wrapper =
 		*)
 		method private write_constructor_function_declaration func write_arg =
 			self#write ("function __construct (");
-			write_args buffer write_arg func.tf_args;
+			write_args self#write write_arg func.tf_args;
 			self#write ") {\n";
 			self#indent_more;
 			self#write_instance_initialization;
@@ -1911,7 +1928,7 @@ class virtual type_builder ctx wrapper =
 		method private write_method_function_declaration name func write_arg =
 			let by_ref = if is_ref func.tf_type then "&" else "" in
 			self#write ("function " ^ by_ref ^ name ^ " (");
-			write_args buffer write_arg func.tf_args;
+			write_args self#write write_arg func.tf_args;
 			self#write ") ";
 			self#write_expr (inject_defaults ctx func)
 		(**
@@ -1920,24 +1937,27 @@ class virtual type_builder ctx wrapper =
 		method private write_closure_declaration func write_arg =
 			vars#dive;
 			self#write "function (";
-			write_args buffer write_arg func.tf_args;
+			write_args self#write write_arg func.tf_args;
 			self#write ")";
 			(* Generate closure body to separate buffer *)
 			let original_buffer = buffer in
+			let sm_pointer_before_body = get_sourcemap_pointer sourcemap in
 			buffer <- Buffer.create 256;
 			self#write_expr (inject_defaults ctx func);
 			let body = Buffer.contents buffer in
 			buffer <- original_buffer;
+			set_sourcemap_pointer sourcemap sm_pointer_before_body;
 			(* Capture local vars used in closures *)
 			let used_vars = vars#pop_used in
 			vars#captured used_vars;
 			self#write " ";
 			if List.length used_vars > 0 then begin
 				self#write " use (";
-				write_args buffer (fun name -> self#write ("&$" ^ name)) used_vars;
+				write_args self#write (fun name -> self#write ("&$" ^ name)) used_vars;
 				self#write ") "
 			end;
-			self#write body
+			self#write_bypassing_sourcemap body;
+			Option.may (fun smap -> smap#fast_forward) sourcemap
 		(**
 			Writes TBlock to output buffer
 		*)
@@ -2018,17 +2038,20 @@ class virtual type_builder ctx wrapper =
 				if unset_locals then
 					begin
 						let original_buffer = buffer in
+						let sm_pointer_before_body = get_sourcemap_pointer sourcemap in
 						buffer <- Buffer.create 256;
 						vars#dive;
 						write_exprs();
 						let body = Buffer.contents buffer in
 						buffer <- original_buffer;
+						set_sourcemap_pointer sourcemap sm_pointer_before_body;
 						let locals = vars#pop_captured in
 						if List.length locals > 0 then begin
 							self#write ("unset($" ^ (String.concat ", $" locals) ^ ");\n");
 							self#write_indentation
 						end;
-						self#write body
+						self#write_bypassing_sourcemap body;
+						Option.may (fun smap -> smap#fast_forward) sourcemap
 					end
 				else
 					write_exprs()
@@ -2160,7 +2183,7 @@ class virtual type_builder ctx wrapper =
 							)
 						| "__call__" ->
 							self#write (code ^ "(");
-							write_args buffer self#write_expr args;
+							write_args self#write self#write_expr args;
 							self#write ")"
 						| "__physeq__" ->
 							(match args with
@@ -2194,8 +2217,12 @@ class virtual type_builder ctx wrapper =
 		*)
 		method private write_expr_type (mtype:module_type) =
 			let ttype = type_of_module_type mtype in
-			match expr_hierarchy with
-				| _ :: { eexpr = TField _ } :: _ -> self#write (self#use_t ttype)
+			match self#parent_expr with
+				(* When type is used to access type fields. E.g. `TypeExpr.someField` *)
+				| Some { eexpr = TField (_, FStatic _) }
+				| Some { eexpr = TField (_, FEnum _) } ->
+					self#write (self#use_t ttype)
+				(* Other cases *)
 				| _ ->
 					let class_name =
 						match self#use_t ttype with
@@ -2232,8 +2259,8 @@ class virtual type_builder ctx wrapper =
 				let write_left = match writer with None -> self#write_expr | Some writer -> writer in
 				let write_right = match right_writer with None -> write_left | Some writer -> writer
 				and need_parenthesis =
-					match expr_hierarchy with
-						| _ :: { eexpr = TBinop (parent, _, _) } :: _ -> need_parenthesis_for_binop operation parent
+					match self#parent_expr with
+						| Some { eexpr = TBinop (parent, _, _) } -> need_parenthesis_for_binop operation parent
 						| _ -> false
 				in
 				if need_parenthesis then self#write "(";
@@ -2375,8 +2402,8 @@ class virtual type_builder ctx wrapper =
 						| _ -> write_access "->" (field_name field)
 					)
 				| (_, FStatic (_, ({ cf_kind = Method MethDynamic } as field))) ->
-					(match expr_hierarchy with
-						| _ :: { eexpr = TCall ({ eexpr = TField (e, a) }, _) } :: _ when a == access ->
+					(match self#parent_expr with
+						| Some { eexpr = TCall ({ eexpr = TField (e, a) }, _) } when a == access ->
 							self#write "(";
 							write_access "::" ("$" ^ (field_name field));
 							self#write ")"
@@ -2441,7 +2468,7 @@ class virtual type_builder ctx wrapper =
 			match access with
 				| FInstance (_, _, ({ cf_kind = Method _ } as field)) ->
 					self#write ((self#use hxstring_type_path) ^ "::" ^ (field_name field) ^ "(");
-					write_args buffer self#write_expr (expr :: args);
+					write_args self#write self#write_expr (expr :: args);
 					self#write ")"
 				| _ -> fail self#pos (try assert false with Assert_failure mlpos -> mlpos)
 		(**
@@ -2457,18 +2484,18 @@ class virtual type_builder ctx wrapper =
 					| TTypeExpr _ -> "::"
 					| _ -> "->"
 			in
-			match expr_hierarchy with
-				| _ :: { eexpr = TCall ({ eexpr = TField (e, FStatic (_, f)) }, _) } :: _ when e == expr && f == field ->
+			match self#parent_expr with
+				| Some { eexpr = TCall ({ eexpr = TField (e, FStatic (_, f)) }, _) } when e == expr && f == field ->
 					write_expr ();
 					self#write (operator ^ (field_name field))
 				| _ ->
 					let (args, return_type) = get_function_signature field  in
 					self#write "function(";
-					write_args buffer (self#write_arg true) args;
+					write_args self#write (self#write_arg true) args;
 					self#write ") { return ";
 					write_expr ();
 					self#write (operator ^ (field_name field) ^ "(");
-					write_args buffer (self#write_arg false) args;
+					write_args self#write (self#write_arg false) args;
 					self#write "); }"
 		(**
 			Writes FClosure field access to output buffer
@@ -2578,7 +2605,7 @@ class virtual type_builder ctx wrapper =
 		*)
 		method private write_expr_lang_array_decl args =
 			self#write "[";
-			write_args buffer (fun e -> self#write_expr e) args;
+			write_args self#write (fun e -> self#write_expr e) args;
 			self#write "]"
 		(**
 			Writes a call to instance method (for `php.Syntax.call()`)
@@ -2590,7 +2617,7 @@ class virtual type_builder ctx wrapper =
 					self#write "->{";
 					self#write_expr method_expr;
 					self#write "}(";
-					write_args buffer (fun e -> self#write_expr e) args;
+					write_args self#write (fun e -> self#write_expr e) args;
 					self#write ")"
 				| _ -> fail self#pos (try assert false with Assert_failure mlpos -> mlpos)
 		(**
@@ -2603,7 +2630,7 @@ class virtual type_builder ctx wrapper =
 					self#write "::{";
 					self#write_expr method_expr;
 					self#write "}(";
-					write_args buffer (fun e -> self#write_expr e) args;
+					write_args self#write (fun e -> self#write_expr e) args;
 					self#write ")"
 				| _ -> fail self#pos (try assert false with Assert_failure mlpos -> mlpos)
 		(**
@@ -2665,7 +2692,7 @@ class virtual type_builder ctx wrapper =
 			self#write "new ";
 			self#write_expr class_expr;
 			self#write "(";
-			write_args buffer (fun e -> self#write_expr e) args;
+			write_args self#write (fun e -> self#write_expr e) args;
 			self#write ")"
 		(**
 			Writes native php type conversion to output buffer (e.g. `php.Syntax.int()`)
@@ -2755,7 +2782,7 @@ class virtual type_builder ctx wrapper =
 			if not !no_call then
 				begin
 					self#write "(";
-					write_args buffer self#write_expr args;
+					write_args self#write self#write_expr args;
 					self#write ")"
 				end
 		(**
@@ -2779,7 +2806,7 @@ class virtual type_builder ctx wrapper =
 		method private write_expr_new inst_class args =
 			let needs_php_prefix = not inst_class.cl_extern in
 			self#write ("new " ^ (self#use ~prefix:needs_php_prefix inst_class.cl_path) ^ "(");
-			write_args buffer self#write_expr args;
+			write_args self#write self#write_expr args;
 			self#write ")"
 		(**
 			Writes ternary operator expressions to output buffer
@@ -2955,7 +2982,7 @@ class enum_builder ctx (enm:tenum) =
 			self#write_doc (DocMethod (args, TEnum (enm, []), field.ef_doc));
 			self#write_indentation;
 			self#write ("static public function " ^ name ^ " (");
-			write_args buffer (self#write_arg true) args;
+			write_args self#write (self#write_arg true) args;
 			self#write ") {\n";
 			self#indent_more;
 			self#write_indentation;
@@ -2965,7 +2992,7 @@ class enum_builder ctx (enm:tenum) =
 				| [] -> self#write ((self#use hxenum_type_path) ^ "::singleton(static::class, '" ^ name ^ "', " ^ index_str ^")")
 				| args ->
 					self#write ("new " ^ self#get_name ^ "('" ^ name ^ "', " ^ index_str ^", [");
-					write_args buffer (fun (name, _, _) -> self#write ("$" ^ name)) args;
+					write_args self#write (fun (name, _, _) -> self#write ("$" ^ name)) args;
 					self#write "])"
 			);
 			self#write ";\n";
@@ -3370,7 +3397,7 @@ class class_builder ctx (cls:tclass) =
 			match field.cf_expr with
 				| None ->
 					self#write ("function " ^ (field_name field) ^ " (");
-					write_args buffer (self#write_arg true) args;
+					write_args self#write (self#write_arg true) args;
 					self#write ")";
 					self#write " ;\n"
 				| Some { eexpr = TFunction fn } ->
@@ -3393,11 +3420,11 @@ class class_builder ctx (cls:tclass) =
 			(match field.cf_expr with
 				| None -> (* interface *)
 					self#write " (";
-					write_args buffer (self#write_arg true) args;
+					write_args self#write (self#write_arg true) args;
 					self#write ");\n";
 				| Some { eexpr = TFunction fn } -> (* normal class *)
 					self#write " (";
-					write_args buffer self#write_function_arg fn.tf_args;
+					write_args self#write self#write_function_arg fn.tf_args;
 					self#write ")\n";
 					self#write_line "{";
 					self#indent_more;
@@ -3485,13 +3512,18 @@ class generator (com:context) =
 			Generates php file for specified type
 		*)
 		method generate (builder:type_builder) =
-			let contents = builder#get_contents
-			and namespace = builder#get_namespace
+			let namespace = builder#get_namespace
 			and name = builder#get_name in
 			let filename = (create_dir_recursive (build_dir :: namespace)) ^ "/" ^ name ^ ".php" in
 			let channel = open_out filename in
-			output_string channel contents;
+			if Common.defined com Define.SourceMap then
+				builder#set_sourcemap_generator (new sourcemap_builder filename);
+			output_string channel builder#get_contents;
 			close_out channel;
+			(match builder#get_sourcemap_generator with
+				| Some smap -> smap#generate com
+				| None -> ()
+			);
 			if builder#get_type_path = boot_type_path then
 				boot <- Some (builder, filename)
 			else if builder#has_magic_init then
@@ -3501,7 +3533,7 @@ class generator (com:context) =
 		*)
 		method finalize : unit =
 			self#generate_magic_init;
-			self#generate_entry_point
+			self#generate_entry_point;
 		(**
 			Generates calls to static __init__ methods in Boot.php
 		*)

+ 302 - 0
src/sourcemaps.ml

@@ -0,0 +1,302 @@
+
+open Globals
+open Ast
+open Lexer
+open Common
+
+(**
+	Characters used for base64 VLQ encoding
+*)
+let chars = [|
+	'A';'B';'C';'D';'E';'F';'G';'H';'I';'J';'K';'L';'M';'N';'O';'P';
+	'Q';'R';'S';'T';'U';'V';'W';'X';'Y';'Z';'a';'b';'c';'d';'e';'f';
+	'g';'h';'i';'j';'k';'l';'m';'n';'o';'p';'q';'r';'s';'t';'u';'v';
+	'w';'x';'y';'z';'0';'1';'2';'3';'4';'5';'6';'7';'8';'9';'+';'/'
+|]
+
+(**
+	Encode an integer in range 0...63 (including 63)
+*)
+let encode_digit digit = Array.unsafe_get chars digit
+
+(**
+	Move the sign bit to the least significant bit.
+	E.g.:
+		1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
+		2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
+*)
+let to_vlq number =
+	if number < 0 then
+		((-number) lsl 1) + 1
+	else
+		number lsl 1
+
+(**
+	Writes sourcemap for a signle `generated_file` to disk.
+	If your code generation is straightforward, you can use this class directly.
+	Otherwise use `sourcemap_builder` (e.g. if you need to generate some middle parts of a code and then generate the beginning).
+*)
+class sourcemap_writer (generated_file:string) =
+	object (self)
+		(** Output buffer for generated sourcemap *)
+		val buffer = Rbuffer.create 1024
+		(** Source Haxe files referenced by this sourcemap *)
+		val files = DynArray.create()
+		(** Positions of source Haxe files in `files` list *)
+		val files_indexes = Hashtbl.create 100
+		(** Index of a source file referenced in previous `map` call *)
+		val mutable last_src_file = 0
+		(** Zero-based index of a source line referenced in previous `map` call *)
+		val mutable last_src_line = 0
+		(** Zero-based index of a source column in a line referenced in previous `map` call *)
+		val mutable last_src_col = 0
+		(** Zero based index of a column in `generated_file` where last written string ended *)
+		val mutable current_out_col = 0
+		(** `current_out_col` value as it was when previous call to `map` was performed *)
+		val mutable last_out_col = 0
+		(** Indicates whether comma should be written to output buffer on next `map` call *)
+		val mutable print_comma = false
+		(** Last position passed to `map` *)
+		val mutable last_mapped_pos = None
+	(**
+		Map specified haxe position.
+		This method should be called right before an expression in `pos` is writtend to generated file.
+	*)
+	method map pos =
+		last_mapped_pos <- Some pos;
+		let src_file = self#get_file_index pos
+		and src_line, src_col = match (Lexer.find_pos pos) with (line, col) -> (line - 1, col) in
+		if print_comma then
+			Rbuffer.add_char buffer ','
+		else
+			print_comma <- true;
+		(*
+			We need to map the start of each line to the first expression on that line.
+			Otherwise languages, which don't provide column in stack trace, will point one line above the correct line.
+		*)
+		if last_out_col = 0 then
+			self#write_base64_vlq 0
+		else
+			self#write_base64_vlq (current_out_col - last_out_col);
+		self#write_base64_vlq (src_file - last_src_file);
+		self#write_base64_vlq (src_line - last_src_line);
+		self#write_base64_vlq (src_col - last_src_col);
+		last_src_file <- src_file;
+		last_src_line <- src_line;
+		last_src_col <- src_col;
+		last_out_col <- current_out_col
+	(**
+		Should be called every time something is written to `generated_file`
+	*)
+	method string_written str =
+		let length = String.length str in
+		let rec handle_next_new_line previous_index =
+			let next_index = try (String.index_from str (previous_index + 1) '\n') with Not_found -> -1 in
+			if next_index < 0 then
+				if previous_index >= 0 then
+					begin
+						print_comma <- false;
+						current_out_col <- length - previous_index;
+						last_out_col <- 0
+					end
+				else
+					current_out_col <- current_out_col + length
+			else begin
+				Rbuffer.add_char buffer ';';
+				handle_next_new_line next_index
+			end
+		in
+		handle_next_new_line (-1);
+		()
+	(**
+		Write generated map to disk.
+		If `file_name` is not provided then `generated_file` will be used with additional `.map` extension.
+		E.g if `generated_file` is `path/to/file.js`, then sourcemap will be written to `path/to/file.js.map`.
+		This function does not try to create missing directories.
+	*)
+	method generate ?file_name ?source_root com =
+		let file_name = match file_name with Some f -> f | None -> generated_file ^ ".map"
+		and source_root = match source_root with Some r -> r | None -> "" in
+		let channel = open_out file_name in
+		let sources = DynArray.to_list files in
+		let to_url file =
+			ExtString.String.map (fun c -> if c == '\\' then '/' else c) (Path.get_full_path file)
+		in
+		output_string channel "{\n";
+		output_string channel "\"version\":3,\n";
+		output_string channel ("\"file\":\"" ^ (String.concat "\\\\" (ExtString.String.nsplit generated_file "\\")) ^ "\",\n");
+		output_string channel ("\"sourceRoot\":\"" ^ source_root ^ "\",\n");
+		output_string channel ("\"sources\":[" ^
+			(String.concat "," (List.map (fun s -> "\"" ^ to_url s ^ "\"") sources)) ^
+			"],\n");
+		if Common.defined com Define.SourceMapContent then begin
+			output_string channel ("\"sourcesContent\":[" ^
+				(String.concat "," (List.map (fun s -> try "\"" ^ Ast.s_escape (Std.input_file ~bin:true s) ^ "\"" with _ -> "null") sources)) ^
+				"],\n");
+		end;
+		output_string channel "\"names\":[],\n";
+		output_string channel "\"mappings\":\"";
+		Rbuffer.output_buffer channel buffer;
+		output_string channel "\"\n";
+		output_string channel "}";
+		close_out channel
+	(**
+		Get source Haxe file position in a list of files referenced by this sourcemap
+	*)
+	method private get_file_index pos =
+		try
+			Hashtbl.find files_indexes pos.pfile
+		with Not_found ->
+			let index = (DynArray.length files) in
+			Hashtbl.add files_indexes pos.pfile index;
+			DynArray.add files pos.pfile;
+			index
+	(**
+		Apply base64 VLQ encoding to `number` and write it to output buffer
+	*)
+	method private write_base64_vlq number =
+		let rec loop vlq =
+			let shift = 5 in
+			let base = 1 lsl shift in
+			let mask = base - 1 in
+			let continuation_bit = base in
+			let digit = vlq land mask in
+			let next = vlq asr shift in
+			Rbuffer.add_char buffer (encode_digit (if next > 0 then digit lor continuation_bit else digit));
+			if next > 0 then loop next else ()
+		in
+		loop (to_vlq number)
+end
+
+type sm_node_data =
+	| SMPos of pos
+	| SMStr of string
+	| SMNil (* this data type marks the beginning and the end of a list *)
+
+type sm_node = {
+	mutable smn_left : sm_node option;
+	mutable smn_right : sm_node option;
+	smn_data : sm_node_data;
+}
+
+let init_sourcemap_node_list () : sm_node =
+	let first =
+		{
+			smn_left = None;
+			smn_right = None;
+			smn_data = SMNil;
+		}
+	in
+	let last =
+		{
+			smn_left = Some first;
+			smn_right = None;
+			smn_data = SMNil;
+		}
+	in
+	first.smn_right <- Some last;
+	first
+
+(**
+	Builds data for sourcemap.
+*)
+class sourcemap_builder (generated_file:string) =
+	object (self)
+	(** Current node *)
+	val mutable current = init_sourcemap_node_list()
+	(**
+		Add data to sourcemap.
+		1. `#insert (SMPos pos)` should be called right before an expression in `pos` is writtend to generated file.
+		2. `#insert (SMStr str)` should be called every time some string is written to generated file.
+	*)
+	method insert (data:sm_node_data) =
+		(* new node which will be inserted after current one *)
+		let inserted =
+			{
+				smn_left = Some current;
+				smn_right = current.smn_right;
+				smn_data = data;
+			}
+		in
+		(* link new node with current and next to current one *)
+		current.smn_right <- Some inserted;
+		(match inserted.smn_right with
+			| Some right -> right.smn_left <- current.smn_right
+			| None -> ()
+		);
+		(* inserted node becomes current *)
+		current <- inserted
+	(**
+		Rewind builder so that next `#insert data` call will insert data in the beginning of this sourcemap.
+	*)
+	method rewind =
+		let rec loop node =
+			match node with
+				| Some ({ smn_data = SMNil } as node) -> current <- node
+				| Some node -> loop node.smn_left
+				| None -> assert false
+		in
+		loop (Some current)
+	(**
+		Fast forward builder so that next `#insert data` call will attach data to the end of this sourcemap.
+	*)
+	method fast_forward =
+		let rec loop node =
+			match node.smn_right with
+				| Some { smn_data = SMNil } -> current <- node
+				| Some node -> loop node
+				| None -> assert false
+		in
+		loop current
+	(**
+		Set builder pointer to `node` so that next `#insert data` call will insert data right after this `node`.
+	*)
+	method seek (node:sm_node) = current <- node
+	(**
+		Get current node in this builder
+	*)
+	method get_pointer = current
+	(**
+		Write source map to disk.
+		If `file_name` is not provided then `generated_file` will be used with additional `.map` extension.
+		E.g if `generated_file` is `path/to/file.js`, then sourcemap will be written to `path/to/file.js.map`.
+		This function does not try to create missing directories.
+	*)
+	method generate ?file_name ?source_root com =
+		(* remember position *)
+		let pointer = self#get_pointer in
+		self#rewind;
+		(* write source map to a buffer *)
+		let writer = new sourcemap_writer generated_file in
+		let rec loop node =
+			(match node.smn_data with
+				| SMPos pos -> writer#map pos
+				| SMStr str -> writer#string_written str
+				| SMNil -> ()
+			);
+			(match node.smn_right with
+				| None -> ()
+				| Some node -> loop node
+			)
+		in
+		loop current;
+		(* dump source map to a file *)
+		let file_name = match file_name with Some f -> f | None -> generated_file ^ ".map"
+		and source_root = match source_root with Some r -> r | None -> "" in
+		writer#generate ~file_name:file_name ~source_root:source_root com;
+		(* restore position *)
+		self#seek pointer
+end
+
+let get_sourcemap_pointer (builder:sourcemap_builder option) =
+	match builder with
+		| Some builder -> Some builder#get_pointer
+		| None -> None
+
+let set_sourcemap_pointer (builder:sourcemap_builder option) (pointer:sm_node option) =
+	match builder with
+		| None -> ()
+		| Some builder ->
+			match pointer with
+				| Some node -> builder#seek node
+				| None -> ()

+ 2 - 0
std/php7/_std/StringBuf.hx

@@ -41,6 +41,8 @@ import php.Syntax;
 			Syntax.binop(b, '.=', 'null');
 		} else if( Global.is_bool(x) ) {
 			Syntax.binop(b, '.=', ((x:Dynamic) ? 'true' : 'false'));
+		} else if( Global.is_string(x) ) {
+			Syntax.binop(b, '.=', x);
 		} else {
 			b += x;
 		}

+ 15 - 1
std/php7/_std/haxe/CallStack.hx

@@ -16,6 +16,13 @@ enum StackItem {
 }
 
 class CallStack {
+	/**
+		If defined this function will be used to transform call stack entries.
+		@param String - generated php file name.
+		@param Int - Line number in generated file.
+	*/
+	static public var mapPosition : String->Int->Null<{?source:String, ?originalLine:Int}>;
+
 	@:ifFeature("haxe.CallStack.exceptionStack")
 	static var lastExceptionTrace : NativeTrace;
 
@@ -83,7 +90,7 @@ class CallStack {
 		for (i in -(count - 1)...1) {
 			var exceptionEntry:NativeAssocArray<Dynamic> = Global.end(lastExceptionTrace);
 
-			if(!Global.isset(exceptionEntry['file'])) {
+			if(!Global.isset(exceptionEntry['file']) || !Global.isset(currentTrace[-i]['file'])) {
 				Global.array_pop(lastExceptionTrace);
 			} else if (currentTrace[-i]['file'] == exceptionEntry['file'] && currentTrace[-i]['line'] == exceptionEntry['line']) {
 				Global.array_pop(lastExceptionTrace);
@@ -129,6 +136,13 @@ class CallStack {
 				}
 			}
 			if (Global.isset(entry['file'])) {
+				if (mapPosition != null) {
+					var pos = mapPosition(entry['file'], entry['line']);
+					if (pos != null && pos.source != null && pos.originalLine != null) {
+						entry['file'] = pos.source;
+						entry['line'] = pos.originalLine;
+					}
+				}
 				result.push(FilePos(item, entry['file'], entry['line']));
 			} else if (item != null) {
 				result.push(item);