Vim's 400 line function to wait for keyboard input
geoff.greer.fm
geoff.greer.fm
Abstracting, however, would take perhaps a day of work. A full library such as libuv would have taken maybe a couple of weeks to develop.
So there you are. QED.
What about easier extensibility? Not many are going to want to look at that code, let alone try to add some additional functionality to it.
From the article:
"The idea that new code is better than old is patently absurd. Old code has been used. It has been tested. Lots of bugs have been found, and they've been fixed. There's nothing wrong with it. It doesn't acquire bugs just by sitting around on your hard drive."
EDIT: Joel isn't actually recommending "do nothing" - on the contrary - the article goes into some depth on ways one can improve software without throwing everything away and starting from scratch.
Well there's your problem. Testless code doesn't acquire bugs by just sitting around on your hard drive, but it doesn't lose any bugs that way, either, and without a test suite you can't afford to do anything but leave it sitting around.
In most enterprises it even has less value than documentation when deadlines approach.
The sad reality is that most software is a by product of the main business and as such the quality goals are always pretty low.
Edit: typo has => as
I don't know if in most, but in many companies there are no automated tests at all, and very few "best practices" such as automated builds/continuous integration and even decent source control.
I don't want to name names, but I'll say this: I know for a fact at least one IT/support department in the local branch of a HUGE multinational energy company (guess a few names and you'll get it) doesn't do automated tests of any kind. They are similarly clueless about most things tech-minded folk would consider best practices of the last decade. This department doesn't work on core software, but instead on inventory/procurement systems, but still...
“We stopped testing his code five years ago.”
You can take this in many ways, but in the end getting the right amount of testing is a really hard Cost / Benefit problem and there is no one size fit’s all solution.
PS: From the same dev, "I don't trust QA." and "TDD/Code contracts/etc. sounds nice, but knowing what the output should be is really the hard part and tests don't help you with that."
If you don't write tests, then you're throwing away your investment. It's that simple. Telling me that the code works today tells me nothing about how much value the code will add to the business in the next year.
Sooner or later, you won't be able to predict whether a 'small' task will take 3 weeks or 3 years. Even if you don't change the software, some integer overflow might abruptly halt everything. ("It's the primary key for everything? And we don't have tests? Oh...")
Anyone can write code, but it's usually high-risk code with lots of hidden costs. The value that career software developers bring to the table is the ability to manage the software development process so that it's more-or-less sustainable, i.e. keeping costs visible and managing risk so that the resulting software retains its value over time.
If your publicly-traded company depends heavily on software that isn't properly tested, then ethically this should be listed as a risk factor in your 10-K forms. Sooner or later, shareholders are going to figure this out and start holding these enterprises' management accountable.
And on that day publicly traded companies will start to implement comprehensive test suites. Until then they are just extra costs as far as (most) management is concerned and management loves to 'trim the fat' and eliminate costs.
"And I've always been totally willing to hack things apart if I find a different way that fits better or a different partitioning. I've never been a lover of existing code. Code by itself almost rots and it's gotta be rewritten. Even when nothing has changed, for some reason it rots."
"I know engineers, they love to change things"
Relevant passage:
"First, there are architectural problems. The code is not factored correctly. The networking code is popping up its own dialog boxes from the middle of nowhere; this should have been handled in the UI code. These problems can be solved, one at a time, by carefully moving code, refactoring, changing interfaces. They can be done by one programmer working carefully and checking in his changes all at once, so that nobody else is disrupted. Even fairly major architectural changes can be done without throwing away the code. On the Juno project we spent several months rearchitecting at one point: just moving things around, cleaning them up, creating base classes that made sense, and creating sharp interfaces between the modules. But we did it carefully, with our existing code base, and we didn't introduce new bugs or throw away working code."
Killing of wasabi: http://blog.fogcreek.com/killing-off-wasabi-part-1/
Codding Horror on Wasabi: http://blog.codinghorror.com/has-joel-spolsky-jumped-the-sha...
Except when sometimes it does. That is, external factors reveal old bugs or introduce new ones. Like when you use it after installing a new package/driver/OS and it blows up. Or after years you give it to a new crazy user which somehow succeeds it hitting 20 keys at a time and your code doesn't handle that. I'm not saying occasions like those require a complete rewrite, just that it's not because code has been fine for 10years that it is bugfree and needs no more work ever. (though in cases like this switching to a proven lib for handling key input if such a thing even exist might be a solution - which then introduces new problems like more dependencies and bugs in that lib making your own software not working properly and so on and so on:)
http://googleresearch.blogspot.com/2006/06/extra-extra-read-...
There was nothing wrong with the program, it just didn't expect to still be in use 17 years later.
It was this game if you're curious: http://www.dosgamesarchive.com/download/the-black-cauldron/
Wrong. There are entire classes of bugs that have to do with adapting to new environments, security standards, etc.
If someone has a reason to extend the code, then you have the choice of hacking in the new feature, or refactoring the code to make it nicer. Each case really needs to be considered on its merits. But IMO it's almost always better to prefer to keep the code as-is rather than making big rewrites. Tiny refactoring is the way to go.
Stepping back and thinking about the long term is valuable. Doesn't mean you have to do the work now for that potential future but thinking of the potential drawbacks of the current design and it's limitation may guide the decision as to when it's appropriate to do a major refactoring.
Also this particular example seems like something that will probably rarely need modifications. I'd be interested in seeing how often this code changes. My guess is the effort to refactor this will probably be equivalent cost to the future changes.
What you would have to gain is maintainability. Maybe it's not worth it; maybe it is. But saying that there is nothing to be gained is just an opinion.
That code represents years of tweaks, fixes and obscure workarounds. There are countless problems that you will re-introduce with a code rewrite, because the subtleties in aged, thoroughly-used code are not immediately obvious.
Oh, but Jeff Attwood said . . . Whatever. And then Jeff Atwood said something very different.
Blah, blah, blah.
If the engineer that wrote the code isn't at the company anymore, and no one really gets it now, rewrite it. At least you will have some chance of understanding the new bugs instead of failing to understand the old bugs (and there are bugs in that old code that you, for whatever reason can't read.)
Rewrite. Always.
A quick glance shows some obvious errors in RealWaitForChar(). For example: with typical preprocessor defines, it uses gettimeofday() in a select() loop. This will break if the system clock changes, either from user intervention or ntpd. The correct solution is to use a monotonically-increasing timer, such as Linux's clock_gettime() or OS X’s mach_absolute_time().
Vim's UI is powerful, but its codebase is a nightmare. My blog post explains why in more detail.
1. http://geoff.greer.fm/2015/01/15/why-neovim-is-better-than-v...
Could you elaborate a bit? I've never experienced a bug related to keyboard input while using Vim, at least that I know of.
I have only experienced a handful of vim crashes but all of them were caused by plugins.
Microsoft Word doesn't even accept patches, which I do indeed hold against it.
Note: CTRL-S does not work on all terminals and might block further input, use CTRL-Q to get going again.
http://vim.wikia.com/wiki/Map_Ctrl-S_to_save_current_or_new_...
(Caveat emptor, can't speak from first hand experience.)
We had a discussion a few days ago about the ways in which some interfaces (command line in particular) can be user-hostile. (https://news.ycombinator.com/item?id=9831429) Vim's Ctrl-S appears to be a function, keyboard-adjacent to several commonly-used functions, whose main effect for many users is "cause the program to fail immediately with no indication of how to fix it." I don't think I could make up a better example of user-hostile design if I tried.
From the blog post:
That if statement’s conditions span 17 lines and 4 different #ifdefs. All to call gettimeofday(). Amusingly, even the body of that statement has a bug: times returned by gettimeofday() are not guaranteed to increase. User intervention or ntpd can cause the system clock to go back in time. The correct solution is to use a monotonically increasing time function, such Linux’s clock_gettime() or OS X’s mach_absolute_time().
If ntpd finds that the clock is ahead, it will slow the ticks, allowing it to gradually drift back to the correct time.
The system time should NEVER move backwards. Ever. Relying on the system time not moving backwards is not unreasonable for trivialities like how long you wait for a keystroke. If you reset your system time, you might have to hit a key to get vim to wake up. Shucks.
> ntpd guarantees (under the default settings now [...]) that the clock will never go backwards.
Can you elaborate on what changed? I just downloaded the latest reference implementation, and the man page still seems to indicate that the default behavior is to step when error > 128ms for a prolonged period.
At the expense of supporting FEWER platforms.
A wrapper function could hide the extra #ifdef, taking it out of OP's count. A macro defined during compilation would be even better|harder to understand.
This is a bug, if you ask me. Or is that impossible for vim to fix?
For example, if you use tmux, add the following to .tmux.conf:
set -s escape-time 0
Other terminal multiplexors and emulators have different commands for this.Edit: There are a few other possible issues you are having: http://www.johnhawthorn.com/2012/09/vi-escape-delays/
I'm not particularly inclined to port neovim to OpenVMS right now, as there are a few other projects in the queue ahead of that.
When it comes to production servers, I'm more likely to build my editor in my home directory than use my sudo privileges to install my editor in the system.
So developers can be building newest versions of code on old systems. I do.
To state some blatantly obvious facts, people do open source on their own time, they have limited such time and many commitments, and they explicitly allow forks like neovim for the aspects they can't find time for. Vim and neovim have chosen very different design constraints, and there's no reason why they can't continue to exchange code where it makes sense. Putting one of the two down seems unproductive.
The leaders of large projects like Neovim are seldom critical of the competition, and when they express criticism it is measured and proportionate. We should all learn from them.
(Your OP here is still quite useful. Thanks for putting it together.)
Mr. Greer's post was necessarily incendiary; He's building his brand. Wasting space on the front page of HN is a feather in his cap.
If you don't believe my claim of selflessness, believe my claim of competence: If I was trying to build a personal brand, I could do much better than delving into Vim's codebase for a one-off snippet. It's not difficult to create insubstantial content that is widely-shared. I can think of a dozen topics whose mere mention can drive immense traffic and waste absurd amounts of time. HN's front page attests to this.
It's a shame that many people scrutinize others for selfishness. Purely selfish behavior is an extreme rarity, yet it takes much longer to refute such accusations than to make them. I wish it were otherwise.
Before deriding others, please remember: Almost no one is evil. Almost everything is broken.[1]
1. A quote from http://blog.jaibot.com/
I'm sorry if I don't think you're being sincere. Let's have a drink sometime! Let's network!
1. Anything as old as vim, continuously developed, will have any number of these frankenfunctions. They're not hard to find.
Still, others read these comments, so I feel compelled to offer corrections: 18 months ago, I pledged $50 to Neovim's Bountysource. My company has not contributed a cent to Neovim. We maintain plugins for many text editors, including Vim and Neovim. That's it.
You keep slinging mud, but there is no ulterior motive at work here. I urge you to treat people more charitably in the future.
Don't ask for sympathy ("They don't want civility or truth!!!"), ask yourself why you were treated "uncivilized".
Truth? I suspect that's a glitch in translation.
>Don't try and pretend your problems are our problems. Struggling to survive, scrap by scrap. Fuck off. If you're ever in the Silicon Valley we should "meet up". I'll be nicer to you.
You trawled through my writings to find one sentence in one blog post[1] to voice outrage at. I'm not sure how to react to that. I guess... congratulations on your hard work.
I live in the bay area, but I prefer not to hang out with people who treat others so callously. I hope you understand.
Edit: It appears roghummal has significantly revised his comment since I finished my reply.
It doesn't 'appear' that way, that's the way it is. My edit was, just guesstimating here, 25 minutes before your post? Edit was 2-5 minutes after my original post?
You've never said anything you quickly realized was indefensible?
Wrong, even?
Your titles are bait. Their substance is minimal. When you've had something to say it has been said before, better. If you care about the code as much as you claim you should let it go. (I'm questioning this last statement.)
You're a natural born CEO. Find your Woz.
Edit: And then someone to run your company. (I just saw "Judging from this comment and others, you seem uninterested in civility or truth. I doubt anything can change your mind on this issue.")
With the risk of sounding like captain obvious here, but in open source you do not have the luxury to allow or disallow forks, they just happen.
Is it ready to download and run now with all the features?
No. Although some features are a work in progress, Neovim isn't at a stable point. Using Neovim should be done with caution as things may change.
This warning is why I haven't yet tried switching over. Are they just being overly-cautious?
[1]: http://neovim.io
Having your text editor corrupt the file it's working on sounds like the absolute worst case scenario.
Is this fuzzy finder thing some third-party plugin? Do you have any idea why it corrupts the files that you point it at?
A github issue about it https://github.com/junegunn/fzf/issues/206
I use it with true color support enabled, and a terminal that supports true color, and a patched version of tmux that supports true color, and finally all color schemes render as they do in gvim!
Also the terminal emulator feature is awesome.
I've kept vim installed as a fallback so I can easily use vim if I run into anything that is a show-stopper, at least until it's fixed in nvim. I still plan on submitting patches to both for the foreseeable future though.
From the example you give, I agree it must be bad. But if you want to see something worse, check out PHP. No, not the billions of programs written in the PHP language (which are, indeed, almost all terrible), but the C source to the PHP interpreter.
Obviously PHP was invented by someone who just doesn't care about creating a decent programming language. Everyone knows that; it has been pointed out so many times that it's a cliche. But if you look at the source to the implementation, you will find that it was developed by someone who is so incompetent that whether they care or not is beside the point. Dealing with that codebase (and thinking about how successful PHP has become) made me question my will to live.
The interesting thing about PHP (the program) is that, in my experience, the code doesn't suck because it was hastily written or because it was written a long time. It sucks because the people who write it and work on it have bizarre, nonsensical philosophies about writing code. I've seen some talks by the PHP maintainers (as recently as 2013) that made me want to throw something at my monitor.
https://en.wikiquote.org/wiki/Rasmus_Lerdorf
And also here where he makes a breaking commit without running unit tests first:
http://www.reddit.com/r/programming/comments/jsudd/you_see_r...
As an aside, I think it's a growing problem that the formulae in Homebrew are not updated to support 10.6.8.
And Vim? It just works. Maybe it's all those #ifdefs that make the code look ugly...
The "#ifdef maze" in OpenSSL was criticized by the OpenSSH/OpenBSD guys as a key part of the picture that allowed something like Heartbleed to happen: http://blather.michaelwlucas.com/archives/2071
Tons of ifdefs make it much more difficult to ensure that all permutations of flags are correct, or can even compile. I think I remember reading that OpenSSL wasn't even capable of being compiled with the standard, system malloc(). No one compiled it that way, so that build configuration broke without anyone knowing it.
I started using a SDL2-only fork of the same library (just one backend! near-0 ifdef count!) and suddenly all those nasty problems went away.
I love vim and have looked at contributing a number of times but the sheer amount of cruft creates a huge barrier.
...is exactly the type of thinking that results is countless of wasted developer time wading through unmaintainable crap, which is never touched because "the code works" and everyone's to damn afraid to change it because it's so excessively complex because no-one bothered to go back and refactor it for readability, maintainability, modern libraries, or anything else.
"working" isn't good enough.
And is more portable, maintainable etc. And criticizing code is not exactly the same thing as saying it should be re-written, given the time investment and low yield.
Yes the code works, but don't write code like this every again.
When the team found that the function was impossible to modify to accommodate C99 format string changes, they undertook a lengthy project to factor out the #ifdef'd features using abstractions available in C++. Not only were they able to turn the code into something much easier to modify, but they also fixed multiple hidden performance bottlenecks, so the end result actually ran 5x faster than the C version.
Interesting Fermi problem for sadistic interviewers: what would be the global energy saving per invocation of this vim function if a 5x performance improvement were to be found?
Encapsulate your #ifdefs people!
IMHO I'd rather scroll over code I'm not interested in, than have to jump around to figure out what really happens if they are defined.
Its not really the classic case of premature optimization either. At least not in my experience. That's more like 'I know I need a spatial partition here, time to research all the ways they can be implemented and their performance tradoffs, and implement a really good one" when you should have just used a hash table and done the other stuff if spatial lookups even show up when profiling.
Stuff like this is way more damaging for both codebase complexity and productivity then anything else. The golden rule is to only do enough optimization to make it easy to do the optimizations you might need to do later, but no more.
Decomposing this function into different implementations with appropriate abstraction doesn't need to break compatibility.
That said, how does one go about submitting a "fix" for such a widely deployed software? Like, before submitting, I need to make sure it works on Amiga, VMS, Irix... and then all the current platforms. That is, like, a pretty high barrier... who in the world has all these platforms handy for testing? You submit a patch and someone says that it has a problem on Xenix 2.37.4 -- what do you do?
This was the OpenBSD project's testing rack in 2009: http://www.openbsd.org/images/rack2009.jpg
Of course, it's possible that some aspect of vim will, say, create a temp file insecurely as root, or read random shit out of /proc, or what have you, and be exploitable. But the historical security problems in vim (and there have been several!) have mostly had to do with editing maliciously-composed files with it.
if (msec > 0 && (
# ifdef FEAT_XCLIPBOARD
xterm_Shell != (Widget)0
# if defined(USE_XSMP) || defined(FEAT_MZSCHEME)
||
# endif
# endif
# ifdef USE_XSMP
xsmp_icefd != -1
# ifdef FEAT_MZSCHEME
||
# endif
# endif
# ifdef FEAT_MZSCHEME
(mzthreads_allowed() && p_mzq > 0)
# endif
))
When they could have written: if (msec > 0 && (
# ifdef FEAT_XCLIPBOARD
xterm_Shell != (Widget)0 ||
# endif
# ifdef USE_XSMP
xsmp_icefd != -1 ||
# endif
# ifdef FEAT_MZSCHEME
(mzthreads_allowed() && p_mzq > 0) ||
# endif
0))
Perhaps they were targeting compilers so primitive that they could not optimize out a "|| 0".(edited to hide my shame)
I was thinking that using `0` would cause a problem when you select none of those compilation options, but then I realize the code won't even compile if you don't select at-least one (You get empty parenthesis fallowing the '&&' in the 'if' statement), so it doesn't seem like a big deal.
Open Vim. Type "1.02". Put the cursor on the 2. Hit C-x; get 1.01. Again; get 1.00. Again; get 1.01777777777777777777777.
- Gary Bernhardt (@garybernhardt)No one knows the circumstances that created this original code. The developer may have been working on 10 projects and threw something together just so it'd work.
If it ain't broke....
Of course, technical debt builds up, and eventually you're badly locked in until you refactor, so it's a balancing act.
http://tomasp.net/blog/2014/why-coeffects-matter/
It would be so amazing to be able to take all of those ifdefs and turn them into type variants and have the compiler enforce the safety and semantics of the system. And it would be phenomenal for old code to break at compile time when the environment changes, instead of waiting for a bug report to roll in.
So, paging T. Petricek...when are we gonna get it in F#? :) Even better if someone can get it into Rust! That is the perfect type of feature for a systems programming language.
Also note that RealWaitForChar has been completely removed from neovim since April of 2014 (https://github.com/neovim/neovim/pull/474/files)
I suppose there may be some negative performance impact if we need to use it for an automated/batch workflow (I can't think of many where something like 'sed' won't be better suited).
Generally speaking, a lot of the cross-platform stuff in the Linux Kernel is handed this way, and done correctly there shouldn't be any over-head.
edit: I read this [1] and maybe the ugly isn't localized =P
edit2: since you're reading this, thank you for ag. I use it every day.
[1] http://geoff.greer.fm/2015/01/15/why-neovim-is-better-than-v...
One way of tidying up the inner parts a bit might be by splitting each FD handling code into an init part, and a part that checks for input and a part that checks for error. For example, here's the code for XSMP, whatever that is. This approach is pretty easy, because you can do it with copy and paste. That's exactly how I did it and that's how I can actually present you code:
#ifdef USE_XSMP
#define InitXSMP() \
if (xsmp_icefd != -1) \
{ \
xsmp_idx = nfd; \
fds[nfd].fd = xsmp_icefd; \
fds[nfd].events = POLLIN; \
nfd++; \
}
#define ShouldCheckXSMP() (xsmp_idx >= 0)
#define IsXSMPInput() (xsmp_idx >= 0 && (fds[xsmp_idx].revents & POLLIN))
#define IsXSMPError() (xsmp_idx >= 0 && (fds[xsmp_idx].revents & POLLHUP))
#else
#define InitXSMP() \
if (xsmp_icefd != -1) \
{ \
FD_SET(xsmp_icefd, &rfds); \
FD_SET(xsmp_icefd, &efds); \
if (maxfd < xsmp_icefd) \
maxfd = xsmp_icefd; \
}
#define ShouldCheckXSMP() (xsmp_icefd != -1)
#define IsXSMPInput() (xsmp_icefd != -1 && FD_ISSET(xsmp_icefd, &efds))
#define IsXSMPError() (xsmp_icefd != -1 && FD_ISSET(xsmp_icefd, &rfds))
#endif
#endif
(You could argue about this - for example, should ShouldCheckXSMP() maybe always just be ``(xsmp_icefd!=-1)''? - but the way the code is written, this puts all the details in one place, and the logic in another.)Then the init code would have this bit:
#ifdef USE_XSMP
InitXSMP();
#endif
And after your poll/select code - which you'd similarly hide in a function or a macro, which I've here assumed sets a flag called `any_events' to say that there were any events that might need looking at - you'd do the business like this: #ifdef USE_XSMP
if (any_events && ShouldCheckXSMP())
{
if (IsXSMPInput())
{
busy = TRUE;
xsmp_handle_requests();
busy = FALSE;
if (--ret == 0)
finished = FALSE; /* keep going if event was only one */
}
else if (IsXSMPError())
{
if (p_verbose > 0)
verb_msg((char_u *)_("XSMP lost ICE connection"));
xsmp_close();
if (--ret == 0)
finished = FALSE; /* keep going if event was only one */
}
}
#endif
(usual disclaimers for forum post code apply.)So: the actual logic is handled in one place, whether or not you're using poll and select, which woud be my key criticism of the code as it stands. And I don't mind having code like this in a #ifdef, if it's only one level deep, particularly if it's somewhat formulaic, which this function would end up being if you approached it this way.
Then repeat for all the parts, and do a bit of work to declare the right variables at the top of the function (something I've just completely ignored).
If you'd prefer to be able to step through it in your average debugger - which tends to do a poor job with #defines - you could do the above with functions, but you'd probably need to move all the state into a struct so that you could pass it around more easily.
Perhaps you could have a mini wrapper for poll and select - for this sort of level of use I'd probably write something local to the file, since it's not so much a separate layer, or a library, or what have you, as just some little helper functions to stop the calling code becoming too awful.
You could always have some kind of extensible function pointer-based system whereby a given descriptor has a callback to be invoked if its FD had an error or has input, which would give you the opportunity to have each subsection of the code supply a low-level function in its own file. (For example, say xsmp_icefd is global only because this function needs to use it - now it could be static to the xsmp support file, which would need only expose a function that would be called from here when input was available or there was an error.)
And so on, and so on. I've worked on this sort of thing quite a lot over the years. There's always a way of doing things that doesn't involve a huge gnarly pile of nested #ifdefs and control structures inside #ifdefs. Either of those, let alone both together, are a good sign that you've taken a wrong turning somewhere.
(Some people are doctrinaire about never including any platform-specific #ifdefs anywhere in the first place. I'm not - but those people definitely do have a point.)
> NOTE: Don't use ANSI style function declarations. A few people still have to use a compiler that doesn't support it.
It's a feature Go has!
I find it fascinating to look at how old but very well used software develops (often, but not always) in ways that seem completely ghastly.
Output processing in JOE is interesting: when writing to the terminal, JOE periodically sleeps for a time based on the baud rate and amount of data. This is to prevent too much output buffering so that at low baud rates you can interrupt the screen refresh with type-ahead. If you don't do this at 1200 baud it's a big issue (you could wait for hours if you hit PageDown too many times).
I'm going to be doing some hacking on z80 assembly via Yaze, and it would be great if I could have the editor I'm used to, natively on the platform (rather than having to edit in OSX or whatever).
this is a monster that has grown over time without any proper love.
I wish all code was equally buggy.
There are people working on BeOS revival: Haiku OS, they would (rightfully) quite annoyed if vim dropped support for the OS they're trying to resurrect..
It's getting old.
Also, you can point out that something is broken even if you have no suggestions on how to fix it.
reference: https://www.openhub.net/p/vim https://www.openhub.net/p/emacs
True enough.
> So is Eclipse.
No, Eclipse must be killed with fire.