vextoly/cryn
0.1.1
Commit ee91cb1 · Vextoly · 2026-10-04T19:48:49+02:00
Comments
No comments yet.
Diff
diff --git a/README.md b/README.md
index 138b779..d6caf2c 100644
--- a/README.md
+++ b/README.md
@@ -158,11 +158,12 @@ The runtime (`src/rt/cryn_rt.*`) provides only:
|-----------------|----------------------------------|--------------------------------|
| `malloc`/`free` | `make <size> of`, anonymous `make`, `free` | 16-byte header for validation and bounds checks |
| value stack | `push`, `pop`, `swap`, `call` | growable array, per thread |
-| focus history | `move` (relative), `back`, `home`| fixed 256 entries per frame |
+| 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 |
| `system()` | `system` | shell escape |
| pthreads | `spawn`, `wait` | join provides the only 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) |
There is no garbage collector, no interpreter, and no mandatory runtime
library beyond the above.
@@ -190,12 +191,16 @@ These are honest boundaries of the first implementation, not pretenses:
for it.
* **Concurrency is minimal.** `spawn` starts a thread running a
zero-parameter transformation; `wait` joins all spawned contexts of the
- current one. Shared world cells may be accessed concurrently; v1 gives
- no atomicity guarantees beyond `wait` acting as a full join/sync point.
- Communication of results happens through shared world cells.
+ 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
+ through shared world cells.
* **Indirect calls** (through `transform` references) are only allowed to
zero-parameter transformations; checked at runtime when the reference is
- not statically known.
+ 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.
diff --git a/gitignore b/gitignore
new file mode 100644
index 0000000..f265a3c
--- /dev/null
+++ b/gitignore
@@ -0,0 +1,22 @@
+# build artifacts
+cryn
+tools/embed
+src/rt_src.h
+*.o
+*.a
+*.so
+
+# test/run artifacts
+tests/cryn_test_*.txt
+*.exe
+
+# generated compiler output
+out.c
+*.c.gen.c
+
+# editors and OS
+.vscode/
+.idea/
+*.swp
+*~
+.DS_Store
diff --git a/src/be_c.c b/src/be_c.c
index 51e4ba9..eed7925 100644
--- a/src/be_c.c
+++ b/src/be_c.c
@@ -15,7 +15,7 @@ static const char* ctype(const Type* t) {
switch (t->kind) {
case TY_NOTHING: return "uint8_t";
case TY_BOOL: return "uint8_t";
- case TY_CHAR: return "uint32_t";
+ case TY_CHAR: return "uint8_t";
case TY_INT8: return "int8_t";
case TY_INT16: return "int16_t";
case TY_INT32: return "int32_t";
@@ -102,36 +102,67 @@ static void emit_local_decl(Buf* o, LocalIR* l) {
/* statement emission */
/* ------------------------------------------------------------------ */
+/* width-tagged relaxed atomic access for a scalar type */
+static const char* ld_fn(const Type* t) {
+ switch (t->kind) {
+ case TY_BOOL: case TY_CHAR: case TY_INT8: case TY_UINT8: return "crn_ld_u8";
+ case TY_INT16: case TY_UINT16: return "crn_ld_u16";
+ case TY_INT32: case TY_UINT32: case TY_FLOAT32: return "crn_ld_u32";
+ default: return "crn_ld_u64";
+ }
+}
+static const char* st_fn(const Type* t) {
+ switch (t->kind) {
+ case TY_BOOL: case TY_CHAR: case TY_INT8: case TY_UINT8: return "crn_st_u8";
+ case TY_INT16: case TY_UINT16: return "crn_st_u16";
+ case TY_INT32: case TY_UINT32: case TY_FLOAT32: return "crn_st_u32";
+ default: return "crn_st_u64";
+ }
+}
+
+/* unsigned C type matching a scalar's width, for atomic store helpers */
+static const char* uct(const Type* t) {
+ switch (t->kind) {
+ case TY_BOOL: case TY_CHAR: case TY_INT8: case TY_UINT8: return "uint8_t";
+ case TY_INT16: case TY_UINT16: return "uint16_t";
+ case TY_INT32: case TY_UINT32: return "uint32_t";
+ default: return "uint64_t";
+ }
+}
+
static void emit_store(Buf* o, Insn* in) {
const Type* t = in->t;
- const char* ct = ctype(t);
+ const char* u = uct(t);
switch (in->b) {
case SC_EXACT:
if (ty_is_float(t)) {
- buf_printf(o, " *(%s*)F = (%s)wv.d;\n", ct, ct);
+ if (t->kind == TY_FLOAT32)
+ buf_printf(o, " crn_st_f32((float*)F, (float)wv.d);\n");
+ else
+ buf_printf(o, " crn_st_f64((double*)F, wv.d);\n");
} else if (t->kind == TY_FUNC) {
+ /* 16-byte reference cells are not race-defined; plain access */
buf_printf(o, " *(CrFnRef**)F = wv.p;\n");
} else if (ty_is_signed_int(t)) {
- buf_printf(o, " *(%s*)F = (%s)wv.i;\n", ct, ct);
+ buf_printf(o, " %s((%s*)F, (%s)wv.i);\n", st_fn(t), u, u);
} else {
- buf_printf(o, " *(%s*)F = (%s)wv.u;\n", ct, ct);
+ buf_printf(o, " %s((%s*)F, (%s)wv.u);\n", st_fn(t), u, u);
}
break;
case SC_INTLIT:
- if (ty_is_float(t)) {
- buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
- } else {
- buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
- }
+ buf_printf(o, " %s((%s*)F, (%s)(int64_t)wv.u);\n", st_fn(t), u, u);
break;
case SC_INTLIT_BOOL:
- buf_printf(o, " *(uint8_t*)F = (uint8_t)(int64_t)wv.u;\n");
+ buf_printf(o, " crn_st_u8((uint8_t*)F, (uint8_t)(int64_t)wv.u);\n");
break;
case SC_INTLIT_FLOAT:
- buf_printf(o, " *(%s*)F = (%s)(int64_t)wv.u;\n", ct, ct);
+ if (t->kind == TY_FLOAT32)
+ buf_printf(o, " crn_st_f32((float*)F, (float)(int64_t)wv.u);\n");
+ else
+ buf_printf(o, " crn_st_f64((double*)F, (double)(int64_t)wv.u);\n");
break;
case SC_F32:
- buf_printf(o, " *(float*)F = (float)wv.d;\n");
+ buf_printf(o, " crn_st_f32((float*)F, (float)wv.d);\n");
break;
default:
buf_printf(o, " /* store coercion %d */\n", in->b);
@@ -305,15 +336,23 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
buf_puts(o, " crn_free(wv.p);\n");
break;
case I_LOAD: {
- const char* ct = ctype(in->t);
- if (ty_is_float(in->t))
- buf_printf(o, " wv.d = (double)*(%s*)F;\n", ct);
- else if (in->t->kind == TY_FUNC)
+ const Type* t = in->t;
+ if (t->kind == TY_FLOAT32)
+ buf_printf(o, " wv.d = (double)crn_ld_f32((const float*)F);\n");
+ else if (t->kind == TY_FLOAT64)
+ buf_printf(o, " wv.d = crn_ld_f64((const double*)F);\n");
+ else if (t->kind == TY_FUNC)
buf_puts(o, " wv.p = *(CrFnRef**)F;\n");
- else if (ty_is_signed_int(in->t))
- buf_printf(o, " wv.i = (int64_t)*(%s*)F;\n", ct);
+ else if (t->kind == TY_INT8)
+ buf_printf(o, " wv.i = (int64_t)(int8_t)crn_ld_u8((const uint8_t*)F);\n");
+ else if (t->kind == TY_INT16)
+ buf_printf(o, " wv.i = (int64_t)(int16_t)crn_ld_u16((const uint16_t*)F);\n");
+ else if (t->kind == TY_INT32)
+ buf_printf(o, " wv.i = (int64_t)(int32_t)crn_ld_u32((const uint32_t*)F);\n");
+ else if (t->kind == TY_INT64)
+ buf_printf(o, " wv.i = (int64_t)crn_ld_u64((const uint64_t*)F);\n");
else
- buf_printf(o, " wv.u = (uint64_t)*(%s*)F;\n", ct);
+ buf_printf(o, " wv.u = (uint64_t)%s((const %s*)F);\n", ld_fn(t), uct(t));
cur = in->t;
break;
}
@@ -347,8 +386,14 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
}
if (in->op == I_DIV)
buf_puts(o, "wv.d = wv.d / b; }\n");
- else if (in->op == I_MOD)
- buf_puts(o, "wv.d = wv.d - b * (wv.d / b); }\n");
+ else if (in->op == I_MOD) {
+ if (t->kind == TY_FLOAT32)
+ /* compute and round in single precision like the
+ other float32 operations */
+ buf_puts(o, "wv.d = (double)((float)wv.d - (float)b * ((float)wv.d / (float)b)); }\n");
+ else
+ buf_puts(o, "wv.d = wv.d - b * (wv.d / b); }\n");
+ }
else if (t->kind == TY_FLOAT32)
buf_printf(o, "wv.d = (double)((float)wv.d %c (float)b); }\n", opstr);
else
@@ -396,11 +441,12 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
opstr, esz);
}
} else if (in->a == 1 && in->op == I_SUB) {
- /* pointer difference */
+ /* pointer difference: computed on the raw 64-bit values and
+ divided by the element size (truncating toward zero), so
+ it is defined for any two addresses of any provenance
+ (C pointer subtraction of unrelated objects is UB) */
uint64_t esz = addr_elem_size(cur);
- buf_printf(o, " wv.i = (int64_t)(((%s*)wv.p) - ((%s*)crn_pop().p));\n",
- ctype(cur->inner->kind == TY_ARR ? cur->inner->inner : cur->inner),
- ctype(cur->inner->kind == TY_ARR ? cur->inner->inner : cur->inner));
+ buf_printf(o, " wv.i = (int64_t)(wv.u - crn_pop().u) / (int64_t)%" PRIu64 ";\n", esz);
(void)esz;
} else if (ty_is_signed_int(t) || t->kind == TY_INTLIT) {
const char* ct = ctype(t);
@@ -588,18 +634,20 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
break;
}
case I_CALLIND:
+ /* the callee's result is discarded; the caller's working value
+ is preserved (an indirect call is a call of unknown result,
+ checked as result-nothing) */
buf_puts(o, " { CrFnRef* r = (CrFnRef*)wv.p; "
"if (r->np != 0) crn_die(\"indirect call of a transformation "
- "that takes parameters\"); r->fn(); wv.u = 0; }\n");
- cur = ty_nothing();
+ "that takes parameters\"); r->fn(); }\n");
break;
case I_RET:
buf_puts(o, " cr_ctx.fh_sp = cr_fh0; return wv;\n");
break;
case I_STOP:
- if (ty_is_float(cur))
+ if (ty_is_float(in->t))
buf_puts(o, " crn_stop(crn_d2i64(wv.d));\n");
- else if (cur->kind == TY_NOTHING || cur->kind == TY_FUNC)
+ else if (in->t->kind == TY_NOTHING || in->t->kind == TY_FUNC)
buf_puts(o, " crn_stop(0);\n");
else
buf_puts(o, " crn_stop((int64_t)wv.u);\n");
@@ -609,18 +657,25 @@ static void emit_fn(Buf* o, ProgramIR* p, int fi) {
cur = ty_int(64);
break;
case I_OUTPUT:
- if (ty_is_float(cur)) {
- if (cur->kind == TY_FLOAT32)
+ if (ty_is_float(in->t)) {
+ if (in->t->kind == TY_FLOAT32)
buf_puts(o, " crn_out_f32((float)wv.d);\n");
else
buf_puts(o, " crn_out_f64(wv.d);\n");
- } else if (cur->kind == TY_CHAR || cur->kind == TY_UINT8) {
+ } else if (in->t->kind == TY_CHAR || in->t->kind == TY_UINT8) {
buf_puts(o, " crn_out_char((unsigned)wv.u);\n");
- } else if (cur->kind == TY_BOOL) {
+ } else if (in->t->kind == TY_BOOL) {
buf_puts(o, " crn_out_bool((int)wv.u);\n");
- } else if (cur->kind == TY_ADDR) {
+ } else if (in->t->kind == TY_ADDR) {
buf_puts(o, " crn_out_addr(wv.u);\n");
- } else if (cur->kind == TY_INTLIT || ty_is_signed_int(cur)) {
+ } else if (in->t->kind == TY_INTLIT) {
+ /* an unadapted literal prints signed when it fits int64,
+ unsigned otherwise */
+ if (!in->t->litneg && in->t->litval > (uint64_t)INT64_MAX)
+ buf_puts(o, " crn_out_u64(wv.u);\n");
+ else
+ buf_puts(o, " crn_out_i64((int64_t)wv.u);\n");
+ } else if (ty_is_signed_int(in->t)) {
buf_puts(o, " crn_out_i64((int64_t)wv.u);\n");
} else {
buf_puts(o, " crn_out_u64(wv.u);\n");
@@ -668,10 +723,11 @@ void be_c_emit(ProgramIR* p, Buf* out) {
buf_puts(out, cr_rt_c_src);
buf_putc(out, '\n');
- /* string table */
+ /* string table: writable static byte arrays (anonymous 'make string'
+ yields an ordinary mutable byte array, like every other string) */
for (int i = 0; i < p->nstrs; i++) {
StrIR* s = &p->strs[i];
- buf_printf(out, "static const uint8_t cr_str_%d[] = ", i);
+ buf_printf(out, "static uint8_t cr_str_%d[] = ", i);
emit_c_str(out, s->bytes, s->len + 1);
buf_puts(out, ";\n");
}
diff --git a/src/check.c b/src/check.c
index 51f34bd..ec667f5 100644
--- a/src/check.c
+++ b/src/check.c
@@ -226,13 +226,21 @@ static void st_init(Chk* c) {
static void st_clone(CState* d, CState* s) { *d = *s; }
/* join two static working-value/stack types for control flow merges.
- Integer literals widen to the type they fit; two literals join to int64. */
+ Integer literals widen to the integer-family type they fit (int/uint/
+ char/bool); two literals join to int64. Joining a literal with a float
+ type is rejected: the two paths would carry different representations,
+ so the value must be materialized (set a float literal or convert)
+ before the paths meet. */
+static int join_lit_target(Type* t) {
+ return ty_is_int(t) || t->kind == TY_CHAR || t->kind == TY_BOOL;
+}
+
static Type* join_types(Type* a, Type* b) {
if (type_eq(a, b)) return a;
if (a->kind == TY_INTLIT && b->kind == TY_INTLIT) return ty_int(64);
- if (a->kind == TY_INTLIT && (ty_is_numeric(b) || b->kind == TY_CHAR || b->kind == TY_BOOL))
+ if (a->kind == TY_INTLIT && join_lit_target(b))
return intlit_fits(a->litval, a->litneg, b) ? b : NULL;
- if (b->kind == TY_INTLIT && (ty_is_numeric(a) || a->kind == TY_CHAR || a->kind == TY_BOOL))
+ if (b->kind == TY_INTLIT && join_lit_target(a))
return intlit_fits(b->litval, b->litneg, a) ? a : NULL;
return NULL;
}
@@ -274,8 +282,11 @@ static int st_merge(CState* into, CState* from, Loc loc, const char* what) {
}
if (!type_eq(js, into->stk[i])) { into->stk[i] = js; changed = 1; }
}
- if (from->cmp && !into->cmp) {
- into->cmp = 1;
+ /* comparison validity joins conservatively: valid after the label only
+ if valid on every incoming path (a conditional jump must never read
+ a comparison that one incoming path never performed) */
+ if (into->cmp && !from->cmp) {
+ into->cmp = 0;
changed = 1;
}
return changed ? 2 : 1;
@@ -314,13 +325,22 @@ static Type* numeric_pair_type(Type* a, Type* b) {
if (ty_is_float(a) || ty_is_float(b)) return ty_float64();
if ((ty_is_int(a) || a->kind == TY_CHAR) && (ty_is_int(b) || b->kind == TY_CHAR)) {
if (type_eq(a, b)) return a;
- int aw = a->kind == TY_CHAR ? 32 : ty_int_bits(a);
- int bw = b->kind == TY_CHAR ? 32 : ty_int_bits(b);
+ int aw = a->kind == TY_CHAR ? 8 : ty_int_bits(a);
+ int bw = b->kind == TY_CHAR ? 8 : ty_int_bits(b);
int as = a->kind == TY_CHAR ? 0 : ty_is_signed_int(a);
int bs = b->kind == TY_CHAR ? 0 : ty_is_signed_int(b);
+ if (as != bs) {
+ /* mixed signedness: only safe when the unsigned operand is
+ strictly narrower than the signed one, so every one of its
+ values fits the signed result type. Equal or wider unsigned
+ operands (e.g. uint64 vs int64) must be converted first. */
+ int uw = as ? bw : aw;
+ int sw = as ? aw : bw;
+ if (uw >= sw) return NULL;
+ return ty_int(sw);
+ }
int w = aw > bw ? aw : bw;
- int sg = as || bs;
- return sg ? ty_int(w) : ty_uint(w);
+ return as ? ty_int(w) : ty_uint(w);
}
if (a->kind == TY_BOOL && b->kind == TY_BOOL) return ty_bool();
return NULL;
@@ -1002,6 +1022,17 @@ static void check_insn(Chk* c, Node* n) {
return;
}
int ok;
+ /* addresses convert only to integers and other address types:
+ bit reinterpretation; float<->address is rejected (a numeric
+ conversion of address bits is meaningless) */
+ if (from->kind == TY_ADDR && ty_is_float(to)) {
+ err(n->loc, "cannot convert an address to a float; convert to int64/uint64 first");
+ return;
+ }
+ if (ty_is_float(from) && to->kind == TY_ADDR) {
+ err(n->loc, "cannot convert a float to an address; convert to int64/uint64 first");
+ return;
+ }
if (from->kind == TY_INTLIT) {
/* integer literals convert to any scalar numeric type; the value
wraps modulo the target width like any integer conversion */
@@ -1101,7 +1132,9 @@ static void check_insn(Chk* c, Node* n) {
return;
}
emit(c, I_CALLIND, n->loc);
- st->wv = ty_nothing();
+ /* the callee's result type is not statically known; an indirect
+ call is checked as a call with result nothing, which preserves
+ the caller's working value (the callee's result is discarded) */
return;
}
case A_RET: {
diff --git a/src/rt/cryn_rt.c b/src/rt/cryn_rt.c
index ff9f743..a3c6150 100644
--- a/src/rt/cryn_rt.c
+++ b/src/rt/cryn_rt.c
@@ -101,6 +101,49 @@ void crn_check_range(uint64_t i, uint64_t n) {
if (i >= n) crn_die_range(i, n);
}
+/* ---- relaxed atomic scalar access ---- */
+
+#if defined(__GNUC__) || defined(__clang__)
+#define CRN_LD(P) __atomic_load_n((P), __ATOMIC_RELAXED)
+#define CRN_ST(P, V) __atomic_store_n((P), (V), __ATOMIC_RELAXED)
+#else
+/* fallback for hosts without the builtins: plain access (single aligned
+ scalar loads/stores do not tear on such hosts in practice) */
+#define CRN_LD(P) (*(P))
+#define CRN_ST(P, V) (*(P) = (V))
+#endif
+
+uint8_t crn_ld_u8(const uint8_t* p) { return CRN_LD((uint8_t*)p); }
+uint16_t crn_ld_u16(const uint16_t* p) { return CRN_LD((uint16_t*)p); }
+uint32_t crn_ld_u32(const uint32_t* p) { return CRN_LD((uint32_t*)p); }
+uint64_t crn_ld_u64(const uint64_t* p) { return CRN_LD((uint64_t*)p); }
+float crn_ld_f32(const float* p) {
+ uint32_t b = crn_ld_u32((const uint32_t*)(const void*)p);
+ float v;
+ memcpy(&v, &b, sizeof v);
+ return v;
+}
+double crn_ld_f64(const double* p) {
+ uint64_t b = crn_ld_u64((const uint64_t*)(const void*)p);
+ double v;
+ memcpy(&v, &b, sizeof v);
+ return v;
+}
+void crn_st_u8(uint8_t* p, uint8_t v) { CRN_ST(p, v); }
+void crn_st_u16(uint16_t* p, uint16_t v) { CRN_ST(p, v); }
+void crn_st_u32(uint32_t* p, uint32_t v) { CRN_ST(p, v); }
+void crn_st_u64(uint64_t* p, uint64_t v) { CRN_ST(p, v); }
+void crn_st_f32(float* p, float v) {
+ uint32_t b;
+ memcpy(&b, &v, sizeof b);
+ crn_st_u32((uint32_t*)(void*)p, b);
+}
+void crn_st_f64(double* p, double v) {
+ uint64_t b;
+ memcpy(&b, &v, sizeof b);
+ crn_st_u64((uint64_t*)(void*)p, b);
+}
+
/* ---- conversions ---- */
int64_t crn_d2i64(double d) {
@@ -195,8 +238,6 @@ int64_t crn_input(void) {
int64_t crn_open(uint8_t* path, int mode) {
const char* m;
- int flags_ok = 1;
- (void)flags_ok;
switch (mode) {
case 0: m = "rb"; break;
case 1: m = "wb"; break;
@@ -204,25 +245,31 @@ int64_t crn_open(uint8_t* path, int mode) {
}
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;
+ }
/* 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 < CR_FILES_MAX ? cr_ctx.nfiles++ : 0] = f;
+ cr_ctx.files[cr_ctx.nfiles++] = f;
return (int64_t)(uintptr_t)f;
}
int64_t crn_close(int64_t h) {
FILE* f = (FILE*)(uintptr_t)h;
- if (h <= 0) return -1;
- if (cr_ctx.in == f) cr_ctx.in = stdin;
- if (cr_ctx.out == f) cr_ctx.out = stdout;
+ int found = -1;
for (int i = 0; i < cr_ctx.nfiles; i++) {
- if (cr_ctx.files[i] == f) {
- cr_ctx.files[i] = cr_ctx.files[--cr_ctx.nfiles];
- break;
- }
+ if (cr_ctx.files[i] == f) { found = i; break; }
}
+ if (found < 0)
+ 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];
+ 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 907cd96..f570181 100644
--- a/src/rt/cryn_rt.h
+++ b/src/rt/cryn_rt.h
@@ -67,6 +67,25 @@ uint64_t crn_block_n(void* data, uint64_t elemsz);
void crn_check_block(void* data, uint64_t i, uint64_t elemsz);
void crn_check_range(uint64_t i, uint64_t n);
+/* Relaxed atomic scalar access. Cryn defines a data race on an aligned
+ scalar cell (<= 8 bytes) as: the access yields an unspecified value of
+ the cell's type and never tears. To keep that promise (and avoid C data
+ races, which are undefined behavior), the backend performs every scalar
+ load/store through these helpers. Racily accessed transform-reference
+ cells (16 bytes) are undefined. No ordering is provided beyond wait. */
+uint8_t crn_ld_u8(const uint8_t* p);
+uint16_t crn_ld_u16(const uint16_t* p);
+uint32_t crn_ld_u32(const uint32_t* p);
+uint64_t crn_ld_u64(const uint64_t* p);
+float crn_ld_f32(const float* p);
+double crn_ld_f64(const double* p);
+void crn_st_u8(uint8_t* p, uint8_t v);
+void crn_st_u16(uint16_t* p, uint16_t v);
+void crn_st_u32(uint32_t* p, uint32_t v);
+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);
+
int64_t crn_d2i64(double d);
uint64_t crn_d2u64(double d);
int64_t crn_d2i8(double d);
diff --git a/src/types.c b/src/types.c
index 27a47db..ae1a235 100644
--- a/src/types.c
+++ b/src/types.c
@@ -83,7 +83,7 @@ uint64_t type_size(const Type* t) {
switch (t->kind) {
case TY_NOTHING: return 1;
case TY_BOOL: return 1;
- case TY_CHAR: return 4;
+ case TY_CHAR: return 1; /* byte 0-255, character intent */
case TY_INT8: case TY_UINT8: return 1;
case TY_INT16: case TY_UINT16: return 2;
case TY_INT32: case TY_UINT32: case TY_FLOAT32: return 4;
diff --git a/src/types.h b/src/types.h
index 3b5a8bd..7a66700 100644
--- a/src/types.h
+++ b/src/types.h
@@ -7,7 +7,7 @@
typedef enum {
TY_NOTHING,
TY_BOOL,
- TY_CHAR, /* 32-bit codepoint */
+ TY_CHAR, /* byte 0-255 with character intent */
TY_INT8, TY_INT16, TY_INT32, TY_INT64,
TY_UINT8, TY_UINT16, TY_UINT32, TY_UINT64,
TY_FLOAT32, TY_FLOAT64,
diff --git a/tests/26_indirect.cryn b/tests/26_indirect.cryn
new file mode 100644
index 0000000..2b1f42d
--- /dev/null
+++ b/tests/26_indirect.cryn
@@ -0,0 +1,31 @@
+[# an indirect call preserves the working value: call call runs twice #]
+make string "x" s
+
+define tick result nothing
+ make string "tick"
+ output text
+ set nothing
+ return
+end
+
+define twice transform f result nothing
+ move f
+ read
+ call
+ call
+ set nothing
+ return
+end
+
+define main
+ make int64 n
+ set 2
+ move n
+ write
+ address tick
+ push
+ call twice
+ make string "\n"
+ output text
+ return
+end
diff --git a/tests/errors/literal_float_join.cryn b/tests/errors/literal_float_join.cryn
new file mode 100644
index 0000000..6c65a8a
--- /dev/null
+++ b/tests/errors/literal_float_join.cryn
@@ -0,0 +1,22 @@
+[# an integer literal does not join a float path: representations differ #]
+define main
+ make float64 f
+ make int64 n
+ set 1
+ move n
+ write
+ set 0
+ mark top
+ move n
+ read
+ compare 0
+ jump small when less
+ set 7
+ jump join
+ mark small
+ set 1.5
+ mark join
+ move f
+ write
+ stop
+end
diff --git a/tests/errors/mixed_sign.cryn b/tests/errors/mixed_sign.cryn
new file mode 100644
index 0000000..2cb7944
--- /dev/null
+++ b/tests/errors/mixed_sign.cryn
@@ -0,0 +1,18 @@
+[# equal-width mixed signedness must be converted first #]
+define main
+ make int64 a
+ make uint64 b
+ set -1
+ move a
+ write
+ set 1
+ move b
+ write
+ move a
+ read
+ push
+ move b
+ read
+ compare
+ stop
+end
diff --git a/tests/expected/26_indirect.txt b/tests/expected/26_indirect.txt
new file mode 100644
index 0000000..e22b493
--- /dev/null
+++ b/tests/expected/26_indirect.txt
@@ -0,0 +1 @@
+ticktick