diff --git a/Cargo.lock b/Cargo.lock index d3647fc3b..7437b1e9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1478,7 +1478,7 @@ dependencies = [ [[package]] name = "incan" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "blake2", "blake3", @@ -1527,14 +1527,14 @@ dependencies = [ [[package]] name = "incan_core" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "serde", ] [[package]] name = "incan_derive" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "proc-macro2", "quote", @@ -1543,14 +1543,14 @@ dependencies = [ [[package]] name = "incan_semantics_core" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "incan_core", ] [[package]] name = "incan_semantics_stdlib" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "incan_core", "incan_semantics_core", @@ -1558,7 +1558,7 @@ dependencies = [ [[package]] name = "incan_stdlib" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "axum", "incan_core", @@ -1573,7 +1573,7 @@ dependencies = [ [[package]] name = "incan_syntax" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "incan_core", "incan_semantics_core", @@ -1594,7 +1594,7 @@ dependencies = [ [[package]] name = "incan_web_macros" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "proc-macro2", "quote", @@ -3164,7 +3164,7 @@ dependencies = [ [[package]] name = "rust_inspect" -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" dependencies = [ "hex", "incan_core", diff --git a/Cargo.toml b/Cargo.toml index a1161f959..b62bde4ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ resolver = "2" ra_ap_proc_macro_api = { path = "crates/third_party/ra_ap_proc_macro_api" } [workspace.package] -version = "0.3.0-dev.49" +version = "0.3.0-dev.50" description = "The Incan programming language compiler" edition = "2024" rust-version = "1.92" diff --git a/Makefile b/Makefile index 0e62d461d..d1943a25a 100644 --- a/Makefile +++ b/Makefile @@ -246,6 +246,15 @@ test-rust-inspect: @cargo test --lib --features rust_inspect frontend::typechecker::tests::test_rust_inspect_unavailable_stays_permissive_for_method_calls @cargo test --lib --features rust_inspect frontend::typechecker::tests::test_rusttype_return_coercion_recorded_for_generic_newtype_method_call +.PHONY: generated-rust-audit-gate ## test - Run deterministic generated Rust audit helper checks +generated-rust-audit-gate: + @echo "\033[1mRunning generated Rust audit helper checks...\033[0m" + @cargo test --test generated_rust_audit_tests + @python3 scripts/generated_rust_audit.py --format json --fail-on-missing \ + --artifact program-main=tests/fixtures/generated_rust_audit/main.rs \ + --artifact stdlib-copy=tests/fixtures/generated_rust_audit/nested >/dev/null + @echo "\033[32m✓ Generated Rust audit helper checks passed\033[0m" + .PHONY: examples ## test - Smoke test examples (check all, run entrypoints with timeout) examples: release @echo "\033[1mRunning examples...\033[0m" diff --git a/scripts/README.md b/scripts/README.md index 6fc06e1df..699f0d901 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -4,8 +4,15 @@ Utility scripts for development and CI. ## Contents +- `generated_rust_audit.py`: Emits an objective generated Rust strict-surface report for selected generated `.rs` files or artifact directories. - `run_examples.sh`: Smoke-tests all examples. It pre-builds nested example library projects, typechecks every `.incn` file under `examples/`, and then runs files that define `def main(...)` with a configurable timeout. Invoked by `make examples`. +## Generated Rust audit helper + +Run `make generated-rust-audit-gate` after changing the audit helper. The target is deterministic: it uses committed fixtures under `tests/fixtures/generated_rust_audit/` and does not require preexisting `target/incan/` generated artifacts. + +For real generated artifacts, run `scripts/generated_rust_audit.py` with explicit `--artifact SURFACE_CLASS=PATH` entries. Repository-local paths are rendered relative to the repository root in reports. + ## Configuration `run_examples.sh` respects these environment variables: diff --git a/scripts/generated_rust_audit.py b/scripts/generated_rust_audit.py new file mode 100755 index 000000000..da3a82bb9 --- /dev/null +++ b/scripts/generated_rust_audit.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python3 +"""Emit a strict-surface audit report for generated Rust artifacts. + +The report is intentionally objective: it records artifact availability, +surface class, simple marker counts, and structured review placeholders. It +does not score generated Rust quality automatically. +""" + +from __future__ import annotations + +import argparse +import json +import sys +from dataclasses import asdict, dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + + +ROOT = Path(__file__).resolve().parents[1] + +DEFAULT_ARTIFACTS = ( + ("program-main", "target/incan/std_encoding_hex_surface/src/main.rs"), + ("stdlib-copy", "target/incan/std_ordinal_map_surface/src/__incan_std/collections.rs"), + ("surface-fixture", "target/incan/std_regex_surface/src"), +) + +MARKER_PATTERNS = { + "clone": (".clone(", ".to_owned("), + "allocation": ( + "Box::new(", + "BTreeMap::new(", + "HashMap::new(", + "HashSet::new(", + "String::from(", + "String::new(", + "Vec::new(", + "format!(", + "to_string(", + "vec![", + ), + "eager_collection": (".collect()", ".collect::<", "collect_vec("), +} + + +@dataclass(frozen=True) +class Marker: + """One literal marker found in generated Rust source.""" + + path: str + line: int + pattern: str + + +@dataclass(frozen=True) +class ReviewNote: + """Structured placeholder for manual audit notes.""" + + status: str + marker_count: int + markers: list[Marker] + notes: str + + +@dataclass(frozen=True) +class ArtifactReport: + """Audit report for one generated Rust artifact or artifact directory.""" + + surface_class: str + artifact_path: str + check_status: str + strictness_status: str + rust_files: list[str] + clone: ReviewNote + allocation: ReviewNote + eager_collection: ReviewNote + message: str + + +def relative(path: Path) -> str: + """Return `path` relative to the repository root when possible.""" + try: + return path.resolve().relative_to(ROOT).as_posix() + except ValueError: + return path.as_posix() + + +def artifact_specs(values: list[str]) -> list[tuple[str, Path]]: + """Parse `SURFACE_CLASS=PATH` artifact specs.""" + if not values: + return [(surface_class, ROOT / rel_path) for surface_class, rel_path in DEFAULT_ARTIFACTS] + + specs: list[tuple[str, Path]] = [] + for value in values: + if "=" not in value: + raise argparse.ArgumentTypeError( + f"artifact `{value}` must use SURFACE_CLASS=PATH form" + ) + surface_class, raw_path = value.split("=", 1) + surface_class = surface_class.strip() + raw_path = raw_path.strip() + if not surface_class: + raise argparse.ArgumentTypeError("artifact surface class cannot be empty") + if not raw_path: + raise argparse.ArgumentTypeError("artifact path cannot be empty") + path = Path(raw_path) + if not path.is_absolute(): + path = ROOT / path + specs.append((surface_class, path)) + return specs + + +def rust_files_for(path: Path) -> tuple[str, list[Path], str]: + """Return check status, Rust files, and a message for one artifact path.""" + if not path.exists(): + return "missing", [], "artifact path does not exist" + if path.is_file(): + if path.suffix == ".rs": + return "present", [path], "artifact file is available" + return "no-rust-files", [], "artifact file is not a Rust source file" + if path.is_dir(): + files = sorted(candidate for candidate in path.rglob("*.rs") if candidate.is_file()) + if files: + return "present", files, f"found {len(files)} Rust source file(s)" + return "no-rust-files", [], "artifact directory contains no Rust source files" + return "unsupported-path", [], "artifact path is neither a file nor a directory" + + +def find_markers(files: Iterable[Path], patterns: tuple[str, ...]) -> list[Marker]: + """Find literal marker occurrences in generated Rust source files.""" + markers: list[Marker] = [] + for path in files: + try: + lines = path.read_text(encoding="utf-8").splitlines() + except UnicodeDecodeError: + markers.append(Marker(path=relative(path), line=0, pattern="")) + continue + for line_number, line in enumerate(lines, start=1): + for pattern in patterns: + start = 0 + while (start := line.find(pattern, start)) != -1: + markers.append(Marker(path=relative(path), line=line_number, pattern=pattern)) + start += len(pattern) + return markers + + +def review_note(files: list[Path], marker_name: str, check_status: str) -> ReviewNote: + """Build the structured manual-review placeholder for one marker class.""" + if check_status != "present": + return ReviewNote( + status="not_available", + marker_count=0, + markers=[], + notes="artifact not available for manual review", + ) + markers = find_markers(files, MARKER_PATTERNS[marker_name]) + return ReviewNote( + status="pending_manual_review", + marker_count=len(markers), + markers=markers, + notes="", + ) + + +def audit_artifact(surface_class: str, path: Path) -> ArtifactReport: + """Build an objective audit report entry for one artifact.""" + check_status, files, message = rust_files_for(path) + strictness_status = "available_for_review" if check_status == "present" else "not_evaluated" + return ArtifactReport( + surface_class=surface_class, + artifact_path=relative(path), + check_status=check_status, + strictness_status=strictness_status, + rust_files=[relative(file) for file in files], + clone=review_note(files, "clone", check_status), + allocation=review_note(files, "allocation", check_status), + eager_collection=review_note(files, "eager_collection", check_status), + message=message, + ) + + +def json_report(reports: list[ArtifactReport]) -> str: + """Render reports as stable JSON.""" + payload = { + "report": "generated-rust-strict-surface", + "generated_at": datetime.now(timezone.utc).isoformat(), + "artifacts": [asdict(report) for report in reports], + } + return json.dumps(payload, indent=2, sort_keys=True) + "\n" + + +def marker_summary(note: ReviewNote) -> str: + """Render a compact marker summary for markdown tables.""" + if note.status == "not_available": + return "not available" + return f"{note.marker_count} marker(s); notes: pending" + + +def note_summary(note: ReviewNote) -> str: + """Render the manual-note placeholder text for markdown details.""" + return note.notes if note.notes else "pending" + + +def markdown_report(reports: list[ArtifactReport]) -> str: + """Render reports as Markdown for reviewer handoff.""" + lines = [ + "# Generated Rust Strict Surface Report", + "", + f"Generated at: `{datetime.now(timezone.utc).isoformat()}`", + "", + ( + "| Surface class | Artifact path | Check status | Strictness status | " + "Clone notes | Allocation notes | Eager collection notes |" + ), + ( + "| ------------- | ------------- | ------------ | ----------------- | " + "----------- | ---------------- | ---------------------- |" + ), + ] + for report in reports: + lines.append( + "| " + + " | ".join( + [ + f"`{report.surface_class}`", + f"`{report.artifact_path}`", + report.check_status, + report.strictness_status, + marker_summary(report.clone), + marker_summary(report.allocation), + marker_summary(report.eager_collection), + ] + ) + + " |" + ) + lines.extend(["", "## Artifact Details", ""]) + for report in reports: + lines.extend( + [ + f"### `{report.surface_class}`", + "", + f"- Artifact path: `{report.artifact_path}`", + f"- Check status: `{report.check_status}`", + f"- Strictness status: `{report.strictness_status}`", + f"- Message: {report.message}", + f"- Rust files: {len(report.rust_files)}", + ( + "- Clone notes: " + f"status=`{report.clone.status}`, markers={report.clone.marker_count}, " + f"notes={note_summary(report.clone)}" + ), + ( + "- Allocation notes: " + f"status=`{report.allocation.status}`, markers={report.allocation.marker_count}, " + f"notes={note_summary(report.allocation)}" + ), + ( + "- Eager collection notes: " + f"status=`{report.eager_collection.status}`, " + f"markers={report.eager_collection.marker_count}, " + f"notes={note_summary(report.eager_collection)}" + ), + "", + ] + ) + return "\n".join(lines) + + +def parse_args(argv: list[str]) -> argparse.Namespace: + """Parse command-line arguments.""" + parser = argparse.ArgumentParser( + description="Emit a generated Rust strict-surface audit report." + ) + parser.add_argument( + "--artifact", + action="append", + default=[], + metavar="SURFACE_CLASS=PATH", + help=( + "Generated Rust artifact file or directory to audit. " + "May be repeated. Defaults to representative target/incan fixtures." + ), + ) + parser.add_argument( + "--format", + choices=("markdown", "json"), + default="markdown", + help="Report output format.", + ) + parser.add_argument( + "--output", + type=Path, + help="Write the report to this path instead of stdout.", + ) + parser.add_argument( + "--fail-on-missing", + action="store_true", + help="Exit non-zero when any requested artifact is missing or contains no Rust files.", + ) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + """Run the generated Rust audit reporter.""" + args = parse_args(argv) + try: + specs = artifact_specs(args.artifact) + except argparse.ArgumentTypeError as err: + print(f"error: {err}", file=sys.stderr) + return 2 + + reports = [audit_artifact(surface_class, path) for surface_class, path in specs] + rendered = json_report(reports) if args.format == "json" else markdown_report(reports) + + if args.output is not None: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(rendered, encoding="utf-8") + else: + print(rendered, end="") + + if args.fail_on_missing and any(report.check_status != "present" for report in reports): + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/src/backend/ir/codegen.rs b/src/backend/ir/codegen.rs index 316ca3214..ccad9d95c 100644 --- a/src/backend/ir/codegen.rs +++ b/src/backend/ir/codegen.rs @@ -3808,6 +3808,167 @@ pub async def run(state: State, plan: Plan) -> None: Ok(()) } + #[cfg(feature = "rust_inspect")] + #[test] + fn test_codegen_awaits_async_rust_backed_method_from_metadata() -> Result<(), Box> { + use crate::frontend::typechecker::TypeChecker; + use incan_core::interop::{ + RustFunctionSig, RustItemKind, RustItemMetadata, RustMethodSig, RustParam, RustTypeInfo, RustVisibility, + }; + + let source = r#" +import std.async +from rust::demo import SessionContext +from rust::demo import CsvReadOptions +from rust::demo import make_context +from rust::demo import make_options + +pub async def register_csv() -> None: + ctx = make_context() + opts = make_options() + match await ctx.register_csv("orders", "orders.csv", opts): + Ok(_) => pass + Err(_) => pass +"#; + let tokens = must_ok(lexer::lex(source)); + let ast = must_ok(parser::parse(&tokens)); + + let tmp = seeded_rust_inspect_workspace()?; + let manifest_dir = tmp.path().to_path_buf(); + let mut tc = TypeChecker::new(); + tc.set_rust_inspect_manifest_dir(manifest_dir.clone()); + tc.rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: "demo::SessionContext".to_string(), + definition_path: Some("demo::SessionContext".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Type(RustTypeInfo { + methods: vec![ + RustMethodSig { + name: "new".to_string(), + signature: RustFunctionSig { + params: Vec::new(), + return_type: "demo::SessionContext".to_string(), + is_async: false, + is_unsafe: false, + }, + }, + RustMethodSig { + name: "register_csv".to_string(), + signature: RustFunctionSig { + params: vec![ + RustParam { + name: Some("self".to_string()), + type_display: "&self".to_string(), + }, + RustParam { + name: Some("name".to_string()), + type_display: "&str".to_string(), + }, + RustParam { + name: Some("path".to_string()), + type_display: "&str".to_string(), + }, + RustParam { + name: Some("options".to_string()), + type_display: "demo::CsvReadOptions".to_string(), + }, + ], + return_type: "Result<(), demo::DataFusionError>".to_string(), + is_async: true, + is_unsafe: false, + }, + }, + ], + implemented_traits: Vec::new(), + fields: vec![], + variants: vec![], + }), + }, + ) + .map_err(|e| std::io::Error::other(format!("seed rust-inspect context: {e}")))?; + tc.rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: "demo::CsvReadOptions".to_string(), + definition_path: Some("demo::CsvReadOptions".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Type(RustTypeInfo { + methods: vec![RustMethodSig { + name: "new".to_string(), + signature: RustFunctionSig { + params: Vec::new(), + return_type: "demo::CsvReadOptions".to_string(), + is_async: false, + is_unsafe: false, + }, + }], + implemented_traits: Vec::new(), + fields: vec![], + variants: vec![], + }), + }, + ) + .map_err(|e| std::io::Error::other(format!("seed rust-inspect options: {e}")))?; + tc.rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: "demo::make_context".to_string(), + definition_path: Some("demo::make_context".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Function(RustFunctionSig { + params: Vec::new(), + return_type: "demo::SessionContext".to_string(), + is_async: false, + is_unsafe: false, + }), + }, + ) + .map_err(|e| std::io::Error::other(format!("seed rust-inspect context factory: {e}")))?; + tc.rust_inspect_cache + .insert_test_item( + &manifest_dir, + RustItemMetadata { + canonical_path: "demo::make_options".to_string(), + definition_path: Some("demo::make_options".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Function(RustFunctionSig { + params: Vec::new(), + return_type: "demo::CsvReadOptions".to_string(), + is_async: false, + is_unsafe: false, + }), + }, + ) + .map_err(|e| std::io::Error::other(format!("seed rust-inspect options factory: {e}")))?; + tc.check_program(&ast) + .map_err(|errs| std::io::Error::other(format!("typecheck failed: {errs:?}")))?; + + let mut lowering = AstLowering::new_with_type_info(tc.type_info().clone()); + let ir_program = lowering + .lower_program(&ast) + .map_err(|err| std::io::Error::other(format!("lowering failed: {err:?}")))?; + + let mut codegen = IrCodegen::new(); + codegen.collect_external_rust_functions(&ast); + + let mut emitter = IrEmitter::new(&ir_program.function_registry); + emitter.set_external_rust_functions(codegen.external_rust_functions.clone()); + let code = emitter + .emit_program(&ir_program) + .map_err(|err| std::io::Error::other(format!("emit failed: {err:?}")))?; + + assert!( + code.contains("ctx.register_csv(") && code.contains(").await"), + "expected async Rust method call to be awaited in generated code; got:\n{code}" + ); + Ok(()) + } + #[cfg(feature = "rust_inspect")] #[test] fn test_codegen_borrows_async_rust_backed_free_function_args_from_real_rust_inspect() diff --git a/src/backend/ir/emit/expressions/comprehensions.rs b/src/backend/ir/emit/expressions/comprehensions.rs index 431824f39..c5042b96a 100644 --- a/src/backend/ir/emit/expressions/comprehensions.rs +++ b/src/backend/ir/emit/expressions/comprehensions.rs @@ -11,7 +11,7 @@ use quote::quote; use super::super::super::expr::{BuiltinFn, IrExprKind, IrGeneratorClause, Pattern, TypedExpr}; use super::super::super::ownership::{ ComprehensionIterationPlan, dict_comprehension_key_needs_clone, plan_dict_comprehension_iteration, - plan_list_comprehension_iteration, + plan_list_comprehension_iteration, plan_owned_iterator_source, }; use super::super::super::types::IrType; use super::super::{EmitError, IrEmitter}; @@ -80,7 +80,8 @@ impl<'a> IrEmitter<'a> { _ if self.is_range_iterable(iterable) || Self::is_generator_iterable(iterable) => self.emit_expr(iterable), _ => { let iter = self.emit_expr(iterable)?; - Ok(quote! { (#iter).clone().into_iter() }) + let source = plan_owned_iterator_source(iterable).apply(iter); + Ok(quote! { #source.into_iter() }) } } } @@ -95,7 +96,7 @@ impl<'a> IrEmitter<'a> { /// Emit a list comprehension. /// /// Converts `[expr for var in iter if cond]` to Rust iterator chain: - /// - Without filter: `iter.iter().cloned().map(|var| expr).collect::>()` + /// - Without filter: `iter.iter().copied().map(...)` for Copy items, otherwise `iter.iter().cloned().map(...)` /// - With filter over ranges: `iter.filter(|&var| cond).map(|var| expr).collect::>()` /// - With filter over non-range iterables: `iter.iter().filter_map(|var| { let var = (*var).clone(); if cond { /// Some(expr) } else { None } })` @@ -120,7 +121,11 @@ impl<'a> IrEmitter<'a> { let is_range = self.is_range_iterable(iterable); let iter_wrapped = quote! { (#iter) }; - match plan_list_comprehension_iteration(is_range, filter.is_some()) { + match plan_list_comprehension_iteration( + Self::comprehension_iterable_item_ty(&iterable.ty), + is_range, + filter.is_some(), + ) { ComprehensionIterationPlan::RangeFilter => { let Some(filter) = filter else { return Err(EmitError::Unsupported( @@ -157,6 +162,31 @@ impl<'a> IrEmitter<'a> { .collect::>() }) } + ComprehensionIterationPlan::FilterMapCopyBinding => { + let Some(filter) = filter else { + return Err(EmitError::Unsupported( + "internal error: filtered comprehension plan requires a filter".to_string(), + )); + }; + let filter_tokens = self.emit_expr(filter)?; + let item_binding = Self::filter_map_item_binding(pattern, &pattern_tokens); + Ok(quote! { + #iter_wrapped + .iter() + .filter_map(|#item_binding| { + let #pattern_tokens = *#item_binding; + if #filter_tokens { + Some(#elem) + } else { + None + } + }) + .collect::>() + }) + } + ComprehensionIterationPlan::IterCopied => Ok(quote! { + #iter_wrapped.iter().copied().map(|#pattern_tokens| #elem).collect::>() + }), ComprehensionIterationPlan::IterCloned => Ok(quote! { #iter_wrapped.iter().cloned().map(|#pattern_tokens| #elem).collect::>() }), @@ -166,9 +196,9 @@ impl<'a> IrEmitter<'a> { /// Emit a dict comprehension. /// /// Converts `{key: value for var in iter if cond}` to Rust iterator chain: - /// - Without filter: `iter.iter().cloned().map(|var| (key, value)).collect::>()` - /// - With filter over borrowed iterables: `iter.iter().filter_map(|var| { let var = (*var).clone(); if cond { - /// Some((key, value)) } else { None } })` + /// - Without filter: `iter.iter().copied().map(...)` for Copy items, otherwise `iter.iter().cloned().map(...)` + /// - With filter over borrowed iterables: `iter.iter().filter_map(...)`, copying or cloning the item before + /// predicate evaluation based on its IR type. pub(in super::super) fn emit_dict_comp( &self, key: &TypedExpr, @@ -198,7 +228,7 @@ impl<'a> IrEmitter<'a> { } let iter = self.emit_expr(iterable)?; - match plan_dict_comprehension_iteration(filter.is_some()) { + match plan_dict_comprehension_iteration(Self::comprehension_iterable_item_ty(&iterable.ty), filter.is_some()) { ComprehensionIterationPlan::FilterMapCloneBinding => { let Some(filter) = filter else { return Err(EmitError::Unsupported( @@ -221,6 +251,31 @@ impl<'a> IrEmitter<'a> { .collect::>() }) } + ComprehensionIterationPlan::FilterMapCopyBinding => { + let Some(filter) = filter else { + return Err(EmitError::Unsupported( + "internal error: filtered dict comprehension plan requires a filter".to_string(), + )); + }; + let filter_tokens = self.emit_expr(filter)?; + let item_binding = Self::filter_map_item_binding(pattern, &pattern_tokens); + Ok(quote! { + #iter + .iter() + .filter_map(|#item_binding| { + let #pattern_tokens = *#item_binding; + if #filter_tokens { + Some((#cloned_key, #value_tokens)) + } else { + None + } + }) + .collect::>() + }) + } + ComprehensionIterationPlan::IterCopied => Ok(quote! { + #iter.iter().copied().map(|#pattern_tokens| (#cloned_key, #value_tokens)).collect::>() + }), ComprehensionIterationPlan::IterCloned => Ok(quote! { #iter.iter().cloned().map(|#pattern_tokens| (#cloned_key, #value_tokens)).collect::>() }), @@ -241,7 +296,7 @@ impl<'a> IrEmitter<'a> { if matches!(func, super::super::super::expr::BuiltinFn::Range)) } - /// Choose the borrowed closure binding used before cloning a filtered non-range comprehension item. + /// Choose the borrowed closure binding used before materializing a filtered non-range comprehension item. fn filter_map_item_binding(pattern: &Pattern, pattern_tokens: &TokenStream) -> TokenStream { if matches!(pattern, Pattern::Var(_)) { pattern_tokens.clone() @@ -250,6 +305,15 @@ impl<'a> IrEmitter<'a> { } } + /// Return the item type a borrowed comprehension iterator yields before copy/clone materialization. + fn comprehension_iterable_item_ty(iterable_ty: &IrType) -> Option<&IrType> { + match iterable_ty { + IrType::List(item_ty) | IrType::Set(item_ty) => Some(item_ty.as_ref()), + IrType::Ref(inner) | IrType::RefMut(inner) => Self::comprehension_iterable_item_ty(inner), + _ => None, + } + } + /// Emit iterable expressions that already produce iterator items with the ownership shape a comprehension expects. fn emit_direct_comprehension_iterable(&self, iterable: &TypedExpr) -> Result, EmitError> { match &iterable.kind { diff --git a/src/backend/ir/emit/expressions/methods/iterator_methods.rs b/src/backend/ir/emit/expressions/methods/iterator_methods.rs index a87b85c4b..97fec1423 100644 --- a/src/backend/ir/emit/expressions/methods/iterator_methods.rs +++ b/src/backend/ir/emit/expressions/methods/iterator_methods.rs @@ -12,6 +12,7 @@ use quote::{format_ident, quote}; use crate::backend::ir::emit::{EmitError, IrEmitter}; use crate::backend::ir::expr::{IrExprKind, IteratorMethodKind, TypedExpr}; +use crate::backend::ir::ownership::plan_owned_iterator_source; use crate::backend::ir::types::IrType; use incan_core::lang::traits::{self as core_traits, TraitId}; @@ -157,7 +158,8 @@ pub(super) fn emit_iterator_method( fn emit_iter_receiver(receiver: &TypedExpr, r: &TokenStream) -> TokenStream { match receiver_type_for_iterator_dispatch(&receiver.ty) { IrType::List(_) => { - quote! { crate::__incan_std::derives::collection::ListIterator { items: (#r).clone(), index: 0i64 } } + let items = plan_owned_iterator_source(receiver).apply(r.clone()); + quote! { crate::__incan_std::derives::collection::ListIterator { items: #items, index: 0i64 } } } IrType::NamedGeneric(name, _) | IrType::Struct(name) if is_iterator_protocol_type_name(name) => quote! { (#r) }, _ => quote! { (#r) }, diff --git a/src/backend/ir/ownership.rs b/src/backend/ir/ownership.rs index b1a748b2a..ee071a198 100644 --- a/src/backend/ir/ownership.rs +++ b/src/backend/ir/ownership.rs @@ -15,7 +15,7 @@ use super::conversions::{ incan_mutable_param_passed_as_rust_mut_ref, }; use super::decl::FunctionParam; -use super::expr::IrExpr; +use super::expr::{IrExpr, IrExprKind, VarAccess}; use super::types::IrType; /// A typed sink/source boundary that needs an ownership/coercion decision. @@ -319,28 +319,62 @@ pub enum ComprehensionIterationPlan { RangeDirect, /// Iterate a range through `.filter(...)`. RangeFilter, + /// Iterate borrowed input and copy scalar/Copy values. + IterCopied, /// Iterate non-range input by cloning yielded values. IterCloned, + /// Filter borrowed input and copy the binding before projection. + FilterMapCopyBinding, /// Filter borrowed input and clone the binding before projection. FilterMapCloneBinding, } /// Plan iteration for a list comprehension. -pub fn plan_list_comprehension_iteration(is_range: bool, filtered: bool) -> ComprehensionIterationPlan { - match (is_range, filtered) { - (true, true) => ComprehensionIterationPlan::RangeFilter, - (true, false) => ComprehensionIterationPlan::RangeDirect, - (false, true) => ComprehensionIterationPlan::FilterMapCloneBinding, - (false, false) => ComprehensionIterationPlan::IterCloned, +pub fn plan_list_comprehension_iteration( + iterable_item_ty: Option<&IrType>, + is_range: bool, + filtered: bool, +) -> ComprehensionIterationPlan { + match ( + is_range, + filtered, + iterable_item_ty.is_some_and(comprehension_item_can_copy_from_shared_ref), + ) { + (true, true, _) => ComprehensionIterationPlan::RangeFilter, + (true, false, _) => ComprehensionIterationPlan::RangeDirect, + (false, true, true) => ComprehensionIterationPlan::FilterMapCopyBinding, + (false, true, false) => ComprehensionIterationPlan::FilterMapCloneBinding, + (false, false, true) => ComprehensionIterationPlan::IterCopied, + (false, false, false) => ComprehensionIterationPlan::IterCloned, } } /// Plan iteration for a dict comprehension. -pub fn plan_dict_comprehension_iteration(filtered: bool) -> ComprehensionIterationPlan { - if filtered { - ComprehensionIterationPlan::FilterMapCloneBinding - } else { - ComprehensionIterationPlan::IterCloned +pub fn plan_dict_comprehension_iteration( + iterable_item_ty: Option<&IrType>, + filtered: bool, +) -> ComprehensionIterationPlan { + match ( + filtered, + iterable_item_ty.is_some_and(comprehension_item_can_copy_from_shared_ref), + ) { + (true, true) => ComprehensionIterationPlan::FilterMapCopyBinding, + (true, false) => ComprehensionIterationPlan::FilterMapCloneBinding, + (false, true) => ComprehensionIterationPlan::IterCopied, + (false, false) => ComprehensionIterationPlan::IterCloned, + } +} + +/// Return whether a comprehension item can be copied out of a shared iterator reference. +fn comprehension_item_can_copy_from_shared_ref(ty: &IrType) -> bool { + match ty { + IrType::RefMut(_) => false, + IrType::Tuple(items) => items.iter().all(comprehension_item_can_copy_from_shared_ref), + IrType::Option(inner) => comprehension_item_can_copy_from_shared_ref(inner), + IrType::Result(ok, err) => { + comprehension_item_can_copy_from_shared_ref(ok) && comprehension_item_can_copy_from_shared_ref(err) + } + _ => ty.is_copy(), } } @@ -349,6 +383,54 @@ pub fn dict_comprehension_key_needs_clone(key_ty: &IrType) -> bool { !key_ty.is_copy() } +/// How an owned iterator adapter should materialize its collection source. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OwnedIteratorSourcePlan { + /// Move the source expression into the adapter. + Move, + /// Clone the source expression before moving it into the adapter. + Clone, +} + +impl OwnedIteratorSourcePlan { + /// Apply the source materialization plan to an already-emitted source expression. + pub fn apply(&self, tokens: TokenStream) -> TokenStream { + match self { + Self::Move => quote! { (#tokens) }, + Self::Clone => quote! { (#tokens).clone() }, + } + } +} + +/// Plan how an adapter that owns its collection source should materialize that source. +/// +/// This intentionally stops short of borrowed iterator architecture. It only avoids whole-collection clones when the +/// lowered expression already proves that moving the value is safe: a last-use variable read or a one-shot expression +/// whose emitted Rust produces an owned value. +pub fn plan_owned_iterator_source(expr: &IrExpr) -> OwnedIteratorSourcePlan { + if matches!(expr.ty, IrType::Ref(_) | IrType::RefMut(_)) { + return OwnedIteratorSourcePlan::Clone; + } + + if expr_can_move_into_owned_iterator(expr) { + OwnedIteratorSourcePlan::Move + } else { + OwnedIteratorSourcePlan::Clone + } +} + +/// Return whether an expression can be moved into an adapter-owned iterator source. +fn expr_can_move_into_owned_iterator(expr: &IrExpr) -> bool { + match &expr.kind { + IrExprKind::Var { access, .. } => matches!(access, VarAccess::Move | VarAccess::Copy), + IrExprKind::StaticRead { .. } | IrExprKind::StaticBinding { .. } | IrExprKind::AssociatedFunction { .. } => { + false + } + IrExprKind::Field { .. } => false, + _ => true, + } +} + #[cfg(test)] mod tests { use super::*; @@ -441,17 +523,72 @@ mod tests { #[test] fn filtered_list_comprehension_uses_filter_map_clone_plan() { assert_eq!( - plan_list_comprehension_iteration(false, true), + plan_list_comprehension_iteration(Some(&IrType::String), false, true), ComprehensionIterationPlan::FilterMapCloneBinding ); } + #[test] + fn copy_list_comprehension_uses_copy_iteration_plans() { + assert_eq!( + plan_list_comprehension_iteration(Some(&IrType::Int), false, false), + ComprehensionIterationPlan::IterCopied + ); + assert_eq!( + plan_dict_comprehension_iteration(Some(&IrType::Int), true), + ComprehensionIterationPlan::FilterMapCopyBinding + ); + } + + #[test] + fn mutable_ref_comprehension_item_does_not_use_copied_plan() { + assert_eq!( + plan_list_comprehension_iteration(Some(&IrType::RefMut(Box::new(IrType::Int))), false, false), + ComprehensionIterationPlan::IterCloned + ); + } + #[test] fn dict_comprehension_marks_noncopy_keys_for_clone() { assert!(dict_comprehension_key_needs_clone(&IrType::String)); assert!(!dict_comprehension_key_needs_clone(&IrType::Int)); } + #[test] + fn owned_iterator_source_moves_last_use_list_var() { + let expr = IrExpr::new( + IrExprKind::Var { + name: "items".to_string(), + access: VarAccess::Move, + ref_kind: VarRefKind::Value, + }, + IrType::List(Box::new(IrType::Int)), + ); + + assert_eq!(plan_owned_iterator_source(&expr), OwnedIteratorSourcePlan::Move); + } + + #[test] + fn owned_iterator_source_clones_reused_list_var() { + let expr = IrExpr::new( + IrExprKind::Var { + name: "items".to_string(), + access: VarAccess::Read, + ref_kind: VarRefKind::Value, + }, + IrType::List(Box::new(IrType::Int)), + ); + + assert_eq!(plan_owned_iterator_source(&expr), OwnedIteratorSourcePlan::Clone); + } + + #[test] + fn owned_iterator_source_moves_one_shot_list_expression() { + let expr = IrExpr::new(IrExprKind::List(Vec::new()), IrType::List(Box::new(IrType::Int))); + + assert_eq!(plan_owned_iterator_source(&expr), OwnedIteratorSourcePlan::Move); + } + #[test] fn external_call_string_var_borrows() { let expr = IrExpr::new( diff --git a/src/frontend/typechecker/check_expr/calls/rust_boundary.rs b/src/frontend/typechecker/check_expr/calls/rust_boundary.rs index 5d203460c..950a908c4 100644 --- a/src/frontend/typechecker/check_expr/calls/rust_boundary.rs +++ b/src/frontend/typechecker/check_expr/calls/rust_boundary.rs @@ -57,6 +57,20 @@ impl TypeChecker { return_ty } + /// Type exposed for an async Rust call when it is not the direct operand of `await`. + /// + /// In an `await` operand we keep the existing source-async behavior: the inner call checks to its output type and + /// `check_await` returns that type. Outside `await`, expose the pending future as `Awaitable[T]` so consumers + /// cannot accidentally match or unwrap `T` before awaiting the Rust future. + fn resolved_rust_call_type_from_sig(&self, sig: &RustFunctionSig, span: Span) -> ResolvedType { + let return_ty = self.resolved_rust_return_type_from_sig(sig); + if sig.is_async && !self.is_in_await_operand(span) { + ResolvedType::Generic("Awaitable".to_string(), vec![return_ty]) + } else { + return_ty + } + } + /// Record an ownership conversion for borrowed Rust scalar-like returns that Incan exposes as owned values. pub(in crate::frontend::typechecker) fn record_rust_return_coercion_from_display( &mut self, @@ -363,6 +377,10 @@ impl TypeChecker { preserves_lookup_arg_shape: bool, span: Span, ) -> ResolvedType { + if sig.is_async && !self.is_in_await_operand(span) { + self.errors + .push(errors::async_call_without_await(callable_display, span)); + } let params = if Self::rust_signature_has_receiver(sig) { &sig.params[1..] } else { @@ -377,7 +395,7 @@ impl TypeChecker { arg_types.len(), span, )); - return self.resolved_rust_return_type_from_sig(sig); + return self.resolved_rust_call_type_from_sig(sig, span); } for ((arg, arg_ty), param) in args.iter().zip(arg_types.iter()).zip(params.iter()) { @@ -409,7 +427,7 @@ impl TypeChecker { } } - let ret = self.resolved_rust_return_type_from_sig(sig); + let ret = self.resolved_rust_call_type_from_sig(sig, span); self.record_rust_return_coercion_from_display(sig.return_type.as_str(), &ret, span); ret } @@ -422,12 +440,15 @@ impl TypeChecker { args: &[CallArg], span: Span, ) -> ResolvedType { + if sig.is_async && !self.is_in_await_operand(span) { + self.errors.push(errors::async_call_without_await(path, span)); + } let arg_types = self.check_call_arg_types(args); self.record_rust_call_site_params(span, &sig.params); if arg_types.len() != sig.params.len() { self.errors .push(errors::builtin_arity(path, sig.params.len(), arg_types.len(), span)); - return self.resolved_rust_return_type_from_sig(sig); + return self.resolved_rust_call_type_from_sig(sig, span); } for ((arg, arg_ty), param) in args.iter().zip(arg_types.iter()).zip(sig.params.iter()) { @@ -456,7 +477,7 @@ impl TypeChecker { } } - let ret = self.resolved_rust_return_type_from_sig(sig); + let ret = self.resolved_rust_call_type_from_sig(sig, span); self.record_rust_return_coercion_from_display(sig.return_type.as_str(), &ret, span); ret } diff --git a/src/frontend/typechecker/check_expr/control_flow.rs b/src/frontend/typechecker/check_expr/control_flow.rs index 0b6a418b9..48f7028b7 100644 --- a/src/frontend/typechecker/check_expr/control_flow.rs +++ b/src/frontend/typechecker/check_expr/control_flow.rs @@ -88,6 +88,9 @@ impl TypeChecker { if self.known_surface_async_method(base_ty, method) { return true; } + if let ResolvedType::RustPath(path) = base_ty { + return self.rust_method_signature(path, method).is_some_and(|sig| sig.is_async); + } let type_name = match base_ty { ResolvedType::Named(name) | ResolvedType::Generic(name, _) => name, _ => return false, @@ -102,7 +105,17 @@ impl TypeChecker { Some(TypeInfo::Enum(info)) => { Self::method_set_has_async_method(&info.methods, &info.method_overloads, method) } - Some(TypeInfo::Newtype(info)) => info.methods.get(method).is_some_and(|method_info| method_info.is_async), + Some(TypeInfo::Newtype(info)) => { + if info.methods.get(method).is_some_and(|method_info| method_info.is_async) { + return true; + } + if info.is_rusttype + && let ResolvedType::RustPath(path) = &info.underlying + { + return self.rust_method_signature(path, method).is_some_and(|sig| sig.is_async); + } + false + } _ => false, } } diff --git a/src/frontend/typechecker/tests.rs b/src/frontend/typechecker/tests.rs index 927d99406..740c2ad60 100644 --- a/src/frontend/typechecker/tests.rs +++ b/src/frontend/typechecker/tests.rs @@ -3543,6 +3543,179 @@ def render[T](value: Label[T]) -> str: Ok(()) } +#[cfg(feature = "rust_inspect")] +fn seed_async_rust_method_probe( + checker: &mut TypeChecker, + manifest_dir: &std::path::Path, +) -> Result<(), Box> { + checker.rust_inspect_cache.insert_test_item( + manifest_dir, + RustItemMetadata { + canonical_path: "demo::SessionContext".to_string(), + definition_path: Some("demo::SessionContext".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Type(RustTypeInfo { + methods: vec![ + RustMethodSig { + name: "new".to_string(), + signature: RustFunctionSig { + params: Vec::new(), + return_type: "demo::SessionContext".to_string(), + is_async: false, + is_unsafe: false, + }, + }, + RustMethodSig { + name: "register_csv".to_string(), + signature: RustFunctionSig { + params: vec![ + RustParam { + name: Some("self".to_string()), + type_display: "&self".to_string(), + }, + RustParam { + name: Some("name".to_string()), + type_display: "&str".to_string(), + }, + RustParam { + name: Some("path".to_string()), + type_display: "&str".to_string(), + }, + RustParam { + name: Some("options".to_string()), + type_display: "demo::CsvReadOptions".to_string(), + }, + ], + return_type: "Result<(), demo::DataFusionError>".to_string(), + is_async: true, + is_unsafe: false, + }, + }, + ], + implemented_traits: Vec::new(), + fields: vec![], + variants: vec![], + }), + }, + )?; + checker.rust_inspect_cache.insert_test_item( + manifest_dir, + RustItemMetadata { + canonical_path: "demo::CsvReadOptions".to_string(), + definition_path: Some("demo::CsvReadOptions".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Type(RustTypeInfo { + methods: vec![RustMethodSig { + name: "new".to_string(), + signature: RustFunctionSig { + params: Vec::new(), + return_type: "demo::CsvReadOptions".to_string(), + is_async: false, + is_unsafe: false, + }, + }], + implemented_traits: Vec::new(), + fields: vec![], + variants: vec![], + }), + }, + )?; + checker.rust_inspect_cache.insert_test_item( + manifest_dir, + RustItemMetadata { + canonical_path: "demo::make_context".to_string(), + definition_path: Some("demo::make_context".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Function(RustFunctionSig { + params: Vec::new(), + return_type: "demo::SessionContext".to_string(), + is_async: false, + is_unsafe: false, + }), + }, + )?; + checker.rust_inspect_cache.insert_test_item( + manifest_dir, + RustItemMetadata { + canonical_path: "demo::make_options".to_string(), + definition_path: Some("demo::make_options".to_string()), + visibility: RustVisibility::Public, + kind: RustItemKind::Function(RustFunctionSig { + params: Vec::new(), + return_type: "demo::CsvReadOptions".to_string(), + is_async: false, + is_unsafe: false, + }), + }, + )?; + Ok(()) +} + +#[cfg(feature = "rust_inspect")] +#[test] +fn test_rust_async_method_call_can_be_awaited() -> Result<(), Box> { + let source = r#" +import std.async +from rust::demo import SessionContext +from rust::demo import CsvReadOptions +from rust::demo import make_context +from rust::demo import make_options + +pub async def register_csv_with_await() -> None: + ctx = make_context() + opts = make_options() + match await ctx.register_csv("orders", "orders.csv", opts): + Ok(_) => pass + Err(_) => pass +"#; + 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()?; + checker.set_rust_inspect_manifest_dir(tmp.path().to_path_buf()); + seed_async_rust_method_probe(&mut checker, tmp.path())?; + checker.check_program(&ast).map_err(|errs| { + std::io::Error::other(format!( + "expected awaited Rust async method call to typecheck: {errs:?}" + )) + })?; + Ok(()) +} + +#[cfg(feature = "rust_inspect")] +#[test] +fn test_rust_async_method_call_without_await_is_rejected() -> Result<(), Box> { + let source = r#" +import std.async +from rust::demo import SessionContext +from rust::demo import CsvReadOptions +from rust::demo import make_context +from rust::demo import make_options + +pub async def register_csv_without_await() -> None: + ctx = make_context() + opts = make_options() + match ctx.register_csv("orders", "orders.csv", opts): + Ok(_) => pass + Err(_) => pass +"#; + 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()?; + checker.set_rust_inspect_manifest_dir(tmp.path().to_path_buf()); + seed_async_rust_method_probe(&mut checker, tmp.path())?; + let Err(errs) = checker.check_program(&ast) else { + return Err(std::io::Error::other("expected un-awaited Rust async method call to fail").into()); + }; + assert!( + errs.iter() + .any(|err| err.message.contains("Awaitable[Result") && err.message.contains("does not resolve")), + "expected un-awaited Rust async method call to expose an Awaitable Result before matching, got {errs:?}" + ); + Ok(()) +} + #[cfg(feature = "rust_inspect")] #[test] fn test_rusttype_alias_resolves_underlying_rust_methods() -> Result<(), Box> { diff --git a/tests/README.md b/tests/README.md index 504585c34..8efce7634 100644 --- a/tests/README.md +++ b/tests/README.md @@ -7,12 +7,17 @@ This directory contains the Incan compiler's integration and snapshot test suite | File | What it tests | How to run | | ----------------------------------- | --------------------------------------------------------------------------------------------- | -------------------------------------------------- | | `codegen_snapshot_tests.rs` | End-to-end codegen: `.incn` input -> generated Rust output, verified against golden snapshots | `cargo test --test codegen_snapshot_tests` | +| `generated_rust_artifact_tests.rs` | Final generated application, library, and `pub::` consumer package artifacts | `cargo test --test generated_rust_artifact_tests` | +| `generated_rust_audit_tests.rs` | Deterministic generated Rust audit helper parsing, reporting, and strict-gate behavior | `cargo test --test generated_rust_audit_tests` | +| `generated_rust_callability_artifact_tests.rs` | Package-facing callable exports and current callable boundary blockers | `cargo test --test generated_rust_callability_artifact_tests` | +| `generated_rust_native_consumer_tests.rs` | Native Rust crate consumption of generated Incan library artifacts | `cargo test --test generated_rust_native_consumer_tests` | | `integration_tests.rs` | Full pipeline integration (parse + typecheck + lower + emit) | `cargo test --test integration_tests` | | `construction_diagnostics_tests.rs` | Diagnostic messages for constructor errors | `cargo test --test construction_diagnostics_tests` | | `lowering_error_propagation.rs` | Error propagation through the lowering stage | `cargo test --test lowering_error_propagation` | | `property_tests.rs` | Property-based tests (formatter idempotency, parseability, conversion determinism) | `cargo test --test property_tests` | | `semantic_core_parity.rs` | Parity between compiler and runtime semantic definitions | `cargo test --test semantic_core_parity` | | `semantic_core_parity_strings.rs` | String-specific semantic parity | `cargo test --test semantic_core_parity_strings` | +| `stdlib_generated_rust_snapshot_tests.rs` | Representative generated Rust snapshots for selected stdlib modules | `cargo test --test stdlib_generated_rust_snapshot_tests` | | `vocab_guardrails.rs` | Vocabulary drift detection between crates | `cargo test --test vocab_guardrails` | | `layering_guard.rs` | Enforces crate dependency layering rules | `cargo test --test layering_guard` | | `test_example.incn` | Example Incan test file (used by the test runner) | `incan test tests/test_example.incn` | diff --git a/tests/codegen_snapshot_tests.rs b/tests/codegen_snapshot_tests.rs index 5a08bcde7..f4b27760f 100644 --- a/tests/codegen_snapshot_tests.rs +++ b/tests/codegen_snapshot_tests.rs @@ -1617,16 +1617,71 @@ fn test_filtered_dict_comp_predicate_codegen() { let rust_code = generate_rust(&source); let compact = rust_code.chars().filter(|ch| !ch.is_whitespace()).collect::(); assert!( - compact.contains(".iter().filter_map(|x|{letx=(*x).clone();ifincan_stdlib::num::py_mod_i64(x,2)==0{Some((x,x*x))}else{None}})"), - "expected filtered dict comprehension to clone inside filter_map before evaluating the predicate; generated:\n{rust_code}" + compact.contains( + ".iter().filter_map(|x|{letx=*x;ifincan_stdlib::num::py_mod_i64(x,2)==0{Some((x,x*x))}else{None}})" + ), + "expected filtered dict comprehension over Copy items to copy inside filter_map before evaluating the predicate; generated:\n{rust_code}" ); assert!( !compact.contains(".filter(|x|incan_stdlib::num::py_mod_i64(x,2)==0)"), "filtered dict comprehension must not leave the predicate closure borrowing `x`; generated:\n{rust_code}" ); + assert!( + !compact.contains("letx=(*x).clone()"), + "filtered dict comprehension over Copy items should not call clone; generated:\n{rust_code}" + ); insta::assert_snapshot!("filtered_dict_comp_predicate", rust_code); } +/// Issue #602: comprehensions over Copy item types should use copied values rather than `.clone()` hot paths. +#[test] +fn test_issue602_comprehension_copy_hotpaths_codegen() { + let source = load_test_file("issue602_comprehension_copy_hotpaths"); + let rust_code = generate_rust(&source); + let compact = rust_code.chars().filter(|ch| !ch.is_whitespace()).collect::(); + assert!( + compact.contains("(xs).iter().copied().map(|x|x*x).collect::>()"), + "expected unfiltered Copy list comprehension to use copied(), generated:\n{rust_code}" + ); + assert!( + compact.contains(".iter().filter_map(|x|{letx=*x;ifx>0{Some(x*x)}else{None}})"), + "expected filtered Copy list comprehension to copy the borrowed item without clone; generated:\n{rust_code}" + ); + assert!( + compact.contains(".iter().filter_map(|x|{letx=*x;ifx>0{Some((x,x*x))}else{None}})"), + "expected filtered Copy dict comprehension to copy the borrowed item without clone; generated:\n{rust_code}" + ); + assert!( + !compact.contains("(*x).clone()") && !compact.contains(".iter().cloned().map(|x|x*x)"), + "Copy comprehension hot paths should not emit clone calls; generated:\n{rust_code}" + ); + insta::assert_snapshot!("issue602_comprehension_copy_hotpaths", rust_code); +} + +#[test] +fn test_issue602_owned_iterator_source_hotpaths_codegen() { + let source = load_test_file("issue602_owned_iterator_source_hotpaths"); + let rust_code = generate_rust(&source); + let compact = rust_code.chars().filter(|ch| !ch.is_whitespace()).collect::(); + assert!( + compact.contains("ListIterator{items:(xs),") && !compact.contains("ListIterator{items:(xs).clone()"), + "last-use list iterator sources should move into ListIterator instead of cloning; generated:\n{rust_code}" + ); + assert!( + compact.contains("(vec![1,2,3]).into_iter()"), + "one-shot generator iterable sources should move into the generator instead of cloning; generated:\n{rust_code}" + ); + assert!( + compact.contains("ListIterator{items:(values).clone(),") && compact.contains("(values).clone().into_iter()"), + "reused list iterator and generator sources should still clone; generated:\n{rust_code}" + ); + assert!( + compact.contains("(xs).clone().into_iter()"), + "generator iterable variables remain cloned until lazy generator capture gets broader move analysis; generated:\n{rust_code}" + ); + insta::assert_snapshot!("issue602_owned_iterator_source_hotpaths", rust_code); +} + // ============================================================================ // Tests for declarations (functions, classes, models, traits, enums) // ============================================================================ diff --git a/tests/codegen_snapshots/issue602_comprehension_copy_hotpaths.incn b/tests/codegen_snapshots/issue602_comprehension_copy_hotpaths.incn new file mode 100644 index 000000000..d8bb78793 --- /dev/null +++ b/tests/codegen_snapshots/issue602_comprehension_copy_hotpaths.incn @@ -0,0 +1,19 @@ +"""Issue #602: copy-typed comprehensions should not emit clone hot paths.""" + +def squares(xs: list[int]) -> list[int]: + return [x * x for x in xs] + +def positive_squares(xs: list[int]) -> list[int]: + return [x * x for x in xs if x > 0] + +def index_by_value(xs: list[int]) -> dict[int, int]: + return {x: x * x for x in xs if x > 0} + +def main() -> None: + values = [1, 2, 3] + copied = squares(values) + filtered = positive_squares(values) + indexed = index_by_value(values) + println(copied[0]) + println(filtered[0]) + println(indexed[1]) diff --git a/tests/codegen_snapshots/issue602_owned_iterator_source_hotpaths.incn b/tests/codegen_snapshots/issue602_owned_iterator_source_hotpaths.incn new file mode 100644 index 000000000..40768a63b --- /dev/null +++ b/tests/codegen_snapshots/issue602_owned_iterator_source_hotpaths.incn @@ -0,0 +1,35 @@ +"""Issue #602: owned iterator sources should avoid whole-list clones on last use.""" + +def double(x: int) -> int: + return x * 2 + +def collect_last_use(xs: list[int]) -> list[int]: + return xs.iter().map(double).collect() + +def generator_last_use(xs: list[int]) -> Generator[int]: + return (x for x in xs if x > 0) + +def generator_one_shot() -> Generator[int]: + return (x for x in [1, 2, 3] if x > 0) + +def collect_reused(values: list[int]) -> int: + total = values.iter().sum() + first = values[0] + return total + first + +def generator_reused(values: list[int]) -> int: + generated = (x for x in values if x > 0).collect() + first = values[0] + return len(generated) + first + +def main() -> None: + collected = collect_last_use([1, 2, 3]) + generated = generator_last_use([1, 2, 3]).collect() + generated_direct = generator_one_shot().collect() + reused_iter = collect_reused([1, 2, 3]) + reused_gen = generator_reused([1, 2, 3]) + println(collected[0]) + println(generated[0]) + println(generated_direct[0]) + println(reused_iter) + println(reused_gen) diff --git a/tests/fixtures/generated_rust_artifacts/app_main.incn b/tests/fixtures/generated_rust_artifacts/app_main.incn new file mode 100644 index 000000000..96bce84aa --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/app_main.incn @@ -0,0 +1,9 @@ +model User: + name: str + +def greet(user: User) -> str: + return "hello " + user.name + +def main() -> None: + user = User(name="artifact") + println(greet(user)) diff --git a/tests/fixtures/generated_rust_artifacts/app_main_rs.fragments b/tests/fixtures/generated_rust_artifacts/app_main_rs.fragments new file mode 100644 index 000000000..3f3ef2e52 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/app_main_rs.fragments @@ -0,0 +1,11 @@ +// Generated by the Incan compiler v +--- +struct User +--- +name: String +--- +fn greet(user: User) -> String +--- +fn main() +--- +println!("{}", greet(user)); diff --git a/tests/fixtures/generated_rust_artifacts/app_required_files.txt b/tests/fixtures/generated_rust_artifacts/app_required_files.txt new file mode 100644 index 000000000..8f6705963 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/app_required_files.txt @@ -0,0 +1,3 @@ +Cargo.toml +Cargo.lock +src/main.rs diff --git a/tests/fixtures/generated_rust_artifacts/consumer_main.incn b/tests/fixtures/generated_rust_artifacts/consumer_main.incn new file mode 100644 index 000000000..5c264f70e --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/consumer_main.incn @@ -0,0 +1,5 @@ +from pub::widgets import Widget as PublicWidget, make_widget + +def main() -> None: + w: PublicWidget = make_widget("ok") + println(w.name) diff --git a/tests/fixtures/generated_rust_artifacts/consumer_main_rs.fragments b/tests/fixtures/generated_rust_artifacts/consumer_main_rs.fragments new file mode 100644 index 000000000..4349028f9 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/consumer_main_rs.fragments @@ -0,0 +1,7 @@ +use widgets::Widget as PublicWidget; +--- +use widgets::make_widget; +--- +let w: PublicWidget = make_widget("ok".to_string()); +--- +println!("{}", w.name); diff --git a/tests/fixtures/generated_rust_artifacts/consumer_required_files.txt b/tests/fixtures/generated_rust_artifacts/consumer_required_files.txt new file mode 100644 index 000000000..8f6705963 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/consumer_required_files.txt @@ -0,0 +1,3 @@ +Cargo.toml +Cargo.lock +src/main.rs diff --git a/tests/fixtures/generated_rust_artifacts/library_lib.incn b/tests/fixtures/generated_rust_artifacts/library_lib.incn new file mode 100644 index 000000000..24d105ae6 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/library_lib.incn @@ -0,0 +1 @@ +pub from widgets import Widget, make_widget diff --git a/tests/fixtures/generated_rust_artifacts/library_lib_rs.fragments b/tests/fixtures/generated_rust_artifacts/library_lib_rs.fragments new file mode 100644 index 000000000..e101c403c --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/library_lib_rs.fragments @@ -0,0 +1,5 @@ +mod widgets; +--- +pub use crate::widgets::Widget; +--- +pub use crate::widgets::make_widget; diff --git a/tests/fixtures/generated_rust_artifacts/library_required_files.txt b/tests/fixtures/generated_rust_artifacts/library_required_files.txt new file mode 100644 index 000000000..87c9f8d1f --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/library_required_files.txt @@ -0,0 +1,5 @@ +Cargo.toml +Cargo.lock +src/lib.rs +src/widgets.rs +artifact_widgets_core.incnlib diff --git a/tests/fixtures/generated_rust_artifacts/library_widgets.incn b/tests/fixtures/generated_rust_artifacts/library_widgets.incn new file mode 100644 index 000000000..de9de0ff5 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/library_widgets.incn @@ -0,0 +1,5 @@ +pub model Widget: + pub name: str + +pub def make_widget(name: str) -> Widget: + return Widget(name=name) diff --git a/tests/fixtures/generated_rust_artifacts/library_widgets_rs.fragments b/tests/fixtures/generated_rust_artifacts/library_widgets_rs.fragments new file mode 100644 index 000000000..d3f175071 --- /dev/null +++ b/tests/fixtures/generated_rust_artifacts/library_widgets_rs.fragments @@ -0,0 +1,5 @@ +pub struct Widget +--- +pub name: String +--- +pub fn make_widget(name: String) -> Widget diff --git a/tests/fixtures/generated_rust_audit/main.rs b/tests/fixtures/generated_rust_audit/main.rs new file mode 100644 index 000000000..10c1771a0 --- /dev/null +++ b/tests/fixtures/generated_rust_audit/main.rs @@ -0,0 +1,7 @@ +fn main() { + let name = String::from("incan").to_string(); + let copy = name.clone(); + let _pair = (copy.clone(), copy.clone()); + let values = vec![1, 2, 3]; + let _collected = values.iter().map(|value| value + 1).collect::>(); +} diff --git a/tests/fixtures/generated_rust_audit/nested/lib.rs b/tests/fixtures/generated_rust_audit/nested/lib.rs new file mode 100644 index 000000000..eaac9ebb8 --- /dev/null +++ b/tests/fixtures/generated_rust_audit/nested/lib.rs @@ -0,0 +1,6 @@ +pub fn render(items: Vec) -> String { + let mut out = String::new(); + let mapped = items.iter().map(|item| item.to_owned()).collect(); + out.push_str(&format!("{:?}", mapped)); + out +} diff --git a/tests/fixtures/generated_rust_audit/no_rust_dir/notes.txt b/tests/fixtures/generated_rust_audit/no_rust_dir/notes.txt new file mode 100644 index 000000000..923c0963a --- /dev/null +++ b/tests/fixtures/generated_rust_audit/no_rust_dir/notes.txt @@ -0,0 +1 @@ +This fixture intentionally contains no Rust source files. diff --git a/tests/fixtures/generated_rust_callability/consumer_borrowed_blocker/src/main.incn b/tests/fixtures/generated_rust_callability/consumer_borrowed_blocker/src/main.incn new file mode 100644 index 000000000..10c9c5e1b --- /dev/null +++ b/tests/fixtures/generated_rust_callability/consumer_borrowed_blocker/src/main.incn @@ -0,0 +1,12 @@ +from pub::callability import Payload, inspect_payload + + +def observe(_payload: Payload) -> None: + pass + + +def main() -> None: + result: Result[Payload, str] = Ok(Payload(value=7)) + match inspect_payload(result, observe): + Ok(payload) => print(payload.value) + Err(error) => print(error) diff --git a/tests/fixtures/generated_rust_callability/consumer_owned/src/main.incn b/tests/fixtures/generated_rust_callability/consumer_owned/src/main.incn new file mode 100644 index 000000000..1c01b4b40 --- /dev/null +++ b/tests/fixtures/generated_rust_callability/consumer_owned/src/main.incn @@ -0,0 +1,7 @@ +from pub::callability import map_owned, plus_one + + +def main() -> None: + mapped = map_owned([1, 2, 3], plus_one) + for item in mapped: + print(item) diff --git a/tests/fixtures/generated_rust_callability/producer/incan.toml b/tests/fixtures/generated_rust_callability/producer/incan.toml new file mode 100644 index 000000000..8f34f735d --- /dev/null +++ b/tests/fixtures/generated_rust_callability/producer/incan.toml @@ -0,0 +1,3 @@ +[project] +name = "callability_core" +version = "0.1.0" diff --git a/tests/fixtures/generated_rust_callability/producer/src/lib.incn b/tests/fixtures/generated_rust_callability/producer/src/lib.incn new file mode 100644 index 000000000..5e2235885 --- /dev/null +++ b/tests/fixtures/generated_rust_callability/producer/src/lib.incn @@ -0,0 +1 @@ +pub from transforms import Payload, plus_one, map_owned, inspect_payload diff --git a/tests/fixtures/generated_rust_callability/producer/src/transforms.incn b/tests/fixtures/generated_rust_callability/producer/src/transforms.incn new file mode 100644 index 000000000..6cfb46915 --- /dev/null +++ b/tests/fixtures/generated_rust_callability/producer/src/transforms.incn @@ -0,0 +1,21 @@ +pub model Payload: + value: int + + +pub def plus_one(value: int) -> int: + return value + 1 + + +pub def map_owned(items: list[int], f: Callable[int, int]) -> list[int]: + mut out: list[int] = [] + for item in items: + out.append(f(item)) + return out + + +pub def inspect_payload(result: Result[Payload, str], f: Callable[Payload, None]) -> Result[Payload, str]: + match result: + Ok(value) => + f(value) + return Ok(value) + Err(error) => return Err(error) diff --git a/tests/fixtures/generated_rust_native_consumer/consumer/Cargo.toml b/tests/fixtures/generated_rust_native_consumer/consumer/Cargo.toml new file mode 100644 index 000000000..d1d148aec --- /dev/null +++ b/tests/fixtures/generated_rust_native_consumer/consumer/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "native_consumer_host" +version = "0.1.0" +edition = "2021" + +[dependencies] +native_consumer_core = { path = "../native_items/target/lib" } diff --git a/tests/fixtures/generated_rust_native_consumer/consumer/src/lib.rs b/tests/fixtures/generated_rust_native_consumer/consumer/src/lib.rs new file mode 100644 index 000000000..37b71100b --- /dev/null +++ b/tests/fixtures/generated_rust_native_consumer/consumer/src/lib.rs @@ -0,0 +1,24 @@ +use native_consumer_core::{add_count, make_counter, rename_counter, Counter}; + +#[test] +fn calls_generated_public_model_and_functions() { + let made = make_counter("rust-host".to_string(), 41); + assert_eq!(made.label, "rust-host"); + assert_eq!(made.count, 41); + + let direct_for_bump = Counter { + label: "manual".to_string(), + count: 2, + }; + let bumped = add_count(direct_for_bump, 5); + assert_eq!(bumped.label, "manual"); + assert_eq!(bumped.count, 7); + + let direct_for_rename = Counter { + label: "manual".to_string(), + count: 2, + }; + let renamed = rename_counter(direct_for_rename, "renamed".to_string()); + assert_eq!(renamed.label, "renamed"); + assert_eq!(renamed.count, 2); +} diff --git a/tests/fixtures/generated_rust_native_consumer/producer/incan.toml b/tests/fixtures/generated_rust_native_consumer/producer/incan.toml new file mode 100644 index 000000000..bcaef53b1 --- /dev/null +++ b/tests/fixtures/generated_rust_native_consumer/producer/incan.toml @@ -0,0 +1,3 @@ +[project] +name = "native_consumer_core" +version = "0.1.0" diff --git a/tests/fixtures/generated_rust_native_consumer/producer/src/counters.incn b/tests/fixtures/generated_rust_native_consumer/producer/src/counters.incn new file mode 100644 index 000000000..3c50615ae --- /dev/null +++ b/tests/fixtures/generated_rust_native_consumer/producer/src/counters.incn @@ -0,0 +1,15 @@ +pub model Counter: + pub label: str + pub count: int + + +pub def make_counter(label: str, count: int) -> Counter: + return Counter(label=label, count=count) + + +pub def add_count(counter: Counter, amount: int) -> Counter: + return Counter(label=counter.label, count=counter.count + amount) + + +pub def rename_counter(counter: Counter, label: str) -> Counter: + return Counter(label=label, count=counter.count) diff --git a/tests/fixtures/generated_rust_native_consumer/producer/src/lib.incn b/tests/fixtures/generated_rust_native_consumer/producer/src/lib.incn new file mode 100644 index 000000000..2e422d458 --- /dev/null +++ b/tests/fixtures/generated_rust_native_consumer/producer/src/lib.incn @@ -0,0 +1 @@ +pub from counters import Counter, add_count, make_counter, rename_counter diff --git a/tests/generated_rust_artifact_tests.rs b/tests/generated_rust_artifact_tests.rs new file mode 100644 index 000000000..9619d7856 --- /dev/null +++ b/tests/generated_rust_artifact_tests.rs @@ -0,0 +1,276 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use incan::library_manifest::LibraryManifest; + +const FIXTURE_ROOT: &str = "tests/fixtures/generated_rust_artifacts"; + +fn incan_binary() -> PathBuf { + if let Ok(path) = std::env::var("CARGO_BIN_EXE_incan") { + return PathBuf::from(path); + } + + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") { + let path = PathBuf::from(target_dir).join("debug").join("incan"); + if path.exists() { + return path; + } + } + + manifest_dir.join("target").join("debug").join("incan") +} + +fn run_incan(current_dir: &Path, args: &[&str]) -> Result> { + Ok(Command::new(incan_binary()) + .args(args) + .current_dir(current_dir) + .env("CARGO_NET_OFFLINE", "true") + .env("INCAN_NO_BANNER", "1") + .output()?) +} + +fn assert_success(output: &Output, context: &str) { + assert!( + output.status.success(), + "{context} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn fixture_path(name: &str) -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join(FIXTURE_ROOT).join(name) +} + +fn read_fixture(name: &str) -> Result> { + Ok(fs::read_to_string(fixture_path(name))?) +} + +fn write_fixture(destination: &Path, fixture: &str) -> Result<(), Box> { + fs::write(destination, read_fixture(fixture)?)?; + Ok(()) +} + +fn assert_required_files(root: &Path, fixture: &str) -> Result<(), Box> { + let expected_files = read_fixture(fixture)?; + for relative in expected_files.lines().map(str::trim) { + if relative.is_empty() || relative.starts_with('#') { + continue; + } + let path = root.join(relative); + assert!(path.is_file(), "expected generated artifact file `{}`", path.display()); + } + Ok(()) +} + +fn assert_contains_fragments(path: &Path, fixture: &str) -> Result<(), Box> { + let actual = fs::read_to_string(path)?; + let fragments = read_fixture(fixture)?; + for fragment in fragments.split("\n---\n") { + let fragment = fragment.trim_matches('\n'); + if fragment.trim().is_empty() { + continue; + } + assert!( + actual.contains(fragment), + "expected `{}` to contain fragment:\n{}\n\nactual:\n{}", + path.display(), + fragment, + actual + ); + } + Ok(()) +} + +fn toml_at<'a>(table: &'a toml::Table, key: &str) -> Result<&'a toml::Value, Box> { + table + .get(key) + .ok_or_else(|| format!("generated Cargo.toml missing `{key}`").into()) +} + +fn toml_table_at<'a>(table: &'a toml::Table, key: &str) -> Result<&'a toml::Table, Box> { + toml_at(table, key)? + .as_table() + .ok_or_else(|| format!("generated Cargo.toml `{key}` was not a table").into()) +} + +fn toml_string_at<'a>(table: &'a toml::Table, key: &str) -> Result<&'a str, Box> { + toml_at(table, key)? + .as_str() + .ok_or_else(|| format!("generated Cargo.toml `{key}` was not a string").into()) +} + +fn read_cargo_toml(path: &Path) -> Result> { + let cargo_toml = fs::read_to_string(path)?; + Ok(toml::from_str(&cargo_toml)?) +} + +#[test] +fn generated_application_artifact_matches_baseline() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let project_root = tmp.path().join("artifact_app_project"); + let src_dir = project_root.join("src"); + fs::create_dir_all(&src_dir)?; + fs::write( + project_root.join("incan.toml"), + "[project]\nname = \"artifact_app_baseline\"\nversion = \"0.1.0\"\n", + )?; + write_fixture(&src_dir.join("main.incn"), "app_main.incn")?; + + let out_dir = project_root.join("out"); + let main_arg = src_dir + .join("main.incn") + .to_str() + .ok_or("application source path was not valid UTF-8")? + .to_string(); + let out_arg = out_dir + .to_str() + .ok_or("application output path was not valid UTF-8")? + .to_string(); + let output = run_incan(&project_root, &["build", &main_arg, &out_arg])?; + assert_success(&output, "incan build application artifact"); + + assert_required_files(&out_dir, "app_required_files.txt")?; + assert_contains_fragments(&out_dir.join("src").join("main.rs"), "app_main_rs.fragments")?; + + let cargo_toml = read_cargo_toml(&out_dir.join("Cargo.toml"))?; + let package = toml_table_at(&cargo_toml, "package")?; + assert_eq!(toml_string_at(package, "name")?, "artifact_app_baseline"); + assert_eq!(toml_string_at(package, "edition")?, "2021"); + let dependencies = toml_table_at(&cargo_toml, "dependencies")?; + assert!( + toml_at(dependencies, "incan_stdlib").is_ok(), + "generated application Cargo.toml should include incan_stdlib" + ); + assert!( + toml_at(dependencies, "incan_derive").is_ok(), + "generated application Cargo.toml should include incan_derive" + ); + + Ok(()) +} + +#[test] +fn generated_library_artifact_matches_baseline() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let project_root = tmp.path().join("artifact_widgets_project"); + let src_dir = project_root.join("src"); + fs::create_dir_all(&src_dir)?; + fs::write( + project_root.join("incan.toml"), + "[project]\nname = \"artifact_widgets_core\"\nversion = \"0.1.0\"\n", + )?; + write_fixture(&src_dir.join("widgets.incn"), "library_widgets.incn")?; + write_fixture(&src_dir.join("lib.incn"), "library_lib.incn")?; + + let output = run_incan(&project_root, &["build", "--lib"])?; + assert_success(&output, "incan build --lib artifact"); + + let artifact_root = project_root.join("target").join("lib"); + assert_required_files(&artifact_root, "library_required_files.txt")?; + assert_contains_fragments(&artifact_root.join("src").join("lib.rs"), "library_lib_rs.fragments")?; + assert_contains_fragments( + &artifact_root.join("src").join("widgets.rs"), + "library_widgets_rs.fragments", + )?; + + let manifest = LibraryManifest::read_from_path(&artifact_root.join("artifact_widgets_core.incnlib"))?; + assert_eq!(manifest.name, "artifact_widgets_core"); + assert_eq!(manifest.version, "0.1.0"); + assert!( + manifest.exports.models.iter().any(|model| model.name == "Widget"), + "generated .incnlib should export Widget, got {:#?}", + manifest.exports.models + ); + assert!( + manifest + .exports + .functions + .iter() + .any(|function| function.name == "make_widget"), + "generated .incnlib should export make_widget, got {:#?}", + manifest.exports.functions + ); + + let cargo_toml = read_cargo_toml(&artifact_root.join("Cargo.toml"))?; + assert_eq!( + toml_string_at(toml_table_at(&cargo_toml, "package")?, "name")?, + "artifact_widgets_core" + ); + assert_eq!( + toml_string_at(toml_table_at(&cargo_toml, "lib")?, "path")?, + "src/lib.rs" + ); + + Ok(()) +} + +#[test] +fn generated_pub_dependency_consumer_artifact_matches_baseline() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let producer_root = tmp.path().join("artifact_widgets_project"); + let producer_src = producer_root.join("src"); + fs::create_dir_all(&producer_src)?; + fs::write( + producer_root.join("incan.toml"), + "[project]\nname = \"artifact_widgets_core\"\nversion = \"0.1.0\"\n", + )?; + write_fixture(&producer_src.join("widgets.incn"), "library_widgets.incn")?; + write_fixture(&producer_src.join("lib.incn"), "library_lib.incn")?; + + let producer_build = run_incan(&producer_root, &["build", "--lib"])?; + assert_success(&producer_build, "incan build --lib producer artifact"); + + let consumer_root = tmp.path().join("artifact_consumer_project"); + let consumer_src = consumer_root.join("src"); + fs::create_dir_all(&consumer_src)?; + fs::write( + consumer_root.join("incan.toml"), + "[project]\nname = \"artifact_consumer\"\nversion = \"0.1.0\"\n\n[dependencies]\nwidgets = { path = \"../artifact_widgets_project\" }\n", + )?; + write_fixture(&consumer_src.join("main.incn"), "consumer_main.incn")?; + + let out_dir = consumer_root.join("out"); + let main_arg = consumer_src + .join("main.incn") + .to_str() + .ok_or("consumer source path was not valid UTF-8")? + .to_string(); + let out_arg = out_dir + .to_str() + .ok_or("consumer output path was not valid UTF-8")? + .to_string(); + let consumer_build = run_incan(&consumer_root, &["build", &main_arg, &out_arg])?; + assert_success(&consumer_build, "incan build pub dependency consumer artifact"); + + assert_required_files(&out_dir, "consumer_required_files.txt")?; + assert_contains_fragments(&out_dir.join("src").join("main.rs"), "consumer_main_rs.fragments")?; + + let generated_toml = fs::read_to_string(out_dir.join("Cargo.toml"))?; + assert!( + generated_toml.contains("[dependencies.widgets]"), + "expected dependency alias table, got:\n{generated_toml}" + ); + assert!( + generated_toml.contains("package = \"artifact_widgets_core\""), + "expected dependency package mapping, got:\n{generated_toml}" + ); + assert!( + generated_toml.contains("path = "), + "expected path dependency to generated library artifact, got:\n{generated_toml}" + ); + + let generated_main_rs = fs::read_to_string(out_dir.join("src").join("main.rs"))?; + assert!( + !generated_main_rs.contains("pub use widgets::Widget as PublicWidget;"), + "private pub:: alias import should not become a public Rust reexport, got:\n{generated_main_rs}" + ); + assert!( + !generated_main_rs.contains("pub use widgets::make_widget;"), + "private pub:: item import should not become a public Rust reexport, got:\n{generated_main_rs}" + ); + + Ok(()) +} diff --git a/tests/generated_rust_audit_tests.rs b/tests/generated_rust_audit_tests.rs new file mode 100644 index 000000000..b39f46f00 --- /dev/null +++ b/tests/generated_rust_audit_tests.rs @@ -0,0 +1,185 @@ +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +fn repo_root() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) +} + +fn audit_script() -> PathBuf { + repo_root().join("scripts/generated_rust_audit.py") +} + +fn fixture_path(relative: &str) -> PathBuf { + repo_root().join("tests/fixtures/generated_rust_audit").join(relative) +} + +fn run_audit(args: &[&str]) -> Result> { + Ok(Command::new("python3") + .arg(audit_script()) + .args(args) + .current_dir(repo_root()) + .output()?) +} + +fn path_string(path: &Path) -> Result> { + Ok(path + .to_str() + .ok_or_else(|| format!("path was not valid UTF-8: {}", path.display()))? + .to_owned()) +} + +fn assert_success(output: &Output, context: &str) { + assert!( + output.status.success(), + "{context} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn assert_failure(output: &Output, context: &str) { + assert!( + !output.status.success(), + "{context} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn artifact<'a>( + json: &'a serde_json::Value, + surface_class: &str, +) -> Result<&'a serde_json::Value, Box> { + json["artifacts"] + .as_array() + .ok_or("report artifacts field was not an array")? + .iter() + .find(|artifact| artifact["surface_class"].as_str() == Some(surface_class)) + .ok_or_else(|| format!("missing artifact for surface class `{surface_class}`").into()) +} + +#[test] +fn json_report_records_explicit_artifacts_and_marker_counts() -> Result<(), Box> { + let file_fixture = fixture_path("main.rs"); + let dir_fixture = fixture_path("nested"); + let file_spec = format!("program-main={}", path_string(&file_fixture)?); + let dir_spec = format!("stdlib-copy={}", path_string(&dir_fixture)?); + + let output = run_audit(&["--format", "json", "--artifact", &file_spec, "--artifact", &dir_spec])?; + assert_success(&output, "generated Rust audit JSON report"); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout)?; + assert_eq!(json["report"], "generated-rust-strict-surface"); + assert!(json["generated_at"].as_str().is_some()); + + let program = artifact(&json, "program-main")?; + assert_eq!(program["artifact_path"], "tests/fixtures/generated_rust_audit/main.rs"); + assert_eq!(program["check_status"], "present"); + assert_eq!(program["strictness_status"], "available_for_review"); + assert_eq!( + program["rust_files"], + serde_json::json!(["tests/fixtures/generated_rust_audit/main.rs"]) + ); + assert_eq!(program["clone"]["status"], "pending_manual_review"); + assert_eq!(program["clone"]["marker_count"], 3); + assert_eq!(program["allocation"]["marker_count"], 3); + assert_eq!(program["eager_collection"]["marker_count"], 1); + assert_eq!(program["clone"]["markers"][0]["line"], 3); + assert_eq!(program["clone"]["markers"][0]["pattern"], ".clone("); + + let stdlib = artifact(&json, "stdlib-copy")?; + assert_eq!(stdlib["artifact_path"], "tests/fixtures/generated_rust_audit/nested"); + assert_eq!( + stdlib["rust_files"], + serde_json::json!(["tests/fixtures/generated_rust_audit/nested/lib.rs"]) + ); + assert_eq!(stdlib["clone"]["marker_count"], 1); + assert_eq!(stdlib["allocation"]["marker_count"], 2); + assert_eq!(stdlib["eager_collection"]["marker_count"], 1); + + let stdout = String::from_utf8(output.stdout)?; + assert!( + !stdout.contains(repo_root().to_str().ok_or("repo root was not valid UTF-8")?), + "repo-contained artifact paths should be rendered relative to the repository root:\n{stdout}" + ); + + Ok(()) +} + +#[test] +fn markdown_report_contains_objective_rows_and_details() -> Result<(), Box> { + let fixture = fixture_path("main.rs"); + let spec = format!("program-main={}", path_string(&fixture)?); + let output = run_audit(&["--artifact", &spec])?; + assert_success(&output, "generated Rust audit Markdown report"); + + let markdown = String::from_utf8(output.stdout)?; + assert!(markdown.contains("# Generated Rust Strict Surface Report")); + assert!(markdown.contains("| `program-main` | `tests/fixtures/generated_rust_audit/main.rs` | present | available_for_review | 3 marker(s); notes: pending | 3 marker(s); notes: pending | 1 marker(s); notes: pending |")); + assert!(markdown.contains("## Artifact Details")); + assert!(markdown.contains("- Clone notes: status=`pending_manual_review`, markers=3, notes=pending")); + assert!(!markdown.contains("score")); + assert!( + !markdown.contains(repo_root().to_str().ok_or("repo root was not valid UTF-8")?), + "Markdown report should not leak absolute repo paths for repo-contained artifacts:\n{markdown}" + ); + + Ok(()) +} + +#[test] +fn missing_and_no_rust_artifacts_are_reported_without_failing_by_default() -> Result<(), Box> { + let missing_spec = "missing-surface=tests/fixtures/generated_rust_audit/missing.rs"; + let no_rust_spec = "notes-only=tests/fixtures/generated_rust_audit/no_rust_dir"; + + let output = run_audit(&[ + "--format", + "json", + "--artifact", + missing_spec, + "--artifact", + no_rust_spec, + ])?; + assert_success(&output, "generated Rust audit missing artifact report"); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout)?; + let missing = artifact(&json, "missing-surface")?; + assert_eq!( + missing["artifact_path"], + "tests/fixtures/generated_rust_audit/missing.rs" + ); + assert_eq!(missing["check_status"], "missing"); + assert_eq!(missing["strictness_status"], "not_evaluated"); + assert_eq!(missing["clone"]["status"], "not_available"); + assert_eq!(missing["clone"]["marker_count"], 0); + assert_eq!(missing["message"], "artifact path does not exist"); + + let notes_only = artifact(&json, "notes-only")?; + assert_eq!(notes_only["check_status"], "no-rust-files"); + assert_eq!(notes_only["strictness_status"], "not_evaluated"); + assert_eq!( + notes_only["message"], + "artifact directory contains no Rust source files" + ); + + Ok(()) +} + +#[test] +fn fail_on_missing_exits_nonzero_after_emitting_report() -> Result<(), Box> { + let output = run_audit(&[ + "--format", + "json", + "--fail-on-missing", + "--artifact", + "missing-surface=tests/fixtures/generated_rust_audit/missing.rs", + ])?; + assert_failure(&output, "generated Rust audit --fail-on-missing"); + + let json: serde_json::Value = serde_json::from_slice(&output.stdout)?; + let missing = artifact(&json, "missing-surface")?; + assert_eq!(missing["check_status"], "missing"); + assert!(output.stderr.is_empty()); + + Ok(()) +} diff --git a/tests/generated_rust_callability_artifact_tests.rs b/tests/generated_rust_callability_artifact_tests.rs new file mode 100644 index 000000000..8efe1ad67 --- /dev/null +++ b/tests/generated_rust_callability_artifact_tests.rs @@ -0,0 +1,265 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +use incan::library_manifest::{LibraryManifest, TypeRef}; + +fn incan_binary() -> PathBuf { + if let Ok(path) = std::env::var("CARGO_BIN_EXE_incan") { + return PathBuf::from(path); + } + if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") { + let path = PathBuf::from(target_dir).join("debug").join("incan"); + if path.exists() { + return path; + } + } + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/debug/incan") +} + +fn run_incan(current_dir: &Path, args: &[&str]) -> Result> { + Ok(Command::new(incan_binary()) + .args(args) + .current_dir(current_dir) + .env("CARGO_NET_OFFLINE", "true") + .env("INCAN_NO_BANNER", "1") + .output()?) +} + +fn assert_success(output: &Output, context: &str) { + assert!( + output.status.success(), + "{context} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn assert_failure(output: &Output, context: &str) { + assert!( + !output.status.success(), + "{context} unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn strip_ansi_escapes(text: &str) -> String { + let mut out = String::with_capacity(text.len()); + let mut chars = text.chars().peekable(); + while let Some(ch) = chars.next() { + if ch == '\u{1b}' && chars.peek() == Some(&'[') { + let _ = chars.next(); + for c in chars.by_ref() { + if c == 'm' { + break; + } + } + continue; + } + out.push(ch); + } + out +} + +fn write_fixture_file(root: &Path, relative_path: &str, contents: &str) -> Result<(), Box> { + let path = root.join(relative_path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, contents)?; + Ok(()) +} + +fn write_producer(root: &Path) -> Result> { + let producer = root.join("callability_lib"); + write_fixture_file( + &producer, + "incan.toml", + include_str!("fixtures/generated_rust_callability/producer/incan.toml"), + )?; + write_fixture_file( + &producer, + "src/transforms.incn", + include_str!("fixtures/generated_rust_callability/producer/src/transforms.incn"), + )?; + write_fixture_file( + &producer, + "src/lib.incn", + include_str!("fixtures/generated_rust_callability/producer/src/lib.incn"), + )?; + Ok(producer) +} + +fn build_producer(root: &Path) -> Result> { + let producer = write_producer(root)?; + let output = run_incan(&producer, &["build", "--lib"])?; + assert_success(&output, "producer incan build --lib"); + Ok(producer) +} + +fn write_consumer( + root: &Path, + dir_name: &str, + main_source: &str, +) -> Result<(PathBuf, PathBuf), Box> { + let consumer = root.join(dir_name); + write_fixture_file( + &consumer, + "incan.toml", + "[project]\nname = \"consumer\"\nversion = \"0.1.0\"\n\n[dependencies]\ncallability = { path = \"../callability_lib\" }\n", + )?; + write_fixture_file(&consumer, "src/main.incn", main_source)?; + Ok((consumer.clone(), consumer.join("src/main.incn"))) +} + +fn function_param_ty<'a>( + manifest: &'a LibraryManifest, + function_name: &str, + param_name: &str, +) -> Result<&'a TypeRef, Box> { + let function = manifest + .exports + .functions + .iter() + .find(|function| function.name == function_name) + .ok_or_else(|| format!("expected function export `{function_name}`"))?; + Ok(&function + .params + .iter() + .find(|param| param.name == param_name) + .ok_or_else(|| format!("expected parameter `{param_name}` on `{function_name}`"))? + .ty) +} + +#[test] +fn build_lib_emits_package_facing_callable_artifact_layout() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let producer = build_producer(tmp.path())?; + let artifact = producer.join("target/lib"); + + let cargo_toml = fs::read_to_string(artifact.join("Cargo.toml"))?; + assert!( + cargo_toml.contains("name = \"callability_core\""), + "expected generated Cargo package name, got:\n{cargo_toml}" + ); + + let lib_rs = fs::read_to_string(artifact.join("src/lib.rs"))?; + assert!( + lib_rs.contains("pub use crate::transforms::map_owned;") + && lib_rs.contains("pub use crate::transforms::inspect_payload;"), + "expected callable exports re-exported from generated package root, got:\n{lib_rs}" + ); + + let transforms_rs = fs::read_to_string(artifact.join("src/transforms.rs"))?; + assert!( + transforms_rs.contains("pub fn map_owned(items: Vec, f: fn(i64) -> i64) -> Vec"), + "expected owned scalar callable to lower to an exported fn pointer parameter, got:\n{transforms_rs}" + ); + assert!( + transforms_rs.contains("out.push(f(item));"), + "expected owned scalar callable invocation in generated package artifact, got:\n{transforms_rs}" + ); + assert!( + transforms_rs.contains("f: fn(&Payload) -> ()") && transforms_rs.contains("f(&value);"), + "expected non-Copy payload observer to lower as borrowed callable in generated package artifact, got:\n{transforms_rs}" + ); + + let manifest = LibraryManifest::read_from_path(&artifact.join("callability_core.incnlib"))?; + assert!(matches!( + function_param_ty(&manifest, "map_owned", "f")?, + TypeRef::Function { params, return_type } + if matches!(params.as_slice(), [TypeRef::Named { name }] if name == "int") + && matches!(&**return_type, TypeRef::Named { name } if name == "int") + )); + assert!(matches!( + function_param_ty(&manifest, "inspect_payload", "f")?, + TypeRef::Function { params, return_type } + if matches!(params.as_slice(), [TypeRef::Named { name }] if name == "Payload") + && matches!(&**return_type, TypeRef::Named { name } if name == "Unit") + )); + Ok(()) +} + +#[test] +fn consumer_can_call_owned_callable_export_across_generated_package_boundary() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + build_producer(tmp.path())?; + let (consumer, main_path) = write_consumer( + tmp.path(), + "owned_consumer", + include_str!("fixtures/generated_rust_callability/consumer_owned/src/main.incn"), + )?; + + let out_dir = consumer.join("out"); + let build_output = run_incan( + &consumer, + &[ + "build", + main_path.to_str().ok_or("main path was not valid UTF-8")?, + out_dir.to_str().ok_or("out path was not valid UTF-8")?, + ], + )?; + assert_success(&build_output, "consumer incan build for owned callable import"); + + let generated_toml = fs::read_to_string(out_dir.join("Cargo.toml"))?; + assert!( + generated_toml.contains("[dependencies.callability]") + && generated_toml.contains("package = \"callability_core\"") + && generated_toml.contains("callability_lib/target/lib"), + "expected consumer generated Cargo.toml to depend on producer target/lib, got:\n{generated_toml}" + ); + let generated_main = fs::read_to_string(out_dir.join("src/main.rs"))?; + assert!( + generated_main.contains("use callability::map_owned;") + && generated_main.contains("use callability::plus_one;") + && generated_main.contains("map_owned(vec![1, 2, 3], plus_one)"), + "expected final generated Rust project to call imported callable export, got:\n{generated_main}" + ); + + let run_output = run_incan( + &consumer, + &["run", main_path.to_str().ok_or("main path was not valid UTF-8")?], + )?; + assert_success(&run_output, "consumer incan run for owned callable import"); + assert_eq!(String::from_utf8_lossy(&run_output.stdout).trim(), "2\n3\n4"); + Ok(()) +} + +#[test] +fn borrowed_callable_export_is_characterized_as_current_pub_consumer_blocker() -> Result<(), Box> +{ + let tmp = tempfile::tempdir()?; + build_producer(tmp.path())?; + let (consumer, main_path) = write_consumer( + tmp.path(), + "borrowed_consumer", + include_str!("fixtures/generated_rust_callability/consumer_borrowed_blocker/src/main.incn"), + )?; + + let out_dir = consumer.join("out"); + let build_output = run_incan( + &consumer, + &[ + "build", + main_path.to_str().ok_or("main path was not valid UTF-8")?, + out_dir.to_str().ok_or("out path was not valid UTF-8")?, + ], + )?; + assert_failure(&build_output, "consumer incan build for borrowed callable import"); + + let stderr = strip_ansi_escapes(&String::from_utf8_lossy(&build_output.stderr)); + assert!( + stderr.contains("expected fn pointer") && stderr.contains("found fn item") && stderr.contains("observe"), + "expected borrowed callable mismatch to document current pub consumer blocker, got:\n{stderr}" + ); + let generated_main = fs::read_to_string(out_dir.join("src/main.rs"))?; + assert!( + generated_main.contains("fn observe(_: Payload)") + && generated_main.contains("inspect_payload(") + && generated_main.contains(", observe)"), + "expected final generated Rust project to show consumer observer shape before Cargo type failure, got:\n{generated_main}" + ); + Ok(()) +} diff --git a/tests/generated_rust_native_consumer_tests.rs b/tests/generated_rust_native_consumer_tests.rs new file mode 100644 index 000000000..c76bc15be --- /dev/null +++ b/tests/generated_rust_native_consumer_tests.rs @@ -0,0 +1,118 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; + +fn incan_binary() -> PathBuf { + if let Ok(path) = std::env::var("CARGO_BIN_EXE_incan") { + return PathBuf::from(path); + } + if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") { + let path = PathBuf::from(target_dir).join("debug").join("incan"); + if path.exists() { + return path; + } + } + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target/debug/incan") +} + +fn run_incan(current_dir: &Path, args: &[&str]) -> Result> { + Ok(Command::new(incan_binary()) + .args(args) + .current_dir(current_dir) + .env("CARGO_NET_OFFLINE", "true") + .env("INCAN_NO_BANNER", "1") + .output()?) +} + +fn run_cargo(current_dir: &Path, args: &[&str], target_dir: &Path) -> Result> { + Ok(Command::new("cargo") + .args(args) + .current_dir(current_dir) + .env("CARGO_NET_OFFLINE", "true") + .env("CARGO_TARGET_DIR", target_dir) + .output()?) +} + +fn assert_success(output: &Output, context: &str) { + assert!( + output.status.success(), + "{context} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} + +fn write_fixture_file(root: &Path, relative_path: &str, contents: &str) -> Result<(), Box> { + let path = root.join(relative_path); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, contents)?; + Ok(()) +} + +fn write_producer(root: &Path) -> Result> { + let producer = root.join("native_items"); + write_fixture_file( + &producer, + "incan.toml", + include_str!("fixtures/generated_rust_native_consumer/producer/incan.toml"), + )?; + write_fixture_file( + &producer, + "src/lib.incn", + include_str!("fixtures/generated_rust_native_consumer/producer/src/lib.incn"), + )?; + write_fixture_file( + &producer, + "src/counters.incn", + include_str!("fixtures/generated_rust_native_consumer/producer/src/counters.incn"), + )?; + Ok(producer) +} + +fn write_consumer(root: &Path) -> Result> { + let consumer = root.join("native_consumer"); + write_fixture_file( + &consumer, + "Cargo.toml", + include_str!("fixtures/generated_rust_native_consumer/consumer/Cargo.toml"), + )?; + write_fixture_file( + &consumer, + "src/lib.rs", + include_str!("fixtures/generated_rust_native_consumer/consumer/src/lib.rs"), + )?; + Ok(consumer) +} + +#[test] +fn native_rust_consumer_can_call_generated_public_items() -> Result<(), Box> { + let tmp = tempfile::tempdir()?; + let producer = write_producer(tmp.path())?; + + let build_output = run_incan(&producer, &["build", "--lib"])?; + assert_success(&build_output, "incan build --lib native consumer producer"); + + let artifact_root = producer.join("target/lib"); + assert!( + artifact_root.join("Cargo.toml").is_file(), + "expected generated Rust library Cargo.toml at {}", + artifact_root.display() + ); + assert!( + artifact_root.join("src/lib.rs").is_file(), + "expected generated Rust library root at {}", + artifact_root.join("src/lib.rs").display() + ); + + let consumer = write_consumer(tmp.path())?; + let cargo_test = run_cargo( + &consumer, + &["test", "--offline"], + &tmp.path().join("native-cargo-target"), + )?; + assert_success(&cargo_test, "native Rust cargo test against generated library"); + + Ok(()) +} diff --git a/tests/snapshots/codegen_snapshot_tests__filtered_dict_comp_predicate.snap b/tests/snapshots/codegen_snapshot_tests__filtered_dict_comp_predicate.snap index 965c40876..0efc098c6 100644 --- a/tests/snapshots/codegen_snapshot_tests__filtered_dict_comp_predicate.snap +++ b/tests/snapshots/codegen_snapshot_tests__filtered_dict_comp_predicate.snap @@ -23,7 +23,7 @@ fn main() { let evens = xs .iter() .filter_map(|x| { - let x = (*x).clone(); + let x = *x; if incan_stdlib::num::py_mod_i64(x, 2) == 0 { Some((x, x * x)) } else { diff --git a/tests/snapshots/codegen_snapshot_tests__issue383_loop_helper_shared_string_list.snap b/tests/snapshots/codegen_snapshot_tests__issue383_loop_helper_shared_string_list.snap index b9fffacc4..1a330cd4f 100644 --- a/tests/snapshots/codegen_snapshot_tests__issue383_loop_helper_shared_string_list.snap +++ b/tests/snapshots/codegen_snapshot_tests__issue383_loop_helper_shared_string_list.snap @@ -28,7 +28,7 @@ fn helper_loop(xs: Vec, ys: Vec) -> Vec { return out.clone(); } fn helper_list(xs: Vec, ys: Vec) -> Vec { - return (ys).iter().cloned().map(|y| match_index(xs.clone(), y)).collect::>(); + return (ys).iter().copied().map(|y| match_index(xs.clone(), y)).collect::>(); } fn main() { std::panic::set_hook( diff --git a/tests/snapshots/codegen_snapshot_tests__issue602_comprehension_copy_hotpaths.snap b/tests/snapshots/codegen_snapshot_tests__issue602_comprehension_copy_hotpaths.snap new file mode 100644 index 000000000..cdd88a70f --- /dev/null +++ b/tests/snapshots/codegen_snapshot_tests__issue602_comprehension_copy_hotpaths.snap @@ -0,0 +1,50 @@ +--- +source: tests/codegen_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +fn squares(xs: Vec) -> Vec { + return (xs).iter().copied().map(|x| x * x).collect::>(); +} +fn positive_squares(xs: Vec) -> Vec { + return (xs) + .iter() + .filter_map(|x| { + let x = *x; + if x > 0 { Some(x * x) } else { None } + }) + .collect::>(); +} +fn index_by_value(xs: Vec) -> std::collections::HashMap { + return xs + .iter() + .filter_map(|x| { + let x = *x; + if x > 0 { Some((x, x * x)) } else { None } + }) + .collect::>(); +} +fn main() { + std::panic::set_hook( + std::boxed::Box::new(|panic_info| { + if let Some(message) = panic_info.payload().downcast_ref::<&str>() { + eprintln!("{message}"); + } else if let Some(message) = panic_info.payload().downcast_ref::() { + eprintln!("{message}"); + } else { + eprintln!("generated program panicked"); + } + }), + ); + let values = vec![1, 2, 3]; + let copied = squares(values.clone()); + let filtered = positive_squares(values.clone()); + let indexed = index_by_value(values); + println!("{}", * incan_stdlib::collections::list_get(& copied, (0) as i64)); + println!("{}", * incan_stdlib::collections::list_get(& filtered, (0) as i64)); + println!("{}", * incan_stdlib::collections::dict_get(& indexed, & 1)); +} diff --git a/tests/snapshots/codegen_snapshot_tests__issue602_owned_iterator_source_hotpaths.snap b/tests/snapshots/codegen_snapshot_tests__issue602_owned_iterator_source_hotpaths.snap new file mode 100644 index 000000000..f2eb8e731 --- /dev/null +++ b/tests/snapshots/codegen_snapshot_tests__issue602_owned_iterator_source_hotpaths.snap @@ -0,0 +1,122 @@ +--- +source: tests/codegen_snapshot_tests.rs +assertion_line: 1682 +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +fn double(x: i64) -> i64 { + return x * 2; +} +fn collect_last_use(xs: Vec) -> Vec { + return { + let mut __incan_iter = (crate::__incan_std::derives::collection::MapIterator { + source: (crate::__incan_std::derives::collection::ListIterator { + items: (xs), + index: 0i64, + }), + f: double, + }); + let mut __incan_items = Vec::new(); + loop { + match crate::__incan_std::derives::collection::Iterator::__next__( + &mut __incan_iter, + ) { + Some(__incan_item) => __incan_items.push(__incan_item), + None => break __incan_items, + } + } + }; +} +fn generator_last_use(xs: Vec) -> incan_stdlib::iter::Generator { + return incan_stdlib::iter::Generator::new( + ((xs).clone().into_iter()) + .flat_map(move |x| { + incan_stdlib::iter::Generator::new( + if x > 0 { + incan_stdlib::iter::Generator::new(std::iter::once(x)) + } else { + incan_stdlib::iter::Generator::new(std::iter::empty()) + }, + ) + }), + ); +} +fn generator_one_shot() -> incan_stdlib::iter::Generator { + return incan_stdlib::iter::Generator::new( + ((vec![1, 2, 3]).into_iter()) + .flat_map(move |x| { + incan_stdlib::iter::Generator::new( + if x > 0 { + incan_stdlib::iter::Generator::new(std::iter::once(x)) + } else { + incan_stdlib::iter::Generator::new(std::iter::empty()) + }, + ) + }), + ); +} +fn collect_reused(values: Vec) -> i64 { + let total = { + let mut __incan_iter = (crate::__incan_std::derives::collection::ListIterator { + items: (values).clone(), + index: 0i64, + }); + let mut __incan_sum: i64 = 0i64; + loop { + match crate::__incan_std::derives::collection::Iterator::__next__( + &mut __incan_iter, + ) { + Some(__incan_item) => __incan_sum += __incan_item, + None => break __incan_sum, + } + } + }; + let first = *incan_stdlib::collections::list_get(&values, (0) as i64); + return total + first; +} +fn generator_reused(values: Vec) -> i64 { + let generated = incan_stdlib::iter::Generator::new( + ((values).clone().into_iter()) + .flat_map(move |x| { + incan_stdlib::iter::Generator::new( + if x > 0 { + incan_stdlib::iter::Generator::new(std::iter::once(x)) + } else { + incan_stdlib::iter::Generator::new(std::iter::empty()) + }, + ) + }), + ) + .collect(); + let first = *incan_stdlib::collections::list_get(&values, (0) as i64); + return ::std::convert::identity(generated.len() as i64) + first; +} +fn main() { + std::panic::set_hook( + std::boxed::Box::new(|panic_info| { + if let Some(message) = panic_info.payload().downcast_ref::<&str>() { + eprintln!("{message}"); + } else if let Some(message) = panic_info.payload().downcast_ref::() { + eprintln!("{message}"); + } else { + eprintln!("generated program panicked"); + } + }), + ); + let collected = collect_last_use(vec![1, 2, 3]); + let generated = generator_last_use(vec![1, 2, 3]).collect(); + let generated_direct = generator_one_shot().collect(); + let reused_iter = collect_reused(vec![1, 2, 3]); + let reused_gen = generator_reused(vec![1, 2, 3]); + println!("{}", * incan_stdlib::collections::list_get(& collected, (0) as i64)); + println!("{}", * incan_stdlib::collections::list_get(& generated, (0) as i64)); + println!( + "{}", * incan_stdlib::collections::list_get(& generated_direct, (0) as i64) + ); + println!("{}", reused_iter); + println!("{}", reused_gen); +} diff --git a/tests/snapshots/codegen_snapshot_tests__rfc088_iterator_adapters.snap b/tests/snapshots/codegen_snapshot_tests__rfc088_iterator_adapters.snap index ddcd57db1..4c155c674 100644 --- a/tests/snapshots/codegen_snapshot_tests__rfc088_iterator_adapters.snap +++ b/tests/snapshots/codegen_snapshot_tests__rfc088_iterator_adapters.snap @@ -1,6 +1,6 @@ --- source: tests/codegen_snapshot_tests.rs -assertion_line: 1057 +assertion_line: 1228 expression: rust_code --- // Generated by the Incan compiler v @@ -44,7 +44,7 @@ impl PositiveInt { fn sum_money(items: Vec) -> Money { return { let mut __incan_iter = (crate::__incan_std::derives::collection::ListIterator { - items: (items).clone(), + items: (items), index: 0i64, }); let mut __incan_sum: i64 = 0i64; @@ -61,7 +61,7 @@ fn sum_money(items: Vec) -> Money { fn sum_positive(items: Vec) -> PositiveInt { return { let mut __incan_iter = (crate::__incan_std::derives::collection::ListIterator { - items: (items).clone(), + items: (items), index: 0i64, }); let mut __incan_sum: i64 = 0i64; @@ -184,7 +184,7 @@ fn main() { }; let total = { let mut __incan_iter = (crate::__incan_std::derives::collection::ListIterator { - items: (xs).clone(), + items: (xs), index: 0i64, }); let mut __incan_sum: i64 = 0i64; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_async_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_async_prelude_import.snap new file mode 100644 index 000000000..a2d167b21 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_async_prelude_import.snap @@ -0,0 +1,15 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::r#async; +pub use crate::__incan_std::r#async::sleep; +pub use crate::__incan_std::r#async::spawn; +pub use crate::__incan_std::r#async::JoinHandle; +pub use crate::__incan_std::r#async::Mutex; +pub use crate::__incan_std::r#async::RaceArm; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_core_source.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_core_source.snap new file mode 100644 index 000000000..e08dfd2a4 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_core_source.snap @@ -0,0 +1,376 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +#[derive(Debug, Clone)] +pub enum __IncanUnionf13d01fefab04049 { + V0(File), + V1(_BytesIO), +} +pub use crate::__incan_std::fs::File; +use crate::__incan_std::io::_BytesIO; +pub use crate::__incan_std::traits::error::Error; +#[derive(Debug, Clone, PartialEq)] +pub enum Codec { + Gzip, + Zlib, + Deflate, + Zstd, + Bz2, + Lzma, + Snappy, +} +impl Codec { + pub fn message(&self) -> String { + match self { + Self::Gzip => "Gzip".to_string(), + Self::Zlib => "Zlib".to_string(), + Self::Deflate => "Deflate".to_string(), + Self::Zstd => "Zstd".to_string(), + Self::Bz2 => "Bz2".to_string(), + Self::Lzma => "Lzma".to_string(), + Self::Snappy => "Snappy".to_string(), + } + } +} +impl Codec { + pub fn value(&self) -> String { + match self { + Self::Gzip => "gzip".to_string(), + Self::Zlib => "zlib".to_string(), + Self::Deflate => "deflate".to_string(), + Self::Zstd => "zstd".to_string(), + Self::Bz2 => "bz2".to_string(), + Self::Lzma => "lzma".to_string(), + Self::Snappy => "snappy".to_string(), + } + } + pub fn from_value(value: impl AsRef) -> Option { + match value.as_ref() { + "gzip" => Some(Self::Gzip), + "zlib" => Some(Self::Zlib), + "deflate" => Some(Self::Deflate), + "zstd" => Some(Self::Zstd), + "bz2" => Some(Self::Bz2), + "lzma" => Some(Self::Lzma), + "snappy" => Some(Self::Snappy), + _ => None, + } + } +} +impl std::fmt::Display for Codec { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Gzip => formatter.write_str("gzip"), + Self::Zlib => formatter.write_str("zlib"), + Self::Deflate => formatter.write_str("deflate"), + Self::Zstd => formatter.write_str("zstd"), + Self::Bz2 => formatter.write_str("bz2"), + Self::Lzma => formatter.write_str("lzma"), + Self::Snappy => formatter.write_str("snappy"), + } + } +} +impl std::str::FromStr for Codec { + type Err = String; + fn from_str(value: &str) -> Result { + Self::from_value(value) + .ok_or_else(|| { + format!("invalid value for {}: {}", stringify!(Codec), value) + }) + } +} +impl Codec { + pub fn all() -> Vec { + "\n Return the stable codec order used by default autodetection filters.\n\n Raw Snappy block compression is intentionally absent because it has no framing signature.\n "; + return vec![ + Codec::Gzip.clone(), Codec::Zlib.clone(), Codec::Deflate.clone(), Codec::Zstd + .clone(), Codec::Bz2.clone(), Codec::Lzma.clone(), Codec::Snappy.clone() + ]; + } +} +#[derive(Clone, Debug, incan_derive::FieldInfo, incan_derive::IncanClass)] +pub struct CompressionError { + pub kind: String, + pub codec: Option, + pub operation: String, + pub detail: String, +} +impl CompressionError { + pub fn message(&self) -> String { + "\n Return the human-readable error detail.\n "; + return self.detail.clone(); + } + pub fn source(&self) -> Option { + "\n Return a structured source error when one is available.\n\n Compression currently normalizes backend details into `detail`, so there is no nested source value.\n "; + return None::<_>; + } + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 4] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("kind"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("kind"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("codec"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("codec"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[Codec]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("operation"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("operation"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("detail"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("detail"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Error for CompressionError { + fn message(&self) -> String { + "\n Return the human-readable error detail.\n "; + return self.detail.clone(); + } + fn source(&self) -> Option { + "\n Return a structured source error when one is available.\n\n Compression currently normalizes backend details into `detail`, so there is no nested source value.\n "; + return None::<_>; + } +} +pub fn _level_or_default( + codec: Codec, + level: Option, + default: i64, + minimum: i64, + maximum: i64, +) -> Result { + "\n Validate a portable integer compression-level request.\n "; + match level { + Some(value) => { + if value < minimum || value > maximum { + return Err::< + i64, + CompressionError, + >(CompressionError { + kind: "invalid_level".to_string(), + codec: Some(codec.clone()), + operation: "compress".to_string(), + detail: { + let __parts: [&str; 4usize] = [ + "level for ", + " must be between ", + " and ", + "", + ]; + let __args: Vec = vec![ + format!("{}", codec.value()), format!("{}", minimum), + format!("{}", maximum) + ]; + incan_stdlib::strings::fstring(&__parts, &__args) + }, + }); + } + return Ok::(value); + } + None => { + return Ok::(default); + } + }; +} +pub fn _reject_level(codec: Codec, level: Option) -> Result<(), CompressionError> { + "\n Reject a level argument for codecs without level support.\n "; + match level { + Some(_) => { + return Err::< + (), + CompressionError, + >(CompressionError { + kind: "unsupported_option".to_string(), + codec: Some(codec.clone()), + operation: "compress".to_string(), + detail: { + let __parts: [&str; 2usize] = [ + "", + " does not support configurable compression levels", + ]; + let __args: Vec = vec![format!("{}", codec.value())]; + incan_stdlib::strings::fstring(&__parts, &__args) + }, + }); + } + None => { + return Ok::<(), CompressionError>(()); + } + }; +} +pub fn _codec_error( + codec: Codec, + operation: String, + detail: String, +) -> CompressionError { + "\n Build a codec failure from backend error text.\n\n Backend crates expose different error enums and messages. The first source pass keeps a stable Incan `kind` by\n classifying the most common invalid/truncated markers and otherwise preserving the backend text as `backend`.\n "; + let mut kind = "backend".to_string(); + if incan_stdlib::strings::str_contains(&detail, &"EOF") + || incan_stdlib::strings::str_contains(&detail, &"eof") + || incan_stdlib::strings::str_contains(&detail, &"early") + || incan_stdlib::strings::str_contains(&detail, &"truncated") + { + kind = "truncated_input".to_string(); + } else { + if incan_stdlib::strings::str_contains(&detail, &"invalid") + || incan_stdlib::strings::str_contains(&detail, &"corrupt") + || incan_stdlib::strings::str_contains(&detail, &"checksum") + { + kind = "invalid_data".to_string(); + } + } + return CompressionError { + kind: kind.to_string(), + codec: Some(codec), + operation: operation, + detail: detail, + }; +} +pub fn _io_error( + codec: Option, + operation: String, + detail: String, +) -> CompressionError { + "\n Build an I/O failure.\n "; + return CompressionError { + kind: "io".to_string(), + codec: codec, + operation: operation, + detail: detail, + }; +} +pub fn _validate_chunk_size( + codec: Option, + operation: String, + chunk_size: i64, +) -> Result<(), CompressionError> { + "\n Reject a non-positive stream chunk size before touching source or target streams.\n "; + if chunk_size <= 0 { + return Err::< + (), + CompressionError, + >(CompressionError { + kind: "invalid_chunk_size".to_string(), + codec: codec, + operation: operation, + detail: "chunk_size must be positive".to_string(), + }); + } + return Ok::<(), CompressionError>(()); +} +pub fn _write_sink_bytes( + codec: Option, + operation: String, + target: __IncanUnionf13d01fefab04049, + data: Vec, +) -> Result<(), CompressionError> { + "\n Write one compressed or decompressed chunk to a `BytesIO` or `File` sink.\n\n Stream helpers keep codec adapters in their codec modules, but use this shared sink boundary so `IoError`\n normalization stays identical across modules.\n "; + match target.clone() { + __IncanUnionf13d01fefab04049::V1(target) => { + match target.write_bytes(data) { + Ok(_) => { + return Ok::<(), CompressionError>(()); + } + Err(err) => { + return Err::< + (), + CompressionError, + >(_io_error(codec.clone(), operation.to_string(), err.message())); + } + }; + } + __IncanUnionf13d01fefab04049::V0(target) => { + match target.write_bytes(data) { + Ok(_) => { + return Ok::<(), CompressionError>(()); + } + Err(err) => { + return Err::< + (), + CompressionError, + >(_io_error(codec, operation, err.message())); + } + }; + } + } +} +pub fn _byte_at(data: Vec, index: i64) -> i64 { + "\n Read one byte as an integer for signature checks.\n "; + return (data[(index) as usize]) as i64; +} +pub fn _looks_like_gzip(data: Vec) -> bool { + "\n Return whether `data` starts with the gzip magic bytes.\n "; + return ::std::convert::identity(data.len() as i64) >= 2 + && _byte_at(data.clone(), 0) == 31 && _byte_at(data, 1) == 139; +} +pub fn _looks_like_zstd(data: Vec) -> bool { + "\n Return whether `data` starts with the zstd frame magic bytes.\n "; + return ::std::convert::identity(data.len() as i64) >= 4 + && _byte_at(data.clone(), 0) == 40 && _byte_at(data.clone(), 1) == 181 + && _byte_at(data.clone(), 2) == 47 && _byte_at(data, 3) == 253; +} +pub fn _looks_like_bz2(data: Vec) -> bool { + "\n Return whether `data` starts with the bzip2 stream signature.\n "; + return ::std::convert::identity(data.len() as i64) >= 3 + && _byte_at(data.clone(), 0) == 66 && _byte_at(data.clone(), 1) == 90 + && _byte_at(data, 2) == 104; +} +pub fn _looks_like_xz(data: Vec) -> bool { + "\n Return whether `data` starts with the XZ container magic bytes.\n "; + return ::std::convert::identity(data.len() as i64) >= 6 + && _byte_at(data.clone(), 0) == 253 && _byte_at(data.clone(), 1) == 55 + && _byte_at(data.clone(), 2) == 122 && _byte_at(data.clone(), 3) == 88 + && _byte_at(data.clone(), 4) == 90 && _byte_at(data, 5) == 0; +} +pub fn _looks_like_snappy_frame(data: Vec) -> bool { + "\n Return whether `data` starts with the framed Snappy stream identifier.\n "; + return ::std::convert::identity(data.len() as i64) >= 10 + && _byte_at(data.clone(), 0) == 255 && _byte_at(data.clone(), 1) == 6 + && _byte_at(data.clone(), 2) == 0 && _byte_at(data.clone(), 3) == 0 + && _byte_at(data.clone(), 4) == 115 && _byte_at(data.clone(), 5) == 78 + && _byte_at(data.clone(), 6) == 97 && _byte_at(data.clone(), 7) == 80 + && _byte_at(data.clone(), 8) == 112 && _byte_at(data, 9) == 89; +} +pub fn _looks_like_zlib(data: Vec) -> bool { + "\n Return whether `data` has a valid zlib header.\n\n Zlib's header does not have a fixed magic sequence. The RFC 1950 check below validates the compression method and\n header checksum; decompression still owns the final data-validity decision.\n "; + if ::std::convert::identity(data.len() as i64) < 2 { + return false; + } + let cmf = _byte_at(data.clone(), 0); + let flg = _byte_at(data, 1); + return incan_stdlib::num::py_mod_i64(cmf, 16) == 8 + && incan_stdlib::num::py_mod_i64(cmf * 256 + flg, 31) == 0; +} diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_prelude_source.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_prelude_source.snap new file mode 100644 index 000000000..18a972f68 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_compression_prelude_source.snap @@ -0,0 +1,13 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::compression::_core::Codec; +pub use crate::__incan_std::compression::_core::CompressionError; +pub use crate::__incan_std::compression::_auto::decompress_auto; +pub use crate::__incan_std::compression::_auto::decompress_auto_stream; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_datetime_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_datetime_prelude_import.snap new file mode 100644 index 000000000..aa434177a --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_datetime_prelude_import.snap @@ -0,0 +1,18 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::datetime::DateTimeError; +pub use crate::__incan_std::datetime::Duration; +pub use crate::__incan_std::datetime::Instant; +pub use crate::__incan_std::datetime::SystemTime; +pub use crate::__incan_std::datetime::TimeDelta; +pub use crate::__incan_std::datetime::Date; +pub use crate::__incan_std::datetime::Time; +pub use crate::__incan_std::datetime::DateTime; +pub use crate::__incan_std::datetime::FixedOffset; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_encoding_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_encoding_prelude_import.snap new file mode 100644 index 000000000..94f2e3079 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_encoding_prelude_import.snap @@ -0,0 +1,13 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::encoding::base32; +pub use crate::__incan_std::encoding::base64; +pub use crate::__incan_std::encoding::hex; +pub use crate::__incan_std::encoding::EncodingError; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_io_source.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_io_source.snap new file mode 100644 index 000000000..b8b13eb6e --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_io_source.snap @@ -0,0 +1,1358 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +use ::std::cell::RefCell; +use ::std::io::Cursor; +use ::std::io::Read; +use ::std::io::Seek; +use ::std::io::SeekFrom; +use ::std::io::Write; +use ::std::rc::Rc; +use ::byteorder::BigEndian; +use ::byteorder::LittleEndian; +use ::byteorder::ReadBytesExt; +use ::byteorder::WriteBytesExt; +pub use crate::__incan_std::traits::error::Error; +#[derive(Clone, Debug, incan_derive::FieldInfo, incan_derive::IncanClass)] +pub struct IoError { + pub kind: String, + pub detail: String, + pub operation: String, + pub position: i64, + pub path: Option, +} +impl IoError { + pub fn message(&self) -> String { + "\n Return the human-readable error detail.\n\n Returns:\n The detail text attached to the failed operation.\n "; + return self.detail.clone(); + } + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 5] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("kind"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("kind"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("detail"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("detail"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("operation"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("operation"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("position"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("position"), + type_name: incan_stdlib::frozen::FrozenStr::new("int"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("path"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("path"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[str]"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Error for IoError { + fn message(&self) -> String { + "\n Return the human-readable error detail.\n\n Returns:\n The detail text attached to the failed operation.\n "; + return self.detail.clone(); + } + fn source(&self) -> Option { + "Optional: Return the underlying cause of this error"; + return None::<_>; + } +} +pub trait BinaryRead { + fn read(&self, endian: Endian) -> Result; +} +pub trait BinaryReader { + fn read_bytes(&self, size: i64) -> Result, IoError>; +} +pub trait BinaryWrite { + fn write(&self, value: T, endian: Endian) -> Result<(), IoError>; +} +#[derive(Debug, Clone, PartialEq)] +pub enum Endian { + Little, + Big, +} +impl Endian { + pub fn message(&self) -> String { + match self { + Self::Little => "Little".to_string(), + Self::Big => "Big".to_string(), + } + } +} +#[derive(Debug, Clone, incan_derive::FieldInfo, incan_derive::IncanClass)] +pub struct _BytesIO { + pub handle: Rc>>>, +} +impl _BytesIO { + pub fn read(&self, size: i64) -> Result, IoError> { + "\n Read up to `size` bytes from the current cursor.\n\n Args:\n size: Maximum bytes to read. Use a negative value to read through EOF.\n\n Returns:\n The bytes read, or `Err(IoError)`.\n\n Example:\n `BytesIO(b\"abc\").read(2)?` returns `b\"ab\"`.\n "; + let mut out: Vec = b"".to_vec(); + let mut cursor = self.handle.borrow_mut(); + if size < 0 { + match cursor.read_to_end(&mut out) { + Ok(_) => { + return Ok::, IoError>(out); + } + Err(err) => { + return Err::< + Vec, + IoError, + >( + _rust_io_error( + "read".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } + match Read::by_ref(&mut *cursor) + .take( + match u64::try_from(size) { + Ok(__incan_take_count) => __incan_take_count, + Err(_) => { + incan_stdlib::errors::raise_value_error( + "take() count must be non-negative and fit u64", + ) + } + }, + ) + .read_to_end(&mut out) + { + Ok(_) => { + return Ok::, IoError>(out); + } + Err(err) => { + return Err::< + Vec, + IoError, + >( + _rust_io_error( + "read".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } + pub fn read_bytes(&self, size: i64) -> Result, IoError> { + "\n Read up to `size` bytes through the `BinaryReader` trait surface.\n "; + return self.read(size); + } + pub fn read_exact(&self, size: i64) -> Result, IoError> { + "\n Read exactly `size` bytes.\n\n Args:\n size: Number of bytes required.\n\n Returns:\n Exactly `size` bytes, or `Err(IoError)` with kind `unexpected_eof` if the buffer ends early.\n\n Example:\n `BytesIO(b\"ABCD\").read_exact(4)?` returns `b\"ABCD\"`.\n "; + if size < 0 { + return Err::< + Vec, + IoError, + >( + _invalid_input( + "read_exact".to_string(), + self.tell(), + "read_exact size must be non-negative".to_string(), + ), + ); + } + let chunk = self.read(size)?; + if ::std::convert::identity(chunk.len() as i64) != size { + return Err::< + Vec, + IoError, + >(IoError { + kind: "unexpected_eof".to_string(), + detail: "buffer ended before read_exact size".to_string(), + operation: "read_exact".to_string(), + position: self.tell(), + path: None::<_>, + }); + } + return Ok::, IoError>(chunk); + } + pub fn read_until(&self, byte: u8) -> Result, IoError> { + "\n Read through `byte` or EOF.\n\n Args:\n byte: Delimiter byte to include when found.\n\n Returns:\n Bytes up to and including the delimiter, or the remaining bytes at EOF.\n\n Example:\n `BytesIO(b\"name\\0rest\").read_until(0)?` returns `b\"name\\0\"`.\n "; + let out = _empty_stream(); + while self.remaining() > 0 { + let value = self._read_u8()?; + out._write_u8(value)?; + if value == byte { + return Ok::, IoError>(out.getvalue()); + } + } + return Ok::, IoError>(out.getvalue()); + } + pub fn skip_until(&self, byte: u8) -> Result { + "\n Discard bytes through `byte` or EOF.\n\n Args:\n byte: Delimiter byte to include in the skipped count when found.\n\n Returns:\n Number of discarded bytes, including the delimiter when present.\n\n Example:\n `BytesIO(b\"header\nbody\").skip_until(10)?` returns `7`.\n "; + let mut count = 0; + while self.remaining() > 0 { + let value = self._read_u8()?; + count = count + 1; + if value == byte { + return Ok::(count); + } + } + return Ok::(count); + } + pub fn tell(&self) -> i64 { + "\n Return the current byte cursor position.\n\n Returns:\n Current offset from the start of the buffer.\n "; + let cursor = self.handle.borrow(); + return (cursor.position()) as i64; + } + pub fn seek(&self, offset: i64, whence: i64) -> Result { + "\n Move the cursor and return the new position.\n\n Args:\n offset: Byte offset interpreted relative to `whence`.\n whence: `0` for start, `1` for current position, and `2` for end.\n\n Returns:\n New cursor position, or `Err(IoError)` for invalid positions or `whence` values.\n\n Example:\n `buf.seek(0, 2)?` moves to the end of the buffer.\n "; + let mut cursor = self.handle.borrow_mut(); + if whence == 0 { + match cursor.position() { + pos => { + let delta = offset - (pos) as i64; + match cursor.seek(SeekFrom::Current(delta)) { + Ok(new_pos) => { + return Ok::((new_pos) as i64); + } + Err(err) => { + return Err::< + i64, + IoError, + >( + _rust_io_error( + "seek".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } + }; + } else { + if whence == 1 { + match cursor.seek(SeekFrom::Current(offset)) { + Ok(new_pos) => { + return Ok::((new_pos) as i64); + } + Err(err) => { + return Err::< + i64, + IoError, + >( + _rust_io_error( + "seek".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } else { + if whence == 2 { + match cursor.seek(SeekFrom::End(offset)) { + Ok(new_pos) => { + return Ok::((new_pos) as i64); + } + Err(err) => { + return Err::< + i64, + IoError, + >( + _rust_io_error( + "seek".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } + } + } + return Err::< + i64, + IoError, + >( + _invalid_input( + "seek".to_string(), + (cursor.position()) as i64, + "whence must be 0, 1, or 2".to_string(), + ), + ); + } + pub fn rewind(&self) -> Result<(), IoError> { + "\n Move the cursor to the start of the buffer.\n\n Returns:\n `Ok(None)` after rewinding, or `Err(IoError)`.\n "; + self.seek(0, 0)?; + return Ok::<(), IoError>(()); + } + pub fn seek_relative(&self, offset: i64) -> Result<(), IoError> { + "\n Move the cursor relative to its current position.\n\n Args:\n offset: Relative byte offset.\n\n Returns:\n `Ok(None)` after moving, or `Err(IoError)` when the result would be invalid.\n "; + self.seek(offset, 1)?; + return Ok::<(), IoError>(()); + } + pub fn write_bytes(&self, data: Vec) -> Result { + "\n Write bytes at the current cursor position.\n\n Args:\n data: Bytes to write.\n\n Returns:\n Number of bytes written, or `Err(IoError)`.\n\n Example:\n `BytesIO().write(b\"ok\")?` returns `2`.\n "; + let mut cursor = self.handle.borrow_mut(); + match cursor.write_all(&data) { + Ok(_) => { + return Ok::(::std::convert::identity(data.len() as i64)); + } + Err(err) => { + return Err::< + i64, + IoError, + >( + _rust_io_error( + "write".to_string(), + (cursor.position()) as i64, + err.to_string(), + ), + ); + } + }; + } + pub fn write(&self, data: Vec) -> Result { + "\n Write bytes at the current cursor position.\n\n Args:\n data: Bytes to write.\n\n Returns:\n Number of bytes written, or `Err(IoError)`.\n\n Example:\n `BytesIO().write(b\"ok\")?` returns `2`.\n "; + return self.write_bytes(data); + } + pub fn truncate(&self, size: Option) -> Result { + "\n Resize the buffer.\n\n Args:\n size: Target size, or `None` to truncate at the current cursor.\n\n Returns:\n The new buffer size, or `Err(IoError)`.\n\n Example:\n After reading two bytes from `BytesIO(b\"abcd\")`, `truncate()?` leaves `b\"ab\"`.\n "; + let position = self.tell(); + let mut target = position; + match size { + Some(value) => { + target = value; + } + _ => {} + } + if target < 0 { + return Err::< + i64, + IoError, + >( + _invalid_input( + "truncate".to_string(), + position, + "truncate size must be non-negative".to_string(), + ), + ); + } + let mut cursor = self.handle.borrow_mut(); + let target_usize = _truncate_target_usize(target, position)?; + let target_u64 = _truncate_target_u64(target, position)?; + cursor.get_mut().resize(target_usize, 0); + if (cursor.position()) as i64 > target { + cursor.set_position(target_u64); + } + return Ok::(target); + } + pub fn getvalue(&self) -> Vec { + "\n Return a snapshot of the buffer contents.\n\n Returns:\n Current buffer bytes without changing the cursor.\n "; + let cursor = self.handle.borrow(); + return cursor.get_ref().clone(); + } + pub fn into_bytes(&self) -> Vec { + "\n Return the buffer bytes.\n\n Returns:\n Current buffer bytes without changing the cursor. The current implementation returns a snapshot because\n `BytesIO` uses shared interior mutability for method calls.\n "; + return self.getvalue(); + } + pub fn remaining(&self) -> i64 { + "\n Return the unread byte count from the current cursor to the end.\n\n Returns:\n Number of bytes remaining in the buffer.\n "; + let cursor = self.handle.borrow(); + let total = ::std::convert::identity(cursor.get_ref().len() as i64); + let pos = (cursor.position()) as i64; + if pos >= total { + return 0; + } + return total - pos; + } + pub fn _read_u8(&self) -> Result { + return _read_u8_value(self.clone(), "read".to_string()); + } + pub fn _write_u8(&self, value: u8) -> Result<(), IoError> { + return _write_u8_value(self.clone(), value, "write".to_string()); + } + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 1] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("handle"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("handle"), + type_name: incan_stdlib::frozen::FrozenStr::new( + "Rc[RefCell[Cursor[bytes]]]", + ), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl BinaryReader for _BytesIO { + fn read_bytes(&self, size: i64) -> Result, IoError> { + "\n Read up to `size` bytes through the `BinaryReader` trait surface.\n "; + return self.read(size); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, _: Endian) -> Result { + return _read_u8_value(self.clone(), "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, _: Endian) -> Result { + return _read_i8_value(self.clone(), "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_u16_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_i16_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_u32_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_i32_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_u64_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_i64_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_u128_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_i128_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_f32_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryRead for _BytesIO { + fn read(&self, endian: Endian) -> Result { + return _read_f64_value(self.clone(), endian, "read".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: u8, _: Endian) -> Result<(), IoError> { + return _write_u8_value(self.clone(), value, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: i8, _: Endian) -> Result<(), IoError> { + return _write_i8_value(self.clone(), value, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: u16, endian: Endian) -> Result<(), IoError> { + return _write_u16_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: i16, endian: Endian) -> Result<(), IoError> { + return _write_i16_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: u32, endian: Endian) -> Result<(), IoError> { + return _write_u32_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: i32, endian: Endian) -> Result<(), IoError> { + return _write_i32_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: u64, endian: Endian) -> Result<(), IoError> { + return _write_u64_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: i64, endian: Endian) -> Result<(), IoError> { + return _write_i64_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: u128, endian: Endian) -> Result<(), IoError> { + return _write_u128_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: i128, endian: Endian) -> Result<(), IoError> { + return _write_i128_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: f32, endian: Endian) -> Result<(), IoError> { + return _write_f32_value(self.clone(), value, endian, "write".to_string()); + } +} +impl BinaryWrite for _BytesIO { + fn write(&self, value: f64, endian: Endian) -> Result<(), IoError> { + return _write_f64_value(self.clone(), value, endian, "write".to_string()); + } +} +#[allow(non_snake_case)] +pub fn BytesIO(initial: Vec) -> _BytesIO { + "\n Construct a writable, seekable in-memory byte stream.\n\n Args:\n initial: Initial buffer contents. The cursor starts at `0`.\n\n Returns:\n A `BytesIO` stream over `initial`.\n\n Example:\n `BytesIO(b\"abc\").read(1)?` returns `b\"a\"`.\n "; + return _BytesIO { + handle: Rc::new(RefCell::new(Cursor::new(initial))), + }; +} +fn _empty_stream() -> _BytesIO { + "\n Construct an internal empty stream without using the public PascalCase factory.\n "; + return _BytesIO { + handle: Rc::new(RefCell::new(Cursor::new(b"".to_vec()))), + }; +} +fn _zeros(size: i64) -> Vec { + "\n Build a zero-filled byte vector for numeric writes.\n "; + let zero: u8 = 0; + return incan_stdlib::collections::list_repeat(zero, (size) as i64); +} +fn _finish_numeric_write( + stream: _BytesIO, + cursor: Cursor>, +) -> Result<(), IoError> { + "\n Write a numeric helper's temporary cursor contents to the target stream.\n "; + stream.write_bytes(cursor.into_inner())?; + return Ok::<(), IoError>(()); +} +fn _read_u8_value(stream: _BytesIO, operation: String) -> Result { + let mut cursor = Cursor::new(stream.read_exact(1)?); + match cursor.read_u8() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u8, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_i8_value(stream: _BytesIO, operation: String) -> Result { + let mut cursor = Cursor::new(stream.read_exact(1)?); + match cursor.read_i8() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i8, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_u8_value( + stream: _BytesIO, + value: u8, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(1)); + match cursor.write_u8(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_i8_value( + stream: _BytesIO, + value: i8, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(1)); + match cursor.write_i8(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_u16_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(2)?); + if endian == Endian::Little { + match cursor.read_u16::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u16, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_u16::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u16, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_i16_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(2)?); + if endian == Endian::Little { + match cursor.read_i16::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i16, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_i16::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i16, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_u16_value( + stream: _BytesIO, + value: u16, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(2)); + if endian == Endian::Little { + match cursor.write_u16::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_u16::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_i16_value( + stream: _BytesIO, + value: i16, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(2)); + if endian == Endian::Little { + match cursor.write_i16::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_i16::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_u32_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(4)?); + if endian == Endian::Little { + match cursor.read_u32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u32, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_u32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u32, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_i32_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(4)?); + if endian == Endian::Little { + match cursor.read_i32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i32, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_i32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i32, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_u32_value( + stream: _BytesIO, + value: u32, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(4)); + if endian == Endian::Little { + match cursor.write_u32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_u32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_i32_value( + stream: _BytesIO, + value: i32, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(4)); + if endian == Endian::Little { + match cursor.write_i32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_i32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_u64_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(8)?); + if endian == Endian::Little { + match cursor.read_u64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u64, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_u64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u64, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_i64_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(8)?); + if endian == Endian::Little { + match cursor.read_i64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i64, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_i64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i64, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_u64_value( + stream: _BytesIO, + value: u64, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(8)); + if endian == Endian::Little { + match cursor.write_u64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_u64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_i64_value( + stream: _BytesIO, + value: i64, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(8)); + if endian == Endian::Little { + match cursor.write_i64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_i64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_u128_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(16)?); + if endian == Endian::Little { + match cursor.read_u128::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u128, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_u128::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + u128, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_i128_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(16)?); + if endian == Endian::Little { + match cursor.read_i128::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i128, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_i128::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + i128, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_u128_value( + stream: _BytesIO, + value: u128, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(16)); + if endian == Endian::Little { + match cursor.write_u128::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_u128::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_i128_value( + stream: _BytesIO, + value: i128, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(16)); + if endian == Endian::Little { + match cursor.write_i128::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_i128::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_f32_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(4)?); + if endian == Endian::Little { + match cursor.read_f32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + f32, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_f32::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + f32, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _read_f64_value( + stream: _BytesIO, + endian: Endian, + operation: String, +) -> Result { + let mut cursor = Cursor::new(stream.read_exact(8)?); + if endian == Endian::Little { + match cursor.read_f64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + f64, + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.read_f64::() { + Ok(value) => { + return Ok::(value); + } + Err(err) => { + return Err::< + f64, + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_f32_value( + stream: _BytesIO, + value: f32, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(4)); + if endian == Endian::Little { + match cursor.write_f32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_f32::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _write_f64_value( + stream: _BytesIO, + value: f64, + endian: Endian, + operation: String, +) -> Result<(), IoError> { + let mut cursor = Cursor::new(_zeros(8)); + if endian == Endian::Little { + match cursor.write_f64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation.to_string(), stream.tell(), err.to_string())); + } + }; + } + match cursor.write_f64::(value) { + Ok(_) => { + return _finish_numeric_write(stream.clone(), cursor.clone()); + } + Err(err) => { + return Err::< + (), + IoError, + >(_rust_io_error(operation, stream.tell(), err.to_string())); + } + }; +} +fn _truncate_target_usize(target: i64, position: i64) -> Result { + "\n Convert a validated truncate target into the host vector size type.\n "; + let maybe_target: Option = incan_stdlib::num::try_resize::<_, usize>(target); + match maybe_target { + Some(value) => { + return Ok::(value); + } + None => { + return Err::< + usize, + IoError, + >( + _invalid_input( + "truncate".to_string(), + position, + "truncate size does not fit host usize".to_string(), + ), + ); + } + }; +} +fn _truncate_target_u64(target: i64, position: i64) -> Result { + "\n Convert a validated truncate target into the cursor position type.\n "; + let maybe_target: Option = incan_stdlib::num::try_resize::<_, u64>(target); + match maybe_target { + Some(value) => { + return Ok::(value); + } + None => { + return Err::< + u64, + IoError, + >( + _invalid_input( + "truncate".to_string(), + position, + "truncate size does not fit u64".to_string(), + ), + ); + } + }; +} +fn _invalid_input(operation: String, position: i64, detail: String) -> IoError { + "\n Create a stable invalid-input error.\n "; + return IoError { + kind: "invalid_input".to_string(), + detail: detail, + operation: operation, + position: position, + path: None::<_>, + }; +} +fn _rust_io_error(operation: String, position: i64, detail: String) -> IoError { + "\n Convert host I/O error text into a `std.io` error.\n "; + if incan_stdlib::strings::str_contains(&detail, &"invalid") { + return IoError { + kind: "invalid_input".to_string(), + detail: detail.to_string(), + operation: operation.to_string(), + position: position, + path: None::<_>, + }; + } else { + if incan_stdlib::strings::str_contains(&detail, &"failed to fill whole buffer") + || incan_stdlib::strings::str_contains(&detail, &"early eof") + { + return IoError { + kind: "unexpected_eof".to_string(), + detail: detail.to_string(), + operation: operation.to_string(), + position: position, + path: None::<_>, + }; + } + } + return IoError { + kind: "other".to_string(), + detail: detail, + operation: operation, + position: position, + path: None::<_>, + }; +} diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_result_source.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_result_source.snap new file mode 100644 index 000000000..c2a3a1ce9 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_result_source.snap @@ -0,0 +1,80 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub fn map(result: Result, f: fn(T) -> U) -> Result { + "\n Transform an `Ok(T)` payload.\n\n Args:\n result: Result to inspect.\n f: Function called with the `Ok(T)` payload.\n\n Returns:\n `Ok(f(value))` when `result` is `Ok(value)`, otherwise the original `Err(E)`.\n "; + match result { + Ok(value) => { + return Ok::(f(value)); + } + Err(error) => { + return Err::(error); + } + }; +} +pub fn map_err(result: Result, f: fn(E) -> F) -> Result { + "\n Transform an `Err(E)` payload.\n\n Args:\n result: Result to inspect.\n f: Function called with the `Err(E)` payload.\n\n Returns:\n `Err(f(error))` when `result` is `Err(error)`, otherwise the original `Ok(T)`.\n "; + match result { + Ok(value) => { + return Ok::(value); + } + Err(error) => { + return Err::(f(error)); + } + }; +} +pub fn and_then( + result: Result, + f: fn(T) -> Result, +) -> Result { + "\n Chain a fallible operation after an `Ok(T)` payload.\n\n Args:\n result: Result to inspect.\n f: Function called with the `Ok(T)` payload. It must return a `Result[U, E]`.\n\n Returns:\n The `Result[U, E]` returned by `f` when `result` is `Ok(value)`, otherwise the original `Err(E)`.\n "; + match result { + Ok(value) => { + return f(value); + } + Err(error) => { + return Err::(error); + } + }; +} +pub fn or_else(result: Result, f: fn(E) -> Result) -> Result { + "\n Recover from an `Err(E)` payload with a fallible operation.\n\n Args:\n result: Result to inspect.\n f: Function called with the `Err(E)` payload. It must return a `Result[T, F]`.\n\n Returns:\n The `Result[T, F]` returned by `f` when `result` is `Err(error)`, otherwise the original `Ok(T)`.\n "; + match result { + Ok(value) => { + return Ok::(value); + } + Err(error) => { + return f(error); + } + }; +} +pub fn inspect(result: Result, f: fn(&T) -> ()) -> Result { + "\n Observe an `Ok(T)` payload while preserving the original result.\n\n Args:\n result: Result to inspect.\n f: Function called with an implicit borrow of the `Ok(T)` payload.\n\n Returns:\n The original `Result[T, E]` after calling `f` for the `Ok(T)` branch.\n "; + match result { + Ok(value) => { + f(&value); + return Ok::(value); + } + Err(error) => { + return Err::(error); + } + }; +} +pub fn inspect_err(result: Result, f: fn(&E) -> ()) -> Result { + "\n Observe an `Err(E)` payload while preserving the original result.\n\n Args:\n result: Result to inspect.\n f: Function called with an implicit borrow of the `Err(E)` payload.\n\n Returns:\n The original `Result[T, E]` after calling `f` for the `Err(E)` branch.\n "; + match result { + Ok(value) => { + return Ok::(value); + } + Err(error) => { + f(&error); + return Err::(error); + } + }; +} diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_root_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_root_prelude_import.snap new file mode 100644 index 000000000..70d4b45fb --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_root_prelude_import.snap @@ -0,0 +1,9 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_serde_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_serde_prelude_import.snap new file mode 100644 index 000000000..c2cead4d5 --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_serde_prelude_import.snap @@ -0,0 +1,10 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::serde::json; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_core_source.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_core_source.snap new file mode 100644 index 000000000..6ab67fc2d --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_core_source.snap @@ -0,0 +1,636 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::serde::json::Serialize; +#[derive(Debug, Clone, PartialEq)] +pub enum TelemetryValueKind { + NONE, + STRING, + BOOL, + INT, + FLOAT, + BYTES, + ARRAY, + MAP, +} +impl TelemetryValueKind { + pub fn message(&self) -> String { + match self { + Self::NONE => "NONE".to_string(), + Self::STRING => "STRING".to_string(), + Self::BOOL => "BOOL".to_string(), + Self::INT => "INT".to_string(), + Self::FLOAT => "FLOAT".to_string(), + Self::BYTES => "BYTES".to_string(), + Self::ARRAY => "ARRAY".to_string(), + Self::MAP => "MAP".to_string(), + } + } +} +impl TelemetryValueKind { + pub fn value(&self) -> String { + match self { + Self::NONE => "none".to_string(), + Self::STRING => "string".to_string(), + Self::BOOL => "bool".to_string(), + Self::INT => "int".to_string(), + Self::FLOAT => "float".to_string(), + Self::BYTES => "bytes".to_string(), + Self::ARRAY => "array".to_string(), + Self::MAP => "map".to_string(), + } + } + pub fn from_value(value: impl AsRef) -> Option { + match value.as_ref() { + "none" => Some(Self::NONE), + "string" => Some(Self::STRING), + "bool" => Some(Self::BOOL), + "int" => Some(Self::INT), + "float" => Some(Self::FLOAT), + "bytes" => Some(Self::BYTES), + "array" => Some(Self::ARRAY), + "map" => Some(Self::MAP), + _ => None, + } + } +} +impl std::fmt::Display for TelemetryValueKind { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::NONE => formatter.write_str("none"), + Self::STRING => formatter.write_str("string"), + Self::BOOL => formatter.write_str("bool"), + Self::INT => formatter.write_str("int"), + Self::FLOAT => formatter.write_str("float"), + Self::BYTES => formatter.write_str("bytes"), + Self::ARRAY => formatter.write_str("array"), + Self::MAP => formatter.write_str("map"), + } + } +} +impl std::str::FromStr for TelemetryValueKind { + type Err = String; + fn from_str(value: &str) -> Result { + Self::from_value(value) + .ok_or_else(|| { + format!( + "invalid value for {}: {}", stringify!(TelemetryValueKind), value + ) + }) + } +} +impl serde::Serialize for TelemetryValueKind { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::NONE => serializer.serialize_str("none"), + Self::STRING => serializer.serialize_str("string"), + Self::BOOL => serializer.serialize_str("bool"), + Self::INT => serializer.serialize_str("int"), + Self::FLOAT => serializer.serialize_str("float"), + Self::BYTES => serializer.serialize_str("bytes"), + Self::ARRAY => serializer.serialize_str("array"), + Self::MAP => serializer.serialize_str("map"), + } + } +} +#[derive(Debug, Clone, serde::Serialize)] +pub struct Timestamp(pub String); +impl Serialize for Timestamp { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive( + Clone, + serde::Serialize, + Debug, + incan_derive::FieldInfo, + incan_derive::IncanClass +)] +pub struct TelemetryValue { + #[serde(rename = "Type")] + pub kind: TelemetryValueKind, + #[serde(rename = "StringValue")] + pub string_value: Option, + #[serde(rename = "BoolValue")] + pub bool_value: Option, + #[serde(rename = "IntValue")] + pub int_value: Option, + #[serde(rename = "FloatValue")] + pub float_value: Option, + #[serde(rename = "BytesValue")] + pub bytes_value: Option, + #[serde(rename = "ArrayValue")] + pub array_value: Vec, + #[serde(rename = "MapValue")] + pub map_value: std::collections::HashMap, +} +impl TelemetryValue { + pub fn none() -> Self { + "Return a telemetry null value."; + return TelemetryValue { + kind: TelemetryValueKind::NONE.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: None::<_>, + float_value: None::<_>, + bytes_value: None::<_>, + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn string(value: String) -> Self { + "Return a telemetry string value."; + return TelemetryValue { + kind: TelemetryValueKind::STRING.clone(), + string_value: Some(value), + bool_value: None::<_>, + int_value: None::<_>, + float_value: None::<_>, + bytes_value: None::<_>, + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn bool(value: bool) -> Self { + "Return a telemetry boolean value."; + return TelemetryValue { + kind: TelemetryValueKind::BOOL.clone(), + string_value: None::<_>, + bool_value: Some(value), + int_value: None::<_>, + float_value: None::<_>, + bytes_value: None::<_>, + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn int(value: i64) -> Self { + "Return a telemetry integer value."; + return TelemetryValue { + kind: TelemetryValueKind::INT.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: Some(value), + float_value: None::<_>, + bytes_value: None::<_>, + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn float(value: f64) -> Self { + "Return a telemetry floating-point value."; + return TelemetryValue { + kind: TelemetryValueKind::FLOAT.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: None::<_>, + float_value: Some(value), + bytes_value: None::<_>, + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn bytes(value: String) -> Self { + "\n Return a telemetry bytes value from an encoded string.\n\n The caller owns the encoding convention.\n "; + return TelemetryValue { + kind: TelemetryValueKind::BYTES.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: None::<_>, + float_value: None::<_>, + bytes_value: Some(value), + array_value: vec![], + map_value: std::collections::HashMap::new(), + }; + } + pub fn array(values: Vec) -> Self { + "Return a telemetry array value."; + return TelemetryValue { + kind: TelemetryValueKind::ARRAY.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: None::<_>, + float_value: None::<_>, + bytes_value: None::<_>, + array_value: values, + map_value: std::collections::HashMap::new(), + }; + } + pub fn map(values: std::collections::HashMap) -> Self { + "Return a telemetry map value."; + return TelemetryValue { + kind: TelemetryValueKind::MAP.clone(), + string_value: None::<_>, + bool_value: None::<_>, + int_value: None::<_>, + float_value: None::<_>, + bytes_value: None::<_>, + array_value: vec![], + map_value: values, + }; + } + pub fn display_text(&self) -> String { + "\n Return a concise text representation for human-oriented output.\n\n String values render as their underlying text. Structured values render through their JSON representation so\n callers do not lose shape when displaying nested telemetry data.\n "; + match self.string_value.clone() { + Some(text) => { + return text; + } + None => { + return self.to_json(); + } + }; + } + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 8] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("kind"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("Type")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Telemetry value kind: none, string, bool, int, float, bytes, array, or map.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("Type"), + type_name: incan_stdlib::frozen::FrozenStr::new("TelemetryValueKind"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("string_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("StringValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "String value when kind is string.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("StringValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[str]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("bool_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("BoolValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Boolean value when kind is bool.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("BoolValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[bool]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("int_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("IntValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Integer value when kind is int.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("IntValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[int]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("float_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("FloatValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Floating-point value when kind is float.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("FloatValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[float]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("bytes_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("BytesValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Encoded byte value when kind is bytes.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("BytesValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[str]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("array_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("ArrayValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Nested array values when kind is array.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("ArrayValue"), + type_name: incan_stdlib::frozen::FrozenStr::new("list[TelemetryValue]"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("map_value"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("MapValue")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Nested map values when kind is map.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("MapValue"), + type_name: incan_stdlib::frozen::FrozenStr::new( + "dict[str, TelemetryValue]", + ), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Serialize for TelemetryValue { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive(Debug, Clone, serde::Serialize)] +pub struct Attributes(pub std::collections::HashMap); +impl Attributes { + pub fn from_string_fields( + fields: std::collections::HashMap, + ) -> Attributes { + "\n Convert string fields into structured telemetry attributes.\n\n Args:\n fields: String-valued field map from ordinary logging call sites.\n "; + return Attributes( + ((fields).keys().cloned()) + .map(|key| ( + key.clone(), + TelemetryValue::string( + incan_stdlib::collections::dict_get( + &fields, + <_ as AsRef>::as_ref(&key), + ) + .clone(), + ), + )) + .collect::>(), + ); + } + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 1] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("0"), + alias: None, + description: None, + wire_name: incan_stdlib::frozen::FrozenStr::new("0"), + type_name: incan_stdlib::frozen::FrozenStr::new( + "dict[str, TelemetryValue]", + ), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Serialize for Attributes { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive(Debug, Clone, serde::Serialize)] +pub struct TraceId(pub String); +impl Serialize for TraceId { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive(Debug, Clone, serde::Serialize)] +pub struct SpanId(pub String); +impl Serialize for SpanId { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive(Debug, Clone, serde::Serialize)] +pub struct TraceFlags(pub String); +impl Serialize for TraceFlags { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive( + serde::Serialize, + Debug, + Clone, + incan_derive::FieldInfo, + incan_derive::IncanClass +)] +pub struct Resource { + #[serde(rename = "Attributes")] + pub attributes: Attributes, +} +impl Resource { + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 1] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("attributes"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("Attributes")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Resource attributes such as service.name or service.version.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("Attributes"), + type_name: incan_stdlib::frozen::FrozenStr::new("Attributes"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Serialize for Resource { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive( + serde::Serialize, + Debug, + Clone, + incan_derive::FieldInfo, + incan_derive::IncanClass +)] +pub struct InstrumentationScope { + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Version")] + pub version: Option, + #[serde(rename = "SchemaUrl")] + pub schema_url: Option, +} +impl InstrumentationScope { + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 3] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("name"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("Name")), + description: Some( + incan_stdlib::frozen::FrozenStr::new("Instrumentation scope name."), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("Name"), + type_name: incan_stdlib::frozen::FrozenStr::new("str"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("version"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("Version")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Instrumentation scope version, when known.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("Version"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[str]"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("schema_url"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("SchemaUrl")), + description: Some( + incan_stdlib::frozen::FrozenStr::new( + "Schema URL for scope metadata, when known.", + ), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("SchemaUrl"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[str]"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Serialize for InstrumentationScope { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} +#[derive( + serde::Serialize, + Debug, + Clone, + incan_derive::FieldInfo, + incan_derive::IncanClass +)] +pub struct SpanContext { + #[serde(rename = "TraceId")] + pub trace_id: TraceId, + #[serde(rename = "SpanId")] + pub span_id: SpanId, + #[serde(rename = "TraceFlags")] + pub trace_flags: Option, +} +impl SpanContext { + /// Returns field metadata for this type. + pub fn __fields__( + &self, + ) -> incan_stdlib::frozen::FrozenList { + static __INCAN_FIELDS: [incan_stdlib::reflection::FieldInfo; 3] = [ + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("trace_id"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("TraceId")), + description: Some( + incan_stdlib::frozen::FrozenStr::new("Trace identifier."), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("TraceId"), + type_name: incan_stdlib::frozen::FrozenStr::new("TraceId"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("span_id"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("SpanId")), + description: Some( + incan_stdlib::frozen::FrozenStr::new("Span identifier."), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("SpanId"), + type_name: incan_stdlib::frozen::FrozenStr::new("SpanId"), + has_default: false, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + incan_stdlib::reflection::FieldInfo { + name: incan_stdlib::frozen::FrozenStr::new("trace_flags"), + alias: Some(incan_stdlib::frozen::FrozenStr::new("TraceFlags")), + description: Some( + incan_stdlib::frozen::FrozenStr::new("W3C trace flags."), + ), + wire_name: incan_stdlib::frozen::FrozenStr::new("TraceFlags"), + type_name: incan_stdlib::frozen::FrozenStr::new("Option[TraceFlags]"), + has_default: true, + extra: incan_stdlib::frozen::FrozenDict::new(&[]), + }, + ]; + incan_stdlib::frozen::FrozenList::new(&__INCAN_FIELDS) + } +} +impl Serialize for SpanContext { + fn to_json(&self) -> String { + return incan_stdlib::json::__private::stringify_or_raise( + &self, + std::any::type_name_of_val(&self), + ); + } +} diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_prelude_import.snap new file mode 100644 index 000000000..ef68b8d0d --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_telemetry_prelude_import.snap @@ -0,0 +1,14 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::telemetry::TelemetryValue; +pub use crate::__incan_std::telemetry::Attributes; +pub use crate::__incan_std::telemetry::Resource; +pub use crate::__incan_std::telemetry::InstrumentationScope; +pub use crate::__incan_std::telemetry::SpanContext; diff --git a/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_web_prelude_import.snap b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_web_prelude_import.snap new file mode 100644 index 000000000..c6b40314e --- /dev/null +++ b/tests/snapshots/stdlib_generated_rust_snapshot_tests__std_web_prelude_import.snap @@ -0,0 +1,27 @@ +--- +source: tests/stdlib_generated_rust_snapshot_tests.rs +expression: rust_code +--- +// Generated by the Incan compiler v + +// __INCAN_INSERT_MODS__ + +incan_stdlib::__incan_stdlib_version_check!(""); +pub use crate::__incan_std::r#async; +pub use crate::__incan_std::web::App; +pub use crate::__incan_std::web::Json; +pub use crate::__incan_std::web::Html; +pub use crate::__incan_std::web::Response; +pub use crate::__incan_std::web::Query; +pub use crate::__incan_std::web::Path; +pub use crate::__incan_std::web::route; +pub use crate::__incan_std::web::GET; +#[derive(Debug, Clone, incan_derive::FieldInfo, incan_derive::IncanClass)] +struct Search { + #[expect(dead_code, reason = "retained for Incan private field semantics")] + q: String, +} +#[incan_web_macros::route("/snapshot/{id}", method = "GET")] +async fn snapshot(_: Path, query: Query) -> Json { + return crate::__incan_std::web::Json(query.value.clone()); +} diff --git a/tests/stdlib_generated_rust_snapshot_tests.rs b/tests/stdlib_generated_rust_snapshot_tests.rs new file mode 100644 index 000000000..b270df245 --- /dev/null +++ b/tests/stdlib_generated_rust_snapshot_tests.rs @@ -0,0 +1,274 @@ +//! Representative generated-Rust snapshots for stdlib inventory gaps. +//! +//! This suite intentionally covers selected high-value stdlib surfaces rather than every stdlib source file. It keeps +//! direct stdlib snapshots separate from `codegen_snapshot_tests` so broad generated-Rust quality work can land without +//! conflicting with the general codegen snapshot suite. + +use incan::backend::IrCodegen; +use incan::frontend::{lexer, parser}; +use std::fs; + +type TestResult = Result<(), Box>; + +fn err_box(message: impl Into) -> Box { + Box::new(std::io::Error::other(message.into())) +} + +fn generate_rust(source: &str, context: &str) -> Result> { + let tokens = lexer::lex(source).map_err(|errs| err_box(format!("{context} lexer failed: {errs:?}")))?; + let ast = parser::parse(&tokens).map_err(|errs| err_box(format!("{context} parser failed: {errs:?}")))?; + let code = IrCodegen::new() + .try_generate(&ast) + .map_err(|err| err_box(format!("{context} codegen failed: {err:?}")))?; + Ok(normalize_codegen_output(&code)) +} + +fn normalize_codegen_output(code: &str) -> String { + let from = format!( + "// Generated by the Incan compiler v{}\n\n", + incan::version::INCAN_VERSION + ); + let to = "// Generated by the Incan compiler v\n\n"; + code.replace(&from, to) + .lines() + .map(|line| { + if line.starts_with("incan_stdlib::__incan_stdlib_version_check!(") { + "incan_stdlib::__incan_stdlib_version_check!(\"\");" + } else { + line + } + }) + .collect::>() + .join("\n") +} + +fn assert_stdlib_source_snapshot(snapshot_name: &str, path: &str) -> TestResult { + let source = fs::read_to_string(path)?; + let rust_code = generate_rust(&source, path)?; + insta::assert_snapshot!(snapshot_name, rust_code); + Ok(()) +} + +fn assert_import_snapshot(snapshot_name: &str, source: &str) -> TestResult { + let rust_code = generate_rust(source, snapshot_name)?; + insta::assert_snapshot!(snapshot_name, rust_code); + Ok(()) +} + +#[test] +fn std_root_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_root_prelude_import", + r#" +from std import Debug, Eq, Clone, From, Add, Error, Index, Callable1 + +def touch_debug(value: Debug) -> None: + return + +def touch_eq(value: Eq) -> None: + return + +def touch_clone(value: Clone) -> None: + return + +def touch_from(value: From[int]) -> None: + return + +def touch_add(value: Add[int]) -> None: + return + +def touch_error(value: Error) -> None: + return + +def touch_index(value: Index[int, str]) -> None: + return + +def touch_callable(value: Callable1[int, str]) -> None: + return +"#, + ) +} + +#[test] +fn std_async_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_async_prelude_import", + r#" +import std.async +from std.async import sleep, spawn, JoinHandle, Mutex, RaceArm + +async def work() -> int: + await sleep(0.01) + return 1 + +def touch_handle(handle: JoinHandle[int]) -> None: + return + +def touch_lock(lock: Mutex[int]) -> None: + return + +def launch() -> JoinHandle[int]: + return spawn(work) + +def touch_arm(arm_value: RaceArm[int]) -> None: + return +"#, + ) +} + +#[test] +fn std_datetime_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_datetime_prelude_import", + r#" +from std.datetime import DateTimeError, Duration, Instant, SystemTime, TimeDelta, Date, Time, DateTime, FixedOffset + +def touch_error(error: DateTimeError) -> None: + return + +def touch_duration(duration: Duration) -> None: + return + +def touch_instant(instant: Instant) -> None: + return + +def touch_system_time(time: SystemTime) -> None: + return + +def touch_delta(delta: TimeDelta) -> None: + return + +def touch_date(date: Date) -> None: + return + +def touch_time(time: Time) -> None: + return + +def touch_datetime(value: DateTime) -> None: + return + +def touch_offset(offset: FixedOffset) -> None: + return +"#, + ) +} + +#[test] +fn std_encoding_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_encoding_prelude_import", + r#" +from std.encoding import base32, base64, hex, EncodingError + +def touch_error(error: EncodingError) -> None: + return + +def encode_samples(value: bytes) -> tuple[str, str, str]: + return (hex.b16encode(value), base32.b32encode(value), base64.b64encode(value)) +"#, + ) +} + +#[test] +fn std_serde_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_serde_prelude_import", + r#" +from std.serde import json + +@derive(json) +model SnapshotConfig: + name: str + enabled: bool + +def encode_config(config: SnapshotConfig) -> str: + return config.to_json() +"#, + ) +} + +#[test] +fn std_telemetry_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_telemetry_prelude_import", + r#" +from std.telemetry import TelemetryValue, Attributes, Resource, InstrumentationScope, SpanContext + +def touch_value(value: TelemetryValue) -> str: + return value.display_text() + +def touch_attributes(attributes: Attributes) -> None: + return + +def touch_resource(resource: Resource) -> None: + return + +def touch_scope(scope: InstrumentationScope) -> None: + return + +def touch_span_context(context: SpanContext) -> None: + return +"#, + ) +} + +#[test] +fn std_web_prelude_import_snapshot() -> TestResult { + assert_import_snapshot( + "std_web_prelude_import", + r#" +import std.async +from std.web import App, Json, Html, Response, Query, Path, route, GET + +model Search: + q: str + +@route("/snapshot/{id}", method=GET) +async def snapshot(id: Path[int], query: Query[Search]) -> Json[Search]: + return Json(query.value) + +def touch_app(app: App) -> None: + return + +def touch_response(response: Response) -> None: + return + +def render_html(value: str) -> Html: + return Html(value) +"#, + ) +} + +#[test] +fn std_result_source_snapshot() -> TestResult { + assert_stdlib_source_snapshot("std_result_source", "crates/incan_stdlib/stdlib/result.incn") +} + +#[test] +fn std_io_source_snapshot() -> TestResult { + assert_stdlib_source_snapshot("std_io_source", "crates/incan_stdlib/stdlib/io.incn") +} + +#[test] +fn std_telemetry_core_source_snapshot() -> TestResult { + assert_stdlib_source_snapshot( + "std_telemetry_core_source", + "crates/incan_stdlib/stdlib/telemetry/core.incn", + ) +} + +#[test] +fn std_compression_prelude_source_snapshot() -> TestResult { + assert_stdlib_source_snapshot( + "std_compression_prelude_source", + "crates/incan_stdlib/stdlib/compression/prelude.incn", + ) +} + +#[test] +fn std_compression_core_source_snapshot() -> TestResult { + assert_stdlib_source_snapshot( + "std_compression_core_source", + "crates/incan_stdlib/stdlib/compression/_core.incn", + ) +} diff --git a/workspaces/docs-site/docs/RFCs/097_rust_hosted_incan_caller.md b/workspaces/docs-site/docs/RFCs/097_rust_hosted_incan_caller.md index 4e46ecf6a..b29df694b 100644 --- a/workspaces/docs-site/docs/RFCs/097_rust_hosted_incan_caller.md +++ b/workspaces/docs-site/docs/RFCs/097_rust_hosted_incan_caller.md @@ -296,6 +296,8 @@ The support crate should remain narrow and versioned. Generated artifacts should Local development may later add a build-script helper that invokes the Incan compiler from a Rust workspace, but that helper should produce the same caller boundary as a prebuilt or published package. +Current package-facing characterization shows that ordinary `incan build --lib` artifacts can already expose owned scalar callable parameters through generated package exports, but borrowed non-`Copy` callable parameters are not yet consumable across a `pub::` package boundary. A producer export such as `Callable[Payload, None]` currently emits a Rust signature shaped like `fn(&Payload) -> ()`, while a downstream Incan consumer observer still emits `fn(Payload)`, causing Cargo type checking to fail. The caller adapter work must either generate a compatible borrowed wrapper for that boundary or reject/document the unsupported export before producing a broken consumer build. + ## Layers affected - **Library artifact model**: library builds must be able to include caller metadata and generated caller adapters alongside existing semantic manifests and generated Rust crates. diff --git a/workspaces/docs-site/docs/contributing/explanation/generated_rust_quality.md b/workspaces/docs-site/docs/contributing/explanation/generated_rust_quality.md new file mode 100644 index 000000000..c8e84b282 --- /dev/null +++ b/workspaces/docs-site/docs/contributing/explanation/generated_rust_quality.md @@ -0,0 +1,134 @@ +# Generated Rust Quality + +Generated Rust is a first-class Incan output. It is not merely a compiler scratch artifact, even when a future Rust-hosted caller layer gives Rust applications a more curated API. A contributor should be able to inspect emitted Rust, understand the important shape, diagnose backend mistakes, and reason about performance without reverse-engineering a pile of accidental scaffolding. + +This page defines the contributor quality contract for generated Rust. It complements [Duckborrowing](duckborrowing.md), which owns backend ownership planning, and [Readable, maintainable Rust](readable-maintainable-rust.md), which covers hand-written Rust in the repository. + +## Product Surface + +Generated Rust has more than one audience: + +- **Rust compiler and Cargo**: emitted projects must build deterministically with the selected dependencies, edition, features, and profile. +- **Incan contributors**: snapshots and `--emit-rust` output must make backend changes reviewable. +- **Rust hosts**: package-facing generated crates should be usable through normal Cargo mechanics, even when not every generated symbol is a stable host API. +- **Future caller tooling**: RFC 097-style caller adapters should build on good generated Rust instead of hiding poor output behind wrappers. + +Not every generated helper is a stable public API. Internal names may be compiler-owned and may change. That does not lower the bar for readability, debuggability, or performance: internal output still needs to be coherent enough to review and debug. + +## Surface Classes + +Classify each generated Rust finding by surface before deciding severity: + +- **Public/package-facing output**: generated library crate exports, public models, classes, enums, newtypes, traits, functions, constants, module paths, Cargo metadata, and `.incnlib` metadata that downstream projects consume. +- **Debuggable artifact output**: `incan --emit-rust`, generated projects under `target/incan/`, codegen snapshots, and generated test harnesses that contributors inspect while debugging. +- **Compiler-private output**: helper bindings, private modules, desugarer plumbing, test harness glue, temporary locals, and implementation details that are not promised as stable Rust APIs. + +Public/package-facing findings usually need the strictest treatment. Debuggable artifacts need enough structure to review and diagnose. Compiler-private output can be more mechanical, but it still should not hide repeated performance costs or block source-level diagnosis. + +## Quality Contract + +Generated Rust should satisfy these expectations unless a feature-specific design explicitly says otherwise. + +### Correct Rust + +- Generated code must compile without requiring users to write Rust escape hatches for ordinary Incan code. +- The Rust shape must preserve checked Incan semantics, including ownership, mutability, type conversions, visibility, imports, and error propagation. +- Backend fixes should prefer semantic or IR-level policy over local emitter patches when the behavior is not truly local. +- Generated Cargo manifests should be structured, deterministic, and reviewable. + +### Readable Rust + +- Generated names should be stable, legal Rust identifiers and should preserve the source concept where that is useful for debugging. +- Compiler-private helpers should use clear, recognizable prefixes such as `__incan_*`. +- Module layout should follow source modules and generated package boundaries rather than flattening unrelated concepts into one file. +- Repeated generated patterns should eventually move into runtime/helper APIs when that makes the emitted code smaller and clearer. +- Formatting should remain Rust-reader-friendly through `prettyplease` or another structured Rust formatter, not ad hoc string assembly. + +### Performant Rust + +- Avoidable `.clone()`, `.to_string()`, `.collect()`, heap allocation, dynamic dispatch, and eager intermediate containers are quality issues, not just style issues. +- Clones and string materialization must be explainable from Incan semantics, a checked target type, a `ValueUseSite`, or an intentional runtime boundary. +- Last-use moves should win over defensive cloning when the IR proves the value can be consumed. +- Lookups, membership tests, iterator adapters, and stdlib helpers should prefer borrowed or lazy shapes when they do not change source semantics. +- Broad emitter rewrites are acceptable when an audit finds a repeated quality or performance problem, but the change needs explicit scope, tests, and a performance argument. +- Hot generated paths should get benchmark or profiling coverage when the performance claim is not obvious from the emitted shape. + +### Hostable Rust + +- Package-facing generated crates should expose coherent Rust items for public Incan declarations where the current package model does so. +- Ordinary Rust crates should be able to depend on generated library artifacts through Cargo path dependencies and call currently exported public generated Rust items directly. +- Rust-hosted caller APIs may provide a curated stable surface later, but they should not be treated as permission for low-quality generated implementation code. +- Public/package-facing output should avoid leaking compiler-internal layout details when a stable helper or adapter can carry the boundary more clearly. +- Generated rustdoc should be considered for declarations that are intended to be inspected from Rust. + +### Debuggable Rust + +- A contributor should be able to run `incan --emit-rust path/to/file.incn` and connect the result back to the source feature. +- Snapshot names should identify the language feature, regression, stdlib module, or interop boundary being protected. +- Diagnostics should report Incan source concepts whenever possible; generated Rust names are fallback evidence, not the primary user-facing explanation. +- When generated Rust must contain unusual scaffolding, tests or docs should make the reason discoverable. + +### Testable Rust + +- Codegen snapshots are the default review gate for emitted shape changes. +- Build or run tests are required when Rust validity, borrow checking, dependency wiring, async runtime setup, or generated Cargo behavior is the risk. +- Ownership changes should include planner/lowering coverage where the decision is made and generated-Rust coverage where the shape is observed. +- Public/package-facing generated output should have representative fixtures, not only minimized internal snippets. +- Native Rust consumer coverage should exercise Cargo against the generated library crate itself rather than only inspecting generated text. + +## Audit Workflow + +Use this loop when changing generated Rust: + +1. Identify the source-level semantic change or quality problem. +2. Classify the affected surface as public/package-facing, debuggable artifact, or compiler-private output. +3. Decide whether the fix belongs in typechecking, lowering, ownership planning, emission, runtime helpers, or project generation. +4. Inspect current output with `incan --emit-rust` or the relevant generated project under `target/incan/`. +5. Label each clone, allocation, eager collection, or helper call as required by semantics, required by current API shape, suspicious, or intentionally optimized. +6. Add or update a focused codegen snapshot when the emitted shape should be reviewable. +7. Add a build/run/integration test when Rust compilation behavior matters. +8. For performance-sensitive changes, explain why the new shape avoids work or add a benchmark/profiling follow-up. +9. Run the narrow test first, then the relevant repository gate before closeout. + +Useful commands: + +```bash +incan --emit-rust path/to/file.incn +cargo test --test codegen_snapshot_tests +INSTA_UPDATE=1 cargo test --test codegen_snapshot_tests +make pre-commit +``` + +## When To Broaden Scope + +Do not keep generated Rust cleanup arbitrarily small when the evidence shows a broader design problem. Broaden the work, or file a clearly scoped follow-up, when any of these are true: + +- The same awkward pattern appears across multiple emitters or snapshots. +- A local fix would bypass duckborrowing, typechecker facts, or IR ownership policy. +- Generated public/package-facing Rust would become harder for Rust hosts to call or inspect. +- The emitted shape adds avoidable work to a likely-hot path. +- The issue blocks RFC 097-style Rust-hosted caller work or future generated crate stability. + +Broadening still needs discipline: state the affected compiler layers, list the generated surfaces that should change, add representative tests, and make the performance story explicit. + +## v0.3 Baseline + +For v0.3, the baseline is: + +- Generated Rust quality is documented as an explicit contributor contract. +- Representative generated Rust snapshots cover ordinary language features, stdlib compiled modules, Rust interop, ownership-sensitive regressions, and generated package/project behavior. +- A native Rust consumer characterization covers direct Cargo use of generated library output for currently supported public models and functions. +- Performance review is part of generated Rust review, especially around clone/allocation-heavy output. +- Larger helper APIs, inspection tooling, or caller-boundary architecture are tracked as follow-up work rather than silently hidden inside one emitter patch. + +Current audit gaps to track from the v0.3 baseline: + +- Direct `IrCodegen` snapshots are broad, but final package-facing artifact sets are thinner: `Cargo.toml`, nested `src/**`, `.incnlib`, Rust ABI metadata, and consumer `pub::` projects need a small golden baseline. +- Real stdlib generated output is covered unevenly. Important public modules should be classified as snapshot, compile-only, import-user-facing, or missing. +- Rust interop coverage is strong in direct snapshots, but weaker across generated library artifacts and downstream consumers. +- Rust-hosted callability coverage is currently limited to generated package artifacts and direct native Rust consumption of supported public items; RFC 097 caller adapters remain follow-up work. +- Iterator and comprehension output currently contain known clone/allocation-heavy shapes. Some are required by current semantics or API shape, but borrowed iterator and callback designs should be tracked as performance work. + +For v0.4, the natural next step is tooling maturity: generated Rust inspection commands, quality gates, or snapshot grouping that make drift easier to review. + +For v0.5 and later, the focus shifts toward architectural stability: stable helper crates, caller adapters, generated crate contracts, and compatibility policy on the path to 1.0. diff --git a/workspaces/docs-site/docs/contributing/how-to/auditing_generated_rust.md b/workspaces/docs-site/docs/contributing/how-to/auditing_generated_rust.md new file mode 100644 index 000000000..715d85a9c --- /dev/null +++ b/workspaces/docs-site/docs/contributing/how-to/auditing_generated_rust.md @@ -0,0 +1,83 @@ +# Auditing generated Rust + +Generated Rust is a derived artifact, but it is still a review surface for correctness, maintainability, and performance regressions. Use the generated Rust audit runner when a change affects lowering, emission, stdlib copying, generated test harnesses, or fixture packages under `target/incan/`. + +The audit report is a review skeleton. It records the artifact class, path, availability, strictness status, and structured placeholders for clone, allocation, and eager-collection notes. It does not assign a subjective score. + +## Run the default report + +After generating representative fixtures, run: + +```bash +scripts/generated_rust_audit.py +``` + +The default artifact list points at representative `target/incan/` fixture outputs. If those outputs have not been generated in the current worktree, the report keeps going and marks them as `missing` with `not_evaluated` strictness. + +To emit JSON for automation or later aggregation: + +```bash +scripts/generated_rust_audit.py --format json +``` + +To make missing artifacts fail a CI-style check: + +```bash +scripts/generated_rust_audit.py --fail-on-missing +``` + +To test the audit helper itself without relying on previously generated artifacts, run: + +```bash +make generated-rust-audit-gate +``` + +That target uses committed Rust fixtures and validates the JSON/Markdown report behavior, marker counting, missing-artifact handling, explicit artifact paths, and `--fail-on-missing`. + +## Audit explicit artifacts + +Pass generated Rust files or directories with `SURFACE_CLASS=PATH` specs: + +```bash +scripts/generated_rust_audit.py \ + --artifact program-main=target/incan/my_fixture/src/main.rs \ + --artifact stdlib-copy=target/incan/my_fixture/src/__incan_std \ + --artifact test-harness=target/incan/tests/my_harness +``` + +Use surface classes that describe the generated artifact's role, not its quality. Common classes: + +| Surface class | Use for | +| --------------- | -------------------------------------------- | +| `program-main` | Generated package entry point | +| `stdlib-copy` | Copied or synthesized `__incan_std` modules | +| `test-harness` | Generated test runner or preheat harnesses | +| `surface-fixture` | Fixture package directories for one feature | + +Directories are scanned recursively for `.rs` files. Non-existent paths and directories without Rust files are reported explicitly instead of being silently skipped. + +When an artifact path is inside the repository, the report renders it as a repository-relative path even if the command received an absolute path. + +## Read the report + +Each artifact row has these fields: + +| Field | Meaning | +| ------------------- | ------------------------------------------------------------ | +| Surface class | Reviewer-provided classification for the artifact role | +| Artifact path | File or directory that was requested | +| Check status | `present`, `missing`, `no-rust-files`, or `unsupported-path` | +| Strictness status | `available_for_review` when Rust files exist; otherwise `not_evaluated` | +| Clone notes | Marker count plus a manual-review placeholder | +| Allocation notes | Marker count plus a manual-review placeholder | +| Eager collection notes | Marker count plus a manual-review placeholder | + +Marker counts are literal occurrence scans for review prompts. They are not findings by themselves. The reviewer decides whether each marker is expected, avoidable, or needs a follow-up change. + +## Suggested contributor loop + +1. Build or run the representative fixture that exercises the compiler surface under review. +2. Run `scripts/generated_rust_audit.py` with explicit `--artifact` entries for the generated files or directories. +3. Save Markdown or JSON output with `--output` if the review needs an artifact. +4. Fill in the clone, allocation, and eager-collection notes during manual review. +5. Attach the report or summarize its objective statuses in the implementation handoff. diff --git a/workspaces/docs-site/docs/contributing/index.md b/workspaces/docs-site/docs/contributing/index.md index 5a9c9d021..4d2d016cf 100644 --- a/workspaces/docs-site/docs/contributing/index.md +++ b/workspaces/docs-site/docs/contributing/index.md @@ -11,6 +11,7 @@ If you’re new, start with: - [Extending the Language](how-to/extending_language.md) — when to add builtins vs new syntax; end-to-end checklists - [Author Library DSLs with `incan_vocab`](how-to/authoring_vocab_crates.md) — how to publish import-activated DSL blocks, scoped surfaces, and desugarers +- [Auditing generated Rust](how-to/auditing_generated_rust.md) — how to produce strict-surface generated Rust review reports ## Tutorials (learn) @@ -20,11 +21,13 @@ If you’re new, start with: - [Architecture](explanation/architecture.md) — compilation pipeline, module layout, internal stages - [Duckborrowing](explanation/duckborrowing.md) — backend ownership planning for generated Rust +- [Generated Rust quality](explanation/generated_rust_quality.md) — quality, performance, and hostability contract for emitted Rust - [Readable, maintainable Rust](explanation/readable-maintainable-rust.md) — team conventions and engineering practices ## Reference (look up) - [Layering rules](explanation/layering.md) — dependency boundaries and guardrails +- [Generated Rust stdlib coverage](reference/generated_rust_stdlib_coverage.md) — generated-Rust coverage inventory for stdlib modules ## Design (RFCs and roadmap) diff --git a/workspaces/docs-site/docs/contributing/reference/generated_rust_stdlib_coverage.md b/workspaces/docs-site/docs/contributing/reference/generated_rust_stdlib_coverage.md new file mode 100644 index 000000000..cd91d639c --- /dev/null +++ b/workspaces/docs-site/docs/contributing/reference/generated_rust_stdlib_coverage.md @@ -0,0 +1,178 @@ +# Generated Rust stdlib coverage inventory + +This inventory tracks generated Rust coverage for every `crates/incan_stdlib/stdlib/**/*.incn` source module. It is a maintenance aid for deciding where generated stdlib Rust needs stronger tests; it is not a claim that the runtime behavior of every exported API is exhaustively covered. + +Generated from repo inspection on 2026-05-20 with: + +```sh +rg --files crates/incan_stdlib/stdlib | rg '\.incn$' +rg -n 'std_|from std\.|import std' tests tests/codegen_snapshots crates/incan_stdlib/tests +``` + +## Coverage labels + +| Label | Meaning | +| --- | --- | +| `snapshot-covered` | A codegen snapshot directly records generated Rust from the stdlib source module or from a focused user import that asserts generated Rust shape. | +| `compile-only-covered` | A test directly generates Rust from the module and asserts it compiles or emits an Incan-generated Rust artifact, but does not snapshot the generated Rust. | +| `import/user-facing-covered` | An `incan run`, fixture, or import-focused test exercises generated Rust through the public stdlib surface, but does not snapshot that module's generated Rust. | +| `indirect-only` | The module is pulled in by another covered module or prelude, but no test directly targets its generated Rust or user-facing surface. | +| `missing` | No current test evidence was found for generated Rust coverage. | + +## Inventory + +### Root modules + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/collections.incn` | `std.collections` | `import/user-facing-covered` | `tests/fixtures/rfc030_std_collections_behavior.incn`, `std_ordinal_map_surface`, `ordinal_key_builtin_impls`, `ordinal_map_str_fast_lookup`, and layering guards. | Add a direct generated-Rust snapshot for the module source or a focused snapshot for the most important generated helpers. | +| `stdlib/result.incn` | `std.result` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_result_source_snapshot` snapshots direct source compilation; `test_std_result_helpers_compile_and_run` and Result method dogfood tests run helper calls through generated projects. | Keep direct source snapshot aligned when Result helper lowering changes. | +| `stdlib/prelude.incn` | `std` prelude | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_root_prelude_import_snapshot` snapshots representative root prelude re-exports for derive, conversion, ops, error, indexing, and callable traits. | Add more imported trait families only when the prelude surface expands. | +| `stdlib/logging.incn` | `std.logging` | `import/user-facing-covered` | Multiple integration tests run `basic_config`, `get_logger`, ambient `log`, JSON rendering, invalid logger names, and structured fields. | Add one generated-Rust snapshot for a minimal `std.logging` import to guard emitted module wiring. | +| `stdlib/testing.incn` | `std.testing` | `snapshot-covered` | `test_std_testing_compiled_codegen` snapshots direct module compilation; many CLI and integration tests exercise assertions, fixtures, parametrization, marks, resources, and skips. | Keep as-is unless new decorators/helpers are added. | +| `stdlib/math.incn` | `std.math` | `snapshot-covered` | `std_math` codegen snapshot plus `test_std_math_module_constants_and_functions_run` and numeric-like helper runtime tests. | Add missing function cases only when public math surface expands. | +| `stdlib/graph.incn` | `std.graph` | `snapshot-covered` | `test_std_graph_compiled_codegen`, `std_graph_import`, and `std_graph_surface` cover declarations, import lowering, constructors, DAGs, and multigraph edge IDs. | Keep import fixture aligned with any new graph types or methods. | +| `stdlib/reflection.incn` | `std.reflection` | `import/user-facing-covered` | `tests/fixtures/valid/std_reflection_import.incn`, `field_info_reflection.incn`, and missing-import diagnostics cover public import and compiler reflection behavior. | Add a generated-Rust snapshot for `FieldInfo` import/use. | +| `stdlib/this.incn` | `std.this` | `missing` | No direct references found in tests or fixtures. | Add either a compile-only source test if this is internal glue, or delete/deprecate if unused. | +| `stdlib/uuid.incn` | `std.uuid` | `snapshot-covered` | `test_std_uuid_compiled_codegen`, `std_uuid_import`, `std_uuid_surface`, and layering guards cover source-defined UUID generation/imports and absence of Rust-backed UUID type. | Keep as-is unless new UUID versions or formatting helpers are added. | +| `stdlib/io.incn` | `std.io` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_io_source_snapshot` snapshots direct source compilation; `test_std_io_compile_and_run_bytesio_core_and_numeric_helpers` exercises `BytesIO` and numeric helpers at runtime; fs, hash, compression, encoding, uuid, and tempfile tests also import it. | Keep direct source snapshot aligned when `BytesIO` or binary reader/writer lowering changes. | +| `stdlib/json.incn` | `std.json` | `import/user-facing-covered` | `test_std_json_value_indexing_emits_checked_helpers`, JSON deserialize/value runtime tests, and serde integration tests. | Add a snapshot covering `JsonValue` constructors plus object/array indexing. | +| `stdlib/tempfile.incn` | `std.tempfile` | `snapshot-covered` | `std_tempfile_import` snapshot and `test_std_tempfile_compile_and_run_named_file_and_directory`. | Keep as-is; add new runtime cases when persistence or cleanup semantics change. | + +### `std.async` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/async/prelude.incn` | `std.async` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_async_prelude_import_snapshot` snapshots representative public async imports; task/time/channel/sync/race modules are covered individually. | Add more re-export names only when the public async prelude expands. | +| `stdlib/async/task.incn` | `std.async.task` | `snapshot-covered` | `test_std_async_task_compiled_codegen`, async task/time wrapper fixture, and spawn runtime tests. | Keep as-is. | +| `stdlib/async/time.incn` | `std.async.time` | `snapshot-covered` | `test_std_async_time_compiled_codegen`, timeout/sleep wrapper fixture, race helper tests, and runtime timeout tests. | Keep as-is. | +| `stdlib/async/channel.incn` | `std.async.channel` | `snapshot-covered` | `test_std_async_channel_compiled_codegen` plus channel runtime tests using `channel`, `unbounded_channel`, and `oneshot`. | Keep as-is. | +| `stdlib/async/sync.incn` | `std.async.sync` | `snapshot-covered` | `test_std_async_sync_compiled_codegen` plus runtime tests for `Mutex`, `RwLock`, `Semaphore`, and `Barrier`. | Keep as-is. | +| `stdlib/async/race.incn` | `std.async.race` | `snapshot-covered` | `test_std_async_race_compiled_codegen`, `race_for_expression_codegen`, and helper runtime tests. | Keep as-is. | + +### `std.compression` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/compression/prelude.incn` | `std.compression` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_compression_prelude_source_snapshot` snapshots direct prelude source compilation; `test_std_compression_modules_compile_codegen` includes this file; `std_compression_surface` runs the public surface. | Keep as-is unless public compression re-exports change. | +| `stdlib/compression/_core.incn` | `std.compression._core` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_compression_core_source_snapshot` snapshots direct core source compilation; direct compile loop and public compression surface tests cover all codecs. | Add codec-specific snapshots only if per-codec lowering diverges from shared core helpers. | +| `stdlib/compression/_auto.incn` | `std.compression._auto` | `compile-only-covered` | Direct compile loop plus `decompress_auto` and `decompress_auto_stream` in the surface fixture. | Add snapshot or runtime cases for ambiguous/unsupported header paths. | +| `stdlib/compression/gzip.incn` | `std.compression.gzip` | `compile-only-covered` | Direct compile loop and surface fixture. | Add per-codec generated-Rust snapshot only if codec lowering diverges. | +| `stdlib/compression/zlib.incn` | `std.compression.zlib` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/deflate.incn` | `std.compression.deflate` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/zstd.incn` | `std.compression.zstd` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/bz2.incn` | `std.compression.bz2` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/lzma.incn` | `std.compression.lzma` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/snappy.incn` | `std.compression.snappy` | `compile-only-covered` | Direct compile loop and surface fixture. | Same as gzip. | +| `stdlib/compression/snappy/raw.incn` | `std.compression.snappy.raw` | `compile-only-covered` | Direct compile loop and surface fixture imports raw snappy compress/decompress. | Add a small generated-Rust assertion for raw module import path if this remains a nested module. | + +### `std.datetime` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/datetime/prelude.incn` | `std.datetime` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_datetime_prelude_import_snapshot` snapshots representative public datetime re-exports; `std_datetime_surface` imports and runs `std.datetime` names. | Add more re-export names only when the public datetime prelude expands. | +| `stdlib/datetime/runtime.incn` | `std.datetime.runtime` | `import/user-facing-covered` | `test_std_datetime_surface_runs_with_std_time_runtime_boundary` reads this file and asserts the Rust `std::time` boundary before running the surface fixture. | Add a snapshot for `Instant`, `Duration`, and `SystemTime` generated Rust if boundary churn continues. | +| `stdlib/datetime/error.incn` | `std.datetime.error` | `indirect-only` | Used by runtime/civil modules and exercised through error cases in `std_datetime_surface`; no direct generated-Rust target found. | Add direct compile or import snapshot for `DateTimeError`. | +| `stdlib/datetime/civil.incn` | `std.datetime.civil` | `import/user-facing-covered` | `std_datetime_surface` reads and runs the civil aggregate with calendar, parsing, formatting, and offset cases. | Add snapshot for aggregate re-exports. | +| `stdlib/datetime/civil/intervals.incn` | `std.datetime.civil.intervals` | `import/user-facing-covered` | Included by the datetime surface test's civil directory read and used by `TimeDelta`, `YearMonthInterval`, and `DateTimeInterval` fixture cases. | Add direct source snapshot if interval generated code changes often. | +| `stdlib/datetime/civil/naive.incn` | `std.datetime.civil.naive` | `snapshot-covered` | `imported_stdlib_value_fragment` snapshots an import from this module; `std_datetime_surface` covers dates, times, parsing, formatting, and ordinal helpers. | Broaden snapshot coverage beyond one imported value if needed. | +| `stdlib/datetime/civil/offset.incn` | `std.datetime.civil.offset` | `import/user-facing-covered` | Included by datetime surface fixture through `DateTimeOffset` cases. | Add direct import snapshot for offset parsing/formatting names. | + +### `std.derives` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/derives/comparison.incn` | `std.derives.comparison` | `snapshot-covered` | `test_std_derives_comparison_compiled_codegen`. | Keep as-is. | +| `stdlib/derives/copying.incn` | `std.derives.copying` | `snapshot-covered` | `test_std_derives_copying_compiled_codegen`. | Keep as-is. | +| `stdlib/derives/string.incn` | `std.derives.string` | `snapshot-covered` | `test_std_derives_string_compiled_codegen`. | Keep as-is. | +| `stdlib/derives/collection.incn` | `std.derives.collection` | `snapshot-covered` | `test_std_derives_collection_compiled_codegen`. | Keep as-is. | + +### `std.encoding` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/encoding/prelude.incn` | `std.encoding` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_encoding_prelude_import_snapshot` snapshots representative public prelude imports for family modules and `EncodingError`; `rfc064_std_encoding_behavior` imports the public prelude; algorithm modules are covered individually. | Add more family imports only when the public encoding prelude expands. | +| `stdlib/encoding/_shared.incn` | `std.encoding._shared` | `indirect-only` | Imported by all algorithm modules and covered through their tests; no direct source/import target found. | Add a compact compile test for `EncodingError` and shared helpers. | +| `stdlib/encoding/hex.incn` | `std.encoding.hex` | `import/user-facing-covered` | `std_encoding_hex_surface` fixture and RFC 064 encoding behavior fixture. | Add direct module-source runtime coverage like the other algorithms, or a generated-Rust snapshot. | +| `stdlib/encoding/base32.incn` | `std.encoding.base32` | `import/user-facing-covered` | `tests/std_encoding_algorithm_modules.rs` runs module source with vector and lenient decode assertions; RFC 064 fixture also imports it. | Add snapshot only if generated helper shape needs review. | +| `stdlib/encoding/base58.incn` | `std.encoding.base58` | `import/user-facing-covered` | `tests/std_encoding_algorithm_modules.rs` and RFC 064 fixture. | Same as base32. | +| `stdlib/encoding/base64.incn` | `std.encoding.base64` | `import/user-facing-covered` | `tests/std_encoding_algorithm_modules.rs` and RFC 064 fixture. | Same as base32. | +| `stdlib/encoding/base85.incn` | `std.encoding.base85` | `import/user-facing-covered` | `tests/std_encoding_algorithm_modules.rs` and RFC 064 fixture. | Same as base32. | +| `stdlib/encoding/bech32.incn` | `std.encoding.bech32` | `import/user-facing-covered` | `tests/std_encoding_algorithm_modules.rs` and RFC 064 fixture. | Same as base32. | + +### `std.fs` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/fs/prelude.incn` | `std.fs` | `snapshot-covered` | `std_fs_import` snapshot and `test_std_fs_compile_and_run_path_file_and_tree_operations`. | Keep as-is. | +| `stdlib/fs/path.incn` | `std.fs.path` | `import/user-facing-covered` | `std.fs` integration test exercises paths, globbing, reads/writes, copy/move/touch/stat, and tree removal. | Add direct source snapshot for `Path` methods if generated method shape is important. | +| `stdlib/fs/file.incn` | `std.fs.file` | `import/user-facing-covered` | `std.fs` integration test exercises open modes, readers/writers, encodings, `OpenOptions`, and byte/text operations. | Add direct source snapshot for file/open option definitions. | +| `stdlib/fs/metadata.incn` | `std.fs.metadata` | `import/user-facing-covered` | `std.fs` integration test exercises `stat`, `modified_unix`, `disk_usage`, and directory entries. | Add snapshot if metadata models change. | +| `stdlib/fs/glob.incn` | `std.fs.glob` | `import/user-facing-covered` | `test_std_fs_glob_string_api_compile_and_run` and `Path.glob`/`Path.rglob` integration coverage. | Add direct generated-Rust assertion for glob helpers only if matching semantics move. | + +### `std.hash` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/hash/prelude.incn` | `std.hash` | `import/user-facing-covered` | `test_std_hash_compile_and_run_digest_file_and_error_paths` imports digest and streaming helpers. | Add generated-Rust snapshot for public hash prelude re-exports. | +| `stdlib/hash/_core.incn` | `std.hash._core` | `import/user-facing-covered` | Covered through digest calls for MD5, SHA, SHA3, Blake, Shake, and xxhash in the hash integration test. | Add direct compile/snapshot for core digest wrappers. | +| `stdlib/hash/_streaming.incn` | `std.hash._streaming` | `import/user-facing-covered` | Covered through `file_digest`, `reader_digest`, and typed file/reader hash helpers in the hash integration test. | Add direct compile/snapshot for streaming helper lowering. | + +### `std.regex` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/regex/prelude.incn` | `std.regex` | `import/user-facing-covered` | `std_regex_surface`, constructor-hook codegen test, and layering guard cover public imports and source-owned behavior. | Add snapshot for public prelude import lowering. | +| `stdlib/regex/_core.incn` | `std.regex._core` | `import/user-facing-covered` | `std_regex_surface` exercises constructors and matching; layering guard checks regex engine construction remains in source. | Add direct source snapshot for core generated Rust. | +| `stdlib/regex/types.incn` | `std.regex.types` | `import/user-facing-covered` | `std_regex_surface` exercises `Captures`, `Match`, and iterators; layering guard includes this file. | Add direct snapshot if iterator generated code changes. | +| `stdlib/regex/_replacement.incn` | `std.regex._replacement` | `import/user-facing-covered` | `std_regex_surface` and layering guard cover replacement helpers staying in Incan source. | Add explicit runtime replacement edge-case test if replacement syntax expands. | + +### `std.serde` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/serde/prelude.incn` | `std.serde` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_serde_prelude_import_snapshot` snapshots `from std.serde import json` derive resolution; existing user fixtures also import `from std.serde import json`. | Keep as-is unless additional serde prelude re-exports are added. | +| `stdlib/serde/json.incn` | `std.serde.json` | `snapshot-covered` | `test_std_serde_json_compiled_codegen`, `std_serde_json_import`, `std_serde_with_serialize_trait`, module derive snapshots, and JSON runtime tests. | Keep as-is. | + +### `std.telemetry` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/telemetry/prelude.incn` | `std.telemetry` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_telemetry_prelude_import_snapshot` snapshots representative public telemetry re-exports; logging tests import `std.telemetry.core`. | Add runtime smoke once telemetry provider APIs become user-facing. | +| `stdlib/telemetry/core.incn` | `std.telemetry.core` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_telemetry_core_source_snapshot` snapshots direct core source compilation; logging JSON structured-field tests import `TelemetryValue`; logging source imports telemetry core types. | Keep direct source snapshot aligned when telemetry data models expand. | + +### `std.traits` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/traits/prelude.incn` | `std.traits` | `snapshot-covered` | `test_std_traits_prelude_compiled_codegen`. | Keep as-is. | +| `stdlib/traits/ops.incn` | `std.traits.ops` | `snapshot-covered` | `test_std_traits_ops_compiled_codegen`. | Keep as-is. | +| `stdlib/traits/error.incn` | `std.traits.error` | `snapshot-covered` | `test_std_traits_error_compiled_codegen`; also used by error-bearing stdlib modules. | Keep as-is. | +| `stdlib/traits/indexing.incn` | `std.traits.indexing` | `snapshot-covered` | `test_std_traits_indexing_compiled_codegen` and `JsonValue` indexing generated-Rust assertions. | Keep as-is. | +| `stdlib/traits/callable.incn` | `std.traits.callable` | `snapshot-covered` | `test_std_traits_callable_compiled_codegen` and callable object Result tests. | Keep as-is. | +| `stdlib/traits/convert.incn` | `std.traits.convert` | `snapshot-covered` | `test_std_traits_convert_compiled_codegen`, `std_traits_convert_usage` snapshot, and runtime usage test. | Keep as-is. | + +### `std.web` + +| Source file | Module | Current coverage | Evidence | Recommended next test | +| --- | --- | --- | --- | --- | +| `stdlib/web/prelude.incn` | `std.web` | `snapshot-covered` | `stdlib_generated_rust_snapshot_tests::std_web_prelude_import_snapshot` snapshots representative public web prelude imports including route macro wiring; existing public web import snapshots exercise re-exported names. | Add more web prelude imports only when the public route/request/response surface expands. | +| `stdlib/web/app.incn` | `std.web.app` | `import/user-facing-covered` | `std_web_routing_compiled`, web route extractor snapshots, and app route codegen tests. | Add direct source snapshot if `App` shape changes. | +| `stdlib/web/request.incn` | `std.web.request` | `import/user-facing-covered` | `web_route_extractors`, nested route extractor snapshots, and `newtype_from_request`. | Add source snapshot for `Query`/`Path` extractor models. | +| `stdlib/web/response.incn` | `std.web.response` | `import/user-facing-covered` | `newtype_web_response`, web route extractor snapshots, and response wrapper codegen. | Add source snapshot for `Json`, `Html`, and `Response` if response semantics change. | +| `stdlib/web/routing.incn` | `std.web.routing` | `snapshot-covered` | `std_web_routing_compiled`, route extractor snapshots, and route invalid-usage tests. | Keep as-is. | +| `stdlib/web/macros.incn` | `std.web.macros` | `import/user-facing-covered` | Used by response/request extractor and route snapshots through `IntoResponse` and `FromRequestParts`. | Add direct compile/snapshot for macro marker traits. | + +## Representative gaps + +- Prelude and re-export modules now have representative generated-Rust snapshots for `std`, `std.async`, `std.datetime`, `std.encoding`, `std.serde`, `std.telemetry`, and `std.web`. Future gaps should focus on newly added re-exports rather than duplicating every imported name. +- `stdlib/this.incn` has no test evidence in current repo searches. It needs an owner decision: add direct coverage if it is intentionally shipped, or remove/deprecate it if it is stale. +- Runtime/user-facing smoke tests are strong for `std.fs`, `std.hash`, `std.logging`, and `std.datetime`, but most of those still do not preserve generated Rust shape. `std.io` and `std.result` now have direct source snapshots; for additional generated Rust regressions, add compact snapshots instead of broad runtime-only tests. +- `std.compression` has direct compile checks for every module plus representative snapshots for the public prelude and shared core. Add codec-specific snapshots only if codec lowering starts to diverge. + +## Recommended next tests + +1. Add one direct generated-Rust snapshot each for `std.hash`, `std.json`, and `std.logging`, because their runtime coverage is useful but does not review emitted Rust shape. +2. Consider direct snapshots for `std.fs.path`, `std.fs.file`, `std.regex._core`, and selected datetime civil modules only if those generated shapes begin changing frequently. +3. Decide whether `stdlib/this.incn` is intentional. If yes, add a compile-only or snapshot test that names its expected purpose. +4. For `std.compression`, keep the current representative snapshots scoped to public prelude and shared core unless per-codec lowering needs direct review. diff --git a/workspaces/docs-site/docs/release_notes/0_3.md b/workspaces/docs-site/docs/release_notes/0_3.md index a02e0f28a..a47deeb5b 100644 --- a/workspaces/docs-site/docs/release_notes/0_3.md +++ b/workspaces/docs-site/docs/release_notes/0_3.md @@ -568,6 +568,7 @@ The sections above are the release story. The list below is the detailed invento - **Tooling**: `incan lock` now treats manifest projects as a project-wide lock surface, covering declared scripts and test harness dependency inputs so multi-entrypoint projects do not alternate stale-lock warnings between `incan test` and `incan run src/extra.incn` (#505). - **Tooling**: `incan fmt` now wraps long class trait adoption headers into parseable parenthesized `with (...)` lists, keeping broad adoption surfaces such as `_BytesIO` readable and below the line-length target (#565). - **Tooling**: Project-aware commands now enforce `[project].requires-incan`, env-level `requires-incan` can narrow named environment workflows, and `incan env show` / `env run --dry-run` report the effective toolchain compatibility before scripts run; RFC 073 matrix expansion remains deferred beyond `0.3` (#401, RFC 073). +- **Tooling/Compiler**: Generated Rust quality now has artifact-level package baselines, representative stdlib generated-Rust snapshots plus coverage inventory, an audit-report helper with a deterministic strict gate, package-facing callable characterization, a native Rust consumer fixture for generated libraries, and ownership-planner hot-path improvements that avoid proven-unnecessary clone calls for Copy comprehensions and selected owned iterator sources (#599, #600, #601, #602, #603). ### Documentation @@ -591,6 +592,7 @@ The sections above are the release story. The list below is the detailed invento - **Project metadata**: Workspace package metadata and Cargo lock entries now identify the development line as `0.3.0-dev.47`. - **Project metadata**: Workspace package metadata and Cargo lock entries now identify the development line as `0.3.0-dev.48`. - **Project metadata**: Workspace package metadata and Cargo lock entries now identify the development line as `0.3.0-dev.49`. +- **Project metadata**: Workspace package metadata and Cargo lock entries now identify the development line as `0.3.0-dev.50`. ## Known limitations (0.3) diff --git a/workspaces/docs-site/mkdocs.yml b/workspaces/docs-site/mkdocs.yml index 93f409469..07e2e6115 100644 --- a/workspaces/docs-site/mkdocs.yml +++ b/workspaces/docs-site/mkdocs.yml @@ -269,16 +269,20 @@ nav: - How-to: - Extending the language: contributing/how-to/extending_language.md - Author library DSLs with incan_vocab: contributing/how-to/authoring_vocab_crates.md + - Auditing generated Rust: contributing/how-to/auditing_generated_rust.md - Benchmarks & profiling (repository): contributing/how-to/benchmarks_and_profiling.md - CI & automation (repository): contributing/how-to/ci_and_automation.md - Writing RFCs: contributing/how-to/writing_rfcs.md - Explanation: - Architecture: contributing/explanation/architecture.md - Duckborrowing: contributing/explanation/duckborrowing.md + - Generated Rust quality: contributing/explanation/generated_rust_quality.md - Layering rules: contributing/explanation/layering.md - Incapunk docs theme: contributing/explanation/incapunk_docs_theme.md - Readable, maintainable Rust: contributing/explanation/readable-maintainable-rust.md - "std.testing internals": contributing/explanation/stdlib_testing_internals.md + - Reference: + - Generated Rust stdlib coverage: contributing/reference/generated_rust_stdlib_coverage.md - RFCs: RFCs/index.md - Release notes: - Overview: release_notes/index.md