Merge branch '41-add-parser-rules-for-casts' of https://github.com/Servostar/gemstone into 41-add-parser-rules-for-casts

This commit is contained in:
Ur Mom 2024-05-12 22:55:03 +02:00
commit 76558e89ed
30 changed files with 1335 additions and 79 deletions

View File

@ -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.1-alpine-3.19.1
SDK: 0.2.3-alpine-3.19.1
jobs:
build-check-sdk:
runs-on: ubuntu-latest
@ -11,4 +11,4 @@ jobs:
- name: Setup SDK
run: docker pull servostar/gemstone:sdk-"$SDK" && docker build --tag gemstone:devkit-"$SDK" .
- name: Compile
run: docker run gemstone:devkit-"$SDK" make check
run: docker run gemstone:devkit-"$SDK" sh run-check-test.sh

3
.gitignore vendored
View File

@ -13,4 +13,5 @@ Makefile
lexer.ll.c
parser.tab.c
parser.tab.h
build
build
/Testing/

View File

@ -21,6 +21,18 @@ 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)
include(CTest)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# ------------------------------------------------ #
@ -79,7 +91,7 @@ add_executable(release
set_target_properties(release
PROPERTIES
OUTPUT_NAME "gsc"
RUNTIME_OUTPUT_DIRECTORY "bin/release")
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/release)
# FIXME: cannot compile with /O2 because of /RTC1 flag
if (MSVC)
@ -111,7 +123,7 @@ add_executable(debug
set_target_properties(debug
PROPERTIES
OUTPUT_NAME "gsc"
RUNTIME_OUTPUT_DIRECTORY "bin/debug")
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/debug)
if (MSVC)
set(DEBUG_FLAGS /DEBUG)
@ -140,7 +152,7 @@ add_executable(check
set_target_properties(check
PROPERTIES
OUTPUT_NAME "gsc"
RUNTIME_OUTPUT_DIRECTORY "bin/check")
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/check)
if (MSVC)
set(CHECK_FLAGS /DEBUG /WX)

View File

@ -1,10 +1,12 @@
FROM servostar/gemstone:sdk-0.2.1-alpine-3.19.1
FROM servostar/gemstone:sdk-0.2.3-alpine-3.19.1
LABEL authors="servostar"
LABEL version="0.2.1"
LABEL version="0.2.3"
LABEL description="docker image for setting up the build pipeline on SDK"
LABEL website="https://github.com/Servostar/gemstone"
COPY --chown=lorang src /home/lorang/src
COPY --chown=lorang tests /home/lorang/tests
COPY --chown=lorang CMakeLists.txt /home/lorang/
COPY --chown=lorang run-check-test.sh /home/lorang/
RUN cmake .

View File

@ -21,6 +21,28 @@ Requires:
- bison
- flex
## Writing Tests
Since the project is build and configured through CMake it makes sense to rely for tests
on CTest. All tests are located in the subfolder `tests`. In this directory is a CMakeLists.txt which specifies which tests
are to be run. Actual tests are located in folders within tests and contain a final CMakeLists.txt which specifies what to run
for a single test.
```
tests
└─ test_group1
└─ CMakeLists.txt # specify tests in this group
└─ ... # test files of group 1
└─ test_group2
└─ CMakeLists.txt # specify tests in this group
└─ ... # test files of group 2
└─ CMakeLists.txt # specify test groups to run
CMakeLists.txt # build configuration
```
## Development with VSCode/Codium
Recommended extensions for getting a decent experience are the following:

40
run-check-test.sh Normal file
View File

@ -0,0 +1,40 @@
#!/usr/bin/env sh
# Author: Sven Vogel
# Created: 02.05.2024
# Description: Builds the project and runs tests
# Returns 0 on success and 1 when something went wrong
echo "+--------------------------------------+"
echo "| BUILDING all TARGETS |"
echo "+--------------------------------------+"
make -B
if [ ! $? -eq 0 ]; then
echo "===> failed to build targets"
exit 1
fi
echo "+--------------------------------------+"
echo "| RUNNING CODE CHECK |"
echo "+--------------------------------------+"
make check
if [ ! $? -eq 0 ]; then
echo "===> failed code check..."
exit 1
fi
echo "+--------------------------------------+"
echo "| RUNNING TESTS |"
echo "+--------------------------------------+"
ctest -VV --output-on-failure --schedule-random -j 4
if [ ! $? -eq 0 ]; then
echo "===> failed tests..."
exit 1
fi
echo "+--------------------------------------+"
echo "| COMPLETED CHECK + TESTS SUCCESSFULLY |"
echo "+--------------------------------------+"

View File

@ -1,11 +1,11 @@
FROM alpine:3.19.1
LABEL authors="servostar"
LABEL version="0.2.1"
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
RUN apk add build-base gcc make cmake bison flex git python3 graphviz
# create user for build
RUN adduser --disabled-password lorang

274
src/ast/ast.c Normal file
View File

@ -0,0 +1,274 @@
#include <ast/ast.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/log.h>
#include <assert.h>
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");
}

200
src/ast/ast.h Normal file
View File

@ -0,0 +1,200 @@
#ifndef _AST_H_
#define _AST_H_
#include <stdio.h>
/**
* @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

View File

@ -38,6 +38,7 @@
"int" {DEBUG("\"%s\" tokenized with \'KeyInt'", yytext); return(KeyInt);};
"float" {DEBUG("\"%s\" tokenized with \'KeyFloat\'", yytext); return(KeyFloat);};
"self" {DEBUG("\"%s\" tokenized with \'KeySelf\'", yytext); return(KeySelf);};
"as" {DEBUG("\"%s\" tokenized with \'KeyAs'", yytext); return (KeyAs);};
"short" {DEBUG("\"%s\" tokenized with \'KeyShort\'", yytext); return(KeyShort);};
"long" {DEBUG("\"%s\" tokenized with \'KeyLong\'", yytext); return(KeyLong);};

View File

@ -1,77 +1,71 @@
#include <ast/ast.h>
#include <stdlib.h>
#include <sys/log.h>
#include <yacc/parser.tab.h>
#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(&notify_exit);
#endif
#if LOG_LEVEL <= LOG_LEVEL_DEBUG
atexit(&notify_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 <filename>\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 <filename>\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;
}

View File

@ -3,6 +3,7 @@
#include <stdio.h>
#include <stdlib.h>
#include <sys/log.h>
#include <assert.h>
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,

View File

@ -2,7 +2,6 @@
#define _SYS_ERR_H_
#include <stdio.h>
#include <string.h>
#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_ */

View File

@ -13,6 +13,7 @@
%token KeyInt
%token KeyFloat
%token KeySelf
%token KeyAs
%token <string> ValInt
%token <string> Ident
@ -63,15 +64,26 @@
%left OpBitand OpBitor OpBitxor OpBitnot
%%
program: statementlist
| fundef;
program: program programbody
| programbody;
programbody: moduleimport
| fundef
| box
| definition
| decl
| typedef;
expr: ValFloat
| ValInt
| ValMultistr
| ValStr
| Ident
| operation;
| operation
| boxaccess
| boxselfaccess;
exprlist: expr ',' exprlist
| expr;
@ -102,9 +114,26 @@ reinterpretcast: '(' type ')' expr { DEBUG("Reinterpret-Cast"); };
paramdecl: type ':' Ident { DEBUG("Param-Declaration"); };
funcall: Ident argumentlist { DEBUG("Function call"); };
box: KeyType KeyBox ':' Ident '{' boxbody '}' { DEBUG("Box"); }
| KeyType KeyBox ':' Ident '{' '}';
assign: Ident '=' expr { DEBUG("Assignment"); };
boxbody: boxbody boxcontent
| boxcontent;
boxcontent: decl { DEBUG("Box decl Content"); }
| definition { DEBUG("Box def Content"); }
| fundef { DEBUG("Box fun Content"); };
boxselfaccess: KeySelf '.' Ident
| KeySelf '.' boxaccess;
boxaccess: Ident '.' Ident
| Ident '.' boxaccess;
boxcall: boxaccess argumentlist
| boxselfaccess argumentlist;
funcall: Ident argumentlist { DEBUG("Function call"); };
moduleimport: KeyImport ValStr { DEBUG("Module-Import"); };
@ -116,7 +145,8 @@ statement: assign
| definition
| while
| branch
| funcall;
| funcall
| boxcall;
branchif: KeyIf expr '{' statementlist '}' { DEBUG("if"); };
branchelse: KeyElse '{' statementlist '}' { DEBUG("if-else"); };
@ -133,17 +163,27 @@ while: KeyWhile expr '{' statementlist '}' { DEBUG("while"); };
identlist: Ident ',' identlist
| Ident;
decl: type ':' identlist;
decl: type ':' identlist
| storagequalifier type ':' identlist
definition: decl '=' expr { DEBUG("Definition"); }
| decl '=' typecast { DEBUG("Definition"); };
| decl '=' reinterpretcast { DEBUG("Definition"); };
assign: Ident '=' expr { DEBUG("Assignment"); };
storagequalifier: KeyGlobal
| KeyStatic
| KeyLocal;
assign: Ident '=' expr { DEBUG("Assignment"); }
| boxaccess '=' expr
| boxselfaccess '=' expr ;
sign: KeySigned
| KeyUnsigned;
typedef: KeyType type':' Ident;
scale: scale KeyShort
| scale KeyHalf
| scale KeyLong

11
tests/CMakeLists.txt Normal file
View File

@ -0,0 +1,11 @@
include(CTest)
set(PROJECT_BINARY_DIR bin)
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_BINARY_DIR}/tests)
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)
add_subdirectory(ast)

51
tests/ast/CMakeLists.txt Normal file
View File

@ -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)

40
tests/ast/build_tree.c Normal file
View File

@ -0,0 +1,40 @@
//
// Created by servostar on 5/7/24.
//
#include <ast/ast.h>
#include <sys/log.h>
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;
}

32
tests/ast/gen_graph.c Normal file
View File

@ -0,0 +1,32 @@
//
// Created by servostar on 5/7/24.
//
#include <ast/ast.h>
#include <sys/log.h>
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;
}

View File

@ -0,0 +1,54 @@
//
// Created by servostar on 5/8/24.
//
#include <ast/ast.h>
#include <sys/log.h>
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;
}

23
tests/ast/print_node.c Normal file
View File

@ -0,0 +1,23 @@
//
// Created by servostar on 5/7/24.
//
#include <ast/ast.h>
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;
}

126
tests/ast/test_ast.py Normal file
View File

@ -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)

View File

@ -0,0 +1,9 @@
include(CTest)
# ------------------------------------------------------- #
# CTEST 1
# test if the program accepts a file as input
add_test(NAME input_file_check
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/bin/check
COMMAND python ${GEMSTONE_TEST_DIR}/input_file/test_input_file.py ${GEMSTONE_TEST_DIR}/input_file/test.gem)

View File

@ -0,0 +1,6 @@
import "std.io"
fun main {
print("Hello, World!!!")
}

View File

@ -0,0 +1,36 @@
import os.path
import subprocess
import sys
import logging
from logging import info
def check_accept():
info("testing handling of input file...")
logging.basicConfig(level=logging.INFO)
test_file_name = sys.argv[1]
p = subprocess.run(["./gsc", test_file_name], capture_output=True, text=True)
assert p.returncode == 0
def check_abort():
info("testing handling of missing input file...")
logging.basicConfig(level=logging.INFO)
p = subprocess.run("./gsc", capture_output=True, text=True)
assert p.returncode == 1
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
info("check if binary exists...")
assert os.path.exists("./gsc")
check_accept()
check_abort()

View File

@ -0,0 +1,63 @@
include(CTest)
include_directories(${PROJECT_SOURCE_DIR}/src)
# ------------------------------------------------------- #
# CTEST 1
# test the default output of the logger
add_executable(logging_output
${PROJECT_SOURCE_DIR}/src/sys/log.c
output.c)
set_target_properties(logging_output
PROPERTIES
OUTPUT_NAME "output"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
add_test(NAME logging_output
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_output)
# ------------------------------------------------------- #
# CTEST 2
# test the panic functionality of the logger
add_executable(logging_panic
${PROJECT_SOURCE_DIR}/src/sys/log.c
panic.c)
set_target_properties(logging_panic
PROPERTIES
OUTPUT_NAME "panic"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
add_test(NAME logging_panic
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_panic)
# ------------------------------------------------------- #
# CTEST 3
# test the ability to write to multiple output streams
add_executable(logging_streams
${PROJECT_SOURCE_DIR}/src/sys/log.c
streams.c)
set_target_properties(logging_streams
PROPERTIES
OUTPUT_NAME "stream"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
add_test(NAME logging_streams
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_stream)
# ------------------------------------------------------- #
# CTEST 4
# test compile time log level switch
add_executable(logging_level
${PROJECT_SOURCE_DIR}/src/sys/log.c
level.c)
set_target_properties(logging_level
PROPERTIES
OUTPUT_NAME "level"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
add_test(NAME logging_level
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_level)

18
tests/logging/level.c Normal file
View File

@ -0,0 +1,18 @@
//
// Created by servostar on 5/2/24.
//
#include "sys/log.h"
#define LOG_LEVEL LOG_LEVEL_WARNING
int main(void) {
log_init();
DEBUG("logging some debug...");
INFO("logging some info...");
WARN("logging some warning...");
ERROR("logging some error...");
return 0;
}

16
tests/logging/output.c Normal file
View File

@ -0,0 +1,16 @@
//
// Created by servostar on 5/1/24.
//
#include "sys/log.h"
int main(void) {
log_init();
DEBUG("logging some debug...");
INFO("logging some info...");
WARN("logging some warning...");
ERROR("logging some error...");
return 0;
}

20
tests/logging/panic.c Normal file
View File

@ -0,0 +1,20 @@
//
// Created by servostar on 5/2/24.
//
#include "sys/log.h"
int main(void) {
log_init();
// this should appear in stderr
INFO("before exit");
PANIC("oooops something happened");
// this should NOT appear in stderr
// ^^^
ERROR("after exit");
return 0;
}

33
tests/logging/streams.c Normal file
View File

@ -0,0 +1,33 @@
//
// Created by servostar on 5/2/24.
//
#include "sys/log.h"
#include <stdlib.h>
static FILE* file;
void close_file(void) {
if (file != NULL) {
fclose(file);
}
}
int main(void) {
log_init();
// this should appear in stderr
INFO("should only be in stderr");
file = fopen("tmp/test.log", "w");
if (file == NULL) {
PANIC("could not open file");
}
atexit(close_file);
log_register_stream(file);
INFO("should be in both");
return 0;
}

View File

@ -0,0 +1,124 @@
import subprocess
import sys
import logging
from logging import info, error
import os
BIN_DIR = "bin/tests/logging/"
def run_check_output():
info("started check output...")
p = subprocess.run(BIN_DIR + "output", capture_output=True, text=True)
info("checking exit code...")
# check exit code
assert p.returncode == 0
output = p.stderr
# check if logs appear in default log output (stderr)
info("checking stderr...")
assert "logging some debug..." in output
assert "logging some info..." in output
assert "logging some warning..." in output
assert "logging some error..." in output
def run_check_level():
info("started check level...")
p = subprocess.run(BIN_DIR + "level", capture_output=True, text=True)
info("checking exit code...")
# check exit code
assert p.returncode == 0
output = p.stderr
# check if logs appear in default log output (stderr)
info("checking stderr...")
assert "logging some debug..." not in output
assert "logging some info..." not in output
assert "logging some warning..." in output
assert "logging some error..." in output
def run_check_panic():
info("started check panic...")
p = subprocess.run(BIN_DIR + "panic", capture_output=True, text=True)
info("checking exit code...")
# check exit code
assert p.returncode == 1
output = p.stderr
# check if logs appear (not) in default log output (stderr)
info("checking stderr...")
assert "before exit" in output
assert "oooops something happened" in output
assert "after exit" not in output
def run_check_stream():
info("started check panic...")
info("creating temporary folder...")
if not os.path.exists("tmp"):
os.mkdir("tmp")
info("cleaning temporary folder...")
if os.path.exists("tmp/test.log"):
os.remove("tmp/test.log")
info("launching test binary...")
p = subprocess.run(BIN_DIR + "stream", capture_output=True, text=True)
info("checking exit code...")
# check exit code
assert p.returncode == 0
with open("tmp/test.log", "r") as file:
assert "should be in both" in "".join(file.readlines())
output = p.stderr
# check if logs appear (not) in default log output (stderr)
info("checking stderr...")
assert "should only be in stderr" in output
assert "should be in both" in output
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
target = sys.argv[1]
info(f"starting logging test suite with target: {target}")
match target:
case "check_output":
run_check_output()
case "check_panic":
run_check_panic()
case "check_stream":
run_check_stream()
case "check_level":
run_check_level()
case _:
error(f"unknown target: {target}")
exit(1)