├── .editorconfig ├── .gitignore ├── .gitmodules ├── README.md ├── day1 ├── main.asm └── run.sh ├── day10 ├── Main.j └── run.sh ├── day11 ├── main.kk └── run.sh ├── day12 ├── main.lean └── run.sh ├── day13 ├── main.mojo └── run.sh ├── day14 ├── main.nim └── run.sh ├── day15 ├── .ocamlformat ├── main.ml └── run.sh ├── day16 ├── day16 ├── main.pony └── run.sh ├── day17 ├── main.qi └── run.sh ├── day18 ├── main.raku └── run.sh ├── day19 ├── main.swift └── run.sh ├── day2 ├── main.bqn └── run.sh ├── day20 ├── main.tcl └── run.sh ├── day23 ├── Cargo.toml ├── run.sh └── src │ └── main.rs ├── day24 ├── day24.yue └── run.sh ├── day25 ├── main.zig └── run.sh ├── day3 ├── main.carp └── run.sh ├── day4 ├── main.dfy ├── package-lock.json ├── package.json └── run.sh ├── day5 ├── main.erl └── run.sh ├── day6 ├── .gitignore ├── flix.toml ├── run.sh └── src │ └── Main.flix ├── day7 ├── .gitignore ├── gleam.toml ├── manifest.toml ├── run.sh └── src │ └── day7.gleam ├── day8 ├── app │ └── Main.hs ├── day8.cabal └── run.sh ├── day9 ├── main.io └── run.sh ├── justfile └── load_data.py /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | trim_trailing_whitespace = true 5 | indent_style = space 6 | 7 | [justfile] 8 | tab_width = 2 9 | 10 | [*.{c,sh,mjs,wast,zsh,ts,json,scala,rb,sql,pl,ml,nim,moon,lisp,idr,hs,cabal,fsx,exs,dart,cr,js,bqn,carp,dfy,io,kk,swift,yue}] 11 | tab_width = 2 12 | 13 | [*.{py,zig,S,kt,kts,jl,erl,j,mojo,tcl}] 14 | tab_width = 4 15 | 16 | [*.{v,go}] 17 | tab_width = 4 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .env 3 | input.txt 4 | 5 | # Day 1 6 | main 7 | 8 | # Day 3 9 | carp-*/ 10 | out/ 11 | 12 | # Day 4 13 | day4/main.js 14 | day4/main.out.js 15 | node_modules/ 16 | 17 | # Day 5 18 | main.beam 19 | erl_crash.dump 20 | 21 | # Day 8 22 | dist-newstyle/ 23 | 24 | # Day 10 25 | jasm-0.7.0/ 26 | *.class 27 | 28 | # Day 11 29 | .koka/ 30 | 31 | # Day 15 32 | main.cmi 33 | main.cmx 34 | main.o 35 | _build/ 36 | 37 | # Day 17 38 | day17/build/ 39 | 40 | # Day 23 41 | target/ 42 | Cargo.lock 43 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "day2/CBQN"] 2 | path = day2/CBQN 3 | url = https://github.com/dzaima/CBQN 4 | [submodule "day2/bqn-libs"] 5 | path = day2/bqn-libs 6 | url = https://github.com/mlochbaum/bqn-libs 7 | [submodule "day17/qi"] 8 | path = day17/qi 9 | url = https://github.com/AnonymousAAArdvark/qi 10 | [submodule "day24/Yuescript"] 11 | path = day24/Yuescript 12 | url = https://github.com/pigpigyyy/Yuescript 13 | [submodule "day24/lua-bint"] 14 | path = day24/lua-bint 15 | url = https://github.com/edubart/lua-bint 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aoc23-alpha 2 | 3 | _Advent of Code 2023 solved with 25 languages, in alphabetical order._ 4 | 5 | Languages are how we express and develop computational ideas; a dialogue between the programmer & designer. I know most of the popular ones, so I picked some cool specimens that I'm curious about. 6 | 7 | I did a similar thing once before [in 2021](https://github.com/ekzhang/aoc21-alpha), but that challenge had relatively tamer languages. This year will be a little more “out there.” 8 | 9 | ## Schedule 10 | 11 | As usual, the hardest problems are at the end, so it's a balance between coolness and the practicality of me actually being able to solve the challenges. 12 | 13 | 1. `A` is for **ARM64 Assembly** — [ARMv8.5-A](https://en.wikipedia.org/wiki/AArch64#ARMv8.5-A) 14 | 2. `B` is for **BQN** — _finally, an APL for your flying saucer_ 15 | 3. `C` is for **Carp** — _a statically typed lisp, without GC, for real-time applications_ 16 | 4. `D` is for **Dafny** — _a verification-ready programming language_ 17 | 5. `E` is for **Erlang** — _practical functional programming for a parallel world_ 18 | 6. `F` is for **Flix** — _polymorphic effect system and first-class Datalog constraints_ 19 | 7. `G` is for **Gleam** — _a friendly language for building type-safe systems that scale_ 20 | 8. `H` is for **Haskell** — [GHC 9.8.1](https://www.haskell.org/ghc/) 21 | 9. `I` is for **Io** — _dynamic prototypes in the same realm as Smalltalk and Self_ 22 | 10. `J` is for **Java bytecode** — _write once, run anywhere_ 23 | 11. `K` is for **Koka** — _functional language with effects_ 24 | 12. `L` is for **Lean** — _interactive theorem prover_ 25 | 13. `M` is for **Mojo** — _Python with systems and metaprogramming features_ 26 | 14. `N` is for **Nim** — _compiled, garbage-collected systems programming_ 27 | 15. `O` is for **OCaml** — [OCaml 5.1.0](https://ocaml.org/releases/5.1.0) 28 | 16. `P` is for **Pony** — _capabilities-secure, high-performance actors_ 29 | 17. `Q` is for **Qi** — _lightweight, fast interpreted language written in Chinese_ 30 | 18. `R` is for **Raku** — _formerly known as Perl 6_ 31 | 19. `S` is for **Swift** — [Swift 5.8.1](https://www.swift.org/blog/swift-5.8-released/) 32 | 20. `T` is for **Tcl** — _a very simple programming language_ 33 | 21. `U` is for **Unison** — _purely functional language for distributed systems_ 34 | 22. `V` is for **Vale** — _single ownership with constraint references for memory safety_ 35 | 23. `W` is for **WebAssembly** — [Wasmtime v15](https://wasmtime.dev/) with Rust 36 | 24. `Y` is for **Yuescript** — _a Moonscript dialect that compiles to Lua_ 37 | 25. `Z` is for **Zig** — _general-purpose systems, without garbage collection_ 38 | 39 | We're skipping X because the only reasonable language I could find was x86-64 assembly, and I'm definitely not solving day 23 in assembly! 40 | 41 | ## Development 42 | 43 | First, create a `.env` file containing your session token from the Advent of Code website, so that the input data can be downloaded. For example: 44 | 45 | ``` 46 | SESSION=30b5d4e5790f02d4c32c71f59f10d5f2f6adfcf5b4c064c64a689ab02b4beb3e84bf74857e40cc9fe31088972fedeb64 47 | ``` 48 | 49 | Then, if you have [Python 3](https://python.org/) and [Just](https://github.com/casey/just) installed, as well as the language runtime for a given day's solution, you can load the input data and run the solution with: 50 | 51 | ``` 52 | just run 53 | ``` 54 | 55 | Each day's solutions are located in their respective folder `dayN`. The source code reads from standard input, and it is executed using the script `run.sh`. 56 | 57 | ## Complete Run 58 | 59 | If you have all of the required packages for the 25 languages installed, you can run all of the solutions sequentially with the command: 60 | 61 | ``` 62 | just run-all 63 | ``` 64 | 65 | ## Runtime Environment 66 | 67 | This is my runtime environment for each language on macOS Ventura v13.2.1, M1 / ARM64 processor, with Rosetta 2 and Xcode CLT. I only used languages that I could install on my own machine; these instructions aren't guaranteed to work on other operating systems or processor architectures. 68 | 69 | - **Day 1:** Apple clang version 14.0.3, target arm64-apple-darwin22.3.0 70 | - **Day 2:** [CBQN 0.4.0](https://github.com/dzaima/CBQN), vendored as submodule and built with Make 71 | - **Day 3:** [Carp 0.5.5](https://github.com/carp-lang/Carp), binary installation in script 72 | - **Day 4:** [Dafny 4.3.0](https://github.com/dafny-lang/dafny) via VSCode extension, with dotnet-sdk 8.0.100 installed via Homebrew Cask 73 | - **Day 5:** Erlang/OTP 26.0.2, installed from Homebrew 74 | - **Day 6:** [Flix 0.42.0](https://github.com/flix/flix/releases/tag/v0.42.0), included in script, with Java runtime OpenJDK 21.0.1 from Homebrew 75 | - **Day 7:** [Gleam 0.32.4](https://gleam.run/), installed from Homebrew, with Erlang/OTP 26.0.2 76 | - **Day 8:** GHC 9.8.1, Cabal 3.10.2.0, installed via GHCup 0.1.20.0; with LLVM 12.0.1 77 | - **Day 9:** Io Programming Language, v. 20151111 installed via Homebrew for x86-64 78 | - **Day 10:** Jasmin v2.4, from Homebrew, with OpenJDK 21 79 | - **Day 11:** [Koka 2.4.2](https://koka-lang.github.io/koka/doc/book.html), installed from the script on the website 80 | - **Day 12:** Lean 4.3.0, installed with the [elan](https://github.com/leanprover/elan) version manager, via Homebrew 81 | - **Day 13:** Mojo 0.6.0 (d55c0025), via proprietary installer 82 | - **Day 14:** Nim 2.0.0, from Homebrew 83 | - **Day 15:** OCaml 4.12.0, installed via opam 2.1.0, from Homebrew 84 | - **Day 16:** ponyc-release-0.55.1-arm64-darwin, from ponyup nightly-20230822, with manually patched symlinks for libressl `libcrypto.50.dylib -> libcrypto.52.dylib` and `libssl.53.dylib -> libssl.55.dylib` 85 | - **Day 17:** [Qi 21b3195](https://github.com/AnonymousAAArdvark/qi/tree/21b3195bb315b4cec0568f2814cc32d940b03657), vendored as submodule and built with Make 86 | - **Day 18:** Rakudo 2023.11 from Homebrew, implementing Raku 6.d 87 | - **Day 19:** Apple Swift 5.8.1, targeting arm64-apple-macosx13.0 88 | - **Day 20:** Tcl 8.6.13 from Homebrew 89 | - **Day 21:** TBD 90 | - **Day 22:** TBD 91 | - **Day 23:** Rust 1.72.1, with Wasmtime 16.0.0 92 | - **Day 24:** Yuescript 0.21.3, vendored as submodule and built with Make, and Lua 5.4.6 93 | - **Day 25:** Zig 0.11.0, from release binary (zigup) 94 | 95 | Note that while exact version numbers are provided above, the code will likely work with newer versions of these languages as well. Also, assume a global dependency on Python 3.11+, Node v20, and NPM v9. 96 | -------------------------------------------------------------------------------- /day1/main.asm: -------------------------------------------------------------------------------- 1 | .global _main 2 | .balign 8 3 | _main: 4 | // Set up the stack frame 5 | stp x29, x30, [sp, -16]! 6 | mov x29, sp 7 | 8 | // Allocate 64 bytes on the stack 9 | // [x29, -8]: array address 10 | // [x29, -16]: sum of calibration values (part 1) 11 | // [x29, -24]: sum of calibration values (part 2) 12 | // [x29, -32]: unused 13 | // ... 14 | // [sp]: varargs 15 | sub sp, sp, 64 16 | 17 | // Allocate the array 18 | mov x0, 1000 19 | bl _malloc 20 | str x0, [x29, -8] 21 | 22 | // Initialize locals 23 | mov x0, 0 24 | str x0, [x29, -16] 25 | str x0, [x29, -24] 26 | 27 | loop1_start: // For each line in input 28 | adrp x0, scanf1@PAGE 29 | add x0, x0, scanf1@PAGEOFF 30 | 31 | ldr x1, [x29, -8] // array address... 32 | str x1, [sp] // placed in vararg for scanf() 33 | 34 | bl _scanf // scanf("%999s\n", word); 35 | cmp x0, 1 36 | b.ne loop1_end 37 | 38 | // Part 1 39 | ldr x0, [x29, -8] // array address 40 | // bl _printf // printf(word); 41 | mov x1, 0 42 | bl _get_calibration_value 43 | ldr x1, [x29, -16] 44 | add x1, x1, x0 45 | str x1, [x29, -16] 46 | 47 | // Part 2 48 | ldr x0, [x29, -8] // array address 49 | mov x1, 1 50 | bl _get_calibration_value 51 | ldr x1, [x29, -24] 52 | add x1, x1, x0 53 | str x1, [x29, -24] 54 | 55 | b loop1_start 56 | 57 | loop1_end: 58 | adrp x0, printf2@PAGE 59 | add x0, x0, printf2@PAGEOFF 60 | ldr x1, [x29, -16] 61 | ldr x2, [x29, -24] 62 | stp x1, x2, [sp] 63 | bl _printf // printf("%d\n%d\n", sum1, sum2); 64 | 65 | // Set the return value to 0 66 | mov x0, 0 67 | 68 | // Clean up the stack frame and return 69 | mov sp, x29 70 | ldp x29, x30, [sp], 16 71 | ret 72 | 73 | 74 | .global _get_calibration_value 75 | .balign 8 76 | _get_calibration_value: // int get_calibration_value(const char* word, bool part2) 77 | stp x29, x30, [sp, -16]! 78 | mov x29, sp 79 | 80 | // Allocate 64 bytes on the stack 81 | // [x29, -8]: current *word pointer 82 | // [x29, -16]: bool part2 83 | // [x29, -24]: first digit in word (or -1) 84 | // [x29, -32]: last digit in word (or -1) 85 | // [x29, -40]: inner loop pointer into *numerals array 86 | // [x29, -48]: numeral temp storage 87 | // [x29, -56]: unused 88 | sub sp, sp, 64 89 | 90 | // Initialize locals 91 | stp x1, x0, [x29, -16] // Save arguments 92 | mov x0, -1 93 | stp x0, x0, [x29, -32] // w7 = -1, w8 = -1 94 | 95 | loop2_start: // For each character in word 96 | ldr x0, [x29, -8] 97 | mov x1, 0 98 | ldrb w1, [x0], 1 // w1 = *word++ 99 | str x0, [x29, -8] 100 | 101 | cmp w1, 0 102 | b.eq loop2_end 103 | 104 | cmp w1, '0' 105 | b.lt loop2_alpha 106 | cmp w1, '9' 107 | b.gt loop2_alpha 108 | 109 | sub w1, w1, '0' 110 | b loop2_found_number 111 | 112 | loop2_alpha: // Check if it is a numeral using string comparison 113 | ldr x0, [x29, -16] 114 | cmp w0, 0 115 | b.eq loop2_start // part 1, so we skip this step 116 | 117 | adrp x0, numerals@PAGE 118 | add x0, x0, numerals@PAGEOFF 119 | str x0, [x29, -40] // Store pointer to numerals array 120 | 121 | loop3_start: // For each numeral in numerals array 122 | ldr x0, [x29, -40] 123 | ldr x1, [x0], 8 // x1 = *numerals++ 124 | str x0, [x29, -40] 125 | 126 | cmp x1, 0 127 | b.eq loop2_start // Failed to find a matching numeral, continue 128 | 129 | str x1, [x29, -48] // Store numeral in temp storage 130 | mov x0, x1 131 | bl _strlen // x2 = strlen(numeral) 132 | mov x2, x0 133 | 134 | ldr x0, [x29, -8] 135 | sub x0, x0, 1 // cancel out the previous increment (oops) 136 | ldr x1, [x29, -48] 137 | bl _strncmp // strncmp(word, numeral, strlen(numeral)); 138 | cmp x0, 0 139 | b.ne loop3_start // Go to the next numeral 140 | 141 | // Found a matching numeral 142 | adrp x0, numerals@PAGE 143 | add x0, x0, numerals@PAGEOFF 144 | ldr x1, [x29, -40] 145 | sub x1, x1, x0 // x1 = index of numeral in numerals array 146 | mov x0, 8 147 | udiv x1, x1, x0 148 | b loop2_found_number 149 | 150 | loop2_found_number: // The numeric digit is now stored in w1 151 | ldr x0, [x29, -24] 152 | cmp w0, -1 153 | b.ne loop2_if_skip 154 | str x1, [x29, -24] 155 | loop2_if_skip: 156 | str x1, [x29, -32] 157 | b loop2_start 158 | 159 | loop2_end: 160 | ldp x8, x7, [x29, -32] 161 | mov x0, 10 162 | mul w0, w0, w7 163 | add w0, w0, w8 164 | 165 | mov sp, x29 166 | ldp x29, x30, [sp], 16 167 | ret 168 | 169 | 170 | .data 171 | scanf1: 172 | .asciz "%999s\n" 173 | printf2: 174 | .asciz "%d\n%d\n" 175 | 176 | // Numerals for part 2 177 | one: 178 | .asciz "one" 179 | two: 180 | .asciz "two" 181 | three: 182 | .asciz "three" 183 | four: 184 | .asciz "four" 185 | five: 186 | .asciz "five" 187 | six: 188 | .asciz "six" 189 | seven: 190 | .asciz "seven" 191 | eight: 192 | .asciz "eight" 193 | nine: 194 | .asciz "nine" 195 | .balign 8 196 | numerals: 197 | .xword one, two, three, four, five, six, seven, eight, nine, 0 198 | -------------------------------------------------------------------------------- /day1/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | clang -o main main.asm 3 | ./main 4 | -------------------------------------------------------------------------------- /day10/Main.j: -------------------------------------------------------------------------------- 1 | ; Yes, this is Java bytecode. I'm basically solving this problem in assembly. 2 | ; 3 | ; This problem is a bit of a slog honestly. But I've already started, and hey, 4 | ; it's not like this will be _impossible_ to solve or anything. 5 | ; 6 | ; I was originally using JASM but switched to Jasmin because it was emitting 7 | ; invalid bytecode for branches (goto, ifeq, etc.) that went to labels on a 8 | ; negative byte offset. Jasmin is more verbose, but less buggy. 9 | ; 10 | ; Reference: 11 | ; - https://docs.oracle.com/javase/specs/jvms/se21/jvms21.pdf 12 | ; - https://en.wikipedia.org/wiki/Java_bytecode 13 | ; - https://en.wikipedia.org/wiki/List_of_Java_bytecode_instructions 14 | ; - https://jasmin.sourceforge.net/instructions.html 15 | 16 | .class public Main 17 | .super java/lang/Object 18 | 19 | 20 | .method static getInput()[[C 21 | .limit stack 8 22 | .limit locals 8 23 | 24 | ; %1 = new BufferedReader(new InputStreamReader(System.in)) 25 | getstatic java/lang/System/in Ljava/io/InputStream; 26 | new java/io/InputStreamReader 27 | dup 28 | astore_1 29 | swap 30 | invokespecial java/io/InputStreamReader/(Ljava/io/InputStream;)V 31 | new java/io/BufferedReader 32 | dup 33 | aload_1 34 | invokespecial java/io/BufferedReader/(Ljava/io/Reader;)V 35 | astore_1 36 | 37 | ; %2 = new ArrayList() 38 | new java/util/ArrayList 39 | dup 40 | invokespecial java/util/ArrayList/()V 41 | astore_2 42 | 43 | ; while (true) 44 | Loop: 45 | ; %3 = %1.readLine() 46 | aload_1 47 | invokevirtual java/io/BufferedReader/readLine()Ljava/lang/String; 48 | astore_3 49 | aload_3 50 | ifnull Endloop 51 | 52 | ; %2.add(%3.toCharArray()) 53 | aload_2 54 | aload_3 55 | invokevirtual java/lang/String/toCharArray()[C 56 | invokevirtual java/util/ArrayList.add(Ljava/lang/Object;)Z 57 | pop 58 | 59 | goto Loop 60 | 61 | Endloop: 62 | ; return %2.toArray() 63 | aload_2 64 | iconst_0 65 | anewarray [C 66 | invokevirtual java/util/ArrayList.toArray([Ljava/lang/Object;)[Ljava/lang/Object; 67 | checkcast [[C 68 | areturn 69 | .end method 70 | 71 | 72 | .method static getPath(Ljava/util/ArrayList;[[CIII)V 73 | .limit stack 8 74 | .limit locals 8 75 | 76 | ; %0: output list 77 | ; %1: grid 78 | ; %2: row 79 | ; %3: col 80 | ; %4: direction: 0 = right, 1 = down, 2 = left, 3 = up 81 | 82 | PathLoop: 83 | aload_0 84 | new java/lang/Integer 85 | dup 86 | iload_2 87 | invokespecial java/lang/Integer/(I)V 88 | invokevirtual java/util/ArrayList.add(Ljava/lang/Object;)Z 89 | pop 90 | 91 | aload_0 92 | new java/lang/Integer 93 | dup 94 | iload_3 95 | invokespecial java/lang/Integer/(I)V 96 | invokevirtual java/util/ArrayList.add(Ljava/lang/Object;)Z 97 | pop 98 | 99 | ; %5 = grid[row][col] 100 | aload_1 101 | iload_2 102 | aaload 103 | iload_3 104 | caload 105 | istore 5 106 | 107 | ; if (%5 == 'S') return; 108 | iload 5 109 | bipush 83 110 | if_icmpne NotEnd 111 | return 112 | 113 | NotEnd: 114 | ; We now compute the next direction and put it on the stack. This will be stored in %6. 115 | iload 5 116 | bipush 124 ; '|' 117 | if_icmpeq Straight 118 | iload 5 119 | bipush 45 ; '-' 120 | if_icmpeq Straight 121 | iload 5 122 | bipush 76 ; 'L' 123 | if_icmpeq TurnL7 124 | iload 5 125 | bipush 70 ; 'F' 126 | if_icmpeq TurnFJ 127 | iload 5 128 | bipush 74 ; 'J' 129 | if_icmpeq TurnFJ 130 | iload 5 131 | bipush 55 ; '7' 132 | if_icmpeq TurnL7 133 | goto Else 134 | 135 | Straight: 136 | iload 4 137 | goto Advance 138 | 139 | TurnFJ: ; up <-> right, left <-> down 140 | iconst_3 141 | iload 4 142 | isub 143 | goto Advance 144 | 145 | TurnL7: ; up <-> left, right <-> down 146 | iconst_5 147 | iload 4 148 | isub 149 | iconst_4 150 | irem 151 | goto Advance 152 | 153 | Else: ; raise an exception with the character being invalid 154 | new java/lang/RuntimeException 155 | dup 156 | new java/lang/StringBuilder 157 | dup 158 | ldc "Invalid character in path: " 159 | invokespecial java/lang/StringBuilder/(Ljava/lang/String;)V 160 | iload 5 161 | invokevirtual java/lang/StringBuilder/append(C)Ljava/lang/StringBuilder; 162 | invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String; 163 | invokespecial java/lang/RuntimeException/(Ljava/lang/String;)V 164 | athrow 165 | 166 | Advance: 167 | istore 6 168 | ; move (%2, %3) in the direction %6 169 | iload 6 170 | tableswitch 0 171 | GoRight 172 | GoDown 173 | GoLeft 174 | GoUp 175 | default : GoNoDirection 176 | 177 | GoRight: 178 | iinc 3 1 179 | goto EndAdvance 180 | 181 | GoDown: 182 | iinc 2 1 183 | goto EndAdvance 184 | 185 | GoLeft: 186 | iinc 3 -1 187 | goto EndAdvance 188 | 189 | GoUp: 190 | iinc 2 -1 191 | goto EndAdvance 192 | 193 | GoNoDirection: 194 | ; raise an exception with the direction being invalid 195 | new java/lang/RuntimeException 196 | dup 197 | new java/lang/StringBuilder 198 | dup 199 | ldc "Invalid direction: " 200 | invokespecial java/lang/StringBuilder/(Ljava/lang/String;)V 201 | iload 6 202 | invokevirtual java/lang/StringBuilder/append(I)Ljava/lang/StringBuilder; 203 | invokevirtual java/lang/StringBuilder/toString()Ljava/lang/String; 204 | invokespecial java/lang/RuntimeException/(Ljava/lang/String;)V 205 | athrow 206 | 207 | EndAdvance: 208 | ; loop through the function! 209 | iload 6 210 | istore 4 211 | goto PathLoop 212 | 213 | return 214 | .end method 215 | 216 | 217 | .method static getArea(Ljava/util/ArrayList;)I 218 | .limit stack 8 219 | .limit locals 8 220 | 221 | ; compute via summation 222 | iconst_0 223 | istore_1 ; i 224 | 225 | iconst_0 226 | istore_2 ; area 227 | 228 | Loop: 229 | iload_1 230 | aload_0 231 | invokevirtual java/util/ArrayList.size()I 232 | if_icmpge EndLoop 233 | 234 | ; area += %0[i] * (%0[(i+3) % %0.size()] - %0[i+1]) 235 | aload_0 236 | iload_1 237 | invokevirtual java/util/ArrayList.get(I)Ljava/lang/Object; 238 | checkcast java/lang/Integer 239 | invokevirtual java/lang/Integer/intValue()I 240 | aload_0 241 | iload_1 242 | iconst_3 243 | iadd 244 | aload_0 245 | invokevirtual java/util/ArrayList.size()I 246 | irem 247 | invokevirtual java/util/ArrayList.get(I)Ljava/lang/Object; 248 | checkcast java/lang/Integer 249 | invokevirtual java/lang/Integer/intValue()I 250 | aload_0 251 | iload_1 252 | iconst_1 253 | iadd 254 | invokevirtual java/util/ArrayList.get(I)Ljava/lang/Object; 255 | checkcast java/lang/Integer 256 | invokevirtual java/lang/Integer/intValue()I 257 | isub 258 | imul 259 | 260 | iload_2 261 | iadd 262 | istore_2 263 | 264 | iinc 1 2 265 | goto Loop 266 | 267 | EndLoop: 268 | ; return Math.abs(area) 269 | iload_2 270 | invokestatic java/lang/Math/abs(I)I 271 | ireturn 272 | .end method 273 | 274 | 275 | .method public static main([Ljava/lang/String;)V 276 | .limit stack 8 277 | .limit locals 8 278 | 279 | ; getstatic java/lang/System/out Ljava/io/PrintStream; 280 | ; ldc "Hello World!" 281 | ; invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V 282 | 283 | getstatic java/lang/System/out Ljava/io/PrintStream; 284 | invokestatic Main/getInput()[[C 285 | astore_0 286 | 287 | ; aload_0 288 | ; invokestatic java.util.Arrays/deepToString([Ljava/lang/Object;)Ljava/lang/String; 289 | ; invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V 290 | 291 | ; %1 = %0.length 292 | aload_0 293 | arraylength 294 | istore_1 295 | 296 | ; %2 = %0[0].length 297 | aload_0 298 | iconst_0 299 | aaload 300 | arraylength 301 | istore_2 302 | 303 | ; Find the coordinates of 'S', the starting position 304 | iconst_0 305 | istore 3 306 | iconst_0 307 | istore 4 308 | 309 | FindLoop1: 310 | iload_3 311 | iload 1 312 | if_icmpge FindLoop1End 313 | 314 | iconst_0 315 | istore 4 316 | FindLoop2: 317 | iload 4 318 | iload 2 319 | if_icmpge FindLoop2End 320 | aload_0 321 | iload_3 322 | aaload 323 | iload 4 324 | caload 325 | bipush 83 326 | if_icmpeq FindLoop1End 327 | iinc 4 1 328 | goto FindLoop2 329 | 330 | FindLoop2End: 331 | iinc 3 1 332 | goto FindLoop1 333 | 334 | FindLoop1End: 335 | ; Now (%3, %4) is the starting position 336 | 337 | ; %5 = new ArrayList() 338 | new java/util/ArrayList 339 | dup 340 | invokespecial java/util/ArrayList/()V 341 | astore 5 342 | 343 | ; Just going to assume we go "up" from the starting position. 344 | ; You need to edit this to reflect your version of the input. I didn't bother 345 | ; making this detect automatically; that's too much work :') 346 | 347 | ; getPath(%5, %0, %3-1, %4, 3) 348 | aload 5 349 | aload_0 350 | iload_3 351 | iconst_1 352 | isub 353 | iload 4 354 | iconst_3 355 | invokestatic Main/getPath(Ljava/util/ArrayList;[[CIII)V 356 | 357 | ; System.out.println(%5.toString()) 358 | ; getstatic java/lang/System/out Ljava/io/PrintStream; 359 | ; aload 5 360 | ; invokevirtual java/util/ArrayList.toString()Ljava/lang/String; 361 | ; invokevirtual java/io/PrintStream/println(Ljava/lang/String;)V 362 | 363 | ;; Part 1 364 | ; %6 = %5.size() / 4 365 | aload 5 366 | invokevirtual java/util/ArrayList.size()I 367 | iconst_4 368 | idiv 369 | istore 6 370 | 371 | ; System.out.println(%6) 372 | getstatic java/lang/System/out Ljava/io/PrintStream; 373 | iload 6 374 | invokevirtual java/io/PrintStream/println(I)V 375 | 376 | ;; Part 2 377 | ; %7 = getArea(%5) 378 | aload 5 379 | invokestatic Main/getArea(Ljava/util/ArrayList;)I 380 | istore 7 381 | 382 | ; Pick's theorem: A = i + b/2 - 1 383 | ; System.out.println(%7 - %6 + 1) 384 | getstatic java/lang/System/out Ljava/io/PrintStream; 385 | iload 7 386 | iload 6 387 | isub 388 | iconst_1 389 | iadd 390 | invokevirtual java/io/PrintStream/println(I)V 391 | 392 | return 393 | .end method 394 | -------------------------------------------------------------------------------- /day10/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "Main.j" -nt "Main.class" ]; then 4 | jasmin Main.j 5 | fi 6 | 7 | java Main 8 | -------------------------------------------------------------------------------- /day11/main.kk: -------------------------------------------------------------------------------- 1 | // I know they're very different even in paradigm, but Koka actually reminds me 2 | // a little bit of Nim or Crystal. Compiled, statically-typed, flexible syntax, 3 | // trailing blocks, and a noise-free appearance. I like it. 4 | // 5 | // Koka's focus is on minimalism and generality ("min-gen"). Notably, the 6 | // tutorial just shares neat tidbits of syntax like blocks, effects, and loop 7 | // constructs that are actually ordinary function with lambdas. The "with" 8 | // statement is an especially neat trick that makes CPS palatable. 9 | // 10 | // I don't have many opinions on the algebraic effect system. Not sure exactly 11 | // how the hierarchy works with total / console / div / ndet. 12 | // 13 | // It's unclear to me how _practical_ algebraic effects and termination checking 14 | // are in practice. I would need to use the language more to find out. My 15 | // suspicion is that proving program termination is not too interesting since an 16 | // O(10^100) function is no better than a diverging one. 17 | // 18 | // Although a bit quirky, since it's able to avoid explicit Monads, it might be 19 | // a nice language for exploring foundations. Like Haskell, but easier. I could 20 | // imagine implementing "Build Systems à la Carte" in Koka. 21 | // 22 | // Also curious how effects relate to Context, Hooks, and "Suspend" in React. 23 | // 24 | // I will say that with all of the interesting ideas here, writing actual code 25 | // with all this purity feels awesome. Also, why is the whitespace funny around 26 | // function arguments! `N.repeat` blocks are awesome though. :') 27 | 28 | import std/os/readline 29 | 30 | fun read-input() : list 31 | // It took me 15 minutes to figure out how to catch an exception for the first 32 | // time, haha. Tried every combination of handlers. 33 | match readline.try fn(_) { "" } 34 | "" -> [] 35 | input -> [input] ++ read-input() 36 | 37 | fun transpose( grid : list> ) : div list> 38 | val first-col = grid.filter-map(head) 39 | match first-col 40 | [] -> [] 41 | _ -> [first-col] ++ transpose(grid.map(tail)) 42 | 43 | // Returns all rows where all chars are repeated '.' chars. 44 | fun empty-rows( grid : list> ) : list 45 | val indices = grid.map-indexed fn(i, row) 46 | match row.all(fn(c) { c == '.' }) 47 | True -> Just(i) 48 | False -> Nothing 49 | indices.concat-maybe 50 | 51 | // Returns all "#" characters in the grid. 52 | fun galaxies( grid : list> ) : list<(int,int)> 53 | val pos = grid.map-indexed fn(i, row) 54 | row.map-indexed fn(j, c) 55 | match c 56 | '#' -> Just((i,j)) 57 | _ -> Nothing 58 | pos.concat.concat-maybe 59 | 60 | fun pairs( xs : list ) : div list<(a,a)> 61 | match xs.head 62 | Nothing -> [] 63 | Just(x) -> xs.tail.map(fn(y) { (x,y) }) ++ pairs(xs.tail) 64 | 65 | fun is-between( x : int, a : int, b : int ) : div bool 66 | (x >= a && x <= b) || (x >= b && x <= a) 67 | 68 | fun main() : () 69 | val grid = read-input().map(list) // list> 70 | val transposed-grid = grid.transpose() 71 | 72 | // println(grid.show-list(string)) 73 | // val n = grid.length 74 | // val m = grid[0].unjust.length 75 | 76 | val erows = empty-rows(grid) 77 | val ecols = empty-rows(transposed-grid) 78 | 79 | val expanded-dists = fn(n) 80 | grid.galaxies.pairs.map fn(((a1,b1),(a2,b2))) 81 | val rdist = abs(a2 - a1) + (n - 1) * erows.filter fn(i) { i.is-between(a1, a2) }.length 82 | val cdist = abs(b2 - b1) + (n - 1) * ecols.filter fn(i) { i.is-between(b1, b2) }.length 83 | rdist + cdist 84 | 85 | // Part 1 86 | println(expanded-dists(2).sum) 87 | 88 | // Part 2 89 | println(expanded-dists(1000000).sum) 90 | -------------------------------------------------------------------------------- /day11/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "main.kk" -nt "main" ]; then 6 | rm -f main # Koka doesn't return a non-zero exit code when it fails to compile 7 | koka -O2 -o main main.kk 8 | chmod +x main 9 | fi 10 | 11 | ./main 12 | -------------------------------------------------------------------------------- /day12/main.lean: -------------------------------------------------------------------------------- 1 | -- I'm going to be honest, Lean isn't the best language for this unless you for 2 | -- whatever reason wanted to formalize a proof of the DP? 3 | -- 4 | -- But as the saying goes: "If it has a display, it can run Doom." 5 | -- 6 | -- Let's do this. References: 7 | -- * https://lean-lang.org/functional_programming_in_lean 8 | -- * https://github.com/leanprover/lean4/blob/v4.3.0/src/Init/System/IO.lean 9 | -- 10 | -- It's actually okay for scripting! Quite friendly and sensible. There's just 11 | -- not a lot of places to get documentation. It seems targeted at mathematicians 12 | -- who want to do some programming. 13 | -- 14 | -- Definitely more palatable with Haskell background. 15 | 16 | partial def getLines (stream : IO.FS.Stream) : IO (Array String) := 17 | let rec read (lines : Array String) := do 18 | let line <- stream.getLine 19 | if line.length == 0 then 20 | pure lines 21 | else if line.back == '\n' then 22 | read <| lines.push (line.dropRight 1) 23 | else 24 | pure <| lines.push line 25 | read #[] 26 | 27 | def parseLine (line : String) : (Prod String (List Nat)) := 28 | let (arr, strnums) := match line.split (· == ' ') with 29 | | [a, b] => (a, b.split (· == ',')) 30 | | _ => panic! "bad input" 31 | let nums := match strnums.mapM String.toNat? with 32 | | some ns => ns 33 | | none => panic! "bad input nums" 34 | (arr, nums) 35 | 36 | def arrangements (s : String) (nums : List Nat) : Nat := 37 | let n := s.length 38 | let k := nums.length 39 | let chars := s.toList 40 | 41 | -- Answer is dp[n + 1][k]. I don't know enough about theorem-proving to fix 42 | -- the "failed to prove index is valid" error well, so I'll just add runtime 43 | -- checks for this issue. 44 | let ans := Id.run do 45 | let mut dp := #[#[1] ++ Array.mkArray k 0] 46 | for i in [1:n+2] do 47 | let mut cur := #[] 48 | for j in [0:k+1] do 49 | let mut x := 0 50 | if i == n + 1 || chars[i - 1]! != '#' then 51 | x := dp[i - 1]![j]! 52 | if j > 0 then 53 | let d := nums[j - 1]! 54 | let substr := (s.drop (i - d - 1)).take d 55 | if i >= d + 1 && substr.all (λ c => c == '#' || c == '?') then 56 | x := x + dp[i - d - 1]![j - 1]! 57 | cur := cur.push x 58 | dp := dp.push cur 59 | dp[n + 1]![k]! 60 | 61 | ans 62 | 63 | def part1 (line : String) : Nat := 64 | let (s, nums) := parseLine line 65 | arrangements s nums 66 | 67 | def part2 (line : String) : Nat := 68 | let (s, nums) := parseLine line 69 | let s := String.join [s, "?", s, "?", s, "?", s, "?", s] 70 | let nums := nums ++ nums ++ nums ++ nums ++ nums 71 | arrangements s nums 72 | 73 | def main : IO Unit := do 74 | let stdin <- IO.getStdin 75 | let lines <- getLines stdin 76 | IO.println <| ((lines.map part1).foldl Nat.add 0) 77 | IO.println <| ((lines.map part2).foldl Nat.add 0) 78 | -------------------------------------------------------------------------------- /day12/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | lean --run main.lean 3 | -------------------------------------------------------------------------------- /day13/main.mojo: -------------------------------------------------------------------------------- 1 | # What the heck is Mojo, anyway? Supposedly I can just write ordinary Python 2 | # code here, and it should work, so... let's find out! 3 | 4 | from python import Python 5 | 6 | 7 | fn input(prompt: StringLiteral) -> String: 8 | # Not off to a great start, how do I get input? Needs some type coercions. 9 | try: 10 | let builtins = Python.import_module("builtins") 11 | let input_function = builtins.input 12 | let user_input: PythonObject = input_function(String(prompt)) 13 | return str(user_input) 14 | except: 15 | return String("") 16 | 17 | 18 | fn part1(grid: DynamicVector[String]) raises -> Int: 19 | # This feels quite a bit more verbose than Python for unnecessary reasons. 20 | for i in range(len(grid) - 1): 21 | var ok = True 22 | for k in range(len(grid)): 23 | if i - k < 0 or i + 1 + k >= len(grid): 24 | break 25 | if grid[i - k] != grid[i + 1 + k]: 26 | ok = False 27 | break 28 | if ok: 29 | return 100 * (i + 1) 30 | 31 | for j in range(len(grid[0]) - 1): 32 | var ok = True 33 | for k in range(len(grid[0])): 34 | if j - k < 0 or j + 1 + k >= len(grid[0]): 35 | break 36 | for i in range(len(grid)): 37 | if grid[i][j - k] != grid[i][j + 1 + k]: 38 | ok = False 39 | break 40 | if not ok: 41 | break 42 | if ok: 43 | return j + 1 44 | 45 | raise Error("No mirror image found in part1") 46 | 47 | 48 | fn part2(grid: DynamicVector[String]) raises -> Int: 49 | # Hmm, if I were to write this in Python, it could be a lot shorter. 50 | for i in range(len(grid) - 1): 51 | var diff = 0 52 | for k in range(len(grid)): 53 | if i - k < 0 or i + 1 + k >= len(grid): 54 | break 55 | for j in range(len(grid[0])): 56 | if grid[i - k][j] != grid[i + 1 + k][j]: 57 | diff += 1 58 | if diff == 1: 59 | return 100 * (i + 1) 60 | 61 | for j in range(len(grid[0]) - 1): 62 | var diff = 0 63 | for k in range(len(grid[0])): 64 | if j - k < 0 or j + 1 + k >= len(grid[0]): 65 | break 66 | for i in range(len(grid)): 67 | if grid[i][j - k] != grid[i][j + 1 + k]: 68 | diff += 1 69 | if diff == 1: 70 | return j + 1 71 | 72 | raise Error("No mirror image found in part2") 73 | 74 | 75 | fn main() raises: 76 | var total_part1 = 0 77 | var total_part2 = 0 78 | 79 | var grid = DynamicVector[String]() 80 | while True: 81 | let line = input("") 82 | if not line: 83 | if not len(grid): 84 | break 85 | 86 | total_part1 += part1(grid) 87 | total_part2 += part2(grid) 88 | 89 | grid.clear() 90 | continue 91 | grid.append(line) 92 | 93 | print(total_part1) 94 | print(total_part2) 95 | -------------------------------------------------------------------------------- /day13/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | mojo main.mojo 3 | -------------------------------------------------------------------------------- /day14/main.nim: -------------------------------------------------------------------------------- 1 | import std/[algorithm, sequtils, strutils, sugar, tables] 2 | 3 | proc tilt(s: string): string = 4 | ## Tilt a string, moving all rocks forward in index. 5 | ## e.g. tilt("O..O.#.") == "...OOX." 6 | var res = newString(s.len) 7 | var rocks = 0 8 | for i in 0 .. s.len: 9 | if i < s.len: 10 | res[i] = if s[i] == '#': '#' else: '.' 11 | if i == s.len or (s[i] == '#' and rocks > 0): 12 | var j = i 13 | while rocks > 0: 14 | j -= 1 15 | res[j] = 'O' 16 | rocks -= 1 17 | elif s[i] == 'O': 18 | rocks += 1 19 | res 20 | 21 | proc tiltBack(s: string): string = 22 | var res = s 23 | res.reverse() 24 | res = tilt(res) 25 | res.reverse() 26 | res 27 | 28 | proc totalLoad(grid: seq[string]): int = 29 | var load = 0 30 | for i in 0 ..< grid.len: 31 | for j in 0 ..< grid[i].len: 32 | if grid[i][j] == 'O': 33 | load += grid.len - i 34 | load 35 | 36 | proc flip(grid: seq[string]): seq[string] = 37 | var cols = newSeq[string]() 38 | for i in 0 ..< grid[0].len: 39 | let col = collect(newSeqOfCap(grid.len)): 40 | for line in grid: 41 | line[i] 42 | cols.add(col.join "") 43 | cols 44 | 45 | 46 | when isMainModule: 47 | var grid = newSeq[string]() 48 | 49 | while true: 50 | try: 51 | let line = stdin.readLine 52 | grid.add(line) 53 | except EOFError: 54 | break 55 | 56 | # Part 1 57 | echo grid.flip.map(tiltBack).flip.totalLoad 58 | 59 | # Part 2 60 | var memory = initTable[string, int]() 61 | var cycles = 1000000000 62 | while cycles > 0: 63 | memory[grid.join "|"] = cycles 64 | let gridN = grid.flip.map(tiltBack).flip; 65 | let gridW = gridN.map(tiltBack); 66 | let gridS = gridW.flip.map(tilt).flip; 67 | let gridE = gridS.map(tilt); 68 | cycles -= 1 69 | grid = gridE 70 | if memory.hasKey(grid.join "|"): 71 | let cycle = memory[grid.join "|"] 72 | cycles = cycles mod (cycle - cycles) 73 | echo grid.totalLoad 74 | -------------------------------------------------------------------------------- /day14/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "main.nim" -nt "main" ]; then 4 | nim c -d:release main.nim 5 | fi 6 | 7 | ./main 8 | -------------------------------------------------------------------------------- /day15/.ocamlformat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekzhang/aoc23-alpha/77cfe26b971b11b025ef5bac1a8a62e0a9053589/day15/.ocamlformat -------------------------------------------------------------------------------- /day15/main.ml: -------------------------------------------------------------------------------- 1 | (* 2 | OCaml is exactly how I remember it. I tried to use more of the `|>` operator 3 | this time! Not a bad solution, though still a little verbose. 4 | 5 | In my freshman year of college, the first CS class I took was compilers, taught 6 | in OCaml. I remember thinking it was really tricky. But compared to the other 7 | languages in this challenge, OCaml feels quite tame and practical. :') 8 | *) 9 | 10 | let hash (s : string) : int = 11 | let rec aux i acc = 12 | if i = String.length s then acc 13 | else 14 | let c = Char.code s.[i] in 15 | aux (i + 1) ((acc + c) * 17 mod 256) 16 | in 17 | aux 0 0 18 | 19 | module IntMap = Map.Make (struct 20 | type t = int 21 | 22 | let compare = compare 23 | end) 24 | 25 | type state = (string * int) list IntMap.t 26 | 27 | let update_lenses (s : state) (token : string) : state = 28 | if token.[String.length token - 1] == '-' then 29 | (* Remove label from its bucket. *) 30 | let label = String.sub token 0 (String.length token - 1) in 31 | IntMap.update (hash label) 32 | (fun x -> 33 | match x with 34 | | None -> None 35 | | Some lenses -> Some (List.filter (fun (l, _) -> l <> label) lenses)) 36 | s 37 | else 38 | (* Add a new lens to the bucket, or replace an existing one. *) 39 | let label = String.sub token 0 (String.length token - 2) in 40 | let strength = 41 | int_of_string (String.sub token (String.length token - 1) 1) 42 | in 43 | let rec assoc_replace_or_add (l, x) lenses = 44 | match lenses with 45 | | [] -> [ (l, x) ] 46 | | (l', x') :: lenses -> 47 | if l = l' then (l, x) :: lenses 48 | else (l', x') :: assoc_replace_or_add (l, x) lenses 49 | in 50 | IntMap.update (hash label) 51 | (fun x -> 52 | match x with 53 | | None -> Some [ (label, strength) ] 54 | | Some lenses -> Some (assoc_replace_or_add (label, strength) lenses)) 55 | s 56 | 57 | let tokens = read_line () |> String.split_on_char ',';; 58 | 59 | (* Part 1 *) 60 | List.map hash tokens |> List.fold_left ( + ) 0 |> string_of_int |> print_endline; 61 | 62 | (* Part 2 *) 63 | let s = List.fold_left update_lenses IntMap.empty tokens in 64 | IntMap.fold 65 | (fun bucket lenses acc -> 66 | lenses 67 | |> List.mapi (fun i (_, x) -> (i + 1) * x) 68 | |> List.fold_left ( + ) 0 69 | |> ( * ) (bucket + 1) 70 | |> ( + ) acc) 71 | s 0 72 | |> string_of_int |> print_endline 73 | -------------------------------------------------------------------------------- /day15/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "main.ml" -nt "main" ]; then 4 | ocamlopt main.ml -o main 5 | fi 6 | 7 | ./main 8 | -------------------------------------------------------------------------------- /day16/day16: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ekzhang/aoc23-alpha/77cfe26b971b11b025ef5bac1a8a62e0a9053589/day16/day16 -------------------------------------------------------------------------------- /day16/main.pony: -------------------------------------------------------------------------------- 1 | // Pony is a capabilities-secure language with concurrent actors. 2 | // 3 | // It's really interesting! The different capabilities for mutability and 4 | // aliasing kind of take Rust's borrowing model to an even stricter regime, but 5 | // it also simplifies concurrency by having a garbage collector. 6 | // 7 | // I could see this being a useful model for secure network services. Would take 8 | // some time to get used to the type system though. There's other quirks, like 9 | // the HashSet.add() function actually cloning the entire data structure, and 10 | // `iso`/`box` capabilities being incompatible. 11 | // 12 | // (The Pony documentation is really well-written, though!) 13 | 14 | use "collections" 15 | use "files" 16 | use "itertools" 17 | 18 | primitive Up is (Equatable[Direction] & Hashable) 19 | fun string(): String => "Up" 20 | fun hash(): USize => 1 21 | primitive Down is (Equatable[Direction] & Hashable) 22 | fun string(): String => "Down" 23 | fun hash(): USize => 2 24 | primitive Left is (Equatable[Direction] & Hashable) 25 | fun string(): String => "Left" 26 | fun hash(): USize => 3 27 | primitive Right is (Equatable[Direction] & Hashable) 28 | fun string(): String => "Right" 29 | fun hash(): USize => 4 30 | 31 | type Direction is (Up | Down | Left | Right) 32 | 33 | primitive Dir 34 | fun move(i: I32, j: I32, dir: Direction): (I32, I32) => 35 | """ 36 | Move one square forward from a direction. 37 | """ 38 | match dir 39 | | Up => (i - 1, j) 40 | | Down => (i + 1, j) 41 | | Left => (i, j - 1) 42 | | Right => (i, j + 1) 43 | end 44 | 45 | fun bounce(loc: U8, dir: Direction): Array[Direction] val => 46 | """ 47 | Bounce a current direction from the character at a location. 48 | """ 49 | match loc 50 | | '\\' => match dir 51 | | Up => [Left] 52 | | Down => [Right] 53 | | Left => [Up] 54 | | Right => [Down] 55 | end 56 | | '/' => match dir 57 | | Up => [Right] 58 | | Down => [Left] 59 | | Left => [Down] 60 | | Right => [Up] 61 | end 62 | | '|' => match dir 63 | | Up | Down => [dir] 64 | | Left | Right => [Up; Down] 65 | end 66 | | '-' => match dir 67 | | Up | Down => [Left; Right] 68 | | Left | Right => [dir] 69 | end 70 | else 71 | [dir] 72 | end 73 | 74 | class Laser is (Equatable[Laser] & Hashable) 75 | """ 76 | An entry in the visited set of the grid. This is just needed to implement Hashable. 77 | """ 78 | 79 | let i: I32 80 | let j: I32 81 | let dir: Direction 82 | 83 | new val create(i': I32, j': I32, dir': Direction) => 84 | i = i' 85 | j = j' 86 | dir = dir' 87 | 88 | fun eq(that: Laser box): Bool => 89 | """ 90 | I spent a long time trying to figure out why my HashMap wasn't working, and 91 | it's because the default implementation of Equatable is reference equality. 92 | 93 | Definitely my fault, but still weird for a language that emphasizes absolute 94 | correctness without bugs. 95 | """ 96 | (i == that.i) and (j == that.j) and (dir == that.dir) 97 | 98 | fun hash(): USize => 99 | """ 100 | Hash the entry. I'm not sure if there's a better way to do this. 101 | """ 102 | (((i.hash() * 1000000007) + j.hash()) * 1000000007) + dir.hash() 103 | 104 | fun string(): String => 105 | """ 106 | Print the entry. 107 | """ 108 | i.string() + ", " + j.string() + ", " + dir.string() 109 | 110 | 111 | primitive Solver 112 | fun bfs(lines: Array[String] val, start: Laser val): Set[Laser val] ref => 113 | """ 114 | Find all visitable squares in the grid. 115 | """ 116 | 117 | var visited = Set[Laser val]() 118 | let queue = List[Laser val].from([start]) 119 | 120 | while true do 121 | let n = try queue.shift()? else break end 122 | let loc = try lines(n.i.usize())?(n.j.usize())? else continue end 123 | 124 | if not visited.contains(n) then 125 | visited.set(n) 126 | let dirs = Dir.bounce(loc, n.dir) 127 | 128 | for dir' in dirs.values() do 129 | (let i', let j') = Dir.move(n.i, n.j, dir') 130 | queue.push(Laser(i', j', dir')) 131 | end 132 | end 133 | end 134 | 135 | visited 136 | 137 | fun energized(lines: Array[String] val, start: Laser val): USize => 138 | let visited = bfs(lines, start) 139 | Iter[Laser val](visited.values()) 140 | .map[Laser val]({(n) => Laser(n.i, n.j, Left) }) 141 | .unique[HashEq[Laser]]() 142 | .count() 143 | 144 | 145 | actor Worker 146 | new create(parent: Main, lines: Array[String] val, start: Laser val) => 147 | let result = Solver.energized(lines, start) 148 | parent.done_part2(result) 149 | 150 | 151 | actor Main 152 | let env: Env 153 | var expected: USize = 0 154 | var results: Array[USize] = Array[USize]() 155 | 156 | new create(env': Env) => 157 | env = env' 158 | 159 | let path = FilePath(FileAuth(env.root), "input.txt") 160 | let lines: Array[String] val = recover match OpenFile(path) 161 | | let file: File => 162 | Iter[String](file.lines()).collect(Array[String]()) 163 | else 164 | env.err.print("Error opening file 'input.txt'") 165 | return 166 | end 167 | end 168 | 169 | let n = lines.size() 170 | let m = try lines(0)?.size() else 0 end 171 | expected = 2 * (n + m) 172 | 173 | // Part 1 174 | env.out.print(Solver.energized(lines, Laser(0, 0, Right)).string()) 175 | 176 | // Part 2 177 | var xs = Array[USize]() 178 | for i in Range(0, n) do 179 | Worker(this, lines, Laser(i.i32(), 0, Right)) 180 | Worker(this, lines, Laser(i.i32(), m.i32() - 1, Left)) 181 | end 182 | for j in Range(0, m) do 183 | Worker(this, lines, Laser(0, j.i32(), Down)) 184 | Worker(this, lines, Laser(n.i32() - 1, j.i32(), Up)) 185 | end 186 | 187 | be done_part2(x: USize) => 188 | results.push(x) 189 | if results.size() == expected then 190 | let max = Iter[USize](results.values()).fold[USize](0, {(a, b) => if (a > b) then a else b end }) 191 | env.out.print(max.string()) 192 | end 193 | -------------------------------------------------------------------------------- /day16/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "main.pony" -nt "day16" ]; then 5 | ponyc 6 | fi 7 | 8 | ./day16 --ponypin 9 | -------------------------------------------------------------------------------- /day17/main.qi: -------------------------------------------------------------------------------- 1 | // Qi is a programming language written in Chinese. 2 | // 3 | // The language itself is pretty small, with similar syntax to C or Python and 4 | // scripting influences, like arrays and dynamic typing. I guess what's unique 5 | // is its ability to target Chinese-native speakers. 6 | // 7 | // I don't write Chinese, so this was a lot of copy-pasting for me. It's 8 | // refreshing to use a nice and simple language now and then. 9 | // 10 | // I implemented Dijkstra's algorithm with a binary heap. Surprised how well 11 | // Copilot was able to start writing code after just a few examples of using 12 | // control flow and keywords, since it knows Chinese better than me. 13 | // 14 | // By the way, I think there's some kind of bug in the Qi interpreter, since 15 | // my program has nondeterministic incorrect output 50% of the time. 16 | 17 | 功能 toints(line)「 18 | 变量 ints = 【】 19 | 对于(变量 i = 0;i 小 line。长度();i++)「 20 | ints。推("0123456789"。指数(line【i】)) 21 | 」 22 | 返回 ints 23 | 」 24 | 25 | 变量 grid = 【】 26 | 27 | 而(真)「 28 | 变量 s = 系统。扫描() 29 | 如果(s 等 "完毕")「 30 | 打断 31 | 」 32 | // Bug workaround in Qi: 扫描 (scanNative) only reads 99 characters, and deletes the next one. 33 | s += 系统。扫描() 34 | grid。推(toints(s)) 35 | 」 36 | 37 | 变量 n = grid。长度() 38 | 变量 m = grid【0】。长度() 39 | 40 | // 系统。打印行(grid) 41 | // 系统。打印行(n) 42 | // 系统。打印行(m) 43 | 44 | 变量 dirs = 【【-1,0】,【0,-1】,【1,0】,【0,1】】 // Up, left, down, right 45 | 46 | 功能 swap(q,i,j)「 47 | 变量 temp = q【i】 48 | q【i】 = q【j】 49 | q【j】 = temp 50 | 」 51 | 52 | // Binary heap push based on pairs of (distance, position) 53 | 功能 pushq(q,dist,pos)「 54 | 变量 i = q。长度() 55 | q。推(【dist,pos】) 56 | 而(i 大 0 和 q【i】【0】 小 q【(i - 1)位右移 1】【0】)「 57 | swap(q,i,(i - 1)位右移 1) 58 | i = (i - 1)位右移 1 59 | 」 60 | 」 61 | 62 | // Binary heap pop based on pairs of (distance, position) 63 | 功能 popq(q)「 64 | 变量 i = 0 65 | 变量 ret = q【0】 66 | q【0】 = q【q。长度() - 1】 67 | q。弹() 68 | 而(真)「 69 | 变量 left = i * 2 + 1 70 | 变量 right = i * 2 + 2 71 | 如果(left 大等 q。长度())「 72 | 打断 73 | 」 74 | 变量 next = left 75 | 如果(right 小 q。长度() 和 q【right】【0】 小 q【left】【0】)「 76 | next = right 77 | 」 78 | 如果(q【i】【0】 大 q【next】【0】)「 79 | swap(q,i,next) 80 | i = next 81 | 」否则「 82 | 打断 83 | 」 84 | 」 85 | 返回 ret 86 | 」 87 | 88 | 功能 dijkstra(minlen,maxlen)「 89 | 变量 dists = 【】 // int[n][m][4] 90 | 对于(变量 i = 0;i 小 n;i++)「 91 | dists。推(【】) 92 | 对于(变量 j = 0;j 小 m;j++)「 93 | dists【i】。推(【】) 94 | 对于(变量 k = 0;k 小 4;k++)「 95 | dists【i】【j】。推(999999999) 96 | 」 97 | 」 98 | 」 99 | dists【0】【0】【2】 = 0 // Starting position, down 100 | dists【0】【0】【3】 = 0 // Starting position, right 101 | 102 | 变量 q = 【】 // Binary heap 103 | pushq(q,0,【0,0,2】) 104 | pushq(q,0,【0,0,3】) 105 | 106 | 而(q。长度() 大 0)「 107 | 变量 cur = popq(q) 108 | 变量 dist = cur【0】 109 | 变量 i = cur【1】【0】 110 | 变量 j = cur【1】【1】 111 | 变量 dir = cur【1】【2】 112 | 如果(i 等 n - 1 和 j 等 m - 1)「 113 | 返回 dist 114 | 」 115 | 如果(dists【i】【j】【dir】 小 dist)「 116 | 继续 117 | 」 118 | 变量 ndist = dist 119 | 对于(变量 k = 1;k 小等 maxlen;k++)「 120 | 变量 ni = i + k * dirs【dir】【0】 121 | 变量 nj = j + k * dirs【dir】【1】 122 | 如果(ni 大等 0 和 ni 小 n 和 nj 大等 0 和 nj 小 m)「 123 | ndist += grid【ni】【nj】 124 | // if k 大等 minlen 125 | 如果(k 大等 minlen)「 126 | // You can either turn left or right, but not 180 degrees. 127 | 如果(ndist 小 dists【ni】【nj】【(dir + 1) % 4】)「 128 | dists【ni】【nj】【(dir + 1) % 4】 = ndist 129 | pushq(q,ndist,【ni,nj,(dir + 1) % 4】) 130 | 」 131 | 如果(ndist 小 dists【ni】【nj】【(dir + 3) % 4】)「 132 | dists【ni】【nj】【(dir + 3) % 4】 = ndist 133 | pushq(q,ndist,【ni,nj,(dir + 3) % 4】) 134 | 」 135 | 」 136 | 」 137 | 」 138 | 」 139 | 」 140 | 141 | 142 | // Part 1 143 | 系统。打印行(dijkstra(1,3)) 144 | 145 | // Part 2 146 | 系统。打印行(dijkstra(4,10)) 147 | -------------------------------------------------------------------------------- /day17/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ ! -f "build/qi" ]; then 5 | cmake -S qi/src -B build 6 | make -j8 -C build 7 | fi 8 | 9 | # Append a final line to the input, due to limitations of Qi. 10 | # Also split lines in half because of a bug. :/ 11 | cat <(cat | fold -w 80) <(echo "完毕") | build/qi main.qi 12 | -------------------------------------------------------------------------------- /day18/main.raku: -------------------------------------------------------------------------------- 1 | # Raku is Perl 6. I've never written much Perl code, but my impression is that 2 | # as a dynamic language, it tends to play very fast and loose with syntax. But 3 | # for this problem it's more than adequate enough! 4 | # 5 | # I'm surprised that I can basically index any non-array object with [0], and it 6 | # will act as the identity. Also surprised by the variable syntax. 7 | # 8 | # There's some really wacky thing about "braids" and sub-languages within Raku 9 | # related to the "HOW" metaprogramming structure. I don't know enough to comment 10 | # on that part. Maybe it's kind of like Ruby's eigenclass hierarchy? 11 | 12 | # Takes an array of two-element lists (dir, len). 13 | sub calculate_volume(@pairs) { 14 | my $y = 0; 15 | my $perimeter = 0; 16 | my $area = 0; 17 | 18 | for @pairs -> ($dir, $len) { 19 | $perimeter += $len; 20 | given $dir { 21 | when 'U' { $y += $len } 22 | when 'D' { $y -= $len } 23 | when 'L' { $area -= $len * $y } 24 | when 'R' { $area += $len * $y } 25 | } 26 | } 27 | 28 | # Use Pick's theorem again! 29 | # Total number of squares is: |area| + perimeter/2 + 1 30 | return abs($area) + $perimeter/2 + 1; 31 | } 32 | 33 | my @part1 = (); 34 | my @part2 = (); 35 | 36 | for $*IN.lines -> $line { 37 | my ($dir, $len, $color) = $line.split(/\s+/); 38 | $len = +$len; 39 | @part1.push(($dir, $len)); 40 | 41 | my $dir2 = 'RDLU'.substr(+$color.substr(7, 1), 1); 42 | my $len2 = :16($color.substr(2, 5)); 43 | @part2.push(($dir2, $len2)); 44 | } 45 | 46 | say calculate_volume(@part1); 47 | say calculate_volume(@part2); 48 | -------------------------------------------------------------------------------- /day18/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | rakudo main.raku 4 | -------------------------------------------------------------------------------- /day19/main.swift: -------------------------------------------------------------------------------- 1 | // Haven't written Swift before, but it felt nice. A statically-typed language 2 | // with modern features and clean syntax. CoW value semantics are cool. 3 | // 4 | // Everything was effortless except dealing with strings and nil-handling. 5 | 6 | import Foundation 7 | 8 | // Declare a mutable map from string -> Array[(Rule?, String)] 9 | struct Rule { 10 | let feature: Character // 'x', 'm', 'a', 's' 11 | let lt: Bool 12 | let value: Int 13 | } 14 | 15 | func parseBranch(s: T) -> (Rule?, String) where T: StringProtocol { 16 | if s.contains(":") { 17 | let parts = s.split(separator: ":") 18 | let condition = parts[0] 19 | let result = parts[1] 20 | var rule: Rule 21 | if condition.contains("<") { 22 | let parts = condition.split(separator: "<") 23 | let value = Int(String(parts[1]))! 24 | rule = Rule(feature: parts[0].first!, lt: true, value: value) 25 | } else { 26 | let parts = condition.split(separator: ">") 27 | let value = Int(String(parts[1]))! 28 | rule = Rule(feature: parts[0].first!, lt: false, value: value) 29 | } 30 | return (rule, String(result)) 31 | } else { 32 | return (nil, String(s)) 33 | } 34 | } 35 | 36 | var rules = [String: [(Rule?, String)]]() 37 | 38 | while let line = readLine() { 39 | if line.isEmpty { 40 | break 41 | } 42 | // Split a line into a name and contents 43 | let parts = line.split(separator: "{") 44 | let name = parts[0] 45 | rules[String(name)] = parts[1].dropLast().split(separator: ",").map { 46 | parseBranch(s: String($0)) 47 | } 48 | } 49 | 50 | // Part 1 51 | func simulate(_ item: [Character: Int]) -> Bool { 52 | var label = "in" 53 | while true { 54 | guard let clauses = rules[label] else { 55 | return label == "A" 56 | } 57 | for clause in clauses { 58 | if let c = clause.0 { 59 | let val = item[c.feature]! 60 | if (c.lt && val < c.value) || (!c.lt && val > c.value) { 61 | label = clause.1 62 | break 63 | } 64 | } else { 65 | label = clause.1 66 | break 67 | } 68 | } 69 | } 70 | } 71 | 72 | var totalRating = 0 73 | while let line = readLine() { 74 | let vals = line.dropFirst().dropLast().split(separator: ",") 75 | var item = [Character: Int]() 76 | for val in vals { 77 | item[val.first!] = Int(String(val[val.index(val.startIndex, offsetBy: 2)...]))! 78 | } 79 | if simulate(item) { 80 | totalRating += item.values.reduce(0, +) 81 | } 82 | } 83 | 84 | print(totalRating) 85 | 86 | // Part 2 87 | func totalCombos(label: String, idx: Int, bounds: [Character: (Int, Int)]) -> Int64 { 88 | guard let clauses = rules[label] else { 89 | if label == "A" { 90 | return bounds.values.reduce(1) { $0 * Int64($1.1 - $1.0 + 1) } 91 | } else { 92 | return 0 93 | } 94 | } 95 | 96 | let (rule, result) = clauses[idx] 97 | if let rule = rule { 98 | let (lo, hi) = bounds[rule.feature]! 99 | var ans = Int64(0) 100 | if rule.lt { 101 | // x < rule.value 102 | if lo < rule.value { 103 | var newBounds = bounds 104 | newBounds[rule.feature] = (lo, rule.value - 1) 105 | ans += totalCombos(label: result, idx: 0, bounds: newBounds) 106 | } 107 | if hi >= rule.value { 108 | var newBounds = bounds 109 | newBounds[rule.feature] = (rule.value, hi) 110 | ans += totalCombos(label: label, idx: idx + 1, bounds: newBounds) 111 | } 112 | } else { 113 | // x > rule.value 114 | if hi > rule.value { 115 | var newBounds = bounds 116 | newBounds[rule.feature] = (rule.value + 1, hi) 117 | ans += totalCombos(label: result, idx: 0, bounds: newBounds) 118 | } 119 | if lo <= rule.value { 120 | var newBounds = bounds 121 | newBounds[rule.feature] = (lo, rule.value) 122 | ans += totalCombos(label: label, idx: idx + 1, bounds: newBounds) 123 | } 124 | } 125 | return ans 126 | } else { 127 | return totalCombos(label: result, idx: 0, bounds: bounds) 128 | } 129 | } 130 | 131 | print(totalCombos( 132 | label: "in", 133 | idx: 0, 134 | bounds: ["x": (1, 4000), "m": (1, 4000), "a": (1, 4000), "s": (1, 4000)] 135 | )) 136 | -------------------------------------------------------------------------------- /day19/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "main.swift" -nt "main" ]; then 5 | swiftc main.swift 6 | fi 7 | 8 | ./main 9 | -------------------------------------------------------------------------------- /day2/main.bqn: -------------------------------------------------------------------------------- 1 | strings ← •Import "bqn-libs/strings.bqn" 2 | 3 | ToNat ← 10⊸×⊸+˜´∘⌽-⟜'0' 4 | 5 | # From the problem statement. 6 | colors ← ⟨ "red", "green", "blue" ⟩ 7 | maxCubes ← ⟨ ⟨ "red", 12 ⟩, ⟨ "green", 13 ⟩, ⟨ "blue", 14 ⟩ ⟩ 8 | 9 | # Incredibly ugly way to implement a dictionary (there must be a better way…). 10 | DictGet ← { 1⊑ ⊑ (𝕩≡⊑)¨ ⊸/ 𝕨 } 11 | 12 | ParseLine ← { 13 | id‿desc ← ": " strings.Split 𝕩 14 | ⟨ ToNat 5 ↓ id, ToNat⌾⊑¨ " "⊸strings.Split¨ ∾ ", "⊸strings.Split¨ "; " strings.Split desc ⟩ 15 | } 16 | 17 | Part1 ← { 18 | id‿plays ← ParseLine 𝕩 19 | id × ∧´ ⊑⊸≤⟜(maxCubes DictGet 1⊸⊑)¨ plays 20 | } 21 | 22 | Part2 ← { 23 | id‿plays ← ParseLine 𝕩 24 | ×´ { ⌈´ ⊑¨ 𝕩⊸≡⟜(1⊸⊑)¨ ⊸/ plays }¨ colors 25 | } 26 | 27 | lines ← •file.Lines "input.txt" 28 | •Show +´ Part1¨ lines 29 | •Show +´ Part2¨ lines 30 | 31 | # Day 1, part 1 solution: 32 | # +´ ((10×⊑)+(⊑⌽))¨ (∊⟜(↕10))⊸/¨ -⟜'0' 33 | -------------------------------------------------------------------------------- /day2/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "CBQN/BQN" ]; then 4 | make -C CBQN 5 | fi 6 | 7 | ./CBQN/BQN main.bqn 8 | -------------------------------------------------------------------------------- /day20/main.tcl: -------------------------------------------------------------------------------- 1 | # Finally, the famously dynamic language Tcl! 2 | # 3 | # Going through https://www.tcl.tk/man/tcl8.5/tutorial/tcltutorial.html, which 4 | # is a classic tutorial. I feel like I'm connecting with ancestors from another 5 | # generation of programmers. Also read some of the Redis testsuite, and a bit of 6 | # writing from antirez: http://antirez.com/articoli/tclmisunderstood.html 7 | # 8 | # The absolute classic thread of "Ousterhout's dichotomy" fame: 9 | # https://groups.google.com/g/comp.lang.tcl/c/7JXGt-Uxqag/m/3JBTj5I43yAJ 10 | 11 | proc gcd {p q} { 12 | # From https://wiki.tcl-lang.org/page/Greatest+common+denominator 13 | while {$q != 0} {set q [expr {$p % [set p $q]}]} 14 | set p 15 | } 16 | 17 | while 1 { 18 | set line [gets stdin] 19 | if {$line eq ""} { 20 | break 21 | } 22 | lassign [split $line -] node dest 23 | if {$node == "broadcaster "} { 24 | set node_type broadcaster 25 | set node broadcaster 26 | } else { 27 | set node_type [string index $node 0] 28 | set node [string range $node 1 end-1] 29 | } 30 | set dest [string range $dest 2 end] 31 | set dest [lmap x [split $dest ,] {string trim $x}] 32 | lappend nodes $node 33 | dict set node_types $node $node_type 34 | dict set edges $node $dest 35 | if {"rx" in $dest} { 36 | set pre_rx $node 37 | } 38 | } 39 | 40 | proc advance {state} { 41 | global nodes node_types edges pre_rx 42 | 43 | # initial button press 44 | set signals(0) 1 45 | set signals(1) 0 46 | set rx_source_list "" 47 | 48 | # state is a dict of the last high/low signal for each node 49 | lappend frontier {broadcaster 0 ""} 50 | 51 | # initialize separate memory for conjunction cell inputs 52 | foreach node $nodes { 53 | foreach dest [dict get $edges $node] { 54 | if {[dict exists $node_types $dest]} { 55 | set node_type [dict get $node_types $dest] 56 | if {$node_type == "&"} { 57 | dict set memory $dest $node [dict get $state $node] 58 | } 59 | } 60 | } 61 | } 62 | 63 | while {[llength $frontier]} { 64 | set next_frontier {} 65 | foreach frontier_item $frontier { 66 | lassign $frontier_item node signal from_node 67 | set node_type [dict get $node_types $node] 68 | set output "" 69 | if {$node_type == "broadcaster"} { 70 | set output $signal 71 | } elseif {$node_type == "%"} { 72 | # Flip-flop cell 73 | if {!$signal} { 74 | set last_output [dict get $state $node] 75 | set output [expr {!$last_output}] 76 | } 77 | } elseif {$node_type == "&"} { 78 | # Conjunction cells are like NAND gates with memory 79 | dict set memory $node $from_node $signal 80 | set all_high 1 81 | foreach prev_input [dict values [dict get $memory $node]] { 82 | if {!$prev_input} { 83 | set all_high 0 84 | break 85 | } 86 | } 87 | set output [expr {!$all_high}] 88 | } else { 89 | # print unknown node type and exit with error 90 | puts "unknown node type $node_type" 91 | exit 1 92 | } 93 | 94 | if {$output != ""} { 95 | dict set state $node $output 96 | foreach dest [dict get $edges $node] { 97 | incr signals($output) 98 | if {$dest == $pre_rx && $output} { 99 | lappend rx_source_list $node 100 | } 101 | if {[dict exists $node_types $dest]} { 102 | lappend next_frontier [list $dest $output $node] 103 | } 104 | } 105 | } 106 | } 107 | set frontier $next_frontier 108 | } 109 | 110 | return [list $signals(0) $signals(1) $rx_source_list $state] 111 | } 112 | 113 | # puts "nodes: $nodes" 114 | # puts "node_types: $node_types" 115 | # puts "edges: $edges" 116 | 117 | foreach node $nodes { 118 | dict set initial_state $node 0 119 | } 120 | 121 | # Part 1 122 | # Advance 1000 times and output the total number of low * high signals. 123 | set state $initial_state 124 | set low_signals 0 125 | set high_signals 0 126 | for {set i 0} {$i < 1000} {incr i} { 127 | lassign [advance $state] signals(0) signals(1) _ state 128 | incr low_signals $signals(0) 129 | incr high_signals $signals(1) 130 | } 131 | puts [expr {$low_signals * $high_signals}] 132 | 133 | # Part 2 134 | # Fewest number of button presses to deliver a low signal to rx. Take the LCM of 135 | # the first step each node delivers a high signal to the conjunction. 136 | foreach node $nodes { 137 | if {$pre_rx in [dict get $edges $node]} { 138 | dict set periods $node 0 139 | } 140 | } 141 | set state $initial_state 142 | for {set i 0} 1 {incr i} { 143 | lassign [advance $state] _ _ rx_source_list state 144 | foreach node $rx_source_list { 145 | if {![dict get $periods $node]} { 146 | dict set periods $node [expr {$i + 1}] 147 | } 148 | } 149 | if {0 ni [dict values $periods]} { 150 | break 151 | } 152 | } 153 | set ans 1 154 | foreach period [dict values $periods] { 155 | set ans [expr {$ans * $period / [gcd $ans $period]}] 156 | } 157 | puts $ans 158 | -------------------------------------------------------------------------------- /day20/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | tclsh main.tcl 4 | -------------------------------------------------------------------------------- /day23/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "day23" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | fnv = "1.0.7" 8 | 9 | [profile.release] 10 | lto = "thin" 11 | -------------------------------------------------------------------------------- /day23/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | cargo build -q --target wasm32-wasi --release 4 | wasmtime target/wasm32-wasi/release/day23.wasm 5 | -------------------------------------------------------------------------------- /day23/src/main.rs: -------------------------------------------------------------------------------- 1 | // Ahh, writing a solution in Rust feels so good. Nice to be using a language 2 | // that I'm very comfortable with again. 3 | // 4 | // I thought compiling to WASI would be more difficult, but actually I didn't 5 | // have to make any code changes. It's just a cross-platform, portable binary 6 | // that runs in a sandbox. Nice! 7 | // 8 | // Long recursion depths can cause stack overflow. You _could_ just fix it by 9 | // setting `ulimit -s` very high, as in competitive programming, but I decided 10 | // to challenge myself with a non-recursive solution instead. We can do this by 11 | // explicitly pushing the "backtrack" operation onto the stack. 12 | // 13 | // By the way, Part 2 is a pretty CPU-intensive bitmask DP. It takes about 1.3 14 | // seconds to run this solution, even with a somewhat optimized bitmask DP. 15 | // Native code (not WASM) only takes 1.0 seconds for the same. 16 | 17 | use std::io; 18 | 19 | use fnv::{FnvHashMap, FnvHashSet}; 20 | 21 | type Pos = (i32, i32); 22 | 23 | enum Op { 24 | Move(Pos), 25 | Backtrack, 26 | } 27 | 28 | fn longest_path(tile: impl Fn(Pos) -> u8, src: Pos, dst: Pos) -> u32 { 29 | let mut ret = 0; 30 | let mut stack = vec![Op::Move(src)]; 31 | let mut path = Vec::new(); 32 | let mut path_set = FnvHashSet::default(); 33 | 34 | while let Some(op) = stack.pop() { 35 | match op { 36 | Op::Move(pos) => { 37 | if pos == dst { 38 | ret = ret.max(path.len() as u32); 39 | } else { 40 | let moves = match tile(pos) { 41 | b'#' => continue, 42 | b'.' => vec![(1, 0), (-1, 0), (0, 1), (0, -1)], 43 | b'>' => vec![(0, 1)], 44 | b'v' => vec![(1, 0)], 45 | b'<' => vec![(0, -1)], 46 | b'^' => vec![(-1, 0)], 47 | t => panic!("invalid tile {t}"), 48 | }; 49 | 50 | path.push(pos); 51 | path_set.insert(pos); 52 | stack.push(Op::Backtrack); 53 | for (di, dj) in moves { 54 | let n = (pos.0 + di, pos.1 + dj); 55 | if !path_set.contains(&n) { 56 | stack.push(Op::Move(n)); 57 | } 58 | } 59 | } 60 | } 61 | Op::Backtrack => { 62 | let pos = path.pop().expect("failed backtrack"); 63 | path_set.remove(&pos); 64 | } 65 | } 66 | } 67 | 68 | ret 69 | } 70 | 71 | /// Return an adjacency list of the graph formed by contracting all paths in the 72 | /// grid, indexed by the index in the `vertices` vector. 73 | fn contracted_neighbors(tile: impl Fn(Pos) -> u8, vertices: &[Pos]) -> Vec> { 74 | let mut adj = Vec::new(); 75 | for start in vertices { 76 | let mut search = vec![(*start, *start, 0)]; 77 | let mut current_adj = Vec::new(); 78 | while let Some((pos, last, dist)) = search.pop() { 79 | if let Some(j) = vertices.iter().position(|&v| v == pos) { 80 | if pos != *start { 81 | current_adj.push((j as u32, dist)); 82 | continue; 83 | } 84 | } 85 | for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] { 86 | let n = (pos.0 + di, pos.1 + dj); 87 | if n != last && tile(n) != b'#' { 88 | search.push((n, pos, dist + 1)); 89 | } 90 | } 91 | } 92 | adj.push(current_adj); 93 | } 94 | adj 95 | } 96 | 97 | /// Computes the longest path in a graph with memoized recursive bitmasks. 98 | fn graph_longest_path( 99 | adj: &[Vec<(u32, u32)>], 100 | dst: u32, 101 | n: u32, 102 | visited: u64, 103 | memo: &mut FnvHashMap<(u32, u64), Option>, 104 | ) -> Option { 105 | if n == dst { 106 | return Some(0); 107 | } 108 | if let Some(&dist) = memo.get(&(n, visited)) { 109 | return dist; 110 | } 111 | let mut dist = None; 112 | for &(j, d) in &adj[n as usize] { 113 | if visited & (1 << j) == 0 { 114 | if let Some(len) = graph_longest_path(adj, dst, j, visited | (1 << j), memo) { 115 | dist = Some(dist.unwrap_or(0).max(d + len)); 116 | } 117 | } 118 | } 119 | memo.insert((n, visited), dist); 120 | dist 121 | } 122 | 123 | fn main() { 124 | let mut grid = Vec::new(); 125 | for line in io::stdin().lines() { 126 | grid.push(line.unwrap().as_bytes().to_vec()); 127 | } 128 | 129 | let tile = |(i, j)| { 130 | if i < 0 || i >= grid.len() as i32 || j < 0 || j >= grid[0].len() as i32 { 131 | b'#' 132 | } else { 133 | grid[i as usize][j as usize] 134 | } 135 | }; 136 | 137 | let src = (0, 1); 138 | let dst = ((grid.len() - 1) as i32, (grid[0].len() - 2) as i32); 139 | 140 | // Part 1 141 | println!("{}", longest_path(tile, src, dst,)); 142 | 143 | // Part 2 144 | let mut vertices = vec![src, dst]; 145 | for i in 0..(grid.len() as i32) { 146 | for j in 0..(grid.len() as i32) { 147 | if tile((i, j)) == b'#' { 148 | continue; 149 | } 150 | let mut num_adj = 0; 151 | for (di, dj) in [(1, 0), (-1, 0), (0, 1), (0, -1)] { 152 | if tile((i + di, j + dj)) != b'#' { 153 | num_adj += 1; 154 | } 155 | } 156 | if num_adj > 2 { 157 | vertices.push((i, j)); 158 | } 159 | } 160 | } 161 | let adj = contracted_neighbors(tile, &vertices); 162 | println!( 163 | "{}", 164 | graph_longest_path(&adj, 1, 0, 1, &mut FnvHashMap::default()).unwrap() 165 | ); 166 | } 167 | -------------------------------------------------------------------------------- /day24/day24.yue: -------------------------------------------------------------------------------- 1 | --[[ 2 | This solution is in Yuescript, a dialect of MoonScript with some experimental 3 | syntax changes. MoonScript itself is a compile-to-Lua language which removes 4 | some of Lua's syntactical oddities. 5 | 6 | It was pretty easy to write this, honestly. Very happy with how it turned out. 7 | The last part was a bit tricky due to the lack of easy-to-use fp128 libraries in 8 | Lua. My code would otherwise work, so I just ended up printing out the two 9 | matrices to paste into Julia's REPL instead. 10 | ]] 11 | 12 | import gmatch from string 13 | 14 | 15 | dgesv = (A, b)-> 16 | -- Solve a linear system Ax = b for x, where A is a square (n, n) matrix. 17 | -- Returns x. 18 | n = #A 19 | assert #b == n 20 | 21 | -- LU decomposition 22 | for k = 1, n-1 23 | for i = k+1, n 24 | A[i][k] /= A[k][k] 25 | for j = k+1, n 26 | A[i][j] -= A[i][k] * A[k][j] 27 | 28 | -- Forward substitution 29 | for k = 1, n-1 30 | for i = k+1, n 31 | b[i] -= A[i][k] * b[k] 32 | 33 | -- Backward substitution 34 | for i = n, 1, -1 35 | for j = i+1, n 36 | b[i] -= A[i][j] * b[j] 37 | b[i] /= A[i][i] 38 | 39 | return b 40 | 41 | 42 | xs = [] 43 | for line in io.lines! 44 | t = [] 45 | for n in gmatch line, "-?%d+" 46 | t[] = tonumber n 47 | xs[] = t 48 | 49 | 50 | -- Part 1 51 | alow = 200000000000000 52 | ahigh = 400000000000000 53 | 54 | part1 = (x1, y1, vx1, vy1, x2, y2, vx2, vy2)-> 55 | [t, s] = dgesv [ [vx1, -vx2], [vy1, -vy2] ], [x2 - x1, y2 - y1] 56 | -- print x1 + t * vx1, x2 + s * vx2 -- should match 57 | xi = x1 + t * vx1 58 | yi = y1 + t * vy1 59 | return t > 0 and s > 0 and 60 | xi > alow and xi < ahigh and yi > alow and yi < ahigh 61 | 62 | count = 0 63 | for i = 1, #xs 64 | for j = 1, i-1 65 | p, q = xs[i], xs[j] 66 | if part1 p[1], p[2], p[4], p[5], q[1], q[2], q[4], q[5] 67 | count = count + 1 68 | print count 69 | 70 | 71 | -- Part 2 72 | --[[ 73 | Let (xr, vr) be the position and velocity vectors of the final asteroid. Work in 74 | the asteroid's reference frame. Then for all i, xi - xr is an integer multiple 75 | of vi - vr. Since two lines through the origin form a plane, we have four 76 | coplanar points {x1 - xr, v1 - vr, x2 - xr, v2 - vr} in 3D space. The plane is 77 | known, since the differences {x1 - x2, v1 - v2} both lie on it. 78 | 79 | Therefore, we have for any pair of asteroids that: 80 | - ((x1 - x2) ⨯ (v1 - v2)) ⋅ (x1 - xr) = 0 81 | - ((x1 - x2) ⨯ (v1 - v2)) ⋅ (v1 - vr) = 0 82 | 83 | These are linear equations in {xr, vr}. So we just use three pairs of asteroids, 84 | compute these coefficients, and solve for the six scalar unknowns. 85 | ]] 86 | bint = (require 'lua-bint/bint') 256 87 | assert #xs >= 4 88 | Ax = [] 89 | bx = [] 90 | for j = 1, 3 91 | p, q = xs[j], xs[j + 1] 92 | x1mx2 = [bint(p[1] - q[1]), bint(p[2] - q[2]), bint(p[3] - q[3])] 93 | v1mv2 = [bint(p[4] - q[4]), bint(p[5] - q[5]), bint(p[6] - q[6])] 94 | cross = [x1mx2[2] * v1mv2[3] - x1mx2[3] * v1mv2[2], 95 | x1mx2[3] * v1mv2[1] - x1mx2[1] * v1mv2[3], 96 | x1mx2[1] * v1mv2[2] - x1mx2[2] * v1mv2[1]] 97 | Ax[] = [cross[1], cross[2], cross[3]] 98 | bx[] = cross[1] * p[1] + cross[2] * p[2] + cross[3] * p[3] 99 | 100 | -- print Ax[1][1], Ax[1][2], Ax[1][3] 101 | -- print Ax[2][1], Ax[2][2], Ax[2][3] 102 | -- print Ax[3][1], Ax[3][2], Ax[3][3] 103 | -- print bx[1], bx[2], bx[3] 104 | 105 | -- this is off by -3, so I just solved the system with Julia :/ 106 | [xr, yr, zr] = dgesv Ax, bx 107 | print math.floor(xr + yr + zr) + 3 -- sorry, haha 108 | -------------------------------------------------------------------------------- /day24/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | YUE_PATH=Yuescript/bin/release/yue 5 | 6 | if [ ! -f $YUE_PATH ]; then 7 | echo "Yue not found, building..." 8 | make -C Yuescript -j8 release LUAI=/opt/homebrew/include/lua LUAL=/opt/homebrew/lib/lua 9 | fi 10 | 11 | $YUE_PATH -e day24.yue 12 | -------------------------------------------------------------------------------- /day25/main.zig: -------------------------------------------------------------------------------- 1 | // Zig is a nice systems programming language. Its safety and error handling 2 | // make C less stressful. 3 | // 4 | // But to start out a project, you need to know how to customize a memory 5 | // allocator and manage all of your memory manually. Those low-level details 6 | // make a quick problem like this a bit verbose. Half of the code is just 7 | // reading input! 8 | // 9 | // (If you're writing a database or high-performance system, you'd need to 10 | // customize those things anyway, and the additional control is worth it.) 11 | // 12 | // I solved this with the Ford-Fulkerson algorithm, in linear time. 13 | 14 | const std = @import("std"); 15 | 16 | const NodePair = std.meta.Tuple(&.{ u32, u32 }); 17 | 18 | /// Augment a flow by 1 unit using Ford-Fulkerson. 19 | fn augment( 20 | adj: *const std.ArrayList(std.ArrayList(u32)), 21 | visited: []bool, 22 | flow: *std.AutoHashMap(NodePair, void), 23 | src: u32, 24 | dst: u32, 25 | ) !bool { 26 | if (src == dst) { 27 | return true; 28 | } 29 | 30 | visited[src] = true; 31 | for (adj.items[src].items) |u| { 32 | if (!visited[u] and flow.get(.{ src, u }) == null) { 33 | if (try augment(adj, visited, flow, u, dst)) { 34 | if (flow.get(.{ u, src }) != null) { 35 | _ = flow.remove(.{ u, src }); 36 | } else { 37 | try flow.put(.{ src, u }, {}); 38 | } 39 | return true; 40 | } 41 | } 42 | } 43 | 44 | return false; 45 | } 46 | 47 | pub fn main() !void { 48 | var input: [512]u8 = undefined; 49 | 50 | const stdin = std.io.getStdIn().reader(); 51 | const stdout = std.io.getStdOut().writer(); 52 | 53 | // var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 54 | // const alloc = gpa.allocator(); 55 | const alloc = std.heap.c_allocator; 56 | 57 | var node_id = std.StringHashMap(u32).init(alloc); 58 | var adj = std.ArrayList(std.ArrayList(u32)).init(alloc); 59 | defer { 60 | for (adj.items) |a| { 61 | a.deinit(); 62 | } 63 | adj.deinit(); 64 | 65 | var it = node_id.iterator(); 66 | while (it.next()) |entry| { 67 | alloc.free(entry.key_ptr.*); 68 | } 69 | node_id.deinit(); 70 | } 71 | 72 | while (true) { 73 | const slice = stdin.readUntilDelimiter(&input, '\n') catch break; 74 | // try stdout.print("input: {s}\n", .{slice}); 75 | 76 | var it = std.mem.split(u8, slice, ": "); 77 | const v = try (it.next() orelse error.InvalidInput); 78 | const us = try (it.next() orelse error.InvalidInput); 79 | 80 | const vi = node_id.get(v) orelse blk: { 81 | const x = @as(u32, node_id.count()); 82 | try node_id.put(try alloc.dupe(u8, v), x); 83 | try adj.append(std.ArrayList(u32).init(alloc)); 84 | break :blk x; 85 | }; 86 | 87 | it = std.mem.split(u8, us, " "); 88 | while (it.next()) |u| { 89 | // try stdout.print("{s}: {s}\n", .{ v, u }); 90 | const ui = node_id.get(u) orelse blk: { 91 | const x = @as(u32, node_id.count()); 92 | try node_id.put(try alloc.dupe(u8, u), x); 93 | try adj.append(std.ArrayList(u32).init(alloc)); 94 | break :blk x; 95 | }; 96 | // try stdout.print("{}: {}\n", .{ vi, ui }); 97 | try adj.items[vi].append(ui); 98 | try adj.items[ui].append(vi); 99 | } 100 | } 101 | 102 | const n = @as(u32, @truncate(adj.items.len)); 103 | var j: u32 = 1; 104 | while (j < n) : (j += 1) { 105 | var flow = std.AutoHashMap(NodePair, void).init(alloc); 106 | defer flow.deinit(); 107 | 108 | var visited = std.ArrayList(bool).init(alloc); 109 | defer visited.deinit(); 110 | try visited.resize(n); 111 | 112 | // try stdout.print("augmenting: {}\n", .{j}); 113 | var totalFlow: u32 = 0; 114 | while (true) : (totalFlow += 1) { 115 | for (adj.items, 0..) |_, i| { 116 | visited.items[i] = false; 117 | } 118 | if (!(try augment(&adj, visited.items, &flow, 0, j))) { 119 | break; 120 | } 121 | } 122 | 123 | if (totalFlow == 3) { 124 | var size: u32 = 0; 125 | for (visited.items) |b| { 126 | size += if (b) 1 else 0; 127 | } 128 | try stdout.print("{}\n", .{size * (n - size)}); 129 | break; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /day25/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | if [ "main.zig" -nt "main" ]; then 5 | zig build-exe main.zig -O ReleaseSafe 6 | fi 7 | 8 | ./main 9 | -------------------------------------------------------------------------------- /day3/main.carp: -------------------------------------------------------------------------------- 1 | ;; Carp aspires to mix the performance and type-safety of Rust with the syntax and macro flexibility 2 | ;; of Clojure, but at the moment, it seems like a slightly buggy compiled Lisp. 3 | ;; 4 | ;; Despite dynamic dispatch & interfaces, it still feels more like "C + ownership" than Rust since 5 | ;; it's barebones with respect to features. In some ways, it's kind of like 6 | ;; Carp:Scheme ~ Cython:CPython. 7 | ;; 8 | ;; Note that I have generally never been good at writing nontrivial Lisp code. It comes out very 9 | ;; stilted. The biggest annoyance with Carp for me so far was mismatching parentheses, since the 10 | ;; parser is not able to make good error messages for that! Also, Carp is kind of verbose, and a lot 11 | ;; of it felt like a worse version of the same code I would write in C: just with shittier syntax. 12 | ;; 13 | ;; Carp also copies all of the syntax and annoyances of ownership in Rust, like dealing with 14 | ;; references and @/& everywhere. But without any of the benefits. And the stdlib is inconsistent 15 | ;; about when it uses references vs copies, and `Maybe` vs runtime errors. :/ 16 | ;; 17 | ;; Still, it's a cool language and certainly very unique! I'm curious how its type inference and 18 | ;; generics / static dispatch works (seems to be painless). 19 | 20 | (use Array) 21 | (use Char) 22 | (use IO) 23 | (use Int) 24 | (use Maybe) 25 | (use String) 26 | 27 | 28 | (deftype Part [row Int, col1 Int, col2 Int, value Int]) 29 | 30 | 31 | (defn display [value] 32 | (println &(str value))) 33 | 34 | 35 | (doc read-input "Read input from stdin and return an array of lines.") 36 | (sig read-input (Fn [] (Array String))) 37 | (defn read-input [] 38 | (let-do [line (chomp &(get-line)) 39 | grid []] 40 | (while-do (not (empty? &line)) 41 | (push-back! &grid @&line) 42 | (set! line (chomp &(get-line)))) 43 | grid)) 44 | 45 | 46 | (doc get-cell "Get the character at the given row and column in the grid.") 47 | (sig get-cell (Fn [(Ref (Array String)) Int Int] (Maybe Char))) 48 | (defn get-cell [grid row col] 49 | ;; Very weird how char-at doesn't do bounds checking, but nth does... 50 | (match (nth grid row) 51 | (Just r) (cond 52 | (and (>= col 0) (< col (length &r))) (Just (char-at &r col)) 53 | (Nothing)) 54 | (Nothing) (Nothing))) 55 | 56 | 57 | (doc is-symbol? "Return true if the character is a symbol.") 58 | (sig is-symbol? (Fn [Char] Bool)) 59 | (defn is-symbol? [c] 60 | (not (or (num? c) (= c \.)))) 61 | 62 | 63 | (doc has-symbol-neighbor? "Return true if the cell has a neighbor with a symbol.") 64 | (sig has-symbol-neighbor? (Fn [(Ref (Array String)) Int Int] Bool)) 65 | (defn has-symbol-neighbor? [grid row col] 66 | (any? 67 | &(fn [x] (is-symbol? (from @x \.))) 68 | &[ 69 | (get-cell grid (- row 1) (- col 1)) 70 | (get-cell grid (- row 1) col) 71 | (get-cell grid (- row 1) (+ col 1)) 72 | (get-cell grid row (- col 1)) 73 | (get-cell grid row (+ col 1)) 74 | (get-cell grid (+ row 1) (- col 1)) 75 | (get-cell grid (+ row 1) col) 76 | (get-cell grid (+ row 1) (+ col 1))])) 77 | 78 | 79 | (doc find-parts "Find all numbers in the grid next to a symbol.") 80 | (sig find-parts (Fn [(Ref (Array String))] (Array Part))) 81 | (defn find-parts [grid] 82 | ;; this function is pretty imperative C-like because the functional programming features of 83 | ;; Carp are a little too impoverished to write it more idiomatically 84 | (let-do [parts [] 85 | i 0 86 | j 0 87 | current-num -1 88 | current-num-col1 -1 89 | current-num-sym false] 90 | 91 | (while-do (< i (length grid)) 92 | (set! j 0) 93 | (set! current-num -1) 94 | (while-do (< j (length (unsafe-nth grid i))) 95 | (let [ch (unsafe-from (get-cell grid i j))] 96 | (if (num? ch) 97 | ;; If the character is a number. 98 | (do 99 | (if (= current-num -1) 100 | (do 101 | (set! current-num 0) 102 | (set! current-num-col1 j) 103 | (set! current-num-sym false)) 104 | ()) 105 | (set! current-num (+ (* current-num 10) (- (to-int ch) 48))) 106 | (set! current-num-sym (or current-num-sym (has-symbol-neighbor? grid i j)))) 107 | 108 | ;; If the character is not a number. 109 | (if (not (= current-num -1)) 110 | (do 111 | (if current-num-sym (push-back! &parts (Part.init i current-num-col1 j current-num)) ()) 112 | (set! current-num -1)) 113 | ()))) 114 | 115 | (set! j (+ j 1))) 116 | 117 | (if (not (= current-num -1)) 118 | (if current-num-sym (push-back! &parts (Part.init i current-num-col1 j current-num)) ()) 119 | ()) 120 | 121 | (set! i (+ i 1))) 122 | 123 | parts)) 124 | 125 | 126 | (doc is-adjacent "Return true if the a part is adjacent to a cell.") 127 | (sig is-adjacent (Fn [(Ref Part) Int Int] Bool)) 128 | (defn is-adjacent [part row col] 129 | (and 130 | (and (>= row (- @(Part.row part) 1)) (<= row (+ @(Part.row part) 1))) 131 | (and (>= col (- @(Part.col1 part) 1)) (< col (+ @(Part.col2 part) 1))))) 132 | 133 | 134 | (doc gear-ratios "Get all products of two part numbers neighboring a * symbol.") 135 | (sig gear-ratios (Fn [(Ref (Array String)) (Ref (Array Part))] (Array Int))) 136 | (defn gear-ratios [grid parts] 137 | ;; this function is pretty imperative C-like because the functional programming features of 138 | ;; Carp are a little too impoverished to write it more idiomatically 139 | (let-do [gears [] 140 | i 0 141 | j 0] 142 | 143 | (while-do (< i (length grid)) 144 | (set! j 0) 145 | (while-do (< j (length (unsafe-nth grid i))) 146 | (let [ch (unsafe-from (get-cell grid i j))] 147 | (if (= ch \*) 148 | (let [adjacent-parts (copy-filter &(fn [x] (is-adjacent x i j)) parts)] 149 | (if (= (length &adjacent-parts) 2) 150 | (push-back! &gears 151 | (* @(Part.value (unsafe-nth &adjacent-parts 0)) 152 | @(Part.value (unsafe-nth &adjacent-parts 1)))) 153 | ())) 154 | ())) 155 | 156 | (set! j (+ j 1))) 157 | (set! i (+ i 1))) 158 | 159 | gears)) 160 | 161 | 162 | (defn-do main [] 163 | (let-do [grid (read-input) 164 | parts (find-parts &grid)] 165 | ;; (display &grid) 166 | 167 | ;; Part 1 168 | (display (sum &(copy-map &(comp copy Part.value) &parts))) 169 | 170 | ;; Part 2 171 | (display (sum &(gear-ratios &grid &parts))))) 172 | -------------------------------------------------------------------------------- /day3/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d carp-v0.5.5-x86_64-macos ]; then 4 | wget -O carp.zip https://github.com/carp-lang/Carp/releases/download/v0.5.5/carp-v0.5.5-x86_64-macos.zip && \ 5 | unzip carp.zip ) { 9 | parts := []; 10 | var part := ""; 11 | for i := 0 to |s| 12 | { 13 | if s[i] == c { 14 | if |part| > 0 { 15 | parts := parts + [part]; 16 | part := ""; 17 | } 18 | } else { 19 | part := part + [s[i]]; 20 | } 21 | } 22 | if |part| > 0 { 23 | parts := parts + [part]; 24 | } 25 | } 26 | 27 | method StringToInt(s: string) returns (result: int) 28 | requires |s| > 0 29 | requires forall i :: 0 <= i < |s| ==> '0' <= s[i] <= '9' 30 | ensures result >= 0 31 | { 32 | result := 0; 33 | for i := 0 to |s| 34 | invariant result >= 0 35 | { 36 | var ch := s[i]; 37 | result := result * 10 + (ch - '0') as int; 38 | } 39 | } 40 | 41 | method ParseInt(s: string) returns (result: int) 42 | ensures result >= 0 43 | { 44 | var found := ""; 45 | var firstNumeral := false; 46 | for i := 0 to |s| 47 | invariant forall j :: 0 <= j < |found| ==> '0' <= found[j] <= '9' 48 | { 49 | var ch := s[i]; 50 | if '0' <= ch <= '9' { 51 | found := found + [ch]; 52 | firstNumeral := true; 53 | } else if firstNumeral { 54 | break; 55 | } 56 | } 57 | if |found| == 0 { 58 | print "warn: no numeral found in ParseInt\n"; 59 | return 0; // Special case of the empty string 60 | } 61 | result := StringToInt(found); 62 | } 63 | 64 | method ScratchWins(line: string) returns (matches: int) 65 | ensures matches >= 0 66 | { 67 | matches := 0; 68 | var parts := StringSplit(line, ':'); 69 | if |parts| != 2 { 70 | print "warn: invalid line semicolons in Part1\n"; 71 | return; 72 | } 73 | var cardNum := ParseInt(parts[0]); 74 | parts := StringSplit(parts[1], '|'); 75 | if |parts| != 2 { 76 | print "warn: missing divider '|' in Part1\n"; 77 | return; 78 | } 79 | var owned := StringSplit(parts[0], ' '); 80 | var winning := StringSplit(parts[1], ' '); 81 | 82 | var winningInts := {}; 83 | for j := 0 to |winning| 84 | invariant |winningInts| <= j 85 | { 86 | var winningCard := ParseInt(winning[j]); 87 | winningInts := winningInts + {winningCard}; 88 | } 89 | assert 0 <= |winningInts| <= |winning|; 90 | 91 | for i := 0 to |owned| 92 | invariant matches >= 0 93 | { 94 | var card := ParseInt(owned[i]); 95 | if card in winningInts { 96 | matches := matches + 1; 97 | } 98 | } 99 | } 100 | 101 | method Pow2(n: int) returns (result: int) 102 | requires n >= 0 103 | ensures result >= 1 104 | { 105 | result := 1; 106 | for i := 0 to n 107 | invariant result >= 1 108 | { 109 | result := result * 2; 110 | } 111 | } 112 | 113 | method Main() { 114 | var input := GetInput(); 115 | var lines := StringSplit(input, '\n'); 116 | 117 | var cardCopies := new int[|lines|]; 118 | for i := 0 to |lines| { 119 | cardCopies[i] := 1; 120 | } 121 | 122 | var totalPoints := 0; 123 | for i := 0 to |lines| { 124 | var matches := ScratchWins(lines[i]); 125 | var points := Pow2(matches); 126 | totalPoints := totalPoints + points; 127 | for j := i + 1 to i + matches + 1 { 128 | if j < |lines| { 129 | cardCopies[j] := cardCopies[j] + cardCopies[i]; 130 | } 131 | } 132 | } 133 | 134 | // Part 1 135 | print totalPoints, "\n"; 136 | 137 | // Part 2 138 | var totalCards := 0; 139 | for i := 0 to |lines| { 140 | totalCards := totalCards + cardCopies[i]; 141 | } 142 | print totalCards, "\n"; 143 | } 144 | -------------------------------------------------------------------------------- /day4/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "day4", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "name": "day4", 8 | "dependencies": { 9 | "bignumber.js": "^9.1.2" 10 | } 11 | }, 12 | "node_modules/bignumber.js": { 13 | "version": "9.1.2", 14 | "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", 15 | "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", 16 | "engines": { 17 | "node": "*" 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /day4/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "day4", 3 | "private": true, 4 | "dependencies": { 5 | "bignumber.js": "^9.1.2" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /day4/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -d "node_modules" ]; then 4 | npm ci 5 | fi 6 | 7 | DAFNY_PATH=~/.vscode/extensions/dafny-lang.ide-vscode-*/out/resources/4.3.0/github/dafny/Dafny.dll 8 | 9 | if [ "main.dfy" -nt "main.out.js" ]; then 10 | dotnet $DAFNY_PATH build main.dfy -t js 11 | if [ $? -ne 0 ]; then 12 | exit 1 13 | fi 14 | 15 | cat < main.out.js 16 | AdventOfCode = { 17 | GetInput() { 18 | return _dafny.Seq.UnicodeFromString(require('fs').readFileSync('/dev/stdin').toString()); 19 | }, 20 | }; 21 | EOF 22 | cat main.js >> main.out.js 23 | fi 24 | 25 | node main.out.js 26 | -------------------------------------------------------------------------------- /day5/main.erl: -------------------------------------------------------------------------------- 1 | % This solution is in Erlang. It's my first time using it, quite interesting! 2 | % 3 | % I found the expression-oriented syntax a little bit hard to get used to at first, especially for 4 | % delimiters, but it wasn't too bad. Felt a bit trickier to read than OCaml or Haskell. Not having 5 | % mutable variables was also a bit tricky, but otherwise fine! List comprehensions were nice. And 6 | % this problem happens to be pretty amenable to functional programming. 7 | % 8 | % The emphasis on pattern-matching and delimiters reminds me a lot of Prolog. Actually, now that 9 | % I search the history (https://www.erlang.org/faq/academic.html), it seems like Erlang was 10 | % originally based on a modified version of Prolog. Score! 11 | % 12 | % Namespaces felt fine, and the ease of writing functions was a nice surprise. 13 | % 14 | % I tripped over the Erlang shell for 30 minutes until I figured out how to run my code. It's 15 | % definitely not the most obvious tool for modern programmers used to CLI. Guess the Unix 16 | % philosophy won out over the past 4 decades. 17 | % 18 | % In summary, lots of cool ideas and unfortunately also some quirks. But the old, OG languages have 19 | % their charm, even if they're not super intuitive to programmers today. :') 20 | 21 | -module(main). 22 | 23 | -export([start/0]). 24 | 25 | -record(data, { 26 | seeds = [] :: [integer()], 27 | maps = [] :: [[{integer(), integer(), integer()}]] 28 | }). 29 | 30 | % Read all of the lines from stdin and parse it into the data record. 31 | % 32 | % This creates an extra [] at the beginning of the maps list, which needs to be stripped off 33 | % by the caller first. 34 | parse_input() -> 35 | case io:get_line("") of 36 | eof -> 37 | #data{maps = [[]]}; 38 | % Skip blank lines. 39 | "\n" -> 40 | parse_input(); 41 | Line -> 42 | TrimmedLine = string:trim(Line, trailing), 43 | case string:prefix(TrimmedLine, "seeds:") /= nomatch of 44 | true -> 45 | [_ | Seeds] = string:tokens(TrimmedLine, " "), 46 | SeedsInt = lists:map(fun list_to_integer/1, Seeds), 47 | Rest = parse_input(), 48 | #data{seeds = SeedsInt, maps = Rest#data.maps}; 49 | false -> 50 | case string:find(TrimmedLine, "map:") /= nomatch of 51 | true -> 52 | #data{maps = Maps} = parse_input(), 53 | #data{maps = [[] | Maps]}; 54 | false -> 55 | [X, Y, Z] = lists:map( 56 | fun list_to_integer/1, string:tokens(TrimmedLine, " ") 57 | ), 58 | #data{maps = [CurrentMap | Maps]} = parse_input(), 59 | #data{maps = [[{X, Y, Z} | CurrentMap] | Maps]} 60 | end 61 | end 62 | end. 63 | 64 | to_location(Num, []) -> 65 | Num; 66 | to_location(Num, [[] | Maps]) -> 67 | to_location(Num, Maps); 68 | to_location(Num, [[{X, Y, Z} | Map] | Maps]) -> 69 | if 70 | Num >= Y andalso Num < Y + Z -> 71 | to_location(X + Num - Y, Maps); 72 | true -> 73 | to_location(Num, [Map | Maps]) 74 | end. 75 | 76 | min_range_to_location(Start, _, []) -> 77 | Start; 78 | min_range_to_location(Start, End, [[] | Maps]) -> 79 | min_range_to_location(Start, End, Maps); 80 | min_range_to_location(Start, End, [[{X, Y, Z} | Map] | Maps]) -> 81 | D = X - Y, 82 | if 83 | Start >= Y + Z orelse End =< Y -> 84 | min_range_to_location(Start, End, [Map | Maps]); 85 | Start >= Y andalso End =< Y + Z -> 86 | min_range_to_location(Start + D, End + D, Maps); 87 | Start >= Y -> 88 | lists:min([ 89 | min_range_to_location(Start + D, Y + Z + D, Maps), 90 | min_range_to_location(Y + Z, End, [Map | Maps]) 91 | ]); 92 | End =< Y + Z -> 93 | lists:min([ 94 | min_range_to_location(Start, Y, [Map | Maps]), 95 | min_range_to_location(Y + D, End + D, Maps) 96 | ]); 97 | true -> 98 | lists:min([ 99 | min_range_to_location(Start, Y, [Map | Maps]), 100 | min_range_to_location(Y + D, Y + Z + D, Maps), 101 | min_range_to_location(Y + Z, End, [Map | Maps]) 102 | ]) 103 | end. 104 | 105 | pairs([X | [Y | Rest]]) -> 106 | [{X, Y} | pairs(Rest)]; 107 | pairs([]) -> 108 | []. 109 | 110 | start() -> 111 | #data{seeds = Seeds, maps = [[] | Maps]} = parse_input(), 112 | 113 | % Part 1 114 | F = fun(S) -> to_location(S, Maps) end, 115 | io:fwrite("~p\n", [lists:min(lists:map(F, Seeds))]), 116 | 117 | % Part 2 118 | G = fun({S, L}) -> min_range_to_location(S, S + L, Maps) end, 119 | io:fwrite("~p\n", [lists:min(lists:map(G, pairs(Seeds)))]). 120 | -------------------------------------------------------------------------------- /day5/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ "main.erl" -nt "main.beam" ]; then 6 | erl -compile main 7 | fi 8 | 9 | erl -noshell -s main -s erlang halt 10 | -------------------------------------------------------------------------------- /day6/.gitignore: -------------------------------------------------------------------------------- 1 | *.fpkg 2 | *.jar 3 | artifact/ 4 | build/ 5 | lib/ 6 | -------------------------------------------------------------------------------- /day6/flix.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "day6" 3 | description = "Advent of Code Day 6 solution" 4 | version = "0.1.0" 5 | flix = "0.42.0" 6 | authors = [] 7 | -------------------------------------------------------------------------------- /day6/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ ! -f "flix.jar" ]; then 6 | wget https://github.com/flix/flix/releases/download/v0.42.0/flix.jar 7 | fi 8 | 9 | if [ "src/Main.flix" -nt "artifact/day6.jar" ]; then 10 | java -jar flix.jar build 11 | java -jar flix.jar build-jar 12 | fi 13 | 14 | java -jar artifact/day6.jar 15 | -------------------------------------------------------------------------------- /day6/src/Main.flix: -------------------------------------------------------------------------------- 1 | // I'm kind of bummed that I didn't get a chance to use Flix's Datalog engine in this problem! It 2 | // would've been fantastic for the solution to Day 5 though. 3 | 4 | use Functor.{<$>, <$$>} 5 | use Monad.{>>=} 6 | 7 | def parseInts(line: String): Option[List[Int64]] = 8 | line |> String.words <$$> Int64.fromString |> List.sequence 9 | 10 | def concatenateNums(nums: List[Int64]): Option[Int64] = 11 | Int64.toString <$> nums |> List.fold |> Int64.fromString 12 | 13 | def binarySearch(lower: Int64, upper: Int64, predicate: Int64 -> Bool): Int64 = 14 | if (lower == upper) 15 | lower 16 | else 17 | let mid = (lower + upper) / 2i64; 18 | if (predicate(mid)) 19 | binarySearch(lower, mid, predicate) 20 | else 21 | binarySearch(mid + 1i64, upper, predicate) 22 | 23 | /// 24 | /// Returns the number of integers `i < time` such that `i * (time - i) > distance`. 25 | /// 26 | def beatRecord(time: Int64, distance: Int64): Int64 = 27 | let halfway = time / 2i64; 28 | let k = binarySearch(0i64, halfway + 1i64, i -> i * (time - i) > distance); 29 | if (k > halfway) { 30 | 0i64 31 | } else { 32 | time - 2i64 * k + 1i64 33 | } 34 | 35 | def main(): Unit \ IO = 36 | let result = for ( 37 | file <- File.open("input.txt", File.Mode.ReadOnly) |> 38 | Result.mapErr(_ -> "Could not open file."); 39 | times <- File.readLine!(file) |> Result.toOption >>= 40 | String.stripPrefix(substr = "Time:") >>= parseInts |> 41 | Option.toOk("Could not parse times."); 42 | distances <- File.readLine!(file) |> Result.toOption >>= 43 | String.stripPrefix(substr = "Distance:") >>= parseInts |> 44 | Option.toOk("Could not parse distances."); 45 | _ <- 46 | if (List.length(times) == List.length(distances)) 47 | Ok(()) 48 | else 49 | Err("Number of times and distances do not match."); 50 | timesCat <- concatenateNums(times) |> Option.toOk("Could not concatenate times."); 51 | distancesCat <- concatenateNums(distances) |> Option.toOk("Could not concatenate distances.") 52 | ) yield { 53 | // Part 1 54 | List.zipWith(beatRecord, times, distances) |> List.foldLeft((x, y) -> x * y, 1i64) |> println; 55 | 56 | // Part 2 57 | beatRecord(timesCat, distancesCat) |> println 58 | }; 59 | match result { 60 | case Ok(()) => (), 61 | case Err(message) => { 62 | println("Error: " ++ message) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /day7/.gitignore: -------------------------------------------------------------------------------- 1 | *.beam 2 | *.ez 3 | build 4 | erl_crash.dump 5 | -------------------------------------------------------------------------------- /day7/gleam.toml: -------------------------------------------------------------------------------- 1 | name = "day7" 2 | version = "0.1.0" 3 | 4 | [dependencies] 5 | gleam_stdlib = "~> 0.32" 6 | simplifile = "~> 1.0" 7 | 8 | [dev-dependencies] 9 | gleeunit = "~> 1.0" 10 | -------------------------------------------------------------------------------- /day7/manifest.toml: -------------------------------------------------------------------------------- 1 | # This file was generated by Gleam 2 | # You typically do not need to edit this file 3 | 4 | packages = [ 5 | { name = "gleam_stdlib", version = "0.33.1", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "3CEAD7B153D896499C78390B22CC968620C27500C922AED3A5DD7B536F922B25" }, 6 | { name = "gleeunit", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "D3682ED8C5F9CAE1C928F2506DE91625588CC752495988CBE0F5653A42A6F334" }, 7 | { name = "simplifile", version = "1.0.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "0BD6F0E7DA1A7E11D18B8AD48453225CAFCA4C8CFB4513D217B372D2866C501C" }, 8 | ] 9 | 10 | [requirements] 11 | gleam_stdlib = { version = "~> 0.32" } 12 | gleeunit = { version = "~> 1.0" } 13 | simplifile = { version = "~> 1.0" } 14 | -------------------------------------------------------------------------------- /day7/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "src/day7.gleam" -nt "build/dev/erlang/day7/ebin/day7.beam" ]; then 4 | gleam build 5 | fi 6 | 7 | export ERL_LIBS=build/dev/erlang 8 | erl -noshell -s day7 main -s erlang halt 9 | -------------------------------------------------------------------------------- /day7/src/day7.gleam: -------------------------------------------------------------------------------- 1 | // And… yet another BEAM VM language. It's my season of Erlang! 2 | // 3 | // First impressions: Gleam appears to be a statically typed functional 4 | // programming language that compiles to Erlang (and JS?). It comes with the 5 | // classic cocktail of functional programming features, plus immutable data 6 | // types and an actor model for concurrency. 7 | // 8 | // It's initially unclear to me how different Gleam is from Elixir, since both 9 | // have immutable variables, rely on the Erlang runtime, and have modern syntax. 10 | // I guess Gleam has static typing and appears a bit more Rusty. 11 | // 12 | // Let's start writing code! It looks like a simple language. 13 | // 14 | // The book and examples have no loops though. I guess I could write everything 15 | // recursively? It's not as natural as in Erlang or Haskell though, where 16 | // function declarations come with pattern matching included. 17 | // 18 | // Nevermind: Didn't need to do any recursion at all! The functional programming 19 | // library functions feel ergonomic, like OCaml / F#, but more effortless. 20 | 21 | import gleam/dict 22 | import gleam/int 23 | import gleam/io 24 | import gleam/list 25 | import gleam/option.{None, Some} 26 | import gleam/order.{type Order, Eq} 27 | import gleam/result 28 | import gleam/string 29 | import simplifile.{read} 30 | 31 | fn display(obj: a) { 32 | io.println(string.inspect(obj)) 33 | } 34 | 35 | // Given in the problem statemenet. 36 | fn rank(c: UtfCodepoint, part2: Bool) -> Int { 37 | // There's no literal syntax for chars, so I guess I'll convert the codepoint 38 | // back to a string and match on that? 39 | let s = string.from_utf_codepoints([c]) 40 | case s { 41 | "2" -> 2 42 | "3" -> 3 43 | "4" -> 4 44 | "5" -> 5 45 | "6" -> 6 46 | "7" -> 7 47 | "8" -> 8 48 | "9" -> 9 49 | "T" -> 10 50 | "J" -> 51 | case part2 { 52 | True -> 1 53 | False -> 11 54 | } 55 | "Q" -> 12 56 | "K" -> 13 57 | "A" -> 14 58 | } 59 | } 60 | 61 | // Returns the partition of a string, like "KKAKA" -> [3, 2]. 62 | fn get_partition(hand: String) -> List(Int) { 63 | string.to_utf_codepoints(hand) 64 | |> list.fold( 65 | dict.new(), 66 | fn(m, c) { dict.update(m, c, fn(x) { option.unwrap(x, 0) + 1 }) }, 67 | ) 68 | |> dict.values 69 | |> list.sort(by: order.reverse(int.compare)) 70 | } 71 | 72 | // Compute the highest-rank partition including J "wildcards". 73 | fn get_partition_wildcard(hand: String) -> List(Int) { 74 | let counts = 75 | string.to_utf_codepoints(hand) 76 | |> list.fold( 77 | dict.new(), 78 | fn(m, c) { dict.update(m, c, fn(x) { option.unwrap(x, 0) + 1 }) }, 79 | ) 80 | 81 | // This seems awkward, but there's no other way to create `UtfCodepoint` types. 82 | let assert [joker] = string.to_utf_codepoints("J") 83 | let wildcards = result.unwrap(dict.get(counts, joker), 0) 84 | 85 | case 86 | dict.delete(counts, joker) 87 | |> dict.values 88 | |> list.sort(by: order.reverse(int.compare)) 89 | { 90 | [f, ..rest] -> [f + wildcards, ..rest] 91 | [] -> [wildcards] 92 | } 93 | } 94 | 95 | // Compare two lists of ints in lexicographic order. 96 | fn lexicographic(a: List(Int), b: List(Int)) -> Order { 97 | list.zip(a, b) 98 | |> list.fold_until( 99 | None, 100 | fn(_, x) { 101 | case int.compare(x.0, x.1) { 102 | Eq -> list.Continue(None) 103 | c -> list.Stop(Some(c)) 104 | } 105 | }, 106 | ) 107 | |> option.lazy_unwrap(fn() { int.compare(list.length(a), list.length(b)) }) 108 | } 109 | 110 | fn compare_part1(h1: #(String, Int), h2: #(String, Int)) -> Order { 111 | let p1 = get_partition(h1.0) 112 | let p2 = get_partition(h2.0) 113 | case lexicographic(p1, p2) { 114 | Eq -> 115 | lexicographic( 116 | string.to_utf_codepoints(h1.0) 117 | |> list.map(rank(_, False)), 118 | string.to_utf_codepoints(h2.0) 119 | |> list.map(rank(_, False)), 120 | ) 121 | c -> c 122 | } 123 | } 124 | 125 | fn compare_part2(h1: #(String, Int), h2: #(String, Int)) -> Order { 126 | let p1 = get_partition_wildcard(h1.0) 127 | let p2 = get_partition_wildcard(h2.0) 128 | case lexicographic(p1, p2) { 129 | Eq -> 130 | lexicographic( 131 | string.to_utf_codepoints(h1.0) 132 | |> list.map(rank(_, True)), 133 | string.to_utf_codepoints(h2.0) 134 | |> list.map(rank(_, True)), 135 | ) 136 | c -> c 137 | } 138 | } 139 | 140 | fn score(hands: List(#(String, Int))) -> Int { 141 | hands 142 | |> list.index_map(with: fn(i, x) { { i + 1 } * x.1 }) 143 | |> int.sum 144 | } 145 | 146 | pub fn main() { 147 | let assert Ok(file) = read(from: "input.txt") 148 | 149 | let hands = 150 | file 151 | |> string.trim_right 152 | |> string.split(on: "\n") 153 | |> list.map(with: fn(s) { 154 | let assert [hand, score] = string.split(s, on: " ") 155 | let assert Ok(score) = int.parse(score) 156 | #(hand, score) 157 | }) 158 | 159 | // Part 1 160 | list.sort(hands, by: compare_part1) 161 | |> score 162 | |> display 163 | 164 | // Part 2 165 | list.sort(hands, by: compare_part2) 166 | |> score 167 | |> display 168 | } 169 | -------------------------------------------------------------------------------- /day8/app/Main.hs: -------------------------------------------------------------------------------- 1 | {- 2 | It's not my first time using Haskell for sure, but this was really fun! 3 | 4 | Particularly nice today was the way I could apply lazy evaluation to infinite 5 | lists, and it would just work. The code actually executed pretty fast too, 6 | despite all of the laziness. 7 | 8 | Work became a lot easier when I got used to `mapM` and `mapM_`. 9 | 10 | Haskell feels a little bit different, since I learn something new every time I 11 | come back to it. It's an experience worth repeating many times. 12 | -} 13 | 14 | module Main where 15 | 16 | import Control.Monad (void) 17 | import Data.List (findIndex) 18 | import Data.Map (Map, fromList, lookup) 19 | import Text.Parsec (char, letter, many1, parse, string) 20 | import Text.Parsec.String (Parser) 21 | 22 | type Graph = Map String (String, String) 23 | 24 | edgeE :: Parser (String, (String, String)) 25 | edgeE = do 26 | node <- many1 letter 27 | void $ string " = (" 28 | left <- many1 letter 29 | void $ string ", " 30 | right <- many1 letter 31 | void $ char ')' 32 | pure (node, (left, right)) 33 | 34 | parseLineOrExit :: String -> IO (String, (String, String)) 35 | parseLineOrExit s = case parse edgeE "" s of 36 | Left pe -> error $ show pe 37 | Right tr -> pure tr 38 | 39 | -- Move a specific sequence of steps from a starting point, stopping at "ZZZ". 40 | move :: Graph -> String -> String -> String 41 | move g = go 42 | where 43 | go :: String -> String -> String 44 | go node "" = node 45 | go "ZZZ" _ = "ZZZ" 46 | go node (c : rest) = case Data.Map.lookup node g of 47 | Nothing -> error $ "No node " <> node 48 | Just (left, right) -> case c of 49 | 'L' -> go left rest 50 | 'R' -> go right rest 51 | _ -> error $ "Invalid direction " <> [c] 52 | 53 | -- Find the smallest non-negative integer that satisfies the predicate, repeatedly doubling. 54 | binSearchInt :: (Int -> Bool) -> Int 55 | binSearchInt p = go 1 56 | where 57 | go :: Int -> Int 58 | go n = if p n then search 0 n else go (2 * n) 59 | search :: Int -> Int -> Int 60 | search lo hi 61 | | lo == hi = lo 62 | | p mid = search lo mid 63 | | otherwise = search (mid + 1) hi 64 | where 65 | mid = (lo + hi) `div` 2 66 | 67 | -- Return a list of nodes from moving from a starting point. 68 | path :: Graph -> String -> String -> [String] 69 | path _ node "" = [node] 70 | path g node (c : rest) = node : path g next rest 71 | where 72 | next = case Data.Map.lookup node g of 73 | Nothing -> error $ "No node " <> node 74 | Just (left, right) -> case c of 75 | 'L' -> left 76 | 'R' -> right 77 | _ -> error $ "Invalid direction " <> [c] 78 | 79 | isStart2 :: String -> Bool 80 | isStart2 node = last node == 'A' 81 | 82 | isFinal2 :: String -> Bool 83 | isFinal2 node = last node == 'Z' 84 | 85 | main :: IO () 86 | main = do 87 | directions <- getLine 88 | _ <- getLine 89 | inp <- getContents 90 | edges <- mapM parseLineOrExit $ lines inp 91 | let nodes = map fst edges 92 | let g = fromList edges 93 | 94 | -- Part 1 95 | print $ binSearchInt (\n -> move g "AAA" (take n $ cycle directions) == "ZZZ") 96 | 97 | -- Hmm, after inspection, it seems from printing out the actual valid indices that they're just 98 | -- integer multiples of the first valid index. So we can just take the LCM. 99 | 100 | {- 101 | mapM_ (go g directions) $ filter isStart2 nodes 102 | where 103 | go :: Graph -> String -> String -> IO () 104 | go g directions node = do 105 | print node 106 | print $ take 100 $ findIndices isFinal2 $ path g node (cycle directions) 107 | -} 108 | 109 | -- Part 2 110 | let indices = 111 | map 112 | ( \node -> case findIndex isFinal2 $ path g node (cycle directions) of 113 | Nothing -> error $ "No path from " <> node 114 | Just n -> n 115 | ) 116 | $ filter isStart2 nodes 117 | 118 | print $ foldl lcm 1 indices 119 | -------------------------------------------------------------------------------- /day8/day8.cabal: -------------------------------------------------------------------------------- 1 | cabal-version: 3.0 2 | name: day8 3 | version: 0.1.0.0 4 | author: Eric Zhang 5 | maintainer: ekzhang1@gmail.com 6 | build-type: Simple 7 | 8 | common warnings 9 | ghc-options: -Wall -fllvm -O3 10 | 11 | executable day8 12 | import: warnings 13 | main-is: Main.hs 14 | build-depends: 15 | base ^>=4.14.3.0, 16 | containers ^>=0.6.5.1, 17 | parsec ^>=3.1.17.0 18 | hs-source-dirs: app 19 | default-language: Haskell2010 20 | -------------------------------------------------------------------------------- /day8/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH="/opt/homebrew/Cellar/llvm@12/12.0.1_1/bin:$PATH" 4 | cabal run 5 | -------------------------------------------------------------------------------- /day9/main.io: -------------------------------------------------------------------------------- 1 | // The best way I can describe Io is as "metaprogramming paradise." 2 | // 3 | // I started using Io, and in the first minute, I had accidentally modified the 4 | // global List prototype and was frantically figuring out how to fix it via 5 | // introspection. And I was getting errors like "Object does not *respond* to …" 6 | // when I had misspelt a variable. :') 7 | // 8 | // Don't even mention the fact that every object takes literals as messages, but 9 | // it just shadows the object! WTF, that's so cool. And scary. But cool! 10 | // 11 | // Still going a bit over my head at the moment, but it'll come with time. 12 | // 13 | // What a crazy flexible syntax; almost every program is syntactically valid. 14 | // And it's not as annoying to use as Lisp. Could probably jam on some cute DSL 15 | // ideas here. It's still really easy & fun to write code! Feels a bit like BQN 16 | // in way it composes, but with friendlier syntax. 17 | // 18 | // I found myself using the REPL a lot to see what methods were available, given 19 | // the lack of documentation. But it's super concise. 20 | 21 | extrapolate := method( 22 | nums := call target 23 | if(nums unique size == 1, return nums first) 24 | nums2 := List clone 25 | for(i, 0, nums size - 2, 26 | nums2 append(nums at(i + 1) - nums at(i)) 27 | ) 28 | nums last + nums2 extrapolate 29 | ) 30 | 31 | lines := List clone 32 | 33 | loop( 34 | line := File standardInput readLine 35 | if(line not, break) 36 | nums := line split map(asNumber) 37 | lines append(nums) 38 | ) 39 | 40 | lines map(extrapolate) sum println 41 | lines map(reverse) map(extrapolate) sum println 42 | 43 | // Io's version of "return 0;" is just "0". I love it. 44 | 0 45 | -------------------------------------------------------------------------------- /day9/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | io main.io 3 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := true 2 | 3 | list: 4 | just -l 5 | 6 | load DAY: 7 | ./load_data.py {{DAY}} 8 | 9 | run DAY: (load DAY) 10 | cd {{DAY}} && ./run.sh < input.txt 11 | 12 | run-all: 13 | #!/usr/bin/env sh 14 | set -e 15 | for i in $(seq 1 25); do 16 | echo "[just run day$i]" 17 | just run "day$i" 2> /dev/null 18 | done 19 | -------------------------------------------------------------------------------- /load_data.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Loads data from the AOC website.""" 3 | 4 | import os 5 | import sys 6 | from urllib.request import Request, urlopen 7 | 8 | if len(sys.argv) != 2: 9 | print("Usage: ./load_data.py ", file=sys.stderr) 10 | sys.exit(1) 11 | 12 | session = os.environ.get("SESSION") 13 | if session is None: 14 | print("Error: Missing session token. Did you set the SESSION variable in .env?") 15 | sys.exit(1) 16 | 17 | day = sys.argv[1] 18 | assert day.startswith("day") 19 | 20 | if not os.path.exists(day): 21 | os.makedirs(day) 22 | 23 | file_path = os.path.join(day, "input.txt") 24 | if not os.path.exists(file_path): 25 | num = int(day[3:]) 26 | req = Request(f"https://adventofcode.com/2023/day/{num}/input") 27 | req.add_header("Cookie", f"session={session}") 28 | with urlopen(req) as resp: 29 | data = resp.read().decode("utf-8") 30 | with open(file_path, "w") as file: 31 | file.write(data) 32 | --------------------------------------------------------------------------------