vextoly/cryn
0.2.0
Commit 641c634 · Vextoly · 2026-10-05T10:16:57+02:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 83c7649..a1ebee2 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ float `remainder`).
> C-level power through a radically small set of composable primitives.
-* **34 commands total.** No `if`, `while`, `for`, `switch`, `array`,
+* **37 commands total.** No `if`, `while`, `for`, `switch`, `array`,
`struct`, `class`, or `pointer` keywords. Control flow is explicit
jumps; arrays are indexed connections; structures are blocks of memory;
pointers are address values.
@@ -51,11 +51,18 @@ float `remainder`).
types, defined overflow (wrap), defined division by zero (diagnostic
abort), saturating float-to-int conversion, bounds-checked indexing.
* **No hidden runtime.** The runtime support layer is a few hundred lines:
- allocation headers, context structs, thread spawn/join, and output
- formatting. Every dependency is documented in this README.
-
-See the wiki for the formal language reference and for
-a course from first principles.
+ allocation headers, context structs, a per-context file-handle table,
+ thread spawn/join, atomic access helpers, and output formatting. Every
+ dependency is documented in this README.
+
+See the [project wiki](https://git.disroot.org/Vextoly/Cryn/wiki/Home) for
+the formal language reference
+([Language Reference](https://git.disroot.org/Vextoly/Cryn/wiki/Language-Reference)),
+a course from first principles
+([Learning Cryn](https://git.disroot.org/Vextoly/Cryn/wiki/Learning-Cryn)),
+and the compiler internals
+([Architecture](https://git.disroot.org/Vextoly/Cryn/wiki/Architecture)).
+The `wiki/` folder holds the sources of those pages.
## Repository layout
@@ -79,6 +86,9 @@ src/
rt_src.h generated: runtime sources as strings (tools/embed)
tools/
embed.c turns runtime sources into string headers
+wiki/ sources of the published wiki pages: Home, Learning-Cryn
+ (TUTORIAL.md), Language-Reference (LANGUAGE.md),
+ Architecture (ARCHITECTURE.md)
tests/
*.cryn test programs
expected/ expected outputs
@@ -140,8 +150,8 @@ runtime is compiled into every generated translation unit.
types, resolves all names, checks argument counts/types, and emits the
fully typed IR in one pass. Recursive transformations with unclear
results must declare `result <type>`.
-4. **IR** (`ir.h`): ~50 typed instructions (navigation, value ops, memory
- access, comparisons, calls, external operations).
+4. **IR** (`ir.h`): 50 typed instructions (navigation, value ops, memory
+ access, comparisons, calls, external operations, atomics).
5. **Optimizer** (`opt.c`): removes unreachable code and unreferenced
labels. (Deliberately small; the C compiler performs the heavy lifting
on the readable C we emit.)
@@ -157,14 +167,15 @@ The runtime (`src/rt/cryn_rt.*`) provides only:
| Facility | Used by | Notes |
|-----------------|----------------------------------|--------------------------------|
-| `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for validation and bounds checks |
+| `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for sizes plus a mutex-protected registry of live blocks; validation of stale addresses is defined, never undefined reads |
| value stack | `push`, `pop`, `swap`, `call` | growable array, per thread |
| focus history | `move` (relative), `back`, `home`| fixed 256 entries per context (shared across active frames) |
-| FILE* streams | `input`, `output`, `open`, `close`| stdin/stdout by default |
+| FILE* streams | `input`, `output`, `open`, `use`, `close`, `flush`| stdin/stdout by default; `use` routes the context's streams through a handle |
| `system()` | `system` | shell escape |
-| pthreads | `spawn`, `wait` | join provides the only ordering guarantee |
+| pthreads | `spawn`, `wait` | join is an ordering guarantee |
| formatting | `output` | shortest round-trip float printing |
-| relaxed atomics | all scalar loads/stores | races yield unspecified values and never tear (§6.7 of LANGUAGE.md) |
+| relaxed atomics | all scalar loads/stores | races yield unspecified values and never tear ([Language Reference](https://git.disroot.org/Vextoly/Cryn/wiki/Language-Reference)) |
+| acq_rel atomics | `exchange` | indivisible swap of working value and focused cell; the only ordering primitive besides `wait` |
There is no garbage collector, no interpreter, and no mandatory runtime
library beyond the above.
@@ -175,14 +186,16 @@ library beyond the above.
make test
```
-35 checks: arithmetic (including overflow wrap and division edge cases),
+45 checks: arithmetic (including overflow wrap and division edge cases),
unsigned arithmetic, floats, all six comparison conditions, branches,
loops, nested loops, functions, recursion, mutual recursion, the value
stack, cells and focus navigation, arrays (sieve of Eratosthenes),
strings, manual memory, pointer arithmetic, linked lists, callbacks
-through transform references, file I/O, threads (`spawn`/`wait`), a word
-counter, bubble sort, a free-list allocator, and ten programs that must
-fail to compile.
+through transform references, file I/O with first-class handles
+(two files interleaved, append mode, flush), threads (`spawn`/`wait`),
+a spinlock built from `exchange`, a word
+counter, bubble sort, a free-list allocator, and sixteen programs that
+must fail to compile.
## Current limitations
@@ -194,17 +207,23 @@ These are honest boundaries of the first implementation, not pretenses:
zero-parameter transformation; `wait` joins all spawned contexts of the
current one. Shared world cells may be accessed concurrently; a race
on an aligned ≤ 8-byte cell yields an unspecified value of the cell's
- type and never tears (scalar access is lowered as relaxed atomics);
- there is no ordering beyond `wait`. Communication of results happens
+ type and never tears (scalar access is lowered as relaxed atomics).
+ The only ordering guarantees are `wait` (join) and `exchange`
+ (an acquire-release atomic swap of the working value with the focused
+ cell; enough to build locks). Communication of results happens
through shared world cells.
+* **File handles are plain integers scoped to one context.** `open`
+ returns a handle (1–64 per context, -1 on failure; `read`, `write`,
+ `both`, and `append` modes). `use` routes the context's streams
+ through a handle — the readable side to input, the writable side to
+ output; `use 0` returns to the standard streams. A spawned context
+ cannot use its parent's handles, and closing a handle frees its
+ number for reuse.
* **Indirect calls** (through `transform` references) are only allowed to
zero-parameter transformations; checked at runtime when the reference is
not statically known. The callee's result is discarded and the caller's
working value (the reference) is preserved; results flow through world
cells or addresses.
-* **`open` sets the context's current input/output stream** rather than
- exposing a first-class handle type; `close` uses the last handle value.
- One stream pair per context at a time.
* **Bounds checks abort** with a diagnostic. A release mode that removes
them is future work.
* **Fixed limits:** 256 focus history entries, 256 labels, 256 cells per
@@ -221,9 +240,12 @@ These are honest boundaries of the first implementation, not pretenses:
cannot be expressed with the existing primitives.
* Keep the compiler portable ISO C11; isolate platform specifics in
`src/rt/`.
-* Every language change needs: a test in `tests/`, updated `LANGUAGE.md`
- and `TUTORIAL.md`, and a note in this README if it affects the runtime
- surface.
+* Every language change needs: a test in `tests/`, updated wiki sources
+ (`wiki/LANGUAGE.md`, published as
+ [Language-Reference](https://git.disroot.org/Vextoly/Cryn/wiki/Language-Reference),
+ and `wiki/TUTORIAL.md`, published as
+ [Learning-Cryn](https://git.disroot.org/Vextoly/Cryn/wiki/Learning-Cryn)),
+ and a note in this README if it affects the runtime surface.
* Run `make test` before committing.
## Future work
@@ -232,7 +254,8 @@ These are honest boundaries of the first implementation, not pretenses:
portable reference).
2. Self-hosting: rewrite the compiler in Cryn.
3. Release mode without bounds checks; compile-time elimination of
- statically provable checks.
-4. First-class file handles; more `open` modes.
-5. Atomics or another minimal, honest concurrency primitive if a use case
- cannot be met with `spawn`/`wait`.
+ statically provable checks.
+4. File positioning (`seek`) for `open both` streams, if a use case
+ cannot be met with the sequential byte-stream model plus `flush`.
+5. Further atomic primitives only when a use case cannot be met with
+ `exchange` (which builds locks) and `wait`.
diff --git a/src/ast.h b/src/ast.h
index 274dea1..48fd681 100644
--- a/src/ast.h
+++ b/src/ast.h
@@ -54,8 +54,9 @@ typedef enum {
A_CALL_NAME, A_CALL_IND,
A_RET, A_STOP,
A_INPUT, A_OUTPUT, A_OUTPUT_TEXT,
- A_OPEN, A_CLOSE, A_SYSTEM,
+ A_OPEN, A_CLOSE, A_SYSTEM, A_USE, A_FLUSH,
A_SPAWN, A_WAIT,
+ A_XCHG,
A_MARK
} AstKind;
@@ -74,7 +75,8 @@ typedef struct Node {
TypeAst* t; /* make value / convert */
TokKind cond; /* jump condition token */
int has_cond;
- TokKind mode; /* open mode: T_READ/T_WRITE/T_BOTH */
+ TokKind mode; /* open mode: T_READ/T_WRITE/T_BOTH/T_APPEND */
+ int has_use_lit; /* use: literal handle given */
} Node;
/* ---- top level ---- */
diff --git a/src/be_c.c b/src/be_c.c
index 41d038e..db203e4 100644
--- a/src/be_c.c
+++ b/src/be_c.c
@@ -130,6 +130,16 @@ static const char* uct(const Type* t) {
}
}
+/* atomic exchange helper for a scalar type */
+static const char* xch_fn(const Type* t) {
+ switch (t->kind) {
+ case TY_BOOL: case TY_CHAR: case TY_INT8: case TY_UINT8: return "crn_xch_u8";
+ case TY_INT16: case TY_UINT16: return "crn_xch_u16";
+ case TY_INT32: case TY_UINT32: case TY_FLOAT32: return "crn_xch_u32";
+ default: return "crn_xch_u64";
+ }
+}
+
static void emit_store(Buf* o, Insn* in) {
const Type* t = in->t;
const char* u = uct(t);
@@ -693,6 +703,15 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
buf_puts(o, " wv.i = crn_close((int64_t)wv.u);\n");
cur = ty_int(64);
break;
+ case I_USE:
+ if (in->b == 1)
+ buf_printf(o, " crn_use((int64_t)%" PRIu64 "ULL);\n", in->imm.u);
+ else
+ buf_puts(o, " crn_use((int64_t)wv.u);\n");
+ break;
+ case I_FLUSH:
+ buf_puts(o, " crn_flush();\n");
+ break;
case I_SYSTEM:
buf_puts(o, " wv.i = crn_system((uint8_t*)(uintptr_t)wv.u);\n");
cur = ty_int(64);
@@ -704,6 +723,21 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
buf_puts(o, " crn_wait(); wv.i = 0;\n");
cur = ty_int(64);
break;
+ case I_XCHG: {
+ const Type* t = in->t;
+ if (t->kind == TY_FLOAT32)
+ buf_puts(o, " wv.d = (double)crn_xch_f32((float*)F, (float)wv.d);\n");
+ else if (t->kind == TY_FLOAT64)
+ buf_puts(o, " wv.d = crn_xch_f64((double*)F, wv.d);\n");
+ else if (ty_is_signed_int(t))
+ buf_printf(o, " wv.i = (int64_t)(%s)%s((%s*)F, (%s)wv.i);\n",
+ ctype(t), xch_fn(t), uct(t), ctype(t));
+ else
+ buf_printf(o, " wv.u = (uint64_t)(%s)%s((%s*)F, (%s)wv.u);\n",
+ uct(t), xch_fn(t), uct(t), uct(t));
+ cur = in->t;
+ break;
+ }
}
}
/* implicit return handled by checker (I_RET always present or unreachable) */
@@ -839,9 +873,12 @@ static const char* irop_name(IROp op) {
case I_OUTTEXT: return "output-text";
case I_OPEN: return "open";
case I_CLOSE: return "close";
+ case I_USE: return "use";
+ case I_FLUSH: return "flush";
case I_SYSTEM: return "system";
case I_SPAWN: return "spawn";
case I_WAIT: return "wait";
+ case I_XCHG: return "exchange";
}
return "?";
}
diff --git a/src/check.c b/src/check.c
index 92d91de..a0804a6 100644
--- a/src/check.c
+++ b/src/check.c
@@ -1264,12 +1264,13 @@ static void check_insn(Chk* c, Node* n) {
Type* wv = st->wv;
if (!(wv->kind == TY_ADDR &&
(wv->inner->kind == TY_UINT8 ||
- (wv->inner->kind == TY_ARR && wv->inner->inner->kind == TY_UINT8)))) {
+ (wv->inner->kind == TY_ARR && wv->inner->inner->kind == TY_UINT8)))) {
err(n->loc, "'open' requires an address to a NUL-terminated byte string");
return;
}
Insn* in = emit(c, I_OPEN, n->loc);
- in->b = n->mode == T_READ ? 0 : n->mode == T_WRITE ? 1 : 2;
+ in->b = n->mode == T_READ ? 0 : n->mode == T_WRITE ? 1 :
+ n->mode == T_BOTH ? 2 : 3;
st->wv = ty_int(64);
return;
}
@@ -1278,10 +1279,40 @@ static void check_insn(Chk* c, Node* n) {
err(n->loc, "'close' requires an integer handle working value");
return;
}
+ if (st->wv->kind == TY_INTLIT) {
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = ty_int(64);
+ st->wv = ty_int(64);
+ }
emit(c, I_CLOSE, n->loc);
st->wv = ty_int(64);
return;
}
+ case A_USE: {
+ if (n->has_use_lit) {
+ Insn* in = emit(c, I_USE, n->loc);
+ in->b = 1;
+ in->imm.kind = IV_UINT;
+ in->imm.u = n->imm.u;
+ return;
+ }
+ if (!ty_is_any_int(st->wv)) {
+ char b[128];
+ err(n->loc, "'use' requires an integer handle working value (found %s)",
+ type_name(st->wv, b, sizeof b));
+ return;
+ }
+ if (st->wv->kind == TY_INTLIT) {
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = ty_int(64);
+ st->wv = ty_int(64);
+ }
+ emit(c, I_USE, n->loc)->b = 0;
+ return;
+ }
+ case A_FLUSH:
+ emit(c, I_FLUSH, n->loc);
+ return;
case A_SYSTEM: {
Type* wv = st->wv;
if (!(wv->kind == TY_ADDR &&
@@ -1316,6 +1347,48 @@ static void check_insn(Chk* c, Node* n) {
st->wv = ty_int(64);
return;
}
+
+ /* ---------------- atomics ---------------- */
+ case A_XCHG: {
+ FocusEnt* f = focus_top(c);
+ if (f->is_frame) {
+ err(n->loc, "the frame root holds no value; move to a cell first");
+ return;
+ }
+ Type* cell = f->cell;
+ if (cell->kind == TY_ARR) {
+ err(n->loc, "cannot exchange a whole array; move to an element first");
+ return;
+ }
+ if (cell->kind == TY_NOTHING) {
+ err(n->loc, "cannot exchange with a cell that holds no value");
+ return;
+ }
+ if (cell->kind == TY_FUNC) {
+ err(n->loc, "transform reference cells cannot be exchanged");
+ return;
+ }
+ Type* wv = st->wv;
+ if (!type_eq(cell, wv)) {
+ /* the working value must carry exactly the cell's type; integer
+ literals and float64 are materialized first (like write) */
+ int ok = 0;
+ if (wv->kind == TY_INTLIT && intlit_fits(wv->litval, wv->litneg, cell)) ok = 1;
+ else if (wv->kind == TY_FLOAT64 && cell->kind == TY_FLOAT32) ok = 1;
+ if (!ok) {
+ char b1[128], b2[128];
+ err(n->loc, "cannot exchange %s with a cell of type %s; use 'convert'",
+ type_name(wv, b1, sizeof b1), type_name(cell, b2, sizeof b2));
+ return;
+ }
+ Insn* cv = emit(c, I_CONV, n->loc);
+ cv->t = cell;
+ }
+ Insn* in = emit(c, I_XCHG, n->loc);
+ in->t = cell;
+ st->wv = cell;
+ return;
+ }
}
}
@@ -1383,6 +1456,10 @@ static void collect_locals(Chk* c, FnAst* fa) {
FnIR* fn = c->fn;
fn->nlocals = 0;
for (size_t i = 0; i < fa->nparams; i++) {
+ if (fn->nlocals >= MAX_LOCALS) {
+ err(fa->params[i].loc, "too many cells in one transformation (limit %d)", MAX_LOCALS);
+ break;
+ }
if (find_local(c, fa->params[i].name) >= 0) {
err(fa->params[i].loc, "duplicate parameter '%s'", fa->params[i].name);
continue;
@@ -1398,7 +1475,7 @@ static void collect_locals(Chk* c, FnAst* fa) {
loc->type = t;
loc->is_param = 1;
}
- for (size_t i = 0; i < fa->nbody && fn->nlocals < MAX_LOCALS; i++) {
+ for (size_t i = 0; i < fa->nbody; i++) {
Node* n = &fa->body[i];
const char* name = NULL;
if (n->kind == A_MAKE_VALUE && n->name) name = n->name;
@@ -1406,6 +1483,10 @@ static void collect_locals(Chk* c, FnAst* fa) {
else if (n->kind == A_MAKE_STRING && n->name) name = n->name;
else if (n->kind == A_MAKE_DYN && n->name2) name = n->name2;
if (!name) continue;
+ if (fn->nlocals >= MAX_LOCALS) {
+ err(n->loc, "too many cells in one transformation (limit %d)", MAX_LOCALS);
+ break;
+ }
if (find_local(c, name) >= 0) {
err(n->loc, "duplicate cell '%s' in transformation '%s'", name, fa->name);
continue;
@@ -1415,10 +1496,6 @@ static void collect_locals(Chk* c, FnAst* fa) {
err(n->loc, "cell '%s' would be an unsized array", name);
ty = ty_nothing();
}
- if (fn->nlocals >= MAX_LOCALS) {
- err(n->loc, "too many cells in one transformation (limit %d)", MAX_LOCALS);
- break;
- }
LocalIR* loc = &fn->locals[fn->nlocals++];
memset(loc, 0, sizeof *loc);
loc->name = (char*)name;
diff --git a/src/ir.h b/src/ir.h
index 4c81b77..25994a0 100644
--- a/src/ir.h
+++ b/src/ir.h
@@ -52,11 +52,14 @@ typedef enum {
I_INPUT,
I_OUTPUT, /* t = working value type */
I_OUTTEXT,
- I_OPEN, /* b = mode 0 read, 1 write, 2 both */
+ I_OPEN, /* b = mode 0 read, 1 write, 2 both, 3 append */
I_CLOSE,
+ I_USE, /* b = 0 handle in working value, 1 imm handle */
+ I_FLUSH,
I_SYSTEM,
I_SPAWN, /* a = fn index */
- I_WAIT
+ I_WAIT,
+ I_XCHG /* t = cell type; atomic exchange of wv with the focused cell */
} IROp;
typedef enum {
diff --git a/src/lex.c b/src/lex.c
index eca7544..c1e9806 100644
--- a/src/lex.c
+++ b/src/lex.c
@@ -57,8 +57,9 @@ static const Kw k_keywords[] = {
{"divide", T_DIVIDE}, {"remainder", T_REMAINDER}, {"negate", T_NEGATE},
{"push", T_PUSH}, {"pop", T_POP}, {"swap", T_SWAP}, {"compare", T_COMPARE},
{"convert", T_CONVERT}, {"jump", T_JUMP}, {"call", T_CALL}, {"return", T_RETURN},
- {"stop", T_STOP}, {"input", T_INPUT}, {"output", T_OUTPUT}, {"open", T_OPEN},
+ {"stop", T_STOP}, {"input", T_INPUT}, {"output", T_OUTPUT}, {"open", T_OPEN},
{"close", T_CLOSE}, {"system", T_SYSTEM}, {"spawn", T_SPAWN}, {"wait", T_WAIT},
+ {"use", T_USE}, {"flush", T_FLUSH}, {"exchange", T_EXCHANGE},
{"define", T_DEFINE}, {"end", T_END}, {"mark", T_MARK},
{"nothing", T_NOTHING}, {"bool", T_BOOL}, {"char", T_CHAR_T},
{"int8", T_INT8}, {"int16", T_INT16}, {"int32", T_INT32}, {"int64", T_INT64},
@@ -69,6 +70,7 @@ static const Kw k_keywords[] = {
{"true", T_TRUE}, {"false", T_FALSE},
{"of", T_OF}, {"when", T_WHEN}, {"result", T_RESULT},
{"index", T_INDEX}, {"at", T_AT}, {"text", T_TEXT}, {"both", T_BOTH},
+ {"append", T_APPEND},
{"less", T_LESS}, {"equal", T_EQUAL}, {"greater", T_GREATER}, {"different", T_DIFFERENT},
{"less-or-equal", T_LESS_OR_EQUAL}, {"greater-or-equal", T_GREATER_OR_EQUAL},
{NULL, T_EOF}
diff --git a/src/lex.h b/src/lex.h
index 09ab725..f37450a 100644
--- a/src/lex.h
+++ b/src/lex.h
@@ -23,6 +23,7 @@ typedef enum {
T_JUMP, T_CALL, T_RETURN, T_STOP,
T_INPUT, T_OUTPUT, T_OPEN, T_CLOSE, T_SYSTEM,
T_SPAWN, T_WAIT,
+ T_USE, T_FLUSH, T_EXCHANGE,
T_DEFINE, T_END, T_MARK,
/* type words */
@@ -34,7 +35,7 @@ typedef enum {
T_TRUE, T_FALSE,
/* structural / argument words */
- T_OF, T_WHEN, T_RESULT, T_INDEX, T_AT, T_TEXT, T_BOTH,
+ T_OF, T_WHEN, T_RESULT, T_INDEX, T_AT, T_TEXT, T_BOTH, T_APPEND,
T_LESS, T_EQUAL, T_GREATER, T_DIFFERENT, T_LESS_OR_EQUAL, T_GREATER_OR_EQUAL
} TokKind;
diff --git a/src/main.c b/src/main.c
index 370753f..9d254da 100644
--- a/src/main.c
+++ b/src/main.c
@@ -14,7 +14,7 @@
#include <time.h>
#include <unistd.h>
-#define CRYN_VERSION "0.1.2"
+#define CRYN_VERSION "0.2.0"
static void usage(void) {
printf(
diff --git a/src/parse.c b/src/parse.c
index 1f6d628..887ef60 100644
--- a/src/parse.c
+++ b/src/parse.c
@@ -281,16 +281,30 @@ static Node* parse_command(Parser* p) {
next(p);
n = node_new(p, A_OPEN);
Token* m = peek(p);
- if (m->kind == T_READ || m->kind == T_WRITE || m->kind == T_BOTH) {
+ if (m->kind == T_READ || m->kind == T_WRITE || m->kind == T_BOTH ||
+ m->kind == T_APPEND) {
next(p);
n->mode = m->kind;
} else {
- crn_error(m->loc, "expected 'read', 'write', or 'both' after 'open'");
+ crn_error(m->loc, "expected 'read', 'write', 'both', or 'append' after 'open'");
n->mode = T_READ;
}
return n;
}
case T_CLOSE: next(p); return node_new(p, A_CLOSE);
+ case T_USE: {
+ next(p);
+ n = node_new(p, A_USE);
+ Token* v = peek(p);
+ if (v->kind == T_INT) {
+ if (v->neg) crn_error(v->loc, "handle literal cannot be negative");
+ next(p);
+ n->imm = imm_from_token(v);
+ n->has_use_lit = 1;
+ }
+ return n;
+ }
+ case T_FLUSH: next(p); return node_new(p, A_FLUSH);
case T_SYSTEM: next(p); return node_new(p, A_SYSTEM);
case T_SPAWN: {
next(p);
@@ -299,6 +313,7 @@ static Node* parse_command(Parser* p) {
return n;
}
case T_WAIT: next(p); return node_new(p, A_WAIT);
+ case T_EXCHANGE: next(p); return node_new(p, A_XCHG);
case T_MARK: {
next(p);
n = node_new(p, A_MARK);
diff --git a/src/rt/cryn_rt.c b/src/rt/cryn_rt.c
index 1bd98b0..be8dbb8 100644
--- a/src/rt/cryn_rt.c
+++ b/src/rt/cryn_rt.c
@@ -59,6 +59,106 @@ typedef struct CrHdr {
#define CR_MAGIC 0x4352594E424C4BULL /* "CRYNBLK" */
+/* Registry of live block addresses. Validation must not read the header
+ of a block that free() may already have returned to the allocator:
+ reading freed memory is undefined in C, so stale-address detection
+ (double free, indexing through a freed or shifted address) goes
+ through this table instead. One global table, mutex protected,
+ because blocks can be shared across spawned contexts. */
+typedef struct CrReg {
+ void** keys; /* NULL = empty, CR_TOMB = deleted */
+ size_t cap; /* power of two */
+ size_t used; /* live + tombstones */
+ pthread_mutex_t mu;
+} CrReg;
+
+#define CR_TOMB ((void*)1)
+
+static CrReg cr_reg = {0, 0, 0, PTHREAD_MUTEX_INITIALIZER};
+static pthread_once_t cr_reg_once = PTHREAD_ONCE_INIT;
+
+static size_t reg_slot(void* p, size_t cap) {
+ uintptr_t x = (uintptr_t)p >> 4; /* malloc blocks are 16-aligned */
+ x ^= x >> 17;
+ x *= 0x9E3779B97F4A7C15ULL;
+ x ^= x >> 29;
+ return (size_t)x & (cap - 1);
+}
+
+static void reg_init(void) {
+ cr_reg.cap = 256;
+ cr_reg.keys = malloc(cr_reg.cap * sizeof(void*));
+ if (!cr_reg.keys) crn_die("out of memory");
+ memset(cr_reg.keys, 0, cr_reg.cap * sizeof(void*));
+ cr_reg.used = 0;
+}
+
+static void reg_grow_locked(void) {
+ size_t ncap = cr_reg.cap * 2;
+ void** nk = malloc(ncap * sizeof(void*));
+ if (!nk) crn_die("out of memory");
+ memset(nk, 0, ncap * sizeof(void*));
+ for (size_t i = 0; i < cr_reg.cap; i++) {
+ void* k = cr_reg.keys[i];
+ if (!k || k == CR_TOMB) continue;
+ size_t j = reg_slot(k, ncap);
+ while (nk[j]) j = (j + 1) & (ncap - 1);
+ nk[j] = k;
+ }
+ free(cr_reg.keys);
+ cr_reg.keys = nk;
+ cr_reg.cap = ncap;
+}
+
+/* returns the slot of p, or cap when absent (probing stops at NULL) */
+static size_t reg_find_locked(void* p) {
+ size_t j = reg_slot(p, cr_reg.cap);
+ for (;;) {
+ void* k = cr_reg.keys[j];
+ if (!k) return cr_reg.cap;
+ if (k == p) return j;
+ j = (j + 1) & (cr_reg.cap - 1);
+ }
+}
+
+static void reg_add(void* p) {
+ pthread_once(&cr_reg_once, reg_init);
+ pthread_mutex_lock(&cr_reg.mu);
+ if ((cr_reg.used + 1) * 10 >= cr_reg.cap * 7) {
+ reg_grow_locked();
+ /* collapse tombstones into the new table */
+ cr_reg.used = 0;
+ for (size_t i = 0; i < cr_reg.cap; i++)
+ if (cr_reg.keys[i]) cr_reg.used++;
+ }
+ size_t j = reg_slot(p, cr_reg.cap);
+ while (cr_reg.keys[j] && cr_reg.keys[j] != CR_TOMB) j = (j + 1) & (cr_reg.cap - 1);
+ cr_reg.keys[j] = p;
+ cr_reg.used++;
+ pthread_mutex_unlock(&cr_reg.mu);
+}
+
+static int reg_has(void* p) {
+ pthread_once(&cr_reg_once, reg_init);
+ pthread_mutex_lock(&cr_reg.mu);
+ int found = reg_find_locked(p) != cr_reg.cap;
+ pthread_mutex_unlock(&cr_reg.mu);
+ return found;
+}
+
+static int reg_take(void* p) {
+ pthread_once(&cr_reg_once, reg_init);
+ pthread_mutex_lock(&cr_reg.mu);
+ size_t j = reg_find_locked(p);
+ if (j == cr_reg.cap) {
+ pthread_mutex_unlock(&cr_reg.mu);
+ return 0;
+ }
+ cr_reg.keys[j] = CR_TOMB;
+ pthread_mutex_unlock(&cr_reg.mu);
+ return 1;
+}
+
void* crn_alloc(uint64_t nbytes) {
CrHdr* h = malloc(sizeof(CrHdr) + (size_t)nbytes + 16);
if (!h) crn_die("out of memory");
@@ -68,27 +168,30 @@ void* crn_alloc(uint64_t nbytes) {
p += sizeof(CrHdr);
/* keep data 16-aligned: sizeof(CrHdr)==16 on 64-bit */
memset(p, 0, (size_t)nbytes);
+ reg_add(p);
return p;
}
void crn_free(void* p) {
if (!p) crn_die("free of the null address");
- CrHdr* h = (CrHdr*)((unsigned char*)p - sizeof(CrHdr));
- if (h->magic != CR_MAGIC) {
+ if (!reg_take(p)) {
crn_die("free of an address that is not a heap block "
- "(only blocks made with 'make <size> of <type>' or anonymous 'make' can be freed)");
+ "(only live blocks made with 'make <size> of <type>' or anonymous "
+ "'make' can be freed)");
}
+ CrHdr* h = (CrHdr*)((unsigned char*)p - sizeof(CrHdr));
h->magic = 0;
free(h);
}
uint64_t crn_block_n(void* data, uint64_t elemsz) {
if (elemsz == 0) elemsz = 1;
- CrHdr* h = (CrHdr*)((unsigned char*)data - sizeof(CrHdr));
- if (h->magic != CR_MAGIC) {
+ if (!reg_has(data)) {
crn_die("index into memory that is not a heap block "
- "(only blocks made with 'make <size> of <type>' support bounds checks)");
+ "(only the base address of a live block made with "
+ "'make <size> of <type>' is bounds checked)");
}
+ CrHdr* h = (CrHdr*)((unsigned char*)data - sizeof(CrHdr));
return h->size / elemsz;
}
@@ -144,6 +247,63 @@ void crn_st_f64(double* p, double v) {
crn_st_u64((uint64_t*)(void*)p, b);
}
+/* ---- atomic exchange (acquire-release) ---- */
+
+uint8_t crn_xch_u8(uint8_t* p, uint8_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+ return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+ /* fallback for hosts without the builtins: a plain swap (correct for
+ a single context; such hosts are not targets for racing programs) */
+ uint8_t t = *p;
+ *p = v;
+ return t;
+#endif
+}
+uint16_t crn_xch_u16(uint16_t* p, uint16_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+ return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+ uint16_t t = *p;
+ *p = v;
+ return t;
+#endif
+}
+uint32_t crn_xch_u32(uint32_t* p, uint32_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+ return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+ uint32_t t = *p;
+ *p = v;
+ return t;
+#endif
+}
+uint64_t crn_xch_u64(uint64_t* p, uint64_t v) {
+#if defined(__GNUC__) || defined(__clang__)
+ return __atomic_exchange_n(p, v, __ATOMIC_ACQ_REL);
+#else
+ uint64_t t = *p;
+ *p = v;
+ return t;
+#endif
+}
+float crn_xch_f32(float* p, float v) {
+ uint32_t b, nv;
+ memcpy(&nv, &v, sizeof nv);
+ b = crn_xch_u32((uint32_t*)(void*)p, nv);
+ float r;
+ memcpy(&r, &b, sizeof r);
+ return r;
+}
+double crn_xch_f64(double* p, double v) {
+ uint64_t b, nv;
+ memcpy(&nv, &v, sizeof nv);
+ b = crn_xch_u64((uint64_t*)(void*)p, nv);
+ double r;
+ memcpy(&r, &b, sizeof r);
+ return r;
+}
+
/* ---- conversions ---- */
int64_t crn_d2i64(double d) {
@@ -249,33 +409,52 @@ int64_t crn_open(uint8_t* path, int mode) {
switch (mode) {
case 0: m = "rb"; break;
case 1: m = "wb"; break;
- default: m = "w+b"; break;
+ case 2: m = "w+b"; break;
+ case 3: m = "ab"; break;
+ default: return -1;
}
FILE* f = fopen((const char*)path, m);
if (!f) return -1;
- /* one open stream table per context: refuse politely at the limit */
- if (cr_ctx.nfiles >= CR_FILES_MAX) {
- fclose(f);
- return -1;
+ /* find a free slot; handles are slot+1 */
+ for (int i = 0; i < CR_FILES_MAX; i++) {
+ if (cr_ctx.files[i]) continue;
+ cr_ctx.files[i] = f;
+ cr_ctx.fmodes[i] = (unsigned char)mode;
+ return i + 1;
}
- /* success: route this context's streams through the file */
- if (mode == 0) cr_ctx.in = f;
- else if (mode == 1) cr_ctx.out = f;
- else { cr_ctx.in = f; cr_ctx.out = f; }
- cr_ctx.files[cr_ctx.nfiles++] = f;
- return (int64_t)(uintptr_t)f;
+ fclose(f);
+ return -1; /* table full */
}
-int64_t crn_close(int64_t h) {
- FILE* f = (FILE*)(uintptr_t)h;
- int found = -1;
- for (int i = 0; i < cr_ctx.nfiles; i++) {
- if (cr_ctx.files[i] == f) { found = i; break; }
+void crn_use(int64_t h) {
+ if (h == 0) {
+ cr_ctx.in = stdin;
+ cr_ctx.out = stdout;
+ return;
+ }
+ if (h < 1 || h > CR_FILES_MAX || !cr_ctx.files[h - 1])
+ crn_die("use of a handle that is not open in this context "
+ "(handles come from 'open'; 0 means the standard streams)");
+ FILE* f = cr_ctx.files[h - 1];
+ switch (cr_ctx.fmodes[h - 1]) {
+ case 0: cr_ctx.in = f; break; /* read */
+ case 2: cr_ctx.in = f; cr_ctx.out = f; break; /* both */
+ default: cr_ctx.out = f; break; /* write / append */
}
- if (found < 0)
+}
+
+void crn_flush(void) {
+ if (fflush(cr_ctx.out) != 0) crn_die("flush failed");
+}
+
+int64_t crn_close(int64_t h) {
+ if (h == 0)
+ crn_die("close of handle 0 (the standard streams cannot be closed)");
+ if (h < 1 || h > CR_FILES_MAX || !cr_ctx.files[h - 1])
crn_die("close of a handle that is not open in this context "
"(handles come from 'open' and are closed once)");
- cr_ctx.files[found] = cr_ctx.files[--cr_ctx.nfiles];
+ FILE* f = cr_ctx.files[h - 1];
+ cr_ctx.files[h - 1] = NULL;
if (cr_ctx.in == f) cr_ctx.in = stdin;
if (cr_ctx.out == f) cr_ctx.out = stdout;
return fclose(f) == 0 ? 0 : -1;
diff --git a/src/rt/cryn_rt.h b/src/rt/cryn_rt.h
index f570181..939ce6b 100644
--- a/src/rt/cryn_rt.h
+++ b/src/rt/cryn_rt.h
@@ -43,8 +43,8 @@ typedef struct CrCtx {
uint64_t fh_sp;
FILE* in; /* current input stream (default stdin) */
FILE* out; /* current output stream (default stdout) */
- FILE* files[CR_FILES_MAX];
- int nfiles;
+ FILE* files[CR_FILES_MAX]; /* open files; slot i has handle i+1 */
+ unsigned char fmodes[CR_FILES_MAX]; /* 0 read, 1 write, 2 both, 3 append */
pthread_t spawned[CR_SPAWN_MAX];
int nspawned;
} CrCtx;
@@ -86,6 +86,19 @@ void crn_st_u64(uint64_t* p, uint64_t v);
void crn_st_f32(float* p, float v);
void crn_st_f64(double* p, double v);
+/* Atomic exchange (acquire-release). `exchange` swaps the working value
+ with the focused cell in one indivisible step and is the only ordering
+ primitive besides wait: an exchange that stores a value releases the
+ accesses that preceded it in program order, and one that loads a value
+ acquires for the accesses that follow it. It never tears. This is
+ enough to build locks (test-and-set). */
+uint8_t crn_xch_u8(uint8_t* p, uint8_t v);
+uint16_t crn_xch_u16(uint16_t* p, uint16_t v);
+uint32_t crn_xch_u32(uint32_t* p, uint32_t v);
+uint64_t crn_xch_u64(uint64_t* p, uint64_t v);
+float crn_xch_f32(float* p, float v);
+double crn_xch_f64(double* p, double v);
+
int64_t crn_d2i64(double d);
uint64_t crn_d2u64(double d);
int64_t crn_d2i8(double d);
@@ -105,7 +118,16 @@ void crn_out_addr(uint64_t a);
void crn_out_text(uint8_t* p);
int64_t crn_input(void);
+/* open modes: 0 read, 1 write, 2 both, 3 append.
+ Returns a handle >= 1, or -1 on failure. A handle is an index into the
+ context's file table; closing a handle frees its number for reuse. */
int64_t crn_open(uint8_t* path, int mode);
+/* Route this context's streams through a handle: the readable side of the
+ file becomes the input stream, the writable side the output stream;
+ sides the mode does not support keep their current stream. Handle 0 is
+ the standard streams (input: stdin, output: stdout). */
+void crn_use(int64_t h);
+void crn_flush(void);
int64_t crn_close(int64_t h);
int64_t crn_system(uint8_t* cmd);
void crn_stop(int64_t code) CRN_NORETURN;
diff --git a/tests/20_files.cryn b/tests/20_files.cryn
index ab18000..b80142f 100644
--- a/tests/20_files.cryn
+++ b/tests/20_files.cryn
@@ -1,53 +1,28 @@
-[# file output and input round trip #]
+[# file output and input round trip through first-class handles #]
define main
make string "cryn_test_20.txt" path
+ make string "cryn was here\n" data
address path
open write
compare 0
jump fail when less
push
-
- make string "cryn was here\n" data
- make int64 i
- set 0
- move i
- write
- set 0
-
- mark wr
- move i
- read
- compare 15
- jump written when greater-or-equal
- move i
- read
- move data
- move index
- read
- output
- move i
- read
- add 1
- move i
- write
- set 0
- jump wr
-
- mark written
+ use
+ address data
+ output text
+ flush
pop
close
- home
compare 0
- jump fail when less
+ jump fail when different
address path
open read
- home
compare 0
jump fail when less
push
-
+ use
set 0
mark rd
input
@@ -57,7 +32,6 @@ define main
output
jump rd
mark readDone
-
pop
close
stop
diff --git a/tests/28_handles.cryn b/tests/28_handles.cryn
new file mode 100644
index 0000000..ec943de
--- /dev/null
+++ b/tests/28_handles.cryn
@@ -0,0 +1,103 @@
+[# first-class handles: two files open at once, stream routing, append #]
+define main
+ make string "cryn_test_28a.txt" pa
+ make string "cryn_test_28b.txt" pb
+ make string "one\n" s1
+ make string "two\n" s2
+ make string "\n" nl
+ make int64 ha
+ make int64 hb
+
+ address pa
+ open write
+ home
+ compare 0
+ jump fail when less
+ move ha
+ write
+
+ address pb
+ open write
+ home
+ compare 0
+ jump fail when less
+ move hb
+ write
+
+ move ha
+ read
+ use
+ home
+ address s1
+ output text
+ flush
+
+ move hb
+ read
+ use
+ home
+ address s2
+ output text
+ flush
+
+ move ha
+ read
+ close
+ home
+ compare 0
+ jump fail when different
+
+ move hb
+ read
+ close
+ home
+ compare 0
+ jump fail when different
+
+ address pa
+ open append
+ home
+ compare 0
+ jump fail when less
+ push
+ use
+ home
+ address s2
+ output text
+ flush
+ pop
+ close
+ home
+ compare 0
+ jump fail when different
+
+ use 0
+ address pa
+ open read
+ home
+ compare 0
+ jump fail when less
+ push
+ use
+ home
+ set 0
+ mark rd
+ input
+ compare -1
+ jump fin when equal
+ convert uint8
+ output
+ jump rd
+ mark fin
+ pop
+ close
+
+ address nl
+ output text
+ set 0
+ stop
+
+ mark fail
+ set 1
+ stop
+end
diff --git a/tests/29_exchange.cryn b/tests/29_exchange.cryn
new file mode 100644
index 0000000..d679a2a
--- /dev/null
+++ b/tests/29_exchange.cryn
@@ -0,0 +1,75 @@
+[# atomic exchange: values swap in one indivisible step, and a spinlock
+ built from exchange orders two workers around a shared counter #]
+make int64 counter
+make int64 lock
+make int64 probe
+make string "\n" nl
+
+define worker
+ make int64 k
+ set 0
+ move k
+ write
+ mark w
+ move k
+ read
+ compare 100
+ jump done when greater-or-equal
+
+ mark again
+ set 1
+ move lock
+ exchange
+ compare 0
+ jump again when different
+
+ move counter
+ read
+ add 1
+ move counter
+ write
+
+ set 0
+ move lock
+ exchange
+ set 0
+
+ move k
+ read
+ add 1
+ move k
+ write
+ set 0
+ jump w
+
+ mark done
+ return
+end
+
+define main
+ set 5
+ move probe
+ write
+ set 9
+ move probe
+ exchange
+ output
+ address nl
+ output text
+ move probe
+ read
+ output
+ address nl
+ output text
+
+ spawn worker
+ spawn worker
+ wait
+
+ move counter
+ read
+ output
+ address nl
+ output text
+ return
+end
diff --git a/tests/errors/exchange_array.cryn b/tests/errors/exchange_array.cryn
new file mode 100644
index 0000000..3b3d532
--- /dev/null
+++ b/tests/errors/exchange_array.cryn
@@ -0,0 +1,7 @@
+[# cannot exchange a whole array #]
+define main
+ make array 4 of int64 a
+ move a
+ exchange
+ return
+end
diff --git a/tests/errors/exchange_frame.cryn b/tests/errors/exchange_frame.cryn
new file mode 100644
index 0000000..8768726
--- /dev/null
+++ b/tests/errors/exchange_frame.cryn
@@ -0,0 +1,6 @@
+[# the frame root holds no value to exchange with #]
+define main
+ set 1
+ exchange
+ return
+end
diff --git a/tests/errors/exchange_type.cryn b/tests/errors/exchange_type.cryn
new file mode 100644
index 0000000..ae14151
--- /dev/null
+++ b/tests/errors/exchange_type.cryn
@@ -0,0 +1,11 @@
+[# exchange requires matching types #]
+define main
+ make int64 x
+ set 1
+ move x
+ write
+ set 2.5
+ move x
+ exchange
+ return
+end
diff --git a/tests/errors/use_non_int.cryn b/tests/errors/use_non_int.cryn
new file mode 100644
index 0000000..e0e8515
--- /dev/null
+++ b/tests/errors/use_non_int.cryn
@@ -0,0 +1,6 @@
+[# 'use' requires an integer handle #]
+define main
+ set 1.5
+ use
+ return
+end
diff --git a/tests/expected/28_handles.txt b/tests/expected/28_handles.txt
new file mode 100644
index 0000000..edd1789
--- /dev/null
+++ b/tests/expected/28_handles.txt
@@ -0,0 +1,3 @@
+one
+two
+
diff --git a/tests/expected/29_exchange.txt b/tests/expected/29_exchange.txt
new file mode 100644
index 0000000..17b70b9
--- /dev/null
+++ b/tests/expected/29_exchange.txt
@@ -0,0 +1,3 @@
+5
+9
+200