Initial commit

Signed-off-by: Slendi <slendi@socopon.com>
This commit is contained in:
2025-07-24 21:25:16 +03:00
commit 123763f48c
11 changed files with 20194 additions and 0 deletions

970
src/dcfg.c Normal file
View File

@@ -0,0 +1,970 @@
#include <dcfg.h>
#include <stdio.h>
#include "vendor/utf8proc.h"
#include "vendor/vec.h"
#define _POSIX_C_SOURCE 200809L
#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
int64_t dcfg_strtoll(const char *s, char **end, int base) {
size_t n = strlen(s);
char *clean = malloc(n + 1);
if (!clean) {
errno = ENOMEM;
if (end)
*end = (char *)s;
return 0;
}
size_t w = 0;
for (size_t r = 0; r < n && s[r]; ++r)
if (s[r] != '_')
clean[w++] = s[r];
clean[w] = '\0';
char *tmp_end;
int64_t v = strtoll(clean, &tmp_end, base);
if (end) {
size_t consumed = (size_t)(tmp_end - clean);
size_t i = 0, c = 0;
while (i < n && c < consumed) {
if (s[i] != '_')
++c;
++i;
}
*end = (char *)s + i;
}
free(clean);
return v;
}
double dcfg_strtod(const char *s, char **end) {
size_t n = strlen(s);
char *clean = malloc(n + 1);
if (!clean) {
errno = ENOMEM;
if (end)
*end = (char *)s;
return 0.0;
}
size_t w = 0;
for (size_t r = 0; r < n && s[r]; ++r)
if (s[r] != '_')
clean[w++] = s[r];
clean[w] = '\0';
char *tmp_end;
double v = strtod(clean, &tmp_end);
if (end) {
size_t consumed = (size_t)(tmp_end - clean);
size_t i = 0, c = 0;
while (i < n && c < consumed) {
if (s[i] != '_')
++c;
++i;
}
*end = (char *)s + i;
}
free(clean);
return v;
}
typedef dcfg_Value Value;
typedef dcfg_Instance Instance;
typedef dcfg_StringView StringView;
#define SV(cstr) ((StringView){.data = cstr, .size = strlen(cstr)})
static inline bool sv_eq(StringView a, StringView b) {
if (a.size != b.size) {
return false;
}
return memcmp(a.data, b.data, a.size) == 0;
}
typedef struct {
int row, col;
} Location;
typedef struct {
Location begin, end;
} LocationRange;
typedef struct {
StringView fp;
LocationRange range;
} SourceLocation;
typedef struct {
StringView k;
Value *v;
} ValueObjectEntry;
typedef struct {
ValueObjectEntry *entryv;
size_t entryc;
} ValueObject;
typedef struct {
Value *valuev;
size_t valuec;
} ValueArray;
typedef struct {
bool is_builtin;
union {
Value *(*bi)(Value **argv, size_t argc);
Value *v;
} v;
} ValueFunction;
typedef struct {
Value *function;
Value **argv;
size_t argc;
} ValueFunctionCall;
struct dcfg_Value {
dcfg_ValueType type;
SourceLocation location;
union {
int64_t i;
double r;
StringView s;
StringView p;
ValueObject o;
ValueArray a;
ValueFunction f;
ValueFunctionCall c;
} v;
};
struct dcfg_Instance {
pthread_mutex_t mtx;
dcfg_AllocFn alloc;
dcfg_FreeFn free;
char last_error[256];
};
#define ALLOC(sz) (instance->alloc((sz)))
#define FREE(ptr) (instance->free((ptr)))
static void *alloc(size_t size) { return calloc(1, size); }
dcfg_Instance *dcfg_make_instance(dcfg_InstanceCreateInfo const *create_info) {
assert(create_info);
dcfg_Instance *instance = calloc(1, sizeof(*instance));
if (!instance) {
return NULL;
}
pthread_mutex_init(&instance->mtx, NULL);
instance->alloc = create_info->alloc;
instance->free = create_info->free;
if (!instance->alloc) {
instance->alloc = alloc;
}
if (!instance->free) {
instance->free = free;
}
return instance;
}
void dcfg_destroy_instance(dcfg_Instance *instance) {
assert(instance);
pthread_mutex_lock(&instance->mtx);
{ // De-init other instance things
}
pthread_mutex_unlock(&instance->mtx);
pthread_mutex_destroy(&instance->mtx);
free(instance);
}
char const *dcfg_last_error(dcfg_Instance *instance) {
assert(instance);
char const *ret = NULL;
pthread_mutex_lock(&instance->mtx);
{
if (!instance->last_error[0]) {
pthread_mutex_unlock(&instance->mtx);
return NULL;
}
ret = instance->last_error;
}
pthread_mutex_unlock(&instance->mtx);
return ret;
}
typedef enum {
TokenType_Identifier,
TokenType_Integer,
TokenType_Real,
TokenType_String,
TokenType_Path,
TokenType_LBracket,
TokenType_RBracket,
TokenType_LParen,
TokenType_RParen,
TokenType_LSquirly,
TokenType_RSquirly,
TokenType_Set,
TokenType_Dot,
TokenType_End,
TokenType_Error,
} TokenType;
typedef struct {
TokenType type;
SourceLocation location;
union {
StringView id;
int64_t i;
double r;
StringView s;
StringView p;
} v;
} Token;
typedef struct {
StringView source;
StringView fp;
int32_t ch, next;
int ch_len, next_len;
Location cursor;
int offset;
} Lexer;
static inline int32_t decode_cp(StringView src, int pos, int *len) {
if (pos >= (int)src.size) {
*len = 0;
return -1;
}
return (int32_t)utf8proc_iterate((const uint8_t *)src.data + pos,
(int)(src.size - pos), len);
}
static inline bool is_space_cp(int32_t cp) {
return (cp <= 0x7F &&
(cp == ' ' || cp == '\t' || cp == '\r' || cp == '\n')) ||
utf8proc_category(cp) == UTF8PROC_CATEGORY_ZS;
}
static inline bool is_alpha_cp(int32_t cp) {
return (cp <= 0x7F && isalpha(cp)) ||
(utf8proc_category(cp) >= UTF8PROC_CATEGORY_LU &&
utf8proc_category(cp) <= UTF8PROC_CATEGORY_LO);
}
static inline bool is_digit_cp(int32_t cp) {
return (cp <= 0x7F && isdigit(cp)) ||
utf8proc_category(cp) == UTF8PROC_CATEGORY_ND;
}
static inline bool is_alnum_cp(int32_t cp) {
return is_alpha_cp(cp) || is_digit_cp(cp);
}
static inline bool is_path_ch_cp(int32_t cp) {
return cp == '_' || cp == '.' || cp == '/' || cp == '-' || cp == ':' ||
is_alnum_cp(cp);
}
#define is_space is_space_cp
#define is_path_ch is_path_ch_cp
#define isalpha_cp is_alpha_cp
#define isdigit_cp is_digit_cp
#define isalnum_cp is_alnum_cp
static void lex_advance(Lexer *lx) {
if (lx->next == -1) {
lx->ch = -1;
return;
}
if (lx->ch == '\n') {
++lx->cursor.row;
lx->cursor.col = 1;
} else {
++lx->cursor.col;
}
lx->offset += lx->ch_len;
lx->ch = lx->next;
lx->ch_len = lx->next_len;
lx->next = decode_cp(lx->source, lx->offset + lx->ch_len, &lx->next_len);
}
static StringView lex_slice(Lexer *lx, int start, int end) {
StringView sv;
sv.data = lx->source.data + start;
sv.size = (size_t)(end - start);
return sv;
}
static void skip_ws_and_comments(Lexer *lx) {
for (;;) {
while (is_space(lx->ch))
lex_advance(lx);
if (lx->ch == '#') {
while (lx->ch != -1 && lx->ch != '\n')
lex_advance(lx);
continue;
}
break;
}
}
static Token make_token(TokenType t, Lexer *lx, int start, int end) {
Token tk = {0};
tk.type = t;
tk.location.fp = lx->fp;
tk.location.range.begin = lx->cursor;
tk.location.range.end = lx->cursor;
switch (t) {
case TokenType_Identifier:
tk.v.id = lex_slice(lx, start, end);
break;
case TokenType_Integer:
tk.v.i = dcfg_strtoll(lex_slice(lx, start, end).data, NULL, 10);
break;
case TokenType_Real:
tk.v.r = dcfg_strtod(lex_slice(lx, start, end).data, NULL);
break;
case TokenType_String:
tk.v.s = lex_slice(lx, start, end);
break;
case TokenType_Path:
tk.v.p = lex_slice(lx, start, end);
break;
default:
break;
}
return tk;
}
static Token lex_number(Lexer *lx) {
int start = lx->offset;
bool real = false;
while (isdigit_cp(lx->ch) || lx->ch == '_')
lex_advance(lx);
if (lx->ch == '.') {
real = true;
lex_advance(lx);
while (isdigit_cp(lx->ch) || lx->ch == '_')
lex_advance(lx);
}
return make_token(real ? TokenType_Real : TokenType_Integer, lx, start,
lx->offset);
}
static Token lex_identifier(Lexer *lx) {
int start = lx->offset;
while (isalnum_cp(lx->ch) || lx->ch == '_')
lex_advance(lx);
return make_token(TokenType_Identifier, lx, start, lx->offset);
}
static Token lex_string(Lexer *lx) {
int quote = lx->ch;
lex_advance(lx); // skip opening quote
int start = lx->offset;
while (lx->ch != quote && lx->ch != -1) {
if (lx->ch == '\\' && lx->next != -1)
lex_advance(lx); // simple escapes
lex_advance(lx);
}
int end = lx->offset;
if (lx->ch == quote)
lex_advance(lx); // skip closing quote
return make_token(TokenType_String, lx, start, end);
}
static Token lex_path(Lexer *lx) {
int start = lx->offset;
for (;;) {
if (lx->ch == '\\') {
lex_advance(lx);
if (lx->ch != -1)
lex_advance(lx);
} else if (is_path_ch(lx->ch)) {
lex_advance(lx);
} else
break;
}
return make_token(TokenType_Path, lx, start, lx->offset);
}
bool Lexer_init(Lexer *lx, StringView src, StringView fp) {
memset(lx, 0, sizeof *lx);
lx->source = src;
lx->fp = fp;
lx->cursor = (Location){1, 1};
lx->ch = decode_cp(src, 0, &lx->ch_len);
lx->next = decode_cp(src, lx->ch_len, &lx->next_len);
return true;
}
Token Lexer_next(Lexer *lx) {
skip_ws_and_comments(lx);
int start_off = lx->offset;
if (lx->ch == -1) {
Token tk = {.type = TokenType_End};
tk.location.fp = lx->fp;
return tk;
}
if (lx->ch == '/' // "/foo"
|| (lx->ch == '.' && lx->next == '/') // "./foo"
|| (lx->ch == '.' && lx->offset + 2 < (int)lx->source.size &&
lx->source.data[lx->offset + 1] == '.' &&
lx->source.data[lx->offset + 2] == '/') // "../foo"
|| (isalpha_cp(lx->ch) && lx->next == ':')) { // "C:/foo"
return lex_path(lx);
}
switch (lx->ch) {
case '[':
lex_advance(lx);
return make_token(TokenType_LBracket, lx, start_off, lx->offset);
case ']':
lex_advance(lx);
return make_token(TokenType_RBracket, lx, start_off, lx->offset);
case '(':
lex_advance(lx);
return make_token(TokenType_LParen, lx, start_off, lx->offset);
case ')':
lex_advance(lx);
return make_token(TokenType_RParen, lx, start_off, lx->offset);
case '{':
lex_advance(lx);
return make_token(TokenType_LSquirly, lx, start_off, lx->offset);
case '}':
lex_advance(lx);
return make_token(TokenType_RSquirly, lx, start_off, lx->offset);
case '=':
lex_advance(lx);
return make_token(TokenType_Set, lx, start_off, lx->offset);
case '.':
lex_advance(lx);
return make_token(TokenType_Dot, lx, start_off, lx->offset);
case '"':
case '\'':
return lex_string(lx);
default:
break;
}
if (isalpha_cp(lx->ch) || lx->ch == '_')
return lex_identifier(lx);
if (isdigit_cp(lx->ch))
return lex_number(lx);
Token err = {.type = TokenType_Error};
err.location.fp = lx->fp;
lex_advance(lx);
return err;
}
typedef enum {
ASTKind_Key, // Identifier + string combo
ASTKind_Path,
ASTKind_Boolean,
ASTKind_Integer,
ASTKind_Real,
ASTKind_Block,
ASTKind_Array,
ASTKind_Function,
ASTKind_MemberAccess,
ASTKind_FunctionCall,
} ASTKind;
typedef struct AST AST;
typedef struct {
StringView *argv;
AST *body;
} ASTFunction;
typedef struct {
AST **childv;
} ASTArray;
typedef struct {
AST *k; // Either MemberAccess or Key
AST *v;
} ASTBlock_Entry;
typedef struct {
ASTBlock_Entry *entryv;
} ASTBlock;
typedef struct {
StringView *accessv;
} ASTMemberAccess;
typedef struct {
AST *function;
AST **argv;
} ASTFunctionCall;
struct AST {
ASTKind kind;
SourceLocation location;
union {
int64_t i;
double r;
StringView s;
StringView p;
bool b;
ASTFunction f;
ASTArray a;
ASTBlock bl;
ASTMemberAccess m;
ASTFunctionCall fc;
} v;
};
typedef struct {
Lexer *lexer;
Instance *instance;
Token cur, next;
} Parser;
bool Parser_next(Parser *parser) {
parser->cur = parser->next;
parser->next = Lexer_next(parser->lexer);
bool ret = parser->next.type != TokenType_Error;
if (!ret) {
strcpy(parser->instance->last_error, "Failed to get parser token");
}
return ret;
}
bool Parser_init(Parser *out_parser, Lexer *lexer, Instance *instance) {
out_parser->lexer = lexer;
out_parser->instance = instance;
if (!Parser_next(out_parser)) {
return false;
}
if (!Parser_next(out_parser)) {
return false;
}
return true;
}
bool Parser_accept(Parser *parser, TokenType type,
SourceLocation *out_location) {
if (parser->cur.type == type) {
if (out_location) {
*out_location = parser->cur.location;
}
if (!Parser_next(parser)) {
return false;
}
return true;
}
return false;
}
#undef ALLOC
#undef FREE
#define ALLOC(sz) (parser->instance->alloc((sz)))
#define FREE(ptr) (parser->instance->free((ptr)))
void AST_free_parser(AST *ast, Parser *parser) {
assert(parser);
if (!ast) {
return;
}
switch (ast->kind) {
case ASTKind_Function:
AST_free_parser(ast->v.f.body, parser);
vector_free(ast->v.f.argv);
break;
case ASTKind_Array:
for (size_t i = 0; i < vector_size(ast->v.a.childv); i++)
AST_free_parser(ast->v.a.childv[i], parser);
vector_free(ast->v.a.childv);
break;
case ASTKind_Block:
for (size_t i = 0; i < vector_size(ast->v.bl.entryv); i++) {
AST_free_parser(ast->v.bl.entryv[i].k, parser);
AST_free_parser(ast->v.bl.entryv[i].v, parser);
}
vector_free(ast->v.bl.entryv);
break;
case ASTKind_MemberAccess:
vector_free(ast->v.m.accessv);
break;
case ASTKind_FunctionCall:
AST_free_parser(ast->v.fc.function, parser);
for (size_t i = 0; i < vector_size(ast->v.fc.argv); i++)
AST_free_parser(ast->v.fc.argv[i], parser);
vector_free(ast->v.fc.argv);
break;
default:
break;
}
FREE(ast);
}
AST *parser_parse_dot_access(Parser *parser, StringView *current) {
assert(parser);
assert(parser->cur.type == TokenType_Identifier ||
parser->cur.type == TokenType_String);
StringView *accessv = vector_create();
if (!accessv) {
strcpy(parser->instance->last_error, "Failed to allocate vector");
return NULL;
}
if (current) {
vector_add(&accessv, *current);
}
SourceLocation loc = parser->cur.location;
SourceLocation last_loc = parser->cur.location;
while (true) {
if (parser->cur.type != TokenType_Identifier &&
parser->cur.type != TokenType_String) {
break;
}
last_loc = parser->cur.location;
vector_add(&accessv, parser->cur.v.s);
if (!Parser_next(parser)) {
strcpy(parser->instance->last_error,
"Failed to get next token in dot access parsing");
vector_free(accessv);
return NULL;
}
if (!Parser_accept(parser, TokenType_Dot, NULL)) {
break;
}
}
assert(vector_size(accessv) != 0);
loc.range.end = last_loc.range.end;
AST *ast = ALLOC(sizeof(*ast));
ast->kind = ASTKind_MemberAccess;
ast->v.m.accessv = accessv;
ast->location = loc;
if (vector_size(accessv) == 1) {
ast->kind = ASTKind_Key;
ast->v.s = accessv[0];
vector_free(accessv);
}
return ast;
}
AST *parser_parse_value(Parser *parser) {
AST *ast = ALLOC(sizeof(*ast));
ast->location = parser->cur.location;
if (parser->cur.type == TokenType_Integer) {
ast->kind = ASTKind_Integer;
ast->v.i = parser->cur.v.i;
} else if (parser->cur.type == TokenType_Real) {
ast->kind = ASTKind_Real;
ast->v.r = parser->cur.v.r;
} else if (parser->cur.type == TokenType_String ||
parser->cur.type == TokenType_Identifier) {
ast->kind = ASTKind_Key;
ast->v.s = parser->cur.v.s;
if (parser->next.type == TokenType_Dot) {
FREE(ast);
return parser_parse_dot_access(parser, NULL);
} else if (parser->cur.type == TokenType_Identifier) {
if (sv_eq(ast->v.s, SV("fn"))) {
if (!Parser_next(parser)) {
strcpy(parser->instance->last_error, "Failed to advance fn keyword");
FREE(ast);
return NULL;
}
ast->kind = ASTKind_Function;
ast->v.f.argv = vector_create();
if (!ast->v.f.argv) {
strcpy(parser->instance->last_error, "Failed to allocate vector");
FREE(ast);
return NULL;
}
while (true) {
if (Parser_accept(parser, TokenType_Set, NULL)) {
break;
}
if (parser->cur.type != TokenType_Identifier) {
strcpy(parser->instance->last_error,
"Expected identifier for function argument");
vector_free(ast->v.f.argv);
FREE(ast);
return NULL;
}
vector_add(&ast->v.f.argv, parser->cur.v.s);
}
ast->v.f.body = parser_parse_value(parser);
if (!ast->v.f.body) {
vector_free(ast->v.f.argv);
FREE(ast);
return NULL;
}
ast->location.range.end = ast->v.f.body->location.range.end;
return ast;
} else if (sv_eq(ast->v.s, SV("on")) || sv_eq(ast->v.s, SV("true"))) {
ast->kind = ASTKind_Boolean;
ast->v.b = true;
} else if (sv_eq(ast->v.s, SV("off")) || sv_eq(ast->v.s, SV("false"))) {
ast->kind = ASTKind_Boolean;
ast->v.b = false;
}
}
} else if (parser->cur.type == TokenType_Path) {
ast->kind = ASTKind_Path;
ast->v.p = parser->cur.v.p;
} else if (Parser_accept(parser, TokenType_LBracket, NULL)) {
ast->kind = ASTKind_Array;
ast->v.a.childv = vector_create();
if (!ast->v.a.childv) {
strcpy(parser->instance->last_error, "Failed to allocate vector");
FREE(ast);
return NULL;
}
SourceLocation rbracket_loc;
while (true) {
if (Parser_accept(parser, TokenType_RBracket, &rbracket_loc)) {
break;
}
AST *value = parser_parse_value(parser);
if (!value) {
for (size_t i = 0; i < vector_size(ast->v.a.childv); i++) {
AST_free_parser(ast->v.a.childv[i], parser);
}
vector_free(ast->v.a.childv);
FREE(ast);
return NULL;
}
vector_add(&ast->v.a.childv, value);
}
ast->location.range.end = rbracket_loc.range.end;
return ast;
} else if (Parser_accept(parser, TokenType_LSquirly, NULL)) {
ast->kind = ASTKind_Block;
ast->v.bl.entryv = vector_create();
if (!ast->v.bl.entryv) {
strcpy(parser->instance->last_error, "Failed to allocate vector");
FREE(ast);
return NULL;
}
SourceLocation rsquirly_loc;
while (true) {
if (Parser_accept(parser, TokenType_RSquirly, &rsquirly_loc)) {
break;
}
if (parser->cur.type != TokenType_Identifier &&
parser->cur.type != TokenType_String) {
strcpy(parser->instance->last_error,
"Expected identifier or string for object key");
vector_free(ast->v.bl.entryv);
FREE(ast);
return NULL;
}
AST *key = parser_parse_dot_access(parser, NULL);
if (!Parser_accept(parser, TokenType_Set, NULL)) {
strcpy(parser->instance->last_error, "Expected = after object key");
vector_free(ast->v.bl.entryv);
FREE(ast);
return NULL;
}
AST *value = parser_parse_value(parser);
if (!value) {
AST_free_parser(key, parser);
vector_free(ast->v.bl.entryv);
FREE(ast);
return NULL;
}
assert(key->kind == ASTKind_MemberAccess || key->kind == ASTKind_Key);
ASTBlock_Entry entry = {
.k = key,
.v = value,
};
vector_add(&ast->v.bl.entryv, entry);
}
return ast;
} else if (Parser_accept(parser, TokenType_LParen, NULL)) {
ast->kind = ASTKind_FunctionCall;
ast->v.fc.function = parser_parse_value(parser);
ast->v.fc.argv = vector_create();
if (!ast->v.fc.argv) {
strcpy(parser->instance->last_error, "Failed to allocate vector");
FREE(ast);
return NULL;
}
SourceLocation rparen_loc;
while (true) {
if (Parser_accept(parser, TokenType_RParen, &rparen_loc)) {
break;
}
AST *value = parser_parse_value(parser);
if (!value) {
for (size_t i = 0; i < vector_size(ast->v.fc.argv); i++) {
AST_free_parser(ast->v.fc.argv[i], parser);
}
vector_free(ast->v.fc.argv);
FREE(ast);
return NULL;
}
vector_add(&ast->v.fc.argv, value);
}
ast->location.range.end = rparen_loc.range.end;
return ast;
} else {
if (parser->cur.type == TokenType_End) {
strcpy(parser->instance->last_error, "Expected value, got end of file");
} else {
strcpy(parser->instance->last_error, "Unexpected token for value");
}
FREE(ast);
ast = NULL;
}
if (!Parser_next(parser)) {
if (ast) {
FREE(ast);
}
return NULL;
}
return ast;
}
// TODO: Print SourceLocation in last error.
AST *Parser_parse(Parser *parser) { return parser_parse_value(parser); }
#undef ALLOC
#undef FREE
#define ALLOC(sz) (instance->alloc((sz)))
#define FREE(ptr) (instance->free((ptr)))
dcfg_Value *dcfg_parse(dcfg_Instance *instance,
dcfg_StringView const file_path) {
char path_buf[file_path.size + 1] = {};
memcpy(path_buf, file_path.data, file_path.size);
char *abs = realpath(path_buf, NULL);
if (!abs) {
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"realpath: %s", strerror(errno));
}
StringView abs_sv = SV(abs);
FILE *fp = fopen(abs, "r");
if (!fp) {
FREE(abs);
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"fopen: %s", strerror(errno));
return NULL;
}
if (fseek(fp, 0, SEEK_END)) {
FREE(abs);
fclose(fp);
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"fseek: %s", strerror(errno));
return NULL;
}
long const size = ftell(fp);
if (fseek(fp, 0, SEEK_SET)) {
FREE(abs);
fclose(fp);
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"fseek: %s", strerror(errno));
return NULL;
}
StringView str = {
.size = size,
};
str.data = ALLOC(size);
if (!fread((void *)str.data, str.size, 1, fp)) {
FREE(abs);
FREE((void *)str.data);
fclose(fp);
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"fread: %s", strerror(errno));
return NULL;
}
Lexer lexer;
if (!Lexer_init(&lexer, str, abs_sv)) {
FREE(abs);
FREE((void *)str.data);
fclose(fp);
snprintf(instance->last_error, sizeof(instance->last_error) - 1,
"Could not init lexer");
return NULL;
}
Parser parser;
Parser_init(&parser, &lexer, instance);
AST *ast = Parser_parse(&parser);
if (!ast) {
FREE(abs);
FREE((void *)str.data);
fclose(fp);
return NULL;
}
// FIXME: Get Value * from AST
}
void dcfg_destroy(dcfg_Value *value) {}
// Libraries
#include "vendor/utf8proc.c"
#include "vendor/vec.c"