*
This commit is contained in:
parent
e18681b309
commit
20da6dd16d
14 changed files with 279 additions and 183 deletions
|
|
@ -1,4 +0,0 @@
|
||||||
#load("std.grr")
|
|
||||||
|
|
||||||
while: [stdin fgetline]
|
|
||||||
[print];
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#load("std.grr")
|
load "../std.grr"
|
||||||
|
|
||||||
def fib {
|
def fib {
|
||||||
0 1 dig [dup [+] dip swap] times drop
|
0 1 dig [dup [+] dip swap] times drop
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,4 @@
|
||||||
def print { file/stdout file/write }
|
load "../std.grr"
|
||||||
def when { [] if }
|
|
||||||
def keep { over [call] dip }
|
|
||||||
def bi { [keep] dip call }
|
|
||||||
|
|
||||||
def times {
|
|
||||||
if: over 0 =
|
|
||||||
[drop drop]
|
|
||||||
[swap over >r >r call r> 1 - r> times];
|
|
||||||
}
|
|
||||||
|
|
||||||
def fizzbuzz? { [3 % 0 =] [5 % 0 =] bi or }
|
def fizzbuzz? { [3 % 0 =] [5 % 0 =] bi or }
|
||||||
def fizz { when: 3 % 0 = ["Fizz" print]; }
|
def fizz { when: 3 % 0 = ["Fizz" print]; }
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ growl_sources = [
|
||||||
'src/core/compiler.c',
|
'src/core/compiler.c',
|
||||||
'src/core/dictionary.c',
|
'src/core/dictionary.c',
|
||||||
'src/core/disasm.c',
|
'src/core/disasm.c',
|
||||||
|
'src/core/dynarray.c',
|
||||||
'src/core/file.c',
|
'src/core/file.c',
|
||||||
'src/core/gc.c',
|
'src/core/gc.c',
|
||||||
'src/core/hash.c',
|
'src/core/hash.c',
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,10 @@ int growl_callable(Growl obj) {
|
||||||
case GROWL_TYPE_COMPOSE:
|
case GROWL_TYPE_COMPOSE:
|
||||||
case GROWL_TYPE_CURRY:
|
case GROWL_TYPE_CURRY:
|
||||||
return 1;
|
return 1;
|
||||||
|
case GROWL_TYPE_ALIEN: {
|
||||||
|
GrowlAlien *alien = (GrowlAlien *)(hdr + 1);
|
||||||
|
return alien->type && alien->type->call != NULL;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,13 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "dynarray.h"
|
||||||
#include "opcodes.h"
|
#include "opcodes.h"
|
||||||
#include "sleb128.h"
|
#include "sleb128.h"
|
||||||
#include "dynarray.h"
|
|
||||||
|
#include <libgen.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
#define COMPILER_DEBUG 0
|
#define COMPILER_DEBUG 0
|
||||||
|
|
||||||
|
|
@ -73,7 +77,7 @@ Primitive primitives[] = {
|
||||||
// clang-format on
|
// clang-format on
|
||||||
|
|
||||||
static void emit_byte(GrowlVM *vm, Chunk *chunk, uint8_t byte) {
|
static void emit_byte(GrowlVM *vm, Chunk *chunk, uint8_t byte) {
|
||||||
*push(chunk, &vm->scratch) = byte;
|
*growl_dynarray_push(chunk, &vm->scratch) = byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void emit_sleb128(GrowlVM *vm, Chunk *chunk, intptr_t num) {
|
static void emit_sleb128(GrowlVM *vm, Chunk *chunk, intptr_t num) {
|
||||||
|
|
@ -95,7 +99,7 @@ static size_t add_constant(GrowlVM *vm, Chunk *chunk, Growl value) {
|
||||||
if (chunk->constants.data[i] == value)
|
if (chunk->constants.data[i] == value)
|
||||||
return i;
|
return i;
|
||||||
}
|
}
|
||||||
*push(&chunk->constants, &vm->scratch) = value;
|
*growl_dynarray_push(&chunk->constants, &vm->scratch) = value;
|
||||||
return chunk->constants.count - 1;
|
return chunk->constants.count - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -110,9 +114,9 @@ static int is_integer(const char *str, long *out) {
|
||||||
}
|
}
|
||||||
|
|
||||||
__attribute__((format(printf, 2, 3))) static void
|
__attribute__((format(printf, 2, 3))) static void
|
||||||
compile_error(GrowlLexer *lexer, const char *fmt, ...) {
|
compile_error(GrowlCompileContext *ctx, const char *fmt, ...) {
|
||||||
fprintf(stderr, "%d:%d: compile error: ", lexer->start_row + 1,
|
fprintf(stderr, "%s:%d:%d: compile error: ", ctx->file_path,
|
||||||
lexer->start_col + 1);
|
ctx->lexer->start_row + 1, ctx->lexer->start_col + 1);
|
||||||
va_list args;
|
va_list args;
|
||||||
va_start(args, fmt);
|
va_start(args, fmt);
|
||||||
vfprintf(stderr, fmt, args);
|
vfprintf(stderr, fmt, args);
|
||||||
|
|
@ -140,85 +144,84 @@ static void optimize_tail_calls(Chunk *chunk) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_token(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk);
|
static int compile_token(GrowlCompileContext *ctx, Chunk *chunk);
|
||||||
|
|
||||||
static int compile_quotation(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
static int compile_quotation(GrowlCompileContext *ctx, Chunk *chunk) {
|
||||||
growl_lexer_next(lexer); // skip '['
|
growl_lexer_next(ctx->lexer); // skip '['
|
||||||
Chunk quot_chunk = {0};
|
Chunk quot_chunk = {0};
|
||||||
|
|
||||||
while (lexer->kind != ']' && lexer->kind != GTOK_EOF &&
|
while (ctx->lexer->kind != ']' && ctx->lexer->kind != GTOK_EOF &&
|
||||||
lexer->kind != GTOK_INVALID) {
|
ctx->lexer->kind != GTOK_INVALID) {
|
||||||
if (compile_token(vm, lexer, "_chunk)) {
|
if (compile_token(ctx, "_chunk)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lexer->kind != ']') {
|
if (ctx->lexer->kind != ']') {
|
||||||
compile_error(lexer, "expected ']' to close quotation");
|
compile_error(ctx, "expected ']' to close quotation");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_byte(vm, "_chunk, GOP_RETURN);
|
emit_byte(ctx->vm, "_chunk, GOP_RETURN);
|
||||||
optimize_tail_calls("_chunk);
|
optimize_tail_calls("_chunk);
|
||||||
Growl quot = growl_make_quotation(vm, quot_chunk.data, quot_chunk.count,
|
Growl quot = growl_make_quotation(ctx->vm, quot_chunk.data, quot_chunk.count,
|
||||||
quot_chunk.constants.data,
|
quot_chunk.constants.data,
|
||||||
quot_chunk.constants.count);
|
quot_chunk.constants.count);
|
||||||
size_t idx = add_constant(vm, chunk, quot);
|
size_t idx = add_constant(ctx->vm, chunk, quot);
|
||||||
emit_byte(vm, chunk, GOP_PUSH_CONSTANT);
|
emit_byte(ctx->vm, chunk, GOP_PUSH_CONSTANT);
|
||||||
emit_sleb128(vm, chunk, (intptr_t)idx);
|
emit_sleb128(ctx->vm, chunk, (intptr_t)idx);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_string(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
static int compile_string(GrowlCompileContext *ctx, Chunk *chunk) {
|
||||||
Growl str = growl_wrap_string_tenured(vm, lexer->buffer);
|
Growl str = growl_wrap_string_tenured(ctx->vm, ctx->lexer->buffer);
|
||||||
size_t const_idx = add_constant(vm, chunk, str);
|
size_t const_idx = add_constant(ctx->vm, chunk, str);
|
||||||
emit_byte(vm, chunk, GOP_PUSH_CONSTANT);
|
emit_byte(ctx->vm, chunk, GOP_PUSH_CONSTANT);
|
||||||
emit_sleb128(vm, chunk, (intptr_t)const_idx);
|
emit_sleb128(ctx->vm, chunk, (intptr_t)const_idx);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_def(GrowlVM *vm, GrowlLexer *lexer) {
|
static int compile_def(GrowlCompileContext *ctx) {
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
if (lexer->kind != GTOK_WORD) {
|
if (ctx->lexer->kind != GTOK_WORD) {
|
||||||
compile_error(lexer, "expected name after 'def'");
|
compile_error(ctx, "expected name after 'def'");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
char *name = growl_arena_strdup(&vm->scratch, lexer->buffer);
|
char *name = growl_arena_strdup(&ctx->vm->scratch, ctx->lexer->buffer);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
if (lexer->kind != GTOK_LBRACE) {
|
if (ctx->lexer->kind != GTOK_LBRACE) {
|
||||||
compile_error(lexer, "expected '{' after def name '%s'", name);
|
compile_error(ctx, "expected '{' after def name '%s'", name);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a forward declaration to the dictionary so the word can reference itself
|
|
||||||
GrowlDictionary *entry =
|
GrowlDictionary *entry =
|
||||||
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
growl_dictionary_upsert(&ctx->vm->dictionary, name, &ctx->vm->arena);
|
||||||
GrowlDefinition *def = push(&vm->defs, &vm->arena);
|
GrowlDefinition *def = growl_dynarray_push(&ctx->vm->defs, &ctx->vm->arena);
|
||||||
def->name = growl_arena_strdup(&vm->arena, name);
|
def->name = growl_arena_strdup(&ctx->vm->arena, name);
|
||||||
def->callable = GROWL_NIL; // Placeholder, will be filled in after compilation
|
def->callable = GROWL_NIL;
|
||||||
entry->callable = GROWL_NIL;
|
entry->callable = GROWL_NIL;
|
||||||
entry->index = vm->defs.count - 1;
|
entry->index = ctx->vm->defs.count - 1;
|
||||||
|
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
Chunk fn_chunk = {0};
|
Chunk fn_chunk = {0};
|
||||||
while (lexer->kind != GTOK_RBRACE && lexer->kind != GTOK_EOF &&
|
while (ctx->lexer->kind != GTOK_RBRACE && ctx->lexer->kind != GTOK_EOF &&
|
||||||
lexer->kind != GTOK_INVALID) {
|
ctx->lexer->kind != GTOK_INVALID) {
|
||||||
if (compile_token(vm, lexer, &fn_chunk)) {
|
if (compile_token(ctx, &fn_chunk)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lexer->kind != GTOK_RBRACE) {
|
if (ctx->lexer->kind != GTOK_RBRACE) {
|
||||||
compile_error(lexer, "expected '}' to close def '%s'", name);
|
compile_error(ctx, "expected '}' to close def '%s'", name);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
emit_byte(vm, &fn_chunk, GOP_RETURN);
|
emit_byte(ctx->vm, &fn_chunk, GOP_RETURN);
|
||||||
optimize_tail_calls(&fn_chunk);
|
optimize_tail_calls(&fn_chunk);
|
||||||
Growl fn =
|
Growl fn =
|
||||||
growl_make_quotation(vm, fn_chunk.data, fn_chunk.count,
|
growl_make_quotation(ctx->vm, fn_chunk.data, fn_chunk.count,
|
||||||
fn_chunk.constants.data, fn_chunk.constants.count);
|
fn_chunk.constants.data, fn_chunk.constants.count);
|
||||||
|
|
||||||
#if COMPILER_DEBUG
|
#if COMPILER_DEBUG
|
||||||
|
|
@ -227,118 +230,206 @@ static int compile_def(GrowlVM *vm, GrowlLexer *lexer) {
|
||||||
growl_disassemble(vm, quot);
|
growl_disassemble(vm, quot);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Now update the definition with the compiled quotation
|
|
||||||
def->callable = fn;
|
def->callable = fn;
|
||||||
entry->callable = fn;
|
entry->callable = fn;
|
||||||
|
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_call(GrowlVM *vm, GrowlLexer *lexer, const char *name,
|
static int compile_call(GrowlCompileContext *ctx, Chunk *chunk,
|
||||||
Chunk *chunk) {
|
const char *name) {
|
||||||
for (size_t i = 0; primitives[i].name != NULL; i++) {
|
for (size_t i = 0; primitives[i].name != NULL; i++) {
|
||||||
if (strcmp(name, primitives[i].name) == 0) {
|
if (strcmp(name, primitives[i].name) == 0) {
|
||||||
for (size_t j = 0; primitives[i].opcodes[j] != 0; j++)
|
for (size_t j = 0; primitives[i].opcodes[j] != 0; j++)
|
||||||
emit_byte(vm, chunk, primitives[i].opcodes[j]);
|
emit_byte(ctx->vm, chunk, primitives[i].opcodes[j]);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GrowlDictionary *entry = growl_dictionary_upsert(&vm->dictionary, name, NULL);
|
GrowlDictionary *entry =
|
||||||
|
growl_dictionary_upsert(&ctx->vm->dictionary, name, NULL);
|
||||||
if (entry == NULL) {
|
if (entry == NULL) {
|
||||||
compile_error(lexer, "undefined word '%s'", name);
|
compile_error(ctx, "undefined word '%s'", name);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
emit_byte(vm, chunk, GOP_WORD);
|
emit_byte(ctx->vm, chunk, GOP_WORD);
|
||||||
emit_sleb128(vm, chunk, entry->index);
|
emit_sleb128(ctx->vm, chunk, entry->index);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_command(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
static char *resolve_module_path(GrowlCompileContext *ctx, const char *path) {
|
||||||
char *name = growl_arena_strdup(&vm->scratch, lexer->buffer);
|
char buf[PATH_MAX];
|
||||||
|
|
||||||
|
// relative to current file
|
||||||
|
if (ctx->file_dir && path[0] != '/') {
|
||||||
|
snprintf(buf, sizeof(buf), "%s/%s", ctx->file_dir, path);
|
||||||
|
char *resolved = realpath(buf, NULL);
|
||||||
|
if (resolved && access(resolved, R_OK) == 0)
|
||||||
|
return resolved;
|
||||||
|
free(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: search paths
|
||||||
|
|
||||||
|
// absolute path
|
||||||
|
if (path[0] == '/') {
|
||||||
|
char *resolved = realpath(path, NULL);
|
||||||
|
if (resolved && access(resolved, R_OK) == 0)
|
||||||
|
return resolved;
|
||||||
|
free(resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int compile_load(GrowlCompileContext *ctx) {
|
||||||
|
growl_lexer_next(ctx->lexer);
|
||||||
|
|
||||||
|
if (ctx->lexer->kind != GTOK_STRING) {
|
||||||
|
compile_error(ctx, "expected string after 'load'");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *path = ctx->lexer->buffer;
|
||||||
|
char *resolved = resolve_module_path(ctx, path);
|
||||||
|
if (!resolved) {
|
||||||
|
compile_error(ctx, "cannot find module '%s'", path);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
FILE *file = fopen(resolved, "r");
|
||||||
|
if (!file) {
|
||||||
|
compile_error(ctx, "cannot open '%s'", resolved);
|
||||||
|
free(resolved);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GrowlLexer mod_lexer = {0};
|
||||||
|
mod_lexer.file = file;
|
||||||
|
|
||||||
|
char *dir = dirname(strdup(resolved));
|
||||||
|
|
||||||
|
// I'd like to understand why clang-format does 4 spaces for aggregate
|
||||||
|
// initialization.
|
||||||
|
GrowlCompileContext mod_ctx = {
|
||||||
|
.file_path = resolved,
|
||||||
|
.file_dir = dir,
|
||||||
|
.parent = ctx,
|
||||||
|
.lexer = &mod_lexer,
|
||||||
|
.vm = ctx->vm,
|
||||||
|
};
|
||||||
|
|
||||||
|
int result = 0;
|
||||||
|
Growl obj = growl_compile_with_context(&mod_ctx);
|
||||||
|
if (obj == GROWL_NIL) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
GrowlQuotation *q = growl_unwrap_quotation(obj);
|
||||||
|
if (growl_vm_execute(ctx->vm, q) != 0)
|
||||||
|
result = 1;
|
||||||
|
|
||||||
|
fclose(file);
|
||||||
|
free(resolved);
|
||||||
|
free(dir);
|
||||||
|
growl_lexer_next(ctx->lexer);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int compile_command(GrowlCompileContext *ctx, Chunk *chunk) {
|
||||||
|
char *name = growl_arena_strdup(&ctx->vm->scratch, ctx->lexer->buffer);
|
||||||
name[strlen(name) - 1] = '\0';
|
name[strlen(name) - 1] = '\0';
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
while (lexer->kind != GTOK_SEMICOLON && lexer->kind != GTOK_EOF &&
|
while (ctx->lexer->kind != GTOK_SEMICOLON && ctx->lexer->kind != GTOK_EOF &&
|
||||||
lexer->kind != GTOK_INVALID) {
|
ctx->lexer->kind != GTOK_INVALID) {
|
||||||
if (compile_token(vm, lexer, chunk)) {
|
if (compile_token(ctx, chunk)) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (lexer->kind != GTOK_SEMICOLON) {
|
if (ctx->lexer->kind != GTOK_SEMICOLON) {
|
||||||
compile_error(lexer, "expected ';' to close command '%s:'", name);
|
compile_error(ctx, "expected ';' to close command '%s:'", name);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return compile_call(vm, lexer, name, chunk);
|
return compile_call(ctx, chunk, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_word(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
static int compile_word(GrowlCompileContext *ctx, Chunk *chunk) {
|
||||||
char *text = lexer->buffer;
|
char *name = ctx->lexer->buffer;
|
||||||
size_t len = strlen(text);
|
size_t len = strlen(name);
|
||||||
|
|
||||||
if (strcmp(text, "load") == 0) {
|
if (strcmp(name, "load") == 0) {
|
||||||
// TODO: loading source files
|
return compile_load(ctx);
|
||||||
compile_error(lexer, "'load' nyi");
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a definition
|
// Compile a definition
|
||||||
if (strcmp(text, "def") == 0) {
|
if (strcmp(name, "def") == 0) {
|
||||||
return compile_def(vm, lexer);
|
return compile_def(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile a command: word: args... ;
|
// Compile a command: word: args... ;
|
||||||
if (len > 1 && text[len - 1] == ':') {
|
if (len > 1 && name[len - 1] == ':') {
|
||||||
return compile_command(vm, lexer, chunk);
|
return compile_command(ctx, chunk);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile an integer value
|
// Compile an integer value
|
||||||
long value;
|
long value;
|
||||||
if (is_integer(text, &value)) {
|
if (is_integer(name, &value)) {
|
||||||
size_t idx = add_constant(vm, chunk, GROWL_NUM(value));
|
size_t idx = add_constant(ctx->vm, chunk, GROWL_NUM(value));
|
||||||
emit_byte(vm, chunk, GOP_PUSH_CONSTANT);
|
emit_byte(ctx->vm, chunk, GOP_PUSH_CONSTANT);
|
||||||
emit_sleb128(vm, chunk, (intptr_t)idx);
|
emit_sleb128(ctx->vm, chunk, (intptr_t)idx);
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
return compile_call(vm, lexer, text, chunk);
|
return compile_call(ctx, chunk, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int compile_token(GrowlVM *vm, GrowlLexer *lexer, Chunk *chunk) {
|
static int compile_token(GrowlCompileContext *ctx, Chunk *chunk) {
|
||||||
switch (lexer->kind) {
|
switch (ctx->lexer->kind) {
|
||||||
case GTOK_WORD:
|
case GTOK_WORD:
|
||||||
return compile_word(vm, lexer, chunk);
|
return compile_word(ctx, chunk);
|
||||||
case GTOK_STRING:
|
case GTOK_STRING:
|
||||||
return compile_string(vm, lexer, chunk);
|
return compile_string(ctx, chunk);
|
||||||
case GTOK_LBRACKET:
|
case GTOK_LBRACKET:
|
||||||
return compile_quotation(vm, lexer, chunk);
|
return compile_quotation(ctx, chunk);
|
||||||
case GTOK_SEMICOLON:
|
case GTOK_SEMICOLON:
|
||||||
case GTOK_RPAREN:
|
case GTOK_RPAREN:
|
||||||
case GTOK_RBRACKET:
|
case GTOK_RBRACKET:
|
||||||
case GTOK_RBRACE:
|
case GTOK_RBRACE:
|
||||||
compile_error(lexer, "unexpected token '%c'", lexer->kind);
|
compile_error(ctx, "unexpected token '%c'", ctx->lexer->kind);
|
||||||
return 1;
|
return 1;
|
||||||
case GTOK_INVALID:
|
case GTOK_INVALID:
|
||||||
compile_error(lexer, "invalid token");
|
compile_error(ctx, "invalid token");
|
||||||
return 1;
|
return 1;
|
||||||
default:
|
default:
|
||||||
compile_error(lexer, "unhandled token type '%c'", lexer->kind);
|
compile_error(ctx, "unhandled token type '%c'", ctx->lexer->kind);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Growl growl_compile(GrowlVM *vm, GrowlLexer *lexer) {
|
Growl growl_compile_with_context(GrowlCompileContext *ctx) {
|
||||||
Chunk chunk = {0};
|
Chunk chunk = {0};
|
||||||
growl_lexer_next(lexer);
|
growl_lexer_next(ctx->lexer);
|
||||||
while (lexer->kind != GTOK_EOF) {
|
while (ctx->lexer->kind != GTOK_EOF) {
|
||||||
if (compile_token(vm, lexer, &chunk))
|
if (compile_token(ctx, &chunk))
|
||||||
return GROWL_NIL;
|
return GROWL_NIL;
|
||||||
}
|
}
|
||||||
emit_byte(vm, &chunk, GOP_RETURN);
|
emit_byte(ctx->vm, &chunk, GOP_RETURN);
|
||||||
optimize_tail_calls(&chunk);
|
optimize_tail_calls(&chunk);
|
||||||
return growl_make_quotation(vm, chunk.data, chunk.count, chunk.constants.data,
|
return growl_make_quotation(ctx->vm, chunk.data, chunk.count,
|
||||||
chunk.constants.count);
|
chunk.constants.data, chunk.constants.count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Growl growl_compile(GrowlVM *vm, GrowlLexer *lexer, const char *path,
|
||||||
|
const char *dirname) {
|
||||||
|
GrowlCompileContext ctx = {0};
|
||||||
|
ctx.vm = vm;
|
||||||
|
ctx.lexer = lexer;
|
||||||
|
ctx.file_path = path;
|
||||||
|
ctx.file_dir = dirname;
|
||||||
|
return growl_compile_with_context(&ctx);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
24
src/core/dynarray.c
Normal file
24
src/core/dynarray.c
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
#include "dynarray.h"
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void growl_dynarray_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));
|
||||||
|
}
|
||||||
|
|
@ -5,38 +5,18 @@
|
||||||
|
|
||||||
#include <growl.h>
|
#include <growl.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
#include <stdint.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define push(s, a) \
|
#define growl_dynarray_push(s, a) \
|
||||||
({ \
|
({ \
|
||||||
typeof(s) s_ = (s); \
|
typeof(s) s_ = (s); \
|
||||||
typeof(a) a_ = (a); \
|
typeof(a) a_ = (a); \
|
||||||
if (s_->count >= s_->capacity) { \
|
if (s_->count >= s_->capacity) { \
|
||||||
__grow(s_, sizeof(*s_->data), _Alignof(*s_->data), a_); \
|
growl_dynarray_grow(s_, sizeof(*s_->data), _Alignof(*s_->data), a_); \
|
||||||
} \
|
} \
|
||||||
s_->data + s_->count++; \
|
s_->data + s_->count++; \
|
||||||
})
|
})
|
||||||
|
|
||||||
static void __grow(void *slice, ptrdiff_t size, ptrdiff_t align, GrowlArena *a) {
|
void growl_dynarray_grow(void *slice, ptrdiff_t size, ptrdiff_t align,
|
||||||
struct {
|
GrowlArena *a);
|
||||||
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
|
#endif // GROWL_DYNARRAY_H
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define GC_DEBUG 0
|
#define GC_DEBUG 1
|
||||||
#define ALIGN(n) (((n) + 7) & ~7)
|
#define ALIGN(n) (((n) + 7) & ~7)
|
||||||
|
|
||||||
static int in_from(GrowlVM *vm, void *ptr) {
|
static int in_from(GrowlVM *vm, void *ptr) {
|
||||||
|
|
@ -139,14 +139,15 @@ void growl_gc_collect(GrowlVM *vm) {
|
||||||
gc_print_stats(vm, "before GC");
|
gc_print_stats(vm, "before GC");
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Forward work stack
|
// Forward stacks
|
||||||
for (size_t i = 0; i < GROWL_STACK_SIZE; ++i) {
|
for (Growl *obj = vm->wst; obj < vm->sp; ++obj) {
|
||||||
vm->wst[i] = forward(vm, vm->wst[i]);
|
*obj = forward(vm, *obj);
|
||||||
}
|
}
|
||||||
|
for (Growl *obj = vm->rst; obj < vm->rsp; ++obj) {
|
||||||
// Forward retain stack
|
*obj = forward(vm, *obj);
|
||||||
for (size_t i = 0; i < GROWL_STACK_SIZE; ++i) {
|
}
|
||||||
vm->rst[i] = forward(vm, vm->rst[i]);
|
for (GrowlFrame *frame = vm->cst; frame < vm->csp; ++frame) {
|
||||||
|
frame->next = forward(vm, frame->next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward GC roots
|
// Forward GC roots
|
||||||
|
|
@ -154,10 +155,10 @@ void growl_gc_collect(GrowlVM *vm) {
|
||||||
*vm->roots[i] = forward(vm, *vm->roots[i]);
|
*vm->roots[i] = forward(vm, *vm->roots[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Forward word definitions
|
// // Forward word definitions
|
||||||
for (size_t i = 0; i < vm->defs.count; ++i) {
|
// for (size_t i = 0; i < vm->defs.count; ++i) {
|
||||||
vm->defs.data[i].callable = forward(vm, vm->defs.data[i].callable);
|
// vm->defs.data[i].callable = forward(vm, vm->defs.data[i].callable);
|
||||||
}
|
// }
|
||||||
|
|
||||||
uint8_t *tenured_scan = vm->tenured.start;
|
uint8_t *tenured_scan = vm->tenured.start;
|
||||||
while (tenured_scan < vm->tenured.free) {
|
while (tenured_scan < vm->tenured.free) {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ void growl_register_native(GrowlVM *vm, const char *name,
|
||||||
Growl alien = growl_make_alien_tenured(vm, &native_type, (void *)fn);
|
Growl alien = growl_make_alien_tenured(vm, &native_type, (void *)fn);
|
||||||
GrowlDictionary *entry =
|
GrowlDictionary *entry =
|
||||||
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
growl_dictionary_upsert(&vm->dictionary, name, &vm->arena);
|
||||||
GrowlDefinition *def = push(&vm->defs, &vm->arena);
|
GrowlDefinition *def = growl_dynarray_push(&vm->defs, &vm->arena);
|
||||||
def->name = growl_arena_strdup(&vm->arena, name);
|
def->name = growl_arena_strdup(&vm->arena, name);
|
||||||
def->callable = alien;
|
def->callable = alien;
|
||||||
entry->callable = alien;
|
entry->callable = alien;
|
||||||
|
|
|
||||||
|
|
@ -117,8 +117,7 @@ static GrowlFrame callstack_pop(GrowlVM *vm) {
|
||||||
return *--vm->csp;
|
return *--vm->csp;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void dispatch(GrowlVM *vm, Growl obj,
|
static inline void dispatch(GrowlVM *vm, Growl obj) {
|
||||||
int tail __attribute__((unused))) {
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
switch (growl_type(obj)) {
|
switch (growl_type(obj)) {
|
||||||
case GROWL_TYPE_QUOTATION: {
|
case GROWL_TYPE_QUOTATION: {
|
||||||
|
|
@ -144,24 +143,21 @@ static inline void dispatch(GrowlVM *vm, Growl obj,
|
||||||
GrowlAlien *alien = (GrowlAlien *)(GROWL_UNBOX(obj) + 1);
|
GrowlAlien *alien = (GrowlAlien *)(GROWL_UNBOX(obj) + 1);
|
||||||
if (alien->type && alien->type->call) {
|
if (alien->type && alien->type->call) {
|
||||||
alien->type->call(vm, alien->data);
|
alien->type->call(vm, alien->data);
|
||||||
// After calling a native function, we need to return to the caller
|
|
||||||
if (vm->csp != vm->cst) {
|
if (vm->csp != vm->cst) {
|
||||||
GrowlFrame frame = callstack_pop(vm);
|
GrowlFrame frame = callstack_pop(vm);
|
||||||
vm->current_quotation = frame.quot;
|
vm->current_quotation = frame.quot;
|
||||||
vm->ip = frame.ip;
|
vm->ip = frame.ip;
|
||||||
vm->next = frame.next;
|
vm->next = frame.next;
|
||||||
} else {
|
} else {
|
||||||
// No frames on call stack, use return trampoline to exit
|
|
||||||
vm->current_quotation = vm->return_trampoline;
|
vm->current_quotation = vm->return_trampoline;
|
||||||
vm->ip = vm->return_trampoline->data;
|
vm->ip = vm->return_trampoline->data;
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
growl_vm_error(vm, "attempt to call non-callable alien");
|
__attribute__((fallthrough));
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
growl_vm_error(vm, "attempt to call non-callable (type=%d)",
|
growl_vm_error(vm, "attempt to call non-callable");
|
||||||
growl_type(obj));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -203,7 +199,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
||||||
growl_vm_error(vm, "constant index %" PRIdPTR " out of bounds", idx);
|
growl_vm_error(vm, "constant index %" PRIdPTR " out of bounds", idx);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
growl_vm_error(vm, "attempt to index nil constant table");
|
growl_vm_error(vm, "attempt to index invalid constant table");
|
||||||
}
|
}
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
|
|
@ -298,18 +294,18 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
||||||
VM_OP(CALL) {
|
VM_OP(CALL) {
|
||||||
Growl obj = growl_pop(vm);
|
Growl obj = growl_pop(vm);
|
||||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||||
dispatch(vm, obj, 0);
|
dispatch(vm, obj);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(CALL_NEXT) {
|
VM_OP(CALL_NEXT) {
|
||||||
Growl callable = vm->next;
|
Growl callable = vm->next;
|
||||||
vm->next = GROWL_NIL;
|
vm->next = GROWL_NIL;
|
||||||
dispatch(vm, callable, 1);
|
dispatch(vm, callable);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(TAIL_CALL) {
|
VM_OP(TAIL_CALL) {
|
||||||
Growl obj = growl_pop(vm);
|
Growl obj = growl_pop(vm);
|
||||||
dispatch(vm, obj, 1);
|
dispatch(vm, obj);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(WORD) {
|
VM_OP(WORD) {
|
||||||
|
|
@ -317,14 +313,14 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
||||||
GrowlDefinition *def = &vm->defs.data[idx];
|
GrowlDefinition *def = &vm->defs.data[idx];
|
||||||
Growl word = def->callable;
|
Growl word = def->callable;
|
||||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||||
dispatch(vm, word, 0);
|
dispatch(vm, word);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(TAIL_WORD) {
|
VM_OP(TAIL_WORD) {
|
||||||
intptr_t idx = growl_sleb128_decode(&vm->ip);
|
intptr_t idx = growl_sleb128_decode(&vm->ip);
|
||||||
GrowlDefinition *def = &vm->defs.data[idx];
|
GrowlDefinition *def = &vm->defs.data[idx];
|
||||||
Growl word = def->callable;
|
Growl word = def->callable;
|
||||||
dispatch(vm, word, 1);
|
dispatch(vm, word);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(RETURN) {
|
VM_OP(RETURN) {
|
||||||
|
|
@ -362,7 +358,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
||||||
callstack_push(vm, vm->current_quotation, vm->ip);
|
callstack_push(vm, vm->current_quotation, vm->ip);
|
||||||
callstack_push(vm, vm->dip_trampoline, vm->dip_trampoline->data);
|
callstack_push(vm, vm->dip_trampoline, vm->dip_trampoline->data);
|
||||||
vm->csp[-1].next = x;
|
vm->csp[-1].next = x;
|
||||||
dispatch(vm, callable, 0);
|
dispatch(vm, callable);
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
VM_OP(PPRINT) {
|
VM_OP(PPRINT) {
|
||||||
|
|
@ -417,7 +413,7 @@ int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot) {
|
||||||
if (GROWL_IMM(a)) {
|
if (GROWL_IMM(a)) {
|
||||||
growl_push(vm, GROWL_NUM(~GROWL_ORD(a)));
|
growl_push(vm, GROWL_NUM(~GROWL_ORD(a)));
|
||||||
} else {
|
} else {
|
||||||
growl_vm_error(vm, "arithmetic on non-numbers");
|
growl_vm_error(vm, "numeric op on non-numbers");
|
||||||
}
|
}
|
||||||
VM_NEXT();
|
VM_NEXT();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,6 @@ char *growl_arena_strdup(GrowlArena *ar, const char *str);
|
||||||
#define GROWL_STACK_SIZE 128
|
#define GROWL_STACK_SIZE 128
|
||||||
#define GROWL_CALL_STACK_SIZE 64
|
#define GROWL_CALL_STACK_SIZE 64
|
||||||
#define GROWL_HEAP_SIZE (4 * 1024 * 1024)
|
#define GROWL_HEAP_SIZE (4 * 1024 * 1024)
|
||||||
#define GROWL_ARENA_SIZE (2 * 1024 * 1024)
|
|
||||||
#define GROWL_SCRATCH_SIZE (1024 * 1024)
|
#define GROWL_SCRATCH_SIZE (1024 * 1024)
|
||||||
|
|
||||||
struct GrowlFrame {
|
struct GrowlFrame {
|
||||||
|
|
@ -208,7 +207,11 @@ struct GrowlModule {
|
||||||
|
|
||||||
struct GrowlCompileContext {
|
struct GrowlCompileContext {
|
||||||
GrowlCompileContext *parent;
|
GrowlCompileContext *parent;
|
||||||
const char *file_path, *file_dir;
|
GrowlVM *vm;
|
||||||
|
GrowlLexer *lexer;
|
||||||
|
const char *name;
|
||||||
|
const char *file_path;
|
||||||
|
const char *file_dir;
|
||||||
};
|
};
|
||||||
|
|
||||||
GrowlDictionary *growl_dictionary_upsert(GrowlDictionary **dict,
|
GrowlDictionary *growl_dictionary_upsert(GrowlDictionary **dict,
|
||||||
|
|
@ -257,7 +260,9 @@ noreturn void growl_vm_error(GrowlVM *vm, const char *fmt, ...);
|
||||||
int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot);
|
int growl_vm_execute(GrowlVM *vm, GrowlQuotation *quot);
|
||||||
|
|
||||||
/** Compiler */
|
/** Compiler */
|
||||||
Growl growl_compile(GrowlVM *vm, GrowlLexer *lexer);
|
Growl growl_compile_with_context(GrowlCompileContext *ctx);
|
||||||
|
Growl growl_compile(GrowlVM *vm, GrowlLexer *lexer, const char *path,
|
||||||
|
const char *dirname);
|
||||||
void growl_disassemble(GrowlVM *vm, GrowlQuotation *quot);
|
void growl_disassemble(GrowlVM *vm, GrowlQuotation *quot);
|
||||||
|
|
||||||
/** Extra libraries */
|
/** Extra libraries */
|
||||||
|
|
|
||||||
23
src/main.c
23
src/main.c
|
|
@ -1,13 +1,25 @@
|
||||||
#include <growl.h>
|
#include <growl.h>
|
||||||
#include <math.h>
|
#include <libgen.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int main(int argc, const char *argv[]) {
|
||||||
|
if (argc == 1) {
|
||||||
|
fprintf(stderr, "usage: %s file.grr\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
GrowlVM *vm = growl_vm_init();
|
GrowlVM *vm = growl_vm_init();
|
||||||
growl_register_file_library(vm);
|
growl_register_file_library(vm);
|
||||||
GrowlLexer lexer = {0};
|
|
||||||
lexer.file = stdin;
|
|
||||||
|
|
||||||
Growl obj = growl_compile(vm, &lexer);
|
GrowlLexer lexer = {0};
|
||||||
|
const char *filename = argv[1];
|
||||||
|
char *dirname_ = strdup(filename);
|
||||||
|
dirname_ = dirname(dirname_);
|
||||||
|
|
||||||
|
lexer.file = fopen(argv[1], "r");
|
||||||
|
|
||||||
|
Growl obj = growl_compile(vm, &lexer, filename, dirname_);
|
||||||
if (obj != GROWL_NIL) {
|
if (obj != GROWL_NIL) {
|
||||||
GrowlQuotation *quot = growl_unwrap_quotation(obj);
|
GrowlQuotation *quot = growl_unwrap_quotation(obj);
|
||||||
if (!growl_vm_execute(vm, quot)) {
|
if (!growl_vm_execute(vm, quot)) {
|
||||||
|
|
@ -24,5 +36,6 @@ int main(void) {
|
||||||
|
|
||||||
growl_gc_collect(vm);
|
growl_gc_collect(vm);
|
||||||
growl_vm_free(vm);
|
growl_vm_free(vm);
|
||||||
|
fclose(lexer.file);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
std.grr
10
std.grr
|
|
@ -1,19 +1,13 @@
|
||||||
def print { stdout fprint }
|
def print { file/stdout file/write }
|
||||||
def println { stdout fprint "\n" stdout fprint }
|
def println { print "\n" print }
|
||||||
def nl { "\n" stdout fprint }
|
|
||||||
|
|
||||||
def eprint { stderr fprint }
|
|
||||||
def eprintln { stderr fprint "\n" stderr fprint }
|
|
||||||
|
|
||||||
def when { [] if }
|
def when { [] if }
|
||||||
def unless { swap when }
|
def unless { swap when }
|
||||||
|
|
||||||
def 2dip { swap [dip] dip }
|
def 2dip { swap [dip] dip }
|
||||||
def 3dip { swap [2dip] dip }
|
|
||||||
|
|
||||||
def keep { over [call] dip }
|
def keep { over [call] dip }
|
||||||
def 2keep { [2dup] dip 2dip }
|
def 2keep { [2dup] dip 2dip }
|
||||||
def 3keep { [dup 2over dig] dip 3dip }
|
|
||||||
|
|
||||||
def bi { [keep] dip call }
|
def bi { [keep] dip call }
|
||||||
def tri { [[keep] dip keep] dip call }
|
def tri { [[keep] dip keep] dip call }
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue