Merge pull request #113 from Servostar/6-add-mechanism-to-read-and-globally-save-options

6 add mechanism to read and globally save options
This commit is contained in:
servostar 2024-06-03 20:04:27 +02:00 committed by GitHub
commit ec1c141e01
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 1236 additions and 113 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "dep/tomlc99"]
path = dep/tomlc99
url = https://github.com/cktan/tomlc99.git

View File

@ -4,8 +4,6 @@ cmake_minimum_required(VERSION 3.15...3.25)
# Header must be included this way: #include <module/header.h>
#
# res
# dep
# klib
# src
# lex
# lexer.l
@ -29,6 +27,8 @@ set(CMAKE_C_STANDARD_REQUIRED TRUE)
set(GEMSTONE_TEST_DIR ${PROJECT_SOURCE_DIR}/tests)
set(GEMSTONE_BINARY_DIR ${PROJECT_SOURCE_DIR}/bin)
add_compile_definitions(GSC_VERSION="${PROJECT_VERSION}")
include(CTest)
if(BUILD_TESTING)
@ -63,10 +63,8 @@ set(YACC_SOURCE_FILE ${PROJECT_SOURCE_DIR}/src/yacc/parser.y)
set(YACC_GENERATED_SOURCE_FILE ${PROJECT_SOURCE_DIR}/src/yacc/parser.tab.c)
add_custom_command(OUTPUT ${YACC_GENERATED_SOURCE_FILE}
COMMAND bison
ARGS -Wno-yacc -Wcounterexamples -d -o ${YACC_GENERATED_SOURCE_FILE} ${YACC_SOURCE_FILE}
COMMENT "generate C source file for parser"
VERBATIM)
@ -77,6 +75,27 @@ add_custom_command(OUTPUT ${YACC_GENERATED_SOURCE_FILE}
find_package(PkgConfig REQUIRED)
pkg_search_module(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
# ------------------------------------------------ #
# TOML-C99 #
# ------------------------------------------------ #
execute_process(COMMAND git submodule init -- dep/tomlc99)
add_custom_command(OUTPUT ${PROJECT_SOURCE_DIR}/dep/tomlc99/toml.c
COMMAND git
ARGS submodule update --init --recursive --checkout
COMMENT "checkout dependency TOML-C99"
VERBATIM)
add_library(tomlc99 ${PROJECT_SOURCE_DIR}/dep/tomlc99/toml.c)
set_target_properties(tomlc99
PROPERTIES
OUTPUT_NAME "toml"
ARCHIVE_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/dep)
include_directories(${PROJECT_SOURCE_DIR}/dep/tomlc99)
# ------------------------------------------------ #
# Source #
# ------------------------------------------------ #
@ -109,6 +128,8 @@ set_target_properties(release
target_link_libraries(release PkgConfig::GLIB)
target_link_libraries(release tomlc99)
# FIXME: cannot compile with /O2 because of /RTC1 flag
if (MSVC)
set(RELEASE_FLAGS)
@ -127,6 +148,9 @@ target_compile_options(release PUBLIC ${FLAGS} ${RELEASE_FLAGS})
# add src directory as include path
target_include_directories(release PUBLIC src)
# disable assertions
target_compile_definitions(release PUBLIC NDEBUG="1")
# ------------------------------------------------ #
# Target DEBUG #
# ------------------------------------------------ #
@ -143,6 +167,8 @@ set_target_properties(debug
target_link_libraries(debug PkgConfig::GLIB)
target_link_libraries(debug tomlc99)
if (MSVC)
set(DEBUG_FLAGS /DEBUG)
else()
@ -174,6 +200,8 @@ set_target_properties(check
target_link_libraries(check PkgConfig::GLIB)
target_link_libraries(check tomlc99)
if (MSVC)
set(CHECK_FLAGS /DEBUG /WX)
else()

View File

@ -10,5 +10,7 @@ COPY --chown=lorang CMakeLists.txt /home/lorang/
COPY --chown=lorang run-check-test.sh /home/lorang/
COPY --chown=lorang .env /home/lorang/
COPY --chown=lorang run-docker-build.sh /home/lorang/
COPY --chown=lorang dep /home/lorang/dep
COPY --chown=lorang .git /home/lorang/.git
RUN cmake .

1
dep/tomlc99 Submodule

@ -0,0 +1 @@
Subproject commit 5221b3d3d66c25a1dc6f0372b4f824f1202fe398

419
src/cfg/opt.c Normal file
View File

@ -0,0 +1,419 @@
//
// Created by servostar on 5/31/24.
//
#include <cfg/opt.h>
#include <string.h>
#include <sys/log.h>
#include <io/files.h>
#include <assert.h>
#include <toml.h>
static GHashTable* args = NULL;
static void clean(void) {
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, args);
while (g_hash_table_iter_next(&iter, &key, &value)) {
free(value);
free(key);
}
g_hash_table_destroy(args);
}
void parse_options(int argc, char* argv[]) {
args = g_hash_table_new(g_str_hash, g_str_equal);
atexit(clean);
for (int i = 0; i < argc; i++) {
Option* option = malloc(sizeof(Option));
option->is_opt = g_str_has_prefix(argv[i], "--");
option->string = strdup(argv[i] + (option->is_opt ? 2 : 0));
option->index = i;
option->value = NULL;
char* equals = strchr(option->string, '=');
if (equals != NULL) {
option->value = equals + 1;
*equals = 0;
}
g_hash_table_insert(args, (gpointer) option->string, (gpointer) option);
}
}
bool is_option_set(const char* option) {
assert(option != NULL);
assert(args != NULL);
return g_hash_table_contains(args, option);
}
const Option* get_option(const char* option) {
if (g_hash_table_contains(args, option)) {
return g_hash_table_lookup(args, option);
}
return NULL;
}
GArray* get_non_options_after(const char* command) {
const Option* command_option = get_option(command);
if (command_option == NULL) {
return NULL;
}
GArray* array = g_array_new(FALSE, FALSE, sizeof(const char*));
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, args);
while (g_hash_table_iter_next(&iter, &key, &value)) {
Option* option = (Option*) value;
if (!option->is_opt && command_option->index < option->index) {
g_array_append_val(array, option->string);
}
}
if (array->len == 0) {
g_array_free(array, FALSE);
return NULL;
}
return array;
}
TargetConfig* default_target_config() {
DEBUG("generating default target config...");
TargetConfig* config = malloc(sizeof(TargetConfig));
config->name = strdup("out");
config->print_ast = false;
config->print_asm = false;
config->print_ir = false;
config->mode = Application;
config->archive_directory = strdup("archive");
config->output_directory = strdup("bin");
config->optimization_level = 1;
config->root_module = NULL;
return config;
}
TargetConfig* default_target_config_from_args() {
DEBUG("generating default target from command line...");
TargetConfig* config = default_target_config();
if (is_option_set("print-ast")) {
config->print_ast = true;
}
if (is_option_set("print-asm")) {
config->print_asm = true;
}
if (is_option_set("print-ir")) {
config->print_ir = true;
}
if (is_option_set("mode")) {
const Option* opt = get_option("mode");
if (opt->value != NULL) {
if (strcmp(opt->value, "app") == 0) {
config->mode = Application;
} else if (strcmp(opt->value, "lib") == 0) {
config->mode = Library;
} else {
print_message(Warning, "Invalid compilation mode: %s", opt->value);
}
}
}
if (is_option_set("output")) {
const Option* opt = get_option("output");
if (opt->value != NULL) {
config->name = strdup(opt->value);
}
}
GArray* files = get_non_options_after("compile");
if (files == NULL) {
print_message(Error, "No input file provided.");
} else {
if (files->len > 1) {
print_message(Warning, "Got more than one file to compile, using first, ignoring others.");
}
config->root_module = strdup( ((char**) files->data) [0]);
g_array_free(files, TRUE);
}
return config;
}
void print_help(void) {
DEBUG("printing help dialog...");
const char *lines[] = {
"Gemstone Compiler (c) GPL-2.0",
"Build a project target: gsc build [target]|all",
"Compile non-project file: gsc compile <target-options> [file]",
"Output information: gsc <option>",
"Target options:",
" --print-ast print resulting abstract syntax tree to a file",
" --print-asm print resulting assembly language to a file",
" --print-ir print resulting LLVM-IR to a file",
" --mode=[app|lib] set the compilation mode to either application or library",
" --output=name name of output files without extension",
"Options:",
" --verbose print logs with level information or higher",
" --debug print debug logs (if not disabled at compile time)",
" --version print the version",
" --help print this hel dialog",
};
for (unsigned int i = 0; i < sizeof(lines) / sizeof(const char *); i++) {
printf("%s\n", lines[i]);
}
}
static void get_bool(bool* boolean, const toml_table_t *table, const char* name) {
DEBUG("retrieving boolean %s", name);
const toml_datum_t datum = toml_bool_in(table, name);
if (datum.ok) {
*boolean = datum.u.b;
DEBUG("boolean has value: %d", datum.u.b);
}
}
static void get_str(char** string, const toml_table_t *table, const char* name) {
DEBUG("retrieving string %s", name);
const toml_datum_t datum = toml_string_in(table, name);
if (datum.ok) {
*string = datum.u.s;
DEBUG("string has value: %s", datum.u.s);
}
}
static void get_int(int* integer, const toml_table_t *table, const char* name) {
DEBUG("retrieving integer %s", name);
const toml_datum_t datum = toml_int_in(table, name);
if (datum.ok) {
*integer = (int) datum.u.i;
DEBUG("integer has value: %ld", datum.u.i);
}
}
static int parse_project_table(ProjectConfig *config, const toml_table_t *project_table) {
DEBUG("parsing project table...");
// project name
get_str(&config->name, project_table, "version");
if (config->name == NULL) {
printf("Invalid project configuration: project must have a name\n\n");
return PROJECT_SEMANTIC_ERR;
}
// project version
get_str(&config->name, project_table, "name");
// author names
toml_array_t *authors = toml_array_in(project_table, "authors");
if (authors) {
config->authors = g_array_new(FALSE, FALSE, sizeof(char *));
for (int i = 0;; i++) {
toml_datum_t author = toml_string_at(authors, i);
if (!author.ok)
break;
g_array_append_val(config->authors, author.u.s);
}
}
// project description
get_str(&config->desc, project_table, "description");
// project license
get_str(&config->license, project_table, "license");
return PROJECT_OK;
}
static int get_mode_from_str(TargetCompilationMode* mode, const char* name) {
if (mode == NULL) {
return PROJECT_SEMANTIC_ERR;
}
if (strcmp(name, "application") == 0) {
*mode = Application;
return PROJECT_OK;
} else if (strcmp(name, "library") == 0) {
*mode = Library;
return PROJECT_OK;
}
printf("Invalid project configuration, mode is invalid: %s\n\n", name);
return PROJECT_SEMANTIC_ERR;
}
static int parse_target(const ProjectConfig *config, const toml_table_t *target_table, const char* name) {
DEBUG("parsing target table...");
TargetConfig* target_config = default_target_config();
target_config->name = (char*) name;
get_bool(&target_config->print_ast, target_table, "print_ast");
get_bool(&target_config->print_asm, target_table, "print_asm");
get_bool(&target_config->print_ir, target_table, "print_ir");
get_str(&target_config->root_module, target_table, "root");
get_str(&target_config->output_directory, target_table, "output");
get_str(&target_config->archive_directory, target_table, "archive");
get_int(&target_config->optimization_level, target_table, "opt");
char* mode = NULL;
get_str(&mode, target_table, "mode");
int err = get_mode_from_str(&target_config->mode, mode);
if (err != PROJECT_OK) {
return err;
}
g_hash_table_insert(config->targets, target_config->name, target_config);
return PROJECT_OK;
}
static int parse_targets(ProjectConfig *config, const toml_table_t *root) {
DEBUG("parsing targets of project \"%s\"", config->name);
toml_table_t *targets = toml_table_in(root, "target");
if (targets == NULL) {
print_message(Warning, "Project has no targets");
return PROJECT_SEMANTIC_ERR;
}
config->targets = g_hash_table_new(g_str_hash, g_str_equal);
for (int i = 0; i < MAX_TARGETS_PER_PROJECT; i++) {
const char *key = toml_key_in(targets, i);
if (key == NULL)
break;
toml_table_t *target = toml_table_in(targets, key);
parse_target(config, target, key);
}
return PROJECT_OK;
}
int load_project_config(ProjectConfig *config) {
DEBUG("loading project config...");
FILE *config_file = fopen(PROJECT_CONFIG_FILE, "r");
if (config_file == NULL) {
print_message(Error, "Cannot open file %s: %s", PROJECT_CONFIG_FILE, strerror(errno));
INFO("project file not found");
return PROJECT_TOML_ERR;
}
char err_buf[TOML_ERROR_MSG_BUF];
toml_table_t *conf = toml_parse_file(config_file, err_buf, sizeof(err_buf));
fclose(config_file);
if (conf == NULL) {
print_message(Error, "Invalid project configuration: %s", err_buf);
return PROJECT_SEMANTIC_ERR;
}
toml_table_t *project = toml_table_in(conf, "project");
if (project == NULL) {
print_message(Error, "Invalid project configuration: missing project table.");
}
if (parse_project_table(config, project) == PROJECT_OK) {
return parse_targets(config, conf);
}
toml_free(conf);
return PROJECT_SEMANTIC_ERR;
}
void delete_target_config(TargetConfig* config) {
if (config->root_module != NULL) {
free(config->root_module);
}
if (config->archive_directory != NULL) {
free(config->archive_directory);
}
if (config->name != NULL) {
free(config->name);
}
if (config->output_directory != NULL) {
free(config->output_directory);
}
free(config);
}
void delete_project_config(ProjectConfig* config) {
if (config->name != NULL) {
free(config->name);
}
if (config->authors != NULL) {
g_array_free(config->authors, TRUE);
}
if (config->desc != NULL) {
free(config->desc);
}
if (config->license != NULL) {
free(config->license);
}
if (config->targets != NULL) {
GHashTableIter iter;
g_hash_table_iter_init(&iter, config->targets);
char* key;
TargetConfig* val;
while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &val)) {
delete_target_config(val);
}
g_hash_table_destroy(config->targets);
}
free(config);
}
ProjectConfig* default_project_config() {
ProjectConfig* config = malloc(sizeof(ProjectConfig));
config->authors = NULL;
config->name = NULL;
config->targets = NULL;
config->license = NULL;
config->version = NULL;
config->desc = NULL;
return config;
}

168
src/cfg/opt.h Normal file
View File

@ -0,0 +1,168 @@
//
// Created by servostar on 5/31/24.
//
#ifndef GEMSTONE_OPT_H
#define GEMSTONE_OPT_H
#include <glib.h>
#define MAX_TARGETS_PER_PROJECT 100
#define PROJECT_CONFIG_FILE "build.toml"
#define PROJECT_OK 0
#define PROJECT_TOML_ERR 1
#define PROJECT_SEMANTIC_ERR 2
#define TOML_ERROR_MSG_BUF 256
typedef enum TargetCompilationMode_t {
// output an executable binary
Application,
// output a binary object file
Library
} TargetCompilationMode;
/**
* @brief A target defines a source file which is to be compiled into a specific
* format. Additionally properties such as output folders can be set.
* Intermediate representations can be printed as well.
*/
typedef struct TargetConfig_t {
char* name;
bool print_ast;
bool print_asm;
bool print_ir;
// root module file which imports all submodules
// if this is NULL use the first commandline argument as root module
char* root_module;
// output directory for binaries
char* output_directory;
// output directory for intermediate representations (LLVM-IR, Assembly, ...)
char* archive_directory;
// mode of compilation
TargetCompilationMode mode;
// number between 1 and 3
int optimization_level;
} TargetConfig;
/**
* @brief A project is a collection of targets. Each target can point to
* very different sources. The project binds these targets together
* and specifies metadata about the authors and others.
*/
typedef struct ProjectConfig_t {
// name of the project
char* name;
// description
char* desc;
// version
char* version;
// license
char* license;
// list of authors
GArray* authors;
GHashTable* targets;
} ProjectConfig;
/**
* @brief Represents a command line option.
*/
typedef struct Option_t {
// index in which the option appeared in the argument array
int index;
// identifier of the option
const char* string;
// option if format is equals to --option=value
const char* value;
// whether or not this option has a value
bool is_opt;
} Option;
/**
* @brief Create the default configuration for targets.
* @return A pointer to a new target configuration.
*/
[[nodiscard("must be freed")]]
TargetConfig* default_target_config();
/**
* @brief Create the default configuration for a project.
* Contains no targets.
* @return A pointer to a new project configuration.
*/
[[nodiscard("must be freed")]]
ProjectConfig* default_project_config();
/**
* @brief Create a new default target configuration an write command line
* option onto the default configuration.
* @return A default config with user specified values.
*/
[[nodiscard("must be freed")]]
TargetConfig* default_target_config_from_args();
/**
* @brief Load a project configuration from a TOML file with the name
* PROJECT_CONFIG_FILE.
* @param config Configuration to fill values from file into.
* @return
*/
[[gnu::nonnull(1)]]
int load_project_config(ProjectConfig *config);
/**
* @brief Print a help dialog to stdout.
*/
void print_help(void);
/**
* @brief Delete a project config by deallocation.
* @param config The config to free.
*/
[[gnu::nonnull(1)]]
void delete_project_config(ProjectConfig* config);
/**
* @brief Delete a target configuration by deallocation.
*/
[[gnu::nonnull(1)]]
void delete_target_config(TargetConfig*);
/**
* @brief Parse the given command line arguments so that calls to
* is_option_set() and get_option() can be made safely.
* @param argc Number of arguments
* @param argv Array of arguments
*/
void parse_options(int argc, char* argv[]);
/**
* @brief Tests whether an option was set as argument.
* @attention Requires a previous call to parse_options()
* @param option Name of option to check for.
* @return 1 if the options was set, 0 otherwise
*/
[[gnu::nonnull(1)]]
bool is_option_set(const char* option);
/**
* @brief Returns the options information if present
* @attention Requires a previous call to parse_options()
* @param option
* @return A valid option struct or NULL if not found.
*/
[[gnu::nonnull(1)]]
const Option* get_option(const char* option);
/**
* @brief Put a copy of all options whos index is greather than the index
* of the option specified by command.
* @param command
* @return an array of options that followed command.
*/
[[gnu::nonnull(1)]]
[[nodiscard("must be freed")]]
GArray* get_non_options_after(const char* command);
#endif //GEMSTONE_OPT_H

248
src/compiler.c Normal file
View File

@ -0,0 +1,248 @@
//
// Created by servostar on 6/2/24.
//
#include <compiler.h>
#include <ast/ast.h>
#include <stdlib.h>
#include <sys/log.h>
#include <yacc/parser.tab.h>
#include <lex/util.h>
#include <io/files.h>
#include <assert.h>
#include <cfg/opt.h>
extern void yyrestart(FILE *);
// Module AST node used by the parser for AST construction.
[[maybe_unused]]
AST_NODE_PTR root;
// Current file which gets compiled the parser.
// NOTE: due to global state no concurrent compilation is possible
// on parser level.
[[maybe_unused]]
ModuleFile *current_file;
/**
* @brief Compile the specified file into AST
* @param ast Initialized AST module node to build program rules
* @param file The file to be processed
* @return EXIT_SUCCESS in case the parsing was success full anything lese if not
*/
[[nodiscard("AST may be in invalid state")]]
[[gnu::nonnull(1), gnu::nonnull(1)]]
static int compile_file_to_ast(AST_NODE_PTR ast, ModuleFile *file) {
assert(file->path != NULL);
assert(ast != NULL);
file->handle = fopen(file->path, "r");
if (file->handle == NULL) {
INFO("unable to open file: %s", file->path);
print_message(Error, "Cannot open file %s: %s", file->path, strerror(errno));
return EXIT_FAILURE;
}
DEBUG("parsing file: %s", file->path);
// setup global state
root = ast;
current_file = file;
yyin = file->handle;
yyrestart(yyin);
lex_reset();
yyparse();
// clean up global state
// current_file = NULL;
root = NULL;
yyin = NULL;
return EXIT_SUCCESS;
}
/**
* @brief Setup the environment of the target.
* @param target
* @return EXIT_SUCCESS if successful EXIT_FAILURE otherwise
*/
static int setup_target_environment(const TargetConfig *target) {
DEBUG("setting up environment for target: %s", target->name);
assert(target->output_directory != NULL);
assert(target->archive_directory != NULL);
int result = create_directory(target->archive_directory);
if (result != 0 && errno != EEXIST) {
const char *message = get_last_error();
assert(message != NULL);
print_message(Error, "Unable to create directory: %s: %s", target->archive_directory, message);
free((void *) message);
return EXIT_FAILURE;
}
result = create_directory(target->output_directory);
if (result != 0 && errno != EEXIST) {
const char *message = get_last_error();
assert(message != NULL);
print_message(Error, "Unable to create directory: %s: %s", target->output_directory, message);
free((void *) message);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
/**
* @brief Print the supplied AST of the specified target to a graphviz ".gv" file
* @param ast
* @param target
*/
static void print_ast_to_file(const AST_NODE_PTR ast, const TargetConfig *target) {
assert(ast != NULL);
assert(target != NULL);
if (!target->print_ast)
return;
// create file path to write graphviz to
const char *path = make_file_path(target->name, ".gv", 1, target->archive_directory);
FILE *output = fopen((const char *) path, "w");
if (output == NULL) {
const char *message = get_last_error();
print_message(Error, "Unable to open file for syntax tree at: %s: %s", path, message);
free((void *) message);
} else {
AST_fprint_graphviz(output, ast);
fclose(output);
}
free((void *) path);
}
/**
* @brief Build the given target
* @param unit
* @param target
*/
static void build_target(ModuleFileStack *unit, const TargetConfig *target) {
print_message(Info, "Building target: %s", target->name);
AST_NODE_PTR ast = AST_new_node(empty_location(), AST_Module, NULL);
ModuleFile *file = push_file(unit, target->root_module);
if (compile_file_to_ast(ast, file) == EXIT_SUCCESS) {
if (setup_target_environment(target) == 0) {
print_ast_to_file(ast, target);
// TODO: parse AST to semantic values
// TODO: backend codegen
}
}
AST_delete_node(ast);
print_file_statistics(file);
}
/**
* @brief Compile a single file.
* Creates a single target by the given command line arguments.
* @param unit
*/
static void compile_file(ModuleFileStack *unit) {
INFO("compiling basic files...");
TargetConfig *target = default_target_config_from_args();
if (target->root_module == NULL) {
print_message(Error, "No input file specified.");
delete_target_config(target);
return;
}
build_target(unit, target);
delete_target_config(target);
}
/**
* @brief Build all project targets specified by the command line arguments.
* @param unit
* @param config
*/
static void build_project_targets(ModuleFileStack *unit, const ProjectConfig *config) {
if (is_option_set("all")) {
// build all targets in the project
GHashTableIter iter;
g_hash_table_iter_init(&iter, config->targets);
char *key;
TargetConfig *val;
while (g_hash_table_iter_next(&iter, (gpointer) &key, (gpointer) &val)) {
build_target(unit, val);
}
return;
}
// build all targets given in the arguments
GArray* targets = get_non_options_after("build");
if (targets != NULL) {
for (guint i = 0; i < targets->len; i++) {
const char *target_name = (((Option*) targets->data) + i)->string;
if (g_hash_table_contains(config->targets, target_name)) {
build_target(unit, g_hash_table_lookup(config->targets, target_name));
} else {
print_message(Error, "Unknown target: %s", target_name);
}
}
g_array_free(targets, FALSE);
} else {
print_message(Error, "No targets specified.");
}
}
/**
* @brief Build targets from project. Configuration is provided by command line arguments.
* @param unit File storage
*/
static void build_project(ModuleFileStack *unit) {
ProjectConfig *config = default_project_config();
int err = load_project_config(config);
if (err == PROJECT_OK) {
build_project_targets(unit, config);
}
delete_project_config(config);
}
void run_compiler() {
ModuleFileStack files = new_file_stack();
if (is_option_set("build")) {
build_project(&files);
} else if (is_option_set("compile")) {
compile_file(&files);
} else {
print_message(Error, "Invalid mode of operation. Rerun with --help.");
}
if (files.files == NULL) {
print_message(Error, "No input files, nothing to do.");
exit(1);
}
print_unit_statistics(&files);
delete_files(&files);
}

13
src/compiler.h Normal file
View File

@ -0,0 +1,13 @@
//
// Created by servostar on 6/2/24.
//
#ifndef GEMSTONE_COMPILER_H
#define GEMSTONE_COMPILER_H
/**
* @brief Run the gemstone compiler with the provided command arguments.
*/
void run_compiler();
#endif //GEMSTONE_COMPILER_H

View File

@ -7,6 +7,27 @@
#include <assert.h>
#include <sys/col.h>
#ifdef __unix__
#include <sys/stat.h>
#define MAX_PATH_BYTES PATH_MAX
#elif defined(_WIN32) || defined(WIN32)
#include <Windows.h>
#define MAX_PATH_BYTES _MAX_PATH
#endif
ModuleFileStack new_file_stack() {
ModuleFileStack stack;
stack.files = NULL;
return stack;
}
ModuleFile *push_file(ModuleFileStack *stack, const char *path) {
assert(stack != NULL);
@ -27,7 +48,7 @@ ModuleFile *push_file(ModuleFileStack *stack, const char *path) {
void delete_files(ModuleFileStack *stack) {
for (size_t i = 0; i < stack->files->len; i++) {
ModuleFile *file = ((ModuleFile *) stack->files->data) + i;
const ModuleFile *file = (ModuleFile *) stack->files->data + i;
if (file->handle != NULL) {
DEBUG("closing file: %s", file->path);
@ -40,6 +61,8 @@ void delete_files(ModuleFileStack *stack) {
DEBUG("deleted module file stack");
}
// Number of bytes to read at once whilest
// seeking the current line in print_diagnostic()
#define SEEK_BUF_BYTES 256
static inline unsigned long int min(unsigned long int a, unsigned long int b) {
@ -92,22 +115,22 @@ void print_diagnostic(ModuleFile *file, TokenLocation *location, Message kind, c
break;
}
char absolute_path[PATH_MAX];
realpath(file->path, absolute_path);
const char *absolute_path = get_absolute_path(file->path);
printf("%s%s:%ld:%s %s%s:%s %s\n", BOLD, absolute_path, location->line_start, RESET, accent_color, kind_text, RESET,
message);
size_t lines = location->line_end - location->line_start + 1;
free((void *) absolute_path);
const size_t lines = location->line_end - location->line_start + 1;
for (size_t l = 0; l < lines; l++) {
printf(" %4ld | ", location->line_start + l);
size_t limit;
size_t chars = 0;
// print line before token group start
limit = min(location->col_start, SEEK_BUF_BYTES);
size_t limit = min(location->col_start, SEEK_BUF_BYTES);
while (limit > 1) {
custom_fgets(buffer, (int) limit, file->handle);
chars += printf("%s", buffer);
@ -217,7 +240,11 @@ void print_unit_statistics(ModuleFileStack *file_stack) {
stats.error_count += file->statistics.error_count;
}
printf("%d files generated ", file_stack->files->len);
if (stats.info_count + stats.warning_count + stats.error_count < 1) {
return;
}
printf("%d file(s) generated ", file_stack->files->len);
if (stats.info_count > 0) {
printf("%ld notice(s) ", stats.info_count);
@ -233,3 +260,97 @@ void print_unit_statistics(ModuleFileStack *file_stack) {
printf("\n\n");
}
void print_message(Message kind, const char *fmt, ...) {
const char *accent_color = RESET;
const char *kind_text = "unknown";
switch (kind) {
case Info:
kind_text = "info";
accent_color = CYAN;
break;
case Warning:
kind_text = "warning";
accent_color = YELLOW;
break;
case Error:
kind_text = "error";
accent_color = RED;
break;
}
va_list args;
va_start(args, fmt);
printf("%s%s:%s ", accent_color, kind_text, RESET);
vprintf(fmt, args);
printf("\n");
va_end(args);
}
int create_directory(const char *path) {
assert(path != NULL);
DEBUG("creating directory: %s", path);
int result;
#ifdef __unix__
result = mkdir(path, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH);
#elif defined(_WIN32) || defined(WIN32)
result = _mkdir(path);
#endif
return result;
}
const char *get_last_error() {
return strdup(strerror(errno));
}
const char *get_absolute_path(const char *path) {
assert(path != NULL);
DEBUG("resolving absolute path of: %s", path);
#ifdef __unix__
// use unix specific function
char absolute_path[MAX_PATH_BYTES];
realpath(path, absolute_path);
#elif defined(_WIN32) || defined(WIN32)
// use Windows CRT specific function
char absolute_path[MAX_PATH_BYTES];
_fullpath(path, absolute_path, _MAX_PATH);
#endif
return strdup(absolute_path);
}
const char* make_file_path(const char* name, const char* ext, int count, ...) {
DEBUG("building file path...");
va_list args;
va_start(args, count); // Initialize the va_list with the first variadic argument
char* path = calloc(MAX_PATH_BYTES, sizeof(char));
for (int i = 0; i < count; i++) {
const char* arg = va_arg(args, const char*);
assert(arg != NULL);
strcat(path, arg);
strcat(path, PATH_SEPARATOR);
}
va_end(args); // Clean up the va_list
if (name != NULL) {
strcat(path, name);
}
if (name != NULL) {
strcat(path, ext);
}
return path;
}

View File

@ -8,6 +8,12 @@
#include <stdio.h>
#include <glib.h>
#if defined(WIN32) || defined(_WIN32)
#define PATH_SEPARATOR "\\"
#else
#define PATH_SEPARATOR "/"
#endif
typedef struct FileDiagnosticStatistics_t {
size_t error_count;
size_t warning_count;
@ -37,6 +43,12 @@ typedef struct TokenLocation_t {
unsigned long int col_end;
} TokenLocation;
/**
* @brief Create a new, empty file stack.
* @return
*/
ModuleFileStack new_file_stack();
/**
* @brief Add a new file to the file stack.
* @attention The file handle returned will be invalid
@ -65,15 +77,80 @@ void delete_files(ModuleFileStack *stack);
TokenLocation new_location(unsigned long int line_start, unsigned long int col_start, unsigned long int line_end,
unsigned long int col_end);
/**
* @brief Create a new empty location with all of its contents set to zero
* @return
*/
TokenLocation empty_location(void);
/**
* @brief Prints some diagnostic message to stdout.
* This also print the token group and the attached source as context.
* @param file
* @param location
* @param kind
* @param message
*/
[[gnu::nonnull(1), gnu::nonnull(2)]]
void print_diagnostic(ModuleFile *file, TokenLocation *location, Message kind, const char *message);
[[gnu::nonnull(2)]]
/**
* @brief Print a general message to stdout. Provides no source context like print_diagnostic()
* @param kind
* @param fmt
* @param ...
*/
void print_message(Message kind, const char *fmt, ...);
/**
* @brief Print statistics about a specific file.
* Will print the amount of infos, warning and errors emitted during compilation.
* @param file
*/
[[gnu::nonnull(1)]]
void print_file_statistics(ModuleFile *file);
/**
* @brief Print statistics of all files in the module stack.
* @param file_stack
*/
[[gnu::nonnull(1)]]
void print_unit_statistics(ModuleFileStack *file_stack);
/**
* @brief Create a new directory. Will return EEXISTS in case the directory already exists.
* @param path
* @return 0 if successful, anything else otherwise
*/
[[gnu::nonnull(1)]]
int create_directory(const char* path);
/**
* @brief Get a string describing the last error set by errno.
* @return a string that must be freed
*/
[[nodiscard("pointer must be freed")]]
const char* get_last_error();
/**
* @brief Resolve the absolute path from a given relative path.
* @param path
* @return
*/
[[gnu::nonnull(1)]]
[[nodiscard("pointer must be freed")]]
const char* get_absolute_path(const char* path);
/**
* @brief Create a file path from a base name, extension a variable amount of directory path segments.
* @param count Amount of path segments to prepend to the basename
* @param name Basename of the file
* @param ext Extension of the file
* @param ... Path segments without path separator
* @return A relative path of a file
*/
[[nodiscard("pointer must be freed")]]
const char* make_file_path(const char* name, const char* ext, int count, ...);
#endif //GEMSTONE_FILES_H

View File

@ -1,55 +1,10 @@
#include <ast/ast.h>
#include <stdlib.h>
#include <sys/log.h>
#include <yacc/parser.tab.h>
#include <sys/col.h>
#include <lex/util.h>
#include <io/files.h>
#include <assert.h>
extern void yyrestart(FILE *);
[[maybe_unused]]
AST_NODE_PTR root;
[[maybe_unused]]
ModuleFile *current_file;
/**
* @brief Compile the specified file into AST
* @param ast
* @param file
* @return EXIT_SUCCESS in case the parsing was success full anything lese if not
*/
[[nodiscard("AST may be in invalid state")]]
[[gnu::nonnull(1), gnu::nonnull(1)]]
static size_t compile_file_to_ast(AST_NODE_PTR ast, ModuleFile *file) {
assert(file->path != NULL);
assert(ast != NULL);
file->handle = fopen(file->path, "r");
if (file->handle == NULL) {
INFO("unable to open file: %s", file->path);
return 1;
}
DEBUG("parsing file: %s", file->path);
// setup global state
root = ast;
current_file = file;
yyin = file->handle;
yyrestart(yyin);
lex_reset();
yyparse();
// clean up global state
// current_file = NULL;
root = NULL;
yyin = NULL;
return 0;
}
#include <cfg/opt.h>
#include <compiler.h>
/**
* @brief Log a debug message to inform about beginning exit procedures
@ -57,23 +12,13 @@ static size_t compile_file_to_ast(AST_NODE_PTR ast, ModuleFile *file) {
*/
void notify_exit(void) { DEBUG("Exiting gemstone..."); }
/**
* @brief Closes File after compiling.
*
*/
void close_file(void) {
if (NULL != yyin) {
fclose(yyin);
}
}
/**
* @brief Run compiler setup here
*
*/
void setup(void) {
void setup(int argc, char *argv[]) {
// setup preample
parse_options(argc, argv);
log_init();
DEBUG("starting gemstone...");
@ -93,42 +38,24 @@ void setup(void) {
}
int main(int argc, char *argv[]) {
setup();
atexit(close_file);
ModuleFileStack files;
files.files = NULL;
for (int i = 1; i < argc; i++) {
printf("Compiling file: %s\n\n", argv[i]);
TokenLocation location = {
.line_start = 0,
.line_end = 0,
.col_start = 0,
.col_end = 0
};
AST_NODE_PTR ast = AST_new_node(location, AST_Module, NULL);
ModuleFile *file = push_file(&files, argv[i]);
if (compile_file_to_ast(ast, file) == EXIT_SUCCESS) {
// TODO: parse AST to semantic values
// TODO: backend codegen
}
AST_delete_node(ast);
print_file_statistics(file);
}
if (files.files == NULL) {
printf("No input files, nothing to do.\n\n");
if (argc <= 1) {
print_help();
exit(1);
}
print_unit_statistics(&files);
setup(argc, argv);
if (is_option_set("help")) {
print_help();
exit(0);
}
if (is_option_set("version")) {
printf("Running GSC version %s\n", GSC_VERSION);
exit(0);
}
run_compiler();
delete_files(&files);
return 0;
}

View File

@ -4,14 +4,29 @@
#include <stdlib.h>
#include <sys/log.h>
#include <assert.h>
#include <string.h>
#include <cfg/opt.h>
static struct Logger_t {
FILE** streams;
size_t stream_count;
} GlobalLogger;
void log_init(void)
int runtime_log_level = LOG_LEVEL_WARNING;
void set_log_level(int level)
{
runtime_log_level = level;
}
void log_init()
{
if (is_option_set("verbose")) {
set_log_level(LOG_LEVEL_INFORMATION);
} else if (is_option_set("debug")) {
set_log_level(LOG_LEVEL_DEBUG);
}
assert(LOG_DEFAULT_STREAM != NULL);
log_register_stream(LOG_DEFAULT_STREAM);
}

View File

@ -53,12 +53,22 @@ will not print.
#define INFO(format, ...) __LOG(LOG_STRING_INFORMATION, LOG_LEVEL_INFORMATION, format"\n", ##__VA_ARGS__)
#define DEBUG(format, ...) __LOG(LOG_STRING_DEBUG, LOG_LEVEL_DEBUG, format"\n", ##__VA_ARGS__)
extern int runtime_log_level;
#define __LOG(level, priority, format, ...) \
do { \
if (LOG_LEVEL <= priority) \
if (runtime_log_level <= priority) \
syslog_logf(level, __FILE_NAME__, __LINE__, __func__, format, ##__VA_ARGS__); \
} while(0)
/**
* @brief Set the runtime log level. Must be one of: LOG_LEVEL_ERROR, LOG_LEVEL_WARNING,
* LOG_LEVEL_INFORMATION, LOG_LEVEL_DEBUG
* @param level the new log level
*/
void set_log_level(int level);
/**
* @brief Log a message into all registered streams
*

View File

@ -10,3 +10,4 @@ add_subdirectory(logging)
add_subdirectory(input_file)
add_subdirectory(ast)
add_subdirectory(glib)
add_subdirectory(project)

View File

@ -10,6 +10,12 @@ find_package(PkgConfig REQUIRED)
pkg_search_module(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
include_directories(PRIVATE ${GLIB_INCLUDE_DIRS})
# ------------------------------------------------ #
# Setup TOML-C99 #
# ------------------------------------------------ #
include_directories(${PROJECT_SOURCE_DIR}/dep/tomlc99)
# ------------------------------------------------------- #
# CTEST 1
# test building the syntax tree
@ -19,6 +25,8 @@ add_executable(ast_build_tree
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/io/files.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/dep/tomlc99/toml.c
build_tree.c)
set_target_properties(ast_build_tree
PROPERTIES
@ -38,6 +46,8 @@ add_executable(ast_print_node
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/io/files.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/dep/tomlc99/toml.c
print_node.c)
set_target_properties(ast_print_node
PROPERTIES
@ -57,6 +67,8 @@ add_executable(ast_graphviz
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/io/files.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/dep/tomlc99/toml.c
print_graphviz.c)
set_target_properties(ast_graphviz
PROPERTIES

View File

@ -12,7 +12,7 @@ def check_accept():
test_file_name = sys.argv[1]
p = subprocess.run(["./gsc", test_file_name], capture_output=True, text=True)
p = subprocess.run(["./gsc", "compile", test_file_name], capture_output=True, text=True)
assert p.returncode == 0
@ -22,7 +22,7 @@ def check_abort():
logging.basicConfig(level=logging.INFO)
p = subprocess.run("./gsc", capture_output=True, text=True)
p = subprocess.run(["./gsc", "compile"], capture_output=True, text=True)
assert p.returncode == 1

View File

@ -8,6 +8,13 @@ include_directories(${PROJECT_SOURCE_DIR}/src)
find_package(PkgConfig REQUIRED)
pkg_search_module(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
include_directories(PRIVATE ${GLIB_INCLUDE_DIRS})
# ------------------------------------------------ #
# Setup TOML-C99 #
# ------------------------------------------------ #
include_directories(${PROJECT_SOURCE_DIR}/dep/tomlc99)
# ------------------------------------------------------- #
# CTEST 1
@ -16,11 +23,15 @@ pkg_search_module(GLIB REQUIRED IMPORTED_TARGET glib-2.0)
add_executable(logging_output
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/src/io/files.c
output.c)
set_target_properties(logging_output
PROPERTIES
OUTPUT_NAME "output"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
target_link_libraries(logging_output PkgConfig::GLIB)
target_link_libraries(logging_output tomlc99)
add_test(NAME logging_output
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_output)
@ -32,11 +43,15 @@ add_test(NAME logging_output
add_executable(logging_panic
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/src/io/files.c
panic.c)
set_target_properties(logging_panic
PROPERTIES
OUTPUT_NAME "panic"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
target_link_libraries(logging_panic PkgConfig::GLIB)
target_link_libraries(logging_panic tomlc99)
add_test(NAME logging_panic
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_panic)
@ -48,11 +63,15 @@ add_test(NAME logging_panic
add_executable(logging_streams
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/src/io/files.c
streams.c)
set_target_properties(logging_streams
PROPERTIES
OUTPUT_NAME "stream"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
target_link_libraries(logging_streams PkgConfig::GLIB)
target_link_libraries(logging_streams tomlc99)
add_test(NAME logging_streams
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_stream)
@ -64,11 +83,15 @@ add_test(NAME logging_streams
add_executable(logging_level
${PROJECT_SOURCE_DIR}/src/sys/log.c
${PROJECT_SOURCE_DIR}/src/sys/col.c
${PROJECT_SOURCE_DIR}/src/cfg/opt.c
${PROJECT_SOURCE_DIR}/src/io/files.c
level.c)
set_target_properties(logging_level
PROPERTIES
OUTPUT_NAME "level"
RUNTIME_OUTPUT_DIRECTORY ${GEMSTONE_BINARY_DIR}/tests/logging)
target_link_libraries(logging_level PkgConfig::GLIB)
target_link_libraries(logging_level tomlc99)
add_test(NAME logging_level
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMAND python ${GEMSTONE_TEST_DIR}/logging/test_logging.py check_level)

View File

@ -4,11 +4,14 @@
#include "sys/log.h"
#include <sys/col.h>
#include <cfg/opt.h>
#define LOG_LEVEL LOG_LEVEL_WARNING
int main(void) {
int main(int argc, char* argv[]) {
parse_options(argc, argv);
log_init();
set_log_level(LOG_LEVEL_DEBUG);
col_init();
DEBUG("logging some debug...");

View File

@ -4,9 +4,12 @@
#include "sys/log.h"
#include <sys/col.h>
#include <cfg/opt.h>
int main(void) {
int main(int argc, char* argv[]) {
parse_options(argc, argv);
log_init();
set_log_level(LOG_LEVEL_DEBUG);
col_init();
DEBUG("logging some debug...");

View File

@ -3,9 +3,12 @@
//
#include "sys/log.h"
#include <cfg/opt.h>
int main(void) {
int main(int argc, char* argv[]) {
parse_options(argc, argv);
log_init();
set_log_level(LOG_LEVEL_DEBUG);
// this should appear in stderr
INFO("before exit");

View File

@ -4,6 +4,7 @@
#include "sys/log.h"
#include <stdlib.h>
#include <cfg/opt.h>
static FILE* file;
@ -13,8 +14,10 @@ void close_file(void) {
}
}
int main(void) {
int main(int argc, char* argv[]) {
parse_options(argc, argv);
log_init();
set_log_level(LOG_LEVEL_DEBUG);
// this should appear in stderr
INFO("should only be in stderr");

View File

@ -0,0 +1,9 @@
include(CTest)
# ------------------------------------------------------- #
# CTEST 1
# test if the program successfully reads the project config
add_test(NAME project_build
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests/project
COMMAND python ${GEMSTONE_TEST_DIR}/project/test_project.py)

15
tests/project/build.toml Normal file
View File

@ -0,0 +1,15 @@
[project]
authors = [ "Sven Vogel" ]
license = "GPL-2.0"
version = "0.1.0"
description = "This is a test project"
[target.debug]
root = "src/main.txt"
output = "bin"
archive = "archive"
opt = 1
print_ast = true
print_asm = true
print_ir = true
mode = "application"

View File

@ -0,0 +1,4 @@
fun main(out int: e) {
e = 0
}

View File

@ -0,0 +1,15 @@
import subprocess
import logging
from logging import info, error
if __name__ == "__main__":
info("started check output...")
p = subprocess.run([ "../../bin/debug/gsc", "build", "all" ], capture_output=True, text=True)
info("checking exit code...")
print(p.stdout)
# check exit code
assert p.returncode == 0