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