next: *
This commit is contained in:
parent
1746d12266
commit
d279bf1d31
17 changed files with 407 additions and 137 deletions
|
|
@ -10,6 +10,16 @@ Growl growl_make_alien(GrowlVM *vm, GrowlAlienType *type, void *data) {
|
|||
return GROWL_BOX(hdr);
|
||||
}
|
||||
|
||||
Growl growl_make_alien_tenured(GrowlVM *vm, GrowlAlienType *type, void *data) {
|
||||
size_t size = sizeof(GrowlObjectHeader) + sizeof(GrowlAlien);
|
||||
GrowlObjectHeader *hdr = growl_gc_alloc_tenured(vm, size);
|
||||
hdr->type = GROWL_TYPE_ALIEN;
|
||||
GrowlAlien *alien = (GrowlAlien *)(hdr + 1);
|
||||
alien->type = type;
|
||||
alien->data = data;
|
||||
return GROWL_BOX(hdr);
|
||||
}
|
||||
|
||||
GrowlAlien *growl_unwrap_alien(Growl obj, GrowlAlienType *type) {
|
||||
if (obj == GROWL_NIL || GROWL_IMM(obj))
|
||||
return NULL;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
#include "opcodes.h"
|
||||
#include "sleb128.h"
|
||||
#include "dynarray.h"
|
||||
|
||||
#define COMPILER_DEBUG 0
|
||||
|
||||
typedef struct {
|
||||
Growl *data;
|
||||
|
|
@ -46,12 +49,15 @@ Primitive primitives[] = {
|
|||
{"call", {GOP_CALL, 0}},
|
||||
{"compose", {GOP_COMPOSE, 0}},
|
||||
{"curry", {GOP_CURRY, 0}},
|
||||
{"dip", {GOP_DIP, 0}},
|
||||
{".", {GOP_PPRINT, 0}},
|
||||
{"+", {GOP_ADD, 0}},
|
||||
{"*", {GOP_MUL, 0}},
|
||||
{"-", {GOP_SUB, 0}},
|
||||
{"/", {GOP_DIV, 0}},
|
||||
{"%", {GOP_MOD, 0}},
|
||||
{"and", {GOP_AND, 0}},
|
||||
{"or", {GOP_OR, 0}},
|
||||
{"=", {GOP_EQ, 0}},
|
||||
{"!=", {GOP_NEQ, 0}},
|
||||
{"<", {GOP_LT, 0}},
|
||||
|
|
@ -66,40 +72,6 @@ Primitive primitives[] = {
|
|||
};
|
||||
// clang-format on
|
||||
|
||||
// See https://nullprogram.com/blog/2023/10/05/
|
||||
#define push(s, a) \
|
||||
({ \
|
||||
typeof(s) s_ = (s); \
|
||||
typeof(a) a_ = (a); \
|
||||
if (s_->count >= s_->capacity) { \
|
||||
grow(s_, sizeof(*s_->data), _Alignof(*s_->data), a_); \
|
||||
} \
|
||||
s_->data + s_->count++; \
|
||||
})
|
||||
|
||||
static void grow(void *slice, ptrdiff_t size, ptrdiff_t align, GrowlArena *a) {
|
||||
struct {
|
||||
uint8_t *data;
|
||||
ptrdiff_t len;
|
||||
ptrdiff_t cap;
|
||||
} replica;
|
||||
memcpy(&replica, slice, sizeof(replica));
|
||||
|
||||
if (!replica.data) {
|
||||
replica.cap = 1;
|
||||
replica.data = growl_arena_alloc(a, 2 * size, align, replica.cap);
|
||||
} else if (a->free == replica.data + size * replica.cap) {
|
||||
growl_arena_alloc(a, size, 1, replica.cap);
|
||||
} else {
|
||||
void *data = growl_arena_alloc(a, 2 * size, align, replica.cap);
|
||||
memcpy(data, replica.data, size * replica.len);
|
||||
replica.data = data;
|
||||
}
|
||||
|
||||
replica.cap *= 2;
|
||||
memcpy(slice, &replica, sizeof(replica));
|
||||
}
|
||||
|
||||
static void emit_byte(GrowlVM *vm, Chunk *chunk, uint8_t byte) {
|
||||
*push(chunk, &vm->scratch) = byte;
|
||||
}
|
||||
|
|
@ -160,10 +132,8 @@ static void optimize_tail_calls(Chunk *chunk) {
|
|||
}
|
||||
if (i < chunk->count && chunk->data[i] == GOP_RETURN) {
|
||||
if (opcode == GOP_CALL) {
|
||||
chunk->data[i] = GOP_NOP;
|
||||
chunk->data[start] = GOP_TAIL_CALL;
|
||||
} else if (opcode == GOP_WORD) {
|
||||
chunk->data[i] = GOP_NOP;
|
||||
chunk->data[start] = GOP_TAIL_WORD;
|
||||
}
|
||||
}
|
||||
|
|
@ -200,7 +170,7 @@ static int compile_quotation(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
|||
}
|
||||
|
||||
static int compile_string(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
||||
Growl str = growl_wrap_string(vm, lexer->buffer);
|
||||
Growl str = growl_wrap_string_tenured(vm, lexer->buffer);
|
||||
size_t const_idx = add_constant(vm, chunk, str);
|
||||
emit_byte(vm, chunk, GOP_PUSH_CONSTANT);
|
||||
emit_sleb128(vm, chunk, (intptr_t)const_idx);
|
||||
|
|
@ -222,6 +192,15 @@ static int compile_def(GrowlVM *vm, GrowlLexer *lexer) {
|
|||
return 1;
|
||||
}
|
||||
|
||||
// Add a forward declaration to the dictionary so the word can reference itself
|
||||
GrowlDictionary *entry =
|
||||
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
||||
GrowlDefinition *def = push(&vm->defs, &vm->arena);
|
||||
def->name = growl_arena_strdup(&vm->arena, name);
|
||||
def->callable = GROWL_NIL; // Placeholder, will be filled in after compilation
|
||||
entry->callable = GROWL_NIL;
|
||||
entry->index = vm->defs.count - 1;
|
||||
|
||||
growl_lexer_next(lexer);
|
||||
Chunk fn_chunk = {0};
|
||||
while (lexer->kind != GTOK_RBRACE && lexer->kind != GTOK_EOF &&
|
||||
|
|
@ -241,14 +220,16 @@ static int compile_def(GrowlVM *vm, GrowlLexer *lexer) {
|
|||
Growl fn =
|
||||
growl_make_quotation(vm, fn_chunk.data, fn_chunk.count,
|
||||
fn_chunk.constants.data, fn_chunk.constants.count);
|
||||
GrowlQuotation *quot = (GrowlQuotation *)(GROWL_UNBOX(fn) + 1);
|
||||
GrowlDictionary *entry =
|
||||
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
||||
GrowlDefinition *def = push(&vm->defs, &vm->arena);
|
||||
def->name = growl_arena_strdup(&vm->arena, name);
|
||||
def->quotation = quot;
|
||||
entry->quotation = quot;
|
||||
entry->index = vm->defs.count - 1;
|
||||
|
||||
#if COMPILER_DEBUG
|
||||
GrowlQuotation *quot = growl_unwrap_quotation(fn);
|
||||
fprintf(stderr, "=== %s ===\n", def->name);
|
||||
growl_disassemble(vm, quot);
|
||||
#endif
|
||||
|
||||
// Now update the definition with the compiled quotation
|
||||
def->callable = fn;
|
||||
entry->callable = fn;
|
||||
|
||||
growl_lexer_next(lexer);
|
||||
return 0;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ static void disassemble(GrowlVM *vm, GrowlQuotation *quot, int indent);
|
|||
static size_t disassemble_instr(GrowlVM *vm, GrowlQuotation *quot,
|
||||
size_t offset, int indent) {
|
||||
for (int i = 0; i < indent; i++) {
|
||||
printf(" ");
|
||||
fprintf(stderr, " ");
|
||||
}
|
||||
printf("%04zu ", offset);
|
||||
fprintf(stderr, "%04zu ", offset);
|
||||
|
||||
uint8_t opcode = quot->data[offset++];
|
||||
|
||||
// clang-format off
|
||||
#define OPCODE(name) case GOP_## name:
|
||||
#define OPCODE1(name) case GOP_## name: printf(#name "\n"); return offset;
|
||||
#define OPCODE1(name) case GOP_## name: fprintf(stderr, #name "\n"); return offset;
|
||||
// clang-format on
|
||||
|
||||
switch (opcode) {
|
||||
|
|
@ -24,26 +24,26 @@ static size_t disassemble_instr(GrowlVM *vm, GrowlQuotation *quot,
|
|||
OPCODE(PUSH_CONSTANT) {
|
||||
intptr_t idx;
|
||||
size_t bytes_read = growl_sleb128_peek("->data[offset], &idx);
|
||||
printf("PUSH_CONSTANT %ld", idx);
|
||||
fprintf(stderr, "PUSH_CONSTANT %ld", idx);
|
||||
if (quot->constants != GROWL_NIL &&
|
||||
growl_type(quot->constants) == GROWL_TYPE_TUPLE) {
|
||||
GrowlTuple *constants = growl_unwrap_tuple(quot->constants);
|
||||
if (idx >= 0 && (size_t)idx < constants->count) {
|
||||
Growl constant = constants->data[idx];
|
||||
printf(" (");
|
||||
growl_print(constant);
|
||||
printf(")");
|
||||
fprintf(stderr, " (");
|
||||
growl_print_to(stderr, constant);
|
||||
fprintf(stderr, ")");
|
||||
|
||||
if (!GROWL_IMM(constant) && constant != GROWL_NIL &&
|
||||
growl_type(constant) == GROWL_TYPE_QUOTATION) {
|
||||
putchar('\n');
|
||||
putc('\n', stderr);
|
||||
GrowlQuotation *inner = growl_unwrap_quotation(constant);
|
||||
disassemble(vm, inner, indent + 1);
|
||||
return offset + bytes_read;
|
||||
}
|
||||
}
|
||||
}
|
||||
putchar('\n');
|
||||
putc('\n', stderr);
|
||||
return offset + bytes_read;
|
||||
}
|
||||
OPCODE1(DROP);
|
||||
|
|
@ -61,22 +61,24 @@ static size_t disassemble_instr(GrowlVM *vm, GrowlQuotation *quot,
|
|||
OPCODE1(CHOOSE);
|
||||
OPCODE1(CALL);
|
||||
OPCODE1(CALL_NEXT);
|
||||
OPCODE1(PUSH_NEXT);
|
||||
OPCODE1(TAIL_CALL);
|
||||
OPCODE(WORD) {
|
||||
intptr_t idx;
|
||||
size_t bytes_read = growl_sleb128_peek("->data[offset], &idx);
|
||||
printf("WORD %s\n", vm->defs.data[idx].name);
|
||||
fprintf(stderr, "WORD %s\n", vm->defs.data[idx].name);
|
||||
return offset + bytes_read;
|
||||
}
|
||||
OPCODE(TAIL_WORD) {
|
||||
intptr_t idx;
|
||||
size_t bytes_read = growl_sleb128_peek("->data[offset], &idx);
|
||||
printf("TAIL_WORD %s\n", vm->defs.data[idx].name);
|
||||
fprintf(stderr, "TAIL_WORD %s\n", vm->defs.data[idx].name);
|
||||
return offset + bytes_read;
|
||||
}
|
||||
OPCODE1(RETURN);
|
||||
OPCODE1(COMPOSE);
|
||||
OPCODE1(CURRY);
|
||||
OPCODE1(DIP);
|
||||
OPCODE1(PPRINT);
|
||||
OPCODE1(ADD);
|
||||
OPCODE1(MUL);
|
||||
|
|
|
|||
42
next/core/dynarray.h
Normal file
42
next/core/dynarray.h
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#ifndef GROWL_DYNARRAY_H
|
||||
#define GROWL_DYNARRAY_H
|
||||
|
||||
// See https://nullprogram.com/blog/2023/10/05/
|
||||
|
||||
#include <growl.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#define push(s, a) \
|
||||
({ \
|
||||
typeof(s) s_ = (s); \
|
||||
typeof(a) a_ = (a); \
|
||||
if (s_->count >= s_->capacity) { \
|
||||
__grow(s_, sizeof(*s_->data), _Alignof(*s_->data), a_); \
|
||||
} \
|
||||
s_->data + s_->count++; \
|
||||
})
|
||||
|
||||
static void __grow(void *slice, ptrdiff_t size, ptrdiff_t align, GrowlArena *a) {
|
||||
struct {
|
||||
uint8_t *data;
|
||||
ptrdiff_t len;
|
||||
ptrdiff_t cap;
|
||||
} replica;
|
||||
memcpy(&replica, slice, sizeof(replica));
|
||||
if (!replica.data) {
|
||||
replica.cap = 1;
|
||||
replica.data = growl_arena_alloc(a, 2 * size, align, replica.cap);
|
||||
} else if (a->free == replica.data + size * replica.cap) {
|
||||
growl_arena_alloc(a, size, 1, replica.cap);
|
||||
} else {
|
||||
void *data = growl_arena_alloc(a, 2 * size, align, replica.cap);
|
||||
memcpy(data, replica.data, size * replica.len);
|
||||
replica.data = data;
|
||||
}
|
||||
replica.cap *= 2;
|
||||
memcpy(slice, &replica, sizeof(replica));
|
||||
}
|
||||
|
||||
#endif // GROWL_DYNARRAY_H
|
||||
49
next/core/file.c
Normal file
49
next/core/file.c
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
#include <growl.h>
|
||||
|
||||
static void file_finalize(void *data) {
|
||||
FILE *f = data;
|
||||
if (f && f != stdin && f != stdout && f != stderr)
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static GrowlAlienType alien_file_type = {
|
||||
.name = "file",
|
||||
.finalizer = file_finalize,
|
||||
.call = NULL,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
static Growl stdout_obj = GROWL_NIL;
|
||||
static void native_file_stdout(GrowlVM *vm) {
|
||||
if (stdout_obj == GROWL_NIL) {
|
||||
GrowlObjectHeader *hdr = growl_gc_alloc_tenured(
|
||||
vm, sizeof(GrowlObjectHeader) + sizeof(GrowlAlien));
|
||||
hdr->type = GROWL_TYPE_ALIEN;
|
||||
GrowlAlien *stdout_alien = (GrowlAlien *)(hdr + 1);
|
||||
stdout_alien->data = stdout;
|
||||
stdout_alien->type = &alien_file_type;
|
||||
stdout_obj = GROWL_BOX(hdr);
|
||||
}
|
||||
growl_push(vm, stdout_obj);
|
||||
}
|
||||
|
||||
static void native_file_write(GrowlVM *vm) {
|
||||
Growl file_obj = growl_pop(vm);
|
||||
Growl string_obj = growl_pop(vm);
|
||||
|
||||
GrowlAlien *file_alien = growl_unwrap_alien(file_obj, &alien_file_type);
|
||||
if (file_alien == NULL)
|
||||
growl_vm_error(vm, "expected file object");
|
||||
|
||||
GrowlString *str = growl_unwrap_string(string_obj);
|
||||
if (str == NULL)
|
||||
growl_vm_error(vm, "expected string");
|
||||
|
||||
fwrite(str->data, sizeof(char), str->len, file_alien->data);
|
||||
}
|
||||
|
||||
void growl_register_file_library(GrowlVM *vm) {
|
||||
growl_register_native(vm, "file/stdout", native_file_stdout);
|
||||
growl_register_native(vm, "file/write", native_file_write);
|
||||
}
|
||||
|
|
@ -139,14 +139,26 @@ void growl_gc_collect(GrowlVM *vm) {
|
|||
gc_print_stats(vm, "before GC");
|
||||
#endif
|
||||
|
||||
// Forward work stack
|
||||
for (size_t i = 0; i < GROWL_STACK_SIZE; ++i) {
|
||||
vm->wst[i] = forward(vm, vm->wst[i]);
|
||||
}
|
||||
|
||||
// Forward retain stack
|
||||
for (size_t i = 0; i < GROWL_STACK_SIZE; ++i) {
|
||||
vm->rst[i] = forward(vm, vm->rst[i]);
|
||||
}
|
||||
|
||||
// Forward GC roots
|
||||
for (size_t i = 0; i < vm->root_count; ++i) {
|
||||
*vm->roots[i] = forward(vm, *vm->roots[i]);
|
||||
}
|
||||
|
||||
// Forward word definitions
|
||||
for (size_t i = 0; i < vm->defs.count; ++i) {
|
||||
vm->defs.data[i].callable = forward(vm, vm->defs.data[i].callable);
|
||||
}
|
||||
|
||||
uint8_t *tenured_scan = vm->tenured.start;
|
||||
while (tenured_scan < vm->tenured.free) {
|
||||
GrowlObjectHeader *hdr = (GrowlObjectHeader *)tenured_scan;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ int growl_lexer_next(GrowlLexer *lexer) {
|
|||
|
||||
switch (next) {
|
||||
case '\\':
|
||||
for (; next != '\n'; next = getc_ws(lexer))
|
||||
for (; next != '\n'; next = lexer_getc(lexer))
|
||||
;
|
||||
return growl_lexer_next(lexer);
|
||||
case '(':
|
||||
|
|
|
|||
29
next/core/native.c
Normal file
29
next/core/native.c
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
#include <growl.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "dynarray.h"
|
||||
|
||||
static void call_native(GrowlVM *vm, void *data) {
|
||||
void (*fn)(GrowlVM *) = data;
|
||||
fn(vm);
|
||||
}
|
||||
|
||||
// clang-format off
|
||||
static GrowlAlienType native_type = {
|
||||
.name = "native",
|
||||
.finalizer = NULL,
|
||||
.call = call_native,
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
void growl_register_native(GrowlVM *vm, const char *name,
|
||||
void (*fn)(GrowlVM *)) {
|
||||
Growl alien = growl_make_alien_tenured(vm, &native_type, (void *)fn);
|
||||
GrowlDictionary *entry =
|
||||
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
||||
GrowlDefinition *def = push(&vm->defs, &vm->arena);
|
||||
def->name = growl_arena_strdup(&vm->arena, name);
|
||||
def->callable = alien;
|
||||
entry->callable = alien;
|
||||
entry->index = vm->defs.count - 1;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ enum GrowlOpcode {
|
|||
GOP_NOP = 0,
|
||||
GOP_PUSH_NIL,
|
||||
GOP_PUSH_CONSTANT,
|
||||
GOP_PUSH_NEXT,
|
||||
GOP_DROP,
|
||||
GOP_DUP,
|
||||
GOP_SWAP,
|
||||
|
|
@ -26,6 +27,7 @@ enum GrowlOpcode {
|
|||
GOP_RETURN,
|
||||
GOP_COMPOSE,
|
||||
GOP_CURRY,
|
||||
GOP_DIP,
|
||||
GOP_PPRINT,
|
||||
GOP_ADD,
|
||||
GOP_MUL,
|
||||
|
|
@ -36,6 +38,8 @@ enum GrowlOpcode {
|
|||
GOP_BOR,
|
||||
GOP_BXOR,
|
||||
GOP_BNOT,
|
||||
GOP_AND,
|
||||
GOP_OR,
|
||||
GOP_EQ,
|
||||
GOP_NEQ,
|
||||
GOP_LT,
|
||||
|
|
|
|||
|
|
@ -8,57 +8,56 @@ void growl_println(Growl value) {
|
|||
putchar('\n');
|
||||
}
|
||||
|
||||
static void print_escaped(const char *data, size_t len) {
|
||||
putchar('"');
|
||||
static void print_escaped(FILE *file, const char *data, size_t len) {
|
||||
putc('"', file);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
switch (data[i]) {
|
||||
case '\0':
|
||||
putchar('\\');
|
||||
putchar('0');
|
||||
putc('\\', file);
|
||||
putc('0', file);
|
||||
break;
|
||||
case '\t':
|
||||
putchar('\\');
|
||||
putchar('t');
|
||||
putc('\\', file);
|
||||
putc('t', file);
|
||||
break;
|
||||
case '\n':
|
||||
putchar('\\');
|
||||
putchar('n');
|
||||
putc('\\', file);
|
||||
putc('n', file);
|
||||
break;
|
||||
case '\r':
|
||||
putchar('\\');
|
||||
putchar('r');
|
||||
putc('\\', file);
|
||||
putc('r', file);
|
||||
break;
|
||||
case '\b':
|
||||
putchar('\\');
|
||||
putchar('b');
|
||||
putc('\\', file);
|
||||
putc('b', file);
|
||||
break;
|
||||
case '\v':
|
||||
putchar('\\');
|
||||
putchar('v');
|
||||
putc('\\', file);
|
||||
putc('v', file);
|
||||
break;
|
||||
case '\f':
|
||||
putchar('\\');
|
||||
putchar('f');
|
||||
putc('\\', file);
|
||||
putc('f', file);
|
||||
break;
|
||||
case '\x1b':
|
||||
putchar('\\');
|
||||
putchar('e');
|
||||
putc('\\', file);
|
||||
putc('e', file);
|
||||
break;
|
||||
case '\\':
|
||||
putchar('\\');
|
||||
putchar('\\');
|
||||
putc('\\', file);
|
||||
putc('\\', file);
|
||||
break;
|
||||
case '"':
|
||||
putchar('\\');
|
||||
putchar('"');
|
||||
putc('\\', file);
|
||||
putc('"', file);
|
||||
break;
|
||||
default:
|
||||
putchar(data[i]);
|
||||
putc(data[i], file);
|
||||
break;
|
||||
|
||||
}
|
||||
}
|
||||
putchar('"');
|
||||
putc('"', file);
|
||||
}
|
||||
|
||||
void growl_print_to(FILE *file, Growl value) {
|
||||
|
|
@ -71,7 +70,7 @@ void growl_print_to(FILE *file, Growl value) {
|
|||
switch (hdr->type) {
|
||||
case GROWL_TYPE_STRING: {
|
||||
GrowlString *str = (GrowlString *)(hdr + 1);
|
||||
print_escaped(str->data, str->len);
|
||||
print_escaped(file, str->data, str->len);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -23,6 +23,18 @@ Growl growl_wrap_string(GrowlVM *vm, const char *cstr) {
|
|||
return GROWL_BOX(hdr);
|
||||
}
|
||||
|
||||
Growl growl_wrap_string_tenured(GrowlVM *vm, const char *cstr) {
|
||||
size_t len = strlen(cstr);
|
||||
size_t size = sizeof(GrowlObjectHeader) + sizeof(GrowlString) + len + 1;
|
||||
GrowlObjectHeader *hdr = growl_gc_alloc_tenured(vm, size);
|
||||
hdr->type = GROWL_TYPE_STRING;
|
||||
GrowlString *str = (GrowlString *)(hdr + 1);
|
||||
str->len = len;
|
||||
memcpy(str->data, cstr, len);
|
||||
str->data[len] = 0;
|
||||
return GROWL_BOX(hdr);
|
||||
}
|
||||
|
||||
GrowlString *growl_unwrap_string(Growl obj) {
|
||||
if (obj == 0 || GROWL_IMM(obj))
|
||||
return NULL;
|
||||
|
|
|
|||
158
next/core/vm.c
158
next/core/vm.c
|
|
@ -31,9 +31,17 @@ GrowlVM *growl_vm_init(void) {
|
|||
vm->root_count = 0;
|
||||
vm->root_capacity = 0;
|
||||
|
||||
static uint8_t trampoline_code[] = {GOP_CALL_NEXT};
|
||||
Growl trampoline = growl_make_quotation(vm, trampoline_code, 1, NULL, 0);
|
||||
vm->compose_trampoline = (GrowlQuotation *)(GROWL_UNBOX(trampoline) + 1);
|
||||
static uint8_t compose_code[] = {GOP_CALL_NEXT};
|
||||
Growl compose_tramp = growl_make_quotation(vm, compose_code, 1, NULL, 0);
|
||||
vm->compose_trampoline = (GrowlQuotation *)(GROWL_UNBOX(compose_tramp) + 1);
|
||||
|
||||
static uint8_t return_code[] = {GOP_RETURN};
|
||||
Growl return_tramp = growl_make_quotation(vm, return_code, 1, NULL, 0);
|
||||
vm->return_trampoline = (GrowlQuotation *)(GROWL_UNBOX(return_tramp) + 1);
|
||||
|
||||
static uint8_t dip_code[] = {GOP_PUSH_NEXT, GOP_RETURN};
|
||||
Growl dip_tramp = growl_make_quotation(vm, dip_code, 2, NULL, 0);
|
||||
vm->dip_trampoline = (GrowlQuotation *)(GROWL_UNBOX(dip_tramp) + 1);
|
||||
|
||||
return vm;
|
||||
}
|
||||
|
|
@ -49,8 +57,8 @@ void growl_vm_free(GrowlVM *vm) {
|
|||
free(vm);
|
||||
}
|
||||
|
||||
__attribute__((format(printf, 2, 3))) static noreturn void
|
||||
vm_error(GrowlVM *vm, const char *fmt, ...) {
|
||||
__attribute__((format(printf, 2, 3))) noreturn void
|
||||
growl_vm_error(GrowlVM *vm, const char *fmt, ...) {
|
||||
va_list args;
|
||||
va_start(args, fmt);
|
||||
fprintf(stderr, "vm: ");
|
||||
|
|
@ -62,19 +70,19 @@ vm_error(GrowlVM *vm, const char *fmt, ...) {
|
|||
|
||||
void growl_push(GrowlVM *vm, Growl obj) {
|
||||
if (vm->sp >= vm->wst + GROWL_STACK_SIZE)
|
||||
vm_error(vm, "work stack overflow");
|
||||
growl_vm_error(vm, "work stack overflow");
|
||||
*vm->sp++ = obj;
|
||||
}
|
||||
|
||||
Growl growl_peek(GrowlVM *vm, size_t depth) {
|
||||
if (vm->sp <= vm->wst + depth)
|
||||
vm_error(vm, "work stack underflow");
|
||||
growl_vm_error(vm, "work stack underflow");
|
||||
return vm->sp[-(depth + 1)];
|
||||
}
|
||||
|
||||
Growl growl_pop(GrowlVM *vm) {
|
||||
if (vm->sp <= vm->wst)
|
||||
vm_error(vm, "work stack underflow");
|
||||
growl_vm_error(vm, "work stack underflow");
|
||||
Growl obj = *--vm->sp;
|
||||
*vm->sp = GROWL_NIL;
|
||||
return obj;
|
||||
|
|
@ -82,13 +90,13 @@ Growl growl_pop(GrowlVM *vm) {
|
|||
|
||||
void growl_rpush(GrowlVM *vm, Growl obj) {
|
||||
if (vm->rsp >= vm->rst + GROWL_STACK_SIZE)
|
||||
vm_error(vm, "work stack overflow");
|
||||
growl_vm_error(vm, "work stack overflow");
|
||||
*vm->rsp++ = obj;
|
||||
}
|
||||
|
||||
Growl growl_rpop(GrowlVM *vm) {
|
||||
if (vm->rsp <= vm->rst)
|
||||
vm_error(vm, "work stack underflow");
|
||||
growl_vm_error(vm, "work stack underflow");
|
||||
Growl obj = *--vm->rsp;
|
||||
*vm->rsp = GROWL_NIL;
|
||||
return obj;
|
||||
|
|
@ -96,7 +104,7 @@ Growl growl_rpop(GrowlVM *vm) {
|
|||
|
||||
static void callstack_push(GrowlVM *vm, GrowlQuotation *q, uint8_t *ip) {
|
||||
if (vm->csp >= vm->cst + GROWL_CALL_STACK_SIZE)
|
||||
vm_error(vm, "call stack overflow");
|
||||
growl_vm_error(vm, "call stack overflow");
|
||||
vm->csp->quot = q;
|
||||
vm->csp->ip = ip;
|
||||
vm->csp->next = GROWL_NIL;
|
||||
|
|
@ -105,11 +113,12 @@ static void callstack_push(GrowlVM *vm, GrowlQuotation *q, uint8_t *ip) {
|
|||
|
||||
static GrowlFrame callstack_pop(GrowlVM *vm) {
|
||||
if (vm->csp <= vm->cst)
|
||||
vm_error(vm, "call stack underflow");
|
||||
growl_vm_error(vm, "call stack underflow");
|
||||
return *--vm->csp;
|
||||
}
|
||||
|
||||
static inline void dispatch(GrowlVM *vm, Growl obj) {
|
||||
static inline void dispatch(GrowlVM *vm, Growl obj,
|
||||
int tail __attribute__((unused))) {
|
||||
for (;;) {
|
||||
switch (growl_type(obj)) {
|
||||
case GROWL_TYPE_QUOTATION: {
|
||||
|
|
@ -131,8 +140,28 @@ static inline void dispatch(GrowlVM *vm, Growl obj) {
|
|||
obj = c->callable;
|
||||
continue;
|
||||
}
|
||||
case GROWL_TYPE_ALIEN: {
|
||||
GrowlAlien *alien = (GrowlAlien *)(GROWL_UNBOX(obj) + 1);
|
||||
if (alien->type && alien->type->call) {
|
||||
alien->type->call(vm, alien->data);
|
||||
// After calling a native function, we need to return to the caller
|
||||
if (vm->csp != vm->cst) {
|
||||
GrowlFrame frame = callstack_pop(vm);
|
||||
vm->current_quotation = frame.quot;
|
||||
vm->ip = frame.ip;
|
||||
vm->next = frame.next;
|
||||
} else {
|
||||
// No frames on call stack, use return trampoline to exit
|
||||
vm->current_quotation = vm->return_trampoline;
|
||||
vm->ip = vm->return_trampoline->data;
|
||||
}
|
||||
return;
|
||||
}
|
||||
growl_vm_error(vm, "attempt to call non-callable alien");
|
||||
}
|
||||
default:
|
||||
vm_error(vm, "attempt to call non-callable");
|
||||
growl_vm_error(vm, "attempt to call non-callable (type=%d)",
|
||||
growl_type(obj));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,13 +200,18 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
if (idx >= 0 && (size_t)idx < constants->count) {
|
||||
growl_push(vm, constants->data[idx]);
|
||||
} else {
|
||||
vm_error(vm, "constant index %" PRIdPTR " out of bounds", idx);
|
||||
growl_vm_error(vm, "constant index %" PRIdPTR " out of bounds", idx);
|
||||
}
|
||||
} else {
|
||||
vm_error(vm, "attempt to index nil constant table");
|
||||
growl_vm_error(vm, "attempt to index nil constant table");
|
||||
}
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(PUSH_NEXT) {
|
||||
growl_push(vm, vm->next);
|
||||
vm->next = GROWL_NIL;
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(DROP) {
|
||||
(void)growl_pop(vm);
|
||||
VM_NEXT();
|
||||
|
|
@ -251,9 +285,9 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
VM_NEXT();
|
||||
}
|
||||
VM_OP(CHOOSE) {
|
||||
Growl cond = growl_pop(vm);
|
||||
Growl f = growl_pop(vm);
|
||||
Growl t = growl_pop(vm);
|
||||
Growl cond = growl_pop(vm);
|
||||
if (cond != GROWL_NIL) {
|
||||
growl_push(vm, t);
|
||||
} else {
|
||||
|
|
@ -264,32 +298,33 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
VM_OP(CALL) {
|
||||
Growl obj = growl_pop(vm);
|
||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||
dispatch(vm, obj);
|
||||
dispatch(vm, obj, 0);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(CALL_NEXT) {
|
||||
growl_push(vm, vm->compose_next);
|
||||
vm->compose_next = GROWL_NIL;
|
||||
__attribute__((__fallthrough__));
|
||||
Growl callable = vm->next;
|
||||
vm->next = GROWL_NIL;
|
||||
dispatch(vm, callable, 1);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(TAIL_CALL) {
|
||||
Growl obj = growl_pop(vm);
|
||||
dispatch(vm, obj);
|
||||
dispatch(vm, obj, 1);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(WORD) {
|
||||
intptr_t idx = growl_sleb128_decode(&vm->ip);
|
||||
GrowlDefinition *def = &vm->defs.data[idx];
|
||||
Growl word = GROWL_BOX((GrowlObjectHeader *)(def->quotation) - 1);
|
||||
Growl word = def->callable;
|
||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||
dispatch(vm, word);
|
||||
dispatch(vm, word, 0);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(TAIL_WORD) {
|
||||
intptr_t idx = growl_sleb128_decode(&vm->ip);
|
||||
GrowlDefinition *def = &vm->defs.data[idx];
|
||||
Growl word = GROWL_BOX((GrowlObjectHeader *)(def->quotation) - 1);
|
||||
dispatch(vm, word);
|
||||
Growl word = def->callable;
|
||||
dispatch(vm, word, 1);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(RETURN) {
|
||||
|
|
@ -297,7 +332,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
GrowlFrame frame = callstack_pop(vm);
|
||||
vm->current_quotation = frame.quot;
|
||||
vm->ip = frame.ip;
|
||||
vm->compose_next = frame.next;
|
||||
vm->next = frame.next;
|
||||
} else {
|
||||
goto done;
|
||||
}
|
||||
|
|
@ -308,7 +343,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
Growl first = growl_pop(vm);
|
||||
Growl composed = growl_compose(vm, first, second);
|
||||
if (composed == GROWL_NIL)
|
||||
vm_error(vm, "attempt to compose with a non-callable");
|
||||
growl_vm_error(vm, "attempt to compose with a non-callable");
|
||||
growl_push(vm, composed);
|
||||
VM_NEXT();
|
||||
}
|
||||
|
|
@ -317,10 +352,19 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
Growl value = growl_pop(vm);
|
||||
Growl curried = growl_curry(vm, value, callable);
|
||||
if (curried == GROWL_NIL)
|
||||
vm_error(vm, "attempt to curry with a non-callable");
|
||||
growl_vm_error(vm, "attempt to curry with a non-callable");
|
||||
growl_push(vm, curried);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(DIP) {
|
||||
Growl callable = growl_pop(vm);
|
||||
Growl x = growl_pop(vm);
|
||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||
callstack_push(vm, vm->dip_trampoline, vm->dip_trampoline->data);
|
||||
vm->csp[-1].next = x;
|
||||
dispatch(vm, callable, 0);
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(PPRINT) {
|
||||
growl_println(growl_pop(vm));
|
||||
VM_NEXT();
|
||||
|
|
@ -333,7 +377,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
if (GROWL_IMM(b) && GROWL_IMM(a)) { \
|
||||
growl_push(vm, GROWL_NUM(GROWL_ORD(a) op GROWL_ORD(b))); \
|
||||
} else { \
|
||||
vm_error(vm, "numeric op on non-numbers"); \
|
||||
growl_vm_error(vm, "numeric op on non-numbers"); \
|
||||
} \
|
||||
VM_NEXT(); \
|
||||
}
|
||||
|
|
@ -346,10 +390,10 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
Growl a = growl_pop(vm);
|
||||
if (GROWL_IMM(b) && GROWL_IMM(a)) {
|
||||
if (GROWL_ORD(b) == 0)
|
||||
vm_error(vm, "division by zero");
|
||||
growl_vm_error(vm, "division by zero");
|
||||
growl_push(vm, GROWL_NUM(GROWL_ORD(a) / GROWL_ORD(b)));
|
||||
} else {
|
||||
vm_error(vm, "numeric op on non-numbers");
|
||||
growl_vm_error(vm, "numeric op on non-numbers");
|
||||
};
|
||||
VM_NEXT();
|
||||
}
|
||||
|
|
@ -358,10 +402,10 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
Growl a = growl_pop(vm);
|
||||
if (GROWL_IMM(b) && GROWL_IMM(a)) {
|
||||
if (GROWL_ORD(b) == 0)
|
||||
vm_error(vm, "division by zero");
|
||||
growl_vm_error(vm, "division by zero");
|
||||
growl_push(vm, GROWL_NUM(GROWL_ORD(a) % GROWL_ORD(b)));
|
||||
} else {
|
||||
vm_error(vm, "numeric op on non-numbers");
|
||||
growl_vm_error(vm, "numeric op on non-numbers");
|
||||
};
|
||||
VM_NEXT();
|
||||
}
|
||||
|
|
@ -373,7 +417,27 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
if (GROWL_IMM(a)) {
|
||||
growl_push(vm, GROWL_NUM(~GROWL_ORD(a)));
|
||||
} else {
|
||||
vm_error(vm, "arithmetic on non-numbers");
|
||||
growl_vm_error(vm, "arithmetic on non-numbers");
|
||||
}
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(AND) {
|
||||
Growl b = growl_pop(vm);
|
||||
Growl a = growl_pop(vm);
|
||||
if (a == GROWL_NIL) {
|
||||
growl_push(vm, a);
|
||||
} else {
|
||||
growl_push(vm, b);
|
||||
}
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_OP(OR) {
|
||||
Growl b = growl_pop(vm);
|
||||
Growl a = growl_pop(vm);
|
||||
if (a == GROWL_NIL) {
|
||||
growl_push(vm, b);
|
||||
} else {
|
||||
growl_push(vm, a);
|
||||
}
|
||||
VM_NEXT();
|
||||
}
|
||||
|
|
@ -399,7 +463,29 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
|||
}
|
||||
VM_NEXT();
|
||||
}
|
||||
VM_DEFAULT() { vm_error(vm, "unknown opcode %d", opcode); }
|
||||
|
||||
#define VM_CMPOP(name, op) \
|
||||
case GOP_##name: { \
|
||||
Growl b = growl_pop(vm); \
|
||||
Growl a = growl_pop(vm); \
|
||||
if (GROWL_IMM(b) && GROWL_IMM(a)) { \
|
||||
if (GROWL_ORD(a) op GROWL_ORD(b)) { \
|
||||
growl_push(vm, GROWL_NUM(1)); \
|
||||
} else { \
|
||||
growl_push(vm, GROWL_NIL); \
|
||||
} \
|
||||
} else { \
|
||||
growl_vm_error(vm, "comparison on non-numbers"); \
|
||||
} \
|
||||
VM_NEXT(); \
|
||||
}
|
||||
|
||||
VM_CMPOP(LT, <);
|
||||
VM_CMPOP(LTE, <=);
|
||||
VM_CMPOP(GT, >);
|
||||
VM_CMPOP(GTE, >=);
|
||||
|
||||
VM_DEFAULT() { growl_vm_error(vm, "unknown opcode %d", opcode); }
|
||||
VM_END()
|
||||
|
||||
done:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdnoreturn.h>
|
||||
|
||||
typedef uintptr_t Growl;
|
||||
|
||||
|
|
@ -28,6 +29,8 @@ typedef struct GrowlAlien GrowlAlien;
|
|||
typedef struct GrowlLexer GrowlLexer;
|
||||
typedef struct GrowlArena GrowlArena;
|
||||
typedef struct GrowlFrame GrowlFrame;
|
||||
typedef struct GrowlModule GrowlModule;
|
||||
typedef struct GrowlCompileContext GrowlCompileContext;
|
||||
typedef struct GrowlDictionary GrowlDictionary;
|
||||
typedef struct GrowlDefinition GrowlDefinition;
|
||||
typedef struct GrowlDefinitionTable GrowlDefinitionTable;
|
||||
|
|
@ -52,6 +55,8 @@ struct GrowlObjectHeader {
|
|||
};
|
||||
|
||||
uint32_t growl_type(Growl obj);
|
||||
int growl_equals(Growl a, Growl b);
|
||||
|
||||
uint64_t growl_hash_combine(uint64_t a, uint64_t b);
|
||||
uint64_t growl_hash_bytes(const uint8_t *data, size_t len);
|
||||
uint64_t growl_hash(Growl obj);
|
||||
|
|
@ -60,8 +65,6 @@ void growl_print_to(FILE *file, Growl value);
|
|||
void growl_print(Growl value);
|
||||
void growl_println(Growl value);
|
||||
|
||||
int growl_equals(Growl a, Growl b);
|
||||
|
||||
struct GrowlString {
|
||||
size_t len;
|
||||
char data[];
|
||||
|
|
@ -69,6 +72,7 @@ struct GrowlString {
|
|||
|
||||
Growl growl_make_string(GrowlVM *vm, size_t len);
|
||||
Growl growl_wrap_string(GrowlVM *vm, const char *cstr);
|
||||
Growl growl_wrap_string_tenured(GrowlVM *vm, const char *cstr);
|
||||
GrowlString *growl_unwrap_string(Growl obj);
|
||||
|
||||
struct GrowlList {
|
||||
|
|
@ -85,7 +89,6 @@ GrowlTuple *growl_unwrap_tuple(Growl obj);
|
|||
struct GrowlTable {};
|
||||
|
||||
GrowlTable *growl_unwrap_table(Growl obj);
|
||||
GrowlTable *growl_table_upsert(GrowlVM *vm, GrowlTable **table, Growl key);
|
||||
|
||||
struct GrowlQuotation {
|
||||
size_t count;
|
||||
|
|
@ -112,6 +115,7 @@ GrowlCurry *growl_unwrap_curry(Growl obj);
|
|||
|
||||
struct GrowlAlienType {
|
||||
const char *name;
|
||||
void (*call)(GrowlVM *, void *);
|
||||
void (*finalizer)(void *);
|
||||
};
|
||||
|
||||
|
|
@ -121,7 +125,10 @@ struct GrowlAlien {
|
|||
};
|
||||
|
||||
Growl growl_make_alien(GrowlVM *vm, GrowlAlienType *type, void *data);
|
||||
Growl growl_make_alien_tenured(GrowlVM *vm, GrowlAlienType *type, void *data);
|
||||
GrowlAlien *growl_unwrap_alien(Growl obj, GrowlAlienType *type);
|
||||
void growl_register_native(GrowlVM *vm, const char *name,
|
||||
void (*fn)(GrowlVM *));
|
||||
|
||||
/** Lexer */
|
||||
enum {
|
||||
|
|
@ -179,7 +186,7 @@ struct GrowlFrame {
|
|||
|
||||
struct GrowlDefinition {
|
||||
const char *name;
|
||||
GrowlQuotation *quotation;
|
||||
Growl callable;
|
||||
};
|
||||
|
||||
struct GrowlDefinitionTable {
|
||||
|
|
@ -190,10 +197,20 @@ struct GrowlDefinitionTable {
|
|||
struct GrowlDictionary {
|
||||
GrowlDictionary *child[4];
|
||||
const char *name;
|
||||
GrowlQuotation *quotation;
|
||||
Growl callable;
|
||||
size_t index;
|
||||
};
|
||||
|
||||
struct GrowlModule {
|
||||
char *resolved_path;
|
||||
GrowlModule *next;
|
||||
};
|
||||
|
||||
struct GrowlCompileContext {
|
||||
GrowlCompileContext *parent;
|
||||
const char *file_path, *file_dir;
|
||||
};
|
||||
|
||||
GrowlDictionary *growl_dictionary_upsert(GrowlDictionary **dict,
|
||||
const char *name, GrowlArena *perm);
|
||||
|
||||
|
|
@ -213,7 +230,9 @@ struct GrowlVM {
|
|||
GrowlFrame cst[GROWL_CALL_STACK_SIZE], *csp;
|
||||
|
||||
GrowlQuotation *compose_trampoline;
|
||||
Growl compose_next;
|
||||
GrowlQuotation *return_trampoline;
|
||||
GrowlQuotation *dip_trampoline;
|
||||
Growl next;
|
||||
|
||||
Growl **roots;
|
||||
size_t root_count, root_capacity;
|
||||
|
|
@ -229,10 +248,19 @@ void growl_gc_collect(GrowlVM *vm);
|
|||
void growl_gc_root(GrowlVM *vm, Growl *ptr);
|
||||
size_t growl_gc_mark(GrowlVM *vm);
|
||||
void growl_gc_reset(GrowlVM *vm, size_t mark);
|
||||
void growl_push(GrowlVM *vm, Growl obj);
|
||||
Growl growl_peek(GrowlVM *vm, size_t depth);
|
||||
Growl growl_pop(GrowlVM *vm);
|
||||
void growl_rpush(GrowlVM *vm, Growl obj);
|
||||
Growl growl_rpop(GrowlVM *vm);
|
||||
noreturn void growl_vm_error(GrowlVM *vm, const char *fmt, ...);
|
||||
int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot);
|
||||
|
||||
/** Compiler */
|
||||
Growl growl_compile(GrowlVM *vm, GrowlLexer *lexer);
|
||||
void growl_disassemble(GrowlVM *vm, GrowlQuotation *quot);
|
||||
|
||||
/** Extra libraries */
|
||||
void growl_register_file_library(GrowlVM *vm);
|
||||
|
||||
#endif // GROWL_H
|
||||
|
|
|
|||
20
next/main.c
20
next/main.c
|
|
@ -1,23 +1,25 @@
|
|||
#include "core/opcodes.h"
|
||||
#include <growl.h>
|
||||
#include <math.h>
|
||||
|
||||
int main(void) {
|
||||
GrowlVM *vm = growl_vm_init();
|
||||
growl_register_file_library(vm);
|
||||
GrowlLexer lexer = {0};
|
||||
lexer.file = stdin;
|
||||
|
||||
Growl obj = growl_compile(vm, &lexer);
|
||||
if (obj != GROWL_NIL) {
|
||||
GrowlQuotation *quot = growl_unwrap_quotation(obj);
|
||||
growl_disassemble(vm, quot);
|
||||
growl_vm_execute(vm, quot);
|
||||
|
||||
printf("Stack:");
|
||||
for (Growl *p = vm->wst; p < vm->sp; p++) {
|
||||
putchar(' ');
|
||||
growl_print(*p);
|
||||
if (!growl_vm_execute(vm, quot)) {
|
||||
if (vm->sp != vm->wst) {
|
||||
fprintf(stderr, "Stack:");
|
||||
for (Growl *p = vm->wst; p < vm->sp; p++) {
|
||||
putc(' ', stderr);
|
||||
growl_print_to(stderr, *p);
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
}
|
||||
putchar('\n');
|
||||
}
|
||||
|
||||
growl_gc_collect(vm);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue