diff --git a/include/Util.h b/include/Util.h index fed441b..2e3d58d 100644 --- a/include/Util.h +++ b/include/Util.h @@ -106,4 +106,38 @@ template struct is_comparable { template constexpr bool is_comparable_v = is_comparable::value; +// trim from start (in place) +inline void ltrim(std::string& s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) { return !std::isspace(ch); })); +} + +// trim from end (in place) +inline void rtrim(std::string& s) { + s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) { return !std::isspace(ch); }).base(), s.end()); +} + +// trim from both ends (in place) +inline void trim(std::string& s) { + rtrim(s); + ltrim(s); +} + +// trim from start (copying) +inline std::string ltrim_copy(std::string s) { + ltrim(s); + return s; +} + +// trim from end (copying) +inline std::string rtrim_copy(std::string s) { + rtrim(s); + return s; +} + +// trim from both ends (copying) +inline std::string trim_copy(std::string s) { + trim(s); + return s; +} + #endif diff --git a/include/VirtualMachine.h b/include/VirtualMachine.h index db3bd73..45f33af 100644 --- a/include/VirtualMachine.h +++ b/include/VirtualMachine.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,10 @@ struct FunctionEntry { : _name(std::move(name)), _argument_count(arg_count), _address(adr) { } + FunctionEntry(std::string name, uint8_t arg_count, const std::string& label) + : _name(std::move(name)), _argument_count(arg_count), _label(label) { + } + auto name() const -> const std::string& { return _name; } @@ -28,11 +33,18 @@ struct FunctionEntry { auto address() const -> std::size_t { return _address; } + auto label() const -> const std::string& { + return _label; + } + void address(std::size_t adr) { + _address = adr; + } private: std::string _name; uint8_t _argument_count; std::size_t _address; + std::string _label; }; template using NativeFunction = std::function(VM* vm, const std::vector&)>; diff --git a/src/pasm/pasm.cpp b/src/pasm/pasm.cpp index aaccf8c..7521ed6 100644 --- a/src/pasm/pasm.cpp +++ b/src/pasm/pasm.cpp @@ -1,5 +1,214 @@ #include "VirtualMachine.h" +#include "Util.h" +#include +#include +#include +#include +#include + +template void print(std::format_string fmt, Args&&... args) { + std::print(std::cout, fmt, std::forward(args)...); +} + +struct PasmOption { + std::string output_file; +}; + +void fatal() { + print("pasm: fatal: no input file specified\n Type pasm -h for help.\n\n"); +} + +void usage() { + print("Usage: pasm [options...] filename\n"); + print(" pasm -v\n"); + print("Options (values in brackets indicate defaults):\n\n"); + print(" -h show this text and exit\n"); + print(" -v print the pasm version number and exit\n"); + print(" -o outfile write output to outfile\n"); +} + +void version() { + print("pasm version 0.0.1 compiled on ...\n"); +} + +void print_usage_and_exit_if(bool condition) { + if (condition) { + usage(); + std::exit(0); + } +} + +auto options(const std::vector& args) -> PasmOption { + PasmOption opt; + for (std::size_t i = 1; i < args.size(); ++i) { + if (args[i] == "-v") { + version(); + std::exit(0); + } + if (args[i] == "-h") { + print_usage_and_exit_if(true); + } + if (args[i] == "-o") { + print_usage_and_exit_if(i + 1 >= args.size()); + opt.output_file = args[i + 1]; + i++; + } + } + return opt; +} + +void inc_index_if_whitespace(std::size_t& i, const std::string& str) { + if (i >= str.length()) { + return; + } + if (str[i] == ' ' || str[i] == '\t' || str[i] == '\n') { + for (; i < str.length(); ++i) { + if (str[i] != ' ' && str[i] != '\t' && str[i] != '\n') { + break; + } + } + } +} + +void store_value_until_whitespace(std::size_t& i, std::string& value, const std::string& str) { + for (; i < str.length(); ++i) { + if (str[i] == ' ' || str[i] == '\t' || str[i] == '\n') { + break; + } + value += str[i]; + } +} + +void extract_value(std::string& value, const std::string& line) { + std::size_t i = 0; + inc_index_if_whitespace(i, line); + store_value_until_whitespace(i, value, line); +} + +void parse_function_entry_item(std::size_t& i, std::string& value, const std::string& entry, std::size_t line_nr) { + inc_index_if_whitespace(i, entry); + store_value_until_whitespace(i, value, entry); + if (value.length() == 0) { + std::string trimmed_entry = trim_copy(entry); + print("pasm: Missing function entry item in line {}! Content is {}\n", line_nr, trimmed_entry); + exit(1); + } +} + +auto make_function_entry(const std::string& entry, std::size_t line_nr) -> FunctionEntry { + std::string fname; + std::string argC; + std::string label; + std::size_t i = 0; + + parse_function_entry_item(i, fname, entry, line_nr); + parse_function_entry_item(i, argC, entry, line_nr); + parse_function_entry_item(i, label, entry, line_nr); + inc_index_if_whitespace(i, entry); + if (i < entry.length()) { + print("pasm: parsing error in function section line {}! valid format is: [function name] [argument] [count label] " + "not {}\n", + line_nr, entry); + std::exit(1); + } + bool is_zero = false; + if (argC.length() == 1 && argC[0] == '0') { + is_zero = true; + } + uint8_t arg_count = std::atoi(argC.c_str()); + + if (arg_count == 0 && !is_zero) { + print("pasm: parsing error in function section in line {}! argument count is not a number: {}\n", line_nr, argC); + std::exit(1); + } + + return FunctionEntry(fname, arg_count, label); +} + +auto parse_function_entries(std::ifstream& stream, std::size_t& line_nr) -> std::vector { + std::string entry; + std::vector f_entries; + bool valid_block = false; + while (!std::getline(stream, entry).eof()) { + line_nr++; + std::string first_item; + extract_value(first_item, entry); + if (first_item.length() > 0 && first_item[0] == '#') { + // comment + continue; + } + if (first_item == "$end") { + valid_block = true; + break; + } + FunctionEntry e = make_function_entry(entry, line_nr); + f_entries.push_back(e); + } + if (!valid_block) { + print("pasm: parsing error in function section! function section not closed with $end\n"); + std::exit(1); + } + return f_entries; +} + +void check_is_stream_open(std::ifstream& stream, const std::string& file) { + if (!stream.is_open()) { + if (!std::filesystem::is_regular_file(file)) { + print("pasm: fatal: unable to open file `{}' File path doesn't refer to a regular file\n", file); + } else { + print("pasm: fatal: unable to open file `{}' Possible cause:\n", file); + print(" - No read access to the file.\n"); + print(" - The file is locked by another process.\n"); + print(" - A file system error occured.\n"); + print(" - Unknown error.\n"); + } + std::exit(1); + } +} + +void run(const PasmOption& opt, const std::string& file) { + std::ifstream stream(file, std::ios::binary); + check_is_stream_open(stream, file); + std::string line; + std::size_t line_nr = 0; + std::vector function_entries; + while (!std::getline(stream, line).eof()) { + line_nr++; + std::string header; + extract_value(header, line); + if (header.length() > 0 && header[0] == '#') { + continue; + } + if (header == "$function_section") { + if (function_entries.size() == 0) { + function_entries = parse_function_entries(stream, line_nr); + } else { + print("pasm: fatal: multiple function_section found in line {}!", line_nr); + std::exit(0); + } + } + if (header == "$code_section") { + // parse_code_section(stream, line_nr); + } + } +} auto main(int argc, char** argv) -> int { + if (argc < 2) { + fatal(); + return 1; + } + std::vector args(argv, argv + argc); + PasmOption opt; + if (argv[1][0] == '-') { + opt = options(args); + } + std::string input_file = *(args.end() - 1); + if (!std::filesystem::exists(input_file)) { + print("pasm: fatal: unable to open input file `{}' No such file or directory\n", input_file); + std::exit(1); + } + + run(opt, input_file); return 0; } diff --git a/tests/pasm_tests/syntax_failed1.pasm b/tests/pasm_tests/syntax_failed1.pasm new file mode 100644 index 0000000..d9ac0d0 --- /dev/null +++ b/tests/pasm_tests/syntax_failed1.pasm @@ -0,0 +1,4 @@ +$function_section + 2 foo_label +$end +# RESULT:pasm: Missing function entry item in line 2! Content is 2 foo_label diff --git a/tests/pasm_tests/syntax_failed2.pasm b/tests/pasm_tests/syntax_failed2.pasm new file mode 100644 index 0000000..eb461c4 --- /dev/null +++ b/tests/pasm_tests/syntax_failed2.pasm @@ -0,0 +1,3 @@ +$function_section + foo 2 foo_label +#RESULT:pasm: parsing error in function section! function section not closed with $end diff --git a/tests/pasm_tests/syntax_failed3.pasm b/tests/pasm_tests/syntax_failed3.pasm new file mode 100644 index 0000000..047ddef --- /dev/null +++ b/tests/pasm_tests/syntax_failed3.pasm @@ -0,0 +1,4 @@ +$function_section + foo a foo_label +$end +#RESULT:pasm: parsing error in function section in line 2! argument count is not a number: a diff --git a/tests/pasm_tests/syntax_test.pasm b/tests/pasm_tests/syntax_test.pasm new file mode 100644 index 0000000..42427ff --- /dev/null +++ b/tests/pasm_tests/syntax_test.pasm @@ -0,0 +1,4 @@ +$function_section + foo 2 foo_label +$end +# RESULT: diff --git a/toolchain/executable_tester.py b/toolchain/executable_tester.py new file mode 100644 index 0000000..3e9d952 --- /dev/null +++ b/toolchain/executable_tester.py @@ -0,0 +1,77 @@ +import subprocess +import os +import re + +# Pfade anpassen +PASM_EXECUTABLE = "./pasm" # Pfad zur ausführbaren Datei +TEST_DIR = "./tests/pasm_tests" # Ordner mit den Testdateien + +def run_test(input_file): + # Input-Datei lesen + with open(input_file, "r") as file: + lines = file.readlines() + + # Erwartetes Ergebnis auslesen (z. B. # RESULT: [...]) + expected_result = None + for line in lines: + match = re.match(r"#[ ]*[Rr][Ee][Ss][Uu][Ll][Tt]:\s*(.*)", line) + if match: + expected_result = match.group(1).strip() + break + + if expected_result is None: + print(f"❌ Expected result not definied in {input_file}!") + return False + + # `pasm` mit der Input-Datei ausführen + try: + result = subprocess.run( + [PASM_EXECUTABLE, input_file], + input="".join(lines), # Input übergeben + text=True, # Textausgabe aktivieren + capture_output=True # Ausgabe abfangen + ) + except FileNotFoundError: + print(f"❌ Executable '{PASM_EXECUTABLE}' not found!") + return False + + # Ausgabe überprüfen + actual_result = result.stdout.strip() + if len(expected_result) > 0 and actual_result == expected_result: + print(f"✅ Test {os.path.basename(input_file)} passed!") + return True + else: + if len(expected_result)==0: + if result.returncode == 0: + print(f"✅ Test {os.path.basename(input_file)} passed!") + return True + print(f"❌ Test {os.path.basename(input_file)} failed!") + print(f" Expected: {expected_result}") + print(f" Result: {actual_result}") + return False + +def main(): + # Alle `.input`-Dateien im Testverzeichnis finden + input_files = [f for f in os.listdir(TEST_DIR) if f.endswith(".pasm")] + + if not input_files: + print("❌ No input file found!") + return + + # Tests ausführen + passed = 0 + failed = 0 + for input_file in input_files: + if run_test(os.path.join(TEST_DIR, input_file)): + passed += 1 + else: + failed += 1 + + # Zusammenfassung + print("\n--- Test conclusion ---") + print(f"Passed: {passed}") + print(f"Failed: {failed}") + print(f"Sum: {len(input_files)}") + +if __name__ == "__main__": + main()