From 5997a9d741721523bf96e15443c63fe88147fdbe Mon Sep 17 00:00:00 2001 From: Cassie Jones Date: Sat, 2 Nov 2019 01:26:09 -0400 Subject: [PATCH] Add a blog post about rust 0x a presses --- posts/2019/hello-world-in-0x-a-presses.md | 258 ++++++++++++++++++++++ 1 file changed, 258 insertions(+) create mode 100644 posts/2019/hello-world-in-0x-a-presses.md diff --git a/posts/2019/hello-world-in-0x-a-presses.md b/posts/2019/hello-world-in-0x-a-presses.md new file mode 100644 index 0000000..2de674b --- /dev/null +++ b/posts/2019/hello-world-in-0x-a-presses.md @@ -0,0 +1,258 @@ +--- +title: "Hello World, in 0x A Presses" +date: 2019-11-02 +summary: "Write a Hello World program in Rust, without ever pressing the A key, and learn some things along the way." +--- + +### The (Mario) A Button Challenge + +In the Super Mario 64 speedrunning scene, many people have gotten good enough at the game that doing normal speedrunning isn't challenging enough anymore. +In order to add tha difficulty back in, there are many "Category Extensions" that add some extra challenge on top. +One of the most interesting of these is the A Button Challenge, where runners attempt to play the game pressing the A button as few times as possible. +Since the A button is the jump button, and this is a game about jumping, this leads to quite a few creative solutions and alternate routes. +"Watch For Rolling Rocks - 0.5x A Presses" is an excellent video showing one of those routes. +If you haven't watched it before, it's much more interesting than anything I can write, watch it first :). + +{% youtube video="kpk2tdsPh0A" %} + +### The (Programming) A Button Challenge + +In mid-October I tweeted a joke about doing the "Advent of Code" A Button Challenge. + +{% tweet name="genderAlgebraist", at="porglezomp", date="2019-10-16", + link="https://twitter.com/porglezomp/status/1184561096859869184" %} +Advent of Code, A Button Challenge: solve all programs without ever writing the letter A in any of them. +{% endtweet %} + +This inspired some discussion in the replies by Swift compiler folks discussing ways to try to handle programs `var`. +It was a lot of fun! + +Just a few days later I was at [Rust Belt Rust][rbr]. +After lunch, I was hanging out in the hall with some people, and we were sharing jokes, and I made the Rust A Button Challenge joke. +At first it doesn't sound too bad, until you realize that `main` has an `a` in it, and so you're immediately in trouble. +We started thinking through different approaches, and eventually got a working solution, and eventually a polished solution. +Doing this looks at a wide variety of interesting topics in Rust, and so it's worth sharing. + +[rbr]: https://rust-belt-rust.com/ + +### Hello, Rust! (Without the Prohibited Letter!) + +So, we want to write Hello World. +Let's make a project. + +```bash +$ cargo init button +``` + +And we've already hit trouble. +We can't even write `cargo`. +Luckily, bash has out backs here, and we can use command substitution with `printf` to get it. + +```bash +$ $(printf 'c\141rgo init button') +``` + +Because this is such a mechanical replacement, for legibility I'm gonna just write the normal commands even when they would include an `a`, and you can assume that we're using the `printf` method. +Technically, this already just wrote a Hello World for us, in `main.rs`, due to the default project. +Let's say, though, that this doesn't meet the spirit of the challenge, and we have to rewrite it. +We can't write it the usual way, with: + +```rust +fn main() { + println!("Hello, World!"); +} +``` + +Because `main` has an `a` right there! +So, we'll have to get more creative. +There are a few ways to override the entry point if you don't want to use Rust's default `main`. +We could try... + +``` +#[start] +``` + +Well that won't work. +And even if it did, it needs a `#![feature]` to enable it, which would cause trouble on its own. + +Libraries have ways to have global symbols that are called at the start. +There are various mechanisms on different platforms, but they're all troublesome. +If we wanted to name a function `__init` (which is one of the Linux symbols for the purpose) and have it be recognized, then we'd need either `#[link_name]` or `#[no_mangle]`, both of which needs an `a`. +There are ways to get the correct setup using a `pub static`... but that also uses an `a`. +So, in the end we've exhausted all the options here. + +What we really want is some attribute which adds a new, custom entry point, we want it to be in the standard library, since otherwise we could cheat by using some external macro written with the letter `a` that does all the work for us. +We can't write one ourselves *without* using the letter `a`, because we'd have to write `proc_macro`. +But, it turns out, we don't need to, since one of these has been sitting in front of us all along. + +```rust +#[test] +``` + +Let's write our first working canidate for Hello World! + +```rust +#[test] +fn hello() { + println!("Hello, World!"); +} +``` + +And now we can run it: + +```bash +$ cargo test +running 1 test +test test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out + + Doc-tests abc + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +Ah right, we need `--nocapture` if we want to see our output. + +```bash +$ cargo test -- --nocapture +Hello, World! +test test ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out + + Doc-tests abc + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +So we've got Hello World for sure, but we also have a bunch of other junk too. +We can clean that up slightly with a few more flags, `--lib` will get rid of the doctest, and `--quiet` will remove slightly more. + +```bash +$ cargo test --lib --quiet -- --nocapture + +running 1 test +Hello, World! +. +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out +``` + +And that's about as clean as we can get with command line arguments. +This still isn't great. +But we can do better. +First, let's avoid that extra text afterward. +Rust's tests are run in-process, so if we just exit immediately, then we clean things up a bit more. +Now, with this program: + +```rust +#[test] +fn test() { + println!("Hello, World!"); + std::process::exit(0); +} +``` + +We get a much cleaner output: + +``` + +running 1 test +Hello, World! +``` + +We can deal with that initial text as well, too. +You can expect that essentially all terminals implement [ANSI escape sequences][ansi], which allow you to control the cursor, mess with text, and more. +In this case, we want to use the sequence `[A`, which is "Cursor Up." + +[ansi]: http://ascii-table.com/ansi-escape-sequences.php + +> Moves the cursor up by the specified number of lines without changing columns. If the cursor is already on the top line, ANSI.SYS ignores this sequence. + +Using this, we can go back to the previous lines and overwrite them with spaces, and then go back yet again to write on the first line. + +```rust +#[test] +fn test() { + print!("\x1b[1\x41 \x1b[1\x41\r"); + println!("Hello, World!"); + std::process::exit(0); +} +``` + +This works in 4 steps. +First, `\x1b[1\x41` moves up by one line. +Then, the sequence of spaces overwrites the existing text on the line. +We repeat the `\x1b[1\x41` sequence to move up on the the first line, and then finally use `\r` to go back to the beginning of the line. +Now text can write from there and it will all be hidden. +Finally, this prints only: + +```bash +$ cargo test +Hello, World! +``` + +### Not Done Yet? + +Ok. +We've written our program without using the A key, but maybe you're not satisfied. +We've got a big 'ol `Cargo.toml` with an `a`, and plenty of `cargo` on the command line. +Sure we never have to type any of that, but maybe you think that's against the spirit of the thing? +After all, I've been using bash here, but using a substitution as your command flat-out doesn't work in Fish, my shell of choice. +If you want to do that, you need `eval`... and you can see where this breaks down. + +Luckily, we don't *have* to use Cargo. +We can use `rustc`, a compiler which graciously has no `a` in its name. +Let's start with the program we finished with in the last section. + +```bash +$ vim hello.rs # Type it in without using the forbidden letter! +$ rustc --test hello.rs +$ ./hello + +running 1 test +``` + +Hm +This time we can't write `--nocapture`, so we don't get any output, and we don't overwrite the harness text. +The test harness is capturing out stdout, so we need to get it back. +This calls for one final trick: let's reopen `stdout` again. + +```rust +use std::io::Write; + +#[test] +fn test() { + let mut f = std::fs::File::open("/dev/stdout").expect("stdout"); + writeln!(f, "\x1b[1\x41 \x1b[2\x41\r") + .expect("write"); + writeln!(f, "Hello, World!").expect("write"); + std::process::exit(0); +} +``` + +Running this, we get: + +```bash +$ rustc --test hello.rs +$ ./hello --quiet +Hello, World! +``` + +Truly in 0x A presses. + +Thanks to [Pannenkoek2012][] for popularizing the Super Mario 64 A Button Challenge, and making the amazing video. +Thanks to [@quietmisdreavus][] for giving suggestions for flags to use when testing with Rust, and [@myrrlyn][] for suggesting ANSI escapes. +You can take a look at [many revisions of a gist containing the program][gist]. + +If you try an A Button Challenge in your programming language of choice, [send me a tweet][] telling me about it and what was difficult! + +[Pannenkoek2012]: https://www.youtube.com/user/pannenkoek2012 +[@quietmisdreavus]: https://twitter.com/quietmisdreavus +[@myrrlyn]: https://twitter.com/myrrlyn +[send me a tweet]: https://twitter.com/porglezomp +[gist]: https://gist.github.com/porglezomp/68f4d9e7be29b758284a7b897269c718/revisions -- 2.43.2