This document describes the C++ coding conventions, idioms, and practices used throughout the Organic Assembler codebase.
The project uses C++20 (CMAKE_CXX_STANDARD 20). Features actively used:
std::variant<>for algebraic data typesstd::shared_ptr<>/std::weak_ptr<>/std::unique_ptr<>for ownershipstd::enable_shared_from_thisfor safe self-references- Range-based for loops everywhere
- Lambda expressions with captures for callbacks and inline logic
constexprfor compile-time computation- Default member initializers
autofor type deduction (used judiciously)- Fold expressions:
((id == ids) || ...)for variadic checks - Scoped enums (
enum class) exclusively
Features intentionally not used:
- RTTI /
dynamic_caston raw pointers (only onshared_ptrcasts) - Exceptions as control flow (used only for invariant violations)
- Concepts / constraints
- Modules (C++20)
- Coroutines
std::optional<>(error strings or variants preferred)- Structured bindings (rarely used)
// PascalCase for all type names
struct FlowGraph;
struct GraphBuilder;
struct TypeExpr;
class CodeGenerator;
enum class NodeTypeID;
enum class TypeKind;// snake_case for all functions and methods
void add_node();
void mark_dirty();
void rebuild_pin_ids();
TypePtr resolve_type();
bool is_numeric(const TypePtr& t);// snake_case for locals and parameters
int result;
FlowNode* source_node;
int temp_counter;
// snake_case with trailing underscore for private members
ArgKind kind_;
std::shared_ptr<GraphBuilder> owner_;
FlowNodeBuilderPtr node_;
bool dirty_ = false;
// snake_case without underscore for public members
std::vector<FlowNode> nodes;
std::vector<FlowLink> links;
std::map<NodeId, BuilderEntryPtr> entries;// PascalCase for enum values
enum class TypeKind { Void, Bool, String, Scalar, Named, Container, ... };
enum class NodeTypeID : uint8_t { Expr, New, Dup, Str, Select, ... };
enum class ArgKind : uint8_t { Net, Number, String, Expr };
enum class IdCategory { Node, Net };// PascalCase with descriptive suffixes
using TypePtr = std::shared_ptr<TypeExpr>;
using ExprPtr = std::shared_ptr<ExprNode>;
using FlowArg2Ptr = std::shared_ptr<FlowArg2>;
using FlowNodeBuilderPtr = std::shared_ptr<FlowNodeBuilder>;
using NetBuilderPtr = std::shared_ptr<NetBuilder>;
using BuilderEntryWeak = std::weak_ptr<BuilderEntry>;
using NodeId = std::string;
using BuilderError = std::string;
using Remaps = std::vector<std::pair<std::string, std::string>>;// Static arrays for descriptors
static const NodeType NODE_TYPES[] = { ... };
static constexpr int NUM_NODE_TYPES = sizeof(...) / sizeof(...);
// Inline helpers in headers
inline bool is_numeric(const TypePtr& t);
inline TypeCategory parse_category(char c);Always #pragma once. No #ifndef guards are used anywhere in the codebase.
#pragma once
#include <string>
#include <vector>
// ...- Standard library headers
- Project headers
#pragma once
#include "model.h"
#include "types.h"
#include "node_types.h"
#include "graph_editor_interfaces.h"
#include <string>
#include <vector>
#include <map>
#include <memory>
#include <variant>
#include <iostream>
#include <algorithm>
#include <stdexcept>
#include <functional>
#include <set>Note: project headers often come first, then standard headers. This is the established convention — not the Google style of standard-first.
The codebase uses smart pointers consistently. Raw pointers are only for non-owning references within the same scope.
// Shared ownership for graph entries
std::shared_ptr<FlowNodeBuilder> node;
std::shared_ptr<NetBuilder> net;
std::shared_ptr<TypeExpr> type;
// Unique ownership for pins
std::unique_ptr<FlowPin> pin;
// Weak references for cycle breaking
std::weak_ptr<IGraphEditor> editor;
std::vector<std::weak_ptr<IArgNetEditor>> net_editors;
// Raw pointers only for non-owning, same-scope references
const PortDesc2* port = nullptr; // borrowed from node type descriptor
FlowNode* node_ptr; // temporary within a functionFlowGraph and similar containers are movable but non-copyable:
FlowGraph(FlowGraph&&) = default;
FlowGraph& operator=(FlowGraph&&) = default;
FlowGraph(const FlowGraph&) = delete;
FlowGraph& operator=(const FlowGraph&) = delete;Used on objects that need to hand out shared pointers to themselves:
struct FlowArg2 : std::enable_shared_from_this<FlowArg2> {
// Can safely call shared_from_this() in methods
};
struct BuilderEntry : std::enable_shared_from_this<BuilderEntry> {
// Entries can refer to themselves in observer callbacks
};Functions that can fail return a variant of success/error:
using BuilderResult = std::variant<std::pair<NodeId, FlowNodeBuilder>, BuilderError>;
using ParseAttoResult = std::variant<std::shared_ptr<GraphBuilder>, BuilderError>;
using ParseResult = std::variant<std::shared_ptr<ParsedArgs2>, std::string>;
using SplitResult = std::variant<std::vector<std::string>, std::string>;Usage:
auto result = parse_atto(stream);
if (auto* err = std::get_if<BuilderError>(&result)) {
// handle error
} else {
auto& gb = std::get<std::shared_ptr<GraphBuilder>>(result);
// use graph builder
}Objects that can be in an error state carry an error string:
struct FlowNode {
std::string error; // non-empty if node has a type error
};
struct FlowLink {
std::string error; // non-empty if connection is invalid
};Exceptions are reserved for programmer errors, never for expected failures:
if (!entry_) throw std::logic_error("ArgNet2: entry must not be null");
if (!owner_) throw std::logic_error("FlowArg2: owner must not be null");These should never fire in correct code — they guard against impossible states.
Functions validate preconditions and return early:
bool validate_type(const std::string& type_str, std::string& error) {
if (type_str.empty()) {
error = "empty type string";
return false;
}
// ...
return true;
}Always enum class (scoped), never plain enum:
enum class TypeKind {
Void, Bool, String, Mutex, Scalar, Named, Container,
ContainerIterator, Array, Tensor, Function, Struct,
Symbol, UndefinedSymbol, MetaType
};
enum class TypeCategory { Data, Reference, Iterator, Lambda, Enum, Bang, Event };
enum class ScalarType { U8, S8, U16, S16, U32, S32, U64, S64, F32, F64 };
enum class ContainerKind { Map, OrderedMap, Set, OrderedSet, List, Queue, Vector };
enum class ArgKind : uint8_t { Net, Number, String, Expr };
enum class NodeTypeID : uint8_t { /* 38 values */ };Underlying types (: uint8_t) specified when the enum is stored in compact structures.
Two approaches are used, depending on context:
using FlowArg = std::variant<ArgPortRef, ArgLambdaRef, ArgVariable, ...>;
using HoverItem = std::variant<std::monostate, BuilderEntryPtr, FlowArg2Ptr, AddPinHover>;struct FlowArg2 {
ArgKind kind() const { return kind_; }
bool is(ArgKind k) const { return kind_ == k; }
std::shared_ptr<ArgNet2> as_net();
std::shared_ptr<ArgNumber2> as_number();
std::shared_ptr<ArgString2> as_string();
std::shared_ptr<ArgExpr2> as_expr();
};The kind-based approach is preferred when the types share substantial base behavior
and need shared_from_this().
std::stringfor all text — nochar*arrays- Pass by
const std::string&for inputs std::move()for ownership transfers- String IDs for nodes and pins:
"guid.pin_name"format
// ID generation
inline std::string generate_guid() {
static std::mt19937_64 rng(std::random_device{}());
// hex from RNG
return s;
}
// Tokenization with nesting awareness
std::vector<std::string> tokenize_args(const std::string& args);
std::vector<std::string> split_args(const std::string& args);Type utility functions are inline in headers for zero-overhead abstraction:
inline bool is_numeric(const TypePtr& t) { ... }
inline bool is_integer(const TypePtr& t) { ... }
inline bool is_float(const TypePtr& t) { ... }
inline TypePtr decay_symbol(const TypePtr& t) { ... }
inline TypePtr strip_literal(const TypePtr& t) { ... }
inline bool is_category_sigil(char c) { ... }Implementation-only helpers are static in the .cpp file, never in headers:
// In serial.cpp
static std::string trim(std::string s);
static std::string unescape_toml(const std::string& s);
static std::string unquote(const std::string& s);
static std::vector<std::string> parse_toml_array(const std::string& val);Each .h / .cpp pair covers a single concept:
| Pair | Concept |
|---|---|
types.h/cpp |
Type system and parsing |
expr.h/cpp |
Expression AST and parsing |
inference.h/cpp |
Type inference algorithm |
graph_builder.h/cpp |
Structured graph editing |
serial.h/cpp |
.atto serialization |
codegen.h/cpp |
C++ code generation |
shadow.h/cpp |
Shadow expression nodes |
symbol_table.h/cpp |
Symbol resolution |
type_utils.h/cpp |
Type compatibility utils |
model.h |
Core data structures |
Headers contain:
#pragma once- Includes (project, then standard)
- Forward declarations
- Type aliases
- Struct/class definitions
- Inline function implementations
Implementation files contain:
- Header include
- Additional includes
- Static helper functions
- Method implementations
Variadic templates with fold expressions for type checks:
template<typename... Ts>
constexpr bool is_any_of(NodeTypeID id, Ts... ids) {
return ((id == ids) || ...);
}Macro usage is minimal and confined:
-
Test framework (only in test_inference.cpp):
#define TEST(name) static void test_##name() #define ASSERT(cond) if (!(cond)) { printf(...); tests_failed++; return; } #define ASSERT_EQ(a, b) #define ASSERT_TYPE(pin_ptr, expected_str)
-
No preprocessor configuration — all configuration is runtime state
struct ArgNet2 : FlowArg2 {
friend struct GraphBuilder; // only GraphBuilder can create
private:
ArgNet2(const std::shared_ptr<GraphBuilder>& owner);
};
// Creation via factory
auto arg = gb->build_arg_net(...);struct TypeExpr {
TypeKind kind = TypeKind::Void;
TypeCategory category = TypeCategory::Data;
bool is_generic = false;
bool is_unvalued_literal = false;
ScalarType scalar = ScalarType::U8;
ContainerKind container = ContainerKind::Vector;
const PortDesc2* port_ = nullptr;
bool dirty_ = false;
};std::vector<>— primary sequential containerstd::map<>— ordered associative (for deterministic iteration)std::set<>— ordered unique collection (for selections)std::unordered_map<>— rare, only for performance-critical lookupsstd::erase_if()— for filtered removal from containers
- Input parameters:
const std::string&,const TypePtr& - Accessor methods:
constqualified - Mutable state: clearly non-const
ArgKind kind() const { return kind_; }
const PortDesc2* port() const { return port_; }
const FlowNodeBuilderPtr& node() const;
void node(const FlowNodeBuilderPtr& n); // setter is non-const