Run and debug a language binding or adapter

Start with your language's workbench project. Keep its tactic and launcher files together, change the tactic, then build and run the project. Hosted profiles supply the SDK and its dependencies; they do not need a copied binding in your source tree.

For local work, install the SDK and the runtime or compiler named in the language's generated guide. The examples here use /opt/clunkyard/sdk. The API name table and each linked language guide contain the callable names, record shapes and complete small robots. Use local trials to run a project against opponents.

Which process owns the turn?

A native binding converts language values to the core's C ABI in the same process. Python's ctypes, Java's FFM and a compiled Lua module all fit this description. They do not launch another robot for each query.

A process adapter keeps a tactic in another language process and forwards its generated API calls to a Python or Node binding. Forth, GNU APL and BQN use persistent tactic processes. CLIPS runs in Python through clipspy; MiniZinc solves a plan at startup and Python follows it. These are separate execution models, even though all use the same core for canonical queries, tracking and controls.

The arena meters the complete robot workload, including adapter processes and solver children. Choosing an adapter does not make its CPU free. Standard uses the launched runtime's Season 3 tier: Forth, APL, CLIPS and MiniZinc use the Python route; BQN uses Node. The VM's JVM profiles use the same configured JVM mode. Own time includes CPU and time spent sleeping or blocking, with measured scheduler/VM delays subtracted; a blocked tactic can exhaust its own-time limit. DSL operations and guest WebAssembly fuel are the Reflex routes. Resources explains the measurements and limits.

Forth, GNU APL and BQN

These profiles accept a tactic directly as bot.json's entry. The hosted launcher starts the adapter and keeps one tactic process for the match. For example:

{
  "name": "My Forth robot",
  "language": "forth",
  "protocol": 2,
  "entry": "tactic.fs",
  "supportedModes": ["2d", "3d"],
  "supportedRulesets": ["classic", "inertial-2d", "gravity-flat", "gravity-hills", "zero-g"]
}

Use language: "apl" with an .apl entry, or language: "bqn" with a .bqn entry, for the other two. The starter projects also include Python or JavaScript launchers so you can add host-language setup around the tactic. The selected entry decides which route launches. You do not need to write protocol JSON in the tactic.

Profile Tactic contract Installed components Local direct-tactic command
Forth on-turn, optional on-start; preserve both stacks across a turn Python core binding, clunkyard_forth, Gforth and generated words Command below
GNU APL Niladic OnTurn, optional OnStart; generated Cy* functions Python core binding, clunkyard_apl, GNU APL and generated functions Command below
BQN Import cy from •args; Turn, optional Start Node core binding, clunkyard_bqn, CBQN and generated namespace Command below

Run these from the project directory; the absolute tactic argument avoids runtime-dependent working-directory resolution:

SDK=/opt/clunkyard/sdk
PYTHONPATH="$SDK/bindings/forth:$SDK/bindings/python" \
  python3 -c 'import clunkyard_forth,sys; clunkyard_forth.run(sys.argv[1])' \
  "$PWD/tactic.fs"

PYTHONPATH="$SDK/bindings/apl:$SDK/bindings/python" \
  python3 -c 'import clunkyard_apl,sys; clunkyard_apl.run(sys.argv[1])' \
  "$PWD/tactic.apl"

NODE_PATH="$SDK/bindings/bqn:$SDK/bindings/javascript" \
  "$SDK/bindings/javascript/run" \
  -e 'require("clunkyard_bqn").run(process.argv[1])' "$PWD/tactic.bqn"

These commands wait for protocol input. Redirect a fixture to smoke-test one, or put the command in a local manifest for a match. For the teaching projects' bot.py or bot.js launchers, use the same environment prefixes and pass the launcher file instead of -c/-e and its expression.

Forth's canonical words use kebab case, with floating-point arguments on the floating stack, integer/enum arguments on the data stack and text as address plus length. cy-result turns status into a presence flag and throws negative errors. APL passes enclosed text and flattened records; CyPresent distinguishes absent values from present empty text. BQN uses record namespaces and @ for absence. Their generated guides specify list and record order; do not infer it from a printed protocol object.

Keep the persistent tactic pipe available for adapter traffic. Log through canonical session logging, for example Forth's cy-session-log, APL's CySessionLog or BQN's cy.SessionLog, using that language's documented argument convention. Unframed output on a tactic's RPC stdout can be read as a reply. Adapter/runtime errors appear in stderr; an error return from a core call should be handled before consuming its result. The adapters' play helpers can run a tactic against recorded lines without a referee.

CLIPS and MiniZinc

Both profiles use a Python launcher as their entry. Include the .clp or .mzn tactic beside it; the builder supplies the adapter package.

SDK=/opt/clunkyard/sdk
PYTHONPATH="$SDK/bindings/clips:$SDK/bindings/python" python3 bot.py
PYTHONPATH="$SDK/bindings/minizinc:$SDK/bindings/python" python3 bot.py

CLIPS uses clunkyard_clips.ClipsRobot and the installed clipspy module. It loads the rules once, resets facts for each state, restores match facts and asserts current observations before running rules. Globals persist between turns. Canonical (cy-...) functions call the core; output facts such as drive, scan, fire, status and forfeit are another control surface. Later facts replace the fields they supply. CLIPS router output is sent to stderr. Use a rule trace or a short status fact to inspect why a rule fired, and check whether a more recent output fact replaced the same control.

MiniZinc uses clunkyard_minizinc.Planner. Set model, include the supplied clunkyard.mzn declarations in the model, and implement Python on_turn using self.plan. model_data can add parameters and call core_query while preparing them. The adapter compiles with MiniZinc, solves with fzn-gecode and decodes the result once at hello; it does not start a solver for each state. Live observations and controls remain the Python binding's. The installed image supplies MiniZinc and Gecode. Check model compilation/solver stderr when startup fails, then inspect plan values separately from the turn logic. Solver JSON uses solver precision.

Shared JVM and .NET bindings

Languages Import/reference Build or launch locally
Java clunkyard.* from bindings/java/clunkyard.jar javac -cp "$SDK/bindings/java/clunkyard.jar" Bot.java; then java --enable-native-access=ALL-UNNAMED -cp ".:$SDK/bindings/java/clunkyard.jar" Bot
Kotlin clunkyard.* from the same jar Compile with kotlinc and that jar on its classpath; launch the output's main class with the jar on the Java classpath.
Scala clunkyard._ from the same jar Compile with scalac; include Scala's runtime and the binding jar on the Java classpath.
Clojure Normal interop with clunkyard classes Put the binding jar and Clojure jar on the classpath; launch clojure.main bot.clj.
C#, F# Clunkyard.dll, with the reference's Private=false dotnet publish -c Release -o out -p:ClunkyardSdk="$SDK"; launch with dotnet "$SDK/bindings/csharp/host/Clunkyard.Host.dll" out/bot.dll. Use the assembly name your project emits.
PowerShell [Clunkyard.Robot] and shared assembly pwsh -NoProfile -File "$SDK/bindings/powershell/run.ps1" bot.ps1

Java, Kotlin, Scala and Clojure share Java's Foreign Function and Memory binding to the native core. Use a JDK/runtime with the FFM API required by the binding; the release image supplies it. --enable-native-access is required by the launch recipe. Hosted JVM launches also use -XX:TieredStopAtLevel=1, so local JVM timing without that flag is a different runtime configuration.

C#, F# and PowerShell share one P/Invoke assembly. The .NET host loads the installed binding while the robot artifact contains its own program and dependencies. Launch PowerShell through its SDK script so the assembly is loaded before robot class declarations are parsed. Do not copy an old Clunkyard.dll into the project output. The shared native library is libclunkyard.so.2; these bindings accept CLUNKYARD_LIB for a local path.

The generated APIs convert absent values to each family's optional/null form. Their run loops log ordinary callback exceptions and send controls already set; explicit Forfeit exceptions use the core's forfeit reply. Import/classpath errors happen before a turn: fix those in the build/start log before debugging tactics.

Other bindings in the robot process

Set SDK=/opt/clunkyard/sdk before using the prefixes in this table. A local shared-library installation must make libclunkyard.so.2 available; use CLUNKYARD_LIB only where that language's guide supports it. Packages with compiled shims are installed in the builder, so the robot needs only the runtime and the supplied package.

Language Bridge and import Local launch
Python ctypes; import clunkyard as cy PYTHONPATH="$SDK/bindings/python" python3 bot.py
R Compiled .Call package; library(clunkyard) R_LIBS="$SDK/bindings/r" Rscript --vanilla bot.R
Ruby Fiddle; require 'clunkyard' RUBYLIB="$SDK/bindings/ruby" ruby bot.rb
Perl Compiled XS; use Clunkyard PERL5LIB="$SDK/bindings/perl" perl bot.pl
Lua Lua 5.4 C module; require('clunkyard') LUA_PATH="$SDK/bindings/lua/?.lua;;" LUA_CPATH="$SDK/bindings/lua/?.so;;" lua5.4 bot.lua
PHP PHP FFI; installed clunkyard.php php -d ffi.enable=preload -d include_path="$SDK/bindings/php" bot.php
Julia ccall; using ClunkyardSDK JULIA_LOAD_PATH="$SDK/bindings/julia:@stdlib" julia bot.jl
Common Lisp SBCL sb-alien; cy package sbcl --noinform --disable-debugger --load "$SDK/bindings/common-lisp/clunkyard.fasl" --script bot.lisp
Erlang In-process NIF; clunkyard module erl -noshell -pa "$SDK/bindings/erlang" -pa . -s bot start after erlc compilation
Elixir Facade over the Erlang NIF elixir -pa "$SDK/bindings/erlang" -pa "$SDK/bindings/elixir" bot.exs
Prolog, CHR SWI-Prolog foreign module; library(clunkyard) or library(clunkyard_chr) swipl -q -p library="$SDK/bindings/prolog" -s bot.pl -g main -t halt; CHR also needs -p library="$SDK/bindings/chr"
Dart dart:ffi; package:clunkyard/clunkyard.dart Compile using the installed package configuration; run the resulting executable.
OCaml Compiled C stubs; Clunkyard Link the installed clunkyard.cmxa; run the executable with the image's shared core.

The hosted BEAM recipes constrain schedulers for the robot's single CPU; the Julia recipe uses its supplied system image and runtime options. Use the hosted-profile route when comparing their resource use. Package/version mismatches can fail before any ready reply: use the matching builder/runtime pair and check the native-loader error first.

Native toolchains and generated shims

These profiles compile a native executable. Their binding marshals calls in that process and links the core statically. Include/import paths and any precompiled binding object differ by language; follow its generated guide.

Languages SDK surface
C Generated clunkyard.h; no extra wrapper
C++ clunkyard.hpp
Rust clunkyard crate passed with --extern
Go import "clunkyard"; cgo package in the installed GOPATH, or the builder's module workspace
Hew import hew.clunkyard; generated scalar/bytes C shim
Swift, Zig Generated native C-compatible declarations and language wrappers
Haskell Clunkyard; FFI and generated Storable records
Fortran clunkyard module using ISO_C_BINDING
D clunkyard interface and compiled object
Ada Clunkyard package with C-convention records
Nim clunkyard module through Nim's C backend
Mercury Installed Mercury library; typed native references and records
Free Pascal Clunkyard unit
FreeBASIC clunkyard.bi and compiled loop helper
COBOL clunkyard.cpy and generated by-reference C shim

A generated shim bridges a calling convention or record representation; it is not another tactic process. Keep compiled objects and module interfaces from the same SDK and target architecture. The arena's runtime contains no compiler. Linker errors naming cy_* usually mean the core archive is missing or in the wrong link order; missing module/type declarations point to an include/import path or a stale SDK copy.

The C API guide explains ownership, statuses, lengths and the session lifecycle underneath these bindings. Exception handling follows the language guide: for example, Hew returned errors are recoverable but a Hew panic ends the process; FreeBASIC runtime errors also end the process.

Node, DSL, WebAssembly and shell

JavaScript and TypeScript import clunkyard from the installed Node package. It loads the core's WebAssembly build in Node; this is an ordinary Standard Node robot. Launch through "$SDK/bindings/javascript/run" with NODE_PATH="$SDK/bindings/javascript". The launcher reserves the protocol descriptor before loading user code and sends stdout diagnostics to stderr. TypeScript uses the same package and types, compiled through the hosted TypeScript profile.

Vane, Cadence and Glyph run through the built-in interpreter, which obtains its vocabulary and helper values from the same core. Local source entries run as node src/dsl/runner.js vane /absolute/path/bot.vane (or cadence/glyph). Their corresponding *-wasm profiles compile the same source into a guest module. The interpreted profiles count DSL operations; compiled guests count fuel and imports.

ABI 4 guests cover WAT and AssemblyScript, C, Rust and Zig compiled to WebAssembly, as well as the compiled DSL profiles. The trusted Wasmtime host owns protocol I/O; the guest receives clunkyard.* imports and exports startup/turn functions. It has no WASI environment. A Node binding internally using a WebAssembly core does not make the robot an ABI 4 guest; its user code still runs in Node and is metered as Node.

The shell helper is a raw protocol 2 route. Its launcher uses one long-running jq, whose module supplies JSON helpers and the persistent turn/memory loop. It does not call the C ABI. Use the shell starter's installed/copied helper arrangement and jq --unbuffered so each reply reaches the referee. cy::log writes diagnostics to stderr. Avoid spawning a command for every small calculation inside a turn.

Diagnose a failed start or turn

First reproduce with the project's actual launcher, imports and dependencies. Use a short protocol fixture or the binding's play helper, then run a real match. A process waiting for its first hello is normal when launched without input. Build errors, missing shared libraries and invalid imports must be fixed before a ready reply is possible.

Keep protocol output reserved for the session. Native stdio sessions move stdout diagnostics to stderr at open; Node does so in its launcher. Memory sessions leave process streams alone. Raw protocol programs must write and flush exactly the expected ready or act reply. For process adapters, also keep the tactic-to-adapter RPC pipe free of unrelated output.

Use status text/value to expose a decision in the viewer and stderr for longer diagnostics. Optional absence is normal: check it before reading a contact, a shot solution or a region. Ordinary callback exceptions generally log and send controls already set; consult the binding guide for explicit forfeit and uncatchable-error behaviour. A core session fault follows the shared core recovery contract.

The installed routes above are selected by the hosted worker. Runtime keys and accounting families come from the rates implementation, and the language-specific surfaces are generated under sdk/bindings.

Download this page as Markdown