1 module test_bindings;
2 
3 import llama;
4 
5 // ---------------------------------------------------------------------------
6 // Compile-time constant checks
7 // ---------------------------------------------------------------------------
8 
9 @("LLAMA constants match header values")
10 unittest
11 {
12     static assert(LLAMA_DEFAULT_SEED == 0xFFFF_FFFFu);
13     static assert(LLAMA_TOKEN_NULL == -1);
14     static assert(LLAMA_SESSION_VERSION == 9);
15 }
16 
17 @("GGML constants match header values")
18 unittest
19 {
20     static assert(GGML_MAX_DIMS == 4);
21     static assert(GGML_MAX_SRC == 10);
22     static assert(GGML_MAX_NAME == 64);
23     static assert(GGML_ROPE_TYPE_NEOX == 2);
24     static assert(GGML_ROPE_TYPE_MROPE == 8);
25     static assert(GGML_ROPE_TYPE_VISION == 24);
26 }
27 
28 // ---------------------------------------------------------------------------
29 // Symbol reachability — core API
30 // ---------------------------------------------------------------------------
31 
32 @("llama_* core symbols are reachable")
33 unittest
34 {
35     auto _0  = &llama_backend_init;
36     auto _1  = &llama_backend_free;
37     auto _2  = &llama_model_load_from_file;
38     auto _3  = &llama_model_free;
39     auto _4  = &llama_init_from_model;
40     auto _5  = &llama_free;
41     auto _6  = &llama_decode;
42     auto _7  = &llama_encode;
43     auto _8  = &llama_get_logits_ith;
44     auto _9  = &llama_tokenize;
45     auto _10 = &llama_detokenize;
46     auto _11 = &llama_token_to_piece;
47     auto _12 = &llama_model_get_vocab;
48     auto _13 = &llama_vocab_bos;
49     auto _14 = &llama_vocab_eos;
50     auto _15 = &llama_vocab_is_eog;
51     auto _16 = &llama_sampler_chain_init;
52     auto _17 = &llama_sampler_chain_add;
53     auto _18 = &llama_sampler_init_greedy;
54     auto _19 = &llama_sampler_sample;
55     auto _20 = &llama_sampler_free;
56     auto _21 = &llama_batch_get_one;
57     auto _22 = &llama_perf_context_print;
58     auto _23 = &llama_perf_sampler_print;
59     auto _24 = &llama_model_n_embd;
60     auto _25 = &llama_model_n_layer;
61     auto _26 = &llama_model_has_encoder;
62     auto _27 = &llama_model_has_decoder;
63 }
64 
65 @("llama_* extended API symbols are reachable")
66 unittest
67 {
68     // Context / memory / state
69     auto _0  = &llama_get_memory;
70     auto _1  = &llama_memory_seq_rm;
71     auto _2  = &llama_memory_seq_cp;
72     auto _3  = &llama_memory_seq_keep;
73     auto _4  = &llama_memory_seq_add;
74     auto _5  = &llama_memory_seq_div;
75     auto _6  = &llama_memory_seq_pos_min;
76     auto _7  = &llama_memory_seq_pos_max;
77     auto _8  = &llama_memory_can_shift;
78     auto _9  = &llama_state_get_size;
79     auto _10 = &llama_state_get_data;
80     auto _11 = &llama_state_set_data;
81     auto _12 = &llama_state_load_file;
82     auto _13 = &llama_state_save_file;
83     // Embeddings
84     auto _14 = &llama_get_embeddings;
85     auto _15 = &llama_get_embeddings_ith;
86     auto _16 = &llama_get_embeddings_seq;
87     // Chat
88     auto _17 = &llama_chat_apply_template;
89     auto _18 = &llama_chat_builtin_templates;
90     // Adapter / LoRA
91     auto _19 = &llama_adapter_lora_init;
92     auto _20 = &llama_adapter_lora_free;
93     auto _21 = &llama_set_adapters_lora;
94     // Model metadata
95     auto _22 = &llama_model_n_ctx_train;
96     auto _23 = &llama_model_n_params;
97     auto _24 = &llama_model_size;
98     auto _25 = &llama_model_desc;
99     auto _26 = &llama_model_chat_template;
100     auto _27 = &llama_model_is_recurrent;
101     auto _28 = &llama_model_meta_count;
102     auto _29 = &llama_model_meta_key_by_index;
103     auto _30 = &llama_model_meta_val_str;
104     auto _31 = &llama_model_meta_val_str_by_index;
105     // Vocab extras
106     auto _32 = &llama_vocab_nl;
107     auto _33 = &llama_vocab_pad;
108     auto _34 = &llama_vocab_sep;
109     auto _35 = &llama_vocab_fim_pre;
110     auto _36 = &llama_vocab_fim_suf;
111     auto _37 = &llama_vocab_fim_mid;
112     auto _38 = &llama_vocab_fim_pad;
113     auto _39 = &llama_vocab_fim_rep;
114     auto _40 = &llama_vocab_fim_sep;
115     auto _41 = &llama_vocab_get_text;
116     auto _42 = &llama_vocab_get_score;
117     auto _43 = &llama_vocab_get_attr;
118     auto _44 = &llama_vocab_is_control;
119     // Samplers
120     auto _45 = &llama_sampler_init_penalties;
121     auto _46 = &llama_sampler_init_typical;
122     auto _47 = &llama_sampler_init_temp_ext;
123     auto _48 = &llama_sampler_init_top_n_sigma;
124     auto _49 = &llama_sampler_init_xtc;
125     auto _50 = &llama_sampler_init_mirostat_v2;
126     auto _51 = &llama_sampler_init_grammar;
127     auto _52 = &llama_sampler_init_dry;
128     auto _53 = &llama_sampler_init_logit_bias;
129 }
130 
131 @("ggml_* symbols are reachable")
132 unittest
133 {
134     auto _0 = &ggml_time_us;
135     auto _1 = &ggml_backend_load_all;
136 }
137 
138 // ---------------------------------------------------------------------------
139 // SamplerChain: chain construction (no model required)
140 // ---------------------------------------------------------------------------
141 
142 @("SamplerChain: basic creation — greedy and noPerf variants")
143 unittest
144 {
145     {
146         auto smpl = SamplerChain.create();
147         smpl.greedy();
148     }
149     {
150         auto smpl = SamplerChain.create(/*noPerf=*/true);
151         smpl.greedy();
152         assert(smpl.ptr !is null);
153     }
154 }
155 
156 @("SamplerChain: temp + topK + topP + dist")
157 unittest
158 {
159     auto smpl = SamplerChain.create();
160     smpl.temp(0.8f).topK(40).topP(0.95f).dist();
161 }
162 
163 @("SamplerChain: temp + minP + dist with explicit seed")
164 unittest
165 {
166     auto smpl = SamplerChain.create();
167     smpl.temp(1.0f).minP(0.05f).dist(42u);
168 }
169 
170 @("SamplerChain: penalties — normal and disabled (zero lastN)")
171 unittest
172 {
173     {
174         auto smpl = SamplerChain.create();
175         smpl.penalties(64, 1.1f, 0.0f, 0.0f).dist();
176     }
177     {
178         auto smpl = SamplerChain.create();
179         smpl.penalties(0).dist(); // penalty_last_n=0 disables the sampler
180     }
181 }
182 
183 @("SamplerChain: typical")
184 unittest
185 {
186     auto smpl = SamplerChain.create();
187     smpl.temp(1.0f).typical(0.95f).dist();
188 }
189 
190 @("SamplerChain: tempExt (dynamic temperature)")
191 unittest
192 {
193     auto smpl = SamplerChain.create();
194     smpl.tempExt(0.8f, 0.5f, 1.5f).dist();
195 }
196 
197 @("SamplerChain: topNSigma")
198 unittest
199 {
200     auto smpl = SamplerChain.create();
201     smpl.topNSigma(2.0f).dist();
202 }
203 
204 @("SamplerChain: xtc")
205 unittest
206 {
207     auto smpl = SamplerChain.create();
208     smpl.xtc(0.1f, 0.1f, 1, LLAMA_DEFAULT_SEED).dist();
209 }
210 
211 @("SamplerChain: mirostatV2")
212 unittest
213 {
214     auto smpl = SamplerChain.create();
215     smpl.mirostatV2(5.0f, 0.1f, LLAMA_DEFAULT_SEED);
216 }
217 
218 @("SamplerChain: logitBias — empty and with explicit entries")
219 @trusted unittest
220 {
221     {
222         auto smpl = SamplerChain.create();
223         smpl.logitBias(32_000, []).dist();
224     }
225     {
226         auto smpl = SamplerChain.create();
227         llama_logit_bias[2] biases;
228         biases[0].token = 1; biases[0].bias = -100.0f;
229         biases[1].token = 2; biases[1].bias =   10.0f;
230         smpl.logitBias(32_000, biases[]).dist();
231     }
232 }
233 
234 @("SamplerChain: combined chain — penalties + typical + tempExt")
235 unittest
236 {
237     auto smpl = SamplerChain.create();
238     smpl.penalties(64, 1.1f).typical(0.9f).tempExt(0.8f, 0.0f, 1.0f).dist();
239 }
240 
241 // ---------------------------------------------------------------------------
242 // Batch helpers
243 // ---------------------------------------------------------------------------
244 
245 @("batchGetOne: multi-token and single-token slices")
246 unittest
247 {
248     {
249         llama_token[4] toks = [1, 2, 3, 4];
250         llama_batch b = batchGetOne(toks[]);
251         assert(b.n_tokens == 4);
252         assert(b.token !is null);
253     }
254     {
255         llama_token[1] tok = [42];
256         llama_batch b = batchGetOne(tok[]);
257         assert(b.n_tokens == 1);
258     }
259 }
260 
261 @("allocBatch: token batch and embedding batch")
262 unittest
263 {
264     {
265         auto ob = allocBatch(512);
266         assert(ob.get().token !is null);
267     }
268     {
269         auto ob = allocBatch(64, 128);
270         assert(ob.get().embd !is null);
271     }
272 }
273 
274 @("batchClear: resets n_tokens to zero")
275 unittest
276 {
277     auto ob = allocBatch(8);
278     batchAdd(ob.get(), 1, 0, 0, true);
279     batchAdd(ob.get(), 2, 1, 0, false);
280     assert(ob.get().n_tokens == 2);
281     batchClear(ob.get());
282     assert(ob.get().n_tokens == 0);
283 }
284 
285 @("batchAdd: populates token, pos, seq_id, logits fields")
286 unittest
287 {
288     auto ob = allocBatch(4);
289     batchAdd(ob.get(), 42, 7, 3, true);
290     ref llama_batch b = ob.get();
291     assert(b.n_tokens        == 1);
292     assert(b.token[0]        == 42);
293     assert(b.pos[0]          == 7);
294     assert(b.n_seq_id[0]     == 1);
295     assert(b.seq_id[0][0]    == 3);
296     assert(b.logits[0]       == true);
297 }
298 
299 // ---------------------------------------------------------------------------
300 // Backend
301 // ---------------------------------------------------------------------------
302 
303 @("loadAllBackends: idempotent — safe to call multiple times")
304 unittest
305 {
306     loadAllBackends();
307     loadAllBackends();
308 }
309 
310 // ---------------------------------------------------------------------------
311 // LlamaModel
312 // ---------------------------------------------------------------------------
313 
314 @("LlamaModel: loadFromFile with nonexistent path returns falsy")
315 unittest
316 {
317     auto model = LlamaModel.loadFromFile("/nonexistent/model.gguf");
318     assert(!model);
319 }
320 
321 @("LlamaModel: new properties compile")
322 unittest
323 {
324     static assert(__traits(hasMember, LlamaModel, "isRecurrent"));
325     static assert(__traits(hasMember, LlamaModel, "nCtxTrain"));
326     static assert(__traits(hasMember, LlamaModel, "nParams"));
327     static assert(__traits(hasMember, LlamaModel, "size"));
328     static assert(__traits(hasMember, LlamaModel, "desc"));
329     static assert(__traits(hasMember, LlamaModel, "chatTemplate"));
330     static assert(__traits(hasMember, LlamaModel, "metaCount"));
331     static assert(__traits(hasMember, LlamaModel, "metaKeyAt"));
332     static assert(__traits(hasMember, LlamaModel, "metaValAt"));
333     static assert(__traits(hasMember, LlamaModel, "metaVal"));
334 }
335 
336 // ---------------------------------------------------------------------------
337 // LlamaContext
338 // ---------------------------------------------------------------------------
339 
340 @("LlamaContext: new properties compile")
341 unittest
342 {
343     static assert(__traits(hasMember, LlamaContext, "poolingType"));
344     static assert(__traits(hasMember, LlamaContext, "memory"));
345     static assert(__traits(hasMember, LlamaContext, "memoryClear"));
346     static assert(__traits(hasMember, LlamaContext, "getEmbeddings"));
347     static assert(__traits(hasMember, LlamaContext, "getEmbeddingsIth"));
348     static assert(__traits(hasMember, LlamaContext, "getEmbeddingsSeq"));
349     static assert(__traits(hasMember, LlamaContext, "stateGetSize"));
350     static assert(__traits(hasMember, LlamaContext, "stateGetData"));
351     static assert(__traits(hasMember, LlamaContext, "stateSetData"));
352     static assert(__traits(hasMember, LlamaContext, "stateSaveFile"));
353     static assert(__traits(hasMember, LlamaContext, "stateLoadFile"));
354     static assert(__traits(hasMember, LlamaContext, "stateSeqGetSize"));
355     static assert(__traits(hasMember, LlamaContext, "stateSeqGetData"));
356     static assert(__traits(hasMember, LlamaContext, "stateSeqSetData"));
357 }
358 
359 // ---------------------------------------------------------------------------
360 // Owned mixin — structural checks
361 // ---------------------------------------------------------------------------
362 
363 @("Owned mixin: injected members present on wrapper structs")
364 unittest
365 {
366     import llama.owned;
367     // ptr() property
368     static assert(__traits(hasMember, LlamaModel,   "ptr"));
369     static assert(__traits(hasMember, LlamaContext,  "ptr"));
370     static assert(__traits(hasMember, SamplerChain,  "ptr"));
371     // bool conversion via opCast
372     static assert(__traits(compiles, { auto m = LlamaModel.loadFromFile("/x"); bool b = cast(bool) m; }));
373     // not copyable
374     static assert(!__traits(compiles, { auto m = LlamaModel.loadFromFile("/x"); auto m2 = m; }));
375 }
376 
377 // ---------------------------------------------------------------------------
378 // Vocab helpers
379 // ---------------------------------------------------------------------------
380 
381 @("tokenize: empty string returns null without touching C")
382 unittest
383 {
384     const(llama_vocab)* nullVocab = null;
385     assert(tokenize(nullVocab, "") is null);
386 }
387 
388 @("detokenize: empty token slice returns empty string without touching C")
389 unittest
390 {
391     const(llama_vocab)* nullVocab = null;
392     assert(detokenize(nullVocab, []) == "");
393 }
394 
395 @("vocab: new helper function symbols compile")
396 unittest
397 {
398     static assert(__traits(compiles, &nlToken));
399     static assert(__traits(compiles, &padToken));
400     static assert(__traits(compiles, &sepToken));
401     static assert(__traits(compiles, &fimPreToken));
402     static assert(__traits(compiles, &fimSufToken));
403     static assert(__traits(compiles, &fimMidToken));
404     static assert(__traits(compiles, &fimPadToken));
405     static assert(__traits(compiles, &fimRepToken));
406     static assert(__traits(compiles, &fimSepToken));
407     static assert(__traits(compiles, &vocabType));
408     static assert(__traits(compiles, &tokenText));
409     static assert(__traits(compiles, &tokenScore));
410     static assert(__traits(compiles, &tokenAttr));
411     static assert(__traits(compiles, &isControl));
412 }
413 
414 // ---------------------------------------------------------------------------
415 // chat.d
416 // ---------------------------------------------------------------------------
417 
418 @("chat: llama_chat_message struct layout")
419 @trusted unittest
420 {
421     import llama.chat;
422     static assert(is(llama_chat_message == struct));
423     llama_chat_message msg;
424     msg.role    = "user";
425     msg.content = "Hello";
426     assert(msg.role !is null && msg.content !is null);
427 }
428 
429 @("chat: builtinTemplates returns a non-empty list of named templates")
430 unittest
431 {
432     import llama.chat : builtinTemplates;
433     auto names = builtinTemplates();
434     assert(names.length > 0);
435     foreach (n; names)
436         assert(n.length > 0);
437 }
438 
439 @("chat: chatApplyTemplate with a known built-in template name")
440 @trusted unittest
441 {
442     import llama.chat : builtinTemplates, chatApplyTemplate;
443     import std.string : toStringz;
444     auto names = builtinTemplates();
445     assert(names.length > 0);
446 
447     llama_chat_message[1] msgs;
448     msgs[0].role    = "user";
449     msgs[0].content = "Hello";
450 
451     char[1024] buf;
452     int n = chatApplyTemplate(names[0].toStringz, msgs[], false, buf[]);
453     assert(n > 0);
454 }
455 
456 @("chat: applyTemplate returns a non-empty D string")
457 @trusted unittest
458 {
459     import llama.chat : builtinTemplates, applyTemplate;
460     import std.string : toStringz;
461     auto names = builtinTemplates();
462     assert(names.length > 0);
463 
464     llama_chat_message[2] msgs;
465     msgs[0].role = "user";      msgs[0].content = "What is 2+2?";
466     msgs[1].role = "assistant"; msgs[1].content = "4";
467 
468     string result = applyTemplate(names[0].toStringz, msgs[], true);
469     assert(result.length > 0);
470 }
471 
472 // ---------------------------------------------------------------------------
473 // adapter.d
474 // ---------------------------------------------------------------------------
475 
476 @("adapter: types and functions compile")
477 unittest
478 {
479     import llama.adapter : LlamaAdapterLora, loadAdapterLora, setAdaptersLora;
480     static assert(is(LlamaAdapterLora == struct));
481     static assert(__traits(compiles, &loadAdapterLora));
482     static assert(__traits(compiles, &setAdaptersLora));
483 }
484 
485 @("adapter: loadAdapterLora with nonexistent path returns falsy")
486 unittest
487 {
488     import llama.adapter : loadAdapterLora;
489     loadAllBackends();
490     auto model = LlamaModel.loadFromFile("/nonexistent/model.gguf");
491     if (!model) return; // no model in CI — skip the rest
492     auto adapter = loadAdapterLora(model, "/nonexistent/lora.gguf");
493     assert(!adapter);
494 }
495 
496 // ---------------------------------------------------------------------------
497 // mtmd — C binding symbol reachability
498 // ---------------------------------------------------------------------------
499 
500 @("mtmd C symbols are reachable")
501 unittest
502 {
503     import llama.mtmd;
504     auto _0  = &mtmd_default_marker;
505     auto _1  = &mtmd_context_params_default;
506     auto _2  = &mtmd_init_from_file;
507     auto _3  = &mtmd_free;
508     auto _4  = &mtmd_bitmap_init;
509     auto _5  = &mtmd_bitmap_init_from_audio;
510     auto _6  = &mtmd_bitmap_free;
511     auto _7  = &mtmd_input_chunks_init;
512     auto _8  = &mtmd_input_chunks_size;
513     auto _9  = &mtmd_input_chunks_free;
514     auto _10 = &mtmd_tokenize;
515     auto _11 = &mtmd_encode_chunk;
516     auto _12 = &mtmd_get_output_embd;
517     auto _13 = &mtmd_helper_bitmap_init_from_file;
518     auto _14 = &mtmd_helper_bitmap_init_from_buf;
519     auto _15 = &mtmd_helper_get_n_tokens;
520     auto _16 = &mtmd_helper_get_n_pos;
521     auto _17 = &mtmd_helper_eval_chunks;
522 }
523 
524 @("mtmd_context_params_default: n_threads is non-negative")
525 unittest
526 {
527     import llama.mtmd : mtmd_context_params_default;
528     auto p = mtmd_context_params_default();
529     assert(p.n_threads >= 0);
530 }
531 
532 // ---------------------------------------------------------------------------
533 // MtmdBitmap wrapper
534 // ---------------------------------------------------------------------------
535 
536 @("MtmdBitmap: fromRGB creates bitmap with correct dimensions and data length")
537 unittest
538 {
539     import llama.mtmd : MtmdBitmap;
540     ubyte[12] rgb; // 2×2 RGB
541     auto bmp = MtmdBitmap.fromRGB(2, 2, rgb[]);
542     assert(cast(bool) bmp);
543     assert(bmp.nx == 2 && bmp.ny == 2);
544     assert(!bmp.isAudio);
545     assert(bmp.data.length == 12);
546     assert(bmp.ptr !is null);
547 }
548 
549 @("MtmdBitmap: fromAudio creates audio bitmap")
550 unittest
551 {
552     import llama.mtmd : MtmdBitmap;
553     float[16_000] pcm;
554     auto bmp = MtmdBitmap.fromAudio(pcm[]);
555     assert(cast(bool) bmp && bmp.isAudio);
556 }
557 
558 // ---------------------------------------------------------------------------
559 // InputChunks wrapper + range
560 // ---------------------------------------------------------------------------
561 
562 @("InputChunks: empty list — length, empty, nTokens, nPos, iteration")
563 unittest
564 {
565     import llama.mtmd : InputChunks;
566     auto chunks = InputChunks.create();
567     assert(chunks.length == 0);
568     assert(chunks.empty);
569     assert(chunks.nTokens == 0);
570     assert(chunks.nPos == 0);
571     int count;
572     foreach (chunk; chunks) count++;
573     assert(count == 0);
574 }
575 
576 // ---------------------------------------------------------------------------
577 // MtmdContext wrapper
578 // ---------------------------------------------------------------------------
579 
580 @("MtmdContext: initFromFile with nonexistent path returns falsy")
581 unittest
582 {
583     import llama.mtmd : MtmdContext;
584     auto ctx = MtmdContext.initFromFile("/nonexistent/mmproj.gguf", null);
585     assert(!ctx);
586 }
587 
588 @("MtmdContext: default marker is a non-empty C string")
589 unittest
590 {
591     import llama.mtmd : mtmd_default_marker;
592     import core.stdc.string : strlen;
593     const(char)* m = mtmd_default_marker();
594     assert(m !is null && strlen(m) > 0);
595 }