diff --git a/.github/workflows/build-check-sdk.yaml b/.github/workflows/build-check-sdk.yaml index 10a4811..b17b734 100644 --- a/.github/workflows/build-check-sdk.yaml +++ b/.github/workflows/build-check-sdk.yaml @@ -2,7 +2,7 @@ name: "Build check gemstone in SDK" run-name: SDK build check to ${{ inputs.deploy_target }} by @${{ github.actor }} on: [push, pull_request] env: - SDK: 0.2.2-alpine-3.19.1 + SDK: 0.2.3-alpine-3.19.1 jobs: build-check-sdk: runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index 824b3d4..12e8c66 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,9 @@ project(gemstone DESCRIPTION "programming language compiler" LANGUAGES C) +set(CMAKE_C_STANDARD 23) +set(CMAKE_C_STANDARD_REQUIRED TRUE) + set(GEMSTONE_TEST_DIR ${PROJECT_SOURCE_DIR}/tests) set(GEMSTONE_BINARY_DIR ${PROJECT_SOURCE_DIR}/bin) diff --git a/Dockerfile b/Dockerfile index 09e34b6..a8c2f7f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ -FROM servostar/gemstone:sdk-0.2.2-alpine-3.19.1 +FROM servostar/gemstone:sdk-0.2.3-alpine-3.19.1 LABEL authors="servostar" -LABEL version="0.2.2" +LABEL version="0.2.3" LABEL description="docker image for setting up the build pipeline on SDK" LABEL website="https://github.com/Servostar/gemstone" diff --git a/sdk/Dockerfile b/sdk/Dockerfile index 1a6a677..aae7438 100644 --- a/sdk/Dockerfile +++ b/sdk/Dockerfile @@ -1,11 +1,11 @@ FROM alpine:3.19.1 LABEL authors="servostar" -LABEL version="0.2.2" +LABEL version="0.2.3" LABEL description="base image for building the gemstone programming language compiler" LABEL website="https://github.com/Servostar/gemstone" # install dependencies -RUN apk add build-base gcc make cmake bison flex git python3 +RUN apk add build-base gcc make cmake bison flex git python3 graphviz # create user for build RUN adduser --disabled-password lorang diff --git a/src/ast/ast.c b/src/ast/ast.c new file mode 100644 index 0000000..233e8da --- /dev/null +++ b/src/ast/ast.c @@ -0,0 +1,274 @@ + +#include +#include +#include +#include +#include + +struct AST_Node_t *AST_new_node(enum AST_SyntaxElement_t kind, const char* value) { + DEBUG("creating new AST node: %d \"%s\"", kind, value); + assert(kind < AST_ELEMENT_COUNT); + + struct AST_Node_t *node = malloc(sizeof(struct AST_Node_t)); + + if (node == NULL) { + PANIC("failed to allocate AST node"); + } + + assert(node != NULL); + + // init to discrete state + node->parent = NULL; + node->children = NULL; + node->child_count = 0; + node->kind = kind; + node->value = value; + + return node; +} + +static const char* lookup_table[AST_ELEMENT_COUNT] = { "__UNINIT__" }; + +void AST_init() { + DEBUG("initializing global syntax tree..."); + + INFO("filling lookup table..."); + + lookup_table[AST_Stmt] = "stmt"; + lookup_table[AST_Expr] = "expr"; + + lookup_table[AST_Add] = "+"; + lookup_table[AST_Sub] = "-"; + lookup_table[AST_Mul] = "*"; + lookup_table[AST_Div] = "/"; + + lookup_table[AST_BitAnd] = "&"; + lookup_table[AST_BitOr] = "|"; + lookup_table[AST_BitXor] = "^"; + lookup_table[AST_BitNot] = "!"; + + lookup_table[AST_Eq] = "=="; + lookup_table[AST_Less] = "<"; + lookup_table[AST_Greater] = ">"; + + lookup_table[AST_BoolAnd] = "&&"; + lookup_table[AST_BoolOr] = "||"; + lookup_table[AST_BoolXor] = "^^"; + lookup_table[AST_BoolNot] = "!!"; + + lookup_table[AST_While] = "while"; + lookup_table[AST_If] = "if"; + lookup_table[AST_IfElse] = "else if"; + lookup_table[AST_Else] = "else"; + + lookup_table[AST_Decl] = "decl"; + lookup_table[AST_Assign] = "assign"; + lookup_table[AST_Def] = "def"; + + lookup_table[AST_Typedef] = "typedef"; + lookup_table[AST_Box] = "box"; + lookup_table[AST_Fun] = "fun"; + + lookup_table[AST_Typecast] = "cast"; + lookup_table[AST_Transmute] = "as"; + lookup_table[AST_Condition] = "condition"; +} + +const char* AST_node_to_string(const struct AST_Node_t* node) { + DEBUG("converting AST node to string: %p", node); + assert(node != NULL); + + const char* string; + + switch(node->kind) { + case AST_Int: + case AST_Float: + case AST_String: + case AST_Ident: + case AST_Macro: + case AST_Import: + case AST_Call: + string = node->value; + break; + default: + string = lookup_table[node->kind]; + } + + assert(string != NULL); + + return string; +} + +void AST_push_node(struct AST_Node_t *owner, struct AST_Node_t *child) { + DEBUG("Adding new node %p to %p", child, owner); + assert(owner != NULL); + assert(child != NULL); + + // if there are no children for now + if (owner->child_count == 0) { + DEBUG("Allocating new children array"); + owner->children = malloc(sizeof(struct AST_Node_t *)); + + } else { + DEBUG("Rellocating old children array"); + const size_t size = sizeof(struct AST_Node_t *) * (owner->child_count + 1); + owner->children = realloc(owner->children, size); + } + + if (owner->children == NULL) { + PANIC("failed to allocate children array of AST node"); + } + + assert(owner->children != NULL); + + owner->children[owner->child_count++] = child; +} + +struct AST_Node_t *AST_get_node(struct AST_Node_t *owner, const size_t idx) { + DEBUG("retrvieng node %d from %p", idx, owner); + assert(owner != NULL); + assert(owner->children != NULL); + assert(idx < owner->child_count); + + if (owner->children == NULL) { + PANIC("AST owner node has no children"); + } + + struct AST_Node_t *child = owner->children[idx]; + + if (child == NULL) { + PANIC("child node is NULL"); + } + + return child; +} + +struct AST_Node_t* AST_remove_child(struct AST_Node_t* owner, const size_t idx) { + assert(owner != NULL); + assert(owner->children != NULL); + assert(idx < owner->child_count); + + struct AST_Node_t* child = owner->children[idx]; + + child->parent = NULL; + + owner->child_count--; + + // shift back every following element by one + for (size_t i = idx; i < owner->child_count; i++) { + owner->children[i] = owner->children[i + 1]; + } + + return child; +} + +struct AST_Node_t* AST_detach_child(struct AST_Node_t* owner, const struct AST_Node_t* child) { + assert(owner != NULL); + assert(child != NULL); + assert(owner->children != NULL); + + for (size_t i = 0; i < owner->child_count; i++) { + if (owner->children[i] == child) { + return AST_remove_child(owner, i); + } + } + + PANIC("Child to detach not a child of parent"); +} + +void AST_delete_node(struct AST_Node_t *node) { + assert(node != NULL); + + DEBUG("Deleting AST node: %p", node); + + if (node->children == NULL) { + return; + } + + if (node->parent != NULL) { + const struct AST_Node_t* child = AST_detach_child(node->parent, node); + assert(child == node); + } + + for (size_t i = 0; i < node->child_count; i++) { + // prevent detach of children node + node->children[i]->parent = NULL; + AST_delete_node(node->children[i]); + } + + free(node->children); + free(node); +} + +static void AST_visit_nodes_recurse2(struct AST_Node_t *root, + void (*for_each)(struct AST_Node_t *node, + size_t depth), + const size_t depth) { + DEBUG("Recursive visit of %p at %d with %p", root, depth, for_each); + + assert(root != NULL); + + (for_each)(root, depth); + + for (size_t i = 0; i < root->child_count; i++) { + AST_visit_nodes_recurse2(root->children[i], for_each, depth + 1); + } +} + +void AST_visit_nodes_recurse(struct AST_Node_t *root, + void (*for_each)(struct AST_Node_t *node, + size_t depth)) { + DEBUG("Starting recursive visit of %p with %p", root, for_each); + + assert(root != NULL); + assert(for_each != NULL); + + AST_visit_nodes_recurse2(root, for_each, 0); +} + +static void AST_fprint_graphviz_node_definition(FILE* stream, const struct AST_Node_t* node) { + DEBUG("Printing graphviz definition of %p", node); + + assert(stream != NULL); + assert(node != NULL); + + fprintf(stream, "\tnode%p [label=\"%s\"]\n", (void*) node, AST_node_to_string(node)); + + if (node->children == NULL) { + return; + } + + for (size_t i = 0; i < node->child_count; i++) { + AST_fprint_graphviz_node_definition(stream, node->children[i]); + } +} + +static void AST_fprint_graphviz_node_connection(FILE* stream, const struct AST_Node_t* node) { + DEBUG("Printing graphviz connection of %p", node); + + assert(stream != NULL); + assert(node != NULL); + + if (node->children == NULL) { + return; + } + + for (size_t i = 0; i < node->child_count; i++) { + fprintf(stream, "\tnode%p -- node%p\n", (void*) node, (void*) node->children[i]); + AST_fprint_graphviz_node_connection(stream, node->children[i]); + } +} + +void AST_fprint_graphviz(FILE* stream, const struct AST_Node_t* root) { + DEBUG("Starting print of graphviz graph of %p", root); + + assert(stream != NULL); + assert(root != NULL); + + fprintf(stream, "graph {\n"); + + AST_fprint_graphviz_node_definition(stream, root); + AST_fprint_graphviz_node_connection(stream, root); + + fprintf(stream, "}\n"); +} diff --git a/src/ast/ast.h b/src/ast/ast.h new file mode 100644 index 0000000..5129765 --- /dev/null +++ b/src/ast/ast.h @@ -0,0 +1,200 @@ + +#ifndef _AST_H_ +#define _AST_H_ + +#include + +/** + * @brief The type of a AST node + * @attention The last element is not to be used in the AST + * as it is used as a lazy way to get the total number of available + * variants of this enum. + */ +enum AST_SyntaxElement_t { + AST_Stmt = 0, + AST_Expr, + // Literals + AST_Int, + AST_Float, + AST_String, + // Control flow + AST_While, + AST_If, + AST_IfElse, + AST_Else, + AST_Condition, + // Variable management + AST_Decl, + AST_Assign, + AST_Def, + AST_Ident, + // Arithmetic operators + AST_Add, + AST_Sub, + AST_Mul, + AST_Div, + // Bitwise operators + AST_BitAnd, + AST_BitOr, + AST_BitXor, + AST_BitNot, + // Boolean operators + AST_BoolAnd, + AST_BoolOr, + AST_BoolXor, + AST_BoolNot, + // Logical operators + AST_Eq, + AST_Greater, + AST_Less, + // Casts + AST_Typecast, // type cast + AST_Transmute, // reinterpret cast + AST_Call, // function call + AST_Macro, // builtin functions: lineno(), filename(), ... + // Defintions + AST_Typedef, + AST_Box, + AST_Fun, + AST_Import, + // amount of variants + // in this enum + AST_ELEMENT_COUNT +}; + +/** + * @brief A single node which can be joined with other nodes like a graph. + * Every node can have one ancestor (parent) but multiple (also none) children. + * Nodes have two properties: + * - kind: The type of the node. Such as AST_Expr, AST_Add, ... + * - value: A string representing an optional value. Can be a integer literal for kind AST_int + */ +struct AST_Node_t { + // parent node that owns this node + struct AST_Node_t *parent; + + // type of AST node: if, declaration, ... + enum AST_SyntaxElement_t kind; + // optional value: integer literal, string literal, ... + const char* value; + + // number of child nodes ownd by this node + // length of children array + size_t child_count; + // variable amount of child nodes + struct AST_Node_t **children; +}; + +/** + * Shorthand type for a single AST node + */ +typedef struct AST_Node_t* AST_NODE_PTR; + +/** + * @brief Initalize the global state of this module. Required for some functionality to work correctly. + */ +void AST_init(void); + +/** + * @brief Returns the string representation of the supplied node + * @attention The retuned pointer is not to be freed as it may either be a statically stored string or + * used by the node after this function call. + * @param node to return string representation of + * @return string represenation of the node + */ +[[maybe_unused]] +[[gnu::nonnull(1)]] +const char* AST_node_to_string(const struct AST_Node_t* node); + +/** + * @brief Create a new node struct on the system heap. Initializes the struct with the given values. + * All other fields are set to either NULL or 0. No allocation for children array is preformed. +* @attention parameter value can be NULL in case no value can be provided for the node + * @param kind the type of this node + * @param value an optional value for this node + * @return + */ +[[maybe_unused]] +[[nodiscard("pointer must be freed")]] +[[gnu::returns_nonnull]] +struct AST_Node_t *AST_new_node(enum AST_SyntaxElement_t kind, const char* value); + +/** + * @brief Deallocate this node and all of its children. + * @attention This function will detach this node from its parent if one is present + * Use of the supplied node after this call is undefined behavior + * @param node The node to deallocate + */ +[[maybe_unused]] +[[gnu::nonnull(1)]] +void AST_delete_node(struct AST_Node_t * node); + +/** + * @brief Add a new child node to a parent node + * @attention This can reallocate the children array + * @param owner node to add a child to + * @param child node to be added as a child + */ +[[maybe_unused]] +[[gnu::nonnull(1), gnu::nonnull(2)]] +void AST_push_node(struct AST_Node_t *owner, struct AST_Node_t *child); + +/** + * @brief Remove the specified child from the owner. + * @attention The parent of the removed node is set to NULL. + * The returned pointer is still valid. It must be freed at some pointer later. + * @param owner Node to remove the child from + * @param idx the index of the child to remove + * @return a pointer to the child which was removed + */ +[[maybe_unused]] +[[nodiscard("pointer must be freed")]] +[[gnu::nonnull(1)]] +struct AST_Node_t* AST_remove_child(struct AST_Node_t* owner, size_t idx); + +/** + * @brief Detach a child from its parent. This involves removing the child from its parent + * and marking the parent of the child as NULL. + * @attention The returned pointer is still valid. It must be freed at some pointer later. + * @param owner the owner to remove the child from + * @param child the child to detach + * @return a pointer to child detached + */ +[[nodiscard("pointer must be freed")]] +[[gnu::nonnull(1), gnu::nonnull(1)]] +struct AST_Node_t* AST_detach_child(struct AST_Node_t* owner, const struct AST_Node_t* child); + +/** + * @brief Return a pointer to the n-th child of a node + * @attention Pointer to childen nodes will never change. + * However, the index a node is stored within a parent can change + * if a child of lower index is removed, thus reducing the childrens index by one. + * @param owner the parent node which owns the children + * @param idx the index of the child to get a pointer to + * @return a pointer to the n-th child of the owner node + */ +[[maybe_unused]] +[[gnu::nonnull(1)]] +struct AST_Node_t *AST_get_node(struct AST_Node_t *owner, size_t idx); + +/** + * @brief Execute a function for every child, grandchild, ... and the supplied node as topmost ancestor + * @param root the root to recursively execute a function for + * @param for_each the function to execute + */ +[[maybe_unused]] +[[gnu::nonnull(1), gnu::nonnull(2)]] +void AST_visit_nodes_recurse(struct AST_Node_t *root, + void (*for_each)(struct AST_Node_t *node, + size_t depth)); + +/** + * @brief Prints a graphviz graph of the node and all its ancestors. + * @param stream The stream to print to. Can be a file, stdout, ... + * @param node the topmost ancestor + */ +[[maybe_unused]] +[[gnu::nonnull(1), gnu::nonnull(2)]] +void AST_fprint_graphviz(FILE* stream, const struct AST_Node_t* node); + +#endif diff --git a/src/main.c b/src/main.c index 38cf9c9..03ab6b9 100644 --- a/src/main.c +++ b/src/main.c @@ -1,77 +1,71 @@ +#include #include #include #include #define LOG_LEVEL LOG_LEVEL_DEBUG -extern FILE* yyin; +extern FILE *yyin; /** * @brief Log a debug message to inform about beginning exit procedures - * + * */ -void notify_exit(void) -{ - DEBUG("Exiting gemstone..."); -} +void notify_exit(void) { DEBUG("Exiting gemstone..."); } /** * @brief Closes File after compiling. - * + * */ -void close_file(void) -{ - if (NULL != yyin) - { - fclose(yyin); - } +void close_file(void) { + if (NULL != yyin) { + fclose(yyin); + } } /** * @brief Run compiler setup here - * + * */ -void setup(void) -{ - // setup preample +void setup(void) { + // setup preample - log_init(); - DEBUG("starting gemstone..."); + log_init(); + DEBUG("starting gemstone..."); - #if LOG_LEVEL <= LOG_LEVEL_DEBUG - atexit(¬ify_exit); - #endif +#if LOG_LEVEL <= LOG_LEVEL_DEBUG + atexit(¬ify_exit); +#endif - // actual setup - - DEBUG("finished starting up gemstone..."); + // actual setup + AST_init(); + + DEBUG("finished starting up gemstone..."); } int main(int argc, char *argv[]) { - setup(); - atexit(close_file); - - // Check for file input as argument - if (2 != argc) - { - INFO("Usage: %s \n", argv[0]); - PANIC("No File could be found"); - } - - // filename as first argument - char *filename = argv[1]; + setup(); + atexit(close_file); - FILE *file = fopen(filename, "r"); + // Check for file input as argument + if (2 != argc) { + INFO("Usage: %s \n", argv[0]); + PANIC("No File could be found"); + } - if (NULL == file) - { - PANIC("File couldn't be opened!"); - } - yyin = file; - - yyparse(); + // filename as first argument + char *filename = argv[1]; - return 0; + FILE *file = fopen(filename, "r"); + + if (NULL == file) { + PANIC("File couldn't be opened!"); + } + yyin = file; + + yyparse(); + + return 0; } diff --git a/src/sys/log.c b/src/sys/log.c index e3a7cd6..0aeab5f 100644 --- a/src/sys/log.c +++ b/src/sys/log.c @@ -3,6 +3,7 @@ #include #include #include +#include static struct Logger_t { FILE** streams; @@ -11,13 +12,15 @@ static struct Logger_t { void log_init(void) { + assert(LOG_DEFAULT_STREAM != NULL); log_register_stream(LOG_DEFAULT_STREAM); } void log_register_stream(FILE* restrict stream) { - if (stream == NULL) - PANIC("stream to register is NULL"); + // replace runtime check with assertion + // only to be used in debug target + assert(stream != NULL); if (GlobalLogger.stream_count == 0) { @@ -57,7 +60,7 @@ static void vflogf( vfprintf(stream, format, args); } -void __logf( +void syslog_logf( const char* restrict level, const char* restrict file, const unsigned long line, @@ -78,7 +81,7 @@ void __logf( va_end(args); } -void __panicf( +void syslog_panicf( const char* restrict file, const unsigned long line, const char* restrict func, @@ -95,7 +98,7 @@ void __panicf( exit(EXIT_FAILURE); } -void __fatalf( +void syslog_fatalf( const char* restrict file, const unsigned long line, const char* restrict func, diff --git a/src/sys/log.h b/src/sys/log.h index aa18c88..6b8991d 100644 --- a/src/sys/log.h +++ b/src/sys/log.h @@ -2,7 +2,6 @@ #define _SYS_ERR_H_ #include -#include #define LOG_DEFAULT_STREAM stderr @@ -35,14 +34,14 @@ * This macro will print debug information to stderr and call abort() to * performa a ungracefull exit. No clean up possible. */ -#define PANIC(format, ...) __panicf(__FILE_NAME__, __LINE__, __func__, format"\n", ##__VA_ARGS__) +#define PANIC(format, ...) syslog_panicf(__FILE_NAME__, __LINE__, __func__, format"\n", ##__VA_ARGS__) /** * @brief Panic is used in cases where the process is in an invalid or undefined state. * This macro will print debug information to stderr and call exit() to * initiate a gracefull exit, giving the process the opportunity to clean up. */ -#define FATAL(format, ...) __fatalf(__FILE_NAME__, __LINE__, __func__, format"\n", ##__VA_ARGS__) +#define FATAL(format, ...) syslog_fatalf(__FILE_NAME__, __LINE__, __func__, format"\n", ##__VA_ARGS__) /* Standard log macros. These will not terminate the application. @@ -57,7 +56,7 @@ will not print. #define __LOG(level, priority, format, ...) \ do { \ if (LOG_LEVEL <= priority) \ - __logf(level, __FILE_NAME__, __LINE__, __func__, format, ##__VA_ARGS__); \ + syslog_logf(level, __FILE_NAME__, __LINE__, __func__, format, ##__VA_ARGS__); \ } while(0) /** @@ -70,10 +69,11 @@ will not print. * @param format the format to print following args in * @param ... */ -void __logf( +[[gnu::nonnull(1), gnu::nonnull(2), gnu::nonnull(4)]] +void syslog_logf( const char* restrict level, const char* restrict file, - const unsigned long line, + unsigned long line, const char* restrict func, const char* restrict format, ...); @@ -87,9 +87,11 @@ void __logf( * @param format the format to print following args in * @param ... */ -void __panicf( +[[noreturn]] +[[gnu::nonnull(1), gnu::nonnull(3), gnu::nonnull(4)]] +void syslog_panicf( const char* restrict file, - const unsigned long line, + unsigned long line, const char* restrict func, const char* restrict format, ...); @@ -103,9 +105,11 @@ void __panicf( * @param format the format to print following args in * @param ... */ -void __fatalf( +[[noreturn]] +[[gnu::nonnull(1), gnu::nonnull(3), gnu::nonnull(4)]] +void syslog_fatalf( const char* restrict file, - const unsigned long line, + unsigned long line, const char* restrict func, const char* restrict format, ...); @@ -121,6 +125,7 @@ void log_init(void); * * @param stream */ +[[gnu::nonnull(1)]] void log_register_stream(FILE* restrict stream); #endif /* _SYS_ERR_H_ */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ab8afdc..b5c1111 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,4 +7,5 @@ set(CTEST_BINARY_DIRECTORY ${PROJECT_BINARY_DIR}/tests) # Provide test to run here or include another CMakeLists.txt add_subdirectory(logging) -add_subdirectory(input_file) \ No newline at end of file +add_subdirectory(input_file) +add_subdirectory(ast) \ No newline at end of file diff --git a/tests/ast/CMakeLists.txt b/tests/ast/CMakeLists.txt new file mode 100644 index 0000000..455130b --- /dev/null +++ b/tests/ast/CMakeLists.txt @@ -0,0 +1,51 @@ +include(CTest) + +include_directories(${PROJECT_SOURCE_DIR}/src) + +# ------------------------------------------------------- # +# CTEST 1 +# test building the syntax tree + +add_executable(ast_build_tree + ${PROJECT_SOURCE_DIR}/src/ast/ast.c + ${PROJECT_SOURCE_DIR}/src/sys/log.c + build_tree.c) +set_target_properties(ast_build_tree + PROPERTIES + OUTPUT_NAME "build_tree" + RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/ast) +add_test(NAME ast_build_tree + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND python ${GEMSTONE_TEST_DIR}/ast/test_ast.py check_build_tree) + +# ------------------------------------------------------- # +# CTEST 2 +# test node to string output + +add_executable(ast_print_node + ${PROJECT_SOURCE_DIR}/src/ast/ast.c + ${PROJECT_SOURCE_DIR}/src/sys/log.c + print_node.c) +set_target_properties(ast_print_node + PROPERTIES + OUTPUT_NAME "print_node" + RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/ast) +add_test(NAME ast_print_node + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND python ${GEMSTONE_TEST_DIR}/ast/test_ast.py check_print_node) + +# ------------------------------------------------------- # +# CTEST 3 +# test graphviz output + +add_executable(ast_graphviz + ${PROJECT_SOURCE_DIR}/src/ast/ast.c + ${PROJECT_SOURCE_DIR}/src/sys/log.c + print_graphviz.c) +set_target_properties(ast_graphviz + PROPERTIES + OUTPUT_NAME "print_graphviz" + RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/ast) +add_test(NAME ast_graphviz + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} + COMMAND python ${GEMSTONE_TEST_DIR}/ast/test_ast.py check_print_graphviz) diff --git a/tests/ast/build_tree.c b/tests/ast/build_tree.c new file mode 100644 index 0000000..dec7b85 --- /dev/null +++ b/tests/ast/build_tree.c @@ -0,0 +1,40 @@ +// +// Created by servostar on 5/7/24. +// + +#include +#include + +void generate_statement(const AST_NODE_PTR stmt) { + const AST_NODE_PTR add = AST_new_node(AST_Add, NULL); + + AST_push_node(add, AST_new_node(AST_Int, "3")); + AST_push_node(add, AST_new_node(AST_Int, "6")); + + AST_push_node(stmt, add); +} + +void generate_branch(const AST_NODE_PTR stmt) { + const AST_NODE_PTR branch = AST_new_node(AST_If, NULL); + const AST_NODE_PTR gt = AST_new_node(AST_Greater, NULL); + + AST_push_node(branch, gt); + + AST_push_node(gt, AST_new_node(AST_Float, "2.3")); + AST_push_node(gt, AST_new_node(AST_Float, "0.79")); + + AST_push_node(stmt, branch); + + generate_statement(branch); +} + +int main(void) { + + const AST_NODE_PTR root = AST_new_node(AST_Stmt, NULL); + + generate_branch(root); + + AST_delete_node(root); + + return 0; +} diff --git a/tests/ast/gen_graph.c b/tests/ast/gen_graph.c new file mode 100644 index 0000000..186d571 --- /dev/null +++ b/tests/ast/gen_graph.c @@ -0,0 +1,32 @@ +// +// Created by servostar on 5/7/24. +// + +#include +#include + +int main(void) { + + struct AST_Node_t* node = AST_new_node(AST_If, NULL); + + struct AST_Node_t* child = AST_new_node(AST_Add, NULL); + AST_push_node(child, AST_new_node(AST_Int, "43")); + AST_push_node(child, AST_new_node(AST_Int, "9")); + + AST_push_node(node, child); + AST_push_node(node, AST_new_node(AST_Expr, NULL)); + AST_push_node(node, AST_new_node(AST_Expr, NULL)); + + FILE* out = fopen("ast.gv", "w+"); + // convert this file ^^^^^^ + // to an svg with: `dot -Tsvg ast.gv > graph.svg` + + AST_fprint_graphviz(out, node); + + AST_delete_node(node); + + fflush(out); + fclose(out); + + return 0; +} diff --git a/tests/ast/print_graphviz.c b/tests/ast/print_graphviz.c new file mode 100644 index 0000000..6df64b4 --- /dev/null +++ b/tests/ast/print_graphviz.c @@ -0,0 +1,54 @@ +// +// Created by servostar on 5/8/24. +// + +#include +#include + +void generate_statement(const AST_NODE_PTR stmt) { + const AST_NODE_PTR add = AST_new_node(AST_Add, NULL); + + AST_push_node(add, AST_new_node(AST_Int, "3")); + AST_push_node(add, AST_new_node(AST_Int, "6")); + + AST_push_node(stmt, add); +} + +void generate_branch(const AST_NODE_PTR stmt) { + const AST_NODE_PTR branch = AST_new_node(AST_If, NULL); + const AST_NODE_PTR gt = AST_new_node(AST_Greater, NULL); + + AST_push_node(branch, gt); + + AST_push_node(gt, AST_new_node(AST_Float, "2.3")); + AST_push_node(gt, AST_new_node(AST_Float, "0.79")); + + AST_push_node(stmt, branch); + + generate_statement(branch); +} + +int main(void) { + + AST_init(); + + const AST_NODE_PTR root = AST_new_node(AST_Stmt, NULL); + + generate_branch(root); + + FILE* output = fopen("tmp/graph.gv", "w"); + + if (output == NULL) { + PANIC("unable to open file"); + } + + AST_fprint_graphviz(output, root); + + fflush(output); + + fclose(output); + + AST_delete_node(root); + + return 0; +} diff --git a/tests/ast/print_node.c b/tests/ast/print_node.c new file mode 100644 index 0000000..38e8a41 --- /dev/null +++ b/tests/ast/print_node.c @@ -0,0 +1,23 @@ +// +// Created by servostar on 5/7/24. +// + +#include + +int main(void) { + + AST_init(); + + const AST_NODE_PTR node = AST_new_node(0, "value"); + + for (size_t i = 0; i < AST_ELEMENT_COUNT; i++) { + // set kind + node->kind = i; + // print symbol + printf("%ld %s\n", i, AST_node_to_string(node)); + } + + AST_delete_node(node); + + return 0; +} diff --git a/tests/ast/test_ast.py b/tests/ast/test_ast.py new file mode 100644 index 0000000..0f9b757 --- /dev/null +++ b/tests/ast/test_ast.py @@ -0,0 +1,126 @@ +import subprocess +import sys +import logging +from logging import info, error +import os + +BIN_DIR = "bin/tests/ast/" + + +def run_check_build_tree(): + info("started check tree build...") + + p = subprocess.run(BIN_DIR + "build_tree", capture_output=True, text=True) + + info("checking exit code...") + + # check exit code + assert p.returncode == 0 + + +def run_check_print_node(): + info("started check node print...") + + p = subprocess.run(BIN_DIR + "print_node", capture_output=True, text=True) + + info("checking exit code...") + + # check exit code + assert p.returncode == 0 + + assert """0 stmt +1 expr +2 value +3 value +4 value +5 while +6 if +7 else if +8 else +9 condition +10 decl +11 assign +12 def +13 value +14 + +15 - +16 * +17 / +18 & +19 | +20 ^ +21 ! +22 && +23 || +24 ^^ +25 !! +26 == +27 > +28 < +29 cast +30 as +31 value +32 value +33 typedef +34 box +35 fun +36 value +""" == p.stdout + + +def run_check_print_graphviz(): + info("started check print graphviz...") + + info("creating temporary folder...") + + if not os.path.exists("tmp"): + os.mkdir("tmp") + + info("cleaning temporary folder...") + + if os.path.exists("tmp/graph.gv"): + os.remove("tmp/graph.gv") + + if os.path.exists("tmp/graph.svg"): + os.remove("tmp/graph.svg") + + p = subprocess.run(BIN_DIR + "print_graphviz", capture_output=True, text=True) + + info("checking exit code...") + + # check exit code + assert p.returncode == 0 + + info("converting gv to svg...") + + p = subprocess.run(["dot", "-Tsvg", "tmp/graph.gv", "-otmp/graph.svg"]) + + info("checking exit code...") + assert p.returncode == 0 + + info("checking svg output...") + with open("tmp/graph.svg", "r") as file: + string = "".join(file.readlines()) + assert "2.3" in string + assert "0.79" in string + assert "stmt" in string + assert "if" in string + + +if __name__ == "__main__": + logging.basicConfig(level=logging.INFO) + + target = sys.argv[1] + + info(f"starting ast test suite with target: {target}") + + match target: + case "check_build_tree": + run_check_build_tree() + case "check_print_node": + run_check_print_node() + case "check_print_graphviz": + run_check_print_graphviz() + case _: + error(f"unknown target: {target}") + exit(1)