goon
goon
https://git.tonybtw.com/goon.git
git://git.tonybtw.com/goon.git
Initial commit
Diff
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..7d66ca9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+goon
+*.o
+notes/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..4d8540f
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,29 @@
+CC = gcc
+CFLAGS = -Wall -Wextra -O2 -std=c99
+PREFIX = /usr/local
+
+SRC = src/main.c src/goon.c
+OBJ = $(SRC:.c=.o)
+
+all: goon
+
+goon: $(SRC)
+ $(CC) $(CFLAGS) -o $@ $(SRC)
+
+debug: CFLAGS = -Wall -Wextra -g -std=c99
+debug: goon
+
+install: goon
+ install -Dm755 goon $(DESTDIR)$(PREFIX)/bin/goon
+
+uninstall:
+ rm -f $(DESTDIR)$(PREFIX)/bin/goon
+
+clean:
+ rm -f goon $(OBJ)
+
+test: goon
+ @echo "Running tests..."
+ @./tests/run_tests.sh
+
+.PHONY: all debug install uninstall clean test
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c74bacd
--- /dev/null
+++ b/README.md
@@ -0,0 +1,56 @@
+# Goon
+
+A 1k line embeddable configuration language with Nix-like syntax.
+
+## Example
+
+```goon
+let name = "goon";
+let version = 1;
+
+let colors = import("./colors");
+
+{
+ name = name;
+ version = version;
+
+ border = {
+ width = 2;
+ color = colors.blue;
+ };
+
+ tags = ["1", "2", "3", "4", "5"];
+}
+```
+
+## CLI Options
+
+```bash
+goon eval config.goon
+goon check config.goon
+```
+
+## Embedding
+
+Copy `src/goon.c` and `src/goon.h` into your project.
+
+```c
+#include "goon.h"
+
+int main() {
+ Goon_Ctx *ctx = goon_create();
+
+ if (goon_load_file(ctx, "config.goon")) {
+ Goon_Value *result = goon_eval_result(ctx);
+ Goon_Value *name = goon_record_get(result, "name");
+ if (goon_is_string(name)) {
+ printf("name: %s\n", goon_to_string(name));
+ }
+ } else {
+ printf("error: %s\n", goon_get_error(ctx));
+ }
+
+ goon_destroy(ctx);
+ return 0;
+}
+```
diff --git a/examples/simple.goon b/examples/simple.goon
new file mode 100644
index 0000000..99983dd
--- /dev/null
+++ b/examples/simple.goon
@@ -0,0 +1,13 @@
+let name = "goon";
+let version = 1;
+
+{
+ name = name;
+ version = version;
+ enabled = true;
+ tags = ["web", "cli", "config"];
+ nested = {
+ foo = 42;
+ bar = "hello";
+ };
+}
diff --git a/src/goon.c b/src/goon.c
new file mode 100644
index 0000000..de279c6
--- /dev/null
+++ b/src/goon.c
@@ -0,0 +1,1192 @@
+#define _POSIX_C_SOURCE 200809L
+#include "goon.h"
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <libgen.h>
+
+typedef enum {
+ TOK_EOF,
+ TOK_LBRACE,
+ TOK_RBRACE,
+ TOK_LBRACKET,
+ TOK_RBRACKET,
+ TOK_LPAREN,
+ TOK_RPAREN,
+ TOK_SEMICOLON,
+ TOK_COMMA,
+ TOK_EQUALS,
+ TOK_COLON,
+ TOK_QUESTION,
+ TOK_SPREAD,
+ TOK_DOT,
+ TOK_INT,
+ TOK_STRING,
+ TOK_IDENT,
+ TOK_TRUE,
+ TOK_FALSE,
+ TOK_LET,
+ TOK_IF,
+ TOK_THEN,
+ TOK_ELSE,
+ TOK_IMPORT,
+} Token_Type;
+
+typedef struct {
+ Token_Type type;
+ union {
+ int64_t integer;
+ char *string;
+ } data;
+} Token;
+
+typedef struct {
+ const char *src;
+ size_t pos;
+ size_t len;
+ Token current;
+ char *error;
+} Lexer;
+
+static void lexer_init(Lexer *lex, const char *src) {
+ lex->src = src;
+ lex->pos = 0;
+ lex->len = strlen(src);
+ lex->current.type = TOK_EOF;
+ lex->current.data.string = NULL;
+ lex->error = NULL;
+}
+
+static void lexer_skip_whitespace(Lexer *lex) {
+ while (lex->pos < lex->len) {
+ char c = lex->src[lex->pos];
+ if (c == ' ' || c == '\t' || c == '\n' || c == '\r') {
+ lex->pos++;
+ } else if (c == '/' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '/') {
+ lex->pos += 2;
+ while (lex->pos < lex->len && lex->src[lex->pos] != '\n') {
+ lex->pos++;
+ }
+ } else if (c == '/' && lex->pos + 1 < lex->len && lex->src[lex->pos + 1] == '*') {
+ lex->pos += 2;
+ while (lex->pos + 1 < lex->len) {
+ if (lex->src[lex->pos] == '*' && lex->src[lex->pos + 1] == '/') {
+ lex->pos += 2;
+ break;
+ }
+ lex->pos++;
+ }
+ } else {
+ break;
+ }
+ }
+}
+
+static bool is_ident_start(char c) {
+ return isalpha(c) || c == '_';
+}
+
+static bool is_ident_char(char c) {
+ return isalnum(c) || c == '_';
+}
+
+static char *strdup_range(const char *start, size_t len) {
+ char *s = malloc(len + 1);
+ if (!s) return NULL;
+ memcpy(s, start, len);
+ s[len] = '\0';
+ return s;
+}
+
+static bool lexer_next(Lexer *lex) {
+ if (lex->current.type == TOK_STRING || lex->current.type == TOK_IDENT) {
+ free(lex->current.data.string);
+ lex->current.data.string = NULL;
+ }
+
+ lexer_skip_whitespace(lex);
+
+ if (lex->pos >= lex->len) {
+ lex->current.type = TOK_EOF;
+ return true;
+ }
+
+ char c = lex->src[lex->pos];
+
+ if (c == '{') { lex->current.type = TOK_LBRACE; lex->pos++; return true; }
+ if (c == '}') { lex->current.type = TOK_RBRACE; lex->pos++; return true; }
+ if (c == '[') { lex->current.type = TOK_LBRACKET; lex->pos++; return true; }
+ if (c == ']') { lex->current.type = TOK_RBRACKET; lex->pos++; return true; }
+ if (c == '(') { lex->current.type = TOK_LPAREN; lex->pos++; return true; }
+ if (c == ')') { lex->current.type = TOK_RPAREN; lex->pos++; return true; }
+ if (c == ';') { lex->current.type = TOK_SEMICOLON; lex->pos++; return true; }
+ if (c == ',') { lex->current.type = TOK_COMMA; lex->pos++; return true; }
+ if (c == '=') { lex->current.type = TOK_EQUALS; lex->pos++; return true; }
+ if (c == ':') { lex->current.type = TOK_COLON; lex->pos++; return true; }
+ if (c == '?') { lex->current.type = TOK_QUESTION; lex->pos++; return true; }
+
+ if (c == '.' && lex->pos + 2 < lex->len &&
+ lex->src[lex->pos + 1] == '.' && lex->src[lex->pos + 2] == '.') {
+ lex->current.type = TOK_SPREAD;
+ lex->pos += 3;
+ return true;
+ }
+
+ if (c == '.') {
+ lex->current.type = TOK_DOT;
+ lex->pos++;
+ return true;
+ }
+
+ if (c == '"') {
+ lex->pos++;
+ size_t buf_size = 256;
+ size_t buf_len = 0;
+ char *buf = malloc(buf_size);
+ if (!buf) return false;
+
+ while (lex->pos < lex->len && lex->src[lex->pos] != '"') {
+ char ch = lex->src[lex->pos];
+ if (ch == '\\' && lex->pos + 1 < lex->len) {
+ lex->pos++;
+ ch = lex->src[lex->pos];
+ switch (ch) {
+ case 'n': ch = '\n'; break;
+ case 't': ch = '\t'; break;
+ case 'r': ch = '\r'; break;
+ case '\\': ch = '\\'; break;
+ case '"': ch = '"'; break;
+ case '$': ch = '$'; break;
+ default: break;
+ }
+ }
+ if (buf_len + 1 >= buf_size) {
+ buf_size *= 2;
+ buf = realloc(buf, buf_size);
+ if (!buf) return false;
+ }
+ buf[buf_len++] = ch;
+ lex->pos++;
+ }
+
+ if (lex->pos >= lex->len) {
+ free(buf);
+ lex->error = strdup("unterminated string");
+ return false;
+ }
+
+ lex->pos++;
+ buf[buf_len] = '\0';
+ lex->current.type = TOK_STRING;
+ lex->current.data.string = buf;
+ return true;
+ }
+
+ if (isdigit(c) || (c == '-' && lex->pos + 1 < lex->len && isdigit(lex->src[lex->pos + 1]))) {
+ int sign = 1;
+ if (c == '-') {
+ sign = -1;
+ lex->pos++;
+ }
+ int64_t val = 0;
+ while (lex->pos < lex->len && isdigit(lex->src[lex->pos])) {
+ val = val * 10 + (lex->src[lex->pos] - '0');
+ lex->pos++;
+ }
+ lex->current.type = TOK_INT;
+ lex->current.data.integer = val * sign;
+ return true;
+ }
+
+ if (is_ident_start(c)) {
+ size_t start = lex->pos;
+ while (lex->pos < lex->len && is_ident_char(lex->src[lex->pos])) {
+ lex->pos++;
+ }
+ char *ident = strdup_range(lex->src + start, lex->pos - start);
+
+ if (strcmp(ident, "true") == 0) {
+ free(ident);
+ lex->current.type = TOK_TRUE;
+ return true;
+ }
+ if (strcmp(ident, "false") == 0) {
+ free(ident);
+ lex->current.type = TOK_FALSE;
+ return true;
+ }
+ if (strcmp(ident, "let") == 0) {
+ free(ident);
+ lex->current.type = TOK_LET;
+ return true;
+ }
+ if (strcmp(ident, "if") == 0) {
+ free(ident);
+ lex->current.type = TOK_IF;
+ return true;
+ }
+ if (strcmp(ident, "then") == 0) {
+ free(ident);
+ lex->current.type = TOK_THEN;
+ return true;
+ }
+ if (strcmp(ident, "else") == 0) {
+ free(ident);
+ lex->current.type = TOK_ELSE;
+ return true;
+ }
+ if (strcmp(ident, "import") == 0) {
+ free(ident);
+ lex->current.type = TOK_IMPORT;
+ return true;
+ }
+
+ lex->current.type = TOK_IDENT;
+ lex->current.data.string = ident;
+ return true;
+ }
+
+ lex->error = strdup("unexpected character");
+ return false;
+}
+
+static Goon_Value *alloc_value(Goon_Ctx *ctx) {
+ Goon_Value *val = malloc(sizeof(Goon_Value));
+ if (!val) return NULL;
+ val->type = GOON_NIL;
+ val->next_alloc = ctx->values;
+ ctx->values = val;
+ return val;
+}
+
+static Goon_Record_Field *alloc_field(Goon_Ctx *ctx) {
+ Goon_Record_Field *field = malloc(sizeof(Goon_Record_Field));
+ if (!field) return NULL;
+ field->key = NULL;
+ field->value = NULL;
+ field->next = ctx->fields;
+ ctx->fields = field;
+ return field;
+}
+
+Goon_Value *goon_nil(Goon_Ctx *ctx) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_NIL;
+ return val;
+}
+
+Goon_Value *goon_bool(Goon_Ctx *ctx, bool b) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_BOOL;
+ val->data.boolean = b;
+ return val;
+}
+
+Goon_Value *goon_int(Goon_Ctx *ctx, int64_t i) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_INT;
+ val->data.integer = i;
+ return val;
+}
+
+Goon_Value *goon_string(Goon_Ctx *ctx, const char *s) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_STRING;
+ val->data.string = strdup(s);
+ return val;
+}
+
+Goon_Value *goon_list(Goon_Ctx *ctx) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_LIST;
+ val->data.list.items = NULL;
+ val->data.list.len = 0;
+ val->data.list.cap = 0;
+ return val;
+}
+
+Goon_Value *goon_record(Goon_Ctx *ctx) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return NULL;
+ val->type = GOON_RECORD;
+ val->data.record.fields = NULL;
+ return val;
+}
+
+bool goon_is_nil(Goon_Value *val) {
+ return val == NULL || val->type == GOON_NIL;
+}
+
+bool goon_is_bool(Goon_Value *val) {
+ return val != NULL && val->type == GOON_BOOL;
+}
+
+bool goon_is_int(Goon_Value *val) {
+ return val != NULL && val->type == GOON_INT;
+}
+
+bool goon_is_string(Goon_Value *val) {
+ return val != NULL && val->type == GOON_STRING;
+}
+
+bool goon_is_list(Goon_Value *val) {
+ return val != NULL && val->type == GOON_LIST;
+}
+
+bool goon_is_record(Goon_Value *val) {
+ return val != NULL && val->type == GOON_RECORD;
+}
+
+bool goon_to_bool(Goon_Value *val) {
+ if (val == NULL) return false;
+ if (val->type == GOON_BOOL) return val->data.boolean;
+ if (val->type == GOON_NIL) return false;
+ return true;
+}
+
+int64_t goon_to_int(Goon_Value *val) {
+ if (val == NULL || val->type != GOON_INT) return 0;
+ return val->data.integer;
+}
+
+const char *goon_to_string(Goon_Value *val) {
+ if (val == NULL || val->type != GOON_STRING) return NULL;
+ return val->data.string;
+}
+
+void goon_list_push(Goon_Ctx *ctx, Goon_Value *list, Goon_Value *item) {
+ (void)ctx;
+ if (!list || list->type != GOON_LIST) return;
+ if (list->data.list.len >= list->data.list.cap) {
+ size_t new_cap = list->data.list.cap == 0 ? 8 : list->data.list.cap * 2;
+ Goon_Value **new_items = realloc(list->data.list.items, new_cap * sizeof(Goon_Value *));
+ if (!new_items) return;
+ list->data.list.items = new_items;
+ list->data.list.cap = new_cap;
+ }
+ list->data.list.items[list->data.list.len++] = item;
+}
+
+size_t goon_list_len(Goon_Value *list) {
+ if (!list || list->type != GOON_LIST) return 0;
+ return list->data.list.len;
+}
+
+Goon_Value *goon_list_get(Goon_Value *list, size_t index) {
+ if (!list || list->type != GOON_LIST) return NULL;
+ if (index >= list->data.list.len) return NULL;
+ return list->data.list.items[index];
+}
+
+void goon_record_set(Goon_Ctx *ctx, Goon_Value *record, const char *key, Goon_Value *value) {
+ if (!record || record->type != GOON_RECORD) return;
+
+ Goon_Record_Field *f = record->data.record.fields;
+ while (f) {
+ if (strcmp(f->key, key) == 0) {
+ f->value = value;
+ return;
+ }
+ f = f->next;
+ }
+
+ Goon_Record_Field *field = alloc_field(ctx);
+ if (!field) return;
+ field->key = strdup(key);
+ field->value = value;
+ field->next = record->data.record.fields;
+ record->data.record.fields = field;
+}
+
+Goon_Value *goon_record_get(Goon_Value *record, const char *key) {
+ if (!record || record->type != GOON_RECORD) return NULL;
+ Goon_Record_Field *f = record->data.record.fields;
+ while (f) {
+ if (strcmp(f->key, key) == 0) {
+ return f->value;
+ }
+ f = f->next;
+ }
+ return NULL;
+}
+
+Goon_Record_Field *goon_record_fields(Goon_Value *record) {
+ if (!record || record->type != GOON_RECORD) return NULL;
+ return record->data.record.fields;
+}
+
+static Goon_Value *lookup(Goon_Ctx *ctx, const char *name) {
+ Goon_Binding *b = ctx->env;
+ while (b) {
+ if (strcmp(b->name, name) == 0) {
+ return b->value;
+ }
+ b = b->next;
+ }
+ return NULL;
+}
+
+static void define(Goon_Ctx *ctx, const char *name, Goon_Value *value) {
+ Goon_Binding *b = ctx->env;
+ while (b) {
+ if (strcmp(b->name, name) == 0) {
+ b->value = value;
+ return;
+ }
+ b = b->next;
+ }
+ b = malloc(sizeof(Goon_Binding));
+ if (!b) return;
+ b->name = strdup(name);
+ b->value = value;
+ b->next = ctx->env;
+ ctx->env = b;
+}
+
+typedef struct {
+ Goon_Ctx *ctx;
+ Lexer *lex;
+} Parser;
+
+static Goon_Value *parse_expr(Parser *p);
+
+static Goon_Value *interpolate_string(Goon_Ctx *ctx, const char *str) {
+ size_t len = strlen(str);
+ size_t buf_size = len * 2 + 1;
+ char *buf = malloc(buf_size);
+ if (!buf) return goon_string(ctx, str);
+
+ size_t buf_len = 0;
+ size_t i = 0;
+
+ while (i < len) {
+ if (str[i] == '$' && i + 1 < len && str[i + 1] == '{') {
+ i += 2;
+ size_t var_start = i;
+ while (i < len && str[i] != '}') {
+ i++;
+ }
+ if (i < len) {
+ char *var_name = strdup_range(str + var_start, i - var_start);
+ Goon_Value *val = lookup(ctx, var_name);
+ free(var_name);
+
+ if (val) {
+ const char *insert = NULL;
+ char num_buf[32];
+ if (val->type == GOON_STRING) {
+ insert = val->data.string;
+ } else if (val->type == GOON_INT) {
+ snprintf(num_buf, sizeof(num_buf), "%ld", val->data.integer);
+ insert = num_buf;
+ } else if (val->type == GOON_BOOL) {
+ insert = val->data.boolean ? "true" : "false";
+ }
+ if (insert) {
+ size_t insert_len = strlen(insert);
+ while (buf_len + insert_len >= buf_size) {
+ buf_size *= 2;
+ buf = realloc(buf, buf_size);
+ }
+ memcpy(buf + buf_len, insert, insert_len);
+ buf_len += insert_len;
+ }
+ }
+ i++;
+ }
+ } else {
+ if (buf_len + 1 >= buf_size) {
+ buf_size *= 2;
+ buf = realloc(buf, buf_size);
+ }
+ buf[buf_len++] = str[i++];
+ }
+ }
+
+ buf[buf_len] = '\0';
+ Goon_Value *result = goon_string(ctx, buf);
+ free(buf);
+ return result;
+}
+
+static Goon_Value *parse_record(Parser *p) {
+ Goon_Value *record = goon_record(p->ctx);
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ while (p->lex->current.type != TOK_RBRACE && p->lex->current.type != TOK_EOF) {
+ if (p->lex->current.type == TOK_SPREAD) {
+ if (!lexer_next(p->lex)) return NULL;
+ Goon_Value *spread_val = parse_expr(p);
+ if (spread_val && spread_val->type == GOON_RECORD) {
+ Goon_Record_Field *f = spread_val->data.record.fields;
+ while (f) {
+ goon_record_set(p->ctx, record, f->key, f->value);
+ f = f->next;
+ }
+ }
+ if (p->lex->current.type == TOK_COMMA) {
+ if (!lexer_next(p->lex)) return NULL;
+ } else if (p->lex->current.type == TOK_SEMICOLON) {
+ if (!lexer_next(p->lex)) return NULL;
+ }
+ continue;
+ }
+
+ if (p->lex->current.type != TOK_IDENT) {
+ p->lex->error = strdup("expected field name");
+ return NULL;
+ }
+
+ char *key = strdup(p->lex->current.data.string);
+ if (!lexer_next(p->lex)) { free(key); return NULL; }
+
+ if (p->lex->current.type == TOK_COLON) {
+ if (!lexer_next(p->lex)) { free(key); return NULL; }
+ if (!lexer_next(p->lex)) { free(key); return NULL; }
+ }
+
+ if (p->lex->current.type != TOK_EQUALS) {
+ p->lex->error = strdup("expected = after field name");
+ free(key);
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) { free(key); return NULL; }
+
+ Goon_Value *value = parse_expr(p);
+ if (!value) { free(key); return NULL; }
+
+ goon_record_set(p->ctx, record, key, value);
+ free(key);
+
+ if (p->lex->current.type == TOK_SEMICOLON) {
+ if (!lexer_next(p->lex)) return NULL;
+ } else if (p->lex->current.type == TOK_COMMA) {
+ if (!lexer_next(p->lex)) return NULL;
+ }
+ }
+
+ if (p->lex->current.type != TOK_RBRACE) {
+ p->lex->error = strdup("expected }");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+ return record;
+}
+
+static Goon_Value *parse_list(Parser *p) {
+ Goon_Value *list = goon_list(p->ctx);
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ while (p->lex->current.type != TOK_RBRACKET && p->lex->current.type != TOK_EOF) {
+ if (p->lex->current.type == TOK_SPREAD) {
+ if (!lexer_next(p->lex)) return NULL;
+ Goon_Value *spread_val = parse_expr(p);
+ if (spread_val && spread_val->type == GOON_LIST) {
+ for (size_t i = 0; i < spread_val->data.list.len; i++) {
+ goon_list_push(p->ctx, list, spread_val->data.list.items[i]);
+ }
+ }
+ } else {
+ Goon_Value *item = parse_expr(p);
+ if (!item) return NULL;
+ goon_list_push(p->ctx, list, item);
+ }
+
+ if (p->lex->current.type == TOK_COMMA) {
+ if (!lexer_next(p->lex)) return NULL;
+ }
+ }
+
+ if (p->lex->current.type != TOK_RBRACKET) {
+ p->lex->error = strdup("expected ]");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+ return list;
+}
+
+static Goon_Value *parse_import(Parser *p) {
+ if (!lexer_next(p->lex)) return NULL;
+
+ if (p->lex->current.type != TOK_LPAREN) {
+ p->lex->error = strdup("expected ( after import");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ if (p->lex->current.type != TOK_STRING) {
+ p->lex->error = strdup("expected string path in import");
+ return NULL;
+ }
+
+ char *path = strdup(p->lex->current.data.string);
+ if (!lexer_next(p->lex)) { free(path); return NULL; }
+
+ if (p->lex->current.type != TOK_RPAREN) {
+ p->lex->error = strdup("expected ) after import path");
+ free(path);
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) { free(path); return NULL; }
+
+ char full_path[1024];
+ if (path[0] == '.' && p->ctx->base_path) {
+ char *base_copy = strdup(p->ctx->base_path);
+ char *dir = dirname(base_copy);
+ snprintf(full_path, sizeof(full_path), "%s/%s", dir, path);
+ free(base_copy);
+ } else if (p->ctx->base_path && path[0] != '/') {
+ char *base_copy = strdup(p->ctx->base_path);
+ char *dir = dirname(base_copy);
+ snprintf(full_path, sizeof(full_path), "%s/%s", dir, path);
+ free(base_copy);
+ } else {
+ snprintf(full_path, sizeof(full_path), "%s", path);
+ }
+
+ size_t plen = strlen(full_path);
+ if (plen < 5 || strcmp(full_path + plen - 5, ".goon") != 0) {
+ strncat(full_path, ".goon", sizeof(full_path) - plen - 1);
+ }
+
+ free(path);
+
+ FILE *f = fopen(full_path, "r");
+ if (!f) {
+ p->lex->error = strdup("could not open import file");
+ return NULL;
+ }
+
+ fseek(f, 0, SEEK_END);
+ long size = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ char *source = malloc(size + 1);
+ if (!source) {
+ fclose(f);
+ return NULL;
+ }
+
+ size_t read_size = fread(source, 1, size, f);
+ source[read_size] = '\0';
+ fclose(f);
+
+ char *old_base = p->ctx->base_path;
+ p->ctx->base_path = strdup(full_path);
+
+ Lexer import_lex;
+ lexer_init(&import_lex, source);
+ Parser import_parser;
+ import_parser.ctx = p->ctx;
+ import_parser.lex = &import_lex;
+
+ if (!lexer_next(&import_lex)) {
+ free(source);
+ free(p->ctx->base_path);
+ p->ctx->base_path = old_base;
+ return NULL;
+ }
+
+ Goon_Value *result = NULL;
+ while (import_lex.current.type != TOK_EOF) {
+ result = parse_expr(&import_parser);
+ if (!result) break;
+ }
+
+ free(source);
+ free(p->ctx->base_path);
+ p->ctx->base_path = old_base;
+
+ return result;
+}
+
+static Goon_Value *parse_call(Parser *p, const char *name) {
+ Goon_Value *fn = lookup(p->ctx, name);
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *args[16];
+ size_t argc = 0;
+
+ while (p->lex->current.type != TOK_RPAREN && p->lex->current.type != TOK_EOF && argc < 16) {
+ args[argc++] = parse_expr(p);
+ if (p->lex->current.type == TOK_COMMA) {
+ if (!lexer_next(p->lex)) return NULL;
+ }
+ }
+
+ if (p->lex->current.type != TOK_RPAREN) {
+ p->lex->error = strdup("expected )");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ if (fn && fn->type == GOON_BUILTIN) {
+ return fn->data.builtin(p->ctx, args, argc);
+ }
+
+ return goon_nil(p->ctx);
+}
+
+static Goon_Value *parse_primary(Parser *p) {
+ Token tok = p->lex->current;
+
+ switch (tok.type) {
+ case TOK_INT: {
+ Goon_Value *val = goon_int(p->ctx, tok.data.integer);
+ if (!lexer_next(p->lex)) return NULL;
+ return val;
+ }
+
+ case TOK_STRING: {
+ Goon_Value *val = interpolate_string(p->ctx, tok.data.string);
+ if (!lexer_next(p->lex)) return NULL;
+ return val;
+ }
+
+ case TOK_TRUE: {
+ Goon_Value *val = goon_bool(p->ctx, true);
+ if (!lexer_next(p->lex)) return NULL;
+ return val;
+ }
+
+ case TOK_FALSE: {
+ Goon_Value *val = goon_bool(p->ctx, false);
+ if (!lexer_next(p->lex)) return NULL;
+ return val;
+ }
+
+ case TOK_IDENT: {
+ char *name = strdup(tok.data.string);
+ if (!lexer_next(p->lex)) { free(name); return NULL; }
+
+ if (p->lex->current.type == TOK_LPAREN) {
+ Goon_Value *result = parse_call(p, name);
+ free(name);
+ return result;
+ }
+
+ if (p->lex->current.type == TOK_DOT) {
+ Goon_Value *val = lookup(p->ctx, name);
+ free(name);
+
+ while (p->lex->current.type == TOK_DOT) {
+ if (!lexer_next(p->lex)) return NULL;
+ if (p->lex->current.type != TOK_IDENT) {
+ p->lex->error = strdup("expected field name after .");
+ return NULL;
+ }
+ char *field = p->lex->current.data.string;
+ val = goon_record_get(val, field);
+ if (!lexer_next(p->lex)) return NULL;
+ }
+ return val ? val : goon_nil(p->ctx);
+ }
+
+ Goon_Value *val = lookup(p->ctx, name);
+ free(name);
+ return val ? val : goon_nil(p->ctx);
+ }
+
+ case TOK_LBRACE:
+ return parse_record(p);
+
+ case TOK_LBRACKET:
+ return parse_list(p);
+
+ case TOK_IMPORT:
+ return parse_import(p);
+
+ case TOK_LPAREN: {
+ if (!lexer_next(p->lex)) return NULL;
+ Goon_Value *val = parse_expr(p);
+ if (p->lex->current.type != TOK_RPAREN) {
+ p->lex->error = strdup("expected )");
+ return NULL;
+ }
+ if (!lexer_next(p->lex)) return NULL;
+ return val;
+ }
+
+ default:
+ return NULL;
+ }
+}
+
+static Goon_Value *parse_expr(Parser *p) {
+ if (p->lex->current.type == TOK_LET) {
+ if (!lexer_next(p->lex)) return NULL;
+
+ if (p->lex->current.type != TOK_IDENT) {
+ p->lex->error = strdup("expected identifier after let");
+ return NULL;
+ }
+
+ char *name = strdup(p->lex->current.data.string);
+ if (!lexer_next(p->lex)) { free(name); return NULL; }
+
+ if (p->lex->current.type == TOK_COLON) {
+ if (!lexer_next(p->lex)) { free(name); return NULL; }
+ if (!lexer_next(p->lex)) { free(name); return NULL; }
+ }
+
+ if (p->lex->current.type != TOK_EQUALS) {
+ p->lex->error = strdup("expected = in let binding");
+ free(name);
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) { free(name); return NULL; }
+
+ Goon_Value *value = parse_expr(p);
+ if (!value) { free(name); return NULL; }
+
+ define(p->ctx, name, value);
+ free(name);
+
+ if (p->lex->current.type == TOK_SEMICOLON) {
+ if (!lexer_next(p->lex)) return NULL;
+ }
+
+ return value;
+ }
+
+ if (p->lex->current.type == TOK_IF) {
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *cond = parse_expr(p);
+ if (!cond) return NULL;
+
+ if (p->lex->current.type != TOK_THEN) {
+ p->lex->error = strdup("expected 'then' after if condition");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *then_val = parse_expr(p);
+ if (!then_val) return NULL;
+
+ if (p->lex->current.type != TOK_ELSE) {
+ p->lex->error = strdup("expected 'else' after then branch");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *else_val = parse_expr(p);
+ if (!else_val) return NULL;
+
+ return goon_to_bool(cond) ? then_val : else_val;
+ }
+
+ Goon_Value *val = parse_primary(p);
+ if (!val) return NULL;
+
+ if (p->lex->current.type == TOK_QUESTION) {
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *then_val = parse_expr(p);
+ if (!then_val) return NULL;
+
+ if (p->lex->current.type != TOK_COLON) {
+ p->lex->error = strdup("expected : in ternary");
+ return NULL;
+ }
+
+ if (!lexer_next(p->lex)) return NULL;
+
+ Goon_Value *else_val = parse_expr(p);
+ if (!else_val) return NULL;
+
+ return goon_to_bool(val) ? then_val : else_val;
+ }
+
+ return val;
+}
+
+Goon_Ctx *goon_create(void) {
+ Goon_Ctx *ctx = malloc(sizeof(Goon_Ctx));
+ if (!ctx) return NULL;
+ ctx->env = NULL;
+ ctx->values = NULL;
+ ctx->fields = NULL;
+ ctx->error = NULL;
+ ctx->base_path = NULL;
+ ctx->userdata = NULL;
+ return ctx;
+}
+
+void goon_destroy(Goon_Ctx *ctx) {
+ if (!ctx) return;
+
+ Goon_Binding *b = ctx->env;
+ while (b) {
+ Goon_Binding *next = b->next;
+ free(b->name);
+ free(b);
+ b = next;
+ }
+
+ Goon_Value *v = ctx->values;
+ while (v) {
+ Goon_Value *next = v->next_alloc;
+ if (v->type == GOON_STRING && v->data.string) {
+ free(v->data.string);
+ } else if (v->type == GOON_LIST && v->data.list.items) {
+ free(v->data.list.items);
+ }
+ free(v);
+ v = next;
+ }
+
+ Goon_Record_Field *f = ctx->fields;
+ while (f) {
+ Goon_Record_Field *next = f->next;
+ if (f->key) free(f->key);
+ free(f);
+ f = next;
+ }
+
+ if (ctx->error) free(ctx->error);
+ if (ctx->base_path) free(ctx->base_path);
+ free(ctx);
+}
+
+void goon_set_userdata(Goon_Ctx *ctx, void *userdata) {
+ ctx->userdata = userdata;
+}
+
+void *goon_get_userdata(Goon_Ctx *ctx) {
+ return ctx->userdata;
+}
+
+void goon_register(Goon_Ctx *ctx, const char *name, Goon_Builtin_Fn fn) {
+ Goon_Value *val = alloc_value(ctx);
+ if (!val) return;
+ val->type = GOON_BUILTIN;
+ val->data.builtin = fn;
+ define(ctx, name, val);
+}
+
+static Goon_Value *last_result = NULL;
+
+bool goon_load_string(Goon_Ctx *ctx, const char *source) {
+ Lexer lex;
+ lexer_init(&lex, source);
+
+ Parser parser;
+ parser.ctx = ctx;
+ parser.lex = &lex;
+
+ if (!lexer_next(&lex)) {
+ if (ctx->error) free(ctx->error);
+ ctx->error = lex.error;
+ return false;
+ }
+
+ last_result = NULL;
+
+ while (lex.current.type != TOK_EOF) {
+ Goon_Value *expr = parse_expr(&parser);
+ if (!expr) {
+ if (lex.error) {
+ if (ctx->error) free(ctx->error);
+ ctx->error = lex.error;
+ }
+ return false;
+ }
+ last_result = expr;
+ }
+
+ return true;
+}
+
+bool goon_load_file(Goon_Ctx *ctx, const char *path) {
+ FILE *f = fopen(path, "r");
+ if (!f) {
+ if (ctx->error) free(ctx->error);
+ ctx->error = strdup("could not open file");
+ return false;
+ }
+
+ fseek(f, 0, SEEK_END);
+ long size = ftell(f);
+ fseek(f, 0, SEEK_SET);
+
+ char *source = malloc(size + 1);
+ if (!source) {
+ fclose(f);
+ if (ctx->error) free(ctx->error);
+ ctx->error = strdup("out of memory");
+ return false;
+ }
+
+ size_t read_size = fread(source, 1, size, f);
+ source[read_size] = '\0';
+ fclose(f);
+
+ ctx->base_path = strdup(path);
+
+ bool result = goon_load_string(ctx, source);
+ free(source);
+ return result;
+}
+
+const char *goon_get_error(Goon_Ctx *ctx) {
+ return ctx->error;
+}
+
+Goon_Value *goon_eval_result(Goon_Ctx *ctx) {
+ (void)ctx;
+ return last_result;
+}
+
+typedef struct {
+ char *buf;
+ size_t len;
+ size_t cap;
+} String_Builder;
+
+static void sb_init(String_Builder *sb) {
+ sb->buf = malloc(256);
+ sb->len = 0;
+ sb->cap = 256;
+ if (sb->buf) sb->buf[0] = '\0';
+}
+
+static void sb_append(String_Builder *sb, const char *str) {
+ if (!sb->buf) return;
+ size_t add_len = strlen(str);
+ while (sb->len + add_len + 1 > sb->cap) {
+ sb->cap *= 2;
+ sb->buf = realloc(sb->buf, sb->cap);
+ if (!sb->buf) return;
+ }
+ memcpy(sb->buf + sb->len, str, add_len + 1);
+ sb->len += add_len;
+}
+
+static void sb_append_char(String_Builder *sb, char c) {
+ char tmp[2] = {c, '\0'};
+ sb_append(sb, tmp);
+}
+
+static void json_escape_string(String_Builder *sb, const char *str) {
+ sb_append_char(sb, '"');
+ while (*str) {
+ switch (*str) {
+ case '"': sb_append(sb, "\\\""); break;
+ case '\\': sb_append(sb, "\\\\"); break;
+ case '\n': sb_append(sb, "\\n"); break;
+ case '\r': sb_append(sb, "\\r"); break;
+ case '\t': sb_append(sb, "\\t"); break;
+ default: sb_append_char(sb, *str); break;
+ }
+ str++;
+ }
+ sb_append_char(sb, '"');
+}
+
+static void value_to_json(String_Builder *sb, Goon_Value *val, int indent, int depth);
+
+static void append_indent(String_Builder *sb, int indent, int depth) {
+ if (indent <= 0) return;
+ for (int i = 0; i < indent * depth; i++) {
+ sb_append_char(sb, ' ');
+ }
+}
+
+static void value_to_json(String_Builder *sb, Goon_Value *val, int indent, int depth) {
+ if (!val || val->type == GOON_NIL) {
+ sb_append(sb, "null");
+ return;
+ }
+
+ switch (val->type) {
+ case GOON_BOOL:
+ sb_append(sb, val->data.boolean ? "true" : "false");
+ break;
+
+ case GOON_INT: {
+ char num[32];
+ snprintf(num, sizeof(num), "%ld", val->data.integer);
+ sb_append(sb, num);
+ break;
+ }
+
+ case GOON_STRING:
+ json_escape_string(sb, val->data.string);
+ break;
+
+ case GOON_LIST: {
+ sb_append_char(sb, '[');
+ if (indent > 0 && val->data.list.len > 0) sb_append_char(sb, '\n');
+ for (size_t i = 0; i < val->data.list.len; i++) {
+ if (indent > 0) append_indent(sb, indent, depth + 1);
+ value_to_json(sb, val->data.list.items[i], indent, depth + 1);
+ if (i < val->data.list.len - 1) sb_append_char(sb, ',');
+ if (indent > 0) sb_append_char(sb, '\n');
+ }
+ if (indent > 0 && val->data.list.len > 0) append_indent(sb, indent, depth);
+ sb_append_char(sb, ']');
+ break;
+ }
+
+ case GOON_RECORD: {
+ sb_append_char(sb, '{');
+ Goon_Record_Field *f = val->data.record.fields;
+ size_t count = 0;
+ Goon_Record_Field *tmp = f;
+ while (tmp) { count++; tmp = tmp->next; }
+ if (indent > 0 && count > 0) sb_append_char(sb, '\n');
+ size_t idx = 0;
+ while (f) {
+ if (indent > 0) append_indent(sb, indent, depth + 1);
+ json_escape_string(sb, f->key);
+ sb_append_char(sb, ':');
+ if (indent > 0) sb_append_char(sb, ' ');
+ value_to_json(sb, f->value, indent, depth + 1);
+ if (f->next) sb_append_char(sb, ',');
+ if (indent > 0) sb_append_char(sb, '\n');
+ f = f->next;
+ idx++;
+ }
+ if (indent > 0 && count > 0) append_indent(sb, indent, depth);
+ sb_append_char(sb, '}');
+ break;
+ }
+
+ default:
+ sb_append(sb, "null");
+ break;
+ }
+}
+
+char *goon_to_json(Goon_Value *val) {
+ String_Builder sb;
+ sb_init(&sb);
+ value_to_json(&sb, val, 0, 0);
+ return sb.buf;
+}
+
+char *goon_to_json_pretty(Goon_Value *val, int indent) {
+ String_Builder sb;
+ sb_init(&sb);
+ value_to_json(&sb, val, indent, 0);
+ return sb.buf;
+}
diff --git a/src/goon.h b/src/goon.h
new file mode 100644
index 0000000..016db8a
--- /dev/null
+++ b/src/goon.h
@@ -0,0 +1,110 @@
+#ifndef GOON_H
+#define GOON_H
+
+#include <stddef.h>
+#include <stdint.h>
+#include <stdbool.h>
+
+#define GOON_VERSION "0.1.0"
+
+typedef enum {
+ GOON_NIL,
+ GOON_BOOL,
+ GOON_INT,
+ GOON_STRING,
+ GOON_LIST,
+ GOON_RECORD,
+ GOON_BUILTIN,
+} Goon_Type;
+
+typedef struct Goon_Value Goon_Value;
+typedef struct Goon_Ctx Goon_Ctx;
+typedef struct Goon_Record_Field Goon_Record_Field;
+
+typedef Goon_Value *(*Goon_Builtin_Fn)(Goon_Ctx *ctx, Goon_Value **args, size_t argc);
+
+struct Goon_Record_Field {
+ char *key;
+ Goon_Value *value;
+ Goon_Record_Field *next;
+};
+
+struct Goon_Value {
+ Goon_Type type;
+ struct Goon_Value *next_alloc;
+ union {
+ bool boolean;
+ int64_t integer;
+ char *string;
+ struct {
+ Goon_Value **items;
+ size_t len;
+ size_t cap;
+ } list;
+ struct {
+ Goon_Record_Field *fields;
+ } record;
+ Goon_Builtin_Fn builtin;
+ } data;
+};
+
+typedef struct Goon_Binding {
+ char *name;
+ Goon_Value *value;
+ struct Goon_Binding *next;
+} Goon_Binding;
+
+struct Goon_Ctx {
+ Goon_Binding *env;
+ Goon_Value *values;
+ Goon_Record_Field *fields;
+ char *error;
+ char *base_path;
+ void *userdata;
+};
+
+Goon_Ctx *goon_create(void);
+void goon_destroy(Goon_Ctx *ctx);
+
+void goon_set_userdata(Goon_Ctx *ctx, void *userdata);
+void *goon_get_userdata(Goon_Ctx *ctx);
+
+void goon_register(Goon_Ctx *ctx, const char *name, Goon_Builtin_Fn fn);
+
+bool goon_load_file(Goon_Ctx *ctx, const char *path);
+bool goon_load_string(Goon_Ctx *ctx, const char *source);
+
+const char *goon_get_error(Goon_Ctx *ctx);
+
+Goon_Value *goon_nil(Goon_Ctx *ctx);
+Goon_Value *goon_bool(Goon_Ctx *ctx, bool val);
+Goon_Value *goon_int(Goon_Ctx *ctx, int64_t val);
+Goon_Value *goon_string(Goon_Ctx *ctx, const char *val);
+Goon_Value *goon_list(Goon_Ctx *ctx);
+Goon_Value *goon_record(Goon_Ctx *ctx);
+
+bool goon_is_nil(Goon_Value *val);
+bool goon_is_bool(Goon_Value *val);
+bool goon_is_int(Goon_Value *val);
+bool goon_is_string(Goon_Value *val);
+bool goon_is_list(Goon_Value *val);
+bool goon_is_record(Goon_Value *val);
+
+bool goon_to_bool(Goon_Value *val);
+int64_t goon_to_int(Goon_Value *val);
+const char *goon_to_string(Goon_Value *val);
+
+void goon_list_push(Goon_Ctx *ctx, Goon_Value *list, Goon_Value *item);
+size_t goon_list_len(Goon_Value *list);
+Goon_Value *goon_list_get(Goon_Value *list, size_t index);
+
+void goon_record_set(Goon_Ctx *ctx, Goon_Value *record, const char *key, Goon_Value *value);
+Goon_Value *goon_record_get(Goon_Value *record, const char *key);
+Goon_Record_Field *goon_record_fields(Goon_Value *record);
+
+Goon_Value *goon_eval_result(Goon_Ctx *ctx);
+
+char *goon_to_json(Goon_Value *val);
+char *goon_to_json_pretty(Goon_Value *val, int indent);
+
+#endif
diff --git a/src/main.c b/src/main.c
new file mode 100644
index 0000000..0729d18
--- /dev/null
+++ b/src/main.c
@@ -0,0 +1,116 @@
+#include "goon.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static void print_usage(const char *prog) {
+ fprintf(stderr, "usage: %s <command> [options]\n", prog);
+ fprintf(stderr, "\n");
+ fprintf(stderr, "commands:\n");
+ fprintf(stderr, " eval <file> evaluate file and output JSON\n");
+ fprintf(stderr, " check <file> validate syntax\n");
+ fprintf(stderr, "\n");
+ fprintf(stderr, "options:\n");
+ fprintf(stderr, " -p, --pretty pretty print JSON output\n");
+ fprintf(stderr, " -h, --help show this help\n");
+ fprintf(stderr, " -v, --version show version\n");
+}
+
+static void print_version(void) {
+ printf("goon %s\n", GOON_VERSION);
+}
+
+static int cmd_eval(const char *path, bool pretty) {
+ Goon_Ctx *ctx = goon_create();
+ if (!ctx) {
+ fprintf(stderr, "error: failed to create context\n");
+ return 1;
+ }
+
+ if (!goon_load_file(ctx, path)) {
+ const char *err = goon_get_error(ctx);
+ fprintf(stderr, "error: %s\n", err ? err : "unknown error");
+ goon_destroy(ctx);
+ return 1;
+ }
+
+ Goon_Value *result = goon_eval_result(ctx);
+ char *json = pretty ? goon_to_json_pretty(result, 2) : goon_to_json(result);
+ if (json) {
+ printf("%s\n", json);
+ free(json);
+ }
+
+ goon_destroy(ctx);
+ return 0;
+}
+
+static int cmd_check(const char *path) {
+ Goon_Ctx *ctx = goon_create();
+ if (!ctx) {
+ fprintf(stderr, "error: failed to create context\n");
+ return 1;
+ }
+
+ if (!goon_load_file(ctx, path)) {
+ const char *err = goon_get_error(ctx);
+ fprintf(stderr, "error: %s\n", err ? err : "unknown error");
+ goon_destroy(ctx);
+ return 1;
+ }
+
+ goon_destroy(ctx);
+ return 0;
+}
+
+int main(int argc, char **argv) {
+ if (argc < 2) {
+ print_usage(argv[0]);
+ return 1;
+ }
+
+ const char *cmd = argv[1];
+
+ if (strcmp(cmd, "-h") == 0 || strcmp(cmd, "--help") == 0) {
+ print_usage(argv[0]);
+ return 0;
+ }
+
+ if (strcmp(cmd, "-v") == 0 || strcmp(cmd, "--version") == 0) {
+ print_version();
+ return 0;
+ }
+
+ if (strcmp(cmd, "eval") == 0) {
+ if (argc < 3) {
+ fprintf(stderr, "error: eval requires a file argument\n");
+ return 1;
+ }
+ bool pretty = false;
+ const char *path = NULL;
+ for (int i = 2; i < argc; i++) {
+ if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--pretty") == 0) {
+ pretty = true;
+ } else if (!path) {
+ path = argv[i];
+ }
+ }
+ if (!path) {
+ fprintf(stderr, "error: eval requires a file argument\n");
+ return 1;
+ }
+ return cmd_eval(path, pretty);
+ }
+
+ if (strcmp(cmd, "check") == 0) {
+ if (argc < 3) {
+ fprintf(stderr, "error: check requires a file argument\n");
+ return 1;
+ }
+ return cmd_check(argv[2]);
+ }
+
+ fprintf(stderr, "error: unknown command '%s'\n", cmd);
+ print_usage(argv[0]);
+ return 1;
+}