diff --git a/.cargo/config.toml b/.cargo/config.toml index f78950a..15abf59 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,7 @@ [alias] xtask = "run -p xtask --" +ci = "xtask dev ci" +fmt-check = "xtask dev fmt-check" qa = "xtask dev qa" lint = "xtask dev lint" test-all = "xtask dev test" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..faa3741 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,96 @@ +name: CI + +on: + push: + branches: [main, master] + pull_request: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + CARGO_INCREMENTAL: 0 + +# `tool/*` Git submodules are Cargo path dependencies. Checkout fetches them so the +# workspace can compile; crate-specific fmt/clippy/test policy belongs in each +# submodule repository’s own CI, not duplicated here beyond what Aura already +# exercises through `cargo test` / `cargo clippy` on this repo. + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo xtask dev fmt-check + + workspace: + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + run_lint: true + - os: windows-latest + run_lint: false + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - if: matrix.run_lint + run: cargo xtask dev lint + - run: cargo xtask dev test + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - run: cargo xtask docs check + + llvm: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: dtolnay/rust-toolchain@stable + - name: Install LLVM prebuilt dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y libxml2 zlib1g libzstd1 wget ca-certificates + # LLVM.org x86_64 Linux tarballs target Ubuntu 18.04 and link libtinfo.so.5. Ubuntu 24.04+ + # images dropped ncurses5 packages; install Jammy's libtinfo5 .deb (same ABI family as tinfo6). + wget -q "http://security.ubuntu.com/ubuntu/pool/universe/n/ncurses/libtinfo5_6.3-2ubuntu0.1_amd64.deb" + sudo apt-get install -y ./libtinfo5_6.3-2ubuntu0.1_amd64.deb + - uses: Swatinem/rust-cache@v2 + # Cache only the downloaded tarball. Caching the extracted `toolchains/llvm` + # tree can restore broken `toolchains/llvm/18` symlinks across runners. + - uses: actions/cache@v4 + with: + path: toolchains/cache + key: llvm-18.1.8-${{ runner.os }}-${{ hashFiles('xtask/src/main.rs') }} + - run: cargo xtask llvm setup + - name: Prefer bundled LLVM shared libraries at runtime (Linux) + if: runner.os == 'Linux' + run: echo "LD_LIBRARY_PATH=${{ github.workspace }}/toolchains/llvm/18/lib:$LD_LIBRARY_PATH" >> "$GITHUB_ENV" + - run: cargo xtask llvm clippy + - run: cargo xtask llvm test diff --git a/AGENTS.md b/AGENTS.md index db33087..6af5ede 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -23,6 +23,8 @@ Agents should treat `docs/` as a maintained second-brain surface, not as optiona - `examples` - `tool` +Repositories under `tool/` are often **Git submodules** with their **own CI** in those repos. Aura’s GitHub workflow checks out submodules only so Cargo `path` dependencies resolve; it does not replace per-repo automation there. + Key entry files: - `crates/aura-frontend/src/token.rs` — token model @@ -59,6 +61,8 @@ cargo xtask dev build cargo xtask dev test cargo xtask dev lint cargo xtask dev fmt +cargo xtask dev fmt-check +cargo xtask dev ci cargo xtask dev qa cargo xtask docs sync cargo xtask docs check @@ -67,6 +71,8 @@ cargo xtask docs check Preferred command runner aliases (defined in `.cargo/config.toml`): ```bash +cargo ci +cargo fmt-check cargo qa cargo lint cargo test-all @@ -77,6 +83,12 @@ cargo docs-sync cargo docs-check ``` +### CI parity before ending a session + +Before wrapping up non-trivial work (especially codegen, xtask, `docs/` generated inventories, or LLVM paths), run **`cargo xtask dev ci`** (or **`cargo ci`**) so local results match [GitHub Actions](https://github.com/nahharris/aura/actions/workflows/ci.yml): rustfmt check and `docs check` (Linux runners), workspace clippy once plus tests on Linux and Windows, then LLVM doctor, clippy, and tests (Linux and Windows). + +Doc-only or inventory edits still use `cargo xtask docs sync` when you change what should be generated; `dev ci` includes `docs check` so a single command catches stale generated notes before you stop. + ## Xtask Usage Use `cargo xtask` for project automation and environment-sensitive routines. @@ -90,6 +102,7 @@ Common commands: ```bash cargo xtask llvm setup cargo xtask llvm doctor +cargo xtask llvm ci cargo xtask llvm check cargo xtask llvm build cargo xtask llvm test diff --git a/README.md b/README.md index a404b4e..b05ae73 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Status: Pre-Alpha](https://img.shields.io/badge/status-pre--alpha-blue)](#project-status) [![Rust Workspace](https://img.shields.io/badge/rust-workspace-orange?logo=rust)](#for-developers) -[![CI](https://img.shields.io/badge/ci-not%20configured-lightgrey)](#for-developers) +[![CI](https://github.com/nahharris/aura/actions/workflows/ci.yml/badge.svg)](https://github.com/nahharris/aura/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-TBD-lightgrey)](#project-status) > [!NOTE] diff --git a/crates/aura-cli/src/main.rs b/crates/aura-cli/src/main.rs index 5c5c390..cc1fff9 100644 --- a/crates/aura-cli/src/main.rs +++ b/crates/aura-cli/src/main.rs @@ -784,7 +784,7 @@ fn render_ir_pretty(module: &CheckedModule) -> String { out.push_str("## Value Types\n"); let mut values: Vec<_> = module.value_types.iter().collect(); - values.sort_by(|(a, _), (b, _)| a.cmp(b)); + values.sort_by_key(|(name, _)| name.as_str()); for (name, ty) in values { out.push_str(&format!("- {name}: ty#{}\n", ty.0)); } @@ -1009,16 +1009,21 @@ fn find_static_decl_name_span(source: &str, name: &str) -> Option { } fn span_from_line(source: &str, line_number: usize, column: usize, len: usize) -> Span { - let mut start = 0usize; - let mut line = 1usize; - for part in source.split_inclusive('\n') { - if line == line_number { - break; + let mut offset = 0usize; + for (idx, part) in source.split_inclusive('\n').enumerate() { + let current_line = idx + 1; + if current_line == line_number { + let start = offset + column.saturating_sub(1); + return Span { + start, + end: start + len.max(1), + line: line_number, + column, + }; } - start += part.len(); - line += 1; + offset += part.len(); } - let start = start + column.saturating_sub(1); + let start = offset + column.saturating_sub(1); Span { start, end: start + len.max(1), diff --git a/crates/aura-codegen/src/llvm/expr.rs b/crates/aura-codegen/src/llvm/expr.rs index 7294b01..2e975cc 100644 --- a/crates/aura-codegen/src/llvm/expr.rs +++ b/crates/aura-codegen/src/llvm/expr.rs @@ -316,9 +316,7 @@ pub fn lower_expr<'ctx, 'm>( } #[cfg(feature = "llvm-backend")] -fn interface_object_type<'ctx>( - cg: &CodegenContext<'ctx, '_>, -) -> inkwell::types::StructType<'ctx> { +fn interface_object_type<'ctx>(cg: &CodegenContext<'ctx, '_>) -> inkwell::types::StructType<'ctx> { let ptr = cg.context.ptr_type(AddressSpace::default()); cg.context.struct_type(&[ptr.into(), ptr.into()], false) } @@ -368,7 +366,10 @@ fn lower_make_interface_obj<'ctx, 'm>( ) .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? }; - let fn_ptr = thunk.as_global_value().as_pointer_value().as_basic_value_enum(); + let fn_ptr = thunk + .as_global_value() + .as_pointer_value() + .as_basic_value_enum(); cg.builder .build_store(slot_ptr, fn_ptr) .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))?; @@ -431,11 +432,7 @@ fn lower_interface_call<'ctx, 'm>( let ptr_ty = cg.context.ptr_type(AddressSpace::default()); let vtable_as_ptr_ptr = cg .builder - .build_bit_cast( - vtable_ptr, - ptr_ty.ptr_type(AddressSpace::default()), - "iface_call_vtable_cast", - ) + .build_bit_cast(vtable_ptr, ptr_ty, "iface_call_vtable_cast") .map_err(|_| CodegenError::UnsupportedExpression("interface_call"))? .into_pointer_value(); let slot_ptr = unsafe { @@ -457,7 +454,7 @@ fn lower_interface_call<'ctx, 'm>( .builder .build_bit_cast( fn_ptr.into_pointer_value(), - fn_ty.ptr_type(AddressSpace::default()), + cg.context.ptr_type(AddressSpace::default()), "iface_call_callable", ) .map_err(|_| CodegenError::UnsupportedExpression("interface_call"))? @@ -533,7 +530,10 @@ fn ensure_interface_thunk<'ctx, 'm>( method_link: &str, method_fn: FunctionValue<'ctx>, ) -> Result, CodegenError> { - let thunk_name = format!("__aura_iface_thunk_{}_{}_{}", concrete_ty.0, method_index, method_link); + let thunk_name = format!( + "__aura_iface_thunk_{}_{}_{}", + concrete_ty.0, method_index, method_link + ); if let Some(existing) = cg.module.get_function(&thunk_name) { return Ok(existing); } @@ -551,7 +551,7 @@ fn ensure_interface_thunk<'ctx, 'm>( .builder .build_bit_cast( data_param, - concrete_basic.ptr_type(AddressSpace::default()), + cg.context.ptr_type(AddressSpace::default()), "iface_thunk_concrete_ptr", ) .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? @@ -577,25 +577,24 @@ fn ensure_interface_thunk<'ctx, 'm>( .try_as_basic_value() .left() .ok_or(CodegenError::UnsupportedExpression("make_interface_obj"))?; - if let Some(expected_ret) = thunk.get_type().get_return_type() { - if let (BasicValueEnum::IntValue(ret_int), BasicTypeEnum::IntType(expected_int)) = + if let Some(expected_ret) = thunk.get_type().get_return_type() + && let (BasicValueEnum::IntValue(ret_int), BasicTypeEnum::IntType(expected_int)) = (ret, expected_ret) - { - let from_w = ret_int.get_type().get_bit_width(); - let to_w = expected_int.get_bit_width(); - if from_w != to_w { - ret = if from_w > to_w { - cg.builder - .build_int_truncate(ret_int, expected_int, "iface_ret_trunc") - .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? - .as_basic_value_enum() - } else { - cg.builder - .build_int_z_extend(ret_int, expected_int, "iface_ret_zext") - .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? - .as_basic_value_enum() - }; - } + { + let from_w = ret_int.get_type().get_bit_width(); + let to_w = expected_int.get_bit_width(); + if from_w != to_w { + ret = if from_w > to_w { + cg.builder + .build_int_truncate(ret_int, expected_int, "iface_ret_trunc") + .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? + .as_basic_value_enum() + } else { + cg.builder + .build_int_z_extend(ret_int, expected_int, "iface_ret_zext") + .map_err(|_| CodegenError::UnsupportedExpression("make_interface_obj"))? + .as_basic_value_enum() + }; } } cg.builder @@ -715,7 +714,7 @@ fn field_ptr<'ctx, 'm>( } #[cfg(feature = "llvm-backend")] -fn coerce_basic_value_for_slot<'ctx, 'm>( +pub(super) fn coerce_basic_value_for_slot<'ctx, 'm>( cg: &CodegenContext<'ctx, 'm>, value: BasicValueEnum<'ctx>, target_ty: aura_typecheck::TyId, @@ -1266,46 +1265,42 @@ fn lower_enum_match<'ctx, 'm>( cg.builder.position_at_end(arm_block); cg.push_local_scope(); - if let Some(name) = &arm.binding_name { - if let Some(payload_ty) = variants + if let Some(name) = &arm.binding_name + && let Some(payload_ty) = variants .get(arm.variant_index) .and_then(|(_, payload)| *payload) - { - let payload_value = - load_enum_payload(cg, scrutinee_slot, enum_basic_ty, payload_ty)?; - let slot = allocate_local_slot(cg, name, payload_ty)?; - cg.builder - .build_store(slot.ptr, payload_value) - .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; - cg.insert_local(name.clone(), slot); - } + { + let payload_value = load_enum_payload(cg, scrutinee_slot, enum_basic_ty, payload_ty)?; + let slot = allocate_local_slot(cg, name, payload_ty)?; + cg.builder + .build_store(slot.ptr, payload_value) + .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; + cg.insert_local(name.clone(), slot); } - if !arm.struct_bindings.is_empty() { - if let Some(payload_ty) = variants + if !arm.struct_bindings.is_empty() + && let Some(payload_ty) = variants .get(arm.variant_index) .and_then(|(_, payload)| *payload) - { - let payload_value = - load_enum_payload(cg, scrutinee_slot, enum_basic_ty, payload_ty)?; - let BasicValueEnum::StructValue(payload_struct) = payload_value else { - return Err(CodegenError::UnsupportedExpression("enum_match")); - }; - for binding in &arm.struct_bindings { - let field_value = cg - .builder - .build_extract_value( - payload_struct, - binding.field_index as u32, - &format!("enum_match_{}", binding.name), - ) - .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; - let slot = allocate_local_slot(cg, &binding.name, binding.ty)?; - let stored = coerce_basic_value_for_slot(cg, field_value, binding.ty)?; - cg.builder - .build_store(slot.ptr, stored) - .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; - cg.insert_local(binding.name.clone(), slot); - } + { + let payload_value = load_enum_payload(cg, scrutinee_slot, enum_basic_ty, payload_ty)?; + let BasicValueEnum::StructValue(payload_struct) = payload_value else { + return Err(CodegenError::UnsupportedExpression("enum_match")); + }; + for binding in &arm.struct_bindings { + let field_value = cg + .builder + .build_extract_value( + payload_struct, + binding.field_index as u32, + &format!("enum_match_{}", binding.name), + ) + .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; + let slot = allocate_local_slot(cg, &binding.name, binding.ty)?; + let stored = coerce_basic_value_for_slot(cg, field_value, binding.ty)?; + cg.builder + .build_store(slot.ptr, stored) + .map_err(|_| CodegenError::UnsupportedExpression("enum_match"))?; + cg.insert_local(binding.name.clone(), slot); } } let arm_value = lower_expr(cg, &arm.body)?; @@ -1445,7 +1440,9 @@ fn lower_force_unwrap<'ctx, 'm>( .map_err(|_| CodegenError::UnsupportedExpression("force_unwrap"))?; let some_block = cg.context.append_basic_block(function, "force_unwrap_some"); let null_block = cg.context.append_basic_block(function, "force_unwrap_null"); - let merge_block = cg.context.append_basic_block(function, "force_unwrap_merge"); + let merge_block = cg + .context + .append_basic_block(function, "force_unwrap_merge"); cg.builder .build_conditional_branch(ok, some_block, null_block) .map_err(|_| CodegenError::UnsupportedExpression("force_unwrap"))?; @@ -1985,25 +1982,24 @@ fn lower_call<'ctx, 'm>( let mut lowered_args = Vec::with_capacity(args.len()); for (idx, arg) in args.iter().enumerate() { let mut lowered = lower_expr(cg, arg)?; - if let Some(param_ty) = param_tys.get(idx) { - if let (BasicValueEnum::IntValue(int_val), BasicTypeEnum::IntType(target_int_ty)) = + if let Some(param_ty) = param_tys.get(idx) + && let (BasicValueEnum::IntValue(int_val), BasicTypeEnum::IntType(target_int_ty)) = (lowered, *param_ty) - { - let from_w = int_val.get_type().get_bit_width(); - let to_w = target_int_ty.get_bit_width(); - if from_w != to_w { - lowered = if from_w > to_w { - cg.builder - .build_int_truncate(int_val, target_int_ty, "arg_trunc") - .map_err(|_| CodegenError::UnsupportedExpression("call"))? - .as_basic_value_enum() - } else { - cg.builder - .build_int_z_extend(int_val, target_int_ty, "arg_zext") - .map_err(|_| CodegenError::UnsupportedExpression("call"))? - .as_basic_value_enum() - }; - } + { + let from_w = int_val.get_type().get_bit_width(); + let to_w = target_int_ty.get_bit_width(); + if from_w != to_w { + lowered = if from_w > to_w { + cg.builder + .build_int_truncate(int_val, target_int_ty, "arg_trunc") + .map_err(|_| CodegenError::UnsupportedExpression("call"))? + .as_basic_value_enum() + } else { + cg.builder + .build_int_z_extend(int_val, target_int_ty, "arg_zext") + .map_err(|_| CodegenError::UnsupportedExpression("call"))? + .as_basic_value_enum() + }; } } lowered_args.push(BasicMetadataValueEnum::from(lowered)); diff --git a/crates/aura-codegen/src/llvm/module.rs b/crates/aura-codegen/src/llvm/module.rs index de4f430..89e76f3 100644 --- a/crates/aura-codegen/src/llvm/module.rs +++ b/crates/aura-codegen/src/llvm/module.rs @@ -29,6 +29,27 @@ use super::{ function::{declare_function, declare_function_with_name, declare_global_stub}, }; +/// Value functions that still mention `Ty::GenericParam` in their lowered function type are +/// generic templates; LLVM lowering uses concrete monomorphizations (emitted under mangled +/// names) instead of the template itself. +#[cfg(feature = "llvm-backend")] +fn is_generic_value_template_decl( + types: &aura_typecheck::TyInterner, + decl: &aura_typecheck::checked_ir::CheckedDecl, +) -> bool { + if decl.is_extern { + return false; + } + let Some(ty) = types.get(decl.ty) else { + return false; + }; + let Ty::Func { params, ret } = ty else { + return false; + }; + let mentions_generic = |tid: TyId| matches!(types.get(tid), Some(Ty::GenericParam(_))); + params.iter().any(|p| mentions_generic(p.ty)) || mentions_generic(*ret) +} + #[cfg(feature = "llvm-backend")] fn bind_function_params<'ctx, 'm>( cg: &CodegenContext<'ctx, 'm>, @@ -93,9 +114,7 @@ fn ensure_native_main_stub<'ctx, 'm>(cg: &CodegenContext<'ctx, 'm>) -> Result<() } #[cfg(feature = "llvm-backend")] -fn find_main_decl<'m>( - checked: &'m CheckedModule, -) -> Option<&'m aura_typecheck::checked_ir::CheckedDecl> { +fn find_main_decl(checked: &CheckedModule) -> Option<&aura_typecheck::checked_ir::CheckedDecl> { checked.ir.declarations.iter().find(|d| d.name == "main") } @@ -167,7 +186,10 @@ pub fn emit_module_stub( .iter() .filter_map(|decl| { let ty = checked.types.get(decl.ty)?; - if matches!(ty, Ty::Func { .. }) && !decl.is_extern { + if matches!(ty, Ty::Func { .. }) + && !decl.is_extern + && !is_generic_value_template_decl(&checked.types, decl) + { return Some(decl.link_name.clone()); } None @@ -183,7 +205,9 @@ pub fn emit_module_stub( }; if matches!(ty, Ty::Func { .. }) { - declare_function(&cg, decl)?; + if !is_generic_value_template_decl(&checked.types, decl) { + declare_function(&cg, decl)?; + } } else { declare_global_stub(&cg, decl)?; } @@ -209,6 +233,15 @@ pub fn emit_module_stub( let lowered = lower_expr(&cg, &decl.value)?; if function.get_type().get_return_type().is_some() { + let Ty::Func { ret, .. } = cg + .checked + .types + .get(decl.ty) + .ok_or(CodegenError::InvalidTypeId(decl.ty.0))? + else { + return Err(CodegenError::InvalidFunctionType(decl.name.clone())); + }; + let lowered = super::expr::coerce_basic_value_for_slot(&cg, lowered, *ret)?; cg.builder.build_return(Some(&lowered)).map_err(|_| { CodegenError::UnsupportedExpression(classify_expr_kind(&decl.value)) })?; @@ -252,7 +285,10 @@ pub fn emit_object_file_with_options( .iter() .filter_map(|decl| { let ty = checked.types.get(decl.ty)?; - if matches!(ty, Ty::Func { .. }) && !decl.is_extern { + if matches!(ty, Ty::Func { .. }) + && !decl.is_extern + && !is_generic_value_template_decl(&checked.types, decl) + { return Some(decl.link_name.clone()); } None @@ -268,7 +304,9 @@ pub fn emit_object_file_with_options( }; if matches!(ty, Ty::Func { .. }) { - declare_function(&cg, decl)?; + if !is_generic_value_template_decl(&checked.types, decl) { + declare_function(&cg, decl)?; + } } else { declare_global_stub(&cg, decl)?; } @@ -294,6 +332,15 @@ pub fn emit_object_file_with_options( let lowered = lower_expr(&cg, &decl.value)?; if function.get_type().get_return_type().is_some() { + let Ty::Func { ret, .. } = cg + .checked + .types + .get(decl.ty) + .ok_or(CodegenError::InvalidTypeId(decl.ty.0))? + else { + return Err(CodegenError::InvalidFunctionType(decl.name.clone())); + }; + let lowered = super::expr::coerce_basic_value_for_slot(&cg, lowered, *ret)?; cg.builder.build_return(Some(&lowered)).map_err(|_| { CodegenError::UnsupportedExpression(classify_expr_kind(&decl.value)) })?; diff --git a/crates/aura-codegen/src/llvm/types.rs b/crates/aura-codegen/src/llvm/types.rs index 0e02379..70d0431 100644 --- a/crates/aura-codegen/src/llvm/types.rs +++ b/crates/aura-codegen/src/llvm/types.rs @@ -242,7 +242,7 @@ mod llvm_lowering { .unwrap_or(1); let payload_field = payload_storage_type(context, max_payload_size, max_payload_align)?; Ok(context - .struct_type(&[context.i32_type().into(), payload_field.into()], false) + .struct_type(&[context.i32_type().into(), payload_field], false) .as_basic_type_enum()) } @@ -306,8 +306,13 @@ mod llvm_lowering { }) } - fn type_contains_managed_pointers(types: &TyInterner, ty_id: TyId) -> Result { - let ty = types.get(ty_id).ok_or(CodegenError::InvalidTypeId(ty_id.0))?; + fn type_contains_managed_pointers( + types: &TyInterner, + ty_id: TyId, + ) -> Result { + let ty = types + .get(ty_id) + .ok_or(CodegenError::InvalidTypeId(ty_id.0))?; Ok(match ty { Ty::Bool | Ty::Int8 @@ -497,6 +502,9 @@ mod tests { let ref_ty = types.intern(Ty::Ref(int_ty)); assert_eq!(type_trace_kind(&types, int_ty).expect("trace kind"), 0); assert_eq!(type_trace_kind(&types, ref_ty).expect("trace kind"), 1); - assert_ne!(type_layout_id(&types, int_ty), type_layout_id(&types, ref_ty)); + assert_ne!( + type_layout_id(&types, int_ty), + type_layout_id(&types, ref_ty) + ); } } diff --git a/crates/aura-codegen/src/project/compile.rs b/crates/aura-codegen/src/project/compile.rs index fb54fd7..27407d4 100644 --- a/crates/aura-codegen/src/project/compile.rs +++ b/crates/aura-codegen/src/project/compile.rs @@ -11,8 +11,7 @@ use aura_typecheck::checked_ir::{CheckedDecl, CheckedExpr}; use aura_typecheck::types::{FuncParam, Ty, TyInterner}; use aura_typecheck::{ CheckContext, CheckOptions, CheckedModule, ImportBinding, MethodImportBinding, - TypeImportBinding, - check_module_with_context, + TypeImportBinding, check_module_with_context, }; use super::discover::find_project_root; @@ -372,9 +371,9 @@ impl ProjectCompiler { ty: ty.clone(), }, }; - if matches!(ty, TypeRef::Macro { .. }) { - macro_exports.push(binding); - } else if matches!(ty, TypeRef::Func { .. }) && decl.is_extern { + if matches!(ty, TypeRef::Macro { .. }) + || (matches!(ty, TypeRef::Func { .. }) && decl.is_extern) + { macro_exports.push(binding); } else { value_exports.push(binding); @@ -596,7 +595,9 @@ impl ProjectCompiler { }); found = true; } - context.imported_methods.extend(method_exports.iter().cloned()); + context + .imported_methods + .extend(method_exports.iter().cloned()); if !found { return Err(ProjectCompileError::Resolve { path: Some(importer_path.to_path_buf()), @@ -1505,10 +1506,7 @@ mod tests { &dependency_root.join("src").join("io.aura"), "def answer() -> Int { 42 }", ); - create_file( - &dependency_root.join("src").join("lib.aura"), - "def x = 1;", - ); + create_file(&dependency_root.join("src").join("lib.aura"), "def x = 1;"); create_file( &root.join("project.auon"), diff --git a/crates/aura-frontend/src/ast.rs b/crates/aura-frontend/src/ast.rs index 88c26b3..dc8f753 100644 --- a/crates/aura-frontend/src/ast.rs +++ b/crates/aura-frontend/src/ast.rs @@ -94,7 +94,10 @@ pub struct Param { #[derive(Debug, Clone, PartialEq, Eq)] pub enum TypeExpr { - Named { name: String, args: Vec }, + Named { + name: String, + args: Vec, + }, /// `interface(name: T, ...)` — empty `interface()` is the top type (see stdlib `Any`). Interface(Vec<(String, TypeExpr)>), Tuple(Vec), diff --git a/crates/aura-frontend/src/parser.rs b/crates/aura-frontend/src/parser.rs index 776770a..07c789d 100644 --- a/crates/aura-frontend/src/parser.rs +++ b/crates/aura-frontend/src/parser.rs @@ -1263,11 +1263,7 @@ where fn parse_catch_expr(&mut self) -> Result { let mark = self.mark(); self.expect_ident_exact("catch")?; - self.expect_simple( - &TokenKind::LParen, - "expected '(' after catch", - vec!["("], - )?; + self.expect_simple(&TokenKind::LParen, "expected '(' after catch", vec!["("])?; let expr = self.parse_expr()?; self.expect_simple( &TokenKind::RParen, @@ -3365,7 +3361,8 @@ mod tests { #[test] fn reject_malformed_interface_member_syntax() { let src = "def Broken = interface(read Func[(), String])"; - let err = Parser::parse_source(src).expect_err("interface member syntax should require ':'"); + let err = + Parser::parse_source(src).expect_err("interface member syntax should require ':'"); assert!(err.message.contains("expected")); } diff --git a/crates/aura-runtime-host/src/lib.rs b/crates/aura-runtime-host/src/lib.rs index 97aee9c..6e6da64 100644 --- a/crates/aura-runtime-host/src/lib.rs +++ b/crates/aura-runtime-host/src/lib.rs @@ -115,7 +115,11 @@ pub struct AuraRawAlloc { len: usize, elem_size: usize, elem_align: usize, + /// Reserved for layout / tracing metadata aligned with generated allocations. + #[allow(dead_code)] layout_id: usize, + /// Reserved for layout / tracing metadata aligned with generated allocations. + #[allow(dead_code)] trace_kind: usize, storage: Vec, } @@ -474,11 +478,18 @@ pub extern "C" fn aura_catch_end() -> i32 { PANIC_ACTIVE.with(|active| { let was_active = active.get(); active.set(false); - if was_active { 1 } else { 0 } + if was_active { + 1 + } else { + 0 + } }) } #[unsafe(no_mangle)] +/// # Safety +/// `_message` is currently unused; callers must pass null or a pointer that remains valid for the +/// duration of the call in case a future implementation reads it. pub unsafe extern "C" fn aura_panic_set_hook(_message: *const u8) { PANIC_HOOK_ENABLED.with(|enabled| enabled.set(true)); } @@ -518,9 +529,9 @@ pub extern "C" fn syscall_exit(code: i32) -> ! { mod tests { use super::{ aura_catch_begin, aura_catch_end, bytes_get, bytes_new, bytes_set, gc_register_root, - gc_safepoint, gc_unregister_root, lock_gc_roots, raw_alloc_len, raw_alloc_new, raw_alloc_ref, - raw_alloc_ref_alloc, raw_alloc_slice, ref_get, ref_set, slice_get, slice_ref_at, slice_set, - string_into, syscall_write, AuraBytes, + gc_safepoint, gc_unregister_root, lock_gc_roots, raw_alloc_len, raw_alloc_new, + raw_alloc_ref, raw_alloc_ref_alloc, raw_alloc_slice, ref_get, ref_set, slice_get, + slice_ref_at, slice_set, string_into, syscall_write, AuraBytes, }; #[test] diff --git a/crates/aura-typecheck/src/checker.rs b/crates/aura-typecheck/src/checker.rs index 3c18547..d90361a 100644 --- a/crates/aura-typecheck/src/checker.rs +++ b/crates/aura-typecheck/src/checker.rs @@ -888,7 +888,8 @@ impl TypeChecker { field: &str, ) -> Option<(usize, Vec, TyId)> { let interface_ty = self.unifier.resolve(interface_ty); - let Some(Ty::Interface(members) | Ty::InterfaceObject(members)) = self.interner.get(interface_ty) + let Some(Ty::Interface(members) | Ty::InterfaceObject(members)) = + self.interner.get(interface_ty) else { return None; }; @@ -911,9 +912,7 @@ impl TypeChecker { trailing: &[LabeledClosureArg], expected: Option, ) -> Option { - let Some((_, params, ret)) = self.interface_method_sig(receiver_ty, field) else { - return None; - }; + let (_, params, ret) = self.interface_method_sig(receiver_ty, field)?; if !trailing.is_empty() { self.diagnostics.push( self.typecheck_error( @@ -998,12 +997,15 @@ impl TypeChecker { | (Some(Ty::Slice(p)), Some(Ty::Slice(a))) | (Some(Ty::Ref(p)), Some(Ty::Ref(a))) => self.match_receiver_ty(*p, *a, subst), (Some(Ty::Dict { key: pk, value: pv }), Some(Ty::Dict { key: ak, value: av })) => { - self.match_receiver_ty(*pk, *ak, subst) - && self.match_receiver_ty(*pv, *av, subst) - } - (Some(Ty::Array { item: pi, size: ps }), Some(Ty::Array { item: ai, size: as_ })) => { - ps == as_ && self.match_receiver_ty(*pi, *ai, subst) + self.match_receiver_ty(*pk, *ak, subst) && self.match_receiver_ty(*pv, *av, subst) } + ( + Some(Ty::Array { item: pi, size: ps }), + Some(Ty::Array { + item: ai, + size: as_, + }), + ) => ps == as_ && self.match_receiver_ty(*pi, *ai, subst), (Some(Ty::Tuple(p)), Some(Ty::Tuple(a))) => { p.len() == a.len() && p.iter() @@ -1906,7 +1908,8 @@ impl TypeChecker { }); let previous_match_subject = self.current_match_subject.clone(); if let Some(first_param) = function.params.first() { - let subject_ty = receiver_ty.unwrap_or_else(|| self.resolve_type_expr(&first_param.ty)); + let subject_ty = + receiver_ty.unwrap_or_else(|| self.resolve_type_expr(&first_param.ty)); self.current_match_subject = Some(MatchSubject { name: first_param.name.clone(), ty: subject_ty, @@ -2299,9 +2302,13 @@ impl TypeChecker { if let Expr::Member { object, field } = TypeChecker::base_expr(callee.as_ref()) { if let Some(receiver_ty) = self.resolve_type_receiver_expr(object) { let receiver_ty = self.unifier.resolve(receiver_ty); - if let Some(ret) = - self.infer_interface_member_call(receiver_ty, field, args, trailing, None) - { + if let Some(ret) = self.infer_interface_member_call( + receiver_ty, + field, + args, + trailing, + None, + ) { return ret; } if let Some(method) = self.lookup_method(receiver_ty, field) { @@ -2713,7 +2720,9 @@ impl TypeChecker { Some(Ty::Never) ); } - if rest_diverges && matches!(Self::base_expr(last), Expr::Tuple(items) if items.is_empty()) { + if rest_diverges + && matches!(Self::base_expr(last), Expr::Tuple(items) if items.is_empty()) + { self.interner.intern(Ty::Never) } else { self.infer_expr_with_expected(last, expected) @@ -2818,7 +2827,11 @@ impl TypeChecker { trailing, Some(expected), ) { - self.require_assignable(expected, actual, "bidirectional expected type"); + self.require_assignable( + expected, + actual, + "bidirectional expected type", + ); return actual; } if let Some(method) = self.lookup_method(receiver_ty, field) { @@ -2832,7 +2845,11 @@ impl TypeChecker { trailing, Some(expected), ); - self.require_assignable(expected, actual, "bidirectional expected type"); + self.require_assignable( + expected, + actual, + "bidirectional expected type", + ); return actual; } } @@ -4354,12 +4371,13 @@ impl TypeChecker { name: iface_name, .. } = interface { - self.pending_constraints.push(TypeConstraint::InterfaceExists { - interface: iface_name.clone(), - context: format!("generic call `{name}` for `{}`", param.name), - obligations: self.obligation_stack.clone(), - span: self.current_expr_span, - }); + self.pending_constraints + .push(TypeConstraint::InterfaceExists { + interface: iface_name.clone(), + context: format!("generic call `{name}` for `{}`", param.name), + obligations: self.obligation_stack.clone(), + span: self.current_expr_span, + }); } self.pending_constraints .push(TypeConstraint::InterfaceBound { @@ -4389,6 +4407,77 @@ impl TypeChecker { self.substitute_ty_id(callee_ty, &subst) } + /// Substitution map for a generic call with explicit static arguments (one type per generic + /// parameter). Returns `None` when static arguments are partial, use value static args, or + /// the callee is not generic. + fn substitution_for_explicit_generic_call( + &mut self, + function_name: &str, + static_args: &[StaticArg], + ) -> Option> { + let generic_params = self.function_generics.get(function_name)?.clone(); + if static_args.is_empty() || static_args.len() != generic_params.len() { + return None; + } + let mut subst = HashMap::new(); + for (idx, param) in generic_params.iter().enumerate() { + let mapped = self.resolve_static_arg_type(static_args.get(idx)?)?; + subst.insert(param.name.clone(), mapped); + } + Some(subst) + } + + fn mangle_static_args_for_specialization(static_args: &[StaticArg]) -> String { + static_args + .iter() + .map(Self::mangle_one_static_arg) + .collect::>() + .join("_") + } + + fn mangle_one_static_arg(arg: &StaticArg) -> String { + match arg { + StaticArg::Type(ty) => Self::mangle_type_expr_for_specialization(ty), + StaticArg::Value(v) => format!("{v:?}"), + } + } + + fn mangle_type_expr_for_specialization(ty: &TypeExpr) -> String { + match ty { + TypeExpr::Named { name, args } => { + if args.is_empty() { + name.clone() + } else { + format!( + "{}_{}", + name, + args.iter() + .map(Self::mangle_one_static_arg) + .collect::>() + .join("_") + ) + } + } + TypeExpr::Interface(_) => "interface".to_string(), + TypeExpr::Tuple(items) => format!( + "tuple_{}", + items + .iter() + .map(Self::mangle_type_expr_for_specialization) + .collect::>() + .join("_") + ), + TypeExpr::Struct(_) => "struct".to_string(), + TypeExpr::Static(inner) => { + format!( + "static_{}", + Self::mangle_type_expr_for_specialization(inner) + ) + } + TypeExpr::InferHole => "infer".to_string(), + } + } + fn substitute_ty_id(&mut self, ty_id: TyId, subst: &HashMap) -> TyId { let resolved = self.unifier.resolve(ty_id); let Some(ty) = self.interner.get(resolved).cloned() else { @@ -4893,9 +4982,7 @@ impl TypeChecker { interface_expr: &TypeExpr, ) -> Option { let subject_ty = self.unifier.resolve(subject_ty); - let Some(subject) = self.interner.get(subject_ty).cloned() else { - return None; - }; + let subject = self.interner.get(subject_ty).cloned()?; if let TypeExpr::Named { name, .. } = interface_expr { if self.interface_name_exists(name) { @@ -4913,9 +5000,7 @@ impl TypeChecker { let interface_ty = self.resolve_type_expr(interface_expr); let interface_ty = self.unifier.resolve(interface_ty); - let Some(interface) = self.interner.get(interface_ty).cloned() else { - return None; - }; + let interface = self.interner.get(interface_ty).cloned()?; let Ty::Interface(required_methods) = interface else { return Some(InterfaceCheckFailure::MissingMethod { interface: self.interface_label(interface_expr), @@ -4949,9 +5034,7 @@ impl TypeChecker { ) -> Option { let subject_ty = self.unifier.resolve(subject_ty); let interface_ty = self.unifier.resolve(interface_ty); - let Some(interface) = self.interner.get(interface_ty).cloned() else { - return None; - }; + let interface = self.interner.get(interface_ty).cloned()?; let Ty::Interface(required_methods) = interface else { return None; }; @@ -5231,18 +5314,12 @@ impl TypeChecker { .all(|(x, y)| self.structurally_equivalent(x.ty, y.ty)) && self.structurally_equivalent(*ar, *br) } - (Ty::Interface(a), Ty::Interface(b)) => { - self.interface_member_sets_equivalent(a, b) - } + (Ty::Interface(a), Ty::Interface(b)) => self.interface_member_sets_equivalent(a, b), _ => false, } } - fn interface_member_sets_equivalent( - &self, - a: &[(String, TyId)], - b: &[(String, TyId)], - ) -> bool { + fn interface_member_sets_equivalent(&self, a: &[(String, TyId)], b: &[(String, TyId)]) -> bool { if a.len() != b.len() { return false; } @@ -5722,7 +5799,12 @@ impl TypeChecker { .map(|(k, v)| (self.lower_expr(k), self.lower_expr(v))) .collect(), ), - Expr::Call { callee, args, .. } => { + Expr::Call { + callee, + static_args, + args, + .. + } => { if let Expr::Ident(name) = TypeChecker::base_expr(callee.as_ref()) { if name == "if" { let condition = args @@ -5866,6 +5948,48 @@ impl TypeChecker { return lowered; } } + if let Expr::Ident(fname) = TypeChecker::base_expr(callee.as_ref()) { + if let Some(subst) = + self.substitution_for_explicit_generic_call(fname, static_args) + { + let mangled = format!( + "{}__{}", + fname, + Self::mangle_static_args_for_specialization(static_args) + ); + if !self.ir.declarations.iter().any(|d| d.name == mangled) { + if let Some(template) = self + .ir + .declarations + .iter() + .find(|d| d.name == *fname && !d.is_extern) + .cloned() + { + // Explicit specialization: substitute the declaration function type, + // then reuse the template's already-lowered `value`. Bodies that still embed + // callee-specific type IDs or nested generic static parameters are not + // re-walked for substitution here; that remains follow-up work if we need + // casts, inner generic calls, or interface plumbing under the mangled symbol. + let new_ty = self.substitute_ty_id(template.ty, &subst); + self.ir.declarations.push(CheckedDecl { + name: mangled.clone(), + link_name: mangled.clone(), + params: template.params.clone(), + ty: new_ty, + is_extern: false, + value: template.value.clone(), + }); + } + } + let lowered_args: Vec<_> = + args.iter().map(|a| self.lower_expr(a)).collect(); + let call = CheckedExpr::Call { + callee: Box::new(CheckedExpr::Ident(mangled)), + args: lowered_args, + }; + return self.wrap_call_with_gc_safepoint(call); + } + } let call = CheckedExpr::Call { callee: Box::new(self.lower_expr(callee)), args: args.iter().map(|a| self.lower_expr(a)).collect(), @@ -6642,9 +6766,9 @@ mod tests { CheckedExpr::Call { callee, args } => { contains_gc_safepoint(callee) || args.iter().any(contains_gc_safepoint) } - CheckedExpr::EnumCtor { payload, .. } | CheckedExpr::DotIdent { payload, .. } => payload - .as_deref() - .is_some_and(contains_gc_safepoint), + CheckedExpr::EnumCtor { payload, .. } | CheckedExpr::DotIdent { payload, .. } => { + payload.as_deref().is_some_and(contains_gc_safepoint) + } CheckedExpr::FieldAccess { object, .. } => contains_gc_safepoint(object), CheckedExpr::ForceUnwrap { expr, .. } => contains_gc_safepoint(expr), CheckedExpr::AssignField { object, value, .. } => { @@ -6659,9 +6783,7 @@ mod tests { } => { contains_gc_safepoint(scrutinee) || arms.iter().any(|arm| contains_gc_safepoint(&arm.body)) - || default_arm - .as_deref() - .is_some_and(contains_gc_safepoint) + || default_arm.as_deref().is_some_and(contains_gc_safepoint) } CheckedExpr::If { condition, @@ -6671,36 +6793,34 @@ mod tests { } => { contains_gc_safepoint(condition) || contains_gc_safepoint(then_branch) - || else_branch - .as_deref() - .is_some_and(contains_gc_safepoint) + || else_branch.as_deref().is_some_and(contains_gc_safepoint) } CheckedExpr::Cases { arms, .. } => arms .iter() .any(|arm| contains_gc_safepoint(&arm.guard) || contains_gc_safepoint(&arm.body)), - CheckedExpr::Loop { condition, body, .. } => { - condition - .as_deref() - .is_some_and(contains_gc_safepoint) + CheckedExpr::Loop { + condition, body, .. + } => { + condition.as_deref().is_some_and(contains_gc_safepoint) || contains_gc_safepoint(body) } CheckedExpr::Return { value, .. } => contains_gc_safepoint(value), - CheckedExpr::Break { value, .. } => value - .as_deref() - .is_some_and(contains_gc_safepoint), + CheckedExpr::Break { value, .. } => value.as_deref().is_some_and(contains_gc_safepoint), CheckedExpr::Coerce { expr, .. } | CheckedExpr::Cast { expr, .. } => { contains_gc_safepoint(expr) } CheckedExpr::Tuple(items) | CheckedExpr::List(items) => { items.iter().any(contains_gc_safepoint) } - CheckedExpr::Struct(fields) => fields.iter().any(|(_, item)| contains_gc_safepoint(item)), + CheckedExpr::Struct(fields) => { + fields.iter().any(|(_, item)| contains_gc_safepoint(item)) + } CheckedExpr::Dict(entries) => entries .iter() .any(|(k, v)| contains_gc_safepoint(k) || contains_gc_safepoint(v)), - CheckedExpr::LocalBind { bindings, .. } => { - bindings.iter().any(|binding| contains_gc_safepoint(&binding.value)) - } + CheckedExpr::LocalBind { bindings, .. } => bindings + .iter() + .any(|binding| contains_gc_safepoint(&binding.value)), CheckedExpr::AssignLocal { value, .. } => contains_gc_safepoint(value), CheckedExpr::MemoryOp { args, .. } => args.iter().any(contains_gc_safepoint), CheckedExpr::MacroApply { operand, .. } => contains_gc_safepoint(operand), @@ -7377,7 +7497,10 @@ mod tests { let module = checked.module.expect("module should exist"); let x_ty = module.value_types.get("x").expect("x should exist"); assert!(matches!(module.types.get(*x_ty), Some(Ty::Never))); - assert!(matches!(module.ir.declarations[0].value, CheckedExpr::Panic { .. })); + assert!(matches!( + module.ir.declarations[0].value, + CheckedExpr::Panic { .. } + )); } #[test] @@ -9251,6 +9374,68 @@ mod tests { assert!(matches!(module.types.get(*y_ty), Some(Ty::Int32))); } + #[test] + fn explicit_generic_specialization_registers_mangled_ir_decl() { + let program = Program { + declarations: vec![ + Decl::Function(FunctionDecl { + doc: None, + static_params: vec![ty_param("T"), ty_param("U")], + receiver: None, + name: "first".to_string(), + params: vec![aura_frontend::ast::Param { + name: "x".to_string(), + ty: TypeExpr::Named { + name: "T".to_string(), + args: Vec::new(), + }, + }], + return_type: TypeExpr::Named { + name: "T".to_string(), + args: Vec::new(), + }, + body: Expr::Block(vec![Expr::Ident("x".to_string())]), + }), + Decl::Assign { + static_params: Vec::new(), + doc: None, + name: "y".to_string(), + value: Expr::Call { + callee: Box::new(Expr::Ident("first".to_string())), + static_args: vec![ + StaticArg::Type(TypeExpr::InferHole), + StaticArg::Type(TypeExpr::Named { + name: "Int".to_string(), + args: Vec::new(), + }), + ], + args: vec![Expr::Int("1".to_string())], + + trailing: Vec::new(), + }, + }, + ], + }; + + let checked = check_module(&program); + assert!(checked.module.is_some()); + let module = checked.module.expect("module should exist"); + assert!( + module + .ir + .declarations + .iter() + .any(|d| d.name == "first__infer_Int"), + "expected mangled specialization decl, have: {:?}", + module + .ir + .declarations + .iter() + .map(|d| d.name.as_str()) + .collect::>() + ); + } + #[test] fn interface_bound_failure_reports_diagnostic() { let program = Program { @@ -9324,12 +9509,10 @@ mod tests { "expected module, diagnostics: {:?}", checked.diagnostics ); - assert!( - checked - .diagnostics - .iter() - .all(|d| d.severity != aura_diagnostics::Severity::Error) - ); + assert!(checked + .diagnostics + .iter() + .all(|d| d.severity != aura_diagnostics::Severity::Error)); } #[test] @@ -9388,7 +9571,11 @@ mod tests { let parsed = Parser::parse_source(src).expect("parse should succeed"); let checked = check_module(&parsed); let module = checked.module.expect("module should exist"); - let x_ty = module.value_types.get("x").copied().expect("x should be typed"); + let x_ty = module + .value_types + .get("x") + .copied() + .expect("x should be typed"); assert!(matches!( module.types.get(x_ty), Some(Ty::Interface(m)) if m.is_empty() diff --git a/crates/aura-typecheck/src/types.rs b/crates/aura-typecheck/src/types.rs index 4b65920..696e8e3 100644 --- a/crates/aura-typecheck/src/types.rs +++ b/crates/aura-typecheck/src/types.rs @@ -61,11 +61,23 @@ pub enum Ty { Slice(TyId), Ref(TyId), List(TyId), - Dict { key: TyId, value: TyId }, + Dict { + key: TyId, + value: TyId, + }, Set(TyId), - Array { item: TyId, size: u64 }, - Func { params: Vec, ret: TyId }, - Macro { params: Vec, ret: TyId }, + Array { + item: TyId, + size: u64, + }, + Func { + params: Vec, + ret: TyId, + }, + Macro { + params: Vec, + ret: TyId, + }, Tuple(Vec), Struct(Vec<(String, TyId)>), Union(Vec), diff --git a/crates/aura-typecheck/src/unify.rs b/crates/aura-typecheck/src/unify.rs index 0ab8cd3..799e6e7 100644 --- a/crates/aura-typecheck/src/unify.rs +++ b/crates/aura-typecheck/src/unify.rs @@ -47,7 +47,9 @@ impl Unifier { return Err(Box::new( Diagnostic::error(Issue::UnifyMismatch) .with_related("interface member count differs", None) - .with_hint("use interfaces with the same method set for this constraint"), + .with_hint( + "use interfaces with the same method set for this constraint", + ), )); } let names_a: HashSet<_> = a.iter().map(|(n, _)| n).collect(); @@ -65,8 +67,13 @@ impl Unifier { let Some(ty_b) = map_b.get(&name).copied() else { return Err(Box::new( Diagnostic::error(Issue::UnifyMismatch) - .with_related("interface member name missing on right-hand side", None) - .with_hint("interfaces unify by member name, not declaration order"), + .with_related( + "interface member name missing on right-hand side", + None, + ) + .with_hint( + "interfaces unify by member name, not declaration order", + ), )); }; let ty = self.unify(interner, ty_a, ty_b, _context)?; diff --git a/crates/aura-typecheck/tests/ir_contract_snapshot.rs b/crates/aura-typecheck/tests/ir_contract_snapshot.rs index a54da63..5d8e32d 100644 --- a/crates/aura-typecheck/tests/ir_contract_snapshot.rs +++ b/crates/aura-typecheck/tests/ir_contract_snapshot.rs @@ -658,9 +658,5 @@ fn interface_values_lower_with_runtime_object_and_dynamic_dispatch_nodes() { .iter() .find(|decl| decl.name == "call") .expect("call function should exist"); - assert!(matches!( - call_decl.value, - CheckedExpr::InterfaceCall { .. } - )); - + assert!(matches!(call_decl.value, CheckedExpr::InterfaceCall { .. })); } diff --git a/docs/.obsidian/workspace.json b/docs/.obsidian/workspace.json index 69eb9ae..4f8b77e 100644 --- a/docs/.obsidian/workspace.json +++ b/docs/.obsidian/workspace.json @@ -182,15 +182,16 @@ }, "active": "120d29afefd7d451", "lastOpenFiles": [ + "Architecture/GitHub Repo Settings.md", + "index.md", + "log.md", + "hot.md", "_meta/taxonomy.md", "_meta", "Language/AUON.md", "Language/Examples Index.md", "Language/Design Overview.md", "Home.md", - "hot.md", - "log.md", - "index.md", "typecheck-ir.md", "Architecture/Testing Strategy.md", "Issues/Panic System.md", diff --git a/docs/Architecture/Build And Dev Workflow.md b/docs/Architecture/Build And Dev Workflow.md index 4884c33..108400a 100644 --- a/docs/Architecture/Build And Dev Workflow.md +++ b/docs/Architecture/Build And Dev Workflow.md @@ -15,8 +15,14 @@ tags: - `cargo xtask dev test` - `cargo xtask dev lint` - `cargo xtask dev fmt` +- `cargo xtask dev fmt-check` — rustfmt in check mode (CI-safe) +- `cargo xtask dev ci` — same checks as GitHub Actions locally (includes docs + LLVM) - `cargo xtask dev qa` +Continuous integration in `.github/workflows/ci.yml` runs in parallel: **fmt-check** and **docs check** on Ubuntu only (same inputs on every OS); **workspace** tests on Ubuntu and Windows with **clippy on Ubuntu only**; **llvm** setup + clippy + tests on both Ubuntu and Windows (toolchains and linking differ by OS). + +GitHub.com branch protection, required checks, and auto-merge are summarized in [[Architecture/GitHub Repo Settings]] (update that note when settings change). + ## LLVM-Sensitive Work Run LLVM-backed checks and CLI builds through `cargo xtask llvm ...` so the managed toolchain is injected consistently. @@ -29,5 +35,6 @@ Run LLVM-backed checks and CLI builds through `cargo xtask llvm ...` so the mana ## Related Notes +- [[Architecture/GitHub Repo Settings]] - [[Subsystems/Xtask]] - [[Generated/Commands Inventory]] diff --git a/docs/Architecture/GitHub Repo Settings.md b/docs/Architecture/GitHub Repo Settings.md new file mode 100644 index 0000000..bbe02cb --- /dev/null +++ b/docs/Architecture/GitHub Repo Settings.md @@ -0,0 +1,68 @@ +--- +title: "GitHub Repo Settings" +kind: architecture +tags: + - aura + - workflow + - ci +--- + +# GitHub Repo Settings + +Snapshot of **GitHub.com** configuration for [`nahharris/aura`](https://github.com/nahharris/aura) (not stored in git). Update this note when you change branch rules, checks, or org policy. + +## Pull requests (repository) + +| Setting | Value | +| --- | --- | +| **Allow auto-merge** | Enabled — PRs can use *Enable auto-merge* once required checks pass. | + +UI: **Settings → General → Pull requests** (or repository **Settings** search for “auto-merge”). + +## Default branch: `master` (branch protection) + +Classic protection rule on **`master`**: + +| Rule | Value | +| --- | --- | +| **Require a pull request before merging** | On — `required_approving_review_count` is **0** (no mandatory reviewers, but merges go through PRs, not direct pushes). | +| **Require status checks to pass before merging** | On, **strict** — branch must be up to date with the base before merge. | +| **Require branches to be up to date** | Implied by strict required checks. | +| **Status checks that are required** | See table below (names must match GitHub Actions job names from workflow `CI`). | +| **Do not allow bypassing the above settings** | On — **Enforce on administrators** (`enforce_admins`). | +| **Allow force pushes** | Off | +| **Allow deletions** | Off | + +UI: **Settings → Branches → Branch protection rules → `master`**. + +### Required GitHub Actions checks (workflow `CI`) + +These job names are registered as required contexts (GitHub Actions app): + +| Check name | +| --- | +| `fmt` | +| `docs` | +| `workspace (ubuntu-latest, true)` | +| `workspace (windows-latest, false)` | +| `llvm (ubuntu-latest)` | +| `llvm (windows-latest)` | + +If `.github/workflows/ci.yml` **renames jobs** or changes the **matrix**, update the branch rule so required contexts still match; otherwise merges stay blocked with “Expected — Waiting for status to be reported”. + +The **llvm** job caches only `toolchains/cache` (the LLVM tarball download), not the extracted `toolchains/llvm` tree, so the `toolchains/llvm/18` symlink is always recreated correctly on each runner. + +### Submodule checkout in CI + +The **CI** workflow checks out **git submodules** so Cargo `path` dependencies under `tool/` resolve. Submodule repositories maintain their **own** CI; Aura does not duplicate their gates beyond what this workspace already builds and tests. See `AGENTS.md` at the repository root (companion surfaces / submodule note) and comments in `.github/workflows/ci.yml`. + +## Changing settings later + +- **Web:** repository **Settings** as above. +- **CLI (authenticated):** e.g. `gh api repos/nahharris/aura/branches/master/protection` (GET) and `PUT` with a JSON body; `gh api repos/nahharris/aura -X PATCH -f allow_auto_merge=true` for the repo flag. + +## Related + +- [[Architecture/Build And Dev Workflow]] +- [[Architecture/Repo Map]] +- [[Subsystems/Xtask]] diff --git a/docs/Architecture/Repo Map.md b/docs/Architecture/Repo Map.md index 589da71..2ab3018 100644 --- a/docs/Architecture/Repo Map.md +++ b/docs/Architecture/Repo Map.md @@ -30,7 +30,7 @@ tags: - Language rules: [[Language/Design Overview]] and [[Language/Syntax And Semantics]] - AUON notation spec: [[Language/AUON]] - Compiler subsystems: [[Subsystems/Frontend]], [[Subsystems/Typecheck]], [[Subsystems/Codegen]] -- Developer workflows: [[Architecture/Build And Dev Workflow]] and [[Architecture/Testing Strategy]] +- Developer workflows: [[Architecture/Build And Dev Workflow]], [[Architecture/GitHub Repo Settings]], and [[Architecture/Testing Strategy]] - Current IR contract: [[Contracts/Typecheck IR]] ## Generated Support diff --git a/docs/Generated/Commands Inventory.md b/docs/Generated/Commands Inventory.md index 02343ca..cc50895 100644 --- a/docs/Generated/Commands Inventory.md +++ b/docs/Generated/Commands Inventory.md @@ -2,7 +2,7 @@ title: Commands Inventory kind: generated generated_by: cargo xtask docs sync -generated_at: 2026-05-03T18:53:52.5470381Z +generated_at: 2026-05-04T01:25:06.708827Z --- # Commands Inventory @@ -16,6 +16,8 @@ generated_at: 2026-05-03T18:53:52.5470381Z | `cargo xtask dev test` | Run the full workspace test suite. | | `cargo xtask dev lint` | Run clippy with warnings denied. | | `cargo xtask dev fmt` | Format the workspace. | +| `cargo xtask dev fmt-check` | Fail if sources are not rustfmt-clean (CI-safe). | +| `cargo xtask dev ci` | Full CI parity: fmt-check, lint, test, docs check, LLVM doctor + clippy + test. | | `cargo xtask dev qa` | Format, lint, and test. | ## LLVM Flow @@ -24,6 +26,7 @@ generated_at: 2026-05-03T18:53:52.5470381Z | --- | --- | | `cargo xtask llvm setup` | Install or validate the managed LLVM toolchain. | | `cargo xtask llvm doctor` | Check the managed LLVM toolchain. | +| `cargo xtask llvm ci` | Doctor, then clippy and tests (toolchain must already be installed). | | `cargo xtask llvm check` | Check `aura-codegen` with the LLVM backend feature. | | `cargo xtask llvm build` | Build `aura-codegen` with the LLVM backend feature. | | `cargo xtask llvm test` | Test `aura-codegen` with the LLVM backend feature. | diff --git a/docs/Generated/Directory Inventory.md b/docs/Generated/Directory Inventory.md index 122d0e8..5f3162e 100644 --- a/docs/Generated/Directory Inventory.md +++ b/docs/Generated/Directory Inventory.md @@ -2,7 +2,7 @@ title: Directory Inventory kind: generated generated_by: cargo xtask docs sync -generated_at: 2026-05-03T18:53:52.5470381Z +generated_at: 2026-05-04T01:25:06.708827Z --- # Directory Inventory @@ -12,6 +12,7 @@ generated_at: 2026-05-03T18:53:52.5470381Z | `.agents` | Repository directory. | | `.cargo` | Cargo aliases and workspace command configuration. | | `.claude` | Repository directory. | +| `.github` | Repository directory. | | `aura-stl` | Aura standard library package. | | `crates` | Rust workspace crates. | | `docs` | Obsidian vault and engineering documentation. | diff --git a/docs/Generated/Examples Inventory.md b/docs/Generated/Examples Inventory.md index 86b34f6..149ed54 100644 --- a/docs/Generated/Examples Inventory.md +++ b/docs/Generated/Examples Inventory.md @@ -2,7 +2,7 @@ title: Examples Inventory kind: generated generated_by: cargo xtask docs sync -generated_at: 2026-05-03T18:53:52.5470381Z +generated_at: 2026-05-04T01:25:06.708827Z --- # Examples Inventory diff --git a/docs/Generated/Test Inventory.md b/docs/Generated/Test Inventory.md index 22942de..78467cf 100644 --- a/docs/Generated/Test Inventory.md +++ b/docs/Generated/Test Inventory.md @@ -2,7 +2,7 @@ title: Test Inventory kind: generated generated_by: cargo xtask docs sync -generated_at: 2026-05-03T18:53:52.5470381Z +generated_at: 2026-05-04T01:25:06.708827Z --- # Test Inventory @@ -12,5 +12,7 @@ generated_at: 2026-05-03T18:53:52.5470381Z | `crates/aura-frontend/tests/diagnostics_snapshot.rs` | frontend | | `crates/aura-typecheck/tests/diagnostics_snapshot.rs` | typecheck | | `crates/aura-typecheck/tests/ir_contract_snapshot.rs` | typecheck | +| `tool/auon-py/tests/test_dom.py` | other | +| `tool/auon-py/tests/test_typed.py` | other | | `tool/auon-rs/tests/api.rs` | other | diff --git a/docs/Generated/Workspace Inventory.md b/docs/Generated/Workspace Inventory.md index 848f6b7..bc32ba3 100644 --- a/docs/Generated/Workspace Inventory.md +++ b/docs/Generated/Workspace Inventory.md @@ -2,7 +2,7 @@ title: Workspace Inventory kind: generated generated_by: cargo xtask docs sync -generated_at: 2026-05-03T18:53:52.5470381Z +generated_at: 2026-05-04T01:25:06.708827Z --- # Workspace Inventory diff --git a/docs/Home.md b/docs/Home.md index bdb0e82..a035859 100644 --- a/docs/Home.md +++ b/docs/Home.md @@ -15,6 +15,7 @@ tags: - [[Architecture/Repo Map]] - [[Architecture/Build And Dev Workflow]] +- [[Architecture/GitHub Repo Settings]] - [[Architecture/Testing Strategy]] - [[Language/Design Overview]] - [[Contracts/Typecheck IR]] diff --git a/docs/Subsystems/Xtask.md b/docs/Subsystems/Xtask.md index 021afd0..1d3da6e 100644 --- a/docs/Subsystems/Xtask.md +++ b/docs/Subsystems/Xtask.md @@ -13,7 +13,7 @@ related_contracts: [] related_notes: - "Architecture/Build And Dev Workflow" -last_reviewed: 2026-04-18 +last_reviewed: 2026-05-05 # Xtask @@ -23,9 +23,17 @@ Centralize automation for the workspace, including dev commands, LLVM toolchain ## Command Families -- `dev` -- `llvm` -- `docs` +- `dev` — includes `fmt-check` and `ci` for CI parity with GitHub Actions +- `llvm` — includes `ci` (doctor + clippy + test) after `llvm setup` +- `docs` — `docs sync` / `docs check` walk the repo for inventories; paths like `.opencode/` are skipped so local agent tooling does not affect generated vault tables or CI. + +## LLVM toolchain + +After extraction, `toolchains/llvm/` is a link to the versioned install. On Unix the link target is **canonical (absolute)** so `bin/llvm-config` resolves correctly (relative targets would be interpreted from the link’s parent directory and could miss the install). + +On Linux, `cargo xtask llvm …` prepends `/lib` to `LD_LIBRARY_PATH` when spawning `cargo` so `llvm-sys` build scripts can execute `llvm-config` and load `libLLVM.so` (matching the CI workflow’s `GITHUB_ENV` step). + +The official x86_64 Linux LLVM 18 prebuilt targets Ubuntu 18.04 and expects **`libtinfo.so.5`**. Ubuntu 24.04+ runners no longer ship that soname via apt; CI installs Ubuntu 22.04’s `libtinfo5` package from the archive (see `.github/workflows/ci.yml`). ## Related diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..05e6ca1 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "stable" +profile = "minimal" +components = ["rustfmt", "clippy"] diff --git a/tool/auon-rs b/tool/auon-rs index e88c65b..824b3b6 160000 --- a/tool/auon-rs +++ b/tool/auon-rs @@ -1 +1 @@ -Subproject commit e88c65bafd6646ed23112b1e68506b4fc6602a5d +Subproject commit 824b3b63bf4c7d5f882c553ac4d20fa916d713d6 diff --git a/xtask/src/docs.rs b/xtask/src/docs.rs index 3180167..2ddcdab 100644 --- a/xtask/src/docs.rs +++ b/xtask/src/docs.rs @@ -262,6 +262,7 @@ fn should_skip_relative_path(path: &str) -> bool { let normalized = path.replace('\\', "/"); let prefixes = [ ".git", + ".opencode", "target", "target2", "toolchains", @@ -525,6 +526,8 @@ fn render_commands_inventory() -> String { | `cargo xtask dev test` | Run the full workspace test suite. | | `cargo xtask dev lint` | Run clippy with warnings denied. | | `cargo xtask dev fmt` | Format the workspace. | +| `cargo xtask dev fmt-check` | Fail if sources are not rustfmt-clean (CI-safe). | +| `cargo xtask dev ci` | Full CI parity: fmt-check, lint, test, docs check, LLVM doctor + clippy + test. | | `cargo xtask dev qa` | Format, lint, and test. | ## LLVM Flow @@ -533,6 +536,7 @@ fn render_commands_inventory() -> String { | --- | --- | | `cargo xtask llvm setup` | Install or validate the managed LLVM toolchain. | | `cargo xtask llvm doctor` | Check the managed LLVM toolchain. | +| `cargo xtask llvm ci` | Doctor, then clippy and tests (toolchain must already be installed). | | `cargo xtask llvm check` | Check `aura-codegen` with the LLVM backend feature. | | `cargo xtask llvm build` | Build `aura-codegen` with the LLVM backend feature. | | `cargo xtask llvm test` | Test `aura-codegen` with the LLVM backend feature. | @@ -1381,6 +1385,10 @@ members = ["crates/aura-cli", "crates/aura-frontend", "xtask"] assert!(should_skip_relative_path( "tool/tree-sitter-aura/node_modules/pkg/index.js" )); + assert!(should_skip_relative_path(".opencode")); + assert!(should_skip_relative_path( + ".opencode/node_modules/pkg/index.js" + )); assert!(!should_skip_relative_path( "crates/aura-frontend/src/lib.rs" )); diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 700b1c6..89491c5 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -50,6 +50,10 @@ enum DevCommands { Test, Lint, Fmt, + /// Fail if the workspace is not rustfmt-clean (does not modify files). + FmtCheck, + /// Full CI parity: fmt check, clippy, tests, docs check, then LLVM doctor + clippy + tests. + Ci, Qa, } @@ -57,6 +61,8 @@ enum DevCommands { enum LlvmCommands { Setup, Doctor, + /// LLVM doctor, clippy, and tests (expects toolchain already installed; use `llvm setup` first on a fresh machine). + Ci, Check, Build, Test, @@ -85,12 +91,15 @@ fn main() -> Result<()> { DevCommands::Test => dev_test(&sh), DevCommands::Lint => dev_lint(&sh), DevCommands::Fmt => dev_fmt(&sh), + DevCommands::FmtCheck => dev_fmt_check(&sh), + DevCommands::Ci => dev_ci(&sh, &root), DevCommands::Qa => dev_qa(&sh), }, Commands::Docs { command } => docs::run(command, &root), Commands::Llvm { command } => match command { LlvmCommands::Setup => llvm_setup(&sh), LlvmCommands::Doctor => llvm_doctor(&sh), + LlvmCommands::Ci => llvm_ci(&sh), LlvmCommands::Check => llvm_check(&sh), LlvmCommands::Build => llvm_build(&sh), LlvmCommands::Test => llvm_test(&sh), @@ -127,6 +136,27 @@ fn dev_fmt(sh: &Shell) -> Result<()> { Ok(()) } +fn dev_fmt_check(sh: &Shell) -> Result<()> { + cmd!(sh, "cargo fmt --all -- --check").run()?; + Ok(()) +} + +fn dev_ci(sh: &Shell, root: &Path) -> Result<()> { + dev_fmt_check(sh)?; + dev_lint(sh)?; + dev_test(sh)?; + docs::run(DocsCommands::Check, root)?; + llvm_ci(sh)?; + Ok(()) +} + +fn llvm_ci(sh: &Shell) -> Result<()> { + llvm_doctor(sh)?; + llvm_clippy(sh)?; + llvm_test(sh)?; + Ok(()) +} + fn dev_qa(sh: &Shell) -> Result<()> { dev_fmt(sh)?; dev_lint(sh)?; @@ -164,36 +194,44 @@ fn llvm_check(sh: &Shell) -> Result<()> { let prefix = llvm_env_prefix()?; ensure_windows_libxml2_stub(&prefix)?; let _guard = sh.push_env("LLVM_SYS_180_PREFIX", &prefix); - cmd!(sh, "cargo check -p aura-codegen --features llvm-backend").run()?; - Ok(()) + with_sh_ld_library_path(sh, &prefix, |sh| { + cmd!(sh, "cargo check -p aura-codegen --features llvm-backend").run()?; + Ok(()) + }) } fn llvm_build(sh: &Shell) -> Result<()> { let prefix = llvm_env_prefix()?; ensure_windows_libxml2_stub(&prefix)?; let _guard = sh.push_env("LLVM_SYS_180_PREFIX", &prefix); - cmd!(sh, "cargo build -p aura-codegen --features llvm-backend").run()?; - Ok(()) + with_sh_ld_library_path(sh, &prefix, |sh| { + cmd!(sh, "cargo build -p aura-codegen --features llvm-backend").run()?; + Ok(()) + }) } fn llvm_test(sh: &Shell) -> Result<()> { let prefix = llvm_env_prefix()?; ensure_windows_libxml2_stub(&prefix)?; let _guard = sh.push_env("LLVM_SYS_180_PREFIX", &prefix); - cmd!(sh, "cargo test -p aura-codegen --features llvm-backend").run()?; - Ok(()) + with_sh_ld_library_path(sh, &prefix, |sh| { + cmd!(sh, "cargo test -p aura-codegen --features llvm-backend").run()?; + Ok(()) + }) } fn llvm_clippy(sh: &Shell) -> Result<()> { let prefix = llvm_env_prefix()?; ensure_windows_libxml2_stub(&prefix)?; let _guard = sh.push_env("LLVM_SYS_180_PREFIX", &prefix); - cmd!( - sh, - "cargo clippy -p aura-codegen --features llvm-backend --all-targets -- -D warnings" - ) - .run()?; - Ok(()) + with_sh_ld_library_path(sh, &prefix, |sh| { + cmd!( + sh, + "cargo clippy -p aura-codegen --features llvm-backend --all-targets -- -D warnings" + ) + .run()?; + Ok(()) + }) } fn llvm_run(args: &[String]) -> Result<()> { @@ -214,6 +252,7 @@ fn llvm_build_runtime_host(prefix: &str) -> Result<()> { let mut cmd = Command::new("cargo"); cmd.arg("build").arg("-p").arg("aura-runtime-host"); cmd.env("LLVM_SYS_180_PREFIX", prefix); + apply_linux_ld_library_path_to_command(&mut cmd, prefix)?; let status = cmd .status() .context("failed to build aura-runtime-host for LLVM flow")?; @@ -243,6 +282,7 @@ fn run_cargo_with_llvm_env(mode: &str, args: &[String], prefix: &str) -> Result< } cmd.args(args); cmd.env("LLVM_SYS_180_PREFIX", prefix); + apply_linux_ld_library_path_to_command(&mut cmd, prefix)?; let status = cmd.status().context("failed to start cargo command")?; if !status.success() { bail!("cargo command failed with status {status}"); @@ -257,6 +297,48 @@ fn llvm_env_prefix() -> Result { prefix_env_value(&paths) } +/// Prepend `/lib` to `LD_LIBRARY_PATH` on Linux so `llvm-config` and `llvm-sys` build +/// scripts can load `libLLVM.so`. CI often sets this via `GITHUB_ENV`, but child `cargo` processes +/// must still see it whenever only `LLVM_SYS_*_PREFIX` is forwarded. +fn linux_llvm_ld_library_path(prefix: &str) -> Result> { + if !cfg!(target_os = "linux") { + return Ok(None); + } + let lib = Path::new(prefix).join("lib"); + let lib = fs::canonicalize(&lib).with_context(|| { + format!( + "failed to canonicalize LLVM lib directory '{}'", + lib.display() + ) + })?; + let lib = lib.to_string_lossy().into_owned(); + Ok(Some(match env::var("LD_LIBRARY_PATH") { + Ok(extra) if !extra.is_empty() => format!("{lib}:{extra}"), + _ => lib, + })) +} + +fn apply_linux_ld_library_path_to_command(cmd: &mut Command, prefix: &str) -> Result<()> { + if let Some(ld) = linux_llvm_ld_library_path(prefix)? { + cmd.env("LD_LIBRARY_PATH", ld); + } + Ok(()) +} + +fn with_sh_ld_library_path( + sh: &Shell, + prefix: &str, + f: impl FnOnce(&Shell) -> Result, +) -> Result { + match linux_llvm_ld_library_path(prefix)? { + Some(ld) => { + let _ld = sh.push_env("LD_LIBRARY_PATH", &ld); + f(sh) + } + None => f(sh), + } +} + fn ensure_windows_libxml2_stub(prefix: &str) -> Result<()> { if !cfg!(windows) { return Ok(()); @@ -530,29 +612,54 @@ fn extract_archive(paths: &LlvmPaths) -> Result<()> { .with_context(|| format!("failed to remove '{}'", paths.install_dir.display()))?; } - let extracted_dir = find_first_subdir(&paths.temp_extract_dir)?; - fs::rename(&extracted_dir, &paths.install_dir).with_context(|| { - format!( - "failed to finalize extracted LLVM directory '{}'", - paths.install_dir.display() - ) - })?; - - fs::remove_dir_all(&paths.temp_extract_dir) - .with_context(|| format!("failed to remove '{}'", paths.temp_extract_dir.display()))?; + let extracted_root = find_llvm_extract_root(&paths.temp_extract_dir)?; + if extracted_root == paths.temp_extract_dir { + fs::rename(&paths.temp_extract_dir, &paths.install_dir).with_context(|| { + format!( + "failed to finalize extracted LLVM directory '{}'", + paths.install_dir.display() + ) + })?; + } else { + fs::rename(&extracted_root, &paths.install_dir).with_context(|| { + format!( + "failed to finalize extracted LLVM directory '{}'", + paths.install_dir.display() + ) + })?; + fs::remove_dir_all(&paths.temp_extract_dir) + .with_context(|| format!("failed to remove '{}'", paths.temp_extract_dir.display()))?; + } Ok(()) } -fn find_first_subdir(root: &Path) -> Result { - let mut dirs = fs::read_dir(root) - .with_context(|| format!("failed to read '{}'", root.display()))? +/// Prefer the directory that actually contains `bin/llvm-config` so we do not pick an unrelated +/// top-level folder when the archive has multiple entries, and support a flat unpack layout. +fn find_llvm_extract_root(extract_root: &Path) -> Result { + if llvm_config_path(extract_root).is_file() { + return Ok(extract_root.to_path_buf()); + } + let mut dirs = fs::read_dir(extract_root) + .with_context(|| format!("failed to read '{}'", extract_root.display()))? .filter_map(|entry| entry.ok().map(|e| e.path())) .filter(|p| p.is_dir()) .collect::>(); dirs.sort(); - dirs.into_iter() - .next() - .context("extracted archive did not contain a top-level directory") + for dir in dirs { + if llvm_config_path(&dir).is_file() { + return Ok(dir); + } + } + let expected = if cfg!(windows) { + "bin/llvm-config.exe" + } else { + "bin/llvm-config" + }; + bail!( + "extracted LLVM archive did not contain '{}' under '{}' or any subdirectory", + expected, + extract_root.display() + ); } fn ensure_major_link(paths: &LlvmPaths) -> Result<()> { @@ -582,7 +689,7 @@ fn ensure_major_link(paths: &LlvmPaths) -> Result<()> { .with_context(|| format!("failed to create '{}'", parent.display()))?; } - create_link(&paths.install_dir, &paths.major_link) + create_link(&canonical_install, &paths.major_link) } fn validate_install(paths: &LlvmPaths) -> Result<()> { @@ -590,6 +697,26 @@ fn validate_install(paths: &LlvmPaths) -> Result<()> { if !config.is_file() { bail!("llvm-config not found at '{}'", config.display()); } + #[cfg(target_os = "linux")] + { + let prefix = prefix_env_value(paths)?; + let mut cmd = Command::new(&config); + cmd.arg("--version"); + if let Some(ld) = linux_llvm_ld_library_path(&prefix)? { + cmd.env("LD_LIBRARY_PATH", &ld); + } + let output = cmd + .output() + .with_context(|| format!("failed to execute '{}'", config.display()))?; + if !output.status.success() { + bail!( + "'{} --version' failed ({}): {}", + config.display(), + output.status, + String::from_utf8_lossy(&output.stderr).trim() + ); + } + } Ok(()) }