Layering Rules¶
This repository follows a strict dependency direction to keep semantics shared and prevent accidental drift between the compiler and the runtime:
incan(compiler) may depend onincan_core.incanmay depend onincan_syntax,incan_semantics_core,incan_semantics_stdlib, and optionalrust_inspectfor compiler/toolchain work.incanmust not depend onincan_stdlibexcept as a dev-dependency for parity tests.incan_stdlibdepends onincan_core.- Generated user programs depend on
incan_stdlib.
flowchart TD
incanCompiler["incan (compiler)"] --> incanCore["incan_core"]
incanCompiler --> incanSyntax["incan_syntax"]
incanCompiler --> semanticsCore["incan_semantics_core"]
incanCompiler --> semanticsStdlib["incan_semantics_stdlib"]
incanCompiler -. optional CLI/LSP interop .-> rustInspect["rust_inspect"]
incanSyntax --> incanCore
semanticsStdlib --> semanticsCore
incanStdlib["incan_stdlib"] --> incanCore
generatedProgram["generated program"] --> incanStdlib
generatedProgram --> incanDerive["incan_derive"]
generatedProgram --> incanWebMacros["incan_web_macros"]
CI/Test guardrails enforce that incan keeps incan_stdlib out of its normal dependencies. If you need runtime helpers inside tests, add them under [dev-dependencies] only.
Workspace crate categories¶
Use this policy when deciding where new code belongs:
- Stable contracts:
incan_core,incan_syntax,incan_semantics_core, andincan_vocab. Other layers build on these crates. Keep them deterministic, dependency-light, and free of runtime side effects. - Compiler/toolchain implementation:
incan,incan_semantics_stdlib, andrust_inspect. These crates are tied to the current compiler/tooling. They may depend on stable contracts but should not become runtime APIs. - Runtime-only implementation:
incan_stdlib,incan_derive, andincan_web_macros. Generated Rust programs use these crates. The compiler may generate references to them but must not depend on them in normal builds. - Transitional runtime surfaces: current
incan_stdlib::weband related macro glue. This runtime code is not yet a stable long-term contract. Keep it quarantined and avoid treating it as compiler-owned policy.
Why we do this¶
We want one “source of truth” for language behavior so the compiler and runtime don’t drift:
- Semantics must match: if const-eval validates something, runtime should do the same thing the same way (especially for Unicode-sensitive string operations and numeric edge cases).
- Diagnostics/panics must stay aligned: user-facing error messages should not diverge between compile-time and runtime.
- Compiler stays lean: the compiler shouldn’t accidentally pull in runtime-only APIs or heavy dependencies.
What goes where (contracts vs implementations)¶
incan_core:
- Pure helpers that define meaning/policy (e.g., string indexing/slicing rules, numeric promotion, canonical error message constants).
- Central registries for language vocabulary and stdlib wiring (for example
incan_core::lang::stdlib::STDLIB_NAMESPACESand keyword metadata used by the lexer/parser). - Must be deterministic and side-effect free.
- Should not depend on compiler internals (AST, spans, lexer/parser state).
- Should not gain new stdlib-owned runtime surface types unless the type metadata is truly shared language policy.
incan_syntax:
- Lexer, parser, AST, and syntax diagnostics shared by compiler, formatter, LSP, and future tooling.
- May use language vocabulary from stable contract crates.
- Must not perform name resolution, typechecking, lowering, Rust interop loading, or runtime behavior.
incan_semantics_core:
- Stable action-descriptor and semantics-pack contracts that compiler stages can consume.
- Owns behavior descriptors, not compiler execution. Packs describe what to do; compiler stages decide how to do it.
incan_semantics_stdlib:
- Stdlib semantics-pack implementation for current built-in library surfaces.
- Toolchain-locked implementation crate, not a stable external API.
- Should return descriptors and canonical targets instead of reaching into compiler internals.
rust_inspect:
- Dedicated Rust metadata preparation, extraction, and caching subsystem.
- Allowed behind compiler/tooling features for Rust interop.
- Should remain explicit and staged: prepare/prewarm metadata at CLI/LSP/project boundaries, then read cached metadata in semantic paths.
incan_stdlib:
- Runtime helpers used by generated Rust code.
- Includes facades that generated code imports (for example
incan_stdlib::r#asyncbacking thestd.asyncnamespace). - Should delegate behavior to
incan_corefor policy/consistency, and implement runtime-only actions (like panicking) using the shared error messages/taxonomy. - May contain transitional implementation modules, but those modules must not become compiler dependencies.
incan_derive / incan_web_macros:
- Runtime-side macro support for generated Rust programs.
- Must not become a backchannel for compiler logic.
- Web macro/runtime glue is transitional until the web surface has a stable long-term ownership model.
incan (compiler):
- Parsing, typing, lowering, codegen, diagnostics.
- May use stable contract crates to implement checks/const-eval and to keep error text aligned.
- Must not use runtime-only crates in normal builds; only
incan_stdlibas a dev-dependency for parity tests.
Allowed / forbidden dependencies¶
Allowed:
incan→incan_core,incan_syntax,incan_semantics_core,incan_semantics_stdlib,incan_vocabas normal compiler/toolchain dependencies.incan→rust_inspectbehind therust_inspect/CLI/LSP interop path.incan_stdlib→incan_coreas a normal dependency.incan→incan_stdlibas a dev-dependency only, for tests.
Forbidden:
incan→incan_stdlibin[dependencies](this breaks layering).incan→incan_deriveorincan_web_macrosin normal dependencies.incan_core,incan_syntax, orincan_semantics_core→ compiler/toolchain/runtime implementation crates.- Runtime crates calling back into compiler crates.
Common pitfalls¶
-
Adding a “quick helper” in
incan_stdliband calling it from the compiler.- Fix: move the policy/logic to
incan_coreand keep only runtime glue (panics, wrappers) inincan_stdlib.
- Fix: move the policy/logic to
-
Adding another stdlib-specific surface type to
incan_corebecause similar metadata already exists there.- Fix: decide whether the type is true language policy or library-owned surface. Prefer library-defined ownership when possible, and document the exception when it must stay core-owned.
-
Emitting direct Rust operations that bypass shared semantics (e.g., slicing Rust
Stringby byte indices).- Fix: emit calls to
incan_stdlibwrappers which themselves delegate toincan_core.
- Fix: emit calls to
-
Duplicating error messages as string literals in multiple places.
- Fix: put canonical text in
incan_coreand reuse it from both compiler and runtime.
- Fix: put canonical text in
-
Loading Rust metadata opportunistically from typechecking or lowering.
- Fix: prewarm through the explicit
rust_inspectpreparation path and keep semantic lookups cache-oriented.
- Fix: prewarm through the explicit
Guardrails (how it is enforced)¶
- Dependency gate:
tests/layering_guard.rsfails ifincan_stdlibappears in the compiler crate’s[dependencies]section of the rootCargo.toml. (Keepingincan_stdlibin[dev-dependencies]for parity tests is allowed.)
How to add shared behavior safely¶
When you notice drift risk (compiler vs runtime):
- Put the policy in
incan_core(pure function + typed error or canonical message). - Add a thin wrapper in
incan_stdlibthat calls semantics and performs runtime-only behavior (panic, allocation, conversions). - Update compiler const-eval / typechecking to use the semantics helper directly (never stdlib).
- Add a parity test in
tests/that compares compiler/semantics/runtime behavior for the edge case.