From 3ed3c3de1263814b6aaa0a8994b363f73cee4ce0 Mon Sep 17 00:00:00 2001 From: Fredrik Robertsen Date: Fri, 16 Jan 2026 09:03:49 +0100 Subject: [PATCH] init --- .envrc | 1 + .gitignore | 69 +++++++++++++++++++++++++ CMakeLists.txt | 4 ++ LICENSE | 18 +++++++ README.md | 3 ++ flake.lock | 27 ++++++++++ flake.nix | 23 +++++++++ src/driver.c | 89 ++++++++++++++++++++++++++++++++ src/table.c | 40 ++++++++++++++ testing/expected/0-go.txt | 11 ++++ testing/expected/1-spaces.txt | 9 ++++ testing/expected/2-dxdy.txt | 21 ++++++++ testing/expected/3-labels.txt | 16 ++++++ testing/expected/4-comments.txt | 14 +++++ testing/expected/5-mixed_ok.txt | 9 ++++ testing/expected/6-mixed_err.txt | 21 ++++++++ testing/test.py | 87 +++++++++++++++++++++++++++++++ testing/tests/0-go.txt | 7 +++ testing/tests/1-spaces.txt | 6 +++ testing/tests/2-dxdy.txt | 15 ++++++ testing/tests/3-labels.txt | 10 ++++ testing/tests/4-comments.txt | 11 ++++ testing/tests/5-mixed_ok.txt | 8 +++ testing/tests/6-mixed_err.txt | 10 ++++ 24 files changed, 529 insertions(+) create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 CMakeLists.txt create mode 100644 LICENSE create mode 100644 README.md create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 src/driver.c create mode 100644 src/table.c create mode 100644 testing/expected/0-go.txt create mode 100644 testing/expected/1-spaces.txt create mode 100644 testing/expected/2-dxdy.txt create mode 100644 testing/expected/3-labels.txt create mode 100644 testing/expected/4-comments.txt create mode 100644 testing/expected/5-mixed_ok.txt create mode 100644 testing/expected/6-mixed_err.txt create mode 100644 testing/test.py create mode 100644 testing/tests/0-go.txt create mode 100644 testing/tests/1-spaces.txt create mode 100644 testing/tests/2-dxdy.txt create mode 100644 testing/tests/3-labels.txt create mode 100644 testing/tests/4-comments.txt create mode 100644 testing/tests/5-mixed_ok.txt create mode 100644 testing/tests/6-mixed_err.txt diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2924bb8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,69 @@ +# ---> CMake +CMakeLists.txt.user +CMakeCache.txt +CMakeFiles +CMakeScripts +Testing +Makefile +cmake_install.cmake +install_manifest.txt +compile_commands.json +CTestTestfile.cmake +_deps +CMakeUserPresets.json + +# ---> C +# Prerequisites +*.d + +# Object files +*.o +*.ko +*.obj +*.elf + +# Linker output +*.ilk +*.map +*.exp + +# Precompiled Headers +*.gch +*.pch + +# Libraries +*.lib +*.a +*.la +*.lo + +# Shared objects (inc. Windows DLLs) +*.dll +*.so +*.so.* +*.dylib + +# Executables +*.exe +*.out +*.app +*.i*86 +*.x86_64 +*.hex + +# Debug files +*.dSYM/ +*.su +*.idb +*.pdb + +# Kernel Module Compile Results +*.mod* +*.cmd +.tmp_versions/ +modules.order +Module.symvers +Mkfile.old +dkms.conf + +build diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..dc3f711 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,4 @@ +cmake_minimum_required(VERSION 3.10) +project(PS1) + +add_executable(ps1 src/driver.c) diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4ddc039 --- /dev/null +++ b/LICENSE @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2026 frero-uni + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2761406 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# TDT4205 + +compiler construction \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..6f6bfa6 --- /dev/null +++ b/flake.lock @@ -0,0 +1,27 @@ +{ + "nodes": { + "nixpkgs": { + "locked": { + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "nixpkgs": "nixpkgs" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..300a613 --- /dev/null +++ b/flake.nix @@ -0,0 +1,23 @@ +{ + description = "DevShell with flex, bison, gdb, cmake"; + + inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + + outputs = + { self, nixpkgs }: + let + system = "x86_64-linux"; + pkgs = nixpkgs.legacyPackages.${system}; + in + { + devShells.${system}.default = pkgs.mkShell { + buildInputs = with pkgs; [ + flex + bison + gdb + cmake + libgcc + ]; + }; + }; +} diff --git a/src/driver.c b/src/driver.c new file mode 100644 index 0000000..e8a9aa0 --- /dev/null +++ b/src/driver.c @@ -0,0 +1,89 @@ +#include +#include +#include +#include +#include + +#include "table.c" + +static_assert(START >= 0 && START < NSTATES, "START must be a valid state"); +static_assert(ACCEPT >= 0 && ACCEPT < NSTATES, "ACCEPT must be a valid state"); +static_assert(ERROR >= 0 && ERROR < NSTATES, "ERROR must be a valid state"); + +// This program takes in lines from stdin and validates them line by line. +// If a line is valid, it is ignored. +// Otherwise, it is printed in full, +// followed by a line indicating at which character the error occurs. +int main() { + + // Fill the table first. This function does no input processing only table creation. + fillTable(); + printf("Table filled!\n"); + + // Lines may not be more than 512 lines long + char line[512]; + int lineNum = 0; + + // For each line in input + while (fgets(line, sizeof(line), stdin)) { + lineNum++; + + int state = START; + int pos = 0; + bool fatalError = false; + while (true) { + unsigned char c = line[pos]; + + // If the line ended without a newline, patch it in + if (c == '\0') + c = '\n'; + + // Use the symbol to go to the next state + state = table[state][c]; + + // Uncomment this for debugging: + /* printf("State %i got c=%c\n", state, c); */ + + if (state < 0 || state >= NSTATES) { + printf("fatal error: DFS entered an invalid state: %d\n", state); + fatalError = true; + } + else if (state == ACCEPT) { + // Make sure statement is accepted at new line + if (c == '\n') { + printf("line %4d: accepted: %.*s\n", lineNum, pos, line); + + // Break inner loop + break; + } + + printf("fatal error: ACCEPT state reached before end of line:\n"); + fatalError = true; + } + else if (c == '\n' && state != ERROR) { + printf("fatal error: Line ended without reaching either ACCEPT or ERROR state: %d\n", state); + fatalError = true; + } + + // Provide the position we were at in the string when the DFS reached the ERROR state, + // or when a fatal error occured. The latter can only happen if the DFS is broken. + if (state == ERROR || fatalError) { + printf("line %4d: error: %s", lineNum, line); + printf("~~~~~~~~~~~~~~~~~~"); + for(int i = 0; i < pos; i++) + putc('~', stdout); + printf("^\n"); + break; + } + + pos++; + } + + // Fatal errors means there is something wrong with the DFS table. Stop processing lines! + if (fatalError) { + return 1; // return code 1 means error + } + } + + return 0; +} diff --git a/src/table.c b/src/table.c new file mode 100644 index 0000000..f8aafd7 --- /dev/null +++ b/src/table.c @@ -0,0 +1,40 @@ +// The number of states in your table +#define NSTATES 14 + +// The starting state, at the beginning of each line +#define START 0 + +// The state to go to after a valid line +// All lines end with the newline character '\n' +#define ACCEPT 12 + +// The state to jump to as soon as a line is invalid +#define ERROR 13 + +int table[NSTATES][256]; + +void fillTable() { + + // Make all states lead to ERROR by default + for (int i = 0; i < NSTATES; i++) { + for (int c = 0; c < 256; c++) { + table[i][c] = ERROR; + } + } + + // Skip whitespace + table[START][' '] = START; + + // If we reach a newline, and are not in the middle of a statement, accept + table[START]['\n'] = ACCEPT; + + // Accept the statement "go" + table[START]['g'] = 1; + table[1]['o'] = 2; + table[2]['\n'] = ACCEPT; + + + // TODO Expand the table to pass (and fail) the described syntax + // table[...][...] = ... + +} diff --git a/testing/expected/0-go.txt b/testing/expected/0-go.txt new file mode 100644 index 0000000..f126674 --- /dev/null +++ b/testing/expected/0-go.txt @@ -0,0 +1,11 @@ +Table filled! +line 1: accepted: go +line 2: error: apple +~~~~~~~~~~~~~~~~~~^ +line 3: accepted: go +line 4: accepted: +line 5: error: banana +~~~~~~~~~~~~~~~~~~^ +line 6: accepted: go +line 7: error: orange +~~~~~~~~~~~~~~~~~~^ diff --git a/testing/expected/1-spaces.txt b/testing/expected/1-spaces.txt new file mode 100644 index 0000000..171b394 --- /dev/null +++ b/testing/expected/1-spaces.txt @@ -0,0 +1,9 @@ +Table filled! +line 1: accepted: go +line 2: accepted: +line 3: accepted: go go +line 4: error: go gogo +~~~~~~~~~~~~~~~~~~~~~~~^ +line 5: accepted: go go go go go go +line 6: error: gogo gogo +~~~~~~~~~~~~~~~~~~~~^ diff --git a/testing/expected/2-dxdy.txt b/testing/expected/2-dxdy.txt new file mode 100644 index 0000000..81eb914 --- /dev/null +++ b/testing/expected/2-dxdy.txt @@ -0,0 +1,21 @@ +Table filled! +line 1: accepted: dx=2 +line 2: accepted: dy=3 +line 3: accepted: dx=9 +line 4: accepted: dx=55 +line 5: accepted: dy=09 +line 6: accepted: dx=5 dx=1 go +line 7: error: da=1 +~~~~~~~~~~~~~~~~~~~^ +line 8: error: indy=1 +~~~~~~~~~~~~~~~~~~^ +line 9: accepted: dx=999 +line 10: accepted: dy=-10 +line 11: error: dx=--5 +~~~~~~~~~~~~~~~~~~~~~~^ +line 12: error: dx=- +~~~~~~~~~~~~~~~~~~~~~~^ +line 13: error: dy= +~~~~~~~~~~~~~~~~~~~~~^ +line 14: accepted: dx=-0 dy=55 go dx=197 dy=-2 +line 15: accepted: dx=20 dy=20 diff --git a/testing/expected/3-labels.txt b/testing/expected/3-labels.txt new file mode 100644 index 0000000..5a78c82 --- /dev/null +++ b/testing/expected/3-labels.txt @@ -0,0 +1,16 @@ +Table filled! +line 1: accepted: 8: go +line 2: accepted: 10: dx=1 +line 3: accepted: 5:go dx=2 +line 4: accepted: 2: +line 5: error: -11: go go +~~~~~~~~~~~~~~~~~~^ +line 6: accepted: 100: go dx=5 +line 7: error: apple: go +~~~~~~~~~~~~~~~~~~^ +line 8: error: go 4: +~~~~~~~~~~~~~~~~~~~~~^ +line 9: error: 22: 22: go +~~~~~~~~~~~~~~~~~~~~~~^ +line 10: error: : go +~~~~~~~~~~~~~~~~~~^ diff --git a/testing/expected/4-comments.txt b/testing/expected/4-comments.txt new file mode 100644 index 0000000..a1c6d9d --- /dev/null +++ b/testing/expected/4-comments.txt @@ -0,0 +1,14 @@ +Table filled! +line 1: accepted: // This file contains tests +line 2: accepted: //Spacesbeforecommentshouldnotmatter +line 3: accepted: go go go // This should be accepted +line 4: accepted: 10: dx=10 dy=-5 go // This should also be accepted +line 5: error: dx=-1 dy=5 go /* This should fail! */ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ +line 6: accepted: go go // dx=-4.2 should be accepted +line 7: accepted: // +line 8: accepted: go// Comments can be right next to statements +line 9: accepted: dx=5// Here as well +line 10: accepted: 7://Labels too! +line 11: error: go / We need two slashes +~~~~~~~~~~~~~~~~~~~~~~^ diff --git a/testing/expected/5-mixed_ok.txt b/testing/expected/5-mixed_ok.txt new file mode 100644 index 0000000..3b42794 --- /dev/null +++ b/testing/expected/5-mixed_ok.txt @@ -0,0 +1,9 @@ +Table filled! +line 1: accepted: go go dx=1 go +line 2: accepted: go// OK +line 3: accepted: 9: dx=-1 // +line 4: accepted: 10: // +line 5: accepted: 11:// +line 6: accepted: dy=-9 // Not dy=--9 +line 7: accepted: 00: dx=-0001 go +line 8: accepted: 94124543535: dx=94124543535 go dy=-111 go // End diff --git a/testing/expected/6-mixed_err.txt b/testing/expected/6-mixed_err.txt new file mode 100644 index 0000000..faee000 --- /dev/null +++ b/testing/expected/6-mixed_err.txt @@ -0,0 +1,21 @@ +Table filled! +line 1: error: dx= // Comment +~~~~~~~~~~~~~~~~~~~~~^ +line 2: error: go go goooo +~~~~~~~~~~~~~~~~~~~~~~~~~~^ +line 3: error: dy=11dx=11 +~~~~~~~~~~~~~~~~~~~~~~~^ +line 4: error: -5: go +~~~~~~~~~~~~~~~~~~^ +line 5: error: : // Empty label? +~~~~~~~~~~~~~~~~~~^ +line 6: error: 5:4: +~~~~~~~~~~~~~~~~~~~~^ +line 7: error: go / Comment +~~~~~~~~~~~~~~~~~~~~~~^ +line 8: error: 8: dy=42 dx=4.2 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^ +line 9: error: I beheld the wretch—the miserable monster whom I had created +~~~~~~~~~~~~~~~~~~^ +line 10: error: apple pie +~~~~~~~~~~~~~~~~~~^ diff --git a/testing/test.py b/testing/test.py new file mode 100644 index 0000000..1225e78 --- /dev/null +++ b/testing/test.py @@ -0,0 +1,87 @@ +import difflib +import pathlib +import os +import subprocess +import sys + + +def main(): + ps1_root_dir = pathlib.Path(__file__).parent.parent.resolve() + + build_dir = ps1_root_dir / "build" + + print("Building and compiling...") + try: + subprocess.run(["cmake", str(ps1_root_dir), "-B", str(build_dir)], check=True) + subprocess.run(["cmake", "--build", str(build_dir)], check=True) + except subprocess.CalledProcessError as e: + print(f"An error occurred while running CMake: {e}", file=sys.stderr) + sys.exit(1) + print("Build complete") + + testing_dir = ps1_root_dir / "testing" + tests_dir = testing_dir / "tests" + outputs_dir = testing_dir / "expected" + + executable = build_dir / "ps1" + + sorted_test_files : list[pathlib.Path] = [] + + for test_file in tests_dir.glob("*.txt"): + sorted_test_files.append(test_file) + + sorted_test_files.sort(key= lambda p : p.name) + + passed = 0 + for test_file in sorted_test_files: + name = test_file.name + print(f"Testing {name}... ", end="") + output_file = outputs_dir / name + + if not output_file.exists(): + print("") + print(f"Error: Could not find {str(output_file)}", file=sys.stderr) + sys.exit(1) + + try: + result : list[str] = [] + with test_file.open("r") as test_input: + result_str : str = subprocess.run( + [executable], + stdin=test_input, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, # Output must be text + check=True + ).stdout + + result = result_str.splitlines() + + expected : list[str] = [] + with output_file.open("r") as expected_output: + expected = expected_output.read().splitlines() + + diff_list : list[str] = [] + for diff in difflib.unified_diff(result, expected, fromfile='your output', tofile='expected_output', lineterm='', n=3): + diff_list.append(diff) + + if not diff_list: + # List is empty; no diffs + passed += 1 + print("OK") + continue + + # If at least one diff, print + print("MISMATCH") + for diff in diff_list: + print(diff) + + except subprocess.CalledProcessError as e: + print("") + print(f"Error: Test {name} failed to execute: {e}", file=sys.stderr) + + print(f"{passed}/{len(sorted_test_files)} tests passed") + + +if __name__ == "__main__": + main() diff --git a/testing/tests/0-go.txt b/testing/tests/0-go.txt new file mode 100644 index 0000000..0ad9518 --- /dev/null +++ b/testing/tests/0-go.txt @@ -0,0 +1,7 @@ +go +apple +go + +banana +go +orange diff --git a/testing/tests/1-spaces.txt b/testing/tests/1-spaces.txt new file mode 100644 index 0000000..4df7ce3 --- /dev/null +++ b/testing/tests/1-spaces.txt @@ -0,0 +1,6 @@ +go + +go go +go gogo + go go go go go go +gogo gogo diff --git a/testing/tests/2-dxdy.txt b/testing/tests/2-dxdy.txt new file mode 100644 index 0000000..653d96b --- /dev/null +++ b/testing/tests/2-dxdy.txt @@ -0,0 +1,15 @@ +dx=2 +dy=3 +dx=9 +dx=55 +dy=09 +dx=5 dx=1 go +da=1 +indy=1 +dx=999 +dy=-10 +dx=--5 +dx=- +dy= +dx=-0 dy=55 go dx=197 dy=-2 +dx=20 dy=20 diff --git a/testing/tests/3-labels.txt b/testing/tests/3-labels.txt new file mode 100644 index 0000000..8d781b4 --- /dev/null +++ b/testing/tests/3-labels.txt @@ -0,0 +1,10 @@ +8: go + 10: dx=1 +5:go dx=2 +2: +-11: go go +100: go dx=5 +apple: go +go 4: +22: 22: go +: go diff --git a/testing/tests/4-comments.txt b/testing/tests/4-comments.txt new file mode 100644 index 0000000..ee85555 --- /dev/null +++ b/testing/tests/4-comments.txt @@ -0,0 +1,11 @@ +// This file contains tests + //Spacesbeforecommentshouldnotmatter +go go go // This should be accepted +10: dx=10 dy=-5 go // This should also be accepted +dx=-1 dy=5 go /* This should fail! */ +go go // dx=-4.2 should be accepted +// +go// Comments can be right next to statements +dx=5// Here as well +7://Labels too! +go / We need two slashes diff --git a/testing/tests/5-mixed_ok.txt b/testing/tests/5-mixed_ok.txt new file mode 100644 index 0000000..d874dcf --- /dev/null +++ b/testing/tests/5-mixed_ok.txt @@ -0,0 +1,8 @@ +go go dx=1 go + go// OK +9: dx=-1 // +10: // + 11:// +dy=-9 // Not dy=--9 +00: dx=-0001 go +94124543535: dx=94124543535 go dy=-111 go // End diff --git a/testing/tests/6-mixed_err.txt b/testing/tests/6-mixed_err.txt new file mode 100644 index 0000000..cf9f3f8 --- /dev/null +++ b/testing/tests/6-mixed_err.txt @@ -0,0 +1,10 @@ +dx= // Comment +go go goooo +dy=11dx=11 +-5: go +: // Empty label? +5:4: +go / Comment +8: dy=42 dx=4.2 +I beheld the wretch—the miserable monster whom I had created +apple pie