diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml
index 2cd25d68..8ef22f5e 100644
--- a/.github/workflows/linux.yml
+++ b/.github/workflows/linux.yml
@@ -22,8 +22,8 @@ jobs:
       - uses: actions/checkout@v3
       - run: sudo apt install -y llvm-${{ matrix.llvm-version }}-dev clang-${{ matrix.llvm-version }} make valgrind
       - run: LLVM_CONFIG=llvm-config-${{ matrix.llvm-version }} make
-      - run: ./runtests.sh --verbose './jou ${{ matrix.opt-level }} %s'
-      - run: ./runtests.sh --verbose './jou ${{ matrix.opt-level }} --verbose %s'
+      - run: ./runtests.sh --verbose 'jou ${{ matrix.opt-level }} %s'
+      - run: ./runtests.sh --verbose 'jou ${{ matrix.opt-level }} --verbose %s'
       # Valgrinding is slow. Do it only when this file or something in the compiler has been modified.
       - name: Figure out if we need to run tests with valgrind
         id: check-need-valgrind
@@ -35,7 +35,7 @@ jobs:
             echo doit=no >> $GITHUB_OUTPUT
           fi
       - if: ${{ steps.check-need-valgrind.outputs.doit == 'yes' }}
-        run: ./runtests.sh --verbose --valgrind './jou ${{ matrix.opt-level }} %s'
+        run: ./runtests.sh --verbose --valgrind 'jou ${{ matrix.opt-level }} %s'
       # valgrind+verbose isn't meaningful: test script would ignore valgrind output
       - run: make clean
       - name: Check that "make clean" deleted all files not committed to Git
diff --git a/.gitignore b/.gitignore
index bec9aa61..8255fe1c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,8 @@ jou_compiled
 
 # ide stuff
 /.vscode/
+
+# Advent of Code input files https://adventofcode.com/
+# These are large text files, and each AoC user gets different input files.
+# Tests use sampleinput.txt files, copied from problem descriptions
+/examples/aoc2023/day*/input.txt
diff --git a/README.md b/README.md
index 98a32902..e3d85ab6 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,10 @@ def main() -> int:
 
 See the [examples](./examples/) and [tests](./tests/) directories for more example programs.
 
+So far, Jou is usable enough to do [Advent of Code 2023](https://adventofcode.com/).
+We'll see whether I get 50 stars with Jou this year.
+See [examples/aoc2023](./examples/aoc2023/) for the code.
+
 Goals:
 - Minimalistic feel of C + simple Python-style syntax
 - Possible target audiences:
diff --git a/compare_compilers.sh b/compare_compilers.sh
index d730e2b3..6f20b51b 100755
--- a/compare_compilers.sh
+++ b/compare_compilers.sh
@@ -28,7 +28,8 @@ for arg in "$@"; do
 done
 
 if [ ${#files[@]} = 0 ]; then
-    mapfile -t files < <( find stdlib examples tests -name '*.jou' | sort )
+    # TODO: do not skip Advent Of Code files
+    mapfile -t files < <( find stdlib examples tests -name '*.jou' | grep -v aoc2023 | sort )
 fi
 if [ ${#actions[@]} = 0 ]; then
     actions=(tokenize parse run)
diff --git a/examples/aoc2023/day01/corner-cases.txt b/examples/aoc2023/day01/corner-cases.txt
new file mode 100644
index 00000000..f93a539f
--- /dev/null
+++ b/examples/aoc2023/day01/corner-cases.txt
@@ -0,0 +1,24 @@
+eight3fiveninefivemtxm9eightwot
+x1vzgnpdjtwonert
+t3sixtwonedmj
+9qzbqxmqonefiveknrnzpxoneightrq
+4btqghfcqx25fivetwo95oneightxf
+48sevensixfoureightwodx
+15qhpvsevensixoneightt
+7fiveeightoneightvs
+fivesevenfour9jslninesevenjtttt7oneightssr
+fivefour852eightwosx
+4ssskfrfqhz9eightfour37oneightjm
+25sixjrjqgl5fivekhtxstwovgxzfpvzfmoneightb
+65rdlfdxjeightwox
+ninercxgj4txpflzvhgtwoneqh
+49seven7threeeightwokr
+onetwonine4noneightvk
+3eightfive88eightwor
+6q9pjsdzponerfnqt6eightwob
+1eightworg
+8onecctzfxreighteightwoq
+ptwo2fivedqxthreesdmbvdcdxrtwonegt
+hdfdltsrtwoseventbqr7pckgcqtcgh8onetwonez
+62xvvkpbhhbthreetwooneeightwozr
+ninesevensrzxkzpmgz8kcjxsbdftwoner
diff --git a/examples/aoc2023/day01/part1.jou b/examples/aoc2023/day01/part1.jou
new file mode 100644
index 00000000..2e0a9f2c
--- /dev/null
+++ b/examples/aoc2023/day01/part1.jou
@@ -0,0 +1,27 @@
+import "stdlib/str.jou"
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+
+
+def main() -> int:
+    f = fopen("sampleinput.txt", "r")
+    assert f != NULL
+
+    result = 0
+    line: byte[1000]
+    while fgets(line, sizeof(line) as int, f) != NULL:
+        first: byte* = line
+        while *first != '\0' and not is_ascii_digit(*first):
+            first++
+
+        last = &line[strlen(line) - 1]
+        while last > &line[0] and not is_ascii_digit(*last):
+            last--
+
+        result += 10*(*first - '0')
+        result += *last - '0'
+
+    fclose(f)
+
+    printf("%d\n", result)  # Output: 142
+    return 0
diff --git a/examples/aoc2023/day01/part2.jou b/examples/aoc2023/day01/part2.jou
new file mode 100644
index 00000000..4767bc86
--- /dev/null
+++ b/examples/aoc2023/day01/part2.jou
@@ -0,0 +1,39 @@
+import "stdlib/str.jou"
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+
+
+def parse_prefix_digit(s: byte*) -> int:
+    if is_ascii_digit(s[0]):
+        return s[0] - '0'
+
+    strings = ["", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine"]
+    for i = 1; i <= 9; i++:
+        if starts_with(s, strings[i]):
+            return i
+
+    return -1
+
+
+def main() -> int:
+    f = fopen("sampleinput2.txt", "r")
+    assert f != NULL
+
+    result = 0
+    line: byte[1000]
+    while fgets(line, sizeof(line) as int, f) != NULL:
+        first: byte* = line
+        while *first != '\0' and parse_prefix_digit(first) == -1:
+            first++
+
+        last = &line[strlen(line) - 1]
+        while last > &line[0] and parse_prefix_digit(last) == -1:
+            last--
+
+        result += 10*parse_prefix_digit(first)
+        result += parse_prefix_digit(last)
+
+    fclose(f)
+
+    printf("%d\n", result)  # Output: 281
+    return 0
diff --git a/examples/aoc2023/day01/sampleinput.txt b/examples/aoc2023/day01/sampleinput.txt
new file mode 100644
index 00000000..7bbc69a6
--- /dev/null
+++ b/examples/aoc2023/day01/sampleinput.txt
@@ -0,0 +1,4 @@
+1abc2
+pqr3stu8vwx
+a1b2c3d4e5f
+treb7uchet
diff --git a/examples/aoc2023/day01/sampleinput2.txt b/examples/aoc2023/day01/sampleinput2.txt
new file mode 100644
index 00000000..41aa89cf
--- /dev/null
+++ b/examples/aoc2023/day01/sampleinput2.txt
@@ -0,0 +1,7 @@
+two1nine
+eightwothree
+abcone2threexyz
+xtwone3four
+4nineeightseven2
+zoneight234
+7pqrstsixteen
diff --git a/examples/aoc2023/day02/part1.jou b/examples/aoc2023/day02/part1.jou
new file mode 100644
index 00000000..5e44a301
--- /dev/null
+++ b/examples/aoc2023/day02/part1.jou
@@ -0,0 +1,53 @@
+import "stdlib/str.jou"
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+
+
+def game_is_possible(game_data: byte*) -> bool:
+    # loop in ; or , separated chunks
+    while *game_data != '\0':
+        chunk_len = strcspn(game_data, ";,")
+        chunk = game_data
+
+        # \0 terminate the chunk and skip it
+        game_data = &game_data[chunk_len]
+        if *game_data != '\0':  # check needed to avoid overflow at end of string
+            *game_data = '\0'
+            game_data++
+
+        trim_ascii_whitespace(chunk)
+
+        n: int
+        color: byte[10]
+        assert sscanf(chunk, "%d %10s", &n, color) == 2
+
+        if strcmp(color, "red") == 0 and n > 12:
+            return False
+        if strcmp(color, "green") == 0 and n > 13:
+            return False
+        if strcmp(color, "blue") == 0 and n > 14:
+            return False
+
+    return True
+
+
+def main() -> int:
+    f = fopen("sampleinput.txt", "r")
+    assert f != NULL
+
+    result = 0
+    line: byte[1000]
+    while fgets(line, sizeof(line) as int, f) != NULL:
+        assert starts_with(line, "Game ")
+        game_id = atoi(&line[5])
+
+        id_end = strchr(line, ':')
+        assert id_end != NULL
+
+        if game_is_possible(&id_end[1]):
+            result += game_id
+
+    fclose(f)
+
+    printf("%d\n", result)  # Output: 8
+    return 0
diff --git a/examples/aoc2023/day02/part2.jou b/examples/aoc2023/day02/part2.jou
new file mode 100644
index 00000000..4517320e
--- /dev/null
+++ b/examples/aoc2023/day02/part2.jou
@@ -0,0 +1,62 @@
+import "stdlib/str.jou"
+import "stdlib/io.jou"
+import "stdlib/ascii.jou"
+
+
+class Game:
+    red: int
+    green: int
+    blue: int
+
+    def update(self, text: byte*) -> void:
+        n: int
+        color: byte[10]
+        assert sscanf(text, "%d %10s", &n, color) == 2
+
+        if strcmp(color, "red") == 0 and n > self->red:
+            self->red = n
+        if strcmp(color, "green") == 0 and n > self->green:
+            self->green = n
+        if strcmp(color, "blue") == 0 and n > self->blue:
+            self->blue = n
+
+    def get_power(self) -> int:
+        return self->red * self->green * self->blue
+
+
+def parse_game(game_data: byte*) -> Game:
+    result = Game{}
+
+    # loop in ; or , separated chunks
+    while *game_data != '\0':
+        chunk_len = strcspn(game_data, ";,")
+        chunk = game_data
+
+        # \0 terminate the chunk and skip it
+        game_data = &game_data[chunk_len]
+        if *game_data != '\0':  # check needed to avoid overflow at end of string
+            *game_data = '\0'
+            game_data++
+
+        trim_ascii_whitespace(chunk)
+        result.update(chunk)
+
+    return result
+
+
+def main() -> int:
+    f = fopen("sampleinput.txt", "r")
+    assert f != NULL
+
+    result = 0
+    line: byte[1000]
+    while fgets(line, sizeof(line) as int, f) != NULL:
+        id_end = strchr(line, ':')
+        assert id_end != NULL
+        game = parse_game(&id_end[1])
+        result += game.get_power()
+
+    fclose(f)
+
+    printf("%d\n", result)  # Output: 2286
+    return 0
diff --git a/examples/aoc2023/day02/sampleinput.txt b/examples/aoc2023/day02/sampleinput.txt
new file mode 100644
index 00000000..295c36dd
--- /dev/null
+++ b/examples/aoc2023/day02/sampleinput.txt
@@ -0,0 +1,5 @@
+Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
+Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
+Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
+Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
+Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
diff --git a/examples/aoc2023/day03/part1.jou b/examples/aoc2023/day03/part1.jou
new file mode 100644
index 00000000..84494615
--- /dev/null
+++ b/examples/aoc2023/day03/part1.jou
@@ -0,0 +1,69 @@
+import "stdlib/ascii.jou"
+import "stdlib/io.jou"
+import "stdlib/mem.jou"
+import "stdlib/str.jou"
+
+
+def read_file(filename: byte*) -> byte*:
+    huge = 1000*1000
+
+    result: byte* = malloc(huge)
+    assert result != NULL
+    memset(result, 0, huge)
+
+    f = fopen(filename, "r")
+    assert f != NULL
+    fread(result, 1, huge, f)
+    fclose(f)
+
+    assert result[huge-1] == '\0'  # check if full
+    return result
+
+
+# Check if there is a part number between start and end.
+# Start and end should be pointers into input.
+def is_part_number(input: byte*, start: byte*, end: byte*) -> bool:
+    if start >= end:
+        # empty range
+        return False
+
+    for p = start; p < end; p++:
+        if not is_ascii_digit(*p):
+            return False
+
+    line_size = strcspn(input, "\n") + 1
+
+    for dy = -1; dy <= 1; dy++:
+        y_offset = line_size * dy
+        check_start = &start[y_offset - 1]
+        check_end = &end[y_offset + 1]
+
+        # stay within input
+        if check_start < input:
+            check_start = input
+        if check_end > &input[strlen(input)]:
+            check_end = &input[strlen(input)]
+
+        for p = check_start; p < check_end; p++:
+            if *p != '.' and is_ascii_punctuation(*p):
+                return True
+
+    return False
+
+
+def main() -> int:
+    input = read_file("sampleinput.txt")
+    sum = 0
+
+    for start = input; *start != '\0'; start++:
+        end = start
+        while *end != '\0' and is_ascii_digit(*end):
+            end++
+
+        if is_part_number(input, start, end):
+            sum += atoi(start)
+            start = end  # skip rest of number
+
+    free(input)
+    printf("%d\n", sum)  # Output: 4361
+    return 0
diff --git a/examples/aoc2023/day03/part2.jou b/examples/aoc2023/day03/part2.jou
new file mode 100644
index 00000000..aeb8649f
--- /dev/null
+++ b/examples/aoc2023/day03/part2.jou
@@ -0,0 +1,84 @@
+import "stdlib/ascii.jou"
+import "stdlib/io.jou"
+import "stdlib/mem.jou"
+import "stdlib/str.jou"
+
+
+def read_file(filename: byte*) -> byte*:
+    huge = 1000*1000
+
+    result: byte* = malloc(huge)
+    assert result != NULL
+    memset(result, 0, huge)
+
+    f = fopen(filename, "r")
+    assert f != NULL
+    fread(result, 1, huge, f)
+    fclose(f)
+
+    assert result[huge-1] == '\0'  # check if full
+    return result
+
+
+def find_whole_number(input: byte*, a_digit: byte*, start: byte**, end: byte**) -> void:
+    *start = a_digit
+    while *start > input and is_ascii_digit((*start)[-1]):
+        --*start
+
+    *end = a_digit
+    while **end != '\0' and is_ascii_digit(**end):
+        ++*end
+
+
+def find_adjacent_numbers(input: byte*, p: byte*) -> int[10]:
+    # p should point somewhere in the input
+    assert input <= p and p < &input[strlen(input)]
+    line_size = strcspn(input, "\n") + 1
+
+    # -1 marks end of array
+    result = [-1, -1, -1, -1, -1, -1, -1, -1, -1, -1]
+    result_len = 0
+
+    # Avoid duplicates, e.g. this will see the top 123 three times:
+    #
+    #   .......
+    #   ..123..
+    #   ...*...
+    #   ..123..
+    #   .......
+    #
+    # We can't do the duplicate check with the int 123, because this counts as a gear.
+    last_start = NULL
+    last_end = NULL
+
+    for dy = -1; dy <= 1; dy++:
+        for dx = -1; dx <= 1; dx++:
+            neighbor = &p[dx + line_size*dy]
+            if input <= neighbor and neighbor < &input[strlen(input)] and is_ascii_digit(*neighbor):
+                start: byte*
+                end: byte*
+                find_whole_number(input, neighbor, &start, &end)
+                if start != last_start or end != last_end:
+                    last_start = start
+                    last_end = end
+                    result[result_len++] = atoi(start)
+
+    return result
+
+
+def main() -> int:
+    input = read_file("sampleinput.txt")
+    sum = 0
+
+    for p = input; *p != '\0'; p++:
+        if *p != '*':
+            continue
+
+        adjacent = find_adjacent_numbers(input, p)
+        if adjacent[0] != -1 and adjacent[1] != -1 and adjacent[2] == -1:
+            # it is a gear
+            sum += adjacent[0] * adjacent[1]
+
+    free(input)
+    printf("%d\n", sum)  # Output: 467835
+    return 0
diff --git a/examples/aoc2023/day03/sampleinput.txt b/examples/aoc2023/day03/sampleinput.txt
new file mode 100644
index 00000000..b20187f3
--- /dev/null
+++ b/examples/aoc2023/day03/sampleinput.txt
@@ -0,0 +1,10 @@
+467..114..
+...*......
+..35..633.
+......#...
+617*......
+.....+.58.
+..592.....
+......755.
+...$.*....
+.664.598..
diff --git a/runtests.sh b/runtests.sh
index 13f7c202..5dc94585 100755
--- a/runtests.sh
+++ b/runtests.sh
@@ -13,7 +13,8 @@ set -e -o pipefail
 
 function usage() {
     echo "Usage: $0 [--valgrind] [--verbose] [--dont-run-make] [TEMPLATE]" >&2
-    echo "TEMPLATE can be e.g. './jou %s', where %s will be replaced by a jou file." >&2
+    echo "TEMPLATE can be e.g. 'jou %s', where %s will be replaced by a jou file." >&2
+    echo "When the command runs, 'jou' points at the executable in repository root."
     exit 2
 }
 
@@ -33,9 +34,9 @@ done
 if [ $# == 0 ]; then
     # No arguments --> run tests in the basic/simple way
     if [[ "$OS" =~ Windows ]]; then
-        command_template='./jou.exe %s'
+        command_template='jou.exe %s'
     else
-        command_template='./jou %s'
+        command_template='jou %s'
     fi
 elif [ $# == 1 ]; then
     command_template="$1"
@@ -139,8 +140,16 @@ function run_test()
     local correct_exit_code="$2"
     local counter="$3"
 
-    local command diffpath
-    command="$(printf "$command_template" $joufile)"
+    local command
+    if [[ "$joufile" =~ ^examples/aoc ]]; then
+        # AoC files use fopen("sampleinput.txt", "r").
+        # We don't do this for all files, because I like relative paths in error messages.
+        command="cd $(dirname $joufile) && $(printf "$command_template" $(basename $joufile))"
+    else
+        command="$(printf "$command_template" $joufile)"
+    fi
+
+    local diffpath
     diffpath=tmp/tests/diff$(printf "%04d" $counter).txt  # consistent alphabetical order
 
     printf "\n\n\x1b[33m*** Command: %s ***\x1b[0m\n\n" "$command" > $diffpath
@@ -159,11 +168,13 @@ function run_test()
     fi
 
     show_run $joufile
-    if diff --text -u --color=always \
-        <(generate_expected_output $joufile $correct_exit_code | tr -d '\r') \
-        <(ulimit -v 500000 2>/dev/null; bash -c "$command; echo Exit code: \$?" 2>&1 | post_process_output $joufile | tr -d '\r') \
-        &>> $diffpath
-    then
+    if diff --text -u --color=always <(
+        generate_expected_output $joufile $correct_exit_code | tr -d '\r'
+    ) <(
+        export PATH="$PWD:$PATH"
+        ulimit -v 500000 2>/dev/null
+        bash -c "$command; echo Exit code: \$?" 2>&1 | post_process_output $joufile | tr -d '\r'
+    ) &>> $diffpath; then
         show_ok $joufile
         rm -f $diffpath
     else
@@ -173,7 +184,7 @@ function run_test()
 }
 
 counter=0
-for joufile in examples/*.jou tests/*/*.jou; do
+for joufile in examples/*.jou examples/aoc2023/day*/*.jou tests/*/*.jou; do
     case $joufile in
         examples/* | tests/should_succeed/*) correct_exit_code=0; ;;
         *) correct_exit_code=1; ;;  # compiler or runtime error