-
Notifications
You must be signed in to change notification settings - Fork 3.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
ANTLR4 runtime cannot be transpiled to ES5 because of incorrect inheritance #3032
Comments
Hi, thanks for this. I struggle a bit with your findings because my transpiled parsers do not suffer at all from the problem you are describing. Eric |
Hmmm... What does your transpiled I'm using TypeScript 3.9.7 which produces this output: var RecognitionException = /** @class */ (function (_super) {
tslib_1.__extends(RecognitionException, _super);
function RecognitionException(params) {
var _this = _super.call(this, params.message) || this;
if (!!Error.captureStackTrace) {
Error.captureStackTrace(_this, RecognitionException);
}
else {
var stack = new Error().stack;
}
_this.message = params.message;
_this.recognizer = params.recognizer;
_this.input = params.input;
_this.ctx = params.ctx;
/**
* The current {@link Token} when an error occurred. Since not all streams
* support accessing symbols by index, we have to track the {@link Token}
* instance itself
*/
_this.offendingToken = null;
/**
* Get the ATN state number the parser was in at the time the error
* occurred. For {@link NoViableAltException} and
* {@link LexerNoViableAltException} exceptions, this is the
* {@link DecisionState} number. For others, it is the state whose outgoing
* edge we couldn't match.
*/
_this.offendingState = -1;
if (_this.recognizer !== null) {
_this.offendingState = _this.recognizer.state;
}
return _this;
}
/**
* Gets the set of input symbols which could potentially follow the
* previously matched symbol at the time this exception was thrown.
*
* <p>If the set of expected tokens is not known and could not be computed,
* this method returns {@code null}.</p>
*
* @return The set of token types that could potentially follow the current
* state in the ATN, or {@code null} if the information is not available.
*/
RecognitionException.prototype.getExpectedTokens = function () {
if (this.recognizer !== null) {
return this.recognizer.atn.getExpectedTokens(this.offendingState, this.ctx);
}
else {
return null;
}
};
// <p>If the state number is not known, this method returns -1.</p>
RecognitionException.prototype.toString = function () {
return this.message;
};
return RecognitionException;
}(Error)); |
Note also that I just now confirmed that the problem is fixed if we add this line (as suggested in the TypeScript docs): class RecognitionException extends Error {
constructor(params) {
super(params.message);
Object.setPrototypeOf(this, RecognitionException.prototype); // <====== option #2 from above
if (!!Error.captureStackTrace) {
Error.captureStackTrace(this, RecognitionException);
} else {
var stack = new Error().stack;
} However my preference would be to do option #1 instead of #2, because it is more portable and less error-prone. |
Can you share your babelrc? |
We don't use Babel. We transpile to ES5 using TypeScript and a polyfill library. This is leaner and faster. (It's also better suited for corporate monorepos with lots of projects, since transpilation happens incrementally rather than centrally.) |
But look at this Stack Overflow thread. It seems to say that Babel has the same limitation as TypeScript. I'm really curious to see your transpiler output for |
ok it seems the npm package is not webpacked as expected, it contains the src code but not the generated minimized bundle |
Hi, here is what babelized code looks like: var RecognitionException =/*#__PURE__*/function (_Error) {
_inherits(RecognitionException, _Error);
var _super = _createSuper(RecognitionException);
function RecognitionException(params) {
var _this;
_classCallCheck(this, RecognitionException);
_this = _super.call(this, params.message);
... with utility code being: function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _createSuper(Derived) {
var hasNativeReflectConstruct = _isNativeReflectConstruct();
return function _createSuperInternal() {
var Super = _getPrototypeOf(Derived), result;
if (hasNativeReflectConstruct) {
var NewTarget = _getPrototypeOf(this).constructor;
result = Reflect.construct(Super, arguments, NewTarget);
} else {
result = Super.apply(this, arguments);
}
return _possibleConstructorReturn(this, result);
};
} So it looks like Babel (with the help of @babel/plugin-transform-classes) is already calling Object.setPrototypeOf. Practically, I see 3 scenarios:
I am not super inclined to add redundant code when the issue seems to lie with the TypeScript transpiler. Also, if you are using TypeScript, should you not be using the TypeScript runtime target ? |
FWIW I hit something similar, in my case when I looked inside my webpacked file, I found two instances of the generated JS code, basically webpack was seeing two versions:
And was bundling them separately, this is what broke the instanceof logic. My fix was simple - move the generated JS grammar out of my "src" folder so that the TypeScript didn't transpile it (thus creating the ambiguity). Alternatively upgrading to 4.9.1 and setting the tsconfig module to "es6" also worked (again since webpack only "bundles" one variant). |
I'm not recommending to add redundant code. My recommendation is to avoid extending the system I can also open a TypeScript issue proposing for the transpiler to call
ANTLR's TypeScript target is still experimental and not officially supported. It might be nice to have, but frankly since humans don't edit the generated parsers, it doesn't matter much whether the generated code is typed or not. |
This will fix the problem for ES6+ runtimes such as Chrome, but it will not work for older runtimes unless you apply a separate transpiler such as Babel. |
Well what you are proposing are 2 workarounds for a TypeScript transpiler bug. So may I suggest that you either:
|
@ericvergnaud Having re-read the original issue and read up on it some more, I have to agree with your synopses. BUT (isn't there always)
@ericvergnaud would you accept a PR which introduces a "safe" Error class I would probably go with old school es5 code like (from MDN):
Yes it is a compromise, but if your goal is to "just work" with a not insignificant audience... |
well I guess you could also workaround the problem as follows: const oldError = window.Error;
window.Error = function(message) {
this.message = message;
return this;
}
try {
callLexerAndParser();
} finally {
window.Error = oldError;
} |
Doesn't look like it would be NodeJS Friendly? Extending Error has been an edge case for as long as I can remember (looking over my code base I can see where I do it "correctly" as per MDN and where I don't as per TS 2.x) - I wonder what runtimes this is actually an issue for (If this is only an IE8 issue then nothing to see here, lets move along...) |
The code fails if you specify The ECMAScript 5 target is needed mainly for Internet Explorer 11, and for devices that shipped with an older web browser (e.g. Playstation, Tizen, etc). These devices can last for many years beyond their initial release, and the OEM generally will never update the browser beyond security fixes. Recently at work I encountered a Comcast TV box that mostly supports ECMAScript 6, but with arbitrary gaps that are most easily solved by transpiling to ES5. ES6 is now 6 years old. I'll be overjoyed on the day when we can finally forget about ES5, but we're not there yet... |
just do it conditionally? i.e. if NodeJS replace the global Error directly. I'm about to close this since this is not an ANTLR4 runtime issue. |
Here's a PR #3041 It's really pretty simple to fix this problem. I don't believe this fix has any practical downsides. |
BTW the TypeScript compiler maintainers replied and said they will not implement Babel's solution because it requires too much extra code. Their behavior is "by design". |
It's much simpler to fix it outside the runtime, see above examples, so closing this. |
My advice is: don't use tools written by lazy people (it not incompetent) |
ericvergnaud wrote, regarding the TypeScript project:
@parrt Is there a Code of Conduct for the people who publicly represent the ANTLR project? Maybe it's time to adopt one. |
Hi @octogonz, yeah, sometimes people have strong opinions and express them publicly. I don't think Eric intended to insult you. Codes of conduct are hard to enforce... better if we all just agree to soften our language if we can. |
In a broad community people will have different ideas about what constitutes professional behavior. So it can be helpful to write something down, even if there is no "enforcement." (BTW Eric didn't insult me, but rather the TypeScript maintainers. Given that TypeScript's NPM stats are 3x the popularity of Babel and 144x the popularity of ANTLR, it seemed a bit cavalier to dismiss their design decision and claim that it is TypeScript's responsibility to accommodate ANTLR's assumption that everyone uses Babel.) I thought of another possibility by the way: If the ANTLR package provided its own ES5 bundle that is built using Babel, we can use that directly, and then we won't need need to use TypeScript to transpile your library. However PR #3041 is still the best solution IMO. |
Yeah, probably should avoid too many digs on other projects...hahah. I can't comment on JS/babel as it's outside my area. |
as I mentioned 2 weeks ago:
so maybe you could submit a PR that does just that: "provided its own ES5 bundle that is built using Babel" |
@octogonz you could try using this branch https://github.com/ericvergnaud/antlr4/tree/purify-javascript-runtime-code |
Here's an initial sketch: PR #3065 It builds two ES5 bundles, one for development, and one for production. By default the It seems that the I can test this and refine it some more. But first I'm wondering how you will publish it. It seems that the version |
@octogonz can you move your last comment to the PR itself - best to discuss there ? |
We will soon be shipping a typescript target, so closing this. |
Background
Web applications must be transpiled to ECMAScript 5 (ES5) if they need to support older JavaScript runtimes, for example Internet Explorer or hardware devices that shipped with older JavaScript engines. In the past, the ANTLR runtime was written using ES5 compatible code. But version 4.9.x recently introduced ES6 class definitions that require transpilation. This is fine.
However, the ANTLR library has some classes that inherit from the system
Error
class, which is incompatible with transpilation. The reason is that ES5 system APIs are modeled as ES6 classes, which use a different inheritance mechanism from transpiled classes. (Seethis article for some details.)
Repro
This code fails to catch
LexerNoViableAltException
:The
e instanceof RecognitionException
test returns false, even thoughLexerNoViableAltException
inherits fromRecognitionException
. This happens because of the incorrect prototype introduced whenRecognitionException
inherits from theError
base class.Possible fixes
The two standard solutions are:
Error
class; instead simply implement its contract using a custom base class. This will breakinstanceof Error
, but generally nobody ever tests that, so this approach works fine in practice. The callstack property can be calculated using(new Error()).stack
.- OR -
Object.setPrototypeOf()
in the constructor for every subclass of theError
class. This also works pretty well. Its main downside is that people sometimes forget to apply the workaround when adding new subclasses.I would be willing to make a PR to fix this. Would you accept a fix?
The text was updated successfully, but these errors were encountered: