From 5e6599af693a08d9c9767c2032012dc77594e152 Mon Sep 17 00:00:00 2001 From: Danny Meijer Date: Wed, 20 May 2026 22:07:29 +0200 Subject: [PATCH 1/4] bugfix - fix v0.3 release blockers (#542, #610, #612) --- CONTRIBUTING.md | 26 +- Cargo.lock | 13 - README.md | 6 +- .../incan_core/src/lang/surface/functions.rs | 2 +- crates/incan_core/src/lang/types/mod.rs | 2 +- crates/incan_stdlib/Cargo.toml | 1 - crates/incan_stdlib/README.md | 13 +- crates/incan_stdlib/src/web.rs | 2 +- crates/incan_stdlib/stdlib/collections.incn | 33 +-- crates/rust_inspect/README.md | 3 +- examples/README.md | 8 +- .../pro/vocab_querykit/consumer/incan.lock | 12 +- .../pro/vocab_querykit/producer/incan.lock | 10 +- .../pro/vocab_routekit/consumer/incan.lock | 12 +- .../pro/vocab_routekit/producer/incan.lock | 10 +- .../pro/vocab_studiokit/consumer/incan.lock | 12 +- .../pro/vocab_studiokit/producer/incan.lock | 10 +- src/cli/commands/common.rs | 13 +- src/cli/test_runner/module_graph.rs | 84 ++++++- src/format/mod.rs | 2 +- src/frontend/DEVNOTES.md | 3 +- src/frontend/typechecker/check_expr/access.rs | 25 +- src/frontend/typechecker/tests.rs | 87 +++++++ src/frontend/vocab_desugar_pass/runtime.rs | 4 +- src/lsp/call_site_type_args.rs | 8 +- src/project_lifecycle/toolchain.rs | 4 +- tests/cli_integration.rs | 76 ++++++ .../docs-site/docs/RFCs/033_ctx_keyword.md | 66 ++---- .../docs/RFCs/034_incan_pub_registry.md | 3 +- ...d_telemetry_opentelemetry_observability.md | 6 +- .../RFCs/closed/implemented/000_core_rfc.md | 4 +- .../closed/implemented/005_rust_interop.md | 83 +++---- .../closed/implemented/008_const_bindings.md | 5 +- .../implemented/011_fstring_error_spans.md | 3 +- .../013_rust_crate_dependencies.md | 137 +++++------ .../implemented/015_hatch_like_tooling.md | 13 +- .../RFCs/closed/implemented/018_testing.md | 27 +-- .../020_offline_locked_reproducible_builds.md | 6 +- .../021_model_field_metadata_and_aliases.md | 150 +++++------- ...stdlib_namespacing_and_compiler_handoff.md | 132 +++++------ ...mpilable_stdlib_and_rust_module_binding.md | 224 ++++++------------ .../024_extensible_derive_protocol.md | 19 +- .../043_rust_trait_impl_from_incan.md | 14 +- .../047_lightweight_directed_graph_stdlib.md | 16 +- .../RFCs/closed/implemented/056_std_io.md | 5 +- .../closed/implemented/061_std_compression.md | 6 +- .../closed/implemented/072_std_logging.md | 72 ++---- .../closed/superseded/001_test_fixtures.md | 4 +- .../closed/superseded/002_test_parametrize.md | 4 +- .../closed/superseded/003_frontend_wasm.md | 12 +- .../closed/superseded/007_inline_tests.md | 7 +- .../026_user_defined_trait_bridges.md | 22 +- .../compiler_pipeline_checklist.md | 5 +- .../docs-site/docs/comparisons/index.md | 12 +- .../docs-site/docs/comparisons/python.md | 14 +- .../comparisons/python_compatibility_tools.md | 16 +- workspaces/docs-site/docs/comparisons/rust.md | 12 +- .../contributing/explanation/architecture.md | 48 ++-- .../contributing/explanation/duckborrowing.md | 6 +- .../docs/contributing/explanation/layering.md | 12 +- .../explanation/readable-maintainable-rust.md | 24 +- .../explanation/stdlib_testing_internals.md | 21 +- .../contributing/how-to/ci_and_automation.md | 6 +- .../contributing/how-to/extending_language.md | 47 ++-- .../docs/contributing/how-to/writing_rfcs.md | 7 +- .../tutorials/book/01_architecture_tour.md | 3 +- .../book/02_layering_and_boundaries.md | 3 +- .../book/03_proposals_issues_vs_rfcs.md | 24 +- .../tutorials/book/04_add_a_builtin.md | 6 +- .../tutorials/book/07_tooling_loop_lsp.md | 3 +- .../book/08_docsite_contributor_loop.md | 6 +- .../docs/contributing/tutorials/book/index.md | 3 +- workspaces/docs-site/docs/index.md | 3 +- .../explanation/call_site_type_arguments.md | 6 +- .../docs/language/explanation/consts.md | 9 +- .../explanation/derives_and_traits.md | 21 +- .../docs/language/explanation/enums.md | 3 +- .../language/explanation/error_handling.md | 9 +- .../language/explanation/how_derives_work.md | 11 +- .../explanation/imports_and_modules.md | 8 +- .../explanation/models_and_classes/index.md | 5 +- .../explanation/models_and_classes/models.md | 28 +-- .../explanation/scopes_and_name_resolution.md | 22 +- .../language/explanation/static_storage.md | 9 +- .../explanation/traits_as_language_hooks.md | 9 +- .../docs/language/explanation/why_incan.md | 9 +- .../language/explanation/why_not_just_rust.md | 13 +- .../docs/language/how-to/async_programming.md | 35 +-- .../language/how-to/binary_text_encoding.md | 29 +-- .../docs/language/how-to/compression.md | 39 +-- .../language/how-to/error_handling_recipes.md | 6 +- .../docs/language/how-to/error_messages.md | 3 +- .../language/how-to/imports_and_modules.md | 3 +- .../language/how-to/modeling_with_enums.md | 5 +- .../docs/language/how-to/performance.md | 8 +- .../docs/language/how-to/rust_interop.md | 24 +- .../how-to/rust_types_for_python_devs.md | 6 +- .../docs/language/reference/code_style.md | 24 +- .../reference/derives/serialization.md | 16 +- .../language/reference/derives/validation.md | 5 +- .../language/reference/derives_and_traits.md | 11 +- .../docs/language/reference/functions.md | 17 +- .../docs/language/reference/glossary.md | 10 +- .../language/reference/imports_and_modules.md | 18 +- .../docs/language/reference/index.md | 6 +- .../docs/language/reference/language.md | 18 +- .../docs/language/reference/newtypes.md | 15 +- .../docs/language/reference/static_storage.md | 3 +- .../docs/language/reference/stdlib/async.md | 12 +- .../language/reference/stdlib/compression.md | 28 +-- .../docs/language/reference/stdlib/derives.md | 6 +- .../language/reference/stdlib/encoding.md | 3 +- .../docs/language/reference/stdlib/graph.md | 59 ++--- .../docs/language/reference/stdlib/io.md | 6 +- .../docs/language/reference/stdlib/json.md | 3 +- .../docs/language/reference/stdlib/logging.md | 40 +--- .../docs/language/reference/stdlib/math.md | 3 +- .../language/reference/stdlib/reflection.md | 6 +- .../docs/language/reference/stdlib/regex.md | 35 +-- .../language/reference/stdlib/tempfile.md | 4 +- .../docs/language/reference/stdlib/traits.md | 3 +- .../reference/stdlib_traits/awaitable.md | 6 +- .../stdlib_traits/collection_protocols.md | 3 +- .../language/reference/stdlib_traits/index.md | 3 +- .../stdlib_traits/indexing_and_slicing.md | 4 +- .../reference/stdlib_traits/operators.md | 4 +- .../docs/language/reference/symbol_aliases.md | 3 +- .../language/tutorials/book/01_hello_world.md | 3 +- .../language/tutorials/book/03_functions.md | 12 +- .../tutorials/book/05_modules_and_imports.md | 6 +- .../docs/language/tutorials/book/06_errors.md | 2 +- .../tutorials/book/10_models_vs_classes.md | 10 +- .../tutorials/book/11_traits_and_derives.md | 6 +- .../language/tutorials/book/12_newtypes.md | 6 +- .../language/tutorials/book/13_unit_tests.md | 2 +- .../docs/language/tutorials/web_framework.md | 8 +- .../docs-site/docs/release_notes/0_1.md | 5 +- .../docs-site/docs/release_notes/0_3.md | 80 ++----- workspaces/docs-site/docs/roadmap.md | 3 +- .../docs-site/docs/start_here/beginner.md | 2 +- .../start_here/pipelines_and_automation.md | 3 +- .../tooling/explanation/projects_today.md | 11 +- .../docs/tooling/how-to/ci_and_automation.md | 3 +- .../docs/tooling/how-to/dependencies.md | 21 +- .../docs/tooling/how-to/editor_setup.md | 6 +- .../docs/tooling/how-to/formatting.md | 22 +- .../docs/tooling/how-to/install_and_run.md | 2 +- .../docs-site/docs/tooling/how-to/testing.md | 6 +- .../docs/tooling/how-to/troubleshooting.md | 3 +- .../docs/tooling/reference/cli_reference.md | 10 +- .../tooling/reference/lsp_protocol_support.md | 3 +- .../reference/project_configuration.md | 9 +- .../docs/tooling/tutorials/getting_started.md | 5 +- .../tooling/tutorials/your_first_project.md | 16 +- 154 files changed, 1127 insertions(+), 1634 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d46f7646..5fd4c7a3f 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: @@ -57,9 +55,9 @@ 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. +`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. +`v`), so version bumps should not churn snapshot files. ### Code Generation Overview @@ -88,9 +86,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 +132,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 +178,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/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..b7b120aaf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -27,13 +27,13 @@ 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. +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. +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. +scoped operator-like glyphs. - `pro/vocab_studiokit` - runnable RFC 040 vocab companion example for workflow-shaped blocks and scoped fallback - glyphs. +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..ff3958adc 100644 --- a/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md +++ b/workspaces/docs-site/docs/RFCs/033_ctx_keyword.md @@ -86,13 +86,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 +127,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 +189,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 @@ -210,7 +204,7 @@ 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. +`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 @@ -234,16 +228,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. +`"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. @@ -305,25 +295,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 +324,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,15 +338,13 @@ 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 @@ -385,26 +366,19 @@ 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. +`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. +`{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. +`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. +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. +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. 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..870a31493 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 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/closed/implemented/000_core_rfc.md b/workspaces/docs-site/docs/RFCs/closed/implemented/000_core_rfc.md index 04d829329..cbc7a3d90 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,6 @@ # 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:** Done **Created:** 2024-11-26 **Issue:** [#50](https://github.com/dannys-code-corner/incan/pull/50) 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..dba8d55c5 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! @@ -37,7 +36,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. +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). @@ -126,7 +125,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). +`rust::std::...` instead (or wait for future `no_std` / target support). ### Crate naming limitations (normative) @@ -146,11 +145,11 @@ 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. +`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). +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. +name), and should live in RFC 013’s `incan.toml` dependency specification rather than in the `rust::` import syntax. ### Type mapping (normative) @@ -168,7 +167,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. +Conversions must be explicit (e.g. via a builtin like `int(...)`) or handled by a dedicated adapter. ### Borrowing and string conversion rules (normative) @@ -178,16 +177,13 @@ To make common Rust APIs usable (especially those taking `&str`), the compiler a - 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. +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. +`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). +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: +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). @@ -196,7 +192,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. +borrow/ownership mismatches are out of scope for this RFC. ### Calling model: methods vs associated functions (normative) @@ -206,15 +202,13 @@ 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...)`. +`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,21 +220,19 @@ 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). +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]). +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 @@ -261,7 +253,7 @@ 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). +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) @@ -314,52 +306,43 @@ 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. +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. +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. +(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..527048f81 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,6 @@ # 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:** Done **Created:** 2025-12-10 **Issue:** [#12](https://github.com/dannys-code-corner/incan/pull/12) **Implemented:** 2025-12-23 ## 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..d4da5fb98 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,14 @@ # 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 **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) ## 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 +56,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. @@ -101,15 +93,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. +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): @@ -121,14 +112,14 @@ Schema note (normative): - 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. +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. +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. +not overload these tables in a way that breaks Rust dependency determinism. Recommended practice (strongly suggested): @@ -210,10 +201,9 @@ 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. +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`. +the “full example”; dependency resolution and `incan.lock` do not depend on build settings like `rust-edition`. ### 2.3 Section Reference @@ -253,11 +243,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): @@ -272,13 +260,12 @@ 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. +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. +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): @@ -295,7 +282,7 @@ 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. +`--cargo-args` escape hatch (e.g. `--cargo-args "--features" "fancy_logging"`), and/or a future RFC 015 mapping. `[dependencies.optional]` table: @@ -305,9 +292,9 @@ 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). +(`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 dependency (using Cargo’s namespaced dependency feature form): ```toml [dependencies] @@ -317,18 +304,17 @@ 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`. +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: +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). @@ -340,14 +326,13 @@ 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.). +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. +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). @@ -405,7 +390,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`. +instruction to run `incan lock`. #### 3.0.1 Project mode vs single-file mode (normative) @@ -484,8 +469,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 @@ -501,9 +485,9 @@ 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). +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`. +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. @@ -530,7 +514,7 @@ 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. +freshness is checked. #### 3.2.2 `incan lock` and restricted environments (normative direction) @@ -539,23 +523,22 @@ 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). +(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). +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. +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. +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. +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) @@ -566,12 +549,12 @@ 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`. +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. +`incan.lock` at the project root. ### 3.4 Design inspiration (informative): Hatch vs uv @@ -597,8 +580,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) @@ -621,7 +603,7 @@ 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). +(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 @@ -657,7 +639,7 @@ 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). +and `default-features` policy). Rules: @@ -671,18 +653,16 @@ 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. +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). +allowlisted by configuration (future RFC 020 / RFC 015 integration). --- @@ -862,7 +842,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. +flag) and must produce a visibly non-reproducible build warning suitable for CI policy enforcement. ### Phase 2: Features Support @@ -982,7 +962,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). +RFC and should be specified in a dedicated future RFC (likely alongside project lifecycle tooling). --- @@ -1003,7 +983,7 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [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. +metadata, which Incan does not currently do. Invalid features are caught by Cargo during the build step. ### Implementing Phase 3: Project Configuration @@ -1017,9 +997,9 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [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 +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 +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 @@ -1027,7 +1007,7 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [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 +(e.g. inline import site(s) + `incan.toml`), and suggest the concrete fix ### Implementing Phase 4: Lock File @@ -1036,9 +1016,9 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [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` +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) +(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 @@ -1058,9 +1038,9 @@ from rust::chrono @ "0.4" with ["serde", "clock"] import DateTime, Utc - [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) +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) +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) @@ -1081,7 +1061,7 @@ To avoid cross-platform churn and ensure deterministic `incan.lock` files: - 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. +reformat or “pretty print” it. ### A.2 `deps-fingerprint` algorithm @@ -1117,7 +1097,7 @@ Canonical representation: `specs`: - Build a list of entries `Spec { crate_name, kind, source, version_req, default_features, features, optional, package }` - where: +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: @@ -1137,13 +1117,12 @@ Canonical representation: - `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))`. +`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. +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. --- @@ -1152,7 +1131,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. +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). @@ -1161,6 +1140,6 @@ 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). +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..64ff79f34 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 @@ -167,8 +167,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. --- @@ -322,8 +321,7 @@ 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): @@ -379,7 +377,7 @@ invoking `incan test` inside an env script must run the test runner directly and 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 - `