diff options
-rwxr-xr-x | scripts/build_debug.sh | 4 | ||||
-rwxr-xr-x | scripts/build_release.sh | 4 | ||||
-rw-r--r-- | src/cli/main.c | 161 | ||||
-rw-r--r-- | src/core/interpret.c | 19 | ||||
-rw-r--r-- | src/core/interpret.h | 20 | ||||
-rw-r--r-- | src/core/state.c | 11 | ||||
-rw-r--r-- | src/core/state.h | 25 |
7 files changed, 236 insertions, 8 deletions
diff --git a/scripts/build_debug.sh b/scripts/build_debug.sh index fcdf16a..bf5f42b 100755 --- a/scripts/build_debug.sh +++ b/scripts/build_debug.sh @@ -1,5 +1,5 @@ #!/bin/sh set -e -u -o pipefail mkdir -p bin/debug -clang -std=c23 -ggdb -O0 -Wall -Wextra -fsanitize=undefined,address -o bin/debug/sand src/{cli,core}/*.c -clang -std=c23 -ggdb -O0 -Wall -Wextra -fsanitize=undefined,address -o bin/debug/unit src/{unit,core}/*.c +clang -std=c23 -D_POSIX_C_SOURCE=200809L -ggdb -O0 -Wall -Wextra -fsanitize=undefined,address -o bin/debug/sand src/{cli,core}/*.c +clang -std=c23 -D_POSIX_C_SOURCE=200809L -ggdb -O0 -Wall -Wextra -fsanitize=undefined,address -o bin/debug/unit src/{unit,core}/*.c diff --git a/scripts/build_release.sh b/scripts/build_release.sh index 6f42aed..cb1ee8b 100755 --- a/scripts/build_release.sh +++ b/scripts/build_release.sh @@ -1,5 +1,5 @@ #!/bin/sh set -e -u -o pipefail mkdir -p bin/release -clang -std=c23 -O3 -Wall -Wextra -o bin/release/sand src/{cli,core}/*.c -clang -std=c23 -O3 -Wall -Wextra -o bin/release/unit src/{unit,core}/*.c +clang -std=c23 -D_POSIX_C_SOURCE=200809L -O3 -Wall -Wextra -o bin/release/sand src/{cli,core}/*.c +clang -std=c23 -D_POSIX_C_SOURCE=200809L -O3 -Wall -Wextra -o bin/release/unit src/{unit,core}/*.c diff --git a/src/cli/main.c b/src/cli/main.c index 4d96aa2..db42a14 100644 --- a/src/cli/main.c +++ b/src/cli/main.c @@ -1,13 +1,166 @@ #include "../core/state.h" +#include "../core/interpret.h" +#include <assert.h> #include <stdio.h> #include <stdlib.h> +#include <string.h> +#include <stdbool.h> +#include <errno.h> + +int read_file(const char *file_path, char **out_buffer, size_t *out_length) { + assert(out_buffer != NULL && *out_buffer == NULL); + assert(file_path != NULL); + + const size_t CHUNK_SIZE = 1024; + + FILE *fp = fopen(file_path, "r"); + if (fp == NULL) { + return -1; + } + + char *buffer = NULL; + size_t allocated = 0; + size_t used = 0; + while (true) { + // Grow buffer, if needed. + if (used + CHUNK_SIZE > allocated) { + // Grow exponentially to guarantee O(log(n)) performance. + allocated = (allocated == 0) ? CHUNK_SIZE : allocated * 2; + + // Overflow check. Some ANSI C compilers may optimize this away, though. + if (allocated <= used) { + free(buffer); + fclose(fp); + errno = EOVERFLOW; + return -1; + } + + char *temp = realloc(buffer, allocated); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + } + + size_t nread = fread(buffer + used, 1, CHUNK_SIZE, fp); + if (nread == 0) { + // End-of-file or errnor has occured. + // FIXME: Should we be checking (nread < CHUNK_SIZE)? + // https://stackoverflow.com/a/39322170 + break; + } + used += nread; + } + + if (ferror(fp)) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + + // Reallocate to optimal size. + char *temp = realloc(buffer, used + 1); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + + // Null-terminate the buffer. Note that buffers may still contain \0, + // so strlen(buffer) == length may not be true. + buffer[used] = '\0'; + + // Return buffer. + *out_buffer = buffer; + if (out_length != NULL) { + *out_length = used; + } + fclose(fp); + return 0; +} + +static void run_file(SandState *S, const char *filename) { + char *source = NULL; + size_t source_length = 0; + if (read_file(filename, &source, &source_length) < 0) { + fprintf(stderr, "Error reading %s: %s\n", filename, strerror(errno)); + exit(74); + } + + SandInterpretResult result = sand_interpret(S, filename, source, source_length); + free(source); // At this point it should be okay to invalidate all references to the source code. + + if (result == SAND_INTERPRET_COMPILE_ERROR) { + exit(65); + } else if (result == SAND_INTERPRET_RUNTIME_ERROR) { + exit(70); + } +} + +static void repl(SandState *S) { + char *line = NULL; + size_t line_length = 0; + + unsigned prompt_nr = 1; + + while (true) { + printf("> "); + fflush(stdout); + + // At this point the memory at `line` is reused, invalidating all existing references. + if (getline(&line, &line_length, stdin) < 0) { + fputc('\n', stdout); + break; // No more input - leave REPL loop. + } + + // We need a dummy filename. + char filename[64]; + snprintf(filename, sizeof(filename), "<repl %u>", prompt_nr); + prompt_nr += 1; + + sand_interpret(S, filename, line, line_length); + } +} + +static void print_help(FILE *stream, const char *argv0) { + fprintf(stream, "Usage: %s <filename>\n", argv0); + fprintf(stream, " %s --help\n", argv0); + fprintf(stream, " %s\n\n", argv0); + fprintf(stream, "When called without arguments, runs a REPL.\n"); +} + +static void print_handler(const char *message, size_t message_length) { + fprintf(stderr, message, message_length); +} int main(int argc, char *argv[]) { - (void)argc; - (void)argv; + SandConfig config = { + .print_handler = print_handler, + }; + SandState state = sand_create_state(config); - printf("TODO\n"); + if (argc == 1) { + repl(&state); + } else if (argc == 2 && strcmp(argv[1], "--help") == 0) { + print_help(stdout, argv[0]); + exit(0); + } else if (argc == 2) { + run_file(&state, argv[1]); + } else { + print_help(stderr, argv[0]); + exit(64); + } - return EXIT_SUCCESS; + sand_destroy_state(&state); + return 0; } diff --git a/src/core/interpret.c b/src/core/interpret.c new file mode 100644 index 0000000..bbb238c --- /dev/null +++ b/src/core/interpret.c @@ -0,0 +1,19 @@ +#include "interpret.h" +#include "tokenizer.h" + +#include <stdbool.h> + +SandInterpretResult sand_interpret(SandState *S, const char *filename, const char *source, size_t source_length) { + SandTokenizer tokenizer = sand_create_tokenizer(source, source_length, filename); + + while (true) { + SandToken token = sand_get_next_token(&tokenizer); + sand_print_location(stdout, &token.location); + printf(": %s \"%.*s\"\n", sand_token_kind_to_string(token.kind), (int)token.content_length, token.content); + if (token.kind == SAND_TOKEN_EOF) { + break; + } + } + + return SAND_INTERPRET_OK; +} diff --git a/src/core/interpret.h b/src/core/interpret.h new file mode 100644 index 0000000..8bdc92f --- /dev/null +++ b/src/core/interpret.h @@ -0,0 +1,20 @@ +#ifndef SAND_INTERPRET_H +#define SAND_INTERPRET_H + +// This module defines a single function which composes all the stuff the +// interpreter does. It should be bascially the only function that an embedder +// needs to know. + +#include "state.h" + +#include <stddef.h> + +typedef enum { + SAND_INTERPRET_OK, + SAND_INTERPRET_COMPILE_ERROR, + SAND_INTERPRET_RUNTIME_ERROR, +} SandInterpretResult; + +SandInterpretResult sand_interpret(SandState *, const char *filename, const char *source, size_t source_length); + +#endif diff --git a/src/core/state.c b/src/core/state.c new file mode 100644 index 0000000..91bd9e4 --- /dev/null +++ b/src/core/state.c @@ -0,0 +1,11 @@ +#include "state.h" + +SandState sand_create_state(SandConfig config) { + return (SandState) { + .config = config, + }; +} + +void sand_destroy_state(SandState *S) { + (void) S; +} diff --git a/src/core/state.h b/src/core/state.h new file mode 100644 index 0000000..406a79c --- /dev/null +++ b/src/core/state.h @@ -0,0 +1,25 @@ +#ifndef SAND_STATE_H +#define SAND_STATE_H + +// This module defines the evaluator state. This is the "world" in Sand. +// Multiple evaluator states can coexist as they are totally separate. + +#include <stddef.h> + +typedef void (* SandPrintHandler)(const char *message, size_t length); + +typedef struct { + SandPrintHandler print_handler; +} SandConfig; + +// This data structure should be treated as entirely opaque by consuming code. +typedef struct { + SandConfig config; +} SandState; + + +SandState sand_create_state(SandConfig config); + +void sand_destroy_state(SandState *); + +#endif |