/* Calls the C ABI the way a JNI or Swift binding will: through include/tally.h, * linked against the real library. * * The Zig tests in c_api.zig cover behaviour and are leak-checked. This covers what * they cannot: that the header matches the library, that the symbols are findable by a * C linker, and that the struct layouts agree across the language boundary. A wrong * field order in tally.h is invisible from Zig and fatal in the field. */ #include "tally.h" #include #include static int failures = 0; static void check(int condition, const char *what) { if (!condition) { printf("FAIL: %s\n", what); failures++; } } static void check_contains(const uint8_t *json, size_t len, const char *needle, const char *what) { /* The JSON is not null-terminated: it is a borrowed (ptr, len). */ char buf[4096]; size_t n = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1; memcpy(buf, json, n); buf[n] = '\0'; if (strstr(buf, needle) == NULL) { printf("FAIL: %s\n wanted: %s\n got: %s\n", what, needle, buf); failures++; } } int main(void) { check(tally_abi_version() == TALLY_ABI_VERSION, "header and library agree on the ABI version"); size_t version_len = 0; const uint8_t *version = tally_version(&version_len); check(version_len == 5 && memcmp(version, "0.1.0", 5) == 0, "tally_version"); tally_session *session = tally_session_new(); check(session != NULL, "tally_session_new"); if (session == NULL) return 1; const uint8_t *json = NULL; size_t len = 0; /* A session that remembers: the whole reason for a handle. */ const char *assign = "x = 2^100"; check(tally_eval(session, (const uint8_t *)assign, strlen(assign), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "assignment evaluates"); const char *use = "x + 1"; check(tally_eval(session, (const uint8_t *)use, strlen(use), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "the variable survives into the next call"); check_contains(json, len, "1,267,650,600,228,229,401,496,703,205,377", "exact 128-bit arithmetic crosses the boundary intact"); const char *ans = "Ans / 2"; check(tally_eval(session, (const uint8_t *)ans, strlen(ans), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "Ans is readable"); check_contains(json, len, "633,825,300,114,114,700,748,351,602,688.5", "half of an odd number, so the exact tier is really exact"); /* A preview answers and stores nothing: neither the variable nor Ans. */ const char *peek = "y = 41 + 1"; check(tally_preview(session, (const uint8_t *)peek, strlen(peek), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "preview evaluates"); check_contains(json, len, "\"display\":\"42\"", "a preview carries the answer"); const char *y = "y"; check(tally_eval(session, (const uint8_t *)y, strlen(y), TALLY_MODE_STANDARD, &json, &len) == TALLY_EVAL_ERROR, "a previewed assignment stored nothing"); const char *same = "Ans"; check(tally_eval(session, (const uint8_t *)same, strlen(same), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "Ans after a preview"); check_contains(json, len, "633,825,300,114,114,700,748,351,602,688.5", "Ans is still the last committed answer"); /* An error is a status plus the engine's own wording. */ const char *bad = "1 / 0"; check(tally_eval(session, (const uint8_t *)bad, strlen(bad), TALLY_MODE_STANDARD, &json, &len) == TALLY_EVAL_ERROR, "division by zero is an eval error"); check_contains(json, len, "division by zero", "the error carries the engine's phrase"); /* Unit conversion, from standard mode, with no separate entry point. */ const char *convert = "100 km to mi"; check(tally_eval(session, (const uint8_t *)convert, strlen(convert), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "conversion evaluates"); check_contains(json, len, "\"from\":\"km\"", "the conversion names its units"); /* The config struct crosses the boundary with its fields where the header says. */ tally_config config; check(tally_session_config(session, &config) == TALLY_OK, "reading the config"); check(config.bits == 64, "default width is 64"); config.bits = 8; config.is_signed = 1; check(tally_session_configure(session, &config) == TALLY_OK, "configuring 8-bit signed"); const char *ff = "0xFF"; check(tally_eval(session, (const uint8_t *)ff, strlen(ff), TALLY_MODE_PROGRAMMER, &json, &len) == TALLY_OK, "programmer mode"); check_contains(json, len, "\"dec_signed\":\"-1\"", "0xFF read as 8-bit signed is -1, so bits and is_signed both landed"); check_contains(json, len, "\"bits\":8", "the width reached the result"); check_contains(json, len, "\"pattern\":\"ff\"", "the pattern to hold, masked and unsigned"); config.bits = 24; check(tally_session_configure(session, &config) == TALLY_INVALID_ARGUMENT, "a width the engine has no name for is refused"); /* The catalogue a picker is built from. */ check(tally_unit_catalog(session, &json, &len) == TALLY_OK, "unit catalogue"); check_contains(json, len, "\"base_unit\":\"m\"", "length's base unit"); check_contains(json, len, "kilometer", "aliases come along for searching"); /* check_contains reads the first 4KB; length is the first category. */ check_contains(json, len, "{\"name\":\"km\",\"label\":\"Kilometer\"", "units carry a display name"); /* One value in every unit of its category, for a converter screen. */ const char *hundred = "100"; const char *celsius = "C"; check(tally_convert(session, (const uint8_t *)hundred, strlen(hundred), (const uint8_t *)celsius, strlen(celsius), &json, &len) == TALLY_OK, "tally_convert"); check_contains(json, len, "{\"unit\":\"F\",\"display\":\"212\"", "100 C is 212 F"); check_contains(json, len, "{\"unit\":\"K\",\"display\":\"373.15\"", "and 373.15 K"); /* Save one session and load it into another: x and Ans come across exactly. */ const char *keep = "x = 2^100"; check(tally_eval(session, (const uint8_t *)keep, strlen(keep), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "assign before save"); check(tally_session_save(session, &json, &len) == TALLY_OK, "tally_session_save"); check_contains(json, len, "\"name\":\"x\",\"value\":{\"exact\":\"1267650600228229401496703205376\"}", "an exact value is saved with every digit"); tally_session *restored = tally_session_new(); check(restored != NULL, "a second session"); if (restored != NULL) { /* The saved bytes belong to the first session, so they are still valid here. */ check(tally_session_load(restored, json, len) == TALLY_OK, "tally_session_load"); const char *use_x = "x + 1"; check(tally_eval(restored, (const uint8_t *)use_x, strlen(use_x), TALLY_MODE_STANDARD, &json, &len) == TALLY_OK, "x after load"); check_contains(json, len, "1,267,650,600,228,229,401,496,703,205,377", "the loaded variable is the saved one"); const char *junk = "{\"format\":1}"; check(tally_session_load(restored, (const uint8_t *)junk, strlen(junk)) == TALLY_INVALID_ARGUMENT, "a malformed state is refused"); tally_session_free(restored); } /* Null handling, since a cleanup path should not need its own checks. */ check(tally_eval(NULL, (const uint8_t *)use, strlen(use), TALLY_MODE_STANDARD, &json, &len) == TALLY_INVALID_ARGUMENT, "a null session is refused"); tally_session_free(NULL); tally_session_free(session); if (failures == 0) { printf("c_abi_test: all checks passed\n"); return 0; } printf("c_abi_test: %d check(s) failed\n", failures); return 1; }