User-facing documentation for b12n-raylib-jnk: 209 raylib examples ported to jank — a native Clojure dialect (C++/LLVM), not the JVM. Each page below covers one interop pattern or raylib API surface, citing the example file that proves it.
jank is young, and almost nothing has been written about using it against a real C library at this scale. Porting 209 raylib examples surfaced a set of interop rules that are not obvious from jank's own documentation and that cost real debugging time to find. Each page here is one of those rules, written up with the committed example that proves it — so the next person does not have to rediscover it by bisecting a failing draw loop.
209 of the official raylib examples — shapes, core, text, textures, shaders, models, and audio — each a small jank namespace under raylib-examples/src/raylib_examples/, sharing one C-binding wrapper, jank-raylib-sys, that exposes raylib's C API directly.
It is the native-Clojure sibling of b12n-raylib-jlt (raylib in Jolt/Chez Scheme) and of an unreleased JVM-Clojure port over coffi/Panama. All three bind the same C library directly; what differs is the boundary each language draws between its own values and C's:
jolt and JVM Clojure cross the FFI boundary at the call: an FFI call marshals values in and out, but a raylib struct can otherwise live anywhere a normal value lives. jank draws the boundary at the value itself: a native C++ value (
Color,Vector2,Model, ...) can be constructed and used inline, but it cannot cross a jank function boundary — not returned, not passed as a parameter, not carried throughloop/recur. Every pattern in this guide is a consequence of that one rule.
Four things follow from it:
let, or park frame-crossing mutable state in a cpp/raw static. (native-value-lifetimes.md)if/cond branch type-checking, numeric coercion between jank and native number types, and struct construction all have sharp, well-defined rules. (type-checking-and-coercion.md)cpp/&, cpp/aget, cpp/new), out-params, callbacks defined inside cpp/raw, and shared C headers shipped by a wrapper. (cpp-interop-toolbox.md)raylib-api-coverage.md)Nothing about jank's C++ interop is raylib-specific — (:include "header.h") and cpp/ reach any C/C++ library. This repo just happens to exercise it against one real, struct-heavy graphics API across 209 examples.
native-value-lifetimes.md — the one rule that explains most crashes, frame-crossing mutable state via cpp/raw statics (and the per-fn-static duplication gotcha), and create-once resources via outer-let capture.type-checking-and-coercion.md — if/cond branch type-checking (including the and/or gotcha), the numeric-traps table (mod/quot/cpp/float/min/max), and constructing native structs from jank data.cpp-interop-toolbox.md — pointer interop (cpp/&, cpp/aget, cpp/new, cpp/raw), int * out-params, callback-taking APIs, shared C headers shipped by a wrapper (jank_rlights.h), shader-uniform shims, and current limitations (known-blocked constructs).raylib-api-coverage.md — fonts, models and animations, audio, 3D mode, rlgl + textures, and compute shaders (with the platform caveat — see the root README's Known limitations).jvm-surface-gaps.md — what replaces the missing JVM surface (Math/*, format, char literals), what's actually available (clojure.core/clojure.string, with caveats), and a few gotchas that save a recompile.getting-started.md — requirements, cloning with the submodule, and the bb task surface.porting-workflow.md — the end-to-end process for porting one example: source of truth, file layout, the five-place registration, the headless smoke test.example-catalog.md — a tour of all 209 examples grouped by raylib category, and how to add one (now with a preview GIF per recorded example — see docs/demos/README.md for the full gallery).raygui-to-keyboard.md — the pattern for porting raygui-based examples (sliders/checkboxes) to keyboard controls.b12n-raylib-jlt — the same idea in Jolt (Chez Scheme) over jolt.ffi. Its FFI boundary is per-call, not per-value — a Camera3D can live in an ordinary variable between FFI calls, unlike jank's native values.coffi/Panama takes the same per-call boundary as Jolt, plus a garbage collector jank doesn't have to work around.Start at index.md — the full page map and the "why this repo is interesting" framing.
The pages under docs/guide/ are the long-form reference: the interop rules with worked examples, citations, and the committed example files that prove each one.
The native-value-lifetime rule seems to block a lot — a native value can't be returned, passed as a parameter, or carried through loop/recur. This page is the toolbox that reaches around those constraints: pointer interop, out-params, callbacks, shared C headers, and shader-uniform shims — and, in the last section, what's still blocked. Each lesson names the committed example that proves it.
SetShaderValue takes a const void* pointing at native float[] data, which jank can't form from a jank vector. Two shims solve it; the second is now the preferred one:
static float jank_uf_buf[4], an index setter jank_uf_set(int i, double v), and a jank_set_uniform(Shader s, int loc, int type) that calls SetShaderValue on the buffer. To push a VEC2 you fill slots 0 and 1 then call the setter with SHADER_UNIFORM_VEC2. Works, but every uniform is 2–5 jank calls and all of them must stay in one fn (the buffer is shared mutable state — the per-fn static rule). INT needs a parallel int slot + setter (texture_outline.jank, texture_waves.jank, mandelbrot_set.jank).double/int scalars and builds the small array inside C: static void jank_set_vec4(Shader s, int loc, double a, double b, double c, double d) {
float v[4] = { (float)a, (float)b, (float)c, (float)d };
SetShaderValue(s, loc, v, SHADER_UNIFORM_VEC4);
}
Now each uniform is one cpp/ call — (cpp/jank_set_vec4 shader loc r g b a) — with no shared buffer, so it reads cleanly and the per-fn-static constraint goes away (rounded_rectangle_shader.jank for VEC4/VEC2/FLOAT, color_correction.jank for FLOAT). Pass jank doubles straight in; the (float) cast happens in C, so you dodge the cpp/float/(+ 0.0 …) boxing dance. - VEC uniforms sourced from a native struct (a Camera3D's position/target): don't read .-x/.-y/.-z field-by-field in jank — hand the setter the struct pointer via (cpp/& camera) (the pointer-taking-APIs pattern) and read the fields in C: jank_set_view(Shader s, int eyeLoc, int centerLoc, Camera3D* c) fills two float[3]s from c->position/c->target (raymarching.jank, the first VEC3 uniforms). - Array uniforms (SetShaderValueV) revive the staging buffer. A per-type scalar setter can't take an 8 x ivec3 palette as arguments, so arrays go back to approach one: a file-level static int jank_ui_buf[24], an index setter, and a jank_set_uniform_v(Shader s, int loc, int type, int count) that calls SetShaderValueV on the buffer with the element count. Fill the buffer from a flat jank vector with a loop/nth, then send once (palette_switch.jank, the first array uniform — the same static-buffer caveat applies: all calls in one fn). - GetShaderLocation returns a plain int — (int (cpp/GetShaderLocation …)) boxes it for a let. LoadShader cpp/nullptr path uses the default vertex shader; pass a real base.vs path as the first arg when the example ships one (rounded_rectangle_shader.jank).jank cannot form a C function pointer, but a callback DEFINED inside a cpp/raw block is ordinary C — a sibling wrapper in the same block attaches its pointer:
static void jank_process_audio(void *buffer, unsigned int frames) { ... }
static void jank_attach_processor(void) { AttachAudioMixedProcessor(jank_process_audio); }
Proven with the audio-thread DSP callback in mixed_processor.jank (the callback mutates/reads C statics; jank tunes parameters through setters and reads results through accessors — never touching the callback thread directly). The same shape unlocks any callback-registering raylib API. SetTraceLogCallback is now proven too (custom_logging.jank): a void (int, const char*, va_list) callback defined in cpp/raw (with its own time/strftime/vprintf va_list machinery) is handed to SetTraceLogCallback by a sibling wrapper, installed before InitWindow, and reformats every raylib log line. Raw audio streams (LoadAudioStream + UpdateAudioStream via a C-shim refill) landed in the same arc (raw_stream.jank).
Gotcha — a custom C callback that writes via printf must fflush(stdout) itself. raylib's own TraceLog flushes after every line (rcore.c), but your replacement callback doesn't inherit that. stdout is fully buffered when redirected to a file (as the headless smoke recipe does, > log 2>&1), so without a flush the buffered log sits unwritten and is lost entirely when timeout SIGTERM-kills the process — the smoke log comes back empty and the port looks broken when it isn't. Add fflush(stdout); at the end of any shim callback that prints. Trace: custom_logging.jank first smoke returned zero log lines; adding fflush surfaced all 42 reformatted lines.
When several examples need the same C helper (rlights.h's Light array, the per-type uniform setters), per-file cpp/raw duplication is not the only option: the -sys wrapper can SHIP a header and emit a second jank-build::include-dir= directive pointing at it. Proven with jank-raylib-sys/include/jank_rlights.h + basic_lighting.jank, which has no cpp/raw block at all — just (:include "raylib.h" "jank_rlights.h").
Design constraints for such headers:
Light array) is module-local and whose API is index-based with scalar parameters (jank_rl_create_light(type, px, py, pz, ..., shader) -> int).static (functions AND state): each including module gets a private copy, so two modules in one binary never collide at link time.:verbatim-paths in the wrapper's project.clj, emit the directive from jank-build.bb off (:src-dir *input*) (NOT the cmake out-dir), and bb install.target/_cache/...-out-.../jank-build-cache.txt and do NOT re-run jank-build.bb just because the artifact changed. After editing a wrapper's jank-build.bb or headers: bb install, then delete the consumer's target/_cache/ (this forces a full raylib rebuild, ~2-4 min). Symptom of staleness: fatal error: 'jank_rlights.h' file not found even though the new jar extracted.(cpp/& x) (from the image arc, 2026-07-03)jank has native pointer interop — the pointer-taking raylib APIs were never actually blocked. The whole Image* mutation family (ImageFormat, ImageColorGrayscale/Tint/Invert/Contrast/Brightness, ImageBlurGaussian, ImageFlipVertical/Horizontal, ImageDrawCircle, ImageDrawRectangle, ...) takes an Image * first arg. Form it with (cpp/& img) — the address-of a mutable let-local — exactly mirroring the C ImageColorInvert(&imCopy):
(let [img (cpp/GenImageColor 256 256 cpp/BLANK)]
(cpp/ImageDrawCircle (cpp/& img) 128 128 100 cpp/RED) ; mutates img in place
(let [tex (cpp/LoadTextureFromImage img)] ...)) ; picks up the change
Proven end-to-end in image_processing.jank (nine filters) and the spike that preceded it. No -sys wrapper change is needed — every raylib fn is already callable through (:include "raylib.h"); the only missing piece was knowing the address-of form.
The wider jank cpp-interop toolbox — see the jank book's cpp-interop chapter for the authoritative reference on each of these:
(cpp/& x) — address-of (unary). (cpp/* p) — dereference (unary).(cpp/aget arr (cpp/int i)) / (aset arr (cpp/int i) v) — array element get/set. Likely unblocks font.glyphs[i] / int* indexing.(cpp/new T init) — heap allocation returning a pointer; type DSL (:* T) for pointer types, (cpp/cast (:* void) x) / (cpp/unbox (:* T) box).(cpp/raw "…C++…") — embed arbitrary C++ (helper fns, out-param shims, constant arrays) right in the .jank file. The fallback when a jank form for some pointer dance doesn't exist yet. PROVEN in image_kernel.jank: the three float[9] convolution kernels + a jank_normalize_kernel helper are declared as C globals in a cpp/raw block, and each global array decays to float* when passed straight to (cpp/ImageKernelConvolution (cpp/& img) cpp/jank_sharpen_kernel 9). This is the general escape hatch for the Vector2 *points / int * array APIs (DrawSplineLinear, etc.) — build/fill the array in cpp/raw and pass the global.int * out-params (PROVEN in gif_player.jank). Some raylib fns write a scalar back through a pointer arg — LoadImageAnim(fileName, int *frames) returns the image AND writes the frame count into *frames. jank makes a mutable native int, passes its address, then boxes the result for jank code:
(let [frames (cpp/int 0) ; a native int lvalue
img (cpp/LoadImageAnim path (cpp/& frames)) ; writes *frames
nframes (int (+ 0.0 frames))] ; box back to jank int
...)
Note (+ 0.0 frames) re-boxes the native int into a jank object before (int …) — a bare (int frames) on the raw native value can trip codegen the same way an all-native f64 chain does (the all-native-chain codegen trap — see type-checking-and-coercion.md).
Runtime-arg pointer arithmetic via cpp/raw. gif_player streams each GIF frame from image.data + w*h*4*frame. jank's cpp/cast uses convert, not reinterpret_cast, so it can't turn the void* image.data into a unsigned char* for byte math. A one-line cpp/raw shim taking the runtime pointer + offset does the cast+arithmetic in C and hands the frame pointer straight to UpdateTexture:
(cpp/raw "static const void* jank_gif_frame_ptr(void* data, int offset) {
return (const void*)(((unsigned char*)data) + offset);
}")
;; ... per frame:
(cpp/UpdateTexture tex (cpp/jank_gif_frame_ptr (.-data img) (int offset)))
Keep offset an int at the call site — mod/quot on the frame index return reals (the mod/quot-returns-reals trap), which the shim's int offset param rejects at runtime (expected integer found small_real). Wrap the frame advance: (int (mod (+ cur 1) nframes)).
Lifecycle caveat (interaction with the native-value-can't-cross-loop/recur rule): a mutated Image is still a native value, so it can't be carried in loop/recur state. Keep the whole build-mutate-read-unload cycle inside a let in the frame that needs it (image_processing's reload block rebuilds imCopy from imOrigin, processes it via (cpp/& imCopy), reads it back with LoadImageColors → UpdateTexture, and UnloadImages it — all in one block; only the process index lives in loop state).
(cpp/aget p (cpp/int i)) on an unsigned int* is proven in compute_hash.jank, which reads the static u32 arrays returned by ComputeMD5/SHA1/SHA256 and matches the canonical CRC32/SHA1/SHA256 test vectors (the u32 elements box to jank ints without sign damage). codepoints_loading.jank adds a load-bearing int* walk (LoadCodepoints' array snapshotted into a jank vector). Struct arrays work too: (cpp/aget (.-glyphs font) (cpp/int i)) on a GlyphInfo* returns the struct by value into a let-local with working field reads (.-value, .-advanceX), and the same on the Rectangle* in font.recs — verified by a throwaway probe against GetFontDefault (2026-07-04, glyph 1 = codepoint 33, rec width 1.0); not yet load-bearing in a committed example, so keep the probe habit when reaching for it. The earlier cpp/raw subscript shims (core_random_sequence's int*, the char** walks) remain fine but are no longer the only way in; text_rectangle_bounds predates the probe and uses the MeasureTextEx re-wrap instead. (int * out-params were already proven — see gif_player in the pointer-interop section.)Vector2 *points array — DrawSplineLinear, DrawTriangleStrip, etc.: for int* the write direction IS now proven — codepoints_loading.jank fills a cpp/raw static int[512] element-wise through a one-line setter shim (jank_cp_set) and hands the pointer to LoadFontEx. A Vector2[] should work the same way (a setter shim taking x,y scalars), but that's still unprobed; cpp/new + aset also remain unprobed. Workarounds that keep the visual identical still apply: draw per-segment DrawLineEx between consecutive points (math_sine_cosine.jank's waves), or recompute each primitive's vertices inline instead of filling an array (triangle_strip.jank draws each wedge's two DrawTriangles from the angle formulas directly — the wrap-around falls out of cos/sin periodicity). By-value struct APIs like DrawLineDashed (raylib 6.0) work fine.Image pixel manipulationImage* / ImageColor* / ImageDraw* APIs all work via (cpp/& img) (image_processing.jank). See the pointer-interop section above.rlgl APIjank-raylib-sys installs rlgl.h next to raylib.h and the rlgl functions are compiled into libraylib, so (:include "raylib.h" "rlgl.h") just works — rlBegin/rlColor4ub/rlVertex2f/rlEnd and the culling toggles all run directly (rlgl_triangle.jank).TextCopy and friends; not worth simulating (text_strings_management skipped on these grounds).A map of the whole suite. Each example is one namespace under raylib-examples/src/raylib_examples/, runnable by a friendly bb <name> task or the underlying lein with-profile +<name> run --disable-sandbox. bb info prints this grouping live; this page adds the "how it's wired" recipe at the end.
Run one, list them, or reel through all of them:
bb <name> # e.g. bb starfield (opens a window)
bb examples # flat list with descriptions
bb info # the grouped cheat-sheet below
bb run-all [secs] # every example, N seconds each (unattended)
See porting-workflow.md for the full end-to-end process (source of truth, docstring format, the headless smoke test). The short version: a new example touches four places in the same commit — raylib-examples/project.clj (a :profiles entry), bb.edn (a bb <name> task), bb/helpers.clj (a row in the examples registry vector, including its :cat), and raylib-examples/README.md (move it from the "not yet ported" queue into the ported table). The repo-root README.md carries no per-example catalog, so it needs no change.
porting-workflow.md — the full registration recipe and the headless smoke-test technique this catalog's examples were all verified with.native-value-lifetimes.md — the interop rule every example in this catalog is written against.This mirrors the root README's Quick start section with a bit more context — if you only need the commands, the README's shorter version is enough.
jank compiler and the lein-jank Leiningen plugin (2026.06-1 or newer — older versions lack the native-build middleware)Verified on macOS with jank 0.1-alpha and lein-jank 2026.06-1.
raylib is vendored as a git submodule of the jank-raylib-sys wrapper, so clone with it:
git clone --recurse-submodules git@github.com:burinc/b12n-raylib-jnk.git
# or, after a plain clone:
git submodule update --init --recursive
bb task surfacebb info # grouped cheat-sheet of everything (start here)
bb examples # list every runnable example
bb starfield # run one (installs jank-raylib-sys on first use)
bb run particles # same, by argument
bb run-all # cycle through every example, ~15s each (a demo reel)
bb run-all 40 # ...longer per example (also covers first-run compiles)
bb install # install jank-raylib-sys into ~/.m2
bb clean # remove */target build dirs
If the lein on your PATH can't bootstrap, set LEIN=/path/to/lein.
bb)cd jank-raylib-sys && lein update-in :prep-tasks empty -- install
cd raylib-examples && lein with-profile +<example> run --disable-sandbox
There is no bwrap on macOS, so the native build must run with sandboxing disabled. Every bb/lein invocation above already passes --disable-sandbox for you.
Known limitation: rlgl-compute needs OpenGL 4.3 compute-shader support that macOS's native GL backend cannot provide (capped at 4.1). This repo builds jank-raylib-sys at OPENGL_VERSION "3.3" by default — every other example works; rlgl-compute does not run out of the box on any platform against this build. See the root README's "Known limitations" section for the manual override recipe (which still won't work on macOS).
If a compile error cites a header outside this repo — most often /opt/homebrew/include/raylib.h or /usr/local/include/raylib.h — you have a system-wide raylib installed that is shadowing the vendored one, and it is an older version than the 6.0 this repo pins.
The tell is the compiler's own diagnostic pointing at the wrong file:
/opt/homebrew/include/raylib.h:1155:21: note: 'ComputeSHA1' declared here
...while compiling a call to ComputeSHA256, which only exists in 6.0.
Fix: unlink the system package for the duration of the build.
brew unlink raylib # macOS; on Linux, remove or unlink the distro package
bb clean && bb install
brew link raylib # restore it afterwards if you want it back
Why it happens, and why the project can't fix it from its build config: jank's compiler has at least two C++ resolution pathways. The main one correctly honours the project's own -I flags (which jank-build.bb emits as jank-build::include-dir= directives pointing at the vendored raylib 6.0 headers). A secondary pathway — used for certain overload-resolution and diagnostic scenarios rather than for every call — does not inherit those flags and falls back to clang's default system include search, which finds the Homebrew header instead. jank exposes no flag or environment variable to control that second pathway's search order.
This is why the failure looks so arbitrary: only examples that call a function which is new in 6.0 or changed signature since 5.5 can trip it. On one machine it hit 8 of 209 — basic-shapes, top-down-lights and shapes-textures-shader (all call DrawCircleGradient, whose signature changed), math-sine-cosine (DrawLineDashed), compute-hash (ComputeSHA256), strings-management, font-sdf (LoadFontData), and point-rendering (rlDisablePointMode). The other 201 compiled fine with the same stray header present, which makes this very easy to misdiagnose as a bug in one example.
Verified as a closed loop on jank 0.1-alpha against Homebrew raylib 5.5: unlink → all 8 compile clean; re-link → all 8 fail again, identically.
jank has no Math/*, format, rand-int, char literals, or String methods — this page covers what replaces them, what's actually available (more of clojure.core/clojure.string than it looks), and a few miscellaneous gotchas that save a recompile.
No Math/*, format, rand-int, char literals, or String methods. The replacements, all proven in committed examples:
| JVM habit | jank replacement | Proof |
|---|---|---|
Math/sin etc. | (:include "math.h") + cpp/sin, cpp/cos, cpp/atan2, cpp/sqrt, cpp/hypot, cpp/pow, cpp/ceil, cpp/floor, cpp/trunc, cpp/exp, cpp/log (all double) | throughout |
Math/PI | (def PI 3.141592653589793) | easings_testbed.jank |
rand-int | cpp/GetRandomValue | camera_2d.jank |
(format "%08d" n) | a zero-pad str loop | format_text.jank |
(format "%.2f" x) | round ×100, split with quot/mod | format_text.jank fmt2 |
char literals / (char c) | subs into an ASCII table string: chars 32..126 in order, (subs ASCII (- c 32) (- c 31)) | input_box.jank |
TextSubtext | subs with the end clamped to count | writing_anim.jank |
| string as tokens | vector of one-char strings via a subs loop | penrose_tile.jank |
Typed input: (int (cpp/GetCharPressed)) in an inner loop until 0 (input_box.jank). System time: (cpp/time cpp/nullptr), (cpp/& t), (cpp/localtime ...) + .-tm_* fields (digital_clock.jank).
What IS available: the full clojure.core seq API and clojure.string. The examples in this repo lean on index-based loop/recur + nth/count, which can read as if the higher-level collection API is missing. It is not. jank's clojure/core.jank defines and self-uses first, rest, next, seq, empty?, second, map, filter, reduce, into, concat, some, every?, mapv, range, repeat, partition, doseq, dotimes, when-let/if-let, etc. — the ordinary Clojure surface. clojure.string ships too (split, split-lines, join, includes?, index-of, trim, upper-case/lower-case, ...), backed by native C++.
Caveat: not every clojure.string / clojure.core fn is implemented yet. The var exists (it's declared in string.jank / core.jank) but some native backers are stubs that throw at runtime — str/replace currently dies with TODO: port clojure.string/replace (hit in rectangle_bounds.jank, worked around by baking the substitution into the source string), and core's flush dies with TODO: port flesh (sic; hit probing compute_hash.jank — stdout is block-buffered when redirected, so println output can vanish if the process is killed; there is no working in-jank flush, shim fflush(stdout) via cpp/raw if a probe needs it). So a function being present in the source is not proof it runs; if in doubt, probe it, or grep its native impl for TODO. split/split-lines/join are confirmed working, as are peek/pop/filterv/into (rectangle_bounds.jank). Pull it in the normal way — :require coexists with a C++ :include in one ns form (jank's own shell.jank does exactly this):
(ns raylib-examples.foo
(:require [clojure.string :as str])
(:include "raylib.h"))
;; then (str/split-lines text), (str/join " " xs), (first coll), etc.
Index-based loops are still fine (and sometimes clearer for tight draw loops), but reach for the seq API / clojure.string when it reads better. text_file_loading.jank is the proof in this repo: it (:require [clojure.string :as str]) beside (:include "raylib.h") and word-wraps with str/split-lines, str/split line #"\s+" (regex literals work) and filterv — compiled and ran clean. Source of truth: jank's own compiler+runtime/src/jank/clojure/ (core.jank, string.jank) — what is implemented there is what you can call.
const char * returns fold into str directly (from the core arc, 2026-07-03). A raylib fn that returns a C string (GetMonitorName, GetClipboardText, GetWindowTitle, ...) can be passed straight to jank's str, which turns it into a jank string:
(str "[" (cpp/GetMonitorName 0) "]") ; => "[Built-in Retina Display]"
Proven in monitor_detector.jank. No conversion helper needed — the native const char * becomes a jank string at the str boundary. (You can also pass it straight to another C fn that wants const char *, e.g. (cpp/DrawText (cpp/GetMonitorName 0) ...), since that's C->C.)
GOTCHA: don't wrap an already-boxed jank int in (int x) INSIDE a str call. monitor_detector cost real debugging over this. When x is already a jank int (e.g. destructured from a map), writing (str "Position: " (int x)) made jank emit C++ that member-accesses an i64, and the WHOLE FILE failed to compile with the misleading member reference base type 'i64' (aka 'long long') is not a structure or union — reported at an unrelated generated line, with no .- in the source at all. Dropping the redundant cast fixed it:
;; BAD — redundant (int x) on an already-jank int inside str -> i64 codegen error
(cpp/DrawText (str "Position: " (int x) " x " (int y)) ...)
;; GOOD — pass the boxed value directly
(cpp/DrawText (str "Position: " x " x " y) ...)
The plain .-x reads and the const char * fold in the same file were both fine; the cast-inside-str was the sole trigger. When a file fails with member reference base type 'i64' and you can't find a matching .- access, suspect an (int ...)/cast folded into a str (or other builder) call — the error line is generated-code position, not source, so don't trust it.
(cpp/! (cpp/WindowShouldClose)), (if (cpp/IsKeyDown cpp/KEY_Q) ...).cpp/NAME: colors, keys, cpp/MOUSE_CURSOR_IBEAM, cpp/TEXTURE_FILTER_BILINEAR, gesture enums (compare as ints: (int (cpp/GetGestureDetected)), values 1/2/4/.../512 — input_gestures.jank).SetConfigFlags ORs each call into its state, so call once per flag (window_letterbox.jank).GetWorldToScreen2D/GetScreenToWorld2D (native Vector2 returns) — the transforms reduce to screen = (world - target)*zoom + offset in jank math (camera_2d_platformer.jank does all five camera modes this way). When rotation matters, both GetScreenToWorld2D and GetWorldToScreen (3D) DO work — bind the returned native Vector2 to a local and read .-x/.-y (camera_2d_mouse_zoom.jank, world_screen.jank).analyze/invalid-fn-parameters: This function has too many parameters. The max is 10). Bundle extra args into a vector and destructure inside — tiled_drawing.jank's tiling helper passed source/dest as two 4-vectors instead of eight scalars. (Moot there in the end, since the native-Texture2D-param rule forced full inlining, but the cap is real and independent.)defn shared by several passes should end with an explicit nil (camera_2d_split_screen.jank's draw-scene).\n inside a DrawText string works (window_letterbox.jank).(:include "raylib.h" "math.h" "time.h").A triple-nested doseq with a fat body (waving_cubes.jank's 15x15x15 cube lattice) compiles in ~3-4 MINUTES, versus ~30-60s for a typical example module. The generated C++ for nested seq iteration with a large inlined body appears to grow multiplicatively. Budget smoke-test alarms accordingly (the standard 40s alarm kills such a build mid-compile and looks like a hang - re-run with a 260s+ alarm before diagnosing). If compile time matters more than faithfulness, hoist the inner body into a defn taking only jank values.
jank is native Clojure (C++/LLVM) — no JVM, no Java interop, no REPL. The compiler statically type-checks the boundary between jank objects and native C++ values, and this page is about the single rule that boundary enforces: a native cpp value only stays native within the form that produced it. Every lesson below is a consequence of that rule. Each lesson names the committed example that proves it — those files are the running test suite for this document.
A native cpp value only stays native within the form that produced it. A Color, Vector2, Rectangle, Camera2D, ... may be:
(cpp/DrawCircleV (cpp/Vector2 ...) ...) ✅let-local and used in the same scope ✅let OUTSIDE the frame loop and used inside it (lexical capture) ✅ — this is how create-once GPU resources live (lines_drawing.jank's RenderTexture, words_alignment.jank's Font)But it may NOT cross a jank fn boundary:
returning a native object of type 'Color', which is not convertible to a jank runtime object. Even via nested if (dashed_line.jank learned this).object_ref and the native call rejects it (digital_clock.jank's draw-hand; tiled_drawing.jank hit this trying to pass a Texture2D to a draw-tiled helper — No matching call to 'DrawTexturePro' ... argument 0 having type 'jank::runtime::object_ref &' — and had to inline the helper so the texture stayed a captured let-local).loop/recur state ❌ (input_mouse.jank).Fix: thread plain jank data (ints, reals, keywords, maps) and resolve the native value inline at the use site. camera_2d_platformer.jank threads the camera as five scalars and rebuilds (cpp/Camera2D ...) each frame; input_mouse.jank threads a color-id int and picks the Color with a nested if at draw time.
When a native resource must BOTH persist across frames AND be recreated at runtime with computed sizes, neither of the two usual homes works: loop/ recur state can't carry a native value (the same rule as above — a native value can't cross a fn boundary — applied to loop/recur state), and a create-once outer-let local can't be rebound. Park the value in a cpp/raw static with tiny accessor fns instead:
(cpp/raw "static RenderTexture2D jank_target = { 0 };
static void jank_resize_target(int w, int h) {
UnloadRenderTexture(jank_target);
jank_target = LoadRenderTexture(w, h);
}
static RenderTexture2D jank_get_target(void) { return jank_target; }")
jank calls (cpp/jank_resize_target w h) on change events and re-fetches (let [target (cpp/jank_get_target)] ...) each frame -- the struct comes back by value into a let-local and never crosses a jank fn boundary. Proof: viewport_scaling.jank, whose RenderTexture is recreated on every window resize / resolution / viewport-mode change with sizes computed from the current window state. (UnloadRenderTexture guards id 0 internally, so the first call against the zero-initialized static is safe.) Reach for this only when recreation is genuinely dynamic; a fixed-size resource should stay a create-once outer-let local (lines_drawing.jank).
CRITICAL: cpp/raw statics are duplicated PER JANK FN. Every jank fn that references the shims gets its OWN copy of the raw block's statics – a helper fn that writes the "same" static writes a private copy the other fns never see. Probe evidence (2026-07-04): after -main called a load-into-static shim, -main read .glyphCount 95 from its copy while a helper defn- read 0 from its own; the helper's writes were likewise invisible to -main. Failure modes are nasty: state silently "resets" across fn boundaries, and reading through a zeroed struct's pointer field segfaults. Rule: route EVERY read/write of a mutable raw static through one single jank fn (in practice -main), inlining helper logic there -- unicode_ranges.jank inlines the C's AddCodepointRange into -main's rebuild block for exactly this reason. Pure-jank helpers (no shim calls) remain safe to factor out. (viewport_scaling.jank was unaffected only because all its shim calls already sat in -main; compute_hash.jank / storage_values.jank / codepoints_loading.jank are safe because their statics are written and read within one fn call's dynamic extent, not across fns.)
LoadRenderTexture / GetFontDefault style resources bind in a let outside the frame loop and get used inside it — lexical capture keeps them native. Unload after the loop.
(let [canvas (cpp/LoadRenderTexture WIDTH HEIGHT)]
(loop [...]
... (cpp/BeginTextureMode canvas) ...)
(cpp/UnloadRenderTexture canvas))
Two RenderTextures at once work (camera_2d_split_screen.jank). Blit a RenderTexture with a negative source height — RTs are stored upside down (lines_drawing.jank, window_letterbox.jank).
This guide is the end-to-end process for porting one official raylib example to jank, as practiced across the first 50 ports. Follow it top to bottom and a port lands as one self-contained, tested, registered commit.
raylib-examples/README.md keeps the prioritized queue under "Not yet ported". Markers tell you the cost up front:
RenderTexture (supported; see lines_drawing.jank)rlgl API. Turns out to work directly: rlgl.h is installed next to raylib.h and its functions live in libraylib, so (:include "rlgl.h") is all it takes (proof: rlgl_triangle.jank)The authoritative originals are raylib's own example programs, under examples/{core,shapes,text,...}/ (easing functions in examples/shapes/reasings.h). You already have a checkout: the vendored submodule at jank-raylib-sys/raylib/examples/ is pinned to the same raylib this repo builds against, so it is the copy whose API actually matches.
Port from the C, never from an intermediate binding. Ports-of-ports drift — if you find an existing Clojure or Lisp translation of an example, treat it as a hint and check it against the C.
Keep formulas and update ordering faithful to the C (a code review of the easings testbed verified all 28 easing formulas term-by-term against reasings.h — that fidelity is the standard). When jank forces a deviation (no mutable arrays, capped pool sizes, keyboard instead of raygui), say so in the namespace docstring:
(ns raylib-examples.bullet-hell
"raylib [shapes] example - bullet hell, ported to jank.
...controls...
Based on raylib/examples/shapes/shapes_bullet_hell.c
(jank-native: bullets are a vector of maps rebuilt per frame instead
of a mutable C array, and MAX-BULLETS is 5000 rather than the C's
500000 calloc headroom - the reset-at-cap behavior is the same.)"
(:include "raylib.h" "math.h"))
Docstring format: title line, controls, Based on <C file>, then a (jank-native: ...) note for intentional deviations. File names use underscores, namespaces use kebab: bullet_hell.jank → raylib-examples.bullet-hell. Comments must be ASCII (an em-dash trips the lexer).
Before writing a new construct, grep the existing examples for a sibling that already uses it — every proven idiom has at least one committed example, and the guide pages index them by theme.
Every example sets (cpp/SetConfigFlags cpp/FLAG_WINDOW_HIGHDPI) before InitWindow so windows scale with the monitor DPI (drawing stays at the C's logical resolution); include it in new ports. SetConfigFlags ORs each call into its state, so it stacks with FLAG_MSAA_4X_HINT etc. Exceptions: window-flags (a flag-state demo) and the two highdpi-* examples (which manage DPI flags themselves).
raylib-examples/project.clj — a :profiles entrybb.edn — a bb <name> taskbb/helpers.clj — a row in the examples registry vector, including its :cat (the raylib category keyword — drives the bb info grouping)raylib-examples/README.md — move the example from the queue into the ported table, bump the progress countsThe repo-root README.md carries no per-example table; it delegates the catalog to raylib-examples/README.md, so nothing there needs touching.
Do not defer any of these; the registration IS part of the port.
Check paren balance BEFORE the first compile. A jank compile costs 30-60 s; a strict reader loop is instant and catches the classic extra-close-paren at the recur tail (which cost one wasted compile on input_gestures_testbed). jank sources read fine with the JVM reader:
cd raylib-examples
clojure -M -e "
(let [text (slurp \"src/raylib_examples/<name>.jank\")
r (java.io.PushbackReader. (java.io.StringReader. text))]
(try
(loop [forms []]
(let [form (read {:read-cond :allow :eof ::eof} r)]
(if (= form ::eof)
(println :total-forms (count forms) :ok)
(recur (conj forms form)))))
(catch Exception e (println :ERROR (.getMessage e)))))"
:total-forms N :ok means balanced; :ERROR Unmatched delimiter means fix before compiling. (A strict read loop, not read-string - the single-form and (do ...)-wrapped variants both miss trailing imbalance.)
macOS has no timeout and this harness blocks a foreground sleep, so the reliable one-shot is a perl alarm:
cd raylib-examples
perl -e 'alarm 25; exec @ARGV' \
lein with-profile +<name> run --disable-sandbox > /tmp/run.log 2>&1
echo "exit=$?"
grep -icE "error|exception|Mismatched|small_real|small_integer|invalid object" /tmp/run.log
Reading the result:
exit=142 (SIGALRM) — the example compiled, opened its window, and survived 25 s of the frame loop. This is the success signal.exit=1 or an early exit — compilation or startup failed; the log has the compiler error.grep must print 0. The markers are the jank/raylib failure vocabulary: Mismatched (if-branch type clash), small_real / small_integer (int/real API mismatch), invalid object (bad conversion).First compile of a new example takes ~60–75 s; cached recompiles ~15 s.
A 25 s headless run only exercises code that runs unconditionally. If the interesting path hides behind input (a hover, a key, a generation count), temporarily force the state, run, then revert before committing:
penrose_tile.jank — forced gen 2 + prebuilt tokens to exercise the L-system, then reverted.input_box.jank — forced on-text? true and seeded the name from the ASCII table so the caret/MeasureText path ran, then reverted.Note the probe in the commit message so reviewers know the gated path was actually executed.
raylib-examples: port <official_source_name>.git add -A/./-u.Bisect: cut the example down to a minimal draw loop, then add one construct back at a time — each lein run recompiles the one changed module in ~30–60 s. The compiler error vocabulary and what each message actually means is in type-checking-and-coercion.md and cpp-interop-toolbox.md.
Many official shapes examples build their UI with raygui sliders and checkboxes. jank-raylib-sys doesn't wrap raygui, so those examples get a keyboard-driven port: sliders become held-key adjustments, checkboxes become toggle keys, and the raygui panel becomes plain DrawText lines showing live values. The pattern was established by easings_testbed.jank and refined across ring_drawing.jank, circle_sector_drawing.jank, rounded_rectangle.jank, and recursive_tree.jank.
One small clamp helper covers every slider:
(defn adj
"Adjust v by step while dn/up are held, clamped to lo..hi."
[v dn up step lo hi]
(let [v (if dn (- v step) v)
v (if up (+ v step) v)]
(if (< v lo) lo (if (> v hi) hi v))))
Call it once per slider in the frame let, feeding IsKeyDown (continuous, slider-like) or IsKeyPressed (stepped, for coarse values like a depth of 1..10):
(let [sa (adj sa (cpp/IsKeyDown cpp/KEY_LEFT) (cpp/IsKeyDown cpp/KEY_RIGHT) 2.0 -450.0 450.0)
segs (adj segs (cpp/IsKeyDown cpp/KEY_O) (cpp/IsKeyDown cpp/KEY_P) 0.5 0.0 100.0)
depth (adj depth (cpp/IsKeyPressed cpp/KEY_Z) (cpp/IsKeyPressed cpp/KEY_X) 1.0 1.0 10.0)]
...)
Pick the step so a slider's full range takes ~2–4 seconds of holding at 60 fps (range / step / 60).
ring? (if (cpp/IsKeyPressed cpp/KEY_R) (not ring?) ring?)
Keep the C's panel geometry (the divider line and tinted rectangle), and replace each GuiSliderBar with a DrawText line naming the value, its current reading, and its keys:
(cpp/DrawText (str "StartAngle: " (fmt1 sa) " (LEFT/RIGHT)") 560 40 10 cpp/DARKGRAY)
(cpp/DrawText (str "[R] Draw Ring: " (if ring? "ON" "OFF")) 560 320 10 cpp/DARKGRAY)
fmt1/fmt2 are the small decimal formatters (there's no format in jank — see jvm-surface-gaps.md). Keep derived readouts from the C, like the MANUAL/AUTO segments mode, including its color switch: (if (>= segs min-segs) cpp/MAROON cpp/DARKGRAY).
[R] Draw Ring, [B] Bezier).easings_testbed.jank keeps the C's Q/W duration keys, so only ESC quits there. Document that clearly in every registry surface. - input_box.jank accepts typed characters, so it quits on Q only while the mouse is OUTSIDE the box.bb/helpers.clj controls string, and the README table row. Drift between those surfaces is a real failure mode a code review caught (the root README's easings-testbed row omitted the quit key while helpers said "ESC quit").GuiSliderBar to a key pair + adj call (note range and a sensible step).GuiCheckBox to a toggle key.What raylib surface area is proven working against this port, and how. Each section cites the committed example that proves it — these examples double as the running test suite for the claims below.
Custom font loading works with zero wrapper changes. LoadFont (BMFont .fnt + its .png atlas) and LoadFontEx (a TTF rasterized at load, with a base size + glyph count) both return a Font native struct — treat it exactly like a Texture2D: bind it in the outer let, draw with DrawTextEx inside the loop, UnloadFont after. LoadFontEx's codepoint-array arg is passed as cpp/nullptr for the default set. .-baseSize reads back fine (nth-box it for the cpp/float size arg). Proof lines: FONT: ... Font loaded successfully, FONT: Data loaded successfully (32 pixel size | 184 glyphs) (font_loading.jank). String literals MAY be UTF-8 (disproving an earlier version of this note): codepoints_loading.jank defs the Japanese Iroha pangram in-source, and the lexer, LoadCodepoints, and DrawTextEx all handle it. The ASCII-only restriction is about COMMENTS (the em-dash lex/invalid-unicode trip).
Mesh/Model loading works with zero wrapper changes — the long-assumed "LoadModel blocker" was never real, exactly as with Font. Proven in texture_tiling.jank:
(cpp/GenMeshCube (cpp/float 1.0) ...) returns a Mesh by value; passing it inline to cpp/LoadModelFromMesh yields a Model that binds as an outer-let local (pointers inside and all) and draws with cpp/DrawModel inside the loop. Proof line: VAO: [ID 2] Mesh uploaded successfully to VRAM (GPU).model.materials[0].maps[MATERIAL_MAP_DIFFUSE].texture = tex (and .shader = s) has no jank spelling; a two-line C shim taking (Model* m, Texture2D tex) does the assignment, called with (cpp/& model) — the same address-of pattern as Image* / UpdateCamera (cpp-interop-toolbox.md).UnloadModel after the loop, as usual for create-once resources.LoadModel from a FILE works too (model_loading.jank, the castle OBJ + its .png diffuse): same shape, just returns a Model. Reading model.meshes[0] for GetMeshBoundingBox stays a one-line shim through (cpp/& model) (a struct-array cpp/aget would probably also work per the GlyphInfo probe, but the shim is certain).cel_shading.jank, the old_car_new.glb toon car): MODEL: ... Model basic data (glb) loaded successfully. Reading a Shader back off the material (m->materials[0].shader) is a Shader-returning shim bound to a let-local, like any create-once native.LoadModelAnimations' ModelAnimation* + int* out-param stay behind C statics with index-based wrappers, and UpdateModelAnimation runs per frame (shadowmap_rendering.jank); a texcoords2 channel can be RL_MALLOC'd, filled and wired to a vertex attribute through (cpp/& mesh) (lightmap_rendering.jank); LoadMaterialDefault binds by value with (cpp/& material) field-write shims (mesh_instancing.jank, which also proves DrawMeshInstanced over a C-static Matrix array).model_loading), GLB (cel_shading), IQM incl. separate animation files (loading_iqm), M3D incl. skeleton access (loading_m3d), and VOX (loading_vox, which also proves UpdateCameraPro with inline movement/rotation Vector3s and GetModelBoundingBox).rlgl_compute.jank proves the whole compute pipeline: compile (rlLoadShader src RL_COMPUTE_SHADER + rlLoadShaderProgramCompute, kept in a path-taking C shim), SSBOs (rlLoadShaderBuffer with cpp/nullptr data), rlBindShaderBuffer, and rlComputeShaderDispatch — all direct rlgl calls with jank-int ids. Prerequisites and patterns:
OPENGL_VERSION "4.3" (set in its jank-build.bb since 2026-07-11): under the default 3.3 the rlgl compute functions compile to no-ops. 4.3 is the same GL 3.3 feature set plus compute, so the GLSL-330 examples are unaffected (regression checked). Proof line: GL: Compute shaders supported.OPENGL_VERSION "3.3" (macOS's native GL backend caps at 4.1, so a global 4.3 build broke window creation for every example on macOS). rlgl-compute now needs the manual 4.3 override described above, which will not work on macOS regardless of the override — see the root README's "Known limitations" section.ssboA <-> ssboB) is just recur with the loop vars exchanged. No native value crosses the loop.rlUpdateShaderBuffer (&struct + sizeof) stays a cpp/raw static behind buffer/count/flush wrappers, like any frame-crossing native state.(:include "raylib.h" "rlgl.h") gives rlBegin/rlColor4ub/rlColor4f/rlVertex2f/rlEnd (rlgl_triangle.jank), custom blend pipelines via rlSetBlendFactors + rlSetBlendMode cpp/BLEND_CUSTOM + rlDrawRenderBatchActive (top_down_lights.jank), and full vertex-colored batches (rectangle_advanced.jank). Raw GL constants pass as plain ints.rlPushMatrix/rlPopMatrix/ rlRotatef/rlTranslatef/rlScalef nest hierarchical transforms inside BeginMode3D, and regular raylib draws (DrawSphere) render through the same batch so the stack applies to them (rlgl_solar_system.jank - Sun/Earth/Moon).(.-r c)/(.-g c)/(.-b c)/(.-a c) — feed them native-to-native into rlColor4ub, or box with (int (+ 0.0 ...)) to store as jank ints (rlgl_color_wheel.jank).(cpp/GenImageChecked ...) → (cpp/LoadTextureFromImage img) → (cpp/UnloadImage img), with the Texture2D held in the outer let like a RenderTexture (top_down_lights.jank). All nine GenImage* algorithms work (image_generation.jank).LoadTexture from a PNG file works — a jank string coerces to the const char* path. Resource files come from the vendored raylib submodule via a path relative to the raylib-examples/ working dir: ../jank-raylib-sys/raylib/examples/textures/resources/... (logo_texture.jank; the run log's FILEIO: ... File loaded successfully is the proof to grep for).image_generation.jank's nine).top_down_lights.jank replaces the C's 16 cached per-light masks this way).top_down_lights.jank's draw-quad).raylib audio works with zero wrapper changes. InitAudioDevice, LoadSound, PlaySound, UnloadSound and CloseAudioDevice are plain raylib.h functions compiled into libraylib (miniaudio / Core Audio backend on macOS). The Sound value is a native struct — same rules as Texture2D: bind it in the outer let, use it inside the frame loop via lexical capture, unload after (sprite_button.jank). The run log's AUDIO: Device initialized successfully + WAVE: Data loaded successfully are the proof lines to grep for. OGG decoding works too (sound_loading.jank), and so do music streams: LoadMusicStream (MP3), UpdateMusicStream once per frame, Play/Stop/Pause/Resume, SetMusicPan/SetMusicVolume (jank real through cpp/float), and GetMusicTimePlayed/GetMusicTimeLength boxed at the binding site (music_stream.jank; proof line STREAM: Initialized successfully). The remaining audio surface to probe is the callback-taking APIs (SetAudioStreamCallback, audio processors) — likely a real blocker, same class as C function pointers elsewhere.
Basic 3D works. cpp/Camera3D constructs inline from three nested cpp/Vector3 args + fovy + cpp/CAMERA_PERSPECTIVE, binds as an outer-let local, and drives BeginMode3D/EndMode3D; DrawGrid and DrawSphere (Vector3 built inline as a call arg) render inside it (sound_positioning.jank). Two constraints shape 3D ports:
UpdateCamera ((cpp/& camera) mode) forms the pointer with the image-processing address-of pattern (cpp-interop-toolbox.md) on an OUTER-let Camera3D, and the mutation persists across frames (probe: 100 orbital frames drifted position.x from 10.0 to 14.03) — camera_3d_free.jank. Struct FIELD writes still have no jank syntax; a one-line pointer shim does them (jank_cam_retarget). The older per-frame-rebuild workaround remains valid and simpler when the camera path is fully jank-driven: billboard_rendering.jank constructs the whole cpp/Camera3D as a frame-let local from an accumulated angle, and DrawBillboard* accept it by value. BoundingBox also constructs inline from two nested cpp/Vector3s for CheckCollisionBoxes / CheckCollisionBoxSphere (box_collisions.jank).Vector3Subtract etc. return native structs, fine inline, but a chain of them can't thread jank helper fns — do the vector math as scalar jank arithmetic on plain reals instead (the attenuation/pan math in sound_positioning.jank replaces five raymath calls this way).jank's compiler enforces the native-value-lifetime rule (native-value-lifetimes.md) strictly at compile time. This page covers the sharp edges that fall out of that strictness: if/cond branch type-checking, numeric coercion between jank and native number types, and constructing native structs from jank data. Each lesson names the committed example that proves it.
jank type-checks every if branch. cond/case expand with an implicit trailing nil, which clashes with a native value type: Mismatched 'if' branch types 'Color' and 'nil'.
cond returning jank values (maps, keywords, strings, vectors) is fine — state machines and [x vx]-vector returns all work.ifs where every branch ends in a concrete value: (if on-text? cpp/RED cpp/DARKGRAY) as a call argument is fine (input_box.jank, bullet_hell.jank).and/or are ifs too — a native struct-field bool clashes with a jank bool. (and native-bool jank-bool) expands to (let [a native-bool] (if a jank-bool a)), so its two implicit branches are jank-bool (plain bool) and native-bool. A struct-field read like (.-hit collision) types as bool & (a reference), not bool, so the combined form errors with Mismatched 'if' branch types 'bool' and 'bool &'. The read is only a problem when a boolean-combining macro forces the two types to unify — using (.-hit c) straight as an if/when condition is fine (picking_3d.jank). Fix: don't and a native field bool with a jank bool; hand-nest the ifs so the native bool is always just a condition, never a returned branch value (basic_voxel.jank's ray-pick loop: (if (.-hit coll) (if (< d best-d) ...) ...)).
| Trap | Symptom | Fix | Proof |
|---|---|---|---|
mod/quot return reals | expected integer found small_real at an int param, or a broken nth | wrap in (int ...) | everywhere; writing_anim.jank |
cpp/float wants a REAL arg | expected real found small_integer | (cpp/float (+ 0.0 n)) | lines_drawing.jank boxes GetMouseX |
(/ int int) shape is unreliable | subtle | precompute constants or (int (quot ...)) | window_letterbox.jank uses (int (/ GAME-H 10)) |
min/max reject a raw C double | invalid operands to binary expression deep in math.hpp | box first with (+ 0.0 x), or clamp with if | dashed_line.jank; isolated to min/max only — + - * / < <= = all take raw doubles (ellipse_collision.jank) |
min/max also reject a boxed int mixed with (int ...)'s unboxed i64 | same math.hpp template error (oref<small_integer> vs long long) | clamp with if comparisons instead (< / >= take the mix fine) | first_person_maze.jank cell clamp |
str with >10 args + a raw (int ...) in the tail | codegen error: member reference base type 'i64' ... .erase() | build long strings in two str calls of ≤10 args | bullet_hell.jank status line |
cpp/float on an ALL-native arithmetic chain | codegen error: convert<float>::from_object — no known conversion from 'f64' | route one operand through a boxed source, e.g. a vector lookup: (nth [0 fh (* 2 fh)] state) | sprite_button.jank frame offset |
(int cpp/KEY_*) on a C enum constant | template error: member reference base type 'const KeyboardKey' in to_int | cast the enum to a native int first with (cpp/int cpp/KEY_*) — the result then boxes fine into jank maps/vectors and round-trips through int params like IsKeyDown | keyboard_testbed.jank ROW data |
Boxing idiom: (+ 0.0 x) turns a raw C int/float/double into a jank real; (int x) truncates to a jank integer. cpp/GetFrameTime, cpp/GetTime, cpp/GetMouseWheelMove returns are routinely boxed at the binding site.
The all-native chain trap is the subtle cousin of the expected real one: (+ 0.0 expr) only boxes when at least one input is already a jank object. When every value in the chain derives from literals and native reads ((.-height tex), if-of-literals), jank keeps the whole expression as an unboxed native f64, and cpp/float's generated from_object call cannot take it. The same shape compiles fine when a loop/recur variable feeds the chain (loop vars are boxed) — that is why sprite_animation.jank's near-identical frame math never hit it. Any jank collection operation re-boxes: nth on a vector of the possible offsets is the cheapest escape hatch.
cpp/Color (the struct ctor) wants native unsigned char fields — jank ints don't match: No matching call to 'Color' constructor. Struct ctors demand exact native types; functions coerce jank ints happily.cpp/GetColor — (cpp/GetColor (+ (* r 16777216) (* g 65536) (* b 256) a)) (camera_2d.jank skyline, recursive_tree.jank panel) — or use cpp/ColorFromHSV, cpp/ColorLerp, cpp/Fade. The C idiom (Color){0,0,0,200} becomes (cpp/Fade cpp/BLACK (cpp/float 0.784)) (bullet_hell.jank).cpp/Camera2D takes two nested cpp/Vector2 plus two cpp/float args, passed straight to cpp/BeginMode2D (camera_2d.jank).int fields need (cpp/int n) casts — the same "struct ctors demand exact native types" rule that bites cpp/Color also bites int fields, but here there IS a cast helper (unlike unsigned char). A jank int literal reaches the ctor as small_integer_ref, which doesn't convert to native int in the generated braced-init: No matching call to 'NPatchInfo' constructor ... argument 1 having type ...small_integer_ref. Wrap each int field in (cpp/int n) (mirror of cpp/float for reals): (cpp/NPatchInfo (cpp/Rectangle ...) (cpp/int 12) (cpp/int 40) (cpp/int 12) (cpp/int 12) cpp/NPATCH_NINE_PATCH) (npatch_drawing.jank). The trailing enum constant (cpp/NPATCH_NINE_PATCH) converts to the int layout field on its own. Note the asymmetry vs cpp/Color: int-field structs get the cpp/int escape hatch, so no GetColor-style function detour is needed..-: (.-texture render-texture), (.-x measured-vec2), and on pointer returns (.-tm_hour lt) (lines_drawing.jank, words_alignment.jank, digital_clock.jank).