I wouldn’t say a “big” part of it was adding location info to AST nodes, but yeah, those are needed so that the “coverage injector” can record map to line/column numbers in the report. Basically GoAWK has to execute the __COVER assignments in the coverage-injected code, for example below:
GoAWK started life as a tree-walking interpreter, but I switched to a byte-code compiler + interpreter a while back. There’s still a runtime cost to coverage as it has to record lines covered in a map, but it’s not on by default, so hopefully not too much of an issue for users.
Kind of a tangent, but I’m curious: when it was an AST interpreter, how did you implement break / continue / return in Go?
I do it with exceptions, but Go doesn’t have those. You can just use the normal style of return result, err, or I have seen people polling a global flag. Maybe you can use panic/recover, though I think that would be frowned upon
When the interpreter walked the AST I used error returns for this: I just had the BreakStmt return a user-defined error errBreak, and then all the loops would check for this error and break, for example WhileStmt. Now that it’s bytecode-compiled I still use error returns, though I can be a bit smarter about turning breaks and continues into jump opcodes most of the time.
Originally in the AST-walking interpreter I had used panic/recover for this, mainly because it simplified evaluation, but it was significantly faster to use error returns (and you’re right, not idiomatic Go), so I switched to that early on.
Very cool! I peeked at the code, and it looks like a big part of it was adding location info to AST nodes?
So I would guest that goawk is an AST interpreter? (like the original awk, but unlike mawk I think)
Is there a runtime cost to coverage mode?
I wouldn’t say a “big” part of it was adding location info to AST nodes, but yeah, those are needed so that the “coverage injector” can record map to line/column numbers in the report. Basically GoAWK has to execute the
__COVER
assignments in the coverage-injected code, for example below:GoAWK started life as a tree-walking interpreter, but I switched to a byte-code compiler + interpreter a while back. There’s still a runtime cost to coverage as it has to record lines covered in a map, but it’s not on by default, so hopefully not too much of an issue for users.
Kind of a tangent, but I’m curious: when it was an AST interpreter, how did you implement break / continue / return in Go?
I do it with exceptions, but Go doesn’t have those. You can just use the normal style of
return result, err
, or I have seen people polling a global flag. Maybe you can use panic/recover, though I think that would be frowned uponWhen the interpreter walked the AST I used error returns for this: I just had the
BreakStmt
return a user-defined errorerrBreak
, and then all the loops would check for this error and break, for example WhileStmt. Now that it’s bytecode-compiled I still use error returns, though I can be a bit smarter about turning breaks and continues into jump opcodes most of the time.Originally in the AST-walking interpreter I had used panic/recover for this, mainly because it simplified evaluation, but it was significantly faster to use error returns (and you’re right, not idiomatic Go), so I switched to that early on.