How I fixed a bug in Atom
davidvgalbraith.com
davidvgalbraith.com
I think articles like this are very useful to beginner/intermediate developers. Everyone in software says "write open source code", "make pull requests to code you use" etc, but there's a very big gap between knowing how to program and knowing how to track down, fix, and submit a PR for a bug in a program (and language!) you've never worked on before.
This is a good little tutorial on ways to attack a bug in a program. I especially like that he starts with two print statements - there's no wizardry here, just a programmer digging into a bug.
Maybe I should write up a blog post like this one..
And why? So you could write some dirty hack rather than writing a simple recursive descent parser to create an AST of the code (which takes O(n)). What did you gain by ruining your regular expression implementation and writing shitty code?
EDIT: Matching might be O(m), but I'm wondering about maximally linked graphs with many epsilon edges. The conversion from DFA to NFA would require O(n^2) in that case. Implementing it as an NFA evaluation might be faster than full conversion, but then you run the risk of having O(n^2) dominating in the matching.
EDIT: The author is somewhat correct on what "catastrophic back referencing" is. While you could argue that it is due to bad implementations of the greedy matching, it's a more endemic problem of how you have to implement a regular expression engine that supports back references. If you have to support back references, then you will always have a class of pathological cases which cause exponential time complexity. I had a useful graph about this on my toy regular expression engine (which performs much better than Python's implementation even though it's written in Python and not C): https://github.com/cyphar/redone.
The problem is not the tool. The problem is using the tool for the wrong job - although who knows, maybe if they didn't just chuck out a quick hacky implementation, then atom simply wouldn't have the feature at all - in which case it is a trade-off between having the feature at all and an evidently tiny corner case bug that was easily fixed by someone who isn't even a regular developer of the project.
So I don't see the reason to be outraged or judgemental here.
This is an effect of the language, not the problem space. Backtracking REs make it very easy to write bad code, and then most languages make writing the parsing code hard.
If you're in a language that makes parsing code easier, like Haskell, then the tradeoff isn't anywhere near so bad. I don't mention Haskell just because it's the trendy thingy, I mention it because parser combinators really do make for some very easy parsers, and while parser combinators can exist in other languages they tend to be very syntactically heavyweight. Perl 6 is supposed to make parsing perhaps even easier, but I haven't played with it to know.
Parsing isn't really that hard, it just plays very poorly with Algol-inspired syntax.
To be clear, since this is the Internet and the presumption of disagreement is strong, this post is not an explanation of why you're wrong... it's an explanation of why you're right. It is absolutely a true statement that in most languages you're way better off bashing out a dangerous RE than writing a proper parser even for something as simple as this.
But how are you suggesting that Haskell could solve the problem of letting CoffeeScript plugins express which text they should apply to in a CoffeeScript text editor?
He's suggesting that the correct solution (recursive descent parser) is not inherently hard, but CoffeeScript makes it hard enough that people reach for less good approaches.
I mean, yes, but I'd argue that "I am writing software which will spend a significant amount of its time parsing code" is not one of the times when that is an appropriate instinct.
Then don't use languages whose grammars require insane amounts of effort to parse.
One can write a decent S-expression parser in an hour or two, and a full parser in a weekend. S-expressions can represent everything anyone ever needs to work with, so why use anything else?
Aside: out of curiosity it would have been interesting to see how a Thompson NFA[1] would have dealt with this.
It wouldn't have dealt with it right? NFAs and DFAs are equivalent and them being actually regular means they can't do things like count parentheses.
I believe you mean NFA to DFA.
> [...] would require O(n^2) in that case
I believe you mean O(2^n). https://en.wikipedia.org/wiki/Powerset_construction#Complexi...
This isn't really a problem if you incur that cost only once by having the regex compiled when the script is parsed. However, idiomatic JS usually includes regex literals in the closure where it is used - decreasing performance, code reuse and clarity. Why? Probably for the same reasons that regex is being used in the first place.
> I believe you mean NFA to DFA.
Yes, you're right. Whoops :P.
> > [...] would require O(n^2) in that case
> I believe you mean O(2^n).
Ah yes, I forgot that you could chain epsilon edges. Fair enough.
This approach gives you the best of both worlds: speedy operation in the common cases in which recursive backtracking beats the Thompson NFA, all the regex features you know and love, and worst case polynomial running time for regexes that the Thompson NFA supports.
I don't think it would have prevented this bug, though, because I don't think the Thompson NFA supports the recursive backreference thing that this regex uses.
See https://swtch.com/~rsc/regexp/regexp1.html, which gives
a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?a?aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
(see the backtracking? me neither) as an example.
If I understand the article correctly, the Perl regex evaluator, if given a string consisting of only the minimum required number of 'a's, first matches them to the optional 'a's, and then has to backtrack to match the required 'a's.
Well, backtracking is how you implement your engine. So "simple" regular expressions that don't use any of the features backtracking provides (like backreferences) will cause exponential complexity, like:
a?a?a?a?aaaa for a string "aaaa"
func("some call :)")
as extra closing paren. (regex101 agrees)I kid, but...really. The number of times I've seen people burnt by non-trivial regexps in production code is absurd.
"Oh, I need to mangle this CSV that's in the wrong format"? Sure, write a one-off regexp.
"Hm, there might be a security vulnerability in this URL parameter, I need some way to filter out bad inputs..." Oh hell no.
Literally two weeks ago I tracked down a crazy bug to a regexp someone had written to try and correct mistyped email addresses in a signup form (?!) that deleted "invalid" characters. Like a "+". So many facepalms for one short line of code...
I noticed this when a web app I worked on froze on certain pages. Runaway regexp matching is one of the easiest ways to really lock a JavaScript thread.
I try to avoid regex where possible and in the event that I do need to use it I try to document it the best I can and to make sure it's as simple as possible to avoid weird issues like this.
(but i get this is difficult: https://gist.github.com/dperini/729294 )
The ABNF grammar is fairly simple though and, I think, free of ambiguities. I've had luck converting it straight in to PEG form. It's not a trivial or wholly useful endeavour though, and, if you do so, remember to check the errata. For HTTP you'll also have to add in the changes from RFC 7230[1].
Oh, and of course, none of this validates DNS names, their labels, etc. for length, the "LDH rule", or the public suffix list[2], or IP addresses to check whether they have publicly routable prefixes.
Bottom line is, if you want to validate a URL, the best thing to do, much like e-mail, is to just try and GET it.
[0] https://tools.ietf.org/html/rfc3986#appendix-B
In this particular case, the Google Caja project[1] is a good starting place for most HTML/JS/CSS sanitization needs (although the project has a much larger scope than just that); and I think the 'sanitizer' package on npm is a fairly popular wrapper/port of it's basic sanitizing code, and I believe ruby/php/python have their own but I couldn't name them offhand. But it would depend on the exact attack vector you're trying to stop, eg, XSS, remote shell via filename params, etc.
If I had to write it myself, I'd probably go for something as braindead as possible; probably a bunch of nested loops backed by some thorough tests. Regexps are great for magic one liners, but magic one liners are antithetical to good security.
It terrifies me to think about how these language modes contain regexps for matching regexp syntax. Of course those are wrong.
You need a full parser for that to happen, most mode authors don't bother[0]. JS2-mode (https://github.com/mooz/js2-mode/) does that (or attempts to).
Most syntactic colorisers are souped-up tokenisers, not actual parsers. It suffices in 99% of cases, but breaks badly elsewhere.
[0] it would help if more languages made fast parsing machinery externally available as a convenient API. ALAS, that's rarely the case, even less so for fast parsing machinery which doesn't throw the information you need for syntactic coloration out the window.
Emacs has a generic lightweight parsing facility that can tell you if you're inside a string, or a comment, or how to skip over a string, or a pair of matching parens/braces/brackets, and so on.
https://github.com/mooz/js2-mode/issues
The goal just shifts from having a semi-accurate regexp tokenizer, to having the language mode's complex parser match the complex parser of the language it targets—which will pretty much never happen, unless the language is exquisitely simple, like maybe restricted dialects of Scheme... or Brainfuck.
It would be easier with a more stable language. I.e. not JavaScript, which is still going through growing pains.
Even so, "25 Open, 204 Closed" seems pretty good to me.
Lexing and producing tokens with a loop is probably enough given that auto-indentation works on a subset of states of the parser, if any. As my comment alluded to, using regex is harder to both write and read than a simple loop; it's also most likely slower.
Edit: I think it might actually be the same 'dave' who authored the article, which demonstrates how much he's learnt just by fixing this one bug.
Some people, when confronted with a problem,
think "I know, I'll use regular expressions."
Now they have two problems.
- Jamie Zawinski- Nietzsche
>As cute as the “now you have two problems” quote is, it seems that Jamie wasn't the first to come up with the idea. The same quote (but with AWK rather than regular expressions as the punch line) shows up in the sig of John Myers post from 1988, where he credits a “D. Tilbrook” for it:
“Whenever faced with a problem, some people say `Lets use AWK.' Now, they have two problems.” -- D. Tilbrook
^\s*[^\s()}]+(?<m>[^()]*\((?:\g<m>|[^()]*)\)[^()]*)*[^()]*\)[,]?$
to solve your problem, then you have IMHO come across a problem which you should not be solving using regular expressions.Case in point is counting and balancing parentheses which is very easily done using a single loop over the string in question.
Regexes are powerful and useful but also dangerous. People who don't thoroughly understand them and try to get fancy often run into problems like this. In general you shouldn't be using them to parse a computer language anyway, it is something you should be using a tokenizer/parser for.
That’s why I weeped when support for named groups and backmatching was added.
To which I might dare add, "... and so does whomever next has to read this code."
But this code? Definitely giving me a headache, and my interest went down to zero. There seems to be a meme and unchallenged claim in hacker circles that CoffeeScript is somehow more "readable" and easier to understand.
Allow me to disagree, and I suspect there's quite a few like me. This code is harder to read. I think I understand what the code does, but I can no longer be certain about the language semantics. That introduces needless uncertainty.
And it just broke all my pre-configured and pre-setup JS-tooling. Can't use any of that for this codebase. Just great.
So what's up with people writing applications and projects in NodeJS, a prime JS-environment which supports "all" modern Ecmascript-features, classes included, and then decide to go use a non-standard language for their app?
And for what gains? How did Coffeescript make this code more readable or easier to debug? It didn't. And now you need to debug code compiled from the actual code you wrote. How does that do anything except make everything harder?
TLDR: CoffeeScript seems like a bad choice for just about everything and I can't see why any big project with a desire for contributors would even consider using it.
Beyond that...
1. The JS world moves stupidly fast, and Coffeescript is a relic of a now-vanished age. It was born, it evolved, and it died. Back in those long ago days of, um, 5 year years ago, there was no ES6, and Coffeescript looked a lot more attractive. So much so, in fact, that ES6 stole a bunch of Coffeescripts better features.
2. If you're familiar with Coffeescript, it's terse and expressive and very readable. If you're not, it looks like gibberish. But that's true of any language. The proper critique of Coffeescript should be "hey, not a lot of potential collaborators know it, so it'll see unreadable to them", not "hey, Coffeescript is generally unreadable". JS is pretty confusing and unreadable if you don't know it too.
Mind you, Atom was released 2 years ago, when Coffeescript was already starting to look dated. And it was an open source project looking for contributors so...yeah. Bad, bad choice. :)
I ended up with a general workflow of having my Coffeescript source and the generated JavaScript sitting side-by-side so I could check that the output was what I was expecting. I've only had the urge to do that with ES6 and Babel a couple of times.
...now that you say that, I did end up going to the Coffeescript REPL on their website to test a bit of syntax a fair number times. I feel like a got used to it pretty fast, but I agree, a big chunk of Coffeescript's learning curve is getting past it's ambiguity, and there were some features I expressly avoided just because they were too confusing. Eg, I could never remember the difference between 'in' and 'of' in CS, so I just used underscore's equivalents. And then there were the comprehensions, and the weird scoping rules, and...yeah.
I'll be honest, switching from CS to a modern Babel/ES6+ configuration, I thought I'd really miss CS, but I really haven't. I maintain that if you know CS, good CS code is easy to understand, but good CS code takes way too much work to write. :)
Normally, pre-source maps, I'd also look at generated JS when debugging in the browser, but this is no longer the case. Other than the mentioned compiler bug I didn't have to look at the generated JS even once in my 2-3 years of using LiveScript.
I think a good question to ask would be why did you have to look at the generated source. What did that give you, and what could replace it? Is looking at the generated source the most efficient way to achieve your goals?
[1] The "killer feature" of LiveScript, for me, is its support for functional programming on par with support for OO. It reminds me of Scala or F# in that regard.
That's not true though. I'm not familiar with C++, for example, but it doesn't look like gibberish when I look at it. Nor does Go, as another example, even though I've never written a line of it.
CoffeeScript, on the other hand, still is hard for me to parse—even as someone who has written thousands of lines for it.
There are very real differences in readability between languages.
It's horrible. It's a nice idea, but the implementation leaves so much to be desired. It's bitten me before and so I avoid it now.
I've worked with both, ES6 is definitely a big progress on Coffeescript, but it takes like 80% of the things from coffee IMO, except the indentation syntax and list comprehension, added an `import` keyword.
My point is: don't complain about coffeescript while cheer at ES6, they are mostly the same.
Except... CoffeeScript does all sorts of nice things that Babel does not. List comprehensions. Lack of == operator. Correct modulo operator. Block regexes. Triple-quoted strings. The @ syntax.
There is a strong cult-like vibe to the JS community, and I really find it off-putting. They have taken a technical limitation "Browsers only support JS" and turned it into a socially enforced rule "You may only use JS". Fuck 'em.
No, they are really not.
ES6 does not change the semantics of the language. It adds a couple of new useful features, but it still maintains the basic readability of JS.
I've written production apps in both. With Coffeescript, I regularly encounter problems where the generated output is not what I expected given the input and the only way I discovered that was examining the output JS.
With ES6/Babel, I have literally never inspected the generated output. It's never produced code contrary to what I expect.
I liked CoffeeScript but haven't used it in years. IMO CoffeeScript is probably a bad choice for a modern dev environment.
I'd say the same for JavaScript
But Coffeescript makes many things easier and has good tooling (just another loader in webpack). While not perfect, It is a reasonnably efficient tool for front-end dev.
Try a tutorial, you may be surprised.
For me CoffeeScript is just shorthand syntax for well structured, fast JS although I agree that ES6 is good CoffeeScript replacement (destructuring FTW) if you can suffer through braces, colons and `function` keyword.
TypeScript is all the rage today.
People don't build parsers because of masochism, but because regular expressions are provably insufficient to capture things like nesting. You need to go at least one level up on the Chomsky hierarchy[1] to pushdown automata for that.
More importantly, shouldn't SOMEBODY working on a text editor know and recognise this sort of thing? I'm all for the hacker mentality of shipping, reducing developer time instead of machine time, using what you know, but this is the sort of hacky fix that just pushes the problem further down the line.
Great write-up though, very enjoyable read!
[1] https://en.wikipedia.org/wiki/Chomsky_hierarchy#The_hierarch...
Yes, but features like backtracking and backreferences (or recursive referencing) make regex implementations non-regular regular expressions. That comes with a whole heap of issues (pathologically exponential complexities in any circumstance where backtracking is inolved).
In any case, a regex is hilariously unsuitable for the purpose here. It's obtuse, it has terrible corner case performance (as noticed here), it probably took vastly longer to write than a simple loop and apparently it isn't even correct.
I understand the need for people to be able to "easily" hack on it (easily in quotes here because really that just means "people who know web languages"), but that goal could still be accomplished in a native application that embedded a JS runtime for plugins.
[1]: https://discuss.atom.io/t/high-usage-cpu-and-memory/16165/3
The easy-to-hack bit is not about the specific scripting language but about the UI being completely hackable.
No other editor (except emacs) lets you hack the interface as well as Atom.
And any attempt at exposing enough hooks to allow a native text editor UI to be as hackable would, in a kind of Greespun's 10th rule, probably result in a bug ridden, poor implementation of an HTML engine.
I am not trying to harp on Vim or Emacs, I myself am a Vim user and use it for C++ code.
vim and emacs are friendly and welcoming to people who already know them; new users in 2016 have some weird expectations due to growing up using insufficiently-powerful UIs, which means that they have quite a learning curve when picking up a powerful UI.
Having first used both vi and emacs (and other text-mode - and even line-mode -- editors), though only casually then, I disagree; vi and emacs are, like almost anything, friendly and welcoming to people who have become deeply familiar with them, but they simply aren't as accessible as tools that benefit from the advances in the intervening years in making UIs easy to use for people that haven't invested huge amounts of time in familiarity.
This is really a different issue than UI power (though if vim and emacs didn't have both powerful UIs and substantially bodies of users with long investments, they wouldn't stick around in the face of their disadvantages in terms of onramp.)
Older users just didn't, when they were new, have alternatives with a simpler learning curve. So, there really was no trade off for the power vi and emacs offered (less powerful alternatives of the time were still just as inaccessible), where now there is.
There's simply nothing out there as good as emacs. Nothing. Eclipse, IntelliJ, Atom, SublimeText, all those pale in comparison. vim has its positive points (it's an excellent way for a human to edit line-oriented text), but ultimately it too falls down in the general case. emacs is simply the best way for a human being to use a computer to edit data.
There still isn't an alternative to vim and emacs. I kinda wish there were, but there ain't. Someday I'd like to use a modern emacs-like editor, implemented in Common Lisp (or a successor language), but right now emacs is the pinnacle of editor evolution.
I think the only thing that Vim doesn't do that it should is actually use a proper language for scripting. Emacs has elisp, but Vim has VimScript which is a horribly stunted langauge.
Aside from that, I much prefer vim to emacs. Just because everything is mode-based, and the keybindings don't require 10 hands to do anything.
"easily in quotes here because really that just means "people who know web languages""
It's my go-to editor/IDE for Rust, which is far from a web language.
"that goal could still be accomplished in a native application that embedded a JS runtime for plugins"
Kind of like Chromium with V8? With full hardware/OS access and linking to native code? If only such a thing existed.
The only tool I could use with some decent widgets is Delphi (but cost too much and kill the idea of release the tool as open source).
After that, what exist? QT is horrible. GTK look bad and weird (equal others like wkWidgets). Doing one for each target is the best but harder.
I know some people think QT/GTK is good, but as user of Delphi, them are ugly in comparison and less featured. Even winforms is barely a match.
HTML+JS+CSS is far easier, at least, to get the look. Then is the problem in how make it attractive for contributors.
I wish exist a way to get a html fast/light html rendered for the GUI.
It works well on the size files I mostly want to use it with, has a strong community producing support for the languages I want to use it with, doesn't want to impose its own structure and management artifacts on projects the way IDEs tend to, and imposes less cognitive overhead than Emacs [0]. It might use more memory, but that's never been a real practical problem, and even on my cheap 6Gb RAM laptop system, the difference between a programming editor taking up ~100Mb and "hundreds of Mb" just isn't an issue in most cases.
[0] I recognize that this is largely due to familiarity, in that Atom follows closer to currently-popular UX paradigms and Emacs is its own unique beast that predates most of the things that Atom is following, but I've been using Emacs intermittently for many years, and Atom was still instantly more accessible. But even so, there are some languages where the available Emacs packages are so good that I prefer Emacs. Just as there are some things for which I prefer Visual Studio or an IntelliJ-based IDE. Its not like one tool is ideal for every person for every task.
Sublime Text almost had it.
Emacs/vim takes too much effort to configure.
You could make an editor that only allows valid programs but that opens up a lot of UI problems, it was tried many times and it never took off in practice.
Trouble with elaborate 'correct' solutions starts when you need another thing. I don't think support in Eclipse for .cjsx or PureScript or whatever is coming soon. I won't wait 10 years till they get it 'right'. I'll use Atom to get the 95% right in few months.
This parser would not be difficult to write; the only "recovery" is in choosing how to react to mismatched token pairs. The simplified problem means heuristics could potentially be used.
expected
func someFunc() {
aSlice := []string{}{
}
}actual
func someFunc() {
aSlice := []string{}{
}}
The end bracket on the slice's initializer never indents correctly when you type it and hit <enter>. It always defaults to the first character of the next line. It seems insertNewLine somehow is not able to grok the idea of more than one set of matching brackets.
Edit: issue filed https://github.com/atom/bracket-matcher/issues/209
I thought JavaScript regexes don't support named capturing groups. Is Atom using some library or custom functionality for that?
EDIT: Or is it a CoffeeScript addition? In their table of contents I only see regex blocks mentioned though (http://coffeescript.org/#regexes).
Good job!
Congratulations to the author, great commit! Small payload, enormous benefits.
Having said that, probably few people would explicitly opt in to using such a plugin unless it's bundled by default to some linter.
# How I fixed the 'atom/language-go' package
The chrome debugger with breakpoints, profilers, and a whole slew of other goodies is great for this sort of debugging.
Yeah, sometimes i need to avoid it because turning it on can actually slow the code down significantly, but the profiler would have been perfect to see where the time was spent here with just a few clicks.
Perhaps a core contributor or someone who regularly works on the code-base would have approached it with the full suite of debugging tools available as you mention.
However, I get the impression that the author of this article is not one of those and simply wanted to crack open his editor to fix this one problem he saw (and happened to learn more about Atom, Coffeescript and regexps along the way).
Being upset over a great open-source contribution because they used "non-ideal" methods to arrive at a solution is just silly.
I often use the same kind of thing for debugging (console.log('got here')) because it's sometimes too much work to leave the code to just get an understanding of if something is hit or not.
When i started reading the article i was really hoping he would use the profiling tools. I just feel sad that they are being overlooked a lot of the time, and this is a textbook perfect use case for it!
Balanced parentheses form a context-free grammar, and thus cannot be parsed by regular expressions. There are extensions to regexps which make them irregular — and hence capable of parsing CFGs — but they often lead to poor performance, as here.
Maybe everyone does it and everybody is happy with it working only in 90% of cases, but it just seems like the wrong approach to me.
Surprised to see that mistake in code that otherwise looks pretty high quality.
Or here's a small blog post I made: http://cliffordfajardo.com/2016/atom-editor-review/
I wish people would stop abusing regular expressions.
It's definitely not the fastest opening editor, and it does have performance issues, but they tend to be more rare than common.
The 2 that bite me are:
* the update/install screens in the settings tend to be a bit slow
* opening "large" files that have 1000+ characters per line will either hang or crash the browser depending on the size. If it's a "normal" looking source file, it runs like a dream with files up to 5gb+ (the largest i've used it for), but if it's something like a minified javascript file, a few kb is enough to hang the editor.
Outside of those 2 issues, i don't see any lag or stuttering in my normal use, and i've got about 70 plugins on top of the defaults.
But to answer your question, it's the customizability and the massive number of plugins that draws me. Plus it's fucking beautiful, and i know this isn't the most popular opinion, but if i'm going to stare at this thing for 6+ hours a day, i want it to look good!
And honestly I don't see a marked difference in visuals between those three. After installing my zenburn colour scheme they're virtually identical with similar if not identical UI features.
I don't expect the Electron based editors to match ST3 for performance (at least not yet), but honestly it's kind of embarrassing how slow Atom is compared to VSCode, particularly when it comes to things like checking for package updates and just starting up.
This is all based on using all three editors within the last three months (I've been swapping around trying to find what I like).
.. which is his main pass time if you see his other posts, rather than spending his effort on fixing bugs that actually affect a lot of people. I am sorry but I don't really appreciate it and have trouble getting over the misrepresentation in the title of the blog posts.
Plus, it is a good write up that explains the problem, solution, and the steps taken well.
It would be a shame to see others put off from fixing bugs, and posting commentary on the solution, in open source software for fear of being put down.
Also, I think you're being rather rude.
Nobody forces you to read whatever the OP posts, much less dig up the history to see if you agree with how he chooses to spend his time or not.
Well I do, and I found the article fascinating, so I'm glad your opinion isn't the only one that matters on Hacker News.
pretty good fix.
Is this really how people troubleshoot JS issues? In 2016?