HaxeFoundation / HaxeFoundation/haxe

Information on Generated Variables and Optimisations for Improving Debugging

Open
#10,888 2 comments 0 reactions 0 assignees View on GitHub
Dominant language
Haxe
Stars
6.9k
Forks
715
Avg merge
2d 2h
Merged PRs (30d)
11

Description

Sorry for the war and peace length issue, wasn't sure if it would be better to split these into separate issues or as one uber-issue.

I've been working on a debugger for hxcpp which uses a sourcemaps approach and there are several areas where a better debugging experience could be provided if I had more information on the transformations and optimisations made to user code. All of these could be used by the debuggers of other targets, so could lean to better debugging experiences across the board.

### Reasons for Generated Variables

```haxe
function main() {
for (i in [ for (j in 0...Std.random(10)) j ]) {
trace(i);
}
}
```

In the above sample the array comprehension is split out, generated with a while loop, and the result stored in a variable called `_g2` which is then used for that for iteration.

```haxe
var _g = 0;
var _g1 = [];
{
var _g2 = 0;
var _g3 = Std.random(10);
while ((_g2 < _g3)) {
var j = _g2 ++;
_g1.push(j);
};
};
var _g2 = _g1;
while ((_g < _g2.length)) {
var i = _g2[_g];
++ _g;
`trace(i, {fileName : "Main.hx", lineNumber : 3, className : "_Main.Main_Fields_", methodName : "main"});
};
```

If the loop had some way of saying that `_g2` holds the results of that array comprehension a debugger could provide the contents of that array on mouse hover or similar by reading the `_g2` variable under the hood. Currently most haxe debuggers show all the generated variables as quite often they contain useful information, but with more contextual info on why they exist and what they represent the number of compiler generated variables shown in debuggers could be reduced or only shown in certain contexts. Maybe some metadata could be attached to the while loop which points to `_g2` as whats being iterated over (I have not checked if generated while loops already store information about their original loop type)?

It may also be helpful for debuggers to have more temporary variables, take the following snippet.

```haxe
final result = myFunc(myOtherFunc(), anotherFunc());
```

If the calls to `myOtherFunc` and `anotherFunc` were split into temporary variables and those variables or the function stored info which allowed a debugger to link the variables to the arguments then the values of those variables could be shown as the return result of those functions without the user needing to manually put them into variables, recompile, and restart the debugging process.
If you were to place a breakpoint on that line many debuggers would ask you if you wanted a breakpoint on the line, or when one of those inner function calls occur, having those calls split into variables also makes this much simpler to implement.

### Inlining History

Libraries like vector-math make heavy use of inlining to avoid as many heap allocations as possible. The downside of this is that when it comes to debugging that code it looks nothing like what you originally wrote and its very easy to have hundreds of inline generated variables which makes debugging a pain.

```haxe
function main() {
final p1 = new Point(1, 2);
final p2 = new Point(3, 4);
final p3 = p1.add(p2);

trace(p3.x, p3.y);
}

class Point {
public var x : Float;
public var y : Float;

public inline function new(_x, _y) {
x = _x;
y = _y;
}

public inline function add(_other : Point) {
return new Point(x + _other.x, y + _other.y);
}
}
```

There does appear to be some consistency with how inlined variables are generated with regards to the original variable name, but I'm assuming they can be mangled in the case of collisions so trying to manually inspect variable names would be error prone, and you lose the type information as well.

```haxe
@:used
private class _Main.Main_Fields_ {

@:keep
public static function main() {
var p1_x = 1;
var p1_y = 2;
var p2_x = 3;
var p2_y = 4;
var p3_x = p1_x + p2_x;
var p3_y = p1_y + p2_y;
haxe.Log.trace(p3_x, {fileName : "Main.hx", lineNumber : 6, className : "_Main.Main_Fields_", methodName : "main", customParams : [p3_y]});
}
}
```

Ideally some sort of "history" of inlining would be available for each variable which would allow the debugger to re-create the structure to the user and the inlining essentially becomes an implementation detail. In the c++ debugger world LLDB and WinDbg call this sort of stuff "synthetic objects", where the debugger can provide "fake" variables / children to better visualise complex containers or "undo" compiler optimisations for debugger display.

Looking at inline.ml it seems like variables created due to inlining are assigned the VInlined type, could this maybe store a list of that inlining history? Where each item in the list contains the original variable name and type before inlining.

The positions of the inlined expressions can be preserved with `keep-inline-positions`, but it would also be nice if functions had a mapping of what lines within map onto inlined functions. This could probably be manually figured out by detecting position changes and looking to see if they're outside of the current functions range, but having a definitive list would be nice as it would be much easier for debuggers to detect "stepping" in / out of inlined functions and present fake stack frames to the user, again making inlining more of an implementation detail. The inverse of this (each function having knowing where its been inlined) would also be useful in cases where you want to place breakpoints in inlined functions and need to track down the inlined position.

### Analyser

This one would also be useful for inline heavy code, when variables are fused together or constants propogated it would be nice if there was some way to retrieve all optimised away variables and the value they held. Much like inline history, this would allow the debugger to provide the value of variables the user may have typed but were optimised away. Back to the vector-math library, this would be very handy as quite often array or matrix elements are optimised away which can make it a pain to see what is actually happening.

### TFunction Name

In the typer the `option string` name of an EFunction is lost and not carried through to the TFunction. This makes it difficult to map back the mangled name of generated cpp closures to a named closure / local function. Not sure if the name is intentionally not carried forward or its an oversight.

Contributor guide

Open the contributing guide

Research direction

Start by reading inline.ml and the typer code around EFunction and TFunction, then inspect how keep-inline-positions is represented. The issue also identifies generated C++ closures and analyser optimisations as affected areas. Done would require an agreed and implemented metadata design covering generated variables, inlining history, optimised-away values, and closure names.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers, devtools
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.