- Strict C17 Standard: All
.ccode must be strict C17 (not C89, not C99, not C11, not C23, not 'C++'). Compile with-std=c17. No 'C++' code at all, only 'C' (C17) source code will be allowed. Compiler-specific pragmas and extensions are forbidden in the core. - Pure ASCII Console I/O: All text input and output generated or processed by the engine must be pure 7-bit ASCII, not UTF-8 (no multibyte or Unicode code-points in terminal output). Raw 8-bit byte passthroughs are only permitted for binary or random-access file I/O operations.
- OS API Isolation & Agnostic Logic: Platform-specific OS APIs (e.g.,
<io.h>,<dirent.h>,<unistd.h>,<windows.h>,<dlfcn.h>) must reside exclusively in platform abstraction modules. Core logic, parsers, execution systems, and shared utilities must remain completely platform-independent and OS agnostic. - No Simulated/Hallucinated Notifications: The agent MUST NOT simulate, fake, or manually output system/tool notification messages. When launching asynchronous background tasks, the agent MUST call no more tools and wait silently for the system to deliver the actual notification.
- Subsystem Architecture: When introducing or implementing a subsystem/module, prioritize portability, readability, maintainability, modularity, deterministic behavior, and future expandability.
- Modularity and Portability: Subsystems, libraries, and modules must be modular, highly portable, and self-contained so that they can be easily reused in other C17 projects without carrying internal engine dependencies.
- Refactorability-First Design: All modules must be structured so that users and developers can safely rename, move, split, or reorganize them. The following conventions apply:
- Explicit Dependency Surfaces: Every
.cfile must#includeonly the headers it directly uses. Never rely on transitive includes (headers pulled in by other headers). This ensures that moving a file to a different directory or project immediately reveals all its true dependencies. - No Cross-Module Internal Access: Modules must only access other modules through their public header APIs. Never
#includeanother module's private/internal.hfiles or reference itsstaticsymbols via extern hacks. - One Responsibility Per File: Each
.cfile should have a single, well-defined responsibility. If a file grows beyond ~1024 lines of logic (excluding the documentation header and comments), evaluate whether it should be split into focused sub-files within the same module directory. - Consistent Naming Conventions: Public API functions must be prefixed with their module namespace (e.g.,
vm_,lexer_,mem_). Internal/static helper functions within a.cfile do not require a prefix but must never collide with public names. - No
BPPorBPP_Prefix: New code must NOT useBPPorBPP_as a prefix for identifiers, macros, include guards, file names, function names, or type names. Instead, use descriptive module-specific prefixes that reflect the component's purpose (e.g.,NUM_FORMAT_Hinstead ofRUNTIME_NUM_FORMAT_H,num_format_displayinstead ofbpp_num_format_display,VERSION_MAJORinstead ofBASIC_VERSION_MAJOR). Legacy code that already usesBPP_prefixes will be migrated incrementally — do not introduce newBPP_/bpp_references even for consistency with existing code. - No Hidden Initialization Order Dependencies: Module initialization functions (e.g.,
*_init(),*_setup()) must be callable in any order unless an explicit dependency is documented in both the caller's and callee's file headers. Circular initialization dependencies are strictly forbidden. - Header Self-Sufficiency: Every
.hfile must compile independently when included alone (i.e., it must include its own prerequisite headers). Test this mentally:#include "foo.h"in an empty.cfile should compile without errors. The most commonly missing direct includes are<stdbool.h>(forbool),<stddef.h>(forsize_t,NULL), and<stdint.h>(forint32_t,uint8_t, etc.). These are frequently pulled in transitively on MSVC but NOT on GCC, causing cross-platform build failures.
- Explicit Dependency Surfaces: Every
- 64-bit Pointer Safety: Never cast pointers directly to/from standard integer types. Use
<stdint.h>typesintptr_toruintptr_t. - Zero-Initialization by Default: All dynamic arrays, collection buffers, hash table buckets, and structures containing pointers/references MUST be zero-initialized immediately upon allocation. Use
callocinstead ofmalloc, or perform an immediatememsetto zero out the memory. This rule also applies to stack-allocated arrays and structs: declare them with= {0}initializers ormemsetthem to zero before use, especially when fields are conditionally populated or when the variable may be read on early-exit error paths. - Iteration Bounds Verification: Loops iterating over collections (maps, hash tables, dynamic arrays) must strictly bound their iterations to the initialized item count (e.g.,
countorsize), NOT the raw physical allocation size (capacity), unless they are designed to scan/re-initialize the raw storage and have guaranteed NULL sentinels. - Bounded String Operations: Never use unbounded string functions (
strchr,strstr,strcmp,strcpy,strlen) directly on raw substrings that are not null-terminated. Always use bounded variants (memchr,memcmp, or copy to a local null-terminated buffer) to prevent out-of-bounds reads. - Safe Realloc Pattern: Unsafe
reallocassignments (*p_buf = realloc(*p_buf, size)) are strictly forbidden. Always use a checked/safe growth helper that preserves the original pointer on failure. Additionally, after a successfulreallocthat increases capacity, the newly added slots/bytes MUST be immediately zeroed withmemset(ptr + old_count, 0, (new_count - old_count) * element_size)to prevent reads of uninitialized memory in the extended region. - Thread-Safe Localtime: Standard thread-unsafe
localtimeis forbidden. Always use thread-safe wrappers likelocaltime_s(Windows) /localtime_r(POSIX) or a unified platform abstraction. - Post-Release Pointer Nullification: After calling
free()or any resource-release function on a pointer or handle that a centralized cleanup path (e.g., a loop,goto cleanup, or function epilogue) may also attempt to release, the pointer/handle MUST be immediately set toNULL(or the handle zeroed). This prevents double-free when both the error-specific path and the centralized path execute. Example:free(ptr); ptr = NULL;. - No Dangling Pointer Comparisons: After
free(ptr), the value ofptris indeterminate. Never compare another pointer against a freed pointer's former value (e.g.,if (other != freed_ptr)is undefined behavior). Instead, use aboolflag set before freeing. - Partial Construction Cleanup: When a loop or multi-step sequence allocates resources incrementally (e.g., deserializing N elements, building an array of strings), any failure mid-stream MUST release all resources successfully created in prior iterations before returning the error. Never
freea container without first releasing the individually-owned resources within it. Track the iteration index to know how many resources need cleanup. - Overflow-Safe Arithmetic Comparisons: Never write bounds checks in the form
if (a + b > limit)whenaandbaresize_tor unsigned types, because the addition can silently wrap around to a small value, bypassing the check. Instead use the overflow-safe equivalent:if (b > limit || a > limit - b). For multiplicative checks, use:if (a > 0 && b > SIZE_MAX / a). When adding a compile-time constant (e.g.,size + sizeof(size_t)), first checkif (size > SIZE_MAX - sizeof(size_t))before performing the addition. - Division Guard on Runtime Data: Before any integer division or modulo operation where the divisor originates from runtime data (user input, file reads, configuration values, struct fields), validate that the divisor is non-zero. Either return an error, or fall back to a documented default value. Never assume a data-derived divisor is valid without an explicit check.
- snprintf Buffer Sizing for GCC: The use of
sprintfis strictly forbidden; always usesnprintfwith an explicit buffer size. Allsnprintfdestination buffers must be sized to accommodate the theoretical worst-case output length of the format string, not just the expected typical output. GCC's-Wformat-truncation=performs conservative worst-case analysis and will emit errors (under-Werror) if the buffer could theoretically be too small. When combining two bounded strings (e.g.,"%s.%s"with two 128-byte inputs), the buffer must be at least128 + 1 + 128 + 1 = 258bytes. For ISO 8601 date formatting, use at least 64 bytes to satisfy GCC's analysis oftm_yearrange. Afterstrncpy, always explicitly null-terminate the destination:buf[sizeof(buf) - 1] = '\0';. - GCC Circular Static Library Resolution: When micro-libraries are linked PUBLIC into a parent static library and reference symbols from the parent's own source files, GCC's single-pass linker will fail with undefined reference errors (MSVC's multi-pass linker resolves these automatically). For any executable target that links against such a parent library on GCC/Clang, apply
set_target_properties(<target> PROPERTIES LINK_FLAGS "-Wl,--start-group")in CMakeLists.txt. - POSIX Feature-Test Macros for Micro-Libraries: When creating new CMake static library targets (micro-libraries) that compile
.cfiles using POSIX APIs, you MUST add explicit compile definitions on UNIX:target_compile_definitions(<target> PRIVATE $<$<BOOL:${UNIX}>: _POSIX_C_SOURCE=200809L _XOPEN_SOURCE=700 _GNU_SOURCE>). These macros are NOT inherited from parent targets and their absence causes implicit function declaration errors on GCC forsetenv,usleep,fileno,gethostname,realpath,strdup,strndup,popen,pclose, etc. - MSVC Compile Options: Size optimization flags
/O1and/GLmust be gated using$<NOT:$<CONFIG:Debug>>generator expressions inCMakeLists.txtto avoid conflict with MSVC's debug run-time check flags (/RTC1). - Reference-Counted Resource Lifecycle: When a function acquires a reference-counted resource (via
str_retain, evaluation functions that return retained strings/maps, or any API documented as "caller must release"), the caller MUST release it on ALL code paths — success, error, and early return. The canonical pattern is: (1) Acquire the resource (call returns with refcount incremented), (2) Validate / use the resource, (3) Release the resource (even if the value was "consumed" by passing it to another API), (4) Return. Areturnstatement between steps 1 and 3 without a preceding release is ALWAYS a bug. When a function acquires multiple reference-counted resources, an error after acquiring the second must release BOTH before returning. This applies even on success paths — passing a string to a "fire-and-forget" API (like setting a window title) does not transfer ownership unless the API is explicitly documented as taking ownership. - Tagged Union Type Validation: When a function receives a value from a dynamically-typed evaluation (any API returning a tagged union with a type discriminator), the caller MUST validate the type tag before accessing type-specific union members. Accessing
.as.stringon a value whose.typeisVAL_NUMBER(or vice versa) is undefined behavior. The canonical guard pattern is: (1) Evaluate / receive the tagged value, (2) Check for evaluation error, (3) If wrong type: release any reference-counted resources held by the value, set appropriate error code, return, (4) Access the type-specific union member, (5) Release reference-counted resources, (6) Return. - Monolith Split Protocol: When a
.cfile exceeds the ~1024-line limit and must be split, follow this protocol: (1) Create a shared*_internal.hheader declaring all internal-only functions and types that the sub-files need to share. This header is NOT part of the module's public API. (2) Extract logically cohesive groups of functions into separate.cfiles, each with a single clear responsibility. Each sub-file includes the*_internal.hheader. (3) The original.cfile retains the primary entry-point logic and is reduced to the core responsibility (e.g., parsing, dispatch, main loop). (4) Update ALLCMakeLists.txttargets that compiled the original file to include the new sub-files. (5) Verify dual-platform compilation after the split. - Collection/Stack Ownership Transfer Discipline: When pushing a reference-counted or heap-allocated resource onto a stack, queue, or collection, the ownership semantics MUST be explicitly defined and consistently followed. In a transfer model (collection takes ownership), the caller relinquishes its reference after push; the collection is responsible for releasing; on pop, ownership transfers back to the caller who must release. In a shared model (collection retains/increments), the push operation must call
retain()or equivalent; the pop operation must callrelease()on removal; the caller retains its own reference independently. In EITHER model, a pop operation must NEVER leave a dangling pointer in the vacated slot — zero/NULL the slot immediately after extracting the value. Comparing a pointer against a formerly-occupied slot's value after pop is undefined behavior. - Setter/Overwrite Release Obligation: Any function that overwrites a pointer field holding a reference-counted or heap-allocated resource MUST release the OLD value before writing the new one. The canonical pattern is: (1) Check if the field currently holds a non-NULL value, (2) Release/free the old value, (3) Write the new value (retaining if needed), (4) Return. Directly overwriting a non-NULL field with a new pointer without releasing the old value is ALWAYS a memory leak. This applies to struct field assignment, global variable updates, and hash table entry replacement.
- Error Propagation Obligation: When a helper function or runtime library function encounters an error, it MUST propagate the error to the caller or the VM error state via the established error reporting mechanism (
vm_set_error(), return code, or error output parameter). Writing an error to a local variable and then discarding it on return is a silent failure bug. If a function has no way to report errors (void return, no error parameter), it must either: (a) be refactored to accept an error output parameter, or (b) use the VM context's error state directly. Silent error swallowing is forbidden. - Intermediate/Temporary Value Lifecycle: When a function creates a temporary or intermediate reference-counted value during multi-step processing (e.g., converting a number to a string for serialization, building a format string, parsing then transforming data), the intermediate value MUST be released after the step that consumes it — even if the consuming step copies the data. The pattern is: (1) Create intermediate value, (2) Pass/copy it to the consumer, (3) Release the intermediate immediately, (4) Continue. Intermediate values must NEVER be left "dangling" with an incremented refcount after the step that needed them completes. This is distinct from the final return value lifecycle.
- Array Shift-Left Cleanup: When deleting an element from a contiguous array by shifting subsequent elements left (e.g.,
for (i = idx; i < count - 1; i++) arr[i] = arr[i+1]; count--;), the vacated trailing slot(s) at indices[count..count+N-1]MUST be immediately zeroed withmemset(&arr[count], 0, N * sizeof(element_type)). Without this, the trailing slots retain shallow copies of shifted structs, leaving duplicate pointers that cause double-free or use-after-free on subsequent buffer expansion, reallocation, or cleanup iteration. This applies to any array of structs containing pointers, file handles, or other owned resources. - Process Exit Cleanup Obligation: Before
returnfrommain()or callingexit(), all heap-allocated buffers, open file handles (FILE*), and temporary files (tmpfile()) MUST be explicitly released. Do not rely on the OS to reclaim resources at process termination. Specifically: (1) iterate over all owned collections andfree()each element's owned sub-allocations, (2)free()the collection itself, (3)fclose()all openFILE*handles, (4) NULL all released pointers. This ensures clean Valgrind/ASAN reports, prevents Windows file-lock issues withtmpfile(), and enables safe future refactoring from standalone processes to embedded/library usage. - Fixed-Size Accumulation Buffer Guard: When writing data into a fixed-size buffer using index-based accumulation (e.g.,
buf[pos++] = datain a loop), the write index MUST be bounds-checked against the buffer's capacity BEFORE every write. The canonical pattern is:if (pos >= buf_size - 1) break;beforebuf[pos++] = .... This applies to clipboard assembly, serialization, text extraction, and any loop that copies variable-length content into a pre-allocated buffer. Prefer dynamically-growing buffers (viarealloc) when the output size is unpredictable; when fixed buffers are used, document the size rationale and ensure the guard cannot be bypassed.
- Dual-Platform Build Verification: After any code or build system change, compilation MUST be verified on BOTH Windows (MSVC) and Linux (GCC) before declaring a fix complete. A change that compiles on one platform but not the other is NOT considered fixed. When possible, run both builds concurrently to minimize turnaround time.
- Clean Multi-Platform Compilation: All targets must compile cleanly with no warnings and no errors under both Windows (MSVC) and Linux (GCC/Clang). Minimum compiler flags:
- MSVC:
/W4 /WX /std:c17 /D_CRT_SECURE_NO_WARNINGS - GCC:
-Wall -Wextra -Wpedantic -Werror -Wshadow -Wconversion -std=c17These flags treat all warnings as errors and enable maximum static analysis. Code that only compiles with lower warning levels is NOT considered clean.
- MSVC:
- Progressive Tests: Keep tests under the
tests/directory as cumulative, progressive regression suites. They must not be treated as disposable. - Bug Hunts: Always perform an exhaustive bug hunt at the end of every task before control is returned back to the user. Only when we are 100% free of bugs then can any walkthrough be displayed.
- C & Header File Documentation Headers: Every
.cand.hfile MUST begin with a standardized header block addressing the following:- What it does: High-level summary of the file's purpose and functionality.
- Why it exists: The architectural problem it solves or context in the project.
- Why it works this way: Explanation of design choices (e.g., iterative vs recursive, memory layouts).
- What can be changed: Safe zones for editing, tuning parameters, or adding features.
- What cannot be changed: Critical invariants, strict boundaries, or performance-sensitive loops.
- What to expect: Expected side-effects, behaviors on error, resource usage, or performance characteristics.
- What to do if something breaks: Debugging steps, diagnostic checks, or assertions that might fire.
- Assumptions: What preconditions must be met (e.g., initialized contexts, alignment).
- Portability concerns: 16-bit vs 32/64-bit limits, endianness, OS dependencies.
- Future expansions: How to add new features or support new systems safely.
- Clean Root Policy: The project root must remain clean and presentable at all times. Never leave temp files, debug output, test data artifacts, or runtime-generated files in the project root after a work session.
- Build Directory Discipline: Only sanctioned build directories (e.g.,
build_win/,build_linux/) are allowed. Never create additional build directories without deleting them after use. .gitignoreSynchronization: Whenever a new build target, binary name, or build directory pattern is introduced, the.gitignoreMUST be updated in the same commit.- Runtime-generated files (
.cfg,.dat,.log,.err) must NEVER be committed. Add patterns to.gitignoreproactively.