C+ Coding Convention

February 22, 2026 · View on GitHub

Lambda adopts a C+ coding convention — a pragmatic subset of C++ that enhances C with selected C++ features while preserving C's simplicity, control, and ABI compatibility.

The guiding philosophy: use C++ where it makes C better, but never surrender control over memory, error flow, or binary layout.

A key practical driver: Lambda uses MIR (Medium Internal Representation) for JIT compilation, and MIR only supports C. All runtime functions callable from JIT-compiled code must have a C-compatible ABI, making extern "C" boundaries and C-safe data layouts a hard requirement — not just a stylistic choice.


1. Use C++ Features to Enhance C

Lambda freely uses C++ features that improve safety, readability, or ergonomics without imposing runtime costs or hidden complexity.

Inline member functions on structs

Structs gain lightweight accessor methods — no vtable, no overhead:

struct Item {
    union { ... };

    inline TypeId type_id() {
        if (this->_type_id) { return this->_type_id; }
        if (this->item) { return *((TypeId*)this->item); }
        return LMD_TYPE_NULL;
    }

    inline double get_double() { ... }
    inline int64_t get_int64() { ... }
};
// lambda/lambda.hpp — Map struct with member functions
struct Map : Container {
    Item get(Item key);
    bool has_field(const char* name);
};

Struct inheritance as structural extension

C++ struct : Base is used for layout-compatible field extension — not for OOP hierarchies:

struct Container { TypeId type_id; uint16_t ref_cnt; ... };
struct List   : Container { Item* items; int count; ... };
struct Map    : Container { ShapeEntry* shape; ... };
struct Element: List      { ... };   // element is a list + attributes
// radiant/view.hpp
struct DomNode    { ... };
struct DomText    : DomNode { ... };
struct DomElement : DomNode { ... };
struct ViewBlock  : ViewSpan { ... };

Templates (sparingly, utility-only)

Templates are used only for type-safe replacements of C macros — never for generic programming:

// radiant/view.hpp
template<typename T, typename U>
inline auto max(T a, U b) -> typename std::common_type<T,U>::type {
    return a > b ? a : b;
}

Other C++ features used

FeatureUsage
autoReturn types in template helpers, range-for loops
constexprCompile-time constants
static_assertCompile-time assertions
initializer_listFluent builder APIs
References (&)Function parameters where pointer would be cumbersome
NamespacesSparingly, mainly for forward-declaring external libs: namespace re2 { class RE2; }
RAII destructorsStack-allocated builders/contexts with automatic cleanup (no new/delete)

2. Maintain C-Compatible ABI

All public APIs and data structures maintain a C-compatible binary interface. This is essential because MIR JIT-compiled code calls into the runtime via C function signatures.

extern "C" on all C-facing APIs

// lambda/lambda-mem.cpp — callable from MIR JIT C code
extern "C" void* heap_calloc(size_t size, TypeId type_id);
extern "C" String* heap_strcpy(char* src, int len);
extern "C" void set_runtime_error_no_trace(...);

.h vs .hpp file convention

ExtensionSemantics
.hPure C header — safe for MIR JIT compilation and C callers
.hppC++ header — may use member functions, inheritance, templates

The .h headers use the standard guard:

// lib/str.h, lib/arraylist.h, lib/hashmap.h, lib/mempool.h, lambda/lambda.h, etc.
#ifdef __cplusplus
extern "C" {
#endif

// ... C declarations ...

#ifdef __cplusplus
}
#endif

Dual struct definitions for C/C++ interop

lambda.h provides C-only struct layouts, lambda.hpp provides C++ struct layouts with member functions — both share the same binary layout:

// lambda/lambda.h — C view
#ifndef __cplusplus
typedef uint64_t Item;      // plain 64-bit value
struct List { TypeId type_id; ...; Item* items; int count; };
#endif
// lambda/lambda.hpp — C++ view (same binary layout, plus methods)
struct Item {
    union { ...; uint64_t _type_id:8; ... };
    inline TypeId type_id() { ... }
    inline double get_double() { ... }
};

3. Custom Memory Management — No new/delete

Lambda uses its own allocators for total control over memory usage, allocation patterns, and lifecycle. C++ new and delete are never used.

Three-tier allocation

TierAllocatorPurposeSpeed
Poolpool_alloc(), pool_calloc()Runtime heap objects (containers, strings)Fast (rpmalloc-backed)
Arenaarena_alloc()Input parsing data (bump-pointer, zero per-allocation overhead)O(1)
NamePoolname_pool_create_len()Deduplicated structural identifiers (field names, symbols)Amortized O(1)
// pool allocation — lambda/lambda-mem.cpp
context->heap = (Heap*)calloc(1, sizeof(Heap));   // top-level only
context->heap->pool = pool_create();
void* data = pool_alloc(heap->pool, size);         // all runtime objects

// arena allocation — lambda/mark_builder.hpp
// "Arena allocation is FAST (bump-pointer, O(1)) with zero per-allocation
//  overhead. All arena data lives until the arena is reset/destroyed."
List*    list_arena(Arena* arena);
Map*     map_arena(Arena* arena);
Element* elmt_arena(Arena* arena);

Reference counting (not GC, not RAII smart pointers)

Ref counts are embedded directly in data structs as bitfields:

// lambda/lambda.h
struct String    { uint32_t len:22; uint32_t ref_cnt:10; char chars[]; };
struct Container { TypeId type_id; ...; uint16_t ref_cnt; };

Manual increment/decrement with recursive free on zero:

// lambda/lambda-mem.cpp
void free_map_item(Map* map) {
    // decrement ref_cnt, recursively free children when zero
}

What to use instead of C++ allocation

Don'tDo
new MyStruct()pool_calloc(pool, sizeof(MyStruct)) or stack-allocate
delete objpool_free(pool, obj) or let arena/pool lifetime manage it
std::make_shared<T>()Embed ref_cnt in the struct, manage manually
std::unique_ptr<T>Stack-allocate with RAII destructor, or arena-allocate

4. No C++ Exception Handling

Errors are handled as return values, not exceptions. There is no try/catch/throw anywhere in the codebase. Error propagation requires explicit checking and unwinding through the call stack.

Error sentinel values

// lambda/lambda.hpp
extern Item ItemNull;    // null/absent value
extern Item ItemError;   // error sentinel

// Functions return ItemError on failure
Item fn_error(EvalContext* ctx, ...) {
    set_runtime_error(ctx, ...);
    return ItemError;
}

GUARD_ERROR macros for propagation

Instead of exceptions bubbling up automatically, errors are checked and propagated explicitly:

// lambda/lambda.hpp
#define GUARD_ERROR1(a) \
    if (get_type_id(a) == LMD_TYPE_ERROR) return (a)

#define GUARD_ERROR2(a, b) \
    if (get_type_id(a) == LMD_TYPE_ERROR) return (a); \
    if (get_type_id(b) == LMD_TYPE_ERROR) return (b)

// Usage — lambda/lambda-eval.cpp
Item fn_join(EvalContext* ctx, Item left, Item right) {
    GUARD_ERROR2(left, right);
    // ... proceed with operation
}

Three-state Bool (not C++ bool)

// lambda/lambda.h
typedef enum { BOOL_FALSE=0, BOOL_TRUE=1, BOOL_ERROR=2 } BoolEnum;
typedef uint8_t Bool;

// lambda/lambda-eval.cpp
Bool is_truthy(Item item);   // can return BOOL_FALSE, BOOL_TRUE, or BOOL_ERROR

Structured error codes

// lambda/lambda-error.h — HTTP-inspired error code ranges
enum LambdaErrorCode {
    // 1xx — Syntax errors
    ERR_SYNTAX_BASE = 100,
    // 2xx — Semantic errors
    ERR_SEMANTIC_BASE = 200,
    // 3xx — Runtime errors
    ERR_RUNTIME_ERROR = 300,
    ERR_NULL_REFERENCE,
    ERR_INDEX_OUT_OF_BOUNDS,
    // 4xx — I/O errors
    ERR_IO_BASE = 400,
    // 5xx — Internal errors
    ERR_INTERNAL_BASE = 500,
};

struct LambdaError {
    LambdaErrorCode code;
    const char* message;
    SourceLocation location;
    StackFrame* stack_trace;
    const char* help;
    LambdaError* cause;           // chained errors
};

Error handling patterns

Don'tDo
throw std::runtime_error(msg)return fn_error(ctx, ERR_RUNTIME_ERROR, msg)
try { ... } catch (...)GUARD_ERROR1(result); // propagate error up
throw in constructorReturn NULL or set error on context
Exception-based stack unwindingWalk the call stack via FP chain for trace

5. No Virtual Member Functions (No vtable)

C++ virtual functions inject a hidden vtable pointer into every object and require heap allocation. Lambda avoids this entirely.

TypeId-based dispatch

Every container begins with a TypeId field at offset 0. Runtime dispatch is an explicit switch:

// lambda/lambda.h
enum EnumTypeId {
    LMD_TYPE_NULL, LMD_TYPE_BOOL, LMD_TYPE_INT, LMD_TYPE_FLOAT,
    LMD_TYPE_STRING, LMD_TYPE_LIST, LMD_TYPE_MAP, LMD_TYPE_ELEMENT,
    // ... 30+ types
};

struct Container { TypeId type_id; ... };   // first field = type tag
// lambda/lambda.h
static inline const char* get_type_name(TypeId type_id) {
    switch (type_id) {
        case LMD_TYPE_NULL:    return "null";
        case LMD_TYPE_BOOL:    return "bool";
        case LMD_TYPE_STRING:  return "string";
        // ... all 30+ types
    }
}

Tagged union for value representation

The core Item type is a 64-bit tagged union — type tag + value in a single word:

// lambda/lambda.hpp
struct Item {
    union {
        void*      item;
        int64_t    int_val:56;
        uint64_t   _type_id:8;
        Container* container;
        List*      list;
        Map*       map;
        Element*   element;
        // ...
    };
};

Manual function-pointer vtable (rare, explicit)

The only vtable-like structure in the entire codebase is VMapVtable — an explicit, manually-managed function pointer table for polymorphic map backends:

// lambda/lambda.hpp
struct VMapVtable {
    Item       (*get)(void* data, Item key);
    void       (*set)(void* data, Item key, Item value);
    int64_t    (*count)(void* data);
    ArrayList* (*keys)(void* data);
    // ...
};

This is C-style polymorphism: visible, controllable, and without hidden costs.


6. Custom Lib Types Instead of C++ STL

Lambda provides its own collection and string types under lib/. These are C-compatible, use custom allocators, and have no hidden allocations.

Custom TypeReplacesHeaderNotes
str_*() functions<cstring>lib/str.hLength-bounded, NULL-tolerant, C99
StrBufstd::string (mutable)lib/strbuf.hGrowable buffer with pool allocator
StrViewstd::string_viewlib/strview.hNon-owning (ptr, len) pair
Stringstd::string (immutable)lambda/lambda.hRef-counted, length-prefixed, flexible array member
ArrayListstd::vector<void*>lib/arraylist.hvoid** data, auto-resizing
HashMapstd::unordered_maplib/hashmap.hRobin Hood hashing, custom allocator slots
Poolstd::pmr::memory_resourcelib/mempool.hOpaque pool via rpmalloc
Arenalib/arena.hBump-pointer allocator

C++ STL usage (permitted where conforming)

C++ STL can be used when it conforms to all the rules above:

  • Does not trigger new/delete behind the scenes in hot paths
  • Does not introduce exception-based control flow
  • Does not require virtual dispatch
  • Does not break C ABI for exposed interfaces

In practice, this means utility headers like <type_traits>, <initializer_list>, <cstdint>, <cstring>, and <utility> are fine. Standard containers (std::vector, std::string, std::map) are not used.


7. Naming Conventions

ElementConventionExamples
Functionssnake_casepool_alloc(), heap_strcpy(), is_truthy(), fn_join()
Types / Structs / ClassesPascalCaseEvalContext, MarkBuilder, ViewElement, LambdaError
Constants / Enums / MacrosUPPER_SNAKE_CASELMD_TYPE_NULL, ERR_RUNTIME_ERROR, GUARD_ERROR1, STR_NPOS
Member variables (private)snake_case_ (trailing underscore)input_, pool_, arena_, name_pool_
Member functions (C++ classes)camelCasecreateName(), createElement(), createString()
File namessnake_caselambda_eval.cpp, mark_builder.hpp, shape_pool.cpp
Inline commentsStart lowercase// process the next token

Summary: What C++ Features Are Used vs. Avoided

UsedAvoided
Inline member functions on structsVirtual member functions / vtable
Struct inheritance (layout extension)Deep OOP class hierarchies
Templates (utility only)Generic / template metaprogramming
auto, constexprnew / delete
RAII destructors (stack-allocated)try / catch / throw
References (&)RTTI (dynamic_cast, typeid)
initializer_listSTL containers (std::vector, std::string, std::map)
Namespaces (sparingly)Operator overloading (sparingly)
C++ static_castC++ streams (iostream, cout)

Prior Art

Lambda's C+ convention is not unique — it draws from a well-established tradition of projects that use C++ as "a better C" rather than embracing the full language.

Project / GuideAuthor / OrgKey OverlapLink
Orthodox C++Branimir Karadžić (bgfx)Near-identical: no exceptions, no RTTI, no STL, no virtuals, custom allocators. The closest published manifesto to Lambda's approach.gist.github.com/bkaradzic/2e39896bc7d8c34e042b
Handmade HeroCasey Muratori"C-style C++" — structs with methods, no exceptions, no STL, manual memory management, data-oriented design.handmadehero.org
id Software (Doom, Quake)John CarmackC++ as "better C", custom allocators, no exceptions, minimal STL. Carmack's coding style influenced a generation of game engines.fabiensanglard.net/doom3
EASTLElectronic ArtsEA replaced the STL entirely because standard allocator model lacked control — the same motivation behind Lambda's ArrayList, HashMap, etc.github.com/electronicarts/EASTL
LLVM / ClangLLVM ProjectNo exceptions, no RTTI, custom bump-pointer allocators (BumpPtrAllocator), custom ADT containers. Uses more templates than Lambda.llvm.org/docs/CodingStandards.html
Google C++ Style GuideGoogleBans exceptions, restricts some features, though still uses STL containers.google.github.io/styleguide/cppguide.html
Linux kernelLinus TorvaldsPure C, but uses the same struct-embedding pattern for "inheritance" (container_of macro) and TypeId-like dispatch.kernel.org/doc/html/latest/process/coding-style.html
SQLiteD. Richard HippPure C, clean ABI, custom allocators (sqlite3_malloc), error codes instead of exceptions. A model for C-based runtime design.sqlite.org/codeofethics.html