commit 3ed3c3de1263814b6aaa0a8994b363f73cee4ce0 Author: Fredrik Robertsen Date: Fri Jan 16 09:03:49 2026 +0100 init 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