Copyright (c) 2026 Muhammad Anisur Rahman. All rights reserved.
Version 1.0.0
icpp is kcpp -icpp: the same compiler, running your C in-process instead of producing assembly. No build step, no object files, no target hardware involved - it executes on the machine you are sitting at.
Beyond C itself it provides 135 builtin functions: collections, strings, files, processes, JSON, regular expressions, test assertions and more.
// hello.icpp
void main()
{
int i;
for (i = 0; i < 3; i++) iprint(i);
iprint("icpp works");
}
kcpp -icpp -icppquiet hello.icpp
0
1
2
icpp works
-icppquiet suppresses the compiler's own status lines (Compiling:, Total errors:). Real error text and your program's output are unaffected, so it is the right flag for a script whose output you intend to consume.
| Option | Does |
|---|---|
-icpp |
interpret and execute in-process. No .asm or .o is produced |
-icppquiet |
drop kcpp's status lines from stdout |
-Ipath |
additional include directory (repeatable) |
-Dname[=value] |
define a macro |
-llmtrace |
walk through every LLM-pipeline builtin as it runs, explaining each step |
-rtl |
translate the script's integer functions to synthesizable Verilog instead of running them |
The compiler options that describe a target - -cpu32, exe=, -core= - have no meaning here. icpp runs host-native, which is the point.
Everything below was verified by running it. Outputs shown are real.
icpp_substr(s, start, len) icpp_strlen(s)
icpp_upper(s) icpp_lower(s) icpp_trim(s)
icpp_strstr(haystack, needle) icpp_strcmp(a, b)
icpp_search_txt(needle, buffer) // index of needle in buffer ignoring case, or -1
icpp_starts_with(s, prefix) icpp_ends_with(s, suffix)
icpp_replace_all(s, from, to) icpp_re(s, pattern)
icpp_split(s, sep) icpp_join(listhandle, sep)
icpp_concat(a, b)
iprint(icpp_substr("hello world", 6, 5)); // world
iprint(icpp_substr("hello world", 6, 0)); // "" (a zero length is empty; a negative one means to the end)
iprint(icpp_trim(" pad ")); // pad
iprint(icpp_strstr("hello", "ll")); // 2 <- an INDEX
iprint(icpp_search_txt("RTOS", "the rtos scheduler")); // 4 <- ignores case; needle FIRST
iprint(icpp_search_txt("haskell", "the rtos scheduler")); // -1
iprint(icpp_strcmp("a", "b")); // -1
iprint(icpp_replace_all("a-b-c", "-", "+"));// a+b+c
iprint(icpp_re("order 1234 shipped", "[0-9]+")); // 1234
iprint(icpp_concat("ab", "cd")); // abcd
icpp_split returns a list handle, not a string:
int h = icpp_split("a,b,c", ",");
iprint(icpp_list_count(h)); // 3
iprint(icpp_list_get(h, 1)); // b <- lists are 0-based
iprint(icpp_join(h, "|")); // a|b|c
icpp_list_free(h);
icpp_atoi(s) icpp_parse_hex(s)
icpp_hex(value, width) icpp_bin(value, width)
icpp_bits(value, lowBit, highBit)
iprint(icpp_atoi("42")); // 42
iprint(icpp_hex(255, 4)); // 00ff
iprint(icpp_parse_hex("ff")); // 255
iprint(icpp_bin(5, 8)); // 00000101
icpp_bits takes an inclusive BIT RANGE, not a start and a count. That is worth stating plainly because the other reading is the natural guess and it is wrong:
iprint(icpp_bits(240, 4, 7)); // 15 bits 4..7 of 0xF0
iprint(icpp_bits(240, 0, 3)); // 0
iprint(icpp_bits(5, 1, 1)); // 0 a single bit
Four types, all handle-based: create one, pass the integer handle around, free it when done.
Full names, so this table can be searched:
icpp_list_new() icpp_list_add(h,v) icpp_list_get(h,i)
icpp_list_count(h) icpp_list_sort(h) icpp_list_free(h)
icpp_dict_new() icpp_dict_set(h,k,v) icpp_dict_get(h,k) icpp_dict_has(h,k)
icpp_dict_count(h) icpp_dict_keys(h) icpp_dict_delete(h,k) icpp_dict_free(h)
icpp_map_new() icpp_map_set(h,k,v) icpp_map_get(h,k) icpp_map_has(h,k)
icpp_map_count(h) icpp_map_keys(h) icpp_map_delete(h,k) icpp_map_free(h)
icpp_set_new() icpp_set_add(h,v) icpp_set_has(h,v)
icpp_set_count(h) icpp_set_remove(h,v) icpp_set_items(h) icpp_set_free(h)
icpp_tuple_new() icpp_tuple_add(h,v) icpp_tuple_get(h,i)
icpp_tuple_count(h) icpp_tuple_free(h)
map and dict have the same operations. list is ordered and indexed, set holds each value once, tuple is an ordered group.
int d = icpp_dict_new();
icpp_dict_set(d, "name", "anis");
icpp_dict_set(d, "lang", "icpp");
iprint(icpp_dict_get(d, "name")); // anis
iprint(icpp_dict_has(d, "nope")); // 0
iprint(icpp_dict_count(d)); // 2
int k = icpp_dict_keys(d); // a LIST handle
iprint(icpp_list_get(k, 0)); // name
icpp_dict_delete(d, "lang");
iprint(icpp_dict_count(d)); // 1
icpp_dict_free(d);
icpp_list_sort sorts in place:
int l = icpp_list_new();
icpp_list_add(l, "b"); icpp_list_add(l, "a");
icpp_list_sort(l);
iprint(icpp_list_get(l, 0)); // a
icpp_list_free(l);
A set really is a set:
int s = icpp_set_new();
icpp_set_add(s, "x"); icpp_set_add(s, "x"); icpp_set_add(s, "y");
iprint(icpp_set_count(s)); // 2, not 3
icpp_dict_keys and icpp_set_items return list handles, so free those too.
icpp_file_read(path) icpp_file_write(path, text)
icpp_file_exists(path) icpp_file_size(path)
icpp_file_lines(path) icpp_file_line(path, n)
icpp_file_contains(path, text) icpp_file_replace(path, from, to)
icpp_file_hash(path) icpp_file_fingerprint(path)
icpp_file_mtime(path) icpp_file_diff(a, b)
icpp_remove(path) icpp_rename(a,b) icpp_mkdir(path)
icpp_glob(pattern) icpp_glob_r(dir, pattern)
icpp_cwd() icpp_chdir(path)
icpp_file_write("demo.txt", "alpha\nbeta\ngamma\n");
iprint(icpp_file_size("demo.txt")); // 17
iprint(icpp_file_lines("demo.txt")); // 3
iprint(icpp_file_line("demo.txt", 1)); // alpha
iprint(icpp_file_contains("demo.txt", "beta"));// 1
iprint(icpp_file_hash("demo.txt")); // 3a3fd8bc
icpp_file_line is 1-BASED - line 1 is the first line, and line 0 is empty. Lists are 0-based. The two disagree, so it is worth checking which one you are holding.
Sequential reading, for a file you do not want in memory at once:
int fh = icpp_fopen("demo.txt");
while (icpp_feof(fh) == 0)
iprint(icpp_trim(icpp_fgets(fh)));
icpp_fclose(fh);
icpp_glob returns a list handle:
int g = icpp_glob("*.icpp");
iprint(icpp_list_count(g));
icpp_list_free(g);
icpp_bin_read(path) icpp_bin_size(h) icpp_bin_get(h, offset)
icpp_bin_int32(h, offset) icpp_bin_float(h, offset)
icpp_bin_find(h, text) icpp_bin_free(h)
int b = icpp_bin_read("demo.txt");
iprint(icpp_bin_size(b)); // 17
iprint(icpp_bin_get(b, 0)); // 97 'a'
iprint(icpp_bin_find(b, "BETA")); // 6 byte offset
icpp_bin_free(b);
icpp_bin_read returns -1 from icpp_bin_size if the file could not be read, so check it before trusting an offset.
icpp_exec(cmd) icpp_exec_capture(cmd)
icpp_exec_to_file(cmd, path) icpp_exec_status()
icpp_getenv(name) icpp_setenv(name, value)
icpp_time() icpp_date_string() icpp_elapsed_ms()
icpp_getparam(name) icpp_exit(code)
iprint(icpp_exec_capture("echo hi")); // hi
iprint(icpp_exec_status()); // 0
iprint(icpp_date_string()); // 2026-08-20 11:08:14
icpp_setenv("ICPP_DEMO", "set-by-script");
iprint(icpp_getenv("ICPP_DEMO")); // set-by-script
icpp_json_get(jsonText, dottedPath)
One function, and it does the drilling for you. A numeric path segment is an array index:
iprint(icpp_json_get("{\"a\":{\"b\":[10,20]}}", "a.b.1")); // 20
Reading a JSON file is that composed with icpp_file_read:
iprint(icpp_json_get(icpp_file_read("d.json"), "user.name"));
iprint(icpp_json_get(icpp_file_read("d.json"), "user.langs.1"));
icpp_assert(condition, message)
icpp_assert_eq_int(got, expected, message)
icpp_assert_eq_str(got, expected, message)
icpp_test_summary()
icpp_assert_eq_int(2 + 2, 4, "arithmetic");
icpp_assert_eq_str(icpp_upper("hi"), "HI", "upper");
icpp_assert_eq_int(1, 2, "this one should fail");
iprint(icpp_test_summary());
PASS: arithmetic (4)
PASS: upper
FAIL: this one should fail - got 1, expected 2
=== 4 checks: 3 passed, 1 failed ===
A failing assertion reports and continues rather than stopping the script, so one run tells you everything that is wrong instead of only the first thing. icpp_test_summary() returns the failure count, which is what a script should pass to icpp_exit().
An icpp library is a .icpp file of reusable functions, pulled in with an ordinary include. No packaging step, no archive format.
#include "stringutil.icpp"
void main()
{
iprint(su_shout("hi")); // -> HI!
iprint(su_wordcount("a b c")); // -> 3
}
lib_icpp/stringutil.icpp is the reference example.
By convention a library starts with tagged comments. They are comments, so the interpreter ignores them - but a script can read them, which is what makes version checking possible.
//@icpp-library stringutil
//@icpp-version 1.2.0
//@icpp-author Muhammad Anisur Rahman
//@icpp-copyright (c) 2026 Muhammad Anisur Rahman - MIT
//@icpp-contact contact@chapai.ai
//@icpp-requires 0.40 ; minimum interpreter version
//@icpp-since 1.2.0: added su_repeat()
| Field | Meaning |
|---|---|
library |
the library's name |
version |
dotted numbers, MAJOR.MINOR.PATCH |
author |
who wrote it |
copyright |
copyright or licence line |
contact |
email or URL |
requires |
minimum interpreter version it needs |
since |
what changed, newest first |
One tag per line; the value runs to end-of-line or an inline ; comment and is trimmed; the first occurrence wins; put the manifest in the first ~200 lines, because that is where the reader stops looking. Extra fields of your own are fine.
icpp_lib_field(file, field) // the @icpp-<field> value, or "" if absent
icpp_lib_version(file) // shorthand for the "version" field
icpp_version_cmp(a, b) // -1 / 0 / 1
icpp_version_cmp compares semantically, not as text - so 1.10.0 is correctly greater than 1.9.0, which a string comparison gets backwards.
if (icpp_version_cmp(icpp_lib_version("stringutil.icpp"), "1.2.0") < 0)
{
iprint("stringutil >= 1.2.0 required");
icpp_exit(1);
}
Worth writing whenever a script depends on a function added in a particular version. Without it the failure is a missing symbol at the point of use, which says nothing about which version you needed.
Running an image, OCR or fingerprint builtin prints:
icpp: this is the CORE build - image, OCR and fingerprint
builtins are not included, because they depend on
third-party libraries that are not distributed with it.
icpp: 'icpp_img_new' is not available in the core build
They are declared and callable, and refuse clearly rather than returning a wrong answer - which is the right behaviour: a silent 0 from an image function would be indistinguishable from a black pixel.
icpp_img_new icpp_img_set icpp_img_get icpp_img_line icpp_img_rect
icpp_img_fill_rect icpp_img_save_png icpp_img_free
icpp_image_phash icpp_image_crop
icpp_ocr_extract
icpp_phash_similarity icpp_fingerprint_similarity icpp_fingerprint_match
icpp_afis_extract icpp_afis_minutiae icpp_afis_match icpp_afis_match_files
icpp_afis_free
These are compiled in - icpp_sql_connect and icpp_mcp_connect return 0 rather than refusing - but exercising them needs a database, a server or an API key, so they are not documented here. Describing behaviour I have not observed would be guessing:
icpp_sql_connect icpp_sql_query icpp_sql_next icpp_sql_get
icpp_sql_count icpp_sql_status icpp_sql_free icpp_sql_disconnect
icpp_mcp_connect icpp_mcp_list_tools icpp_mcp_call icpp_mcp_disconnect
icpp_openai_chat
icpp_text_embed
They are listed so this manual is a complete inventory of what the interpreter has, not so you can infer how they behave from their names.
The -llmtrace flag prints a step-by-step walkthrough of the LLM-pipeline builtins as they run - tokenising, embeddings, positional encoding, attention, softmax, loss, backward pass, SGD - including why each step is there. It exists to show how a language model works rather than to debug one.
kcpp -rtl adder.icpp # -> adder.v and adder_tb.v
-rtl translates the script's integer functions into synthesizable Verilog plus a self-checking testbench. The testbench is the part that matters: it is what lets you confirm the generated hardware agrees with the C you wrote, rather than assuming the translation was faithful.
KCPP=<path to kcpp.exe> bash test/icpp_conformance/run_conformance.sh
Prints per-test results and a final RESULT: line. Always pass KCPP= explicitly - the runner otherwise defaults to a binary that may not be the one you just built, and a suite that silently tested the wrong compiler is worse than one that failed.
2026-09-16
icpp_search_txt(needle, buffer): the position of needle in buffer ignoring ASCII case,or -1. The needle comes first ("search for this, in that"), the reverse of icpp_strstr.
-icppquiet now also silences the #include bookkeeping, so a page that includes a helperno longer starts with compiler trace lines.
2026-09-15
to both show the last result, because the callee's own buffer was handed out and overwritten.
icpp_substr(s, start, 0) is an empty string. It used to return all of s; only a negativelength means "to the end".
$var may share a function's name without crashing the interpreter.All four are covered by test/icpp_conformance/11_function_strings.icpp.
python(<file>) and java(<file>) - system functions to read and execute a Python script or a Java source file from inside icpp. Not in this release.