Need people Post a job, review proposals, and pay through escrow. Hire a freelancer at Far Reach →

icpp - running C as a script

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.

Running something


// 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.

Options

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.


Builtins

Everything below was verified by running it. Outputs shown are real.

Strings


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);

Numbers and bits


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

Collections

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.

Files


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);

Binary files


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.

Processes and environment


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

JSON


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"));

Test assertions


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().

Libraries

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.

The manifest

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.

Checking a version before relying on it


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.


Not in this build

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

Present, but needing something external

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.


Verilog from a script


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.

Conformance suite


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.

Changes

2026-09-16

2026-09-15

All four are covered by test/icpp_conformance/11_function_strings.icpp.

Planned

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.