PROJECT_RULES
This document defines the mandatory coding rules for the project. It replaces undocumented or inconsistent conventions with explicit standards. The goal is to keep the codebase deterministic, readable, easy to review, and consistent across modules, translators, optimizers, executors, and infrastructure code.
1. Core Principles
- Prefer consistency over local preference.
- Follow existing project architecture and extension points instead of adding ad hoc logic.
- Keep modules focused: one module, one responsibility.
- All new public behavior must be understandable from names, signatures, and short English documentation.
- When a rule conflicts with old code, new code must follow this document.
- During refactoring, align touched code with these rules.
- Preserve the project's existing universality, layering, and composition principles instead of narrowing them for local convenience.
- Prefer designs where loss of universality requires an explicit architectural change rather than a small special-case patch.
- Convenience layers must remain optional and must not become hidden framework-level sources of truth.
- New behavior-selection strings must have one clear owner: descriptor, provider, manifest, dialect definition, CLI contract, or shared constant at the owning boundary.
- Do not hardcode product, profile, module, backend, rule, intrinsic, or function names in generic framework layers.
- A new architectural rule should include a practical fitness check: test, analyzer, script, CI smoke check, or explicit review checklist entry.
2. Language and Comments
2.1 English-only rule
All comments, XML documentation, exception messages, debug messages, and user-facing technical text must be written in English.
This includes:
//comments/* ... */comments- XML comments (
<summary>,<param>,<returns>, etc.) - assertion messages
- thrown error messages
- README-style inline notes inside code files
2.2 Forbidden
Do not write comments in Russian or mix English with Russian inside code files.
2.3 Comment quality
Comments must explain one of the following:
- intent
- invariant
- non-obvious limitation
- architectural reason
- correctness constraint
Do not write comments that only restate syntax.
2.4 Preferred documentation style
Use XML documentation for public APIs and short // comments only where local clarification is really needed.
/// <summary>
/// Builds a prepared execution pipeline for the provided compilation input.
/// </summary>
public PreparedExecution Build(CompilationInput input)
{
...
}3. Naming Conventions
The current codebase already strongly uses PascalCase for types and members such as BasicCoreImpl, PreparedExecutionBuilder, CompilationInput, ExecutionEnvironment, TryVisit, and NormalizeRuntimeInput, and underscore-prefixed private fields such as _prepared, _inputNormalizer, _intrinsicCompiler, _assemblyCache, and _separator. These conventions must be treated as standard.
3.1 Types
Use PascalCase for:
- classes
- records
- interfaces
- enums
- delegates
- attributes
Examples:
BasicCoreImplPreparedExecutionIExecutionEnvironmentExternalBindingKind
3.2 Methods and properties
Use PascalCase for:
- methods
- properties
- events
- local functions
Examples:
PrepareToRunRunPreparedBuildGetExecutableTryVisit
3.3 Parameters and local variables
Use camelCase for:
- parameters
- local variables
- lambda parameters
Examples:
codeparameterstargetBytecodelabelStacksserviceType
3.4 Private fields
Use _camelCase for private instance and static fields.
Examples:
_prepared_preparedExecutionBuilder_intrinsicTypeRegistry_methodCache_separator
3.5 Interface naming
Interfaces must start with I.
Examples:
ICoreRunnableIAbstractMethodsTranslatorIAirOptimizer
3.6 Attribute naming
Attribute types must end with Attribute.
Example:
AutoRegisterServiceAttribute
3.7 Boolean naming
Boolean variables, fields, and methods should read like conditions.
Preferred:
isValidhasValueneedCastinginitializeWithDefaultTryGetProcessor
3.8 Collection and dictionary naming
Use plural names for collections and maps unless the variable represents one logical lookup object.
Preferred:
modulesoptimizersexternalSlotslabelStacksbindings
3.9 Abbreviations
Avoid cryptic abbreviations in new code.
Allowed when already domain-standard in the project:
IRAIRCILILAST
Do not introduce new unclear abbreviations without strong reason.
3.10 Test entity naming
Test classes must end with Tests.
Preferred:
PreparedExecutionBuilderTestsIntrinsicDescriptorProviderTests
Test methods must follow: MethodOrFeature_Scenario_ExpectedResult
Preferred:
Build_WhenInputIsInvalid_ReturnsDiagnosticsRunPrepared_WhenExecutionIsNotPrepared_ThrowsResolve_WhenAliasIsUnknown_ReturnsFailure
Avoid vague names such as:
Test1CheckShouldWorkBugFix
3.11 Extension type naming and purpose
Classes that define extension methods must end with Extensions.
Use extension methods to extend the base capabilities of an existing entity without pushing unrelated convenience logic into the entity itself.
Preferred:
- add small compositional helpers
- add readable guard helpers
- add domain-aligned convenience methods that naturally belong to the extended type
Forbidden:
- using extension classes as a dumping ground for unrelated helpers
- adding broad orchestration logic through extensions
- hiding important side effects behind innocent-looking extension names
If logic represents a new service, workflow, builder, resolver, validator, translator, optimizer, compiler, or interpreter role, create a dedicated type instead of an extension method.
3.12 Role suffixes must match behavior
Type names must reflect the actual responsibility of the type.
Use role suffixes consistently:
Builder— accumulates configuration and builds an objectFactory— creates instancesProvider— provides an already available value or objectResolver— resolves by key, type, alias, or contextRegistry— stores registrations or mappingsValidator— validates and reports violationsTranslator— translates between representationsVisitor— traverses a structure according to a visitor contractOptimizer— rewrites or improves an intermediate representationCompiler— produces compiled/executable outputInterpreter— executes a representation directly
Do not use vague suffixes such as:
ManagerHelperServicewhen a more precise role name is available.
4. File and Type Layout
4.1 One main type per file
Each file should contain one main public type. Small tightly-coupled helper types may be nested when this improves locality.
4.2 File name = main type name
The file name should match the main type name.
Examples:
PreparedExecutionBuilder.cs→PreparedExecutionBuilderThrower.cs→Thrower
4.3 Namespaces
Use file-scoped namespaces.
namespace BasicCore.Core;4.4 Global usings
Shared and stable imports should be moved to a dedicated GlobalUsings.cs file per project instead of being repeatedly imported at the top of individual files.
Prefer:
- one dedicated global usings file per project
- moving common and stable imports there
Avoid:
- repeating the same stable imports across many files
- scattering pseudo-global imports through unrelated source files
5. Formatting and Braces
The codebase consistently places opening braces on a new line for methods, properties, constructors, control blocks, and switch arms with block bodies, while often keeping single-line guard clauses without braces. This should be formalized as the project style.
5.1 Base brace style
Use Allman style.
public void PrepareToRun(CompilationInput input)
{
_prepared = _preparedExecutionBuilder.Build(input);
}5.2 Type declarations with primary constructors
Primary constructors are allowed when they make the code shorter without reducing readability.
public sealed class PreparedExecution(
string sourceText,
TCompilationOutput compilationOutput,
IExecutor executor,
IExecutionEnvironment executionEnvironment)
{
...
}5.3 Single-line statements
A single statement after if, for, foreach, or while may omit braces only when all of the following are true:
- the body is exactly one short statement
- there is no nested control flow
- readability is not reduced
- future modification risk is low
Preferred:
if (obj == null)
Thrower.ArgumentNull(nameof(obj));Required braces:
if (obj == null)
{
LogFailure();
Thrower.ArgumentNull(nameof(obj));
}5.4 Guard clauses
Prefer early return / early throw over deep nesting.
if (scope.SafeGet(childIndex) == null)
return false;5.5 Line length and wrapping
Break long argument lists and constructor parameter lists vertically. Align continuation for readability, not for clever compactness.
5.6 Expression-bodied members
Allowed only for short trivial members.
Preferred:
public object? GetExternalValue(int slot) => _values[slot];Avoid expression-bodied members when the logic is not trivial.
5.7 Collection expressions
Collection expressions such as [] are allowed when they improve clarity and match target framework support already used in the project.
Async Rules
Async.1 Naming
Methods returning Task, Task<T>, ValueTask, or ValueTask<T> must end with the Async suffix.
Preferred:
BuildAsyncPrepareToRunAsyncLoadModulesAsync
Forbidden:
BuildPrepareToRunLoadModuleswhen the method is asynchronous
Async.2 async void
async void is forbidden except for true event handlers required by an external framework.
Prefer:
TaskTask<T>ValueTaskValueTask<T>
6. Null Handling
The project uses Thrower as the only allowed null-check mechanism. Null argument validation must happen only at public API boundaries. Internal code must rely on validated contracts and preserve invariants instead of repeatedly guarding every hop.
6.1 General rule
Validate null arguments only at public API boundaries. Inside the project, prefer trusted contracts and explicit invariants over repetitive defensive argument checks.
6.2 Mandatory API for null checks
Use only:
Thrower.ArgumentNull(...)obj.ArgNotNull(...)for public API boundary argumentsobj.NotNull(...)Thrower.AssertAlways(...)for internal invariantsThrower.NullException(...)only in rare low-level helper scenarios
6.3 Forbidden
Do not use:
ArgumentNullException.ThrowIfNull(...)- direct
throw new ArgumentNullException(...) - silent fallback to null when null is invalid
- null-forgiving operator
!when.NotNull(...)or proper validation can express the contract directly
6.4 Preferred boundary normalization style
For non-nullable reference arguments in public constructors and public methods, prefer immediate normalization through obj.ArgNotNull(...) over repetitive defensive if (obj == null) Thrower.ArgumentNull(...) blocks.
Preferred:
public Runner(IExecutor executor)
{
_executor = executor.ArgNotNull();
}Avoid when unnecessary:
public Runner(IExecutor executor)
{
if (executor == null)
Thrower.ArgumentNull(nameof(executor));
_executor = executor;
}Do not apply this blindly to:
- intentionally nullable arguments
- trivial forwarding overloads
- private/internal helpers that already operate under validated invariants
- string arguments that require stronger semantic validation than null-only checks
6.5 Boundary validation
Validate null arguments only in:
- public constructors
- public methods
- other explicitly public API entry points
Do not add routine null argument checks to private or internal helpers when the contract is already validated by the public boundary.
6.6 Internal invariants
Use obj.NotNull(...) or Thrower.AssertAlways(...) only when null would indicate a broken internal invariant or internal bug.
Do not use internal invariant helpers as a substitute for routine argument guarding of private/internal methods.
Thrower.AssertAlways(_prepared != null, "Prepared execution must be initialized before RunPrepared.");7. Exception Policy
The codebase already has a centralized Thrower service with InvalidOpEx, AssertAlways, NotImplementedException, ArgumentNull, Argument, FileNotFound, NotSupported, and InvalidCast. This must be the only allowed way to throw project exceptions.
7.1 Mandatory rule
All project exceptions must be issued through Thrower or approved Thrower-based extension helpers.
7.2 Allowed APIs
Use:
Thrower.InvalidOpEx(...)Thrower.AssertAlways(...)Thrower.NotImplementedException(...)Thrower.ArgumentNull(...)Thrower.Argument(...)Thrower.FileNotFound(...)Thrower.NotSupported(...)Thrower.InvalidCast(...)- approved
Throwerextension helpers such asArgNotNull(...)andNotNull(...)
7.3 Forbidden
Do not use the throw keyword directly outside:
Thrower- approved
Throwerextension/helper types dedicated to exception construction and null-guard flow
Do not write:
throw new Exception(...);
throw new InvalidOperationException(...);
throw new ArgumentNullException(...);
throw new NotImplementedException(...);
throw;7.4 Exception messages
Exception messages must:
- be in English
- be short and precise
- describe the violated invariant or invalid input
- avoid noise and implementation trivia
Preferred:
"Unknown intrinsic 'load_xyz'.""Expected return value on stack.""Argument 'provider' cannot be null."
Avoid vague messages like:
"Error""Something went wrong""Invalid state"
8. Public API Documentation
8.1 XML comments
Public types and non-trivial public members should have XML documentation.
8.2 Required for
Required XML docs for:
- public services
- extension points
- public options/configuration types
- infrastructure helpers with non-obvious behavior
8.3 Not required for
XML comments are optional for tiny obvious DTO properties or trivial one-line methods.
8.4 English only
XML documentation must be in English only.
9. Collections, Immutability, and Data Flow
9.1 Prefer read-only abstractions
Public APIs should prefer:
IReadOnlyListIReadOnlyDictionary- immutable snapshots where practical
9.2 Avoid leaking mutable internals
Do not expose mutable collections directly unless the design explicitly requires mutation.
9.3 Input normalization
Normalize user/runtime input once at the boundary, then pass structured models deeper into the system.
This matches the current split around CompilationInput and should remain the standard.
10. Parser and AST Rules
The existing parser rules in the current PROJECT_RULES.md remain valid and are retained here with tighter wording: use SafeGet, mark processed nodes, do not mutate ancestors above the parent scope inside IAstNodeCreator, and process NodeCreators in priority order.
10.1 IAstNodeCreator
Allowed:
- change
NodeType - move existing nodes within the allowed local scope
- create new nodes
- add tags
Forbidden:
- mutate ancestors above the current parent scope
- hide index changes after removal
- rely on unsafe child access
10.2 Child access
Use SafeGet when index may be out of range.
10.3 Processed nodes
When parser logic consumes or transforms a node in parser flow, mark it with MarkAsParserHandled() when required by the parsing contract.
10.4 Visitor order
In AST visitors, process child nodes before the current node unless a specific visitor contract requires otherwise. This is the current dominant project pattern.
11. Type Stack and IR Rules
The current project rules around stack order, type inference, generic resolution, backend-dependent intrinsics, and AIR-level peephole optimizations remain valid.
11.1 Stack order
Arguments are pushed left to right and read from the end of the stack.
11.2 Type inference
Use actual stack types or explicit literal types. Do not invent implicit conversions without a clearly documented rule.
11.3 Generic resolution
Use GenericTypeResolver for generic method calls.
11.4 Intrinsics
Do not add new intrinsics without:
- backend support analysis
- type stack rule registration
- clear naming
- tests for both compiler and interpreter paths if applicable
12. Architectural Universality and Anti-Debt Guardrails
12.1 Preserve universality by design
Framework entities must be designed so that reducing universality, introducing concrete coupling, or turning a reusable abstraction into product-specific logic is difficult and explicit.
Preferred:
- thin optional convenience layers
- declarative definitions over imperative special cases
- data-only descriptors when a type only identifies shipped profiles, presets, or examples
- explicit boundaries between framework-level and product-level code
- architecture tests for important dependency boundaries
Avoid designs where one small patch can silently convert a reusable framework abstraction into a concrete Wist-only or profile-only mechanism.
12.2 Convenience layers must stay optional
Catalogs, registries, profile sources, facades, loaders, and other convenience layers must not become mandatory framework dependencies.
The core system must remain usable through general composition paths without requiring those layers.
12.3 Do not create hidden authorities
Do not let a convenience entity become a hidden authority that decides framework-level semantics, module selection rules, backend selection rules, or dialect composition rules through imperative branching.
If an entity only exposes shipped presets or built-in profiles, keep it small, data-driven, and replaceable.
12.4 Prevent easy universality erosion
When designing entities that sit near composition, runtime selection, profile loading, diagnostics, or binding resolution, prefer structures where harmful narrowing requires an explicit architectural refactor.
Preferred:
- external declarative profile files or manifests
- immutable descriptors with minimal metadata
- shared workflow paths for built-in and user-defined configurations
- dependency boundaries enforced by tests
Avoid:
- rich descriptors that duplicate the real language model
- branching by concrete profile id, dialect id, module name, or backend name in framework-level logic
- built-in catalogs that mutate composition behavior instead of only supplying data
- helpers that are trivially extendable by adding one more concrete
if/switcharm
12.5 Technical debt prevention rule
A design is unacceptable if it is easy to modify in a way that:
- reduces universality,
- increases coupling to concrete dialects/modules/backends,
- bypasses declared extension points,
- or accumulates hidden technical debt through special-case branching.
When such risk exists, restructure the design first so that the harmful modification path becomes harder, more visible, and more testable.
13. Dependency Injection and Reflection
The project already highlights determinism problems around assembly scanning and reflection-based discovery, and explicitly plans to centralize reflection and redesign DI behavior. These concerns should be reflected in the rules.
13.1 DI registration
Prefer explicit registrations for critical infrastructure. Auto-registration is acceptable for modules and clearly marked extension points.
13.2 Reflection
Reflection usage should be centralized in dedicated helpers or registries. Do not scatter ad hoc reflection logic across unrelated modules.
13.3 Determinism
Do not make behavior depend on unstable assembly scan order. Where ordering matters, define it explicitly.
14. Using Directives
14.1 Order
Use the following order:
System...- third-party namespaces
- project namespaces
14.2 Redundant usings
Do not keep redundant local using directives when a stable GlobalUsings.cs already exists. If an import is common for a project and consistently needed across many files, prefer moving it into the dedicated global usings file instead of repeating it locally.
15. Testing Rules
15.1 Every non-trivial rule change needs tests
Add or update tests when changing:
- parsing precedence
- binding behavior
- intrinsic mapping
- optimization passes
- interpreter/compiler semantic parity
- DI composition
- architecture boundaries that protect universality or layering
15.2 Fixes need regression tests
Every bug fix should add a regression test when practical.
15.3 Semantic parity
For language behavior, prefer paired tests that verify both interpreter and compiled execution produce the same observable result.
15.4 Architecture guardrails
When introducing a new convenience layer, catalog, registry, or profile loader near framework composition, add tests when practical that protect dependency direction and shared workflow behavior.
16. Forbidden Practices
Forbidden in new code:
- direct exception throwing instead of
Thrower - comments not in English
- XML docs not in English
- mixing null-check styles
- hidden mutation of parent/ancestor AST structure outside the allowed local scope
- unsafe indexing when
SafeGetis required - new reflection logic outside dedicated infrastructure helpers
- unclear abbreviations in public APIs
- magic strings for protocol-like behavior without shared constants or clear justification
- silent swallowing of exceptions without explicit reason
- mutable (
stateful)staticfields, properties, or collections in new code async voidoutside true event handlers- asynchronous methods without the
Asyncsuffix - direct
throwoutsideThrowerand approvedThrowerextension helpers - routine null argument checks in private/internal helpers when validation belongs to the public API boundary
- vague test names such as
Test1,Check, orShouldWork - convenience entities that become hidden sources of framework truth through imperative special-case logic
- framework-level branching on concrete shipped profile ids, concrete dialect ids, or concrete built-in module sets when a general path already exists
17. Preferred Patterns
17.1 Good
public void RegisterAssembly(Assembly assembly)
{
assembly = assembly.ArgNotNull();
lock (_syncLock)
{
if (!IsValidAssembly(assembly))
return;
_loadedAssemblies.Add(assembly);
CacheAssembly(assembly);
LoadDependencies(assembly);
}
}17.2 Bad
public Runner Build(string profileId)
{
if (profileId == "full-default")
return BuildFullDefault();
if (profileId == "composition-restricted")
return BuildRestrictedSandboxWithExtraRules();
throw new InvalidOperationException();
}18. Migration Notes for Existing Code
This document is stricter than parts of the current codebase. The repository still contains mixed-language comments and mixed null-check styles, so these rules should be treated as the target standard for cleanup rather than a claim that all current files already comply.
Evidence of mixed English/Russian comments and mixed guard approaches is visible in the current code snapshot.
Priority cleanup order:
- Convert all non-English comments and XML docs to English.
- Replace direct framework null guards with
Thrower-based guards. - Replace direct
throw new ...withThrowercalls. - Normalize brace usage in touched files.
- Move repeated imports into
GlobalUsings.cswhere stable. - Reduce convenience-layer logic that hides framework decisions behind concrete special cases.
19. Short Checklist for Contributors
Before merging code, verify:
- names follow project casing conventions
- comments and XML docs are English only
- null checks use
Thrower - exceptions use
Thrower - braces follow Allman style
- public APIs are documented when non-trivial
- parser/visitor/IR rules preserve stack invariants
- new intrinsics have backend and type-processing support
- tests cover non-trivial behavior changes
- no mutable (
stateful)static - async methods end with
Async - no
async voidexcept true event handlers - test classes end with
Tests - test methods follow
MethodOrFeature_Scenario_ExpectedResult - extension classes are used for natural capability extension, not as helper dumping grounds
- shared stable imports are placed in a dedicated
GlobalUsings.csfile instead of being repeated across files - role suffix matches behavior
- direct
throwis not used outsideThrowerand approvedThrowerhelpers - null argument validation is done only at public API boundaries
- the change preserves existing universality and layering instead of narrowing them for a local case
- new convenience entities stay optional, data-driven where reasonable, and do not become hidden framework authorities
20. Source Basis
This document was updated based on the current PROJECT_RULES.md, the Thrower implementation, the existing core pipeline classes, and the repository-wide conventions visible in the uploaded project snapshot.
21. Runtime reflection and activation boundaries
- Reflection is allowed only when centralized, deterministic, and architecturally justified.
- The canonical runtime path must not use broad eager assembly/type discovery as a hidden source of truth.
- Targeted exact activation of selected runtime components/backends from manifests is allowed and expected.
- Do not scatter reflection-based activation across unrelated modules; keep it in runtime integration infrastructure.
- Compatibility/eager discovery helpers must stay optional and must not become framework truth for dialect composition.