diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d46f7646..c5e391a23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,7 +1,6 @@ # Contributing to Incan -Thank you for your interest in contributing to the Incan programming language! This document provides -guidelines for contributing to the project. +Thank you for your interest in contributing to the Incan programming language! This document provides guidelines for contributing to the project. ## Start Here (Docs) @@ -33,8 +32,7 @@ guidelines for contributing to the project. ## Project Structure -The compiler is organized into a **frontend** (lex/parse/typecheck), a **backend** (lowering + Rust -emission), plus CLI and tooling. +The compiler is organized into a **frontend** (lex/parse/typecheck), a **backend** (lowering + Rust emission), plus CLI and tooling. For an up-to-date module map, see: @@ -56,10 +54,8 @@ The workspace uses **Cargo workspace package metadata**, so you only bump versio Notes: -- The compiler exposes the version as `incan::version::INCAN_VERSION`, backed by - `env!("CARGO_PKG_VERSION")`, so it updates automatically with the Cargo version. -- Codegen snapshots are version-agnostic (they normalize the codegen header to - `v`), so version bumps should not churn snapshot files. +- The compiler exposes the version as `incan::version::INCAN_VERSION`, backed by `env!("CARGO_PKG_VERSION")`, so it updates automatically with the Cargo version. +- Codegen snapshots are version-agnostic (they normalize the codegen header to `v`), so version bumps should not churn snapshot files. ### Code Generation Overview @@ -88,9 +84,7 @@ Key files: ### Type Conversions System -The `conversions` module (`src/backend/ir/conversions.rs`) provides centralized handling of type -conversions and borrow checking during Rust codegen. This is where we handle the mismatch between -Incan's simple `str` type and Rust's `&str` vs `String` split for example. +The `conversions` module (`src/backend/ir/conversions.rs`) provides centralized handling of type conversions and borrow checking during Rust codegen. This is where we handle the mismatch between Incan's simple `str` type and Rust's `&str` vs `String` split for example. **When to use conversions:** @@ -136,8 +130,7 @@ Example: ### Adding a New Expression Type -See [Extending the Language](docs/contributing/extending_language.md) for the up-to-date end-to-end -checklist (lexer → parser/AST → typechecker → lowering → IR → emission). +See [Extending the Language](docs/contributing/extending_language.md) for the up-to-date end-to-end checklist (lexer → parser/AST → typechecker → lowering → IR → emission). ### Running Snapshot Tests @@ -183,16 +176,11 @@ From `src/lib.rs`: ### CLI Design -The CLI uses clap with derive macros. Commands return `CliResult` -instead of calling `process::exit` directly. This makes commands testable. +The CLI uses clap with derive macros. Commands return `CliResult` instead of calling `process::exit` directly. This makes commands testable. ### Prelude Status -The stdlib surface now compiles through the normal pipeline under `crates/incan_stdlib/stdlib/`. -Source declarations are the primary contract for `std.*` modules, including the prelude-facing -trait definitions. Some behavior is still realized by backend lowering or runtime bridges -(for example derive-backed Rust traits and host-backed stdlib leaves), but the compiler no longer -treats the stdlib as documentation-only stubs. +The stdlib surface now compiles through the normal pipeline under `crates/incan_stdlib/stdlib/`. Source declarations are the primary contract for `std.*` modules, including the prelude-facing trait definitions. Some behavior is still realized by backend lowering or runtime bridges (for example derive-backed Rust traits and host-backed stdlib leaves), but the compiler no longer treats the stdlib as documentation-only stubs. ### Property-Based Testing diff --git a/Cargo.lock b/Cargo.lock index f0f84bde5..7a138f40a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1564,7 +1564,6 @@ dependencies = [ "incan_core", "incan_derive", "inventory", - "regex", "serde", "serde_json", "tokio", @@ -3120,18 +3119,6 @@ dependencies = [ "smallvec", ] -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.14" diff --git a/README.md b/README.md index 4c3423c38..10300bcc2 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Incan Programming Language -Incan is a statically typed language for writing clear, high-level application code that compiles to native Rust. -It aims to feel lightweight and expressive while keeping the things that matter in large codebases explicit: types, errors, and mutability. +Incan is a statically typed language for writing clear, high-level application code that compiles to native Rust. It aims to feel lightweight and expressive while keeping the things that matter in large codebases explicit: types, errors, and mutability. ## Positioning @@ -96,8 +95,7 @@ make docs-serve ## Performance -Incan compiles to Rust and then to a native binary. Runtime performance can be close to Rust for many workloads, -depending on current codegen and library behavior. +Incan compiles to Rust and then to a native binary. Runtime performance can be close to Rust for many workloads, depending on current codegen and library behavior. - Benchmarks: `benchmarks/` - Results: `benchmarks/results/results.md` diff --git a/crates/incan_core/src/bin/generate_lang_reference.rs b/crates/incan_core/src/bin/generate_lang_reference.rs index d02e84fe4..4e6e451e3 100644 --- a/crates/incan_core/src/bin/generate_lang_reference.rs +++ b/crates/incan_core/src/bin/generate_lang_reference.rs @@ -94,8 +94,7 @@ fn write_language_reference(path: &Path) { let mut out = String::new(); out.push_str("# Incan language reference\n\n"); out.push_str("!!! warning \"Generated file\"\n"); - out.push_str(" Do not edit this page by hand.\n"); - out.push_str(" If it looks wrong/outdated, regenerate it from source and commit the result.\n"); + out.push_str(" Do not edit this page by hand. If it looks wrong/outdated, regenerate it from source and commit the result.\n"); out.push('\n'); out.push_str(" Regenerate with: `cargo run -p incan_core --bin generate_lang_reference`\n\n"); @@ -152,9 +151,8 @@ fn write_feature_inventory_reference(path: &Path) { let mut out = String::new(); out.push_str("# Incan feature inventory\n\n"); out.push_str("!!! warning \"Generated file\"\n"); - out.push_str(" Do not edit this page by hand.\n"); out.push_str( - " If it looks wrong/outdated, update `crates/incan_core/src/lang/features.rs` and regenerate it.\n", + " Do not edit this page by hand. If it looks wrong/outdated, update `crates/incan_core/src/lang/features.rs` and regenerate it.\n", ); out.push('\n'); out.push_str(" Regenerate with: `cargo run -p incan_core --bin generate_lang_reference`\n\n"); @@ -483,9 +481,7 @@ fn render_decorators_section(out: &mut String) { start_section(out, "## Decorators"); out.push_str( - r#"User-defined decorators are valid on top-level `def` / `async def` declarations and instance methods. A -decorator is an ordinary callable value that receives the decorated function value and returns the binding that should -replace it: + r#"User-defined decorators are valid on top-level `def` / `async def` declarations and instance methods. A decorator is an ordinary callable value that receives the decorated function value and returns the binding that should replace it: ```incan def parse(value: int) -> int: @@ -502,19 +498,11 @@ def main() -> None: result = label(1) # int ``` -Stacked decorators apply bottom-up, matching Python's declaration model: the decorator closest to `def` receives the -original function value first, and the outer decorators receive each previous result. Decorator factories such as -`@logged("name")` are checked by first evaluating the factory expression as a callable-producing expression and then -applying the produced decorator to the function value. +Stacked decorators apply bottom-up, matching Python's declaration model: the decorator closest to `def` receives the original function value first, and the outer decorators receive each previous result. Decorator factories such as `@logged("name")` are checked by first evaluating the factory expression as a callable-producing expression and then applying the produced decorator to the function value. -Method decorators receive an unbound callable shape with the receiver first. A decorator on -`def label(self, value: int) -> str` sees `(&Box, int) -> str`; a decorator on -`def bump(mut self, value: int) -> int` sees `(&mut Box, int) -> int`. The wrapper passes the actual receiver borrow -through to the decorated callable, so method decorators do not require cloning the receiver. +Method decorators receive an unbound callable shape with the receiver first. A decorator on `def label(self, value: int) -> str` sees `(&Box, int) -> str`; a decorator on `def bump(mut self, value: int) -> int` sees `(&mut Box, int) -> int`. The wrapper passes the actual receiver borrow through to the decorated callable, so method decorators do not require cloning the receiver. -Class, model, trait, enum, newtype, field, alias, and module decorators remain limited to compiler-owned decorators. -Compiler-owned decorators such as `@derive`, `@route`, `@rust.extern`, `@rust.allow`, `@staticmethod`, `@classmethod`, -and `@requires` keep their existing special behavior. +Class, model, trait, enum, newtype, field, alias, and module decorators remain limited to compiler-owned decorators. Compiler-owned decorators such as `@derive`, `@route`, `@rust.extern`, `@rust.allow`, `@staticmethod`, `@classmethod`, and `@requires` keep their existing special behavior. "#, ); diff --git a/crates/incan_core/src/lang/surface/functions.rs b/crates/incan_core/src/lang/surface/functions.rs index d2b7998cc..4da806239 100644 --- a/crates/incan_core/src/lang/surface/functions.rs +++ b/crates/incan_core/src/lang/surface/functions.rs @@ -32,7 +32,7 @@ pub type SurfaceFnInfo = LangItemInfo; pub const SURFACE_FUNCTIONS: &[SurfaceFnInfo] = &[ info( SurfaceFnId::SleepMs, - // TODO: consider mergeing sleep and sleep_ms or at least moving them together... + // TODO: consider merging sleep and sleep_ms or at least moving them together... "sleep_ms", &[], "Sleep for N milliseconds.", diff --git a/crates/incan_core/src/lang/types/mod.rs b/crates/incan_core/src/lang/types/mod.rs index ab329b667..2b623289d 100644 --- a/crates/incan_core/src/lang/types/mod.rs +++ b/crates/incan_core/src/lang/types/mod.rs @@ -1,6 +1,6 @@ //! Builtin type vocabularies. //! -//! This module defines registries for builtin/blesed type names (and their aliases) that are +//! This module defines registries for builtin/blessed type names (and their aliases) that are //! recognized by the compiler. //! //! ## Notes diff --git a/crates/incan_stdlib/Cargo.toml b/crates/incan_stdlib/Cargo.toml index 864d6fc61..c09cdb9c7 100644 --- a/crates/incan_stdlib/Cargo.toml +++ b/crates/incan_stdlib/Cargo.toml @@ -17,7 +17,6 @@ serde_json = { version = "1.0", optional = true } axum = { version = "0.8", optional = true } tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros", "time", "sync", "net"] } inventory = { version = "0.3", optional = true } -regex = "1.0" xxhash_rust = { package = "xxhash-rust", version = "0.8", features = ["xxh3"] } [features] diff --git a/crates/incan_stdlib/README.md b/crates/incan_stdlib/README.md index 2d239ddc8..f9bda648c 100644 --- a/crates/incan_stdlib/README.md +++ b/crates/incan_stdlib/README.md @@ -76,14 +76,11 @@ Enables the current Axum-backed host runtime for generated Incan web programs. ### Incan stdlib stubs -The Incan-source stdlib stubs live under `crates/incan_stdlib/stdlib/` (for example -`crates/incan_stdlib/stdlib/testing.incn`). +The Incan-source stdlib stubs live under `crates/incan_stdlib/stdlib/` (for example `crates/incan_stdlib/stdlib/testing.incn`). These files define the user-facing `std.*` API surface and are parsed by the compiler for signature/validation metadata. -For `std.testing`, this also includes marker semantics metadata consumed by `incan test` discovery/execution. -The Rust runtime in `src/testing.rs` only provides irreducible host boundaries declared via `@rust.extern`. -The language `assert` statement is always available without importing `std.testing`; the stdlib assertion helpers mirror that behavior for call-style assertions and unwrap-style helpers. +For `std.testing`, this also includes marker semantics metadata consumed by `incan test` discovery/execution. The Rust runtime in `src/testing.rs` only provides irreducible host boundaries declared via `@rust.extern`. The language `assert` statement is always available without importing `std.testing`; the stdlib assertion helpers mirror that behavior for call-style assertions and unwrap-style helpers. ### Transitional web runtime @@ -97,8 +94,7 @@ The `__private` module re-exports host crates needed by generated Rust. It is to ### Why a Separate Crate? -Generated Incan programs need access to these traits and utilities, but shouldn't depend on the entire compiler. -This crate provides a minimal, stable API surface for compiled code. +Generated Incan programs need access to these traits and utilities, but shouldn't depend on the entire compiler. This crate provides a minimal, stable API surface for compiled code. ### Relationship with `incan_derive` @@ -124,8 +120,7 @@ pub struct User { ## Version Compatibility -This crate follows the Incan compiler version. For example, when you compile with `incan v0.1.0`, the generated code -depends on `incan_stdlib v0.1.0`. +This crate follows the Incan compiler version. For example, when you compile with `incan v0.1.0`, the generated code depends on `incan_stdlib v0.1.0`. ## License diff --git a/crates/incan_stdlib/src/web.rs b/crates/incan_stdlib/src/web.rs index 83bb0bc23..7203732e3 100644 --- a/crates/incan_stdlib/src/web.rs +++ b/crates/incan_stdlib/src/web.rs @@ -8,7 +8,7 @@ //! are still settling. Public items here should be treated as generated-code support unless the Incan stdlib stubs and //! language docs explicitly expose them. -// FIXME: this module need to be rewritten in incan once the appropriate RFCs are implemented +// FIXME: this module needs to be rewritten in Incan once the appropriate RFCs are implemented. use std::net::SocketAddr; diff --git a/crates/incan_stdlib/stdlib/collections.incn b/crates/incan_stdlib/stdlib/collections.incn index 521d369f3..01e45f246 100644 --- a/crates/incan_stdlib/stdlib/collections.incn +++ b/crates/incan_stdlib/stdlib/collections.incn @@ -874,8 +874,7 @@ pub model Counter[T with (Clone, Eq)]: count: New non-negative count. A zero count removes the value. """ if count < 0: - _runtime_error("Counter counts must be non-negative") - return + return raise_value_error("Counter counts must be non-negative") index = self.index_of(key) if count == 0: if index >= 0: @@ -895,8 +894,7 @@ pub model Counter[T with (Clone, Eq)]: amount: Non-negative amount to add. """ if amount < 0: - _runtime_error("Counter.increment requires a non-negative amount") - return + return raise_value_error("Counter.increment requires a non-negative amount") self.set(key, self.get(key) + amount) def decrement(mut self, key: T, amount: int = 1) -> None: @@ -908,8 +906,7 @@ pub model Counter[T with (Clone, Eq)]: amount: Non-negative amount to subtract. """ if amount < 0: - _runtime_error("Counter.decrement requires a non-negative amount") - return + return raise_value_error("Counter.decrement requires a non-negative amount") current = self.get(key) if amount >= current: self.set(key, 0) @@ -1221,7 +1218,7 @@ pub model DefaultDict[K with (Clone, Eq), V with Clone]: None => pass match self.default_value: Some(value) => return value - None => return _missing_value[V]() + None => return raise_key_error("DefaultDict key is not present") def index_of(self, key: K) -> int: return _entry_index(self.entries, key) @@ -1306,7 +1303,7 @@ pub model OrderedDict[K with (Clone, Eq), V with Clone]: """ index = self.index_of(key) if index < 0: - return _missing_value[V]() + return raise_key_error("OrderedDict key is not present") return self.entries[index].value def __setitem__(mut self, key: K, value: V) -> None: @@ -1360,7 +1357,7 @@ pub model OrderedDict[K with (Clone, Eq), V with Clone]: """ index = self.index_of(key) if index < 0: - return _missing_value[V]() + return raise_key_error("OrderedDict key is not present") value = self.entries[index].value self.entries.remove(index) return value @@ -1588,7 +1585,7 @@ pub model SortedDict[K with (Clone, Ord), V with Clone]: """ index = self.index_of(key) if index < 0: - return _missing_value[V]() + return raise_key_error("SortedDict key is not present") return self.entries[index].value def __setitem__(mut self, key: K, value: V) -> None: @@ -1643,7 +1640,7 @@ pub model SortedDict[K with (Clone, Ord), V with Clone]: """ index = self.index_of(key) if index < 0: - return _missing_value[V]() + return raise_key_error("SortedDict key is not present") value = self.entries[index].value self.entries.remove(index) return value @@ -1940,7 +1937,7 @@ pub model ChainMap[K with (Clone, Eq), V with Clone]: match layer.get(key): Some(value) => return value None => pass - return _missing_value[V]() + return raise_key_error("ChainMap key is not present") def __setitem__(mut self, key: K, value: V) -> None: """ @@ -2375,7 +2372,6 @@ def _merge_ordinal_records[K with (Clone, OrdinalKey)]( right_index += 1 return out - def _ordinal_map_magic() -> bytes: """ Return the fixed `OrdinalMap` container magic prefix. @@ -2785,14 +2781,3 @@ def _chain_items[K with (Clone, Eq), V with Clone](layers: list[OrderedDict[K, V break None => pass return out - - -def _missing_value[T with Clone]() -> T: - missing: list[T] = [] - return missing[0] - - -def _runtime_error(message: str) -> None: - _ = message - missing: list[int] = [] - missing[0] diff --git a/crates/rust_inspect/README.md b/crates/rust_inspect/README.md index b31569a43..95c29cd84 100644 --- a/crates/rust_inspect/README.md +++ b/crates/rust_inspect/README.md @@ -90,8 +90,7 @@ The stable architectural rule is the phase boundary: extraction happens before h Structured logging for durable diagnostics uses `tracing` (for example disk-cache parse failures and failed persists). -The on-disk cache filename is `.incan_rust_inspect_cache.json`. The cache loader still reads the older -`.incan_rust_metadata_cache.json` filename for backward compatibility. +The on-disk cache filename is `.incan_rust_inspect_cache.json`. The cache loader still reads the older `.incan_rust_metadata_cache.json` filename for backward compatibility. ## Limitations diff --git a/examples/README.md b/examples/README.md index c731b9ba7..84204886e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -26,14 +26,10 @@ Projects aimed at library and tooling authors: companion crates, vocab/desugarin Notable pro Rust interop example: -- `pro/rust_interop_pro.incn` - RFC 041 authoring surface (`rusttype`, `interop`, `std.rust` bounds, async wrappers). - Includes a "new to Rust" mental model that explains `rusttype`, `...` method declarations, and `interop:` edges. -- `pro/vocab_querykit` - runnable RFC 040 vocab companion example for query blocks, leading-dot fields, and - leading-dot fields in registered method arguments. -- `pro/vocab_routekit` - runnable RFC 040 vocab companion example for block headers, nested block-context clauses, and - scoped operator-like glyphs. -- `pro/vocab_studiokit` - runnable RFC 040 vocab companion example for workflow-shaped blocks and scoped fallback - glyphs. +- `pro/rust_interop_pro.incn` - RFC 041 authoring surface (`rusttype`, `interop`, `std.rust` bounds, async wrappers). Includes a "new to Rust" mental model that explains `rusttype`, `...` method declarations, and `interop:` edges. +- `pro/vocab_querykit` - runnable RFC 040 vocab companion example for query blocks, leading-dot fields, and leading-dot fields in registered method arguments. +- `pro/vocab_routekit` - runnable RFC 040 vocab companion example for block headers, nested block-context clauses, and scoped operator-like glyphs. +- `pro/vocab_studiokit` - runnable RFC 040 vocab companion example for workflow-shaped blocks and scoped fallback glyphs. ### `web/` diff --git a/examples/pro/vocab_querykit/consumer/incan.lock b/examples/pro/vocab_querykit/consumer/incan.lock index cce9f687c..9dbbbe516 100644 --- a/examples/pro/vocab_querykit/consumer/incan.lock +++ b/examples/pro/vocab_querykit/consumer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:d66866eca21aa7a29b265ef932049fe5b6da692cbe734cd4f7d300ce7163b359" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "querykit_consumer" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", @@ -59,7 +59,7 @@ dependencies = [ [[package]] name = "querykit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/examples/pro/vocab_querykit/producer/incan.lock b/examples/pro/vocab_querykit/producer/incan.lock index edf91ac0e..615fe6444 100644 --- a/examples/pro/vocab_querykit/producer/incan.lock +++ b/examples/pro/vocab_querykit/producer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:17f122844d2fa1c9756f9a1976d222f15255557e74d975b8d8ff46536ea82b87" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -50,7 +50,7 @@ dependencies = [ [[package]] name = "querykit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/examples/pro/vocab_routekit/consumer/incan.lock b/examples/pro/vocab_routekit/consumer/incan.lock index e4ea8e4ae..7e9a1589c 100644 --- a/examples/pro/vocab_routekit/consumer/incan.lock +++ b/examples/pro/vocab_routekit/consumer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:316bf142e6f8ea3b5838746eabec99c7e77d0acbcca01f8890c489b63498a743" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -59,7 +59,7 @@ dependencies = [ [[package]] name = "routekit_consumer" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", @@ -68,7 +68,7 @@ dependencies = [ [[package]] name = "routekit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/examples/pro/vocab_routekit/producer/incan.lock b/examples/pro/vocab_routekit/producer/incan.lock index b71d0f527..971820470 100644 --- a/examples/pro/vocab_routekit/producer/incan.lock +++ b/examples/pro/vocab_routekit/producer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:17f122844d2fa1c9756f9a1976d222f15255557e74d975b8d8ff46536ea82b87" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -59,7 +59,7 @@ dependencies = [ [[package]] name = "routekit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/examples/pro/vocab_studiokit/consumer/incan.lock b/examples/pro/vocab_studiokit/consumer/incan.lock index dc36ae80e..04ee44ea8 100644 --- a/examples/pro/vocab_studiokit/consumer/incan.lock +++ b/examples/pro/vocab_studiokit/consumer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:e434303c58e58e0d05c2ffbd9b4c3b5a5984c4d74d64978e203d295f87495eae" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "studiokit_consumer" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", @@ -98,7 +98,7 @@ dependencies = [ [[package]] name = "studiokit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/examples/pro/vocab_studiokit/producer/incan.lock b/examples/pro/vocab_studiokit/producer/incan.lock index bb504700b..2ecb8140b 100644 --- a/examples/pro/vocab_studiokit/producer/incan.lock +++ b/examples/pro/vocab_studiokit/producer/incan.lock @@ -3,7 +3,7 @@ [incan] format = 1 -incan-version = "0.3.0-dev.24" +incan-version = "0.3.0-dev.51" deps-fingerprint = "sha256:17f122844d2fa1c9756f9a1976d222f15255557e74d975b8d8ff46536ea82b87" cargo-features = [] cargo-no-default-features = false @@ -17,14 +17,14 @@ version = 4 [[package]] name = "incan_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "proc-macro2", "quote", @@ -33,7 +33,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_core", "incan_derive", @@ -89,7 +89,7 @@ dependencies = [ [[package]] name = "studiokit_core" -version = "0.3.0-dev.24" +version = "0.3.0-dev.51" dependencies = [ "incan_derive", "incan_stdlib", diff --git a/src/cli/commands/common.rs b/src/cli/commands/common.rs index c052ff816..597c0cc7e 100644 --- a/src/cli/commands/common.rs +++ b/src/cli/commands/common.rs @@ -858,7 +858,7 @@ pub fn read_source(file_path: &str) -> CliResult { } /// Return whether a parsed module uses RFC 088 iterator surface methods that require stdlib adapter modules. -fn uses_iterator_adapter_surface(program: &Program) -> bool { +pub(crate) fn uses_iterator_adapter_surface(program: &Program) -> bool { ast_walk::any_expr_in_program(program, |expr| match expr { crate::frontend::ast::Expr::MethodCall(_, method, _, _) => matches!( method.as_str(), @@ -889,7 +889,7 @@ fn uses_iterator_adapter_surface(program: &Program) -> bool { } /// Return whether a parsed module uses RFC 070 Result combinators backed by std.result helpers. -fn uses_result_combinator_surface(program: &Program) -> bool { +pub(crate) fn uses_result_combinator_surface(program: &Program) -> bool { ast_walk::any_expr_in_program(program, |expr| match expr { crate::frontend::ast::Expr::MethodCall(_, method, _, _) => result_methods::from_str(method).is_some(), _ => false, @@ -900,12 +900,9 @@ fn uses_result_combinator_surface(program: &Program) -> bool { /// /// # Note on Prelude /// -/// The stdlib prelude (`stdlib/prelude.incn`) exists but is not currently wired into the compilation pipeline. -/// Prelude traits like `Debug`, `Display`, `Clone` are recognized by codegen heuristics rather than actual trait -/// definitions. -/// -/// Future work: integrate prelude ASTs into typechecking so trait bounds are validated and derives work through actual -/// trait implementations. +/// The stdlib root prelude (`stdlib/prelude.incn`) exists, but it is not auto-imported into every compilation unit. +/// Source-backed stdlib trait modules and builtin fallback traits are still discovered explicitly when the parsed AST +/// needs them. pub fn collect_modules(entry_path: &str) -> CliResult> { let path = if Path::new(entry_path).is_absolute() { PathBuf::from(entry_path) diff --git a/src/cli/test_runner/module_graph.rs b/src/cli/test_runner/module_graph.rs index 9213b79ae..6f579c68d 100644 --- a/src/cli/test_runner/module_graph.rs +++ b/src/cli/test_runner/module_graph.rs @@ -2,7 +2,9 @@ use std::collections::{HashMap, HashSet}; use std::fs; use std::path::{Path, PathBuf}; -use crate::cli::commands::common::resolve_stdlib_module_source_path; +use crate::cli::commands::common::{ + resolve_stdlib_module_source_path, uses_iterator_adapter_surface, uses_result_combinator_surface, +}; use crate::cli::prelude::ParsedModule; use crate::frontend::ast::Program; use crate::frontend::library_manifest_index::LibraryManifestIndex; @@ -68,6 +70,36 @@ fn queue_resolved_source_import( Ok(()) } +/// Queue implicit source stdlib helper modules that generated Rust may reference without a source import. +fn queue_implicit_stdlib_helpers( + program: &Program, + incan_source_stdlib_module_paths: &mut HashMap, + processed: &HashSet, + to_process: &mut Vec<(PathBuf, String, Vec)>, +) -> Result<(), String> { + if uses_iterator_adapter_surface(program) { + queue_incan_stdlib_source_module( + &[ + stdlib::STDLIB_ROOT.to_string(), + "derives".to_string(), + "collection".to_string(), + ], + incan_source_stdlib_module_paths, + processed, + to_process, + )?; + } + if uses_result_combinator_surface(program) { + queue_incan_stdlib_source_module( + &[stdlib::STDLIB_ROOT.to_string(), "result".to_string()], + incan_source_stdlib_module_paths, + processed, + to_process, + )?; + } + Ok(()) +} + /// Collect source modules referenced by a test file's imports. /// /// Walks the test AST for `from import ...` statements that reference user modules and materialized stdlib @@ -87,6 +119,13 @@ pub(crate) fn collect_source_modules_for_test( let mut to_process: Vec<(PathBuf, String, Vec)> = Vec::new(); let mut incan_source_stdlib_module_paths: HashMap = HashMap::new(); + queue_implicit_stdlib_helpers( + test_ast, + &mut incan_source_stdlib_module_paths, + &processed, + &mut to_process, + )?; + // ---- Walk test AST to find user module imports ---- for resolved in resolve_program_source_imports(test_ast, source_root, Some(source_root)) { queue_resolved_source_import( @@ -145,6 +184,8 @@ pub(crate) fn collect_source_modules_for_test( eprint!("{}", diagnostics::format_error(&fp, &source, warn)); } + queue_implicit_stdlib_helpers(&ast, &mut incan_source_stdlib_module_paths, &processed, &mut to_process)?; + // Walk this module's imports for transitive dependencies. let current_base = file_path.parent().unwrap_or(source_root); for resolved in resolve_program_source_imports(&ast, current_base, Some(source_root)) { @@ -215,4 +256,45 @@ mod tests { Ok(()) } + + #[test] + fn test_runner_collects_implicit_result_helper_modules() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let src_dir = tmp.path().join("src"); + std::fs::create_dir_all(&src_dir)?; + + let test_source = r#" +def produce_error() -> Result[int, str]: + return Err("bad") + + +def convert_error(err: str) -> int: + return len(err) + + +def test_map_err_result_helper_is_packaged() -> None: + match produce_error().map_err(convert_error): + Ok(_) => assert false + Err(code) => assert code == 3 +"#; + let tokens = lexer::lex(test_source).map_err(|errs| errs[0].message.clone())?; + let ast = parser::parse_with_context(&tokens, Some("tests/test_result_map_err.incn"), None) + .map_err(|errs| errs[0].message.clone())?; + + let modules = collect_source_modules_for_test(&ast, &src_dir, None, None, None)?; + + assert!( + modules.iter().any(|module| module.path_segments + == vec![ + incan_core::lang::stdlib::INCAN_STD_NAMESPACE.to_string(), + "result".to_string() + ]), + "expected std.result helper module to be collected, got {:?}", + modules + .iter() + .map(|module| module.path_segments.clone()) + .collect::>() + ); + Ok(()) + } } diff --git a/src/format/mod.rs b/src/format/mod.rs index 37c4e09ee..b707b0fdd 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -1091,7 +1091,7 @@ async def run() -> int: Ok(()) } - /// Regression #235: qualified constructor patterns use `::` in the AST; the formatter must print Incansurface `.`. + /// Regression #235: qualified constructor patterns use `::` in the AST; the formatter must print Incan surface `.`. #[test] fn test_format_source_qualified_match_pattern_round_trip() -> Result<(), FormatError> { let source = r#"def f(x: int) -> int: diff --git a/src/frontend/DEVNOTES.md b/src/frontend/DEVNOTES.md index 3dc3c5c80..265ffa64f 100644 --- a/src/frontend/DEVNOTES.md +++ b/src/frontend/DEVNOTES.md @@ -43,8 +43,7 @@ The Incan compiler frontend consists of several components that transform source ### Indentation-Based Syntax -Like Python, Incan uses indentation for blocks. The lexer tracks indent levels and emits `INDENT` and `DEDENT` tokens. -Incan uses 2-space or 4-space indentation by default (tabs are treated as 4 spaces). +Like Python, Incan uses indentation for blocks. The lexer tracks indent levels and emits `INDENT` and `DEDENT` tokens. Incan uses 2-space or 4-space indentation by default (tabs are treated as 4 spaces). ### Rust-Style Imports diff --git a/src/frontend/typechecker/check_expr/access.rs b/src/frontend/typechecker/check_expr/access.rs index a8d45c4c4..b41eb0351 100644 --- a/src/frontend/typechecker/check_expr/access.rs +++ b/src/frontend/typechecker/check_expr/access.rs @@ -1346,9 +1346,21 @@ impl TypeChecker { match &metadata.kind { RustItemKind::Type(_) => { let Some(sig) = self.rust_method_signature(rust_path, method) else { - self.record_rust_extension_trait_import_for_call(&metadata, method, span); - // Metadata only covers inherent methods plus direct trait impl summaries. Stay permissive rather - // than false-positiving on valid calls when no unambiguous imported trait can be selected. + if let Some(import_use) = self.record_rust_extension_trait_import_for_call(&metadata, method, span) + && let Some(sig) = import_use.signature.as_ref() + { + let callable_display = format!("rust::{rust_path}.{method}"); + let ret = self.validate_rust_method_call( + callable_display.as_str(), + sig, + args, + arg_types, + preserves_lookup_arg_shape, + span, + ); + return Some(Self::substitute_rust_self_type(ret, rust_path)); + } + // Stay permissive when no unambiguous imported trait or trait method signature can be selected. return Some(ResolvedType::Unknown); }; if Self::rust_signature_has_receiver(&sig) @@ -1394,9 +1406,9 @@ impl TypeChecker { receiver_metadata: &incan_core::interop::RustItemMetadata, method: &str, span: Span, - ) { + ) -> Option { let RustItemKind::Type(type_info) = &receiver_metadata.kind else { - return; + return None; }; let matches = self .type_info @@ -1415,10 +1427,11 @@ impl TypeChecker { }) .collect::>(); let [import_use] = matches.as_slice() else { - return; + return None; }; self.type_info .record_rust_method_trait_import_use(span, import_use.clone()); + Some(import_use.clone()) } /// Return the trait method signature when `import` is implemented by `type_info` and declares `method`. diff --git a/src/frontend/typechecker/tests.rs b/src/frontend/typechecker/tests.rs index 740c2ad60..7425f8c94 100644 --- a/src/frontend/typechecker/tests.rs +++ b/src/frontend/typechecker/tests.rs @@ -8054,6 +8054,93 @@ def f(w: Widget) -> None: Ok(()) } +#[test] +fn test_rust_extension_trait_associated_call_records_param_shape() -> Result<(), Box> { + let source = r#" +from rust::demo import Cursor, FileDescriptorSet, Message + +def f(cursor: Cursor) -> None: + _ = FileDescriptorSet.decode(cursor) +"#; + let tokens = lexer::lex(source).map_err(|errs| std::io::Error::other(format!("lex failed: {errs:?}")))?; + let ast = parser::parse(&tokens).map_err(|errs| std::io::Error::other(format!("parse failed: {errs:?}")))?; + let mut checker = TypeChecker::new(); + let tmp = seeded_rust_inspect_workspace()?; + let manifest_dir = tmp.path().to_path_buf(); + checker.set_rust_inspect_manifest_dir(manifest_dir.clone()); + checker + .rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: "demo::Message".to_string(), + definition_path: Some("demo::Message".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Trait(RustTraitInfo { + items: vec![RustTraitAssoc::Function { + name: "decode".to_string(), + signature: RustFunctionSig { + params: vec![RustParam { + name: Some("buf".to_string()), + type_display: "T".to_string(), + }], + return_type: "Self".to_string(), + is_async: false, + is_unsafe: false, + }, + }], + }), + }, + ) + .map_err(|err| std::io::Error::other(format!("seed trait metadata: {err}")))?; + for path in ["demo::Cursor", "demo::FileDescriptorSet"] { + checker + .rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: path.to_string(), + definition_path: Some(path.to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Type(RustTypeInfo { + methods: Vec::new(), + implemented_traits: if path.ends_with("FileDescriptorSet") { + vec![RustImplementedTrait { + path: "demo::Message".to_string(), + }] + } else { + Vec::new() + }, + fields: Vec::new(), + variants: Vec::new(), + }), + }, + ) + .map_err(|err| std::io::Error::other(format!("seed type metadata: {err}")))?; + } + + checker + .check_program(&ast) + .map_err(|errs| std::io::Error::other(format!("typecheck failed: {errs:?}")))?; + let uses = &checker.type_info().rust.method_trait_import_uses; + assert!( + uses.values() + .any(|import_use| import_use.binding == "Message" && import_use.method == "decode"), + "expected Message import use, got {uses:?}" + ); + assert!( + checker + .type_info() + .calls + .call_site_callable_params + .values() + .any(|params| params.len() == 1 && params[0].ty == ResolvedType::TypeVar("T".to_string())), + "expected trait-provided decode parameter shape to be recorded, got {:?}", + checker.type_info().calls.call_site_callable_params + ); + Ok(()) +} + #[cfg(feature = "rust_inspect")] #[test] fn test_rusttype_bodyless_rust_trait_forwarding_uses_metadata_and_skips_impl() -> Result<(), Box> diff --git a/src/frontend/vocab_desugar_pass/runtime.rs b/src/frontend/vocab_desugar_pass/runtime.rs index fe8f92eab..f916ce984 100644 --- a/src/frontend/vocab_desugar_pass/runtime.rs +++ b/src/frontend/vocab_desugar_pass/runtime.rs @@ -25,8 +25,8 @@ const MEMORY_EXPORT: &str = incan_vocab::WASM_DESUGAR_MEMORY_EXPORT; const SUCCESS_STATUS: i32 = incan_vocab::WASM_DESUGAR_SUCCESS_STATUS; /// Default fuel budget for one desugarer invocation. /// -/// The clean #455 nested companion repro traps at 250 000 units while the guest drops or serializes nested public AST -/// output. 5 000 000 admits that production-shaped path while still bounding accidental long-running desugarers. +/// The clean #455 nested companion repro traps at `250_000` units while the guest drops or serializes nested public AST +/// output. `5_000_000` admits that production-shaped path while still bounding accidental long-running desugarers. /// If another legitimate companion desugarer hits this limit, raise it with a comment explaining the measured repro. const DEFAULT_WASM_FUEL: u64 = 5_000_000; diff --git a/src/lsp/call_site_type_args.rs b/src/lsp/call_site_type_args.rs index 307e488a6..3d307022e 100644 --- a/src/lsp/call_site_type_args.rs +++ b/src/lsp/call_site_type_args.rs @@ -369,12 +369,10 @@ fn call_site_type_in_assert_stmt( }) } -/// Search a control-flow condition for explicit call-site type arguments at the -/// requested offset. +/// Search a control-flow condition for explicit call-site type arguments at the requested offset. /// -/// Let-pattern conditions only expose type arguments from the scrutinee -/// expression; pattern nodes themselves do not currently carry call-site type -/// argument syntax. +/// Let-pattern conditions only expose type arguments from the scrutinee expression; pattern nodes themselves do not +/// currently carry call-site type argument syntax. fn call_site_type_in_condition(condition: &Condition, offset: usize) -> Option<&Spanned> { match condition { Condition::Expr(expr) => call_site_type_in_expr(expr, offset), diff --git a/src/project_lifecycle/toolchain.rs b/src/project_lifecycle/toolchain.rs index c162278d9..74094ea73 100644 --- a/src/project_lifecycle/toolchain.rs +++ b/src/project_lifecycle/toolchain.rs @@ -224,8 +224,8 @@ impl std::error::Error for ToolchainConstraintError {} /// Return whether a requirement matches the active toolchain. /// /// Incan dev builds are published as SemVer prereleases such as `0.3.0-dev.48`. For lifecycle compatibility, a dev -/// build is allowed to satisfy a range that admits its release-core version (`0.3.0`) so projects can write the normal -/// line constraint `>=0.3,<0.4` while the line is still in development. +/// build is allowed to satisfy a range that admits its release-core version (`0.3.0`) so projects can write a normal +/// release-line constraint such as `>=0.3,<0.4` while the line is still in development. fn requirement_matches_active_toolchain(requirement: &VersionReq, active: &Version) -> bool { if requirement.matches(active) { return true; diff --git a/tests/cli_integration.rs b/tests/cli_integration.rs index f53f2b9a6..80b82d3da 100644 --- a/tests/cli_integration.rs +++ b/tests/cli_integration.rs @@ -732,6 +732,82 @@ impl FileDescriptorSet { Ok(()) } +#[test] +fn run_accepts_trait_provided_by_value_generic_decode_rust_param_issue612() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let main_path = write_minimal_project( + tmp.path(), + "cli_trait_by_value_generic_decode_project", + r#" + +[rust-dependencies] +decode_trait_helper = { path = "rust/decode_trait_helper" } +"#, + )?; + fs::write( + &main_path, + r#"from rust::decode_trait_helper import FileDescriptorSet, Message + + +def main() -> None: + encoded = b"abc" + match FileDescriptorSet.decode(encoded.as_slice()): + Ok(_) => println("ok") + Err(_) => println("err") +"#, + )?; + let helper_src = tmp.path().join("rust").join("decode_trait_helper").join("src"); + fs::create_dir_all(&helper_src)?; + fs::write( + helper_src + .parent() + .ok_or("helper src has no parent")? + .join("Cargo.toml"), + r#"[package] +name = "decode_trait_helper" +version = "0.1.0" +edition = "2021" +"#, + )?; + fs::write( + helper_src.join("lib.rs"), + r#"pub trait DecodeBuf {} + +impl DecodeBuf for &[u8] {} + +pub struct DecodeError; + +pub struct FileDescriptorSet; + +pub trait Message: Sized { + fn decode(_buf: T) -> Result; +} + +impl Message for FileDescriptorSet { + fn decode(_buf: T) -> Result { + Ok(Self) + } +} +"#, + )?; + + let output = run_incan( + tmp.path(), + &["run", main_path.to_str().ok_or("main path was not valid UTF-8")?], + )?; + + assert_success( + &output, + "incan run with trait-provided by-value generic decode Rust param", + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + stdout.contains("ok"), + "expected trait-provided by-value generic decode helper output, got:\n{stdout}" + ); + Ok(()) +} + #[test] fn build_locked_rejects_stale_lockfile() -> Result<(), Box> { let tmp = tempfile::tempdir()?; diff --git a/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md b/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md index d7de83ed4..fab17e577 100644 --- a/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md +++ b/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md @@ -18,14 +18,11 @@ Introduce `ctx` as a core language keyword that declares a typed, globally acces Every non-trivial application needs configuration: database paths, batch sizes, API endpoints, feature flags. Today's patterns are painful: -- **Python dataclass + env vars**: manual `os.environ.get("KEY", "default")` -for every field. No compile-time validation. String-typed. Error-prone. -- **Pydantic BaseSettings**: typed and env-var-aware, but runtime-only -validation. No multi-environment resolution. No language integration. +- **Python dataclass + env vars**: manual `os.environ.get("KEY", "default")` for every field. No compile-time validation. String-typed. Error-prone. +- **Pydantic BaseSettings**: typed and env-var-aware, but runtime-only validation. No multi-environment resolution. No language integration. - **YAML + merge**: `common.yaml` + `prod.yaml` merged at load time. Untyped, no IDE support, merge surprises. - **Django settings**: Python module importable globally. Untyped, single-environment, no structured overrides. -- **Koheesio Context**: dict-like, YAML-loaded, `.get("key")` returns `Any`. -The surface stays dynamic where Incan should be typed. +- **Koheesio Context**: dict-like, YAML-loaded, `.get("key")` returns `Any`. The surface stays dynamic where Incan should be typed. The common pain points: @@ -86,13 +83,11 @@ def process_batch(items: list[Item]) -> list[Result]: ... ``` -`AppConfig.batch_size` is a global read. The compiler knows the type (`int`) at -compile time. IDE completion works. Rename-refactoring works. +`AppConfig.batch_size` is a global read. The compiler knows the type (`int`) at compile time. IDE completion works. Rename-refactoring works. ### How env_prefix works -`ctx AppConfig(env_prefix="APP_")` maps each field to an environment variable -using `{env_prefix}{UPPER_SNAKE_CASE_FIELD_NAME}`: +`ctx AppConfig(env_prefix="APP_")` maps each field to an environment variable using `{env_prefix}{UPPER_SNAKE_CASE_FIELD_NAME}`: | Field | Env var | Lookup order | | -------------- | ------------------ | ----------------------------- | @@ -129,9 +124,7 @@ This matches what every ops engineer already expects: "set it in the environment ### Per-field env var customization -All fields read from env vars by default via `env_prefix` plus -`UPPER_SNAKE_CASE`. For fields that need a custom env var name, use field -metadata: +All fields read from env vars by default via `env_prefix` plus `UPPER_SNAKE_CASE`. For fields that need a custom env var name, use field metadata: ```incan ctx AppConfig(env_prefix="APP_"): @@ -193,9 +186,7 @@ case_arm ::= "case" IDENT ":" NEWLINE INDENT field_override+ DEDENT field_override ::= IDENT "=" expr NEWLINE ``` -**`ctx` is a core keyword** and is always reserved, not a soft keyword. It has -singleton, env-var, and match-block semantics that do not fit `model` or -`class`. +**`ctx` is a core keyword** and is always reserved, not a soft keyword. It has singleton, env-var, and match-block semantics that do not fit `model` or `class`. ### Declaration rules @@ -204,19 +195,14 @@ singleton, env-var, and match-block semantics that do not fit `model` or 3. The body contains **field declarations** (same syntax as `model` fields) and **match blocks**. 4. Match blocks reference a user-defined `enum` type. The enum must be in scope. 5. Match arms can only override fields declared in the same `ctx`. They cannot introduce new fields. -6. Fields without defaults and without coverage in all match arms are a compile -error. The context must be fully resolvable. +6. Fields without defaults and without coverage in all match arms are a compile error. The context must be fully resolvable. ### Type checking rules -1. **Field types**: same as `model` fields. Primitive types (`str`, `int`, - `float`, `bool`), `Option[T]`, `list[T]`, and model types are all valid. -2. **Match arm overrides**: the assigned value must be type-compatible with the -field's declared type. `batch_size: int = 100` can only be overridden with an `int`. -3. **Global access**: `AppConfig.field_name` is typed using the field's -declared type. The typechecker resolves it as a static field access on a known singleton, not a regular instance access. -4. **Immutability**: context fields are read-only after initialization. -Assigning to `AppConfig.field = x` outside of a match arm or `.init()` is a compile error. +1. **Field types**: same as `model` fields. Primitive types (`str`, `int`, `float`, `bool`), `Option[T]`, `list[T]`, and model types are all valid. +2. **Match arm overrides**: the assigned value must be type-compatible with the field's declared type. `batch_size: int = 100` can only be overridden with an `int`. +3. **Global access**: `AppConfig.field_name` is typed using the field's declared type. The typechecker resolves it as a static field access on a known singleton, not a regular instance access. +4. **Immutability**: context fields are read-only after initialization. Assigning to `AppConfig.field = x` outside of a match arm or `.init()` is a compile error. ### Env var type coercion @@ -234,18 +220,12 @@ If coercion fails, the program exits at startup with a clear error message namin ### Axis resolution -For each `match EnumType:` block in a `ctx`, the runtime reads -`{env_prefix}{ENUM_TYPE_NAME}` from the environment, for example `APP_ENV` for -`match Env:`. The value must match an enum variant name case-insensitively. If -the env var is absent, the match block is skipped entirely and field defaults are used. +For each `match EnumType:` block in a `ctx`, the runtime reads `{env_prefix}{ENUM_TYPE_NAME}` from the environment, for example `APP_ENV` for `match Env:`. The value must match an enum variant name case-insensitively. If the env var is absent, the match block is skipped entirely and field defaults are used. ### Lifecycle -1. **Before init**: accessing any `ctx` field panics with - `"AppConfig not initialized"`. If the compiler can prove a field is -accessed before `main()`, it emits a compile error. -2. **Init**: at program startup, or explicitly via `.init()` in tests, the -runtime resolves axis env vars, evaluates matching arms, applies env var field overrides, and then locks the singleton. +1. **Before init**: accessing any `ctx` field panics with `"AppConfig not initialized"`. If the compiler can prove a field is accessed before `main()`, it emits a compile error. +2. **Init**: at program startup, or explicitly via `.init()` in tests, the runtime resolves axis env vars, evaluates matching arms, applies env var field overrides, and then locks the singleton. 3. **After init** — `AppConfig.field` is a zero-cost read. Immutable. Thread-safe. 4. **In tests** — `.init()` can be called again (resets the singleton) for per-test configuration. @@ -305,25 +285,19 @@ ctx InfraConfig(env_prefix="INFRA_"): ) ``` -Nested access works naturally: `InfraConfig.cluster.node_type`. The flow is one-directional: `ctx` contains models, but models do not contain `ctx`. A -`Transaction` model should never reference `AppConfig`. +Nested access works naturally: `InfraConfig.cluster.node_type`. The flow is one-directional: `ctx` contains models, but models do not contain `ctx`. A `Transaction` model should never reference `AppConfig`. ### Interaction with existing features **Traits:** `ctx` types do not implement traits. They are singletons, not polymorphic data types. -**async/await:** `ctx` fields are synchronously available. No `await` is -needed because they are resolved at startup. Async functions can read `ctx` fields freely. +**async/await:** `ctx` fields are synchronously available. No `await` is needed because they are resolved at startup. Async functions can read `ctx` fields freely. -**Imports/modules:** `ctx` declarations follow normal module visibility. A -`pub ctx` in a library can be imported by consumers, but initialization is the -consumer's responsibility, not the library's. +**Imports/modules:** `ctx` declarations follow normal module visibility. A `pub ctx` in a library can be imported by consumers, but initialization is the consumer's responsibility, not the library's. -**Error handling:** env var parsing failures at init time produce a clear -startup error, not a `Result`. This is intentional: misconfigured environments should crash immediately rather than propagate silently. +**Error handling:** env var parsing failures at init time produce a clear startup error, not a `Result`. This is intentional: misconfigured environments should crash immediately rather than propagate silently. -**Field metadata:** `ctx` fields support the same `[key=value]` metadata -syntax as model fields. The currently defined key is `env`, which provides a custom env var name or disables env lookup with `false`. +**Field metadata:** `ctx` fields support the same `[key=value]` metadata syntax as model fields. The currently defined key is `env`, which provides a custom env var name or disables env lookup with `false`. ### Compatibility / migration @@ -340,8 +314,7 @@ model AppConfig: ... ``` -**Rejected.** The `match Env:` blocks do not fit naturally inside a `model` -body. The singleton semantics, env var integration, and match resolution are different enough from `model` to warrant a distinct keyword. A decorator would hide that semantic difference. +**Rejected.** The `match Env:` blocks do not fit naturally inside a `model` body. The singleton semantics, env var integration, and match resolution are different enough from `model` to warrant a distinct keyword. A decorator would hide that semantic difference. ### Runtime-only config (like Pydantic BaseSettings) @@ -355,23 +328,19 @@ model AppConfig: config = AppConfig.from_env(prefix="APP_") ``` -**Rejected.** This loses compile-time validation of field references, requires -passing configuration around as a parameter, and cannot express match-block environment overrides. It is just a typed version of what Python already has. +**Rejected.** This loses compile-time validation of field references, requires passing configuration around as a parameter, and cannot express match-block environment overrides. It is just a typed version of what Python already has. ### Soft keyword via library Ship `ctx` as a library-provided soft keyword instead of a core keyword. -**Rejected.** `ctx` is universally useful rather than domain-specific. Every -application needs configuration. Making it a library feature would mean the most basic use case, reading a setting from an env var, requires a dependency. The env var integration and singleton semantics also belong to the language contract, not an optional library add-on. +**Rejected.** `ctx` is universally useful rather than domain-specific. Every application needs configuration. Making it a library feature would mean the most basic use case, reading a setting from an env var, requires a dependency. The env var integration and singleton semantics also belong to the language contract, not an optional library add-on. ## Drawbacks - **New keyword** — `ctx` becomes reserved, breaking any code using it as an identifier. Acceptable pre-1.0. -- **Global mutable state**: technically a singleton, which some consider an -anti-pattern. This is mitigated by immutability after init and reset support in tests. -- **Hidden dependency**: functions that read `AppConfig.field` have an -implicit dependency on the context being initialized. That dependency is not visible in the function signature. This is the intended trade-off between explicitness and boilerplate reduction. +- **Global mutable state**: technically a singleton, which some consider an anti-pattern. This is mitigated by immutability after init and reset support in tests. +- **Hidden dependency**: functions that read `AppConfig.field` have an implicit dependency on the context being initialized. That dependency is not visible in the function signature. This is the intended trade-off between explicitness and boilerplate reduction. ## Layers affected @@ -384,30 +353,17 @@ implicit dependency on the context being initialized. That dependency is not vis ## Unresolved questions -1. **Should fields without defaults require exhaustive match coverage?** If - `auth_token: str` has no default and `match Env` only covers `Prod`, what -happens in `Dev`? Options: compile error requiring all arms, or requiring a default value. Leaning toward: fields without defaults must be covered by all match arms or have a matching env var annotation, with init-time failure if the env var is also absent. +1. **Should fields without defaults require exhaustive match coverage?** If `auth_token: str` has no default and `match Env` only covers `Prod`, what happens in `Dev`? Options: compile error requiring all arms, or requiring a default value. Leaning toward: fields without defaults must be covered by all match arms or have a matching env var annotation, with init-time failure if the env var is also absent. -2. **Should `env_prefix` be optional?** It could default to - `{UPPER_SNAKE_CASE_CTX_NAME}_`. Leaning toward: required, because explicit -is better than implicit for something that maps to external env vars. +2. **Should `env_prefix` be optional?** It could default to `{UPPER_SNAKE_CASE_CTX_NAME}_`. Leaning toward: required, because explicit is better than implicit for something that maps to external env vars. -3. **How should nested model fields map to env vars?** For - `cluster: ClusterConfig`, should `INFRA_CLUSTER__NODE_TYPE` work via a -delimiter, or should nested models only be overridable as a whole? Leaning toward: defer nested env var mapping. The initial contract can stay flat, with nested models overridden through match arms. +3. **How should nested model fields map to env vars?** For `cluster: ClusterConfig`, should `INFRA_CLUSTER__NODE_TYPE` work via a delimiter, or should nested models only be overridable as a whole? Leaning toward: defer nested env var mapping. The initial contract can stay flat, with nested models overridden through match arms. -4. **Auto-init or explicit init?** Should the compiler automatically insert the -generated init call at the start of `main()`, or must the user call - `AppConfig.init()`? Auto-init is more ergonomic; explicit init gives control -over ordering when there are multiple `ctx` declarations. Leaning toward: auto-init in `main()`, with `.init()` still available for tests and explicit control. +4. **Auto-init or explicit init?** Should the compiler automatically insert the generated init call at the start of `main()`, or must the user call `AppConfig.init()`? Auto-init is more ergonomic; explicit init gives control over ordering when there are multiple `ctx` declarations. Leaning toward: auto-init in `main()`, with `.init()` still available for tests and explicit control. -5. **Multiple match axes scope.** This RFC covers single-axis matching only. -Should multi-axis matching, such as `match RunMode:` plus - `match (Env, RunMode):`, be a follow-up RFC or part of this one? Leaning -toward: a follow-up RFC. Single-axis matching covers the common case and keeps the initial implementation focused. +5. **Multiple match axes scope.** This RFC covers single-axis matching only. Should multi-axis matching, such as `match RunMode:` plus `match (Env, RunMode):`, be a follow-up RFC or part of this one? Leaning toward: a follow-up RFC. Single-axis matching covers the common case and keeps the initial implementation focused. -6. **Generated panic compatibility with strict lint settings.** The generated init path may use fail-fast panics for -env var parse failures and singleton access that occurs before initialization. Both are intentional startup/programming errors, but projects that enable strict lints against generated panic sites will still see diagnostics. Should the generated init function return `Result[None, CtxInitError]` and propagate errors to `main` instead of panicking? That would remove the panic sites but would also require users to handle init errors explicitly. Leaning toward: **keep fail-fast panics in this RFC** — fail-fast startup is the point, and a `Result` return complicates auto-init. +6. **Generated panic compatibility with strict lint settings.** The generated init path may use fail-fast panics for env var parse failures and singleton access that occurs before initialization. Both are intentional startup/programming errors, but projects that enable strict lints against generated panic sites will still see diagnostics. Should the generated init function return `Result[None, CtxInitError]` and propagate errors to `main` instead of panicking? That would remove the panic sites but would also require users to handle init errors explicitly. Leaning toward: **keep fail-fast panics in this RFC** — fail-fast startup is the point, and a `Result` return complicates auto-init. diff --git a/workspaces/docs-site/docs/RFCs/034_incan_pub_registry.md b/workspaces/docs-site/docs/RFCs/034_incan_pub_registry.md index 45b614f5b..efa76f6e8 100644 --- a/workspaces/docs-site/docs/RFCs/034_incan_pub_registry.md +++ b/workspaces/docs-site/docs/RFCs/034_incan_pub_registry.md @@ -293,8 +293,7 @@ If verification fails, `incan build` refuses to use the package and emits a clea **Sigstore is optional in Phase 1** — the `signatures` field in the index is nullable. Unsigned packages are accepted but display a warning during `incan build`. The goal is to make signing the default from early on, then make it mandatory once the tooling is proven. -**Implementation note:** the service can rely on existing Sigstore client -libraries rather than inventing its own signing stack. The registry still verifies signatures on publish so invalid artifacts are rejected early. +**Implementation note:** the service can rely on existing Sigstore client libraries rather than inventing its own signing stack. The registry still verifies signatures on publish so invalid artifacts are rejected early. ### Security properties @@ -446,8 +445,7 @@ The important design constraint is portability: 1. the service must be able to run on EU-hosted infrastructure with hard cost caps; 2. object storage and CDN choices should remain replaceable; -3. the registry protocol must not depend on whether the service is implemented -in Incan or in a temporary bootstrap implementation. +3. the registry protocol must not depend on whether the service is implemented in Incan or in a temporary bootstrap implementation. ## Interaction with existing features @@ -482,8 +480,7 @@ German provider, good pricing. However: no scale-to-zero compute (minimum €4.5 - **Complexity.** A package registry is a significant piece of infrastructure to build and maintain, even a simple one. - **Dependency on Scaleway/Bunny.net.** The architecture is provider-portable (S3 API + HTTP CDN), but the initial deployment is tied to these specific providers. - **Sigstore learning curve.** Keyless signing via OIDC is unfamiliar to many developers. Clear documentation and good CLI UX can mitigate this. -- **Bootstrap dependency.** A first-class Incan implementation depends on the -relevant web/runtime capabilities shipping in time. A temporary bootstrap implementation may be needed if ecosystem timing forces the registry to arrive earlier. +- **Bootstrap dependency.** A first-class Incan implementation depends on the relevant web/runtime capabilities shipping in time. A temporary bootstrap implementation may be needed if ecosystem timing forces the registry to arrive earlier. ## Implementation architecture diff --git a/workspaces/docs-site/docs/RFCs/037_native_web_stdlib_redesign.md b/workspaces/docs-site/docs/RFCs/037_native_web_stdlib_redesign.md index 673871810..962dbf1d0 100644 --- a/workspaces/docs-site/docs/RFCs/037_native_web_stdlib_redesign.md +++ b/workspaces/docs-site/docs/RFCs/037_native_web_stdlib_redesign.md @@ -31,11 +31,9 @@ The current `std.web` proves that Incan can compile web programs, but it does no Current problems: -- Users still encounter backend leakage such as explicit wrapper types and -backend-oriented handoff details. +- Users still encounter backend leakage such as explicit wrapper types and backend-oriented handoff details. - Routing behavior is split across compiler logic, macros, and runtime helpers. -- The platform does not yet present one complete framework story for auth, -middleware, validation, docs, and lifecycle. +- The platform does not yet present one complete framework story for auth, middleware, validation, docs, and lifecycle. The goal is not just "web works." The goal is that Incan developers can expose networked applications and APIs with one coherent server-side mental model. @@ -141,8 +139,7 @@ Those may be refined later if needed, but the end-state described here is the ta 1. **FastAPI-first server UX:** the primary serving experience is `App`-owned, decorator-driven, and typed. 2. **Complete server platform scope:** serving, auth, validation, middleware, lifecycle, and docs are all part of the intended platform, not optional afterthoughts. -3. **No backend leakage in public APIs:** users should not have to think in -backend-runtime terms for ordinary API work. +3. **No backend leakage in public APIs:** users should not have to think in backend-runtime terms for ordinary API work. 4. **One platform, multiple surfaces:** FastAPI-style APIs, Django-style organization, and DSLs must reduce to one coherent underlying model. 5. **Library ownership over compiler ownership:** framework behavior belongs in stdlib/libraries; compiler support should remain primitive and general where possible. 6. **Client compatibility without client ownership:** `std.web` should align with RFC 066 where concepts overlap, but must not own the `std.http` implementation schedule. diff --git a/workspaces/docs-site/docs/RFCs/093_std_telemetry_opentelemetry_observability.md b/workspaces/docs-site/docs/RFCs/093_std_telemetry_opentelemetry_observability.md index be3d90932..448fc0e0a 100644 --- a/workspaces/docs-site/docs/RFCs/093_std_telemetry_opentelemetry_observability.md +++ b/workspaces/docs-site/docs/RFCs/093_std_telemetry_opentelemetry_observability.md @@ -151,11 +151,7 @@ def authorize(req: Request) -> Result[Auth, Error]: return gateway.authorize(req) ``` -Telemetry APIs should therefore stay partial-friendly: prefer named parameters, expose semantic-convention helpers as -plain functions that return `Attributes`, and allow configured decorator callables to be named and reused. Partials are -not a separate telemetry abstraction; they are the language-level way to preset ordinary callable surfaces. Top-level -partials still follow RFC 084's declaration-safe preset rules, so dynamic values and function references belong in local -partials, wrapper functions, or decorator application arguments rather than top-level partial presets. +Telemetry APIs should therefore stay partial-friendly: prefer named parameters, expose semantic-convention helpers as plain functions that return `Attributes`, and allow configured decorator callables to be named and reused. Partials are not a separate telemetry abstraction; they are the language-level way to preset ordinary callable surfaces. Top-level partials still follow RFC 084's declaration-safe preset rules, so dynamic values and function references belong in local partials, wrapper functions, or decorator application arguments rather than top-level partial presets. ### Block-level spans use vocabulary syntax diff --git a/workspaces/docs-site/docs/RFCs/TEMPLATE.md b/workspaces/docs-site/docs/RFCs/TEMPLATE.md index ef3ec10ad..11de4333b 100644 --- a/workspaces/docs-site/docs/RFCs/TEMPLATE.md +++ b/workspaces/docs-site/docs/RFCs/TEMPLATE.md @@ -15,8 +15,7 @@ RFC NNN: \ - Deferred: Implementation is deferred to a later time. - Done: Implementation is complete. - Superseded by RFC NNN: This RFC is superseded by RFC NNN. -- Rejected: This RFC is rejected. - --> +- Rejected: This RFC is rejected. --> - **Status:** Draft - **Created:** \ diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/000_core_rfc.md b/workspaces/docs-site/docs/RFCs/closed/implemented/000_core_rfc.md index 04d829329..7b6958c8f 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/000_core_rfc.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/000_core_rfc.md @@ -1,8 +1,13 @@ # RFC 000: Incan Core Language RFC (Phase 1) -**Status:** Done -**Created:** 2024-11-26 -**Issue:** [#50](https://github.com/dannys-code-corner/incan/pull/50) +- **Status:** Implemented +- **Created:** 2024-11-26 +- **Author(s):** Danny Meijer (@dannymeijer) +- **Related:** — +- **Issue:** — +- **RFC PR:** [#50](https://github.com/dannys-code-corner/incan/pull/50) +- **Written against:** v0.1 +- **Shipped in:** v0.1 This RFC consolidates the core semantics decisions for Incan's first implementation phase. diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/005_rust_interop.md b/workspaces/docs-site/docs/RFCs/closed/implemented/005_rust_interop.md index 737118ba9..143c9f048 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/005_rust_interop.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/005_rust_interop.md @@ -21,8 +21,7 @@ This RFC tightens the contract so we avoid “it looks like Rust” leakage and - common ownership/borrow friction (especially `str` vs `&str`) is handled without Rust syntax in user code - limitations are stated up front (interop is powerful but not “everything in crates.io just works”) -Dependency pinning and lockfiles are specified by [RFC 013]. -Cargo policy enforcement (`--offline/--locked/--frozen`) and generated-project persistence are specified by [RFC 020]. +Dependency pinning and lockfiles are specified by [RFC 013]. Cargo policy enforcement (`--offline/--locked/--frozen`) and generated-project persistence are specified by [RFC 020]. Prime directive: interop must not force users to learn Rust borrowing/lifetimes/traits at the surface level! @@ -36,8 +35,7 @@ Prime directive: interop must not force users to learn Rust borrowing/lifetimes/ ## Non-Goals -- A promise that “any Rust crate works”. Interop is scoped; outside that scope, failures are expected but must be - diagnosable. +- A promise that “any Rust crate works”. Interop is scoped; outside that scope, failures are expected but must be diagnosable. - Exposing Rust surface syntax in Incan (`&`, `&mut`, lifetimes, turbofish `::`). - Arbitrary proc-macros. (Incan may support a curated derive surface via `@derive`; see below.) - Calling `unsafe` Rust items without an explicit Incan opt-in (out of scope for this RFC). @@ -125,8 +123,7 @@ If `crate_name` is `std`, then: Reserved (out of scope for this RFC): -- If `crate_name` is `core` or `alloc`, the compiler must emit a compile-time error instructing the user to use - `rust::std::...` instead (or wait for future `no_std` / target support). +- If `crate_name` is `core` or `alloc`, the compiler must emit a compile-time error instructing the user to use `rust::std::...` instead (or wait for future `no_std` / target support). ### Crate naming limitations (normative) @@ -145,12 +142,9 @@ from rust::wasm_bindgen import prelude Note: -- Cargo/crates.io normalize `-` and `_` in crate names, so `wasm-bindgen` is correctly resolved when referenced as - `wasm_bindgen` in generated Rust code and dependency keys. -- The generated Cargo dependency key uses the exact `crate_name` spelling from the `rust::` import (the underscore/Rust- - identifier form). -- Explicit package↔crate mapping is only needed for non-trivial mismatches (e.g. `package = "..."` with a different crate - name), and should live in RFC 013’s `incan.toml` dependency specification rather than in the `rust::` import syntax. +- Cargo/crates.io normalize `-` and `_` in crate names, so `wasm-bindgen` is correctly resolved when referenced as `wasm_bindgen` in generated Rust code and dependency keys. +- The generated Cargo dependency key uses the exact `crate_name` spelling from the `rust::` import (the underscore/Rust- identifier form). +- Explicit package↔crate mapping is only needed for non-trivial mismatches (e.g. `package = "..."` with a different crate name), and should live in RFC 013’s `incan.toml` dependency specification rather than in the `rust::` import syntax. ### Type mapping (normative) @@ -167,8 +161,7 @@ Interop uses deterministic core type mapping: Numeric note: -- Rust integer widths other than `i64` (e.g. `usize`, `u128`) are not implicitly mapped to `int`. - Conversions must be explicit (e.g. via a builtin like `int(...)`) or handled by a dedicated adapter. +- Rust integer widths other than `i64` (e.g. `usize`, `u128`) are not implicitly mapped to `int`. Conversions must be explicit (e.g. via a builtin like `int(...)`) or handled by a dedicated adapter. ### Borrowing and string conversion rules (normative) @@ -177,17 +170,10 @@ Incan does not expose Rust borrowing syntax. To make common Rust APIs usable (especially those taking `&str`), the compiler applies: - string literals used where an owned string is required are lowered with `.to_string()` -- when calling an **external Rust function** (imported via `rust::...`), an argument expression of Incan type `str` is - lowered as a **borrowed string view** (`&str`) by default (implemented by borrowing the underlying `String`, e.g. - `value.as_str()` / `&value` on the Rust side). This makes the common Rust API pattern “takes `&str`” ergonomic without - exposing Rust syntax in user code. -- **Forcing an owned string**: if user code syntactically constructs an owned string expression via `.to_string()` (e.g. - `value.to_string()`), the compiler must treat that argument as **owned** and pass it by value (a clone), rather than - applying the default borrow lowering. -- This RFC does not require Rust signature inspection to choose between `&str` vs `String`. The rule is purely based on - the Incan argument expression shape (default: borrowed view; explicit `.to_string()`: owned clone). -- if a Rust interop call fails due to a `String`/`&str` mismatch for an argument originating from an Incan `str`, - the compiler must emit a targeted diagnostic pointing at the argument expression and suggesting either: +- when calling an **external Rust function** (imported via `rust::...`), an argument expression of Incan type `str` is lowered as a **borrowed string view** (`&str`) by default (implemented by borrowing the underlying `String`, e.g. `value.as_str()` / `&value` on the Rust side). This makes the common Rust API pattern “takes `&str`” ergonomic without exposing Rust syntax in user code. +- **Forcing an owned string**: if user code syntactically constructs an owned string expression via `.to_string()` (e.g. `value.to_string()`), the compiler must treat that argument as **owned** and pass it by value (a clone), rather than applying the default borrow lowering. +- This RFC does not require Rust signature inspection to choose between `&str` vs `String`. The rule is purely based on the Incan argument expression shape (default: borrowed view; explicit `.to_string()`: owned clone). +- if a Rust interop call fails due to a `String`/`&str` mismatch for an argument originating from an Incan `str`, the compiler must emit a targeted diagnostic pointing at the argument expression and suggesting either: - add `.to_string()` to force passing an owned `String` (clone), or - remove `.to_string()` / pass the value directly so the compiler can pass a borrowed view (`&str`) (default). @@ -195,8 +181,7 @@ Scope: - this RFC requires borrow/ownership adaptation for **strings** (the most common interop mismatch) - general borrow inference for arbitrary Rust types is out of scope -- rust signature inspection (e.g. via rustdoc/rust-analyzer metadata) and compile‑retry ‘guessing’ strategies to auto-fix - borrow/ownership mismatches are out of scope for this RFC. +- rust signature inspection (e.g. via rustdoc/rust-analyzer metadata) and compile‑retry ‘guessing’ strategies to auto-fix borrow/ownership mismatches are out of scope for this RFC. ### Calling model: methods vs associated functions (normative) @@ -205,16 +190,13 @@ Incan uses a single dot-call syntax at the surface for both methods and associat Lowering rules: - If the receiver is a **value**, `value.method(args...)` lowers to a Rust method call: `value.method(args...)`. -- If the receiver resolves to a **type-like identifier** (an Incan type name or an imported Rust type), then - `Type.method(args...)` lowers to a Rust associated function call: `Type::method(args...)`. +- If the receiver resolves to a **type-like identifier** (an Incan type name or an imported Rust type), then `Type.method(args...)` lowers to a Rust associated function call: `Type::method(args...)`. -This is why the examples below use `Instant.now()` and `Uuid.new_v4()` even though the corresponding Rust spelling is -`Instant::now()` / `Uuid::new_v4()`. +This is why the examples below use `Instant.now()` and `Uuid.new_v4()` even though the corresponding Rust spelling is `Instant::now()` / `Uuid::new_v4()`. ### Derives, traits, and serde (normative direction) -Many Rust APIs require trait bounds (e.g. `HashMap` keys require `Eq + Hash`; `serde_json` requires `Serialize` / -`Deserialize`). +Many Rust APIs require trait bounds (e.g. `HashMap` keys require `Eq + Hash`; `serde_json` requires `Serialize` / `Deserialize`). Incan’s user-facing mechanism for this is the `@derive(...)` decorator (not Rust proc-macro syntax). @@ -226,33 +208,27 @@ Requirement: - `Debug`, `Clone`, `Eq`, `Hash` - `Serialize`, `Deserialize` (to make `serde_json` usable on Incan models) -This is intentionally **not** “arbitrary proc-macros”: the derive set is curated and wired into the compiler/runtime -contract. +This is intentionally **not** “arbitrary proc-macros”: the derive set is curated and wired into the compiler/runtime contract. Implementation model note (important for determinism): -- Even with a curated `@derive(...)` list, the implementation may emit Rust `#[derive(...)]` for those traits and thus - execute Rust proc-macros at build time (e.g. serde derives). -- This is acceptable only in combination with locked/pinned dependency resolution ([RFC 013]) and reproducible/offline - build policy controls ([RFC 020]). +- Even with a curated `@derive(...)` list, the implementation may emit Rust `#[derive(...)]` for those traits and thus execute Rust proc-macros at build time (e.g. serde derives). +- This is acceptable only in combination with locked/pinned dependency resolution ([RFC 013]) and reproducible/offline build policy controls ([RFC 020]). - The curated derive list is part of Incan’s compatibility contract (versioned, documented, and stable-by-default). ### Panic/unwind and error policy (normative) -Rust interop compiles into a single Rust program (generated code + dependencies). This is **not** an `extern "C"` [^extern-c] -FFI (Foreign Function Interface) boundary. +Rust interop compiles into a single Rust program (generated code + dependencies). This is **not** an `extern "C"` [^extern-c] FFI (Foreign Function Interface) boundary. [^extern-c]: `extern "C"` selects the C ABI/calling convention for interop with C. It matters because unwinding (panics) - across a real `extern "C"` boundary is not allowed; in Incan interop we generate one Rust program, so this is a normal - Rust-to-Rust call path, not an FFI boundary. + across a real `extern "C"` boundary is not allowed; in Incan interop we generate one Rust program, so this is a normal Rust-to-Rust call path, not an FFI boundary. **Policy**: - Rust `Result`/`Option` values map to Incan `Result`/`Option` and work with `?`/pattern matching as usual. - Rust panics behave like panics in generated Rust code: - by default they terminate the program/test (panic semantics are Rust-defined) - - implementations should ensure the error output clearly indicates “this was a Rust panic” and includes enough context - (crate/function if available) to debug + - implementations should ensure the error output clearly indicates “this was a Rust panic” and includes enough context (crate/function if available) to debug - catching panics and converting them into Incan runtime errors is a possible future extension, but out of scope here ### Unsafe policy (normative) @@ -260,8 +236,7 @@ FFI (Foreign Function Interface) boundary. Calling `unsafe` Rust items is out of scope for this RFC. - The compiler must not generate Rust `unsafe { ... }` on behalf of user code. -- Therefore, Rust APIs that require `unsafe` are unsupported and should produce a clear, targeted diagnostic explaining - that “unsafe interop is out of scope” (even if the underlying trigger originates from Rust compilation). +- Therefore, Rust APIs that require `unsafe` are unsupported and should produce a clear, targeted diagnostic explaining that “unsafe interop is out of scope” (even if the underlying trigger originates from Rust compilation). - A future RFC may introduce an explicit `unsafe` block/marker in Incan (and an associated safety policy). ### Diagnostics expectations (normative) @@ -313,53 +288,41 @@ def parse_user_data(json_str: str) -> Result[UserData, JsonError]: Rationale (why these limits exist): -- Arbitrary proc-macros are effectively “run arbitrary Rust at build time”; they undermine determinism, portability, - and the “Incan stays Incan” surface. A curated `@derive(...)` set keeps the interop contract explicit and reviewable. -- Trait-heavy and lifetime-heavy Rust APIs often require expressing trait bounds and borrowing/lifetimes at call - sites; trait bound inference and explicit annotation syntax are addressed by RFC 023 (Compilable Stdlib & Rust Module - Binding). Lifetime/borrow surface beyond strings is deferred to a future RFC. -- Incan’s goal is to remove Rust’s borrow-checker ergonomics from user code. The compiler may adapt borrows internally - (currently scoped mainly to strings), but users should not be forced to write Rust-like lifetime/borrow annotations. +- Arbitrary proc-macros are effectively “run arbitrary Rust at build time”; they undermine determinism, portability, and the “Incan stays Incan” surface. A curated `@derive(...)` set keeps the interop contract explicit and reviewable. +- Trait-heavy and lifetime-heavy Rust APIs often require expressing trait bounds and borrowing/lifetimes at call sites; trait bound inference and explicit annotation syntax are addressed by RFC 023 (Compilable Stdlib & Rust Module Binding). Lifetime/borrow surface beyond strings is deferred to a future RFC. +- Incan’s goal is to remove Rust’s borrow-checker ergonomics from user code. The compiler may adapt borrows internally (currently scoped mainly to strings), but users should not be forced to write Rust-like lifetime/borrow annotations. ## Design decisions 1. **Non-trivial package↔crate mapping** - Decision: keep `rust::` imports crate-identifier-only; represent non-trivial package renames in `incan.toml` - (`package = "..."` / dependency aliasing), not in import syntax. +Decision: keep `rust::` imports crate-identifier-only; represent non-trivial package renames in `incan.toml` (`package = "..."` / dependency aliasing), not in import syntax. - Reason: this preserves a simple and deterministic language surface while aligning with RFC 013 ownership of Cargo - dependency modeling. +Reason: this preserves a simple and deterministic language surface while aligning with RFC 013 ownership of Cargo dependency modeling. 2. **Borrow adaptation scope beyond strings** - Decision: RFC 005 stays string-focused (`str` ergonomics only). No general non-string borrow inference and no Rust - signature inspection in this RFC. +Decision: RFC 005 stays string-focused (`str` ergonomics only). No general non-string borrow inference and no Rust signature inspection in this RFC. - Reason: broader adaptation requires high-complexity type/signature analysis and risks unpredictable behavior; - string-only adaptation captures the dominant interop friction at low complexity. +Reason: broader adaptation requires high-complexity type/signature analysis and risks unpredictable behavior; string-only adaptation captures the dominant interop friction at low complexity. - Follow-up tracking: incremental non-string ownership/borrow improvements are tracked by issue #121. +Follow-up tracking: incremental non-string ownership/borrow improvements are tracked by issue #121. 3. **Panic handling policy for Rust interop** - Decision: preserve Rust panic semantics in RFC 005 (no catch-and-convert runtime boundary in this RFC). +Decision: preserve Rust panic semantics in RFC 005 (no catch-and-convert runtime boundary in this RFC). - Reason: generated programs are Rust-to-Rust call paths, and preserving native panic behavior avoids hidden control-flow - changes. Future opt-in conversion can be introduced in a dedicated RFC. +Reason: generated programs are Rust-to-Rust call paths, and preserving native panic behavior avoids hidden control-flow changes. Future opt-in conversion can be introduced in a dedicated RFC. 4. **Non-native target behavior (`wasm32`, etc.)** - Decision: target-specific interop constraints are out of RFC 005 scope and must be specified in target/toolchain RFCs - (e.g., [RFC 092](../../092_interactive_runtime_stdlib_contracts.md) or a dedicated target-constraints RFC). +Decision: target-specific interop constraints are out of RFC 005 scope and must be specified in target/toolchain RFCs (e.g., [RFC 092](../../092_interactive_runtime_stdlib_contracts.md) or a dedicated target-constraints RFC). - Reason: interop validity is target-dependent (runtime availability, crate support, panic model), so policy belongs in - the target model rather than the base Rust interop contract. +Reason: interop validity is target-dependent (runtime availability, crate support, panic model), so policy belongs in the target model rather than the base Rust interop contract. ## Appendix: `crate::...` absolute module paths for Incan modules (normative) -`crate::...` is a Rust-style spelling for **Incan module paths** (project-root absolute imports). It is **not** related -to `rust::...` (Rust crate imports). +`crate::...` is a Rust-style spelling for **Incan module paths** (project-root absolute imports). It is **not** related to `rust::...` (Rust crate imports). ```incan import crate::config as cfg diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/008_const_bindings.md b/workspaces/docs-site/docs/RFCs/closed/implemented/008_const_bindings.md index c3799f389..78ade5a98 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/008_const_bindings.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/008_const_bindings.md @@ -1,9 +1,13 @@ # RFC 008: Const Bindings -**Status:** Done -**Created:** 2025-12-10 -**Issue:** [#12](https://github.com/dannys-code-corner/incan/pull/12) -**Implemented:** 2025-12-23 +- **Status:** Implemented +- **Created:** 2025-12-10 +- **Author(s):** Danny Meijer (@dannymeijer) +- **Related:** RFC 000 (core language) +- **Issue:** — +- **RFC PR:** [#12](https://github.com/dannys-code-corner/incan/pull/12) +- **Written against:** v0.1 +- **Shipped in:** v0.1 ## Summary diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/011_fstring_error_spans.md b/workspaces/docs-site/docs/RFCs/closed/implemented/011_fstring_error_spans.md index b0c28a3cd..f5ecbd481 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/011_fstring_error_spans.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/011_fstring_error_spans.md @@ -9,8 +9,7 @@ ## Summary -Improve error messages for f-string interpolation expressions to point to the specific `{expr}` that caused the error, -rather than the entire f-string. +Improve error messages for f-string interpolation expressions to point to the specific `{expr}` that caused the error, rather than the entire f-string. ## Motivation diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/013_rust_crate_dependencies.md b/workspaces/docs-site/docs/RFCs/closed/implemented/013_rust_crate_dependencies.md index 087bce656..51032d735 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/013_rust_crate_dependencies.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/013_rust_crate_dependencies.md @@ -1,21 +1,25 @@ # RFC 013: Rust Crate Dependencies -**Status:** Implemented -**Created:** 2025-12-16 -**Issue:** [#72](https://github.com/dannys-code-corner/incan/issues/72) -**Author(s):** Danny Meijer (@danny-meijer) -**Supersedes:** Parts of RFC 005 (Cargo integration section) -**Related:** RFC 015 (project lifecycle + `incan.toml`), RFC 020 (Cargo offline/locked policy) +- **Status:** Implemented +- **Created:** 2025-12-16 +- **Author(s):** Danny Meijer (@dannymeijer) +- **Supersedes:** Parts of RFC 005 (Cargo integration section) +- **Related:** + - RFC 005 (Rust interop) + - RFC 015 (project lifecycle + `incan.toml`) + - RFC 020 (Cargo offline/locked policy) +- **Issue:** [#72](https://github.com/dannys-code-corner/incan/issues/72) +- **RFC PR:** — +- **Written against:** v0.1 +- **Shipped in:** v0.2 ## Summary -Define a comprehensive system for specifying Rust crate dependencies in Incan, including inline version annotations, -project configuration (`incan.toml`), and lock files for reproducibility. +Define a comprehensive system for specifying Rust crate dependencies in Incan, including inline version annotations, project configuration (`incan.toml`), and lock files for reproducibility. ## Motivation -Incan compiles to Rust, meaning access to the Rust ecosystem is a core value proposition. -The current implementation (v0.1) has limitations: +Incan compiles to Rust, meaning access to the Rust ecosystem is a core value proposition. The current implementation (v0.1) has limitations: 1. **Known-good crates work** - common crates like `serde`, `tokio` have curated defaults 2. **Unknown crates error** - no user-facing way to specify version/features @@ -63,8 +67,7 @@ import rust::exact_crate @ "=1.2.3" # exactly 1.2.3 #### 1.1.1 Version requirement syntax (normative) -All version requirement strings in this RFC use **Cargo’s SemVer requirement syntax** (the same syntax accepted by Cargo -in `Cargo.toml`). +All version requirement strings in this RFC use **Cargo’s SemVer requirement syntax** (the same syntax accepted by Cargo in `Cargo.toml`). - Shorthand `"1.2.3"` is equivalent to caret `"^1.2.3"`. - Comma-separated constraints (e.g. `">=1.30, <2.0"`) are allowed. @@ -100,16 +103,14 @@ import_list = IDENT { "," IDENT } ; Normative note (interop alignment): -- A `rust::...` import may include a module path (e.g. `rust::chrono::naive::date`), but dependency resolution is always - keyed by the **crate segment** (the first identifier after `rust::`), per RFC 005’s crate/path decomposition rules. +- A `rust::...` import may include a module path (e.g. `rust::chrono::naive::date`), but dependency resolution is always keyed by the **crate segment** (the first identifier after `rust::`), per RFC 005’s crate/path decomposition rules. - Therefore, `@ "..."` / `with [...]` always apply to that crate segment, not to the module path. --- ## 2. Project Configuration (`incan.toml`) -We propose to add a central configuration file for Incan projects called `incan.toml`. The `incan.toml` format is heavily -inspired by Python's `pyproject.toml` - familiar, readable, and declarative. +We propose to add a central configuration file for Incan projects called `incan.toml`. The `incan.toml` format is heavily inspired by Python's `pyproject.toml` - familiar, readable, and declarative. Schema note (normative): @@ -118,17 +119,13 @@ Schema note (normative): - **`[dev-dependencies]`** for test-only dependencies (bench tooling is a future extension) - Compatibility/aliasing (for this RFC): - `[rust.dependencies]` / `[rust.dev-dependencies]` are accepted as aliases for `[dependencies]` / `[dev-dependencies]`. - - It is a configuration error to specify both the canonical and alias tables for the same kind (e.g. both - `[dependencies]` and `[rust.dependencies]`). -- RFC 015 defines the broader project lifecycle and treats `incan.toml` as the project metadata root; it must not define - a separate, conflicting table for dependency policy. + - It is a configuration error to specify both the canonical and alias tables for the same kind (e.g. both `[dependencies]` and `[rust.dependencies]`). +- RFC 015 defines the broader project lifecycle and treats `incan.toml` as the project metadata root; it must not define a separate, conflicting table for dependency policy. Clarity note (future-proofing): -- In this RFC, `[dependencies]` / `[dev-dependencies]` refer specifically to **Cargo/Rust crates** used by `rust::...` - imports. -- Incan does not yet define a separate “Incan package registry” concept. If a future packaging story is added, it must - not overload these tables in a way that breaks Rust dependency determinism. +- In this RFC, `[dependencies]` / `[dev-dependencies]` refer specifically to **Cargo/Rust crates** used by `rust::...` imports. +- Incan does not yet define a separate “Incan package registry” concept. If a future packaging story is added, it must not overload these tables in a way that breaks Rust dependency determinism. Recommended practice (strongly suggested): @@ -209,11 +206,8 @@ full = ["json", "fancy_logging"] Note: - `[project.features]` is part of the general Incan project model (RFC 015). It is included here for context only. -- This RFC does **not** specify an automatic mapping between `[project.features]` and Rust crate features/optional - dependencies. Any such mapping should be specified by RFC 015 (or a dedicated follow-up), so that Cargo feature - semantics are not implicitly re-invented in multiple places. -- `[build]` is also part of the general Incan project model (RFC 015). This RFC references it only for completeness in - the “full example”; dependency resolution and `incan.lock` do not depend on build settings like `rust-edition`. +- This RFC does **not** specify an automatic mapping between `[project.features]` and Rust crate features/optional dependencies. Any such mapping should be specified by RFC 015 (or a dedicated follow-up), so that Cargo feature semantics are not implicitly re-invented in multiple places. +- `[build]` is also part of the general Incan project model (RFC 015). This RFC references it only for completeness in the “full example”; dependency resolution and `incan.lock` do not depend on build settings like `rust-edition`. ### 2.3 Section Reference @@ -253,11 +247,9 @@ crate_h = { version = "1.0", default-features = false, features = ["only-this"] ### 2.4.1 Dependency renames (`package = "..."`) (normative) -Cargo supports depending on a package whose crates.io/package name differs from the identifier you want to use in code via -dependency renaming (`package = "..."`). +Cargo supports depending on a package whose crates.io/package name differs from the identifier you want to use in code via dependency renaming (`package = "..."`). -In Incan, `rust::...` imports refer to the **dependency key** (the Rust identifier form), which is also the name used in -generated Rust code. +In Incan, `rust::...` imports refer to the **dependency key** (the Rust identifier form), which is also the name used in generated Rust code. Supported (for this RFC): @@ -271,14 +263,11 @@ serde_json = { package = "serde-json", version = "1.0" } - The `rust::` import must use the dependency key (`rust::serde_json` in the example above). - If `package` is specified, it must be a non-empty string. -- It is a configuration error to specify two dependencies that would resolve to the same `(source, package)` pair with - different `crate_name` keys. +- It is a configuration error to specify two dependencies that would resolve to the same `(source, package)` pair with different `crate_name` keys. Note: -- For the common case where the crates.io package name differs only by `-` vs `_`, Cargo/crates.io name normalization is - typically sufficient. Using `package = "..."` is still allowed (and may improve clarity), and is required for - non-trivial mismatches beyond `-`/`_` normalization. +- For the common case where the crates.io package name differs only by `-` vs `_`, Cargo/crates.io name normalization is typically sufficient. Using `package = "..."` is still allowed (and may improve clarity), and is required for non-trivial mismatches beyond `-`/`_` normalization. Out of scope (for this RFC): @@ -294,8 +283,7 @@ Incan supports Cargo-style optional dependencies via the standard Cargo fields: Enablement model (for this RFC): - This RFC does **not** define an Incan-native feature system that automatically toggles Cargo optional dependencies. -- Optional dependency enablement is controlled by **Cargo features** (Cargo semantics), surfaced to users via RFC 020’s - `--cargo-args` escape hatch (e.g. `--cargo-args "--features" "fancy_logging"`), and/or a future RFC 015 mapping. +- Optional dependency enablement is controlled by **Cargo features** (Cargo semantics), surfaced to users via RFC 020’s `--cargo-args` escape hatch (e.g. `--cargo-args "--features" "fancy_logging"`), and/or a future RFC 015 mapping. `[dependencies.optional]` table: @@ -304,10 +292,8 @@ Enablement model (for this RFC): Cargo project generation (normative): -- Optional dependencies in `incan.toml` must be emitted as Cargo optional dependencies in the generated `Cargo.toml` - (`optional = true` on the dependency entry). -- The generated Cargo package must expose a Cargo feature with the **same name as the dependency key** that enables the - optional dependency (using Cargo’s namespaced dependency feature form): +- Optional dependencies in `incan.toml` must be emitted as Cargo optional dependencies in the generated `Cargo.toml` (`optional = true` on the dependency entry). +- The generated Cargo package must expose a Cargo feature with the **same name as the dependency key** that enables the optional dependency (using Cargo’s namespaced dependency feature form): ```toml [dependencies] @@ -317,18 +303,15 @@ fancy_logging = { version = "0.3", optional = true } fancy_logging = ["dep:fancy_logging"] ``` -This avoids relying on “implicit optional-dependency features,” which are edition/toolchain-sensitive and can create -confusing behavior. +This avoids relying on “implicit optional-dependency features,” which are edition/toolchain-sensitive and can create confusing behavior. Locking note (normative): -- Because Cargo feature selection can change the resolved dependency graph, strict builds (`--locked` / `--frozen`) must - ensure that the **feature selection used for the build/test** matches the one that was used to generate `incan.lock`. +- Because Cargo feature selection can change the resolved dependency graph, strict builds (`--locked` / `--frozen`) must ensure that the **feature selection used for the build/test** matches the one that was used to generate `incan.lock`. Diagnostics expectation (normative): -- If user code imports an optional dependency but the required Cargo feature(s) are not enabled for the build/test, the - toolchain must produce a targeted diagnostic explaining: +- If user code imports an optional dependency but the required Cargo feature(s) are not enabled for the build/test, the toolchain must produce a targeted diagnostic explaining: - the crate is optional, - which Cargo feature name is expected (typically the dependency key), - and how to enable it (e.g. via RFC 020’s `--cargo-args "--features" ""`, or via a future RFC 015 mapping). @@ -339,15 +322,12 @@ Incan supports Rust **development dependencies** for test tooling that must not #### 2.5.1 What they are -- `[dev-dependencies]` has the **same specification formats** as `[dependencies]` (version string shorthand or table form - with `version`, `features`, `git`, `path`, `default-features`, etc.). -- A crate listed in `[dev-dependencies]` is **dev-only**: it is available only in test contexts and must not be - used by production code. +- `[dev-dependencies]` has the **same specification formats** as `[dependencies]` (version string shorthand or table form with `version`, `features`, `git`, `path`, `default-features`, etc.). +- A crate listed in `[dev-dependencies]` is **dev-only**: it is available only in test contexts and must not be used by production code. #### 2.5.2 Where dev-dependencies may be used (import gating) -Crates that are only present in `[dev-dependencies]` may be imported via `rust::...` **only** from test contexts as -defined by the testing RFCs: +Crates that are only present in `[dev-dependencies]` may be imported via `rust::...` **only** from test contexts as defined by the testing RFCs: - test files under `tests/` (`test_*.incn` / `*_test.incn`) (RFC 019), and - inline `module tests:` blocks inside production source files (RFC 018/019). @@ -404,8 +384,7 @@ Design goal: - `incan.lock` lives at the **project root** and is intended to be **committed** to version control. - `incan.lock` is the **source of truth** for what “locked” means in Incan. -- In locked modes, tooling must never silently change dependency resolution; any drift must be a hard error with a clear - instruction to run `incan lock`. +- In locked modes, tooling must never silently change dependency resolution; any drift must be a hard error with a clear instruction to run `incan lock`. #### 3.0.1 Project mode vs single-file mode (normative) @@ -420,12 +399,10 @@ Rules: - In **project mode**, strict flags (`--locked` / `--frozen`, RFC 020) are enforced against `incan.lock`: - `incan.lock` must exist and be up to date (fingerprint matches), otherwise error with "run `incan lock`". - - Cargo is invoked with the corresponding Cargo policy flags (RFC 020), and `Cargo.lock` is materialized from - `incan.lock` into the generated project directory. + - Cargo is invoked with the corresponding Cargo policy flags (RFC 020), and `Cargo.lock` is materialized from `incan.lock` into the generated project directory. - In **single-file mode**, there is no `incan.lock`: - Rust dependencies may only be specified via inline annotations (`@ "..."` / `with [...]`) and/or known-good defaults. - - Strict flags (`--locked` / `--frozen`) are interpreted at the Cargo layer only (RFC 020) using the generated - project’s `Cargo.lock` as the lock artifact. + - Strict flags (`--locked` / `--frozen`) are interpreted at the Cargo layer only (RFC 020) using the generated project’s `Cargo.lock` as the lock artifact. ### 3.1 Format @@ -484,8 +461,7 @@ Notes: ### 3.1.1 Canonicalization and `deps-fingerprint` (normative) -The canonicalization rules for the embedded Cargo lock payload, and the full `deps-fingerprint` algorithm, are specified -in **Appendix A**. +The canonicalization rules for the embedded Cargo lock payload, and the full `deps-fingerprint` algorithm, are specified in **Appendix A**. ### 3.2 CLI Commands @@ -500,10 +476,8 @@ Note (normative): Default behavior (normative; uv-inspired): -- If `incan.lock` is missing and no strict policy flag is set, `incan build` / `incan test` may resolve dependencies and - create `incan.lock` automatically (first-run convenience). -- If a strict policy flag is set (`--locked` or `--frozen` per RFC 020), `incan.lock` must already exist; otherwise the - command must fail with a targeted diagnostic instructing the user to run `incan lock`. +- If `incan.lock` is missing and no strict policy flag is set, `incan build` / `incan test` may resolve dependencies and create `incan.lock` automatically (first-run convenience). +- If a strict policy flag is set (`--locked` or `--frozen` per RFC 020), `incan.lock` must already exist; otherwise the command must fail with a targeted diagnostic instructing the user to run `incan lock`. > Note: Cargo-level offline/locked enforcement flags (`--offline/--locked/--frozen`) are specified by RFC 020. This RFC > defines the Incan-level dependency and lockfile model; Cargo policy is a separate concern. @@ -518,19 +492,16 @@ These flags have a **combined** effect: Rules: - `incan build/test --locked`: - - **Incan lock layer**: `incan.lock` must exist and be **up to date** (its `deps-fingerprint` matches the current - effective dependency inputs). If not, fail with “run `incan lock`”. + - **Incan lock layer**: `incan.lock` must exist and be **up to date** (its `deps-fingerprint` matches the current effective dependency inputs). If not, fail with “run `incan lock`”. - **Cargo policy layer**: Incan must invoke Cargo with `--locked`. - `incan build/test --frozen`: - **Incan lock layer**: same as `--locked` (lock must exist and be up to date). - - **Cargo policy layer**: Incan must invoke Cargo with `--frozen` (equivalent to Cargo `--offline --locked`, per RFC - 020). + - **Cargo policy layer**: Incan must invoke Cargo with `--frozen` (equivalent to Cargo `--offline --locked`, per RFC 020). Rationale: - `--frozen` is the **strictest** mode and should not allow stale locks. -- The difference between `--locked` and `--frozen` is the **offline** constraint at the Cargo layer, not whether lock - freshness is checked. +- The difference between `--locked` and `--frozen` is the **offline** constraint at the Cargo layer, not whether lock freshness is checked. #### 3.2.2 `incan lock` and restricted environments (normative direction) @@ -538,24 +509,18 @@ Rationale: Normative requirements: -- Strict flags `--locked/--frozen` are not meaningful for `incan lock` and should be rejected with a clear diagnostic - (including when set via CI environment variables). +- Strict flags `--locked/--frozen` are not meaningful for `incan lock` and should be rejected with a clear diagnostic (including when set via CI environment variables). - `incan lock` must accept the same Cargo policy sources as other commands (RFC 020 precedence rules): - CLI flags: `--offline` and `--cargo-args ...` - Environment variables: `INCAN_OFFLINE=1` and `INCAN_CARGO_ARGS="..."` -- `incan lock` must forward the relevant policy to the underlying Cargo operations it uses for resolution (e.g. it must - honor `--offline` when set). -- In offline mode, lock generation must fail if the required dependency sources are not already available locally (Cargo - cache, mirror, or vendor directory). This is expected behavior and should surface Cargo’s error with a short prefix - clarifying that offline policy caused the failure. +- `incan lock` must forward the relevant policy to the underlying Cargo operations it uses for resolution (e.g. it must honor `--offline` when set). +- In offline mode, lock generation must fail if the required dependency sources are not already available locally (Cargo cache, mirror, or vendor directory). This is expected behavior and should surface Cargo’s error with a short prefix clarifying that offline policy caused the failure. Recommended workflows (informative; enterprise-friendly): -- **Cache priming**: run `incan lock` once without offline policy in a controlled environment to populate registries/git - caches, then use `--frozen` in CI. -- **Mirrors**: use Cargo registry mirrors / sparse index configuration via `.cargo/config.toml` (generated-project - compatible) to avoid public network dependencies. +- **Cache priming**: run `incan lock` once without offline policy in a controlled environment to populate registries/git caches, then use `--frozen` in CI. +- **Mirrors**: use Cargo registry mirrors / sparse index configuration via `.cargo/config.toml` (generated-project compatible) to avoid public network dependencies. - **Vendoring**: prefer a `cargo vendor`-style workflow for fully air-gapped builds (future extension; RFC 020). ### 3.3 Relationship to `Cargo.lock` (normative) @@ -565,13 +530,11 @@ Cargo uses `Cargo.lock` as its lockfile. Rules: - `incan.lock` is the project’s committed lockfile and contains an embedded `Cargo.lock` payload. -- `Cargo.lock` files written into generated Rust output directories are **derived artifacts** produced by extracting the - embedded payload from `incan.lock`. +- `Cargo.lock` files written into generated Rust output directories are **derived artifacts** produced by extracting the embedded payload from `incan.lock`. - In locked modes, Incan must ensure the embedded lock matches the current dependency inputs: - `--locked`: lock must exist and be **up to date** (fingerprint matches); otherwise fail and instruct `incan lock`. - `--frozen`: same as `--locked` for freshness; additionally enforce Cargo frozen/offline policy (RFC 020). -- Cargo policy flags still apply to the underlying Cargo invocation (RFC 020), but the authoritative lock artifact is - `incan.lock` at the project root. +- Cargo policy flags still apply to the underlying Cargo invocation (RFC 020), but the authoritative lock artifact is `incan.lock` at the project root. ### 3.4 Design inspiration (informative): Hatch vs uv @@ -597,8 +560,7 @@ Note (normative): ### 4.1 Known-Good Defaults -The compiler maintains a curated list of common crates with tested version/feature combinations. -These serve as **convenient defaults**, not restrictions: +The compiler maintains a curated list of common crates with tested version/feature combinations. These serve as **convenient defaults**, not restrictions: ```rust // In compiler (simplified) @@ -620,12 +582,10 @@ Normative governance: User-facing contract (strongly recommended): -- The toolchain must publish a documented list of “known-good crates” (and their default version/features) per release - (e.g. in release notes and/or a dedicated docs page). +- The toolchain must publish a documented list of “known-good crates” (and their default version/features) per release (e.g. in release notes and/or a dedicated docs page). - Changes to known-good defaults are allowed, but they must be treated as compatibility-relevant: - security updates may require updating defaults, - - and toolchain upgrades may require re-locking for projects that rely on defaults (because defaults participate in - `deps-fingerprint`). + - and toolchain upgrades may require re-locking for projects that rely on defaults (because defaults participate in `deps-fingerprint`). - If a project wants stability across toolchain upgrades, it should pin versions/features explicitly in `incan.toml`. ### 4.2 Conflict Resolution @@ -656,14 +616,12 @@ The resolver collects all crate requirements across a project before invoking Ca Definitions: -- A **crate spec** is the resolved dependency request for a single Rust crate (version requirement, source, features, - and `default-features` policy). +- A **crate spec** is the resolved dependency request for a single Rust crate (version requirement, source, features, and `default-features` policy). Rules: - **Single source of truth when `incan.toml` exists**: - - If a crate is specified in `incan.toml` (either `[dependencies]` or `[dev-dependencies]`, including their `rust.*` - aliases), inline annotations for that crate are a **compile-time error**. + - If a crate is specified in `incan.toml` (either `[dependencies]` or `[dev-dependencies]`, including their `rust.*` aliases), inline annotations for that crate are a **compile-time error**. - Rationale: avoid scattering dependency policy throughout code; keep audits and reviews centralized. - **Multiple inline sites (no `incan.toml` entry)**: - Version requirement strings must match exactly across all inline sites for a given crate (for this RFC). @@ -671,18 +629,14 @@ Rules: - Features are **unioned** across all sites. - `default-features` must be consistent across all sites; mismatches are an error. -Future extensions may loosen the “inline is error when `incan.toml` exists” rule (e.g. allow inline *feature adds* only), -but within the scope of this RFC, it should remain strict. +Future extensions may loosen the “inline is error when `incan.toml` exists” rule (e.g. allow inline *feature adds* only), but within the scope of this RFC, it should remain strict. ### 4.4 Source policy (security + reproducibility) (normative direction) Git and path dependencies are supported, but reproducible builds require additional constraints: -- In locked/CI mode, git dependencies must resolve to an **exact commit** (e.g. `rev = "..."` or an immutable tag that is - recorded as a commit in `incan.lock`). Floating `branch = "..."` is not reproducible and should be rejected in strict - mode. -- Path dependencies are inherently machine-local unless vendored; locked/CI mode may reject them unless explicitly - allowlisted by configuration (future RFC 020 / RFC 015 integration). +- In locked/CI mode, git dependencies must resolve to an **exact commit** (e.g. `rev = "..."` or an immutable tag that is recorded as a commit in `incan.lock`). Floating `branch = "..."` is not reproducible and should be rejected in strict mode. +- Path dependencies are inherently machine-local unless vendored; locked/CI mode may reject them unless explicitly allowlisted by configuration (future RFC 020 / RFC 015 integration). --- @@ -861,8 +815,7 @@ Fix: keep only one key for this package/source, or use a different package/sourc Optional escape hatch (non-default; not required by this RFC): -- If an “unpinned dependencies” mode exists for local prototyping, it must be **explicitly enabled** (e.g. via a CLI - flag) and must produce a visibly non-reproducible build warning suitable for CI policy enforcement. +- If an “unpinned dependencies” mode exists for local prototyping, it must be **explicitly enabled** (e.g. via a CLI flag) and must produce a visibly non-reproducible build warning suitable for CI policy enforcement. ### Phase 2: Features Support @@ -981,8 +934,7 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc 1. **Workspace support**: Should `incan.toml` support workspaces with multiple packages? 2. **Private registries**: How to authenticate with private Cargo registries? -3. **Auto-update tooling**: Dependency update behavior (an `incan update` command) is intentionally out of scope for this - RFC and should be specified in a dedicated future RFC (likely alongside project lifecycle tooling). +3. **Auto-update tooling**: Dependency update behavior (an `incan update` command) is intentionally out of scope for this RFC and should be specified in a dedicated future RFC (likely alongside project lifecycle tooling). --- @@ -1002,8 +954,7 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [x] Parser: `with ["features"]` syntax - [x] Resolution: features are unioned across sites per merging rules - [x] Codegen: emit features in generated Cargo metadata -- [x] Error: unknown feature — **deferred to Cargo**: feature validation requires querying crate registry - metadata, which Incan does not currently do. Invalid features are caught by Cargo during the build step. +- [x] Error: unknown feature — **deferred to Cargo**: feature validation requires querying crate registry metadata, which Incan does not currently do. Invalid features are caught by Cargo during the build step. ### Implementing Phase 3: Project Configuration @@ -1016,18 +967,15 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [x] CLI: `incan init` command - [x] Resolution: apply precedence rules (incan.toml > inline > known-good > error) - [x] Known-good defaults: apply only when there is no `incan.toml` spec (canonical or alias) and no inline annotation -- [x] Rule: if a crate is specified in incan.toml (canonical or alias tables), inline annotations for that crate are a - compile-time error -- [x] Dev-dependencies gating: crates that are only in `[dev-dependencies]` are only allowed in test contexts (RFC 018/019) - and error in production code +- [x] Rule: if a crate is specified in incan.toml (canonical or alias tables), inline annotations for that crate are a compile-time error +- [x] Dev-dependencies gating: crates that are only in `[dev-dependencies]` are only allowed in test contexts (RFC 018/019) and error in production code - [x] Error: version/source/default-features conflicts across sites (per §4.2/§4.3) - [x] Multiple inline sites (no incan.toml entry): enforce this RFC’s merge rules: - [x] version requirement strings must match exactly across inline sites - [x] source must match across inline sites (inline imports are always registry) - [x] `default-features` must match across inline sites (inline imports always use default) - [x] features are unioned across inline sites -- [x] Diagnostics: conflicting specs and resolution failures produce actionable errors that point to all relevant locations - (e.g. inline import site(s) + `incan.toml`), and suggest the concrete fix +- [x] Diagnostics: conflicting specs and resolution failures produce actionable errors that point to all relevant locations (e.g. inline import site(s) + `incan.toml`), and suggest the concrete fix ### Implementing Phase 4: Lock File @@ -1035,14 +983,11 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [x] `incan.lock` format (container + embedded Cargo.lock payload): - [x] `[incan]` metadata (`format`, `incan-version`, `generated`, `deps-fingerprint`, cargo feature selection) - [x] `[cargo].lock` verbatim embedded `Cargo.lock` payload -- [x] `deps-fingerprint` computation fingerprints dependency inputs and Cargo feature selection, and excludes non-dependency - settings like `[build].rust-edition` -- [x] Default behavior: if `incan.lock` is missing and no strict policy flag is set, builds/tests may generate it - (first-run convenience) +- [x] `deps-fingerprint` computation fingerprints dependency inputs and Cargo feature selection, and excludes non-dependency settings like `[build].rust-edition` +- [x] Default behavior: if `incan.lock` is missing and no strict policy flag is set, builds/tests may generate it (first-run convenience) - [x] Strict behavior (uv-style; see RFC 020 for flags): - [x] `--locked`: `incan.lock` must exist and be up-to-date (fingerprint matches) or fail with “run incan lock” - - [x] `--frozen`: `incan.lock` must exist and be up-to-date (fingerprint matches); use it and also enforce Cargo - `--frozen` policy (offline + locked) + - [x] `--frozen`: `incan.lock` must exist and be up-to-date (fingerprint matches); use it and also enforce Cargo `--frozen` policy (offline + locked) - [x] Diagnostics: missing/out-of-date lock failures are targeted and instruct the user to run `incan lock` - [x] Materialization: embedded `Cargo.lock` is written as `Cargo.lock` into generated build/test Cargo project directories - [x] Ensure dev-dependencies are represented so `incan test --locked` is meaningful @@ -1057,10 +1002,8 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc ### Error messages (section 5) - [x] 5.1: Unknown crate without version -- [x] 5.2: Feature not found -- **deferred to Cargo** (requires registry queries; invalid features are caught by Cargo - during the build step) -- [x] 5.3: Version not found -- **deferred to Cargo** (requires registry queries; invalid versions are caught by Cargo - during the build step) +- [x] 5.2: Feature not found -- **deferred to Cargo** (requires registry queries; invalid features are caught by Cargo during the build step) +- [x] 5.3: Version not found -- **deferred to Cargo** (requires registry queries; invalid versions are caught by Cargo during the build step) - [x] 5.4: Lock out of date (includes fingerprint details and explanation) - [x] 5.5: Optional dependency not enabled - [x] 5.6: Inline annotation forbidden (crate in incan.toml) @@ -1080,8 +1023,7 @@ To avoid cross-platform churn and ensure deterministic `incan.lock` files: - The embedded `[cargo].lock` payload must be stored as **UTF-8** text. - Newlines must be normalized to `\n` (LF). `\r\n` must not appear in the embedded payload. - The payload must end with a trailing newline. -- The payload content is otherwise treated as an opaque string (verbatim Cargo output); Incan does not attempt to - reformat or “pretty print” it. +- The payload content is otherwise treated as an opaque string (verbatim Cargo output); Incan does not attempt to reformat or “pretty print” it. ### A.2 `deps-fingerprint` algorithm @@ -1116,8 +1058,7 @@ Canonical representation: `specs`: -- Build a list of entries `Spec { crate_name, kind, source, version_req, default_features, features, optional, package }` - where: +- Build a list of entries `Spec { crate_name, kind, source, version_req, default_features, features, optional, package }` where: - `crate_name` is the dependency key used by `rust::crate_name` imports (Rust identifier form). - `kind` is `"normal"` or `"dev"`. - `source` is one of: @@ -1136,14 +1077,11 @@ Canonical representation: - `optional` is a boolean (missing defaults to `false`). - `package` is either absent or a string (see §2.4.1 for renames). - Sort the list by `(kind, crate_name)` where `kind` sorts `"normal"` before `"dev"`. -- Serialize the canonical object into canonical JSON (UTF-8, no whitespace, deterministic key order) and compute: - `deps-fingerprint = "sha256:" + hex(sha256(json_bytes))`. +- Serialize the canonical object into canonical JSON (UTF-8, no whitespace, deterministic key order) and compute: `deps-fingerprint = "sha256:" + hex(sha256(json_bytes))`. Stability note: -- Because the fingerprint is computed from the **effective** crate specs, toolchain-provided known-good defaults become - part of the fingerprint for projects that rely on defaults. Changing the toolchain may therefore require re-locking, - which is expected when you are not fully pinned by explicit `incan.toml` configuration. +- Because the fingerprint is computed from the **effective** crate specs, toolchain-provided known-good defaults become part of the fingerprint for projects that rely on defaults. Changing the toolchain may therefore require re-locking, which is expected when you are not fully pinned by explicit `incan.toml` configuration. --- @@ -1151,8 +1089,7 @@ Stability note: This design is inspired by the developer experience of modern Python tooling: -- Tools like Hatch focus on project workflow and environment management; reproducible installs are often achieved via a - separate lock mechanism or plugin ecosystem. +- Tools like Hatch focus on project workflow and environment management; reproducible installs are often achieved via a separate lock mechanism or plugin ecosystem. - Tools like Astral’s `uv` popularized a clear separation between: - “lock” (resolve and write a lockfile), and - “sync/run” (use the lockfile in strict modes). @@ -1160,7 +1097,6 @@ This design is inspired by the developer experience of modern Python tooling: Incan adopts the same mental model: - `incan lock` produces/refreshes `incan.lock` -- `--locked` / `--frozen` (RFC 020) control the Cargo policy mode (`--locked` vs `--frozen`). - In both cases, Incan requires a project-root `incan.lock` that is present and up to date (fingerprint matches). +- `--locked` / `--frozen` (RFC 020) control the Cargo policy mode (`--locked` vs `--frozen`). In both cases, Incan requires a project-root `incan.lock` that is present and up to date (fingerprint matches). RFC 015 provides more details on the project model and lifecycle. diff --git a/workspaces/docs-site/docs/RFCs/closed/implemented/015_hatch_like_tooling.md b/workspaces/docs-site/docs/RFCs/closed/implemented/015_hatch_like_tooling.md index 2f0859373..19d8741a1 100644 --- a/workspaces/docs-site/docs/RFCs/closed/implemented/015_hatch_like_tooling.md +++ b/workspaces/docs-site/docs/RFCs/closed/implemented/015_hatch_like_tooling.md @@ -29,8 +29,7 @@ Incan is a compiler + runtime ecosystem, but day-to-day developer experience is - Starting a new project should be **one command**. - Bumping versions should be **correct and consistent** across project metadata, derived artifacts, and any package metadata. -- Running tests should support **repeatable environments** and **matrix execution**, without forcing users to learn Cargo -internals. +- Running tests should support **repeatable environments** and **matrix execution**, without forcing users to learn Cargo internals. - Release workflows should be **scriptable** and **standard** across projects. Python’s Hatch demonstrates that a single tool can cover the project lifecycle. This RFC adapts the useful parts to Incan. @@ -53,8 +52,7 @@ Python’s Hatch demonstrates that a single tool can cover the project lifecycle ## Terminology - **Project**: An Incan repository containing Incan sources and metadata. -- **Environment**: A named configuration overlay for repeatable command execution (`cwd`, `env-vars`, scripts, -dependency overlays). +- **Environment**: A named configuration overlay for repeatable command execution (`cwd`, `env-vars`, scripts, dependency overlays). - **Matrix**: Running an environment set across multiple dimensions (e.g., debug/release, features on/off). ## Project Metadata @@ -80,13 +78,11 @@ serde = { version = "1.0", features = ["derive"] } Notes: -- `[tool.incan]` may contain additional tool-specific configuration (e.g., formatter settings, test timeouts). -These are defined by their respective RFCs (e.g., RFC 019 for test configuration) and are not specified here. +- `[tool.incan]` may contain additional tool-specific configuration (e.g., formatter settings, test timeouts). These are defined by their respective RFCs (e.g., RFC 019 for test configuration) and are not specified here. - `version` is SemVer-compatible with pre-release tags. - Rust dependencies integrate with RFC 013 rules. - `incan.toml` is the **project metadata** and is intended to be edited. -- Generated build artifacts under `target/` are readable for debugging, but are **not** intended for manual editing -(RFC 020). +- Generated build artifacts under `target/` are readable for debugging, but are **not** intended for manual editing (RFC 020). ### `[project]` schema (normative) @@ -167,8 +163,7 @@ tokio = { version = "1", features = ["rt-multi-thread", "macros"] } This RFC now defines project-aware `incan run` behavior for the default `main` script. Bare `incan run` may resolve `[project.scripts].main` when no file path is provided. -Note: `[project.scripts]` maps script names to `.incn` entrypoint paths. This is distinct from -`[tool.incan.envs..scripts]` (defined below), which maps script names to shell command argv lists for env execution. +Note: `[project.scripts]` maps script names to `.incn` entrypoint paths. This is distinct from `[tool.incan.envs..scripts]` (defined below), which maps script names to shell command argv lists for env execution. --- @@ -206,8 +201,7 @@ Override: `incan build ` may operate in either mode: - In **project mode**, project-level dependencies and strict lock semantics apply (RFC 013/020). -- In **single-file mode**, dependency configuration is limited to inline annotations and known-good defaults (RFC 013), -and strict flags operate on the generated project’s `Cargo.lock` (RFC 020). +- In **single-file mode**, dependency configuration is limited to inline annotations and known-good defaults (RFC 013), and strict flags operate on the generated project’s `Cargo.lock` (RFC 020). ## CLI Design @@ -311,8 +305,7 @@ Default test runner entrypoint. Behavior: - `incan test` runs the Incan test runner as specified by RFC 019 (project-neutral behavior). -- Cargo policy flags (`--offline/--locked/--frozen`) must be propagated consistently to any Cargo subprocesses, -as per RFC 020. +- Cargo policy flags (`--offline/--locked/--frozen`) must be propagated consistently to any Cargo subprocesses, as per RFC 020. This RFC intentionally does **not** define repo-maintainer workflows for the Incan compiler repository (e.g. “run all workspace Rust tests”); those are out of scope for user-facing tooling semantics. @@ -322,13 +315,11 @@ Flags: ### `incan env` -`incan env` provides a small “task/env runner” layer for repeatable commands, without changing the semantics of core -commands like `incan test`. +`incan env` provides a small “task/env runner” layer for repeatable commands, without changing the semantics of core commands like `incan test`. Core command stability (normative): -- Core commands like `incan test` are **not configurable** via `incan.toml`. Configuration is applied only when the user -explicitly uses `incan env run ...` or `incan env show ...`. +- Core commands like `incan test` are **not configurable** via `incan.toml`. Configuration is applied only when the user explicitly uses `incan env run ...` or `incan env show ...`. Core shape: @@ -364,8 +355,7 @@ docs_build = ["python3", "-m", "mkdocs", "build", "-q"] Normative behavior: -- `incan env list` must output all configured env names. In `text` mode, one env name per line. In `json` mode, a JSON -array of env names. +- `incan env list` must output all configured env names. In `text` mode, one env name per line. In `json` mode, a JSON array of env names. - `incan env show ` must resolve env inheritance and merging using the same rules as `incan env run` and then print: - resolved overlay chain (base → default? → extends… → env) - resolved `cwd` @@ -373,13 +363,10 @@ array of env names. - resolved scripts (and the final argv for each script) - resolved dependency overlays (base + env additions/overrides) - `--format` controls output format; if omitted, `text` is used. -- `incan env run ...` executes the configured script **without** any further env selection/indirection. In particular, -invoking `incan test` inside an env script must run the test runner directly and must not “re-enter” env resolution. -- Implementations must prevent accidental recursive self-invocation (e.g. an env script calling `incan env run ...` in a -way that would re-resolve the same env). If recursion is detected, the command must fail with a clear diagnostic. +- `incan env run ...` executes the configured script **without** any further env selection/indirection. In particular, invoking `incan test` inside an env script must run the test runner directly and must not “re-enter” env resolution. +- Implementations must prevent accidental recursive self-invocation (e.g. an env script calling `incan env run ...` in a way that would re-resolve the same env). If recursion is detected, the command must fail with a clear diagnostic. - `--` separates `incan env run` arguments from additional user arguments passed through to the underlying command. -- There are no implicit lifecycle hooks (e.g. no automatic `pre*`/`post*` script execution). Only the explicitly-invoked - `