From 586e6cc88b9702978273ab5311e871e9b3d9c0a4 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Fri, 12 Jun 2026 21:31:37 +0200 Subject: [PATCH 1/6] Expand analysis pipeline facts --- README.md | 5 +- docs/cir_passes_pipeline.md | 306 +++++++++--- docs/mojo_ir_normalization_pipeline.md | 11 +- examples/cairo/generate.sh | 21 +- examples/common.sh | 58 +++ examples/libpng/generate.sh | 20 +- examples/sqlite/generate.sh | 16 +- examples/zlib/generate.sh | 24 +- mojo_bindgen/analysis/__init__.py | 41 ++ mojo_bindgen/analysis/alias_classification.py | 153 ++++++ mojo_bindgen/analysis/cir_canonicalizer.py | 15 +- mojo_bindgen/analysis/context.py | 73 +++ mojo_bindgen/analysis/dependency_graph.py | 136 ++++++ mojo_bindgen/analysis/orchestrator.py | 57 ++- mojo_bindgen/analysis/record_shape.py | 459 ++++++++++++++++++ mojo_bindgen/analysis/reference_validation.py | 143 ++++++ mojo_bindgen/analysis/struct_lowering.py | 341 ++----------- mojo_bindgen/analysis/unit_lowering.py | 29 +- mojo_bindgen/analysis/validate_ir.py | 10 +- pixi.toml | 2 +- tests/unit/passes/test_pipeline.py | 77 ++- tests/unit/test_analysis_context.py | 169 +++++++ tests/unit/test_analysis_record_shape.py | 168 +++++++ tests/unit/test_analysis_struct_lowering.py | 41 ++ tests/unit/test_cir_canonicalizer.py | 34 ++ 25 files changed, 1965 insertions(+), 444 deletions(-) create mode 100644 examples/common.sh create mode 100644 mojo_bindgen/analysis/alias_classification.py create mode 100644 mojo_bindgen/analysis/context.py create mode 100644 mojo_bindgen/analysis/dependency_graph.py create mode 100644 mojo_bindgen/analysis/record_shape.py create mode 100644 mojo_bindgen/analysis/reference_validation.py create mode 100644 tests/unit/test_analysis_context.py create mode 100644 tests/unit/test_analysis_record_shape.py diff --git a/README.md b/README.md index 61e724d..4bc0ffa 100644 --- a/README.md +++ b/README.md @@ -196,9 +196,12 @@ The repository includes worked examples and smoke programs for: - Cairo: [examples/cairo](examples/cairo) - libpng: [examples/libpng](examples/libpng) - zlib: [examples/zlib](examples/zlib) +- globals/constants: [examples/global_consts](examples/global_consts) These examples do more than generate bindings: their `generate.sh` scripts also -build and run small functional tests to check the usability of the generated bindings. +build smoke artifacts or run small functional tests to check the usability of +the generated bindings. They also emit layout-test sidecars when file output is +generated. The test suite also has end-to-end runtime coverage for: diff --git a/docs/cir_passes_pipeline.md b/docs/cir_passes_pipeline.md index 6547bca..5871ee4 100644 --- a/docs/cir_passes_pipeline.md +++ b/docs/cir_passes_pipeline.md @@ -1,94 +1,286 @@ -# CIR Pass Workflow +# Analysis Pass Pipeline -This document shows the current CIR-to-CIR normalization pipeline and where it -fits between parsing and Mojo-facing lowering. +This is the main reference for analysis-owned passes in `mojo-bindgen`. +Analysis starts with a raw parser `Unit`, validates and normalizes CIR, computes +shared facts, lowers into policy-light MojoIR, and assigns late Mojo record +policies. + +`NormalizeMojoModulePass` is intentionally not documented here in detail. It is +codegen-facing and has its own reference: +[mojo_ir_normalization_pipeline.md](mojo_ir_normalization_pipeline.md). ## Overview ```mermaid flowchart TD - A[raw parser Unit] --> B[AnalysisOrchestrator.normalize_cir] - B --> C[ValidateIRPass] - C --> D{IR invariants hold?} - D -->|no| E[IRValidationError] - D -->|yes| F[SignatureRecordStubPass] - F --> G[CIRCanonicalizer] - G --> H[normalized CIR Unit] - H --> I[LowerUnitPass] -``` + A[raw parser Unit] --> B[ValidateIRPass] + B --> C[SignatureRecordStubPass] + C --> D[CIRCanonicalizer] + D --> E[ValidateIRPass] + E --> F[ValidateReferencesPass] + F --> G[build_analysis_context] + + G --> H[declaration indexes] + G --> I[DeclDependencyGraph] + G --> J[AliasClassification] + G --> K[RecordLayoutFacts] + G --> L[RecordAnalysisFacts] -## Active Passes + G --> M[LowerUnitPass] + M --> N[LowerTypePass] + M --> O[LowerConstExprPass] + M --> P[UnitDeclLowerer] + P --> Q[struct_lowering] + P --> R[LowerUnionPass] + + M --> S[policy-light MojoModule] + S --> T[PolicyInferencePass] + T --> U[policy-final MojoModule] + U --> V[NormalizeMojoModulePass in codegen docs] +``` -The current CIR normalization sequence is owned by -`AnalysisOrchestrator.normalize_cir()`: +The compatibility entry points are: -1. `ValidateIRPass` -2. `SignatureRecordStubPass` -3. `CIRCanonicalizer` +- `run_ir_passes(unit)`: validates and normalizes CIR. +- `lower_unit(unit)`: lowers normalized CIR, building fallback context if needed. +- `AnalysisOrchestrator.analyze_with_artifacts(unit)`: returns the stable public + `AnalysisResult`. +- `AnalysisOrchestrator.analyze_pipeline(unit)`: returns `AnalysisArtifacts` + with normalized CIR, `AnalysisContext`, policy-light MojoIR, and final MojoIR. -Source: -- [orchestrator.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/orchestrator.py:26) +## Stage 1: CIR Validation And Normalization -## Pass Details +These passes operate on C-facing IR (`Unit`) and must not depend on MojoIR. ### `ValidateIRPass` Purpose: -- verify `decl_id` uniqueness across the `Unit` -- require stable identity on declarations and nested reference nodes that need it -- reject malformed `TypeRef`, `StructRef`, and `OpaqueRecordRef` structures early -This is a fail-fast structural correctness gate. It does not rewrite the IR. +- verify stable declaration identity where downstream passes require it +- reject conflicting duplicate `decl_id` values +- require `decl_id` on `Typedef`, `Struct`, `TypeRef`, `StructRef`, and + `OpaqueRecordRef` positions that participate in cross-reference analysis -Source: -- [validate_ir.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/validate_ir.py:17) +Placement: + +- runs first on raw parser CIR +- runs again after CIR repair/canonicalization + +It does not prove ABI representability or reference reachability. Those are +owned by later analysis passes. ### `SignatureRecordStubPass` Purpose: -- walk reachable CIR type positions and selected const-expression type positions -- find `StructRef` uses that lack a top-level `Struct` declaration -- prepend synthesized incomplete `Struct` declarations for those reachable refs -This ensures later lowering can still emit opaque record stubs for signature-only -record references such as `int f(struct opaque *p);`. +- walk reachable type positions in functions, typedefs, records, globals, + constants, and macros +- optionally walk const-expression type positions +- synthesize incomplete `Struct` declarations for signature-only `StructRef` + uses that have no top-level record declaration -Source: -- [reachability.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/reachability.py:153) +This keeps signatures such as `int f(struct opaque *p);` lowerable as opaque +Mojo record stubs. ### `CIRCanonicalizer` Purpose: -- apply analysis-owned CIR cleanup before MojoIR lowering -- canonicalize declaration ordering/details that should not remain parser-local -- deduplicate IR nodes where the parser may have produced equivalent duplicates -This keeps downstream lowering working from one normalized CIR shape rather than -raw parser output quirks. +- keep one record declaration per `decl_id`, preferring complete definitions +- deduplicate equivalent function declarations +- keep the last definition for repeated macro names +- drop self-alias macros already represented by constants or enumerants +- choose enum primary names from typedef/tag information +- rewrite enum references to the chosen primary names + +The canonicalizer returns a fresh `Unit`; callers should not rely on mutation of +the raw parser result. + +### `ValidateReferencesPass` + +Purpose: + +- reject normalized CIR references that should resolve but do not +- require `EnumRef` declarations to exist in the normalized `Unit` +- require concrete non-union `StructRef` declarations to exist after signature + stub materialization +- allow external typedef references, because `LowerUnitPass` can synthesize + external typedef aliases +- allow `OpaqueRecordRef` to remain opaque/external + +This is the dangling-reference gate for normalized CIR. + +## Stage 2: Shared Analysis Context + +`build_analysis_context(unit)` computes reusable whole-unit facts after CIR is +normalized and reference-validated. This stage should absorb analysis work that +would otherwise be repeated inside lowerers. + +### Declaration Indexes + +`AnalysisContext` indexes normalized declarations by stable keys: + +- records by `decl_id` +- typedefs by `decl_id` +- enums by `decl_id` +- functions by `decl_id` +- globals by `decl_id` +- constants by name +- macros by name + +These indexes are the basic lookup surface for later passes. + +### `DeclDependencyGraph` + +Purpose: + +- record type-reference edges from declarations to typedefs, records, and enums +- record symbol-reference edges from constant expressions and macros +- provide a foundation for diagnostics, stable ordering, and future + allowlist/pruning support + +This pass is fact-building only; it does not reorder or delete declarations. + +### `AliasClassification` + +Purpose: + +- classify local typedef declarations +- classify external typedef references found in type positions +- identify callback typedefs, enum aliases, record aliases, exact-width stdint + aliases, ordinary typedefs, and external typedefs + +This keeps alias meaning available as shared facts instead of rediscovering it +inside lowerers. + +### `RecordLayoutFacts` + +Purpose: -Source: -- [cir_canonicalizer.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/cir_canonicalizer.py:1) +- analyze physical record layout from CIR offsets/sizes +- compute plain field facts, bitfield storage runs, padding spans, natural typed + alignment, and layout problems +- preserve incomplete-record facts without forcing a lowering decision + +`analyze_record_layout()` owns pure C-layout checks. It does not decide how a +Mojo `StructDecl` should be emitted. + +### `RecordAnalysisFacts` / `RecordShapeAnalyzer` + +Purpose: -## Why These Passes Exist +- centralize record shape and representability decisions +- classify record storage as incomplete, union, typed, or opaque storage +- validate direct and embedded flexible-tail patterns +- analyze recursive by-value record shapes +- carry flexible-tail metadata for struct lowering +- carry fallback reasons for opaque-storage diagnostics -The parser intentionally stays source-driven and conservative. That means it can -return structurally faithful CIR that still needs: +This is the central record analysis pass. `struct_lowering` consumes these facts +and performs MojoIR member construction; it should not own recursive record +shape policy. -- validation before deeper analysis -- synthesized incomplete record declarations for signature-only refs -- minor canonical cleanup before module lowering +## Stage 3: CIR To MojoIR Lowering + +Lowering consumes normalized CIR plus `AnalysisContext` and produces a +policy-light `MojoModule`. These passes are Mojo-facing, but still live in +analysis because they convert analyzed CIR into MojoIR. + +### `LowerUnitPass` + +Purpose: + +- create shared lowerers for one unit +- synthesize aliases for external typedef references when needed +- lower top-level declarations through `UnitDeclLowerer` +- build module metadata and linking mode + +`lower_unit(unit)` remains a compatibility helper. If no `AnalysisContext` is +provided, it builds one internally. + +### `UnitDeclLowerer` + +Purpose: + +- dispatch each top-level CIR declaration to the correct lowerer +- lower typedefs, enums, functions, globals, constants, macros, structs, and + unions +- preserve declaration order except for intentionally synthesized aliases + +This is orchestration, not whole-unit analysis. New cross-declaration facts +should generally be added to `AnalysisContext`. + +### `LowerTypePass` + +Purpose: + +- map CIR types to MojoIR type nodes +- preserve named typedef/record/enum surfaces as `NamedType` +- lower pointers, arrays, function pointers, atomics, vectors, complex values, + and unsupported sized types + +This is a recursive type lowerer. It does not decide declaration reachability or +record storage policy. + +### `LowerConstExprPass` + +Purpose: + +- lower CIR constant-expression nodes to MojoIR constant-expression nodes +- lower cast and `sizeof` target types through `LowerTypePass` +- reject constant forms that have no direct MojoIR value form, such as null + pointer literals + +Macro emission policy lives in `UnitDeclLowerer`; expression lowering only +rewrites supported expression shapes. + +### Struct Lowering + +Purpose: + +- consume `RecordAnalysisFacts` and `RecordLayoutFacts` +- emit opaque declarations for incomplete records +- emit byte-storage structs for opaque-storage decisions +- lower typed plain fields and bitfield groups to MojoIR members +- attach flexible-tail metadata computed by record analysis +- compute Mojo alignment decorator policy + +Struct lowering should not recompute recursive record shape decisions. + +### `LowerUnionPass` + +Purpose: + +- lower complete unions to `UnsafeUnion[...]` when member types are distinct and + representable +- fall back to `InlineArray[UInt8, size]` when union lowering would be unsafe or + ambiguous +- emit placeholder aliases for incomplete unions + +Union lowering remains separate because unions lower to alias-style layout +types, not `StructDecl`. + +## Stage 4: Late MojoIR Policy Analysis + +### `PolicyInferencePass` + +Purpose: -Analysis owns that repair boundary so parsing can stay focused on faithful C -> -CIR lowering. +- infer record passability +- assign Mojo traits +- decide fieldwise initializer eligibility +- handle recursion, opaque storage, arrays, atomics, pointers, aliases, and + nested records -## What Comes Next +This pass runs after CIR-to-MojoIR lowering because it reasons over final MojoIR +types and declaration relationships. -After CIR normalization, analysis continues with Mojo-facing lowering and -finalization: +## Codegen Boundary -1. `LowerUnitPass.run(unit)` lowers normalized CIR into a policy-light `MojoModule` -2. `assign_record_policies(module)` derives struct passability, traits, and fieldwise-init policy -3. `normalize_mojo_module(module)` makes printer-facing facts explicit +After `PolicyInferencePass`, analysis hands a policy-final `MojoModule` to +codegen. `NormalizeMojoModulePass` then makes printer-facing facts explicit: +callback alias hoisting, imports, support declarations, final call targets, and +nested type normalization. -So the CIR pass layer is intentionally narrow: validate, materialize -signature-only record stubs, canonicalize, then hand off to MojoIR lowering. +That codegen-facing pass is documented separately in +[mojo_ir_normalization_pipeline.md](mojo_ir_normalization_pipeline.md). diff --git a/docs/mojo_ir_normalization_pipeline.md b/docs/mojo_ir_normalization_pipeline.md index 2a58f67..b826185 100644 --- a/docs/mojo_ir_normalization_pipeline.md +++ b/docs/mojo_ir_normalization_pipeline.md @@ -105,11 +105,12 @@ rewrite is part of the normalization contract, not printer logic. The current public render path is: -1. `AnalysisOrchestrator.run_ir_passes(unit)` repairs raw CIR -2. `LowerUnitPass.run(unit)` produces a policy-light `MojoModule` -3. `assign_record_policies(module)` derives late record passability, traits, and fieldwise-init policy -4. `normalize_mojo_module(module)` makes printer-facing facts explicit -5. `render_mojo_module(module, options)` emits Mojo source +1. `AnalysisOrchestrator.run_ir_passes(unit)` validates and normalizes raw CIR +2. `build_analysis_context(unit)` computes shared declaration and layout facts +3. `LowerUnitPass.run(unit)` produces a policy-light `MojoModule` +4. `PolicyInferencePass` derives late record passability, traits, and fieldwise-init policy +5. `normalize_mojo_module(module)` makes printer-facing facts explicit +6. `render_mojo_module(module, options)` emits Mojo source Source: - [orchestrator.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/orchestrator.py:79) diff --git a/examples/cairo/generate.sh b/examples/cairo/generate.sh index e60d6bb..f784ef3 100755 --- a/examples/cairo/generate.sh +++ b/examples/cairo/generate.sh @@ -18,11 +18,9 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" cd "$HERE" -if command -v mojo-bindgen >/dev/null 2>&1; then - BG=(mojo-bindgen) -else - BG=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) -fi +source "$REPO_ROOT/examples/common.sh" +set_mojo_bindgen_cmd BG "$REPO_ROOT" +set_mojo_cmd MOJO "$REPO_ROOT" find_cairo_h() { local d @@ -57,18 +55,13 @@ CAIRO_H="$(find_cairo_h)" || { exit 1 } -"${BG[@]}" "$CAIRO_H" --library cairo --link-name cairo -o cairo_bindings.mojo +generate_bindings "$CAIRO_H" cairo cairo cairo_bindings.mojo -if command -v mojo >/dev/null 2>&1; then - MJ=(mojo) -else - MJ=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo) -fi OBJ="$(mktemp "${TMPDIR:-/tmp}/cairo-bindings-XXXXXX.o")" trap 'rm -f "$OBJ"' EXIT -"${MJ[@]}" build --emit object cairo_bindings.mojo -o "$OBJ" -"${MJ[@]}" build cairo_smoke.mojo -I "$HERE" -Xlinker -lcairo -o cairo_smoke +build_bindings_object cairo_bindings.mojo "$OBJ" +"${MOJO[@]}" build cairo_smoke.mojo -I "$HERE" -Xlinker -lcairo -o cairo_smoke -echo "Wrote $HERE/cairo_bindings.mojo (from $CAIRO_H)" +echo "Wrote $HERE/cairo_bindings.mojo and cairo_bindings_layout_tests.mojo (from $CAIRO_H)" echo "Running cairo_smoke (runtime render + file round-trip proof)" "$HERE/cairo_smoke" diff --git a/examples/common.sh b/examples/common.sh new file mode 100644 index 0000000..dd99dbc --- /dev/null +++ b/examples/common.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +# Shared helpers for repository example generation scripts. + +set -euo pipefail + +example_repo_root() { + local here="$1" + cd "$here/../.." && pwd +} + +set_mojo_bindgen_cmd() { + local -n out="$1" + local repo_root="$2" + if command -v mojo-bindgen >/dev/null 2>&1; then + out=(mojo-bindgen) + return 0 + fi + out=(pixi run --manifest-path "$repo_root/pixi.toml" mojo-bindgen) +} + +set_mojo_cmd() { + local -n out="$1" + local repo_root="$2" + if command -v mojo >/dev/null 2>&1; then + out=(mojo) + return 0 + fi + out=(pixi run --manifest-path "$repo_root/pixi.toml" mojo) +} + +set_repo_mojo_cmd() { + local -n out="$1" + local repo_root="$2" + out=(pixi run --manifest-path "$repo_root/pixi.toml" mojo) +} + +generate_bindings() { + local header="$1" + local library="$2" + local link_name="$3" + local output="$4" + shift 4 + + "${BG[@]}" "$header" \ + --library "$library" \ + --link-name "$link_name" \ + --layout-tests \ + --layout-test-output "${output%.mojo}_layout_tests.mojo" \ + "$@" \ + -o "$output" +} + +build_bindings_object() { + local source="$1" + local object="$2" + shift 2 + "${MOJO[@]}" build "$source" --emit object -o "$object" "$@" +} diff --git a/examples/libpng/generate.sh b/examples/libpng/generate.sh index e6004be..719bfc1 100755 --- a/examples/libpng/generate.sh +++ b/examples/libpng/generate.sh @@ -19,17 +19,9 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" cd "$HERE" -if command -v mojo-bindgen >/dev/null 2>&1; then - BG=(mojo-bindgen) -else - BG=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) -fi - -if command -v mojo >/dev/null 2>&1; then - MOJO=(mojo) -else - MOJO=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo) -fi +source "$REPO_ROOT/examples/common.sh" +set_mojo_bindgen_cmd BG "$REPO_ROOT" +set_mojo_cmd MOJO "$REPO_ROOT" detect_png_pkg() { local module @@ -102,13 +94,13 @@ PNG_H="$(find_png_h "$PNG_PKG")" || { } LINK_NAME="$(find_link_name "$PNG_PKG")" -"${BG[@]}" "$PNG_H" --library "$LINK_NAME" --link-name "$LINK_NAME" -o libpng_bindings.mojo +generate_bindings "$PNG_H" "$LINK_NAME" "$LINK_NAME" libpng_bindings.mojo OBJ="$(mktemp "${TMPDIR:-/tmp}/libpng-bindings-XXXXXX.o")" trap 'rm -f "$OBJ"' EXIT -"${MOJO[@]}" build libpng_bindings.mojo -Xlinker "-l$LINK_NAME" --emit object -o "$OBJ" +build_bindings_object libpng_bindings.mojo "$OBJ" -Xlinker "-l$LINK_NAME" "${MOJO[@]}" build libpng_smoke.mojo -I "$HERE" -Xlinker "-l$LINK_NAME" -o libpng_smoke -echo "Wrote $HERE/libpng_bindings.mojo (from $PNG_H)" +echo "Wrote $HERE/libpng_bindings.mojo and libpng_bindings_layout_tests.mojo (from $PNG_H)" echo "Running libpng_smoke (runtime version probe proof)" "$HERE/libpng_smoke" diff --git a/examples/sqlite/generate.sh b/examples/sqlite/generate.sh index b03008f..885e7f9 100755 --- a/examples/sqlite/generate.sh +++ b/examples/sqlite/generate.sh @@ -20,14 +20,10 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" cd "$HERE" -if command -v mojo-bindgen >/dev/null 2>&1; then - BG=(mojo-bindgen) -else - BG=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) -fi - +source "$REPO_ROOT/examples/common.sh" +set_mojo_bindgen_cmd BG "$REPO_ROOT" # Use the repository Pixi environment so Mojo resolves `std` consistently. -MOJO=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo) +set_repo_mojo_cmd MOJO "$REPO_ROOT" find_sqlite3_h() { if [[ -f "$HERE/sqlite3.h" ]]; then @@ -88,12 +84,12 @@ SQLITE_H="$(find_sqlite3_h)" || { exit 1 } -"${BG[@]}" "$SQLITE_H" --library sqlite3 --link-name sqlite3 -o sqlite3_bindings.mojo +generate_bindings "$SQLITE_H" sqlite3 sqlite3 sqlite3_bindings.mojo LINK_EXTRA="$(sqlite_link_extra || true)" OBJ="$(mktemp "${TMPDIR:-/tmp}/sqlite-bindings-XXXXXX.o")" trap 'rm -f "$OBJ"' EXIT -"${MOJO[@]}" build sqlite3_bindings.mojo --emit object -o "$OBJ" +build_bindings_object sqlite3_bindings.mojo "$OBJ" if [[ -n "$LINK_EXTRA" ]]; then # shellcheck disable=SC2206 EXTRA=(-Xlinker "$LINK_EXTRA") @@ -102,6 +98,6 @@ else fi "${MOJO[@]}" build sqlite_smoke.mojo -I "$HERE" "${EXTRA[@]}" -Xlinker -lsqlite3 -o sqlite_smoke -echo "Wrote $HERE/sqlite3_bindings.mojo (from $SQLITE_H)" +echo "Wrote $HERE/sqlite3_bindings.mojo and sqlite3_bindings_layout_tests.mojo (from $SQLITE_H)" echo "Running sqlite_smoke (composite runtime proof)" "$HERE/sqlite_smoke" diff --git a/examples/zlib/generate.sh b/examples/zlib/generate.sh index 1bfe73e..6cf0c26 100755 --- a/examples/zlib/generate.sh +++ b/examples/zlib/generate.sh @@ -21,17 +21,9 @@ HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" cd "$HERE" -if command -v mojo-bindgen >/dev/null 2>&1; then - BG=(mojo-bindgen) -else - BG=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo-bindgen) -fi - -if command -v mojo >/dev/null 2>&1; then - MOJO=(mojo) -else - MOJO=(pixi run --manifest-path "$REPO_ROOT/pixi.toml" mojo) -fi +source "$REPO_ROOT/examples/common.sh" +set_mojo_bindgen_cmd BG "$REPO_ROOT" +set_mojo_cmd MOJO "$REPO_ROOT" find_zlib_h() { local d @@ -100,15 +92,15 @@ ZLIB_H="$(find_zlib_h)" || { } -"${BG[@]}" "$ZLIB_H" --library z --link-name z -o zlib_bindings.mojo -"${MOJO[@]}" build zlib_bindings.mojo -Xlinker -lz --emit object -o zlib_bindings.o +generate_bindings "$ZLIB_H" z z zlib_bindings.mojo +build_bindings_object zlib_bindings.mojo zlib_bindings.o -Xlinker -lz -echo "Wrote $HERE/zlib_bindings.mojo and zlib_bindings.o (from $ZLIB_H)" +echo "Wrote $HERE/zlib_bindings.mojo, zlib_bindings_layout_tests.mojo, and zlib_bindings.o (from $ZLIB_H)" LIBZ_SO="$(find_libz_so)" || true if [[ -n "${LIBZ_SO:-}" ]]; then - "${BG[@]}" "$ZLIB_H" --library z --link-name z \ - --linking owned_dl_handle --library-path-hint "$LIBZ_SO" -o zlib_bindings_dl.mojo + generate_bindings "$ZLIB_H" z z zlib_bindings_dl.mojo \ + --linking owned_dl_handle --library-path-hint "$LIBZ_SO" "${MOJO[@]}" build zlib_dl_smoke.mojo -I "$HERE" -o zlib_dl_smoke echo "Wrote $HERE/zlib_bindings_dl.mojo (dlopen: $LIBZ_SO) and built zlib_dl_smoke" echo "Running zlib_dl_smoke (owned_dl_handle runtime proof)" diff --git a/mojo_bindgen/analysis/__init__.py b/mojo_bindgen/analysis/__init__.py index 7d73388..dec6f59 100644 --- a/mojo_bindgen/analysis/__init__.py +++ b/mojo_bindgen/analysis/__init__.py @@ -1,13 +1,25 @@ """Public analysis API for CIR passes and CIR -> MojoIR lowering.""" +from mojo_bindgen.analysis.alias_classification import ( + AliasClass, + AliasClassification, + AliasInfo, + classify_aliases, +) from mojo_bindgen.analysis.const_lowering import ( ConstExprLoweringError, LowerConstExprPass, lower_const_expr, ) +from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context from mojo_bindgen.analysis.decl_lowerer import UnitLoweringError +from mojo_bindgen.analysis.dependency_graph import ( + DeclDependencyGraph, + build_decl_dependency_graph, +) from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.orchestrator import ( + AnalysisArtifacts, AnalysisOrchestrator, AnalysisResult, run_ir_passes, @@ -24,6 +36,18 @@ PolicyInferencePass, assign_record_policies, ) +from mojo_bindgen.analysis.record_shape import ( + ByValueRecordShape, + RecordAnalysisFacts, + RecordShapeFacts, + RecordStorageKind, + analyze_record_shape, + analyze_record_shapes, +) +from mojo_bindgen.analysis.reference_validation import ( + ReferenceValidationError, + ValidateReferencesPass, +) from mojo_bindgen.analysis.struct_lowering import ( StructLoweringContext, StructLoweringError, @@ -50,7 +74,12 @@ "AssignRecordPoliciesError", "AssignRecordPoliciesPass", "AnalysisResult", + "AnalysisArtifacts", + "AnalysisContext", "AnalysisOrchestrator", + "AliasClass", + "AliasClassification", + "AliasInfo", "ConstExprLoweringError", "IRValidationError", "LowerConstExprPass", @@ -59,6 +88,12 @@ "LowerUnitPass", "MojoEmitOptions", "PolicyInferencePass", + "DeclDependencyGraph", + "ReferenceValidationError", + "ByValueRecordShape", + "RecordAnalysisFacts", + "RecordShapeFacts", + "RecordStorageKind", "SignatureRecordStubOptions", "SignatureRecordStubPass", "SignatureRecordStubResult", @@ -66,10 +101,16 @@ "StructLoweringError", "TypeLoweringError", "ValidateIRPass", + "ValidateReferencesPass", "UnionLoweringError", "UnitLoweringError", "TypeWalkOptions", "assign_record_policies", + "analyze_record_shape", + "analyze_record_shapes", + "build_analysis_context", + "build_decl_dependency_graph", + "classify_aliases", "iter_type_nodes", "lower_const_expr", "lower_struct", diff --git a/mojo_bindgen/analysis/alias_classification.py b/mojo_bindgen/analysis/alias_classification.py new file mode 100644 index 0000000..dcb6a2f --- /dev/null +++ b/mojo_bindgen/analysis/alias_classification.py @@ -0,0 +1,153 @@ +"""Typedef and alias classification for normalized CIR.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import StrEnum + +from mojo_bindgen.analysis.type_lowering import exact_width_stdint_alias_type +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes +from mojo_bindgen.ir import ( + Const, + EnumRef, + Function, + FunctionPtr, + GlobalVar, + MacroDecl, + Struct, + StructRef, + Type, + Typedef, + TypeRef, + Unit, +) + + +class AliasClass(StrEnum): + """Semantic classes for C typedef-like declarations and references.""" + + CALLBACK = "callback" + ENUM = "enum" + EXACT_WIDTH_STDINT = "exact_width_stdint" + EXTERNAL_TYPEDEF = "external_typedef" + NO_OP_SELF_ALIAS = "no_op_self_alias" + RECORD = "record" + TYPEDEF = "typedef" + + +@dataclass(frozen=True) +class AliasInfo: + """Classification for one typedef declaration or external typedef reference.""" + + decl_id: str + name: str + alias_class: AliasClass + target_decl_id: str | None = None + + +@dataclass(frozen=True) +class AliasClassification: + """Alias classifications keyed by declaration id.""" + + aliases_by_decl_id: dict[str, AliasInfo] + external_aliases_by_decl_id: dict[str, AliasInfo] + + +def classify_aliases(unit: Unit) -> AliasClassification: + """Classify local typedef declarations and external typedef references.""" + + local_typedefs = {decl.decl_id for decl in unit.decls if isinstance(decl, Typedef)} + aliases = { + decl.decl_id: AliasInfo( + decl_id=decl.decl_id, + name=decl.name, + alias_class=_classify_typedef(decl), + target_decl_id=_target_decl_id(decl.aliased) or _target_decl_id(decl.canonical), + ) + for decl in unit.decls + if isinstance(decl, Typedef) + } + + external: dict[str, AliasInfo] = {} + for ref in _typeref_uses(unit): + if ref.decl_id in local_typedefs or ref.decl_id in external: + continue + external[ref.decl_id] = AliasInfo( + decl_id=ref.decl_id, + name=ref.name, + alias_class=AliasClass.EXTERNAL_TYPEDEF, + target_decl_id=_target_decl_id(ref.canonical), + ) + + return AliasClassification( + aliases_by_decl_id=aliases, + external_aliases_by_decl_id=external, + ) + + +def _classify_typedef(decl: Typedef) -> AliasClass: + if exact_width_stdint_alias_type(decl.name) is not None: + return AliasClass.EXACT_WIDTH_STDINT + if isinstance(decl.aliased, FunctionPtr) or isinstance(decl.canonical, FunctionPtr): + return AliasClass.CALLBACK + target = _target_type(decl.aliased) or _target_type(decl.canonical) + if isinstance(target, EnumRef): + return AliasClass.ENUM + if isinstance(target, StructRef): + return AliasClass.RECORD + if isinstance(decl.aliased, TypeRef) and decl.aliased.decl_id == decl.decl_id: + return AliasClass.NO_OP_SELF_ALIAS + return AliasClass.TYPEDEF + + +def _target_type(t: Type) -> EnumRef | StructRef | None: + if isinstance(t, TypeRef): + return _target_type(t.canonical) + if isinstance(t, (EnumRef, StructRef)): + return t + return None + + +def _target_decl_id(t: Type) -> str | None: + target = _target_type(t) + return None if target is None else target.decl_id + + +def _typeref_uses(unit: Unit) -> tuple[TypeRef, ...]: + out: list[TypeRef] = [] + for decl in unit.decls: + for t in _decl_types(decl): + out.extend( + node + for node in collect_type_nodes( + t, + lambda node: isinstance(node, TypeRef), + options=TypeWalkOptions(descend_vector_element=True), + ) + if isinstance(node, TypeRef) + ) + return tuple(out) + + +def _decl_types(decl) -> tuple[Type, ...]: + if isinstance(decl, Function): + return (decl.ret, *(param.type for param in decl.params)) + if isinstance(decl, Typedef): + return (decl.aliased, decl.canonical) + if isinstance(decl, Struct): + return tuple(field.type for field in decl.fields) + if isinstance(decl, GlobalVar): + return (decl.type,) + if isinstance(decl, Const): + return (decl.type,) + if isinstance(decl, MacroDecl): + return () if decl.type is None else (decl.type,) + return () + + +__all__ = [ + "AliasClass", + "AliasClassification", + "AliasInfo", + "classify_aliases", +] diff --git a/mojo_bindgen/analysis/cir_canonicalizer.py b/mojo_bindgen/analysis/cir_canonicalizer.py index 86f043f..e426e6b 100644 --- a/mojo_bindgen/analysis/cir_canonicalizer.py +++ b/mojo_bindgen/analysis/cir_canonicalizer.py @@ -36,7 +36,17 @@ class CIRCanonicalizer: - """Canonicalize cross-declaration CIR facts before Mojo lowering.""" + """Canonicalize cross-declaration CIR facts before Mojo lowering. + + This pass repairs parser-local declaration quirks without changing the + caller-owned ``Unit``. Its ordered steps are: + + 1. keep one record row per ``decl_id``, preferring complete definitions + 2. remove duplicate equivalent function declarations + 3. keep the last macro definition for repeated macro names + 4. drop self-alias macros already represented by constants/enumerants + 5. choose enum primary names and rewrite enum references to those names + """ def __init__( self, @@ -49,8 +59,7 @@ def canonicalize(self, unit: Unit) -> Unit: decls = _dedupe_macros_last_wins(decls) decls = _drop_constant_self_alias_macros(decls) enum_names = _resolve_enum_names(decls) - unit.decls = [_rewrite_decl(decl, enum_names) for decl in decls] - return unit + return replace(unit, decls=[_rewrite_decl(decl, enum_names) for decl in decls]) def _dedupe_structs(self, unit: Unit) -> list[Decl]: out = list[Decl]() diff --git a/mojo_bindgen/analysis/context.py b/mojo_bindgen/analysis/context.py new file mode 100644 index 0000000..4f398f1 --- /dev/null +++ b/mojo_bindgen/analysis/context.py @@ -0,0 +1,73 @@ +"""Shared analysis facts for the CIR -> MojoIR pipeline.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mojo_bindgen.analysis.alias_classification import AliasClassification, classify_aliases +from mojo_bindgen.analysis.dependency_graph import DeclDependencyGraph, build_decl_dependency_graph +from mojo_bindgen.analysis.lowering_support import record_by_decl_id +from mojo_bindgen.analysis.record_layout import RecordLayoutFacts, analyze_record_layout +from mojo_bindgen.analysis.record_shape import RecordAnalysisFacts, analyze_record_shapes +from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit + + +@dataclass(frozen=True) +class AnalysisContext: + """Reusable facts computed after CIR normalization and before MojoIR lowering.""" + + unit: Unit + records_by_decl_id: dict[str, Struct] + typedefs_by_decl_id: dict[str, Typedef] + enums_by_decl_id: dict[str, Enum] + functions_by_decl_id: dict[str, Function] + globals_by_decl_id: dict[str, GlobalVar] + consts_by_name: dict[str, Const] + macros_by_name: dict[str, MacroDecl] + dependency_graph: DeclDependencyGraph + alias_classification: AliasClassification + record_layouts: dict[str, RecordLayoutFacts] + record_shapes: dict[str, RecordAnalysisFacts] + + +def build_analysis_context(unit: Unit) -> AnalysisContext: + """Build shared declaration indexes and record layout facts for ``unit``.""" + + records = record_by_decl_id(unit) + record_layouts = { + decl_id: analyze_record_layout( + record, + target_abi=unit.target_abi, + record_map=records, + ) + for decl_id, record in records.items() + } + dependency_graph = build_decl_dependency_graph(unit) + alias_classification = classify_aliases(unit) + record_shapes = analyze_record_shapes(records, record_layouts) + return AnalysisContext( + unit=unit, + records_by_decl_id=records, + typedefs_by_decl_id={ + decl.decl_id: decl for decl in unit.decls if isinstance(decl, Typedef) + }, + enums_by_decl_id={decl.decl_id: decl for decl in unit.decls if isinstance(decl, Enum)}, + functions_by_decl_id={ + decl.decl_id: decl for decl in unit.decls if isinstance(decl, Function) + }, + globals_by_decl_id={ + decl.decl_id: decl for decl in unit.decls if isinstance(decl, GlobalVar) + }, + consts_by_name={decl.name: decl for decl in unit.decls if isinstance(decl, Const)}, + macros_by_name={decl.name: decl for decl in unit.decls if isinstance(decl, MacroDecl)}, + dependency_graph=dependency_graph, + alias_classification=alias_classification, + record_layouts=record_layouts, + record_shapes=record_shapes, + ) + + +__all__ = [ + "AnalysisContext", + "build_analysis_context", +] diff --git a/mojo_bindgen/analysis/dependency_graph.py b/mojo_bindgen/analysis/dependency_graph.py new file mode 100644 index 0000000..1c3992d --- /dev/null +++ b/mojo_bindgen/analysis/dependency_graph.py @@ -0,0 +1,136 @@ +"""Declaration dependency graph for normalized CIR.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes +from mojo_bindgen.ir import ( + BinaryExpr, + CastExpr, + Const, + ConstExpr, + Decl, + Enum, + EnumRef, + Function, + GlobalVar, + MacroDecl, + RefExpr, + SizeOfExpr, + Struct, + StructRef, + Type, + Typedef, + TypeRef, + UnaryExpr, + Unit, +) + + +@dataclass(frozen=True) +class DeclDependencyGraph: + """Dependency edges from one top-level CIR declaration to other declarations.""" + + edges_by_decl_id: dict[str, frozenset[str]] + symbol_edges_by_decl_id: dict[str, frozenset[str]] + + +def build_decl_dependency_graph(unit: Unit) -> DeclDependencyGraph: + """Build type/reference edges for declarations that have stable identities.""" + + edges: dict[str, set[str]] = {} + symbol_edges: dict[str, set[str]] = {} + for decl in unit.decls: + decl_id = _decl_id(decl) + if decl_id is None: + continue + type_edges: set[str] = set() + symbols: set[str] = set() + for t in _decl_types(decl): + _collect_type_edges(t, type_edges) + for expr in _decl_exprs(decl): + _collect_const_expr_edges(expr, type_edges, symbols) + edges[decl_id] = type_edges + symbol_edges[decl_id] = symbols + + return DeclDependencyGraph( + edges_by_decl_id={decl_id: frozenset(values) for decl_id, values in edges.items()}, + symbol_edges_by_decl_id={ + decl_id: frozenset(values) for decl_id, values in symbol_edges.items() + }, + ) + + +def _decl_id(decl: Decl) -> str | None: + if isinstance(decl, (Function, Struct, Typedef, GlobalVar)): + return decl.decl_id + if isinstance(decl, Enum): + return decl.decl_id + if isinstance(decl, (Const, MacroDecl)): + return decl.name + return None + + +def _decl_types(decl: Decl) -> tuple[Type, ...]: + if isinstance(decl, Function): + return (decl.ret, *(param.type for param in decl.params)) + if isinstance(decl, Typedef): + return (decl.aliased, decl.canonical) + if isinstance(decl, Struct): + return tuple(field.type for field in decl.fields) + if isinstance(decl, GlobalVar): + return (decl.type,) + if isinstance(decl, Const): + return (decl.type,) + if isinstance(decl, MacroDecl): + return () if decl.type is None else (decl.type,) + return () + + +def _decl_exprs(decl: Decl) -> tuple[ConstExpr, ...]: + if isinstance(decl, Const): + return (decl.expr,) + if isinstance(decl, GlobalVar) and decl.initializer is not None: + return (decl.initializer,) + if isinstance(decl, MacroDecl) and decl.expr is not None: + return (decl.expr,) + return () + + +def _collect_type_edges(t: Type, out: set[str]) -> None: + for node in iter_type_nodes( + t, + options=TypeWalkOptions(descend_vector_element=True), + ): + if isinstance(node, (TypeRef, StructRef, EnumRef)): + out.add(node.decl_id) + + +def _collect_const_expr_edges( + expr: ConstExpr, + type_edges: set[str], + symbol_edges: set[str], +) -> None: + if isinstance(expr, RefExpr): + symbol_edges.add(expr.name) + return + if isinstance(expr, CastExpr): + _collect_type_edges(expr.target, type_edges) + _collect_const_expr_edges(expr.expr, type_edges, symbol_edges) + return + if isinstance(expr, SizeOfExpr): + _collect_type_edges(expr.target, type_edges) + return + if isinstance(expr, UnaryExpr): + _collect_const_expr_edges(expr.operand, type_edges, symbol_edges) + return + if isinstance(expr, BinaryExpr): + _collect_const_expr_edges(expr.lhs, type_edges, symbol_edges) + _collect_const_expr_edges(expr.rhs, type_edges, symbol_edges) + + +__all__ = [ + "DeclDependencyGraph", + "build_decl_dependency_graph", +] diff --git a/mojo_bindgen/analysis/orchestrator.py b/mojo_bindgen/analysis/orchestrator.py index c554aaa..72a9fca 100644 --- a/mojo_bindgen/analysis/orchestrator.py +++ b/mojo_bindgen/analysis/orchestrator.py @@ -5,9 +5,11 @@ from dataclasses import dataclass from mojo_bindgen.analysis.cir_canonicalizer import CIRCanonicalizer +from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.reachability import SignatureRecordStubPass from mojo_bindgen.analysis.record_policies import assign_record_policies +from mojo_bindgen.analysis.reference_validation import ValidateReferencesPass from mojo_bindgen.analysis.unit_lowering import lower_unit from mojo_bindgen.analysis.validate_ir import ValidateIRPass from mojo_bindgen.codegen.normalize_mojo_module import normalize_mojo_module @@ -20,6 +22,18 @@ class AnalysisResult: mojo_module: MojoModule +@dataclass(frozen=True) +class AnalysisArtifacts: + """Artifacts produced by the staged analysis pipeline.""" + + raw_unit: Unit + validated_unit: Unit + normalized_unit: Unit + context: AnalysisContext + policy_light_module: MojoModule + mojo_module: MojoModule + + class AnalysisOrchestrator: """Own the raw-CIR -> finalized-MojoIR analysis pipeline.""" @@ -31,19 +45,25 @@ def options(self) -> MojoEmitOptions: return self._options def normalize_cir(self, unit: Unit) -> Unit: - """Run the CIR repair sequence before MojoIR lowering.""" - current = ValidateIRPass().run(unit) - current = SignatureRecordStubPass().run(current).unit - current = CIRCanonicalizer().canonicalize(current) - return current + """Run the ordered, pure CIR repair sequence before MojoIR lowering.""" + validated = ValidateIRPass().run(unit) + with_signature_stubs = SignatureRecordStubPass().run(validated).unit + normalized = CIRCanonicalizer().canonicalize(with_signature_stubs) + normalized = ValidateIRPass().run(normalized) + return ValidateReferencesPass().run(normalized) def run_ir_passes(self, unit: Unit) -> Unit: """Compatibility alias for the CIR normalization pass sequence.""" return self.normalize_cir(unit) - def lower_normalized(self, unit: Unit) -> MojoModule: + def lower_normalized( + self, + unit: Unit, + *, + context: AnalysisContext | None = None, + ) -> MojoModule: """Lower already-normalized CIR into policy-free MojoIR.""" - return lower_unit(unit, options=self._options) + return lower_unit(unit, options=self._options, context=context) def lower(self, unit: Unit) -> MojoModule: """Lower validated CIR into policy-free MojoIR.""" @@ -58,10 +78,28 @@ def analyze(self, unit: Unit) -> MojoModule: return self.analyze_with_artifacts(unit).mojo_module def analyze_with_artifacts(self, unit: Unit) -> AnalysisResult: - normalized_unit = self.run_ir_passes(unit) - lowered = self.lower_normalized(normalized_unit) + artifacts = self.analyze_pipeline(unit) return AnalysisResult( + normalized_unit=artifacts.normalized_unit, + mojo_module=artifacts.mojo_module, + ) + + def analyze_pipeline(self, unit: Unit) -> AnalysisArtifacts: + """Run the full analysis pipeline and return all major stage artifacts.""" + validated_unit = ValidateIRPass().run(unit) + with_signature_stubs = SignatureRecordStubPass().run(validated_unit).unit + normalized_unit = ValidateIRPass().run( + CIRCanonicalizer().canonicalize(with_signature_stubs) + ) + normalized_unit = ValidateReferencesPass().run(normalized_unit) + context = build_analysis_context(normalized_unit) + lowered = self.lower_normalized(normalized_unit, context=context) + return AnalysisArtifacts( + raw_unit=unit, + validated_unit=validated_unit, normalized_unit=normalized_unit, + context=context, + policy_light_module=lowered, mojo_module=self.finalize(lowered), ) @@ -73,6 +111,7 @@ def run_ir_passes(unit: Unit) -> Unit: __all__ = [ "AnalysisResult", + "AnalysisArtifacts", "AnalysisOrchestrator", "run_ir_passes", ] diff --git a/mojo_bindgen/analysis/record_shape.py b/mojo_bindgen/analysis/record_shape.py new file mode 100644 index 0000000..534b3a0 --- /dev/null +++ b/mojo_bindgen/analysis/record_shape.py @@ -0,0 +1,459 @@ +"""Central record layout and shape analysis for normalized CIR.""" + +from __future__ import annotations + +from dataclasses import dataclass, replace +from enum import StrEnum + +from mojo_bindgen.analysis.lowering_support import field_display_name, try_lower_type +from mojo_bindgen.analysis.record_layout import RecordLayoutFacts +from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes +from mojo_bindgen.ir import ( + Array, + AtomicType, + FlexibleTail, + ParametricBase, + ParametricType, + QualifiedType, + Struct, + StructRef, + Type, + TypeRef, +) + + +class RecordStorageKind(StrEnum): + """High-level storage decision for one CIR record.""" + + INCOMPLETE = "incomplete" + UNION = "union" + TYPED = "typed" + OPAQUE_STORAGE = "opaque_storage" + + +@dataclass(frozen=True) +class ByValueRecordShape: + """Whether a record can be embedded by value in another typed record.""" + + valid: bool + detail: tuple[str, ...] = () + flexible_tail: FlexibleTail | None = None + + +@dataclass(frozen=True) +class RecordAnalysisFacts: + """Central per-record facts used by struct lowering and later policy passes.""" + + decl_id: str + is_union: bool + is_complete: bool + layout: RecordLayoutFacts + storage_kind: RecordStorageKind + by_value_shape: ByValueRecordShape + flexible_tail: FlexibleTail | None = None + diagnostic_notes: tuple[str, ...] = () + fallback_reasons: tuple[str, ...] = () + + @property + def typed_storage_candidate(self) -> bool: + return self.storage_kind == RecordStorageKind.TYPED + + @property + def opaque_storage_required(self) -> bool: + return self.storage_kind == RecordStorageKind.OPAQUE_STORAGE + + @property + def layout_known(self) -> bool: + return self.layout.is_complete and not self.layout.layout_problems + + @property + def flexible_tail_field_index(self) -> int | None: + if self.flexible_tail is None: + return None + # Kept for compatibility with the first shape-facts API. Prefer + # ``flexible_tail`` for new callers. + return None + + +def analyze_record_shapes( + records_by_decl_id: dict[str, Struct], + record_layouts: dict[str, RecordLayoutFacts], + *, + type_lowerer: LowerTypePass | None = None, +) -> dict[str, RecordAnalysisFacts]: + """Analyze every CIR record's layout shape and typed-storage eligibility.""" + + analyzer = RecordShapeAnalyzer( + records_by_decl_id=records_by_decl_id, + record_layouts=record_layouts, + type_lowerer=type_lowerer or LowerTypePass(), + ) + return { + decl_id: analyzer.analyze_record(record) for decl_id, record in records_by_decl_id.items() + } + + +def analyze_record_shape( + record: Struct, + layout: RecordLayoutFacts, + *, + records_by_decl_id: dict[str, Struct] | None = None, + record_layouts: dict[str, RecordLayoutFacts] | None = None, + type_lowerer: LowerTypePass | None = None, +) -> RecordAnalysisFacts: + """Analyze one CIR record. + + Direct callers can pass only ``record`` and ``layout`` for isolated tests; + recursive by-value checks use the optional maps when available. + """ + + records = records_by_decl_id or {record.decl_id: record} + layouts = record_layouts or {record.decl_id: layout} + return RecordShapeAnalyzer( + records_by_decl_id=records, + record_layouts=layouts, + type_lowerer=type_lowerer or LowerTypePass(), + ).analyze_record(record) + + +@dataclass +class RecordShapeAnalyzer: + """Compute reusable record shape facts for a normalized CIR unit.""" + + records_by_decl_id: dict[str, Struct] + record_layouts: dict[str, RecordLayoutFacts] + type_lowerer: LowerTypePass + + def __post_init__(self) -> None: + self._record_cache: dict[str, RecordAnalysisFacts] = {} + self._by_value_cache: dict[str, ByValueRecordShape] = {} + + def analyze_record(self, record: Struct) -> RecordAnalysisFacts: + cached = self._record_cache.get(record.decl_id) + if cached is not None: + return cached + + layout = self._layout(record) + if record.is_union: + facts = RecordAnalysisFacts( + decl_id=record.decl_id, + is_union=True, + is_complete=record.is_complete, + layout=layout, + storage_kind=RecordStorageKind.UNION, + by_value_shape=ByValueRecordShape(True), + ) + self._record_cache[record.decl_id] = facts + return facts + + if not layout.is_complete: + facts = RecordAnalysisFacts( + decl_id=record.decl_id, + is_union=False, + is_complete=False, + layout=layout, + storage_kind=RecordStorageKind.INCOMPLETE, + by_value_shape=ByValueRecordShape(False, ("referenced definition is incomplete",)), + ) + self._record_cache[record.decl_id] = facts + return facts + + if layout.layout_problems: + facts = RecordAnalysisFacts( + decl_id=record.decl_id, + is_union=False, + is_complete=True, + layout=layout, + storage_kind=RecordStorageKind.OPAQUE_STORAGE, + by_value_shape=ByValueRecordShape(False, (layout.layout_problems[0],)), + fallback_reasons=layout.layout_problems, + ) + self._record_cache[record.decl_id] = facts + return facts + + by_value = self._record_typed_shape_by_value(record.decl_id, active=set()) + storage_kind = ( + RecordStorageKind.TYPED if by_value.valid else RecordStorageKind.OPAQUE_STORAGE + ) + facts = RecordAnalysisFacts( + decl_id=record.decl_id, + is_union=False, + is_complete=True, + layout=layout, + storage_kind=storage_kind, + by_value_shape=by_value, + flexible_tail=by_value.flexible_tail, + fallback_reasons=() if by_value.valid else by_value.detail, + ) + self._record_cache[record.decl_id] = facts + return facts + + def _record_typed_shape_by_value( + self, + decl_id: str, + *, + active: set[str], + ) -> ByValueRecordShape: + cached = self._by_value_cache.get(decl_id) + if cached is not None: + return cached + + decl = self.records_by_decl_id.get(decl_id) + if decl is None: + return self._cache_by_value( + decl_id, ByValueRecordShape(False, ("referenced definition is unavailable",)) + ) + if decl.is_union: + return self._cache_by_value(decl_id, ByValueRecordShape(True)) + if not decl.is_complete: + return self._cache_by_value( + decl_id, ByValueRecordShape(False, ("referenced definition is incomplete",)) + ) + if decl_id in active: + return self._cache_by_value(decl_id, ByValueRecordShape(True)) + + facts = self._layout(decl) + if not facts.is_complete: + return self._cache_by_value( + decl_id, ByValueRecordShape(False, ("referenced definition is incomplete",)) + ) + if facts.layout_problems: + return self._cache_by_value( + decl_id, ByValueRecordShape(False, (facts.layout_problems[0],)) + ) + + active.add(decl_id) + try: + flexible_tail: FlexibleTail | None = None + for field_fact in facts.plain_fields: + field = decl.fields[field_fact.index] + lowered_type, reason = try_lower_type( + self.type_lowerer, + field.type, + subject=f"field `{field_display_name(field, field_fact.index)}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None or lowered_type is None: + return self._cache_by_value( + decl_id, + ByValueRecordShape(False, ((reason or "field could not be lowered"),)), + ) + if isinstance(field.type, AtomicType) and not ( + isinstance(lowered_type, ParametricType) + and lowered_type.base == ParametricBase.ATOMIC + ): + # This remains typed-storage-compatible; struct lowering emits + # the user-facing note while rendering the member. + pass + if field.fam_pattern is not None: + direct_tail, tail_reason = _lower_flexible_tail_metadata( + field, + field_fact.index, + field_fact.byte_offset, + lowered_type, + ) + if tail_reason is not None or direct_tail is None: + return self._cache_by_value( + decl_id, + ByValueRecordShape( + False, ((tail_reason or "flexible tail could not lower"),) + ), + ) + flexible_tail = direct_tail + continue + + refs = _embedded_struct_refs(field.type) + if not refs: + continue + direct_ref = _direct_embedded_struct_ref(field.type) + nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]] = [] + for ref in refs: + shape = self._record_typed_shape_by_value( + ref.decl_id, + active=active, + ) + if not shape.valid: + target_name = ref.c_name or ref.name + suffix = ( + shape.detail[0] + if shape.detail + else "embedded record is not layout-emittable by value" + ) + return self._cache_by_value( + decl_id, + ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds " + f"struct `{target_name}` by value, but {suffix}; opaque storage emitted", + ), + ), + ) + if shape.flexible_tail is not None: + nested_tail_shapes.append((ref, shape)) + if not nested_tail_shapes: + continue + if len(nested_tail_shapes) > 1 or direct_ref is None: + return self._cache_by_value( + decl_id, + ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds a " + "flexible-tail record through a non-direct aggregate type; opaque " + "storage emitted", + ), + ), + ) + nested_ref, nested_shape = nested_tail_shapes[0] + if nested_ref.decl_id != direct_ref.decl_id: + return self._cache_by_value( + decl_id, + ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds a " + "flexible-tail record through a non-direct aggregate type; opaque " + "storage emitted", + ), + ), + ) + if not _field_is_terminal_in_enclosing_record(facts, field_fact): + target_name = nested_ref.c_name or nested_ref.name + return self._cache_by_value( + decl_id, + ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds struct " + f"`{target_name}` by value, but embedded flexible tail is not terminal " + "in the enclosing record; opaque storage emitted", + ), + ), + ) + flexible_tail = replace( + nested_shape.flexible_tail, # type: ignore + byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, # type: ignore + ) + + for run_layout in facts.bitfield_runs: + _, reason = try_lower_type( + self.type_lowerer, + run_layout.unsigned_storage_type, + subject=f"bitfield storage `{run_layout.name}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None: + return self._cache_by_value(decl_id, ByValueRecordShape(False, (reason,))) + + for field_layout in run_layout.fields: + _, reason = try_lower_type( + self.type_lowerer, + field_layout.logical_type, + subject=f"bitfield `{field_display_name(field_layout.field, field_layout.index)}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None: + return self._cache_by_value(decl_id, ByValueRecordShape(False, (reason,))) + finally: + active.remove(decl_id) + + return self._cache_by_value(decl_id, ByValueRecordShape(True, flexible_tail=flexible_tail)) + + def _layout(self, record: Struct) -> RecordLayoutFacts: + return self.record_layouts[record.decl_id] + + def _cache_by_value( + self, + decl_id: str, + shape: ByValueRecordShape, + ) -> ByValueRecordShape: + self._by_value_cache[decl_id] = shape + return shape + + +def _lower_flexible_tail_metadata( + field, + index: int, + byte_offset: int, + lowered_type, +) -> tuple[FlexibleTail | None, str | None]: + if not isinstance(field.type, Array): + return None, "flexible tail field did not lower from an array type; opaque storage emitted" + if not isinstance(lowered_type, Array) or lowered_type.size != 0: + return ( + None, + f"field `{field_display_name(field, index)}` did not lower to InlineArray[..., 0]; " + "opaque storage emitted", + ) + return ( + FlexibleTail( + field_name=field.name, + element_type=lowered_type.element, + pattern=field.fam_pattern, + byte_offset=byte_offset, + ), + None, + ) + + +def _direct_embedded_struct_ref(t: Type) -> StructRef | None: + while True: + if isinstance(t, QualifiedType): + t = t.unqualified + continue + if isinstance(t, TypeRef): + t = t.canonical + continue + break + if isinstance(t, StructRef) and not t.is_union: + return t + return None + + +def _field_is_terminal_in_enclosing_record( + facts: RecordLayoutFacts, + field_fact, +) -> bool: + field_end = field_fact.byte_offset + field_fact.size_bytes + if any( + other.byte_offset >= field_end + for other in facts.plain_fields + if other.index != field_fact.index + ): + return False + if any(run.byte_offset >= field_end for run in facts.bitfield_runs): + return False + return True + + +def _embedded_struct_refs(t: Type) -> tuple[StructRef, ...]: + return tuple( + node + for node in collect_type_nodes( + t, + lambda node: isinstance(node, StructRef) and not node.is_union, + options=TypeWalkOptions( + descend_pointer=False, + descend_function_ptr=False, + descend_vector_element=True, + ), + ) + if isinstance(node, StructRef) + ) + + +# Compatibility aliases for the initial lightweight shape API. +RecordShapeFacts = RecordAnalysisFacts + + +__all__ = [ + "ByValueRecordShape", + "RecordAnalysisFacts", + "RecordShapeFacts", + "RecordShapeAnalyzer", + "RecordStorageKind", + "analyze_record_shape", + "analyze_record_shapes", +] diff --git a/mojo_bindgen/analysis/reference_validation.py b/mojo_bindgen/analysis/reference_validation.py new file mode 100644 index 0000000..a88bcd7 --- /dev/null +++ b/mojo_bindgen/analysis/reference_validation.py @@ -0,0 +1,143 @@ +"""Cross-declaration reference validation for normalized CIR.""" + +from __future__ import annotations + +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes +from mojo_bindgen.ir import ( + BinaryExpr, + CastExpr, + Const, + ConstExpr, + Enum, + EnumRef, + Function, + GlobalVar, + MacroDecl, + OpaqueRecordRef, + SizeOfExpr, + Struct, + StructRef, + Type, + Typedef, + TypeRef, + UnaryExpr, + Unit, +) + + +class ReferenceValidationError(ValueError): + """Raised when normalized CIR contains an invalid internal reference.""" + + +class ValidateReferencesPass: + """Validate cross-declaration references that must resolve inside the ``Unit``. + + Missing ``TypeRef`` declarations are treated as external typedef uses because + ``LowerUnitPass`` can synthesize aliases for those. ``OpaqueRecordRef`` is + also allowed to remain external. Concrete non-union ``StructRef`` and all + ``EnumRef`` nodes must resolve after signature-stub materialization and enum + canonicalization. + """ + + def run(self, unit: Unit) -> Unit: + struct_ids = {decl.decl_id for decl in unit.decls if isinstance(decl, Struct)} + enum_ids = {decl.decl_id for decl in unit.decls if isinstance(decl, Enum)} + + for decl in unit.decls: + for t in _decl_types(decl): + self._validate_type( + t, + struct_ids=struct_ids, + enum_ids=enum_ids, + ) + for expr in _decl_exprs(decl): + for t in _const_expr_types(expr): + self._validate_type( + t, + struct_ids=struct_ids, + enum_ids=enum_ids, + ) + return unit + + def _validate_type( + self, + t: Type, + *, + struct_ids: set[str], + enum_ids: set[str], + ) -> None: + for node in iter_type_nodes( + t, + options=TypeWalkOptions(descend_vector_element=True), + ): + if isinstance(node, TypeRef): + if not node.decl_id: + raise ReferenceValidationError(f"TypeRef {node.name!r} is missing decl_id") + # External typedef refs are supported by synthetic alias lowering. + continue + if isinstance(node, StructRef): + if node.decl_id not in struct_ids and not node.is_union: + raise ReferenceValidationError( + f"StructRef {node.name!r} points to missing decl_id {node.decl_id!r}" + ) + continue + if isinstance(node, EnumRef) and node.decl_id not in enum_ids: + raise ReferenceValidationError( + f"EnumRef {node.name!r} points to missing decl_id {node.decl_id!r}" + ) + if isinstance(node, OpaqueRecordRef): + continue + + +def _decl_types(decl) -> tuple[Type, ...]: + if isinstance(decl, Function): + return (decl.ret, *(param.type for param in decl.params)) + if isinstance(decl, Typedef): + return (decl.aliased, decl.canonical) + if isinstance(decl, Struct): + return tuple(field.type for field in decl.fields) + if isinstance(decl, GlobalVar): + return (decl.type,) + if isinstance(decl, Const): + return (decl.type,) + if isinstance(decl, MacroDecl): + return () if decl.type is None else (decl.type,) + return () + + +def _decl_exprs(decl) -> tuple[ConstExpr, ...]: + if isinstance(decl, Const): + return (decl.expr,) + if isinstance(decl, GlobalVar) and decl.initializer is not None: + return (decl.initializer,) + if isinstance(decl, MacroDecl) and decl.expr is not None: + return (decl.expr,) + return () + + +def _const_expr_types(expr: ConstExpr) -> tuple[Type, ...]: + out: list[Type] = [] + + def walk(node: ConstExpr) -> None: + if isinstance(node, CastExpr): + out.append(node.target) + walk(node.expr) + return + if isinstance(node, SizeOfExpr): + out.append(node.target) + return + if isinstance(node, UnaryExpr): + walk(node.operand) + return + if isinstance(node, BinaryExpr): + walk(node.lhs) + walk(node.rhs) + + walk(expr) + return tuple(out) + + +__all__ = [ + "ReferenceValidationError", + "ValidateReferencesPass", +] diff --git a/mojo_bindgen/analysis/struct_lowering.py b/mojo_bindgen/analysis/struct_lowering.py index ef6658d..fb8ecdc 100644 --- a/mojo_bindgen/analysis/struct_lowering.py +++ b/mojo_bindgen/analysis/struct_lowering.py @@ -2,7 +2,7 @@ from __future__ import annotations -from dataclasses import dataclass, field, replace +from dataclasses import dataclass, field from mojo_bindgen.analysis.common import _mojo_align_decorator_ok from mojo_bindgen.analysis.lowering_support import ( @@ -13,29 +13,27 @@ try_lower_type, ) from mojo_bindgen.analysis.record_layout import RecordLayoutFacts, analyze_record_layout +from mojo_bindgen.analysis.record_shape import ( + RecordAnalysisFacts, + RecordStorageKind, + analyze_record_shapes, +) from mojo_bindgen.analysis.type_lowering import LowerTypePass -from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( - Array, AtomicType, BitfieldField, BitfieldGroupMember, - FlexibleTail, Initializer, InitializerParam, OpaqueStorageMember, PaddingMember, ParametricBase, ParametricType, - QualifiedType, StoredMember, Struct, StructDecl, StructKind, - StructRef, TargetABI, - Type, - TypeRef, ) @@ -48,14 +46,8 @@ class StructLoweringContext: record_map: dict[str, Struct] target_abi: TargetABI type_lowerer: LowerTypePass - by_value_typed_shape_cache: dict[str, _TypedRecordShape] = field(default_factory=dict) - - -@dataclass(frozen=True) -class _TypedRecordShape: - valid: bool - detail: tuple[str, ...] = () - flexible_tail: FlexibleTail | None = None + record_layouts: dict[str, RecordLayoutFacts] = field(default_factory=dict) + record_shapes: dict[str, RecordAnalysisFacts] = field(default_factory=dict) def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: @@ -66,16 +58,19 @@ def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: f"expected non-union Struct declaration, got union {decl.decl_id!r}" ) - facts = analyze_record_layout( - decl, - record_map=context.record_map, - target_abi=context.target_abi, - ) - if not facts.is_complete: + shape = _record_shape(decl, context=context) + facts = shape.layout + if shape.storage_kind == RecordStorageKind.INCOMPLETE: return _incomplete_struct_decl(decl) - if facts.layout_problems: - return _opaque_storage_struct_decl(decl, facts) - plain_fields, lowered_bitfield_groups, flexible_tail, diagnostic_notes, fallback_reasons = ( + if shape.storage_kind == RecordStorageKind.OPAQUE_STORAGE: + return _opaque_storage_struct_decl( + decl, + facts, + diagnostic_notes=shape.diagnostic_notes, + fallback_reasons=shape.fallback_reasons, + ) + + plain_fields, lowered_bitfield_groups, diagnostic_notes, fallback_reasons = ( _lower_typed_members( decl, facts, @@ -104,7 +99,7 @@ def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: lowered_bitfield_groups, uses_opaque_storage=False, ), - flexible_tail=flexible_tail, + flexible_tail=shape.flexible_tail, diagnostics=_build_diagnostics( facts.layout_problems, diagnostic_notes=diagnostic_notes, @@ -122,13 +117,11 @@ def _lower_typed_members( ) -> tuple[ list[StoredMember], list[BitfieldGroupMember], - FlexibleTail | None, tuple[str, ...], tuple[str, ...], ]: plain_fields: list[StoredMember] = [] lowered_bitfield_groups: list[BitfieldGroupMember] = [] - flexible_tail: FlexibleTail | None = None diagnostic_notes: list[str] = [] fallback_reasons: list[str] = [] @@ -164,70 +157,8 @@ def _lower_typed_members( ) ) if field.fam_pattern is not None: - flexible_tail, reason = _lower_flexible_tail_metadata( - field, - field_fact.index, - field_fact.byte_offset, - lowered_type, - ) - if reason is not None: - fallback_reasons.append(reason) - flexible_tail = None continue - refs = _embedded_struct_refs(field.type) - if not refs: - continue - direct_ref = _direct_embedded_struct_ref(field.type) - nested_tail_shapes: list[tuple[StructRef, _TypedRecordShape]] = [] - for ref in refs: - shape = _record_typed_shape_by_value( - ref.decl_id, - context=context, - active={decl.decl_id}, - ) - if not shape.valid: - target_name = ref.c_name or ref.name - suffix = ( - shape.detail[0] - if shape.detail - else "embedded record is not layout-emittable by value" - ) - fallback_reasons.append( - f"field `{display_name}` embeds struct `{target_name}` by value, but {suffix}; " - "opaque storage emitted" - ) - break - if shape.flexible_tail is not None: - nested_tail_shapes.append((ref, shape)) - else: - if not nested_tail_shapes: - continue - if len(nested_tail_shapes) > 1 or direct_ref is None: - fallback_reasons.append( - f"field `{display_name}` embeds a flexible-tail record through a non-direct " - "aggregate type; opaque storage emitted" - ) - continue - nested_ref, nested_shape = nested_tail_shapes[0] - if nested_ref.decl_id != direct_ref.decl_id: - fallback_reasons.append( - f"field `{display_name}` embeds a flexible-tail record through a non-direct " - "aggregate type; opaque storage emitted" - ) - continue - if not _field_is_terminal_in_enclosing_record(facts, field_fact): - target_name = nested_ref.c_name or nested_ref.name - fallback_reasons.append( - f"field `{display_name}` embeds struct `{target_name}` by value, but embedded " - "flexible tail is not terminal in the enclosing record; opaque storage emitted" - ) - continue - flexible_tail = replace( - nested_shape.flexible_tail, # type: ignore - byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, # type: ignore - ) - for run_layout in facts.bitfield_runs: lowered_storage_type, reason = try_lower_type( context.type_lowerer, @@ -280,231 +211,35 @@ def _lower_typed_members( return ( plain_fields, lowered_bitfield_groups, - flexible_tail, tuple(diagnostic_notes), tuple(fallback_reasons), ) -def _lower_flexible_tail_metadata( - field, - index: int, - byte_offset: int, - lowered_type, -) -> tuple[FlexibleTail | None, str | None]: - if not isinstance(field.type, Array): - return None, "flexible tail field did not lower from an array type; opaque storage emitted" - if not isinstance(lowered_type, Array) or lowered_type.size != 0: - return ( - None, - f"field `{field_display_name(field, index)}` did not lower to InlineArray[..., 0]; " - "opaque storage emitted", - ) - return ( - FlexibleTail( - field_name=field.name, - element_type=lowered_type.element, - pattern=field.fam_pattern, - byte_offset=byte_offset, - ), - None, - ) - - -def _record_typed_shape_by_value( - decl_id: str, +def _record_shape( + decl: Struct, *, context: StructLoweringContext, - active: set[str], -) -> _TypedRecordShape: - cached = context.by_value_typed_shape_cache.get(decl_id) +) -> RecordAnalysisFacts: + cached = context.record_shapes.get(decl.decl_id) if cached is not None: return cached - decl = context.record_map.get(decl_id) - if decl is None: - result = _TypedRecordShape(False, ("referenced definition is unavailable",)) - context.by_value_typed_shape_cache[decl_id] = result - return result - if decl.is_union: - result = _TypedRecordShape(True) - context.by_value_typed_shape_cache[decl_id] = result - return result - if not decl.is_complete: - result = _TypedRecordShape(False, ("referenced definition is incomplete",)) - context.by_value_typed_shape_cache[decl_id] = result - return result - if decl_id in active: - result = _TypedRecordShape(True) - context.by_value_typed_shape_cache[decl_id] = result - return result - - facts = analyze_record_layout( - decl, - record_map=context.record_map, - target_abi=context.target_abi, - ) - if not facts.is_complete: - result = _TypedRecordShape(False, ("referenced definition is incomplete",)) - context.by_value_typed_shape_cache[decl_id] = result - return result - if facts.layout_problems: - result = _TypedRecordShape(False, (facts.layout_problems[0],)) - context.by_value_typed_shape_cache[decl_id] = result - return result - - active.add(decl_id) - try: - flexible_tail: FlexibleTail | None = None - for field_fact in facts.plain_fields: - field = decl.fields[field_fact.index] - lowered_type, reason = try_lower_type( - context.type_lowerer, - field.type, - subject=f"field `{field_display_name(field, field_fact.index)}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None or lowered_type is None: - result = _TypedRecordShape(False, ((reason or "field could not be lowered"),)) - context.by_value_typed_shape_cache[decl_id] = result - return result - if field.fam_pattern is not None: - direct_tail, tail_reason = _lower_flexible_tail_metadata( - field, - field_fact.index, - field_fact.byte_offset, - lowered_type, - ) - if tail_reason is not None or direct_tail is None: - result = _TypedRecordShape( - False, ((tail_reason or "flexible tail could not lower"),) - ) - context.by_value_typed_shape_cache[decl_id] = result - return result - flexible_tail = direct_tail - continue - - refs = _embedded_struct_refs(field.type) - if not refs: - continue - direct_ref = _direct_embedded_struct_ref(field.type) - nested_tail_shapes: list[tuple[StructRef, _TypedRecordShape]] = [] - for ref in refs: - shape = _record_typed_shape_by_value( - ref.decl_id, - context=context, - active=active, - ) - if not shape.valid: - result = _TypedRecordShape(False, shape.detail) - context.by_value_typed_shape_cache[decl_id] = result - return result - if shape.flexible_tail is not None: - nested_tail_shapes.append((ref, shape)) - if not nested_tail_shapes: - continue - if len(nested_tail_shapes) > 1 or direct_ref is None: - result = _TypedRecordShape( - False, - ("embedded flexible tail is not a direct terminal by-value struct field",), - ) - context.by_value_typed_shape_cache[decl_id] = result - return result - nested_ref, nested_shape = nested_tail_shapes[0] - if nested_ref.decl_id != direct_ref.decl_id: - result = _TypedRecordShape( - False, - ("embedded flexible tail is not a direct terminal by-value struct field",), - ) - context.by_value_typed_shape_cache[decl_id] = result - return result - if not _field_is_terminal_in_enclosing_record(facts, field_fact): - result = _TypedRecordShape( - False, - ("embedded flexible tail is not terminal in the enclosing record",), - ) - context.by_value_typed_shape_cache[decl_id] = result - return result - flexible_tail = replace( - nested_shape.flexible_tail, # type: ignore - byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, # type: ignore + layouts = dict(context.record_layouts) + for record in context.record_map.values(): + if record.decl_id not in layouts: + layouts[record.decl_id] = analyze_record_layout( + record, + record_map=context.record_map, + target_abi=context.target_abi, ) - - for run_layout in facts.bitfield_runs: - _, reason = try_lower_type( - context.type_lowerer, - run_layout.unsigned_storage_type, - subject=f"bitfield storage `{run_layout.name}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None: - result = _TypedRecordShape(False, (reason,)) - context.by_value_typed_shape_cache[decl_id] = result - return result - - for field_layout in run_layout.fields: - _, reason = try_lower_type( - context.type_lowerer, - field_layout.logical_type, - subject=f"bitfield `{field_display_name(field_layout.field, field_layout.index)}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None: - result = _TypedRecordShape(False, (reason,)) - context.by_value_typed_shape_cache[decl_id] = result - return result - finally: - active.remove(decl_id) - - result = _TypedRecordShape(True, flexible_tail=flexible_tail) - context.by_value_typed_shape_cache[decl_id] = result - return result - - -def _direct_embedded_struct_ref(t: Type) -> StructRef | None: - while True: - if isinstance(t, QualifiedType): - t = t.unqualified - continue - if isinstance(t, TypeRef): - t = t.canonical - continue - break - if isinstance(t, StructRef) and not t.is_union: - return t - return None - - -def _field_is_terminal_in_enclosing_record( - facts: RecordLayoutFacts, - field_fact, -) -> bool: - field_end = field_fact.byte_offset + field_fact.size_bytes - if any( - other.byte_offset >= field_end - for other in facts.plain_fields - if other.index != field_fact.index - ): - return False - if any(run.byte_offset >= field_end for run in facts.bitfield_runs): - return False - return True - - -def _embedded_struct_refs(t: Type) -> tuple[StructRef, ...]: - return tuple( - node - for node in collect_type_nodes( - t, - lambda node: isinstance(node, StructRef) and not node.is_union, - options=TypeWalkOptions( - descend_pointer=False, - descend_function_ptr=False, - descend_vector_element=True, - ), - ) - if isinstance(node, StructRef) + context.record_layouts = layouts + context.record_shapes = analyze_record_shapes( + context.record_map, + layouts, + type_lowerer=context.type_lowerer, ) + return context.record_shapes[decl.decl_id] def _build_members( diff --git a/mojo_bindgen/analysis/unit_lowering.py b/mojo_bindgen/analysis/unit_lowering.py index e21b9ad..b0e9f15 100644 --- a/mojo_bindgen/analysis/unit_lowering.py +++ b/mojo_bindgen/analysis/unit_lowering.py @@ -11,14 +11,12 @@ from mojo_bindgen.analysis.const_lowering import ( LowerConstExprPass, ) +from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context from mojo_bindgen.analysis.decl_lowerer import ( LoweringSession, UnitDeclLowerer, _link_mode_for_options, ) -from mojo_bindgen.analysis.lowering_support import ( - record_by_decl_id, -) from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.struct_lowering import ( StructLoweringContext, @@ -50,19 +48,29 @@ class LowerUnitPass: """Lower an already-normalized CIR ``Unit`` into a MojoIR ``MojoModule``.""" - def __init__(self, options: MojoEmitOptions | None = None) -> None: + def __init__( + self, + options: MojoEmitOptions | None = None, + *, + context: AnalysisContext | None = None, + ) -> None: self._options = options or MojoEmitOptions() + self._context = context def run(self, unit: Unit) -> MojoModule: + context = self._context or build_analysis_context(unit) + if context.unit is not unit: + context = build_analysis_context(unit) type_lowerer = LowerTypePass() - record_map = record_by_decl_id(unit) session = LoweringSession( unit=unit, options=self._options, type_lowerer=type_lowerer, const_lowerer=LowerConstExprPass(type_lowering=type_lowerer), struct_context=StructLoweringContext( - record_map=record_map, + record_map=context.records_by_decl_id, + record_layouts=context.record_layouts, + record_shapes=context.record_shapes, target_abi=unit.target_abi, type_lowerer=type_lowerer, ), @@ -172,10 +180,15 @@ def _collect_typeref_uses(unit: Unit) -> list[TypeRef]: return collected -def lower_unit(unit: Unit, *, options: MojoEmitOptions | None = None) -> MojoModule: +def lower_unit( + unit: Unit, + *, + options: MojoEmitOptions | None = None, + context: AnalysisContext | None = None, +) -> MojoModule: """Lower one CIR ``Unit`` into a standalone ``MojoModule``.""" - return LowerUnitPass(options=options).run(unit) + return LowerUnitPass(options=options, context=context).run(unit) __all__ = [ diff --git a/mojo_bindgen/analysis/validate_ir.py b/mojo_bindgen/analysis/validate_ir.py index 3d34151..d1f779b 100644 --- a/mojo_bindgen/analysis/validate_ir.py +++ b/mojo_bindgen/analysis/validate_ir.py @@ -22,7 +22,15 @@ class IRValidationError(ValueError): class ValidateIRPass: - """Validate basic declaration identity and type-reference invariants.""" + """Validate structural invariants required by downstream analysis. + + The pass is intentionally fail-fast and non-mutating. It guarantees stable + declaration identities for declarations that participate in cross-reference + analysis, and it verifies nested record/type references carry the identity + fields needed by canonicalization, reachability, layout, and lowering. It + does not prove ABI correctness or Mojo representability; later layout and + lowering passes own those decisions. + """ def run(self, unit: Unit) -> Unit: decl_ids: dict[str, object] = {} diff --git a/pixi.toml b/pixi.toml index c581ee7..dad516e 100644 --- a/pixi.toml +++ b/pixi.toml @@ -26,6 +26,7 @@ example-zlib = "bash examples/zlib/generate.sh" example-cairo = "bash examples/cairo/generate.sh" example-libpng = "bash examples/libpng/generate.sh" example-sqlite = "bash examples/sqlite/generate.sh" +example-global-consts = "bash examples/global_consts/generate.sh" [dependencies] mojo = ">=1.0.0b1,<2" @@ -65,4 +66,3 @@ build = "python -m build" build-wheel = "python -m build --wheel" build-sdist = "python -m build --sdist" clean-dist = "python -c \"import shutil; from pathlib import Path; [shutil.rmtree(p, ignore_errors=True) for p in (Path('build'), Path('dist'))]; [shutil.rmtree(p, ignore_errors=True) if p.is_dir() else p.unlink(missing_ok=True) for p in Path('.').glob('*.egg-info')]\"" - diff --git a/tests/unit/passes/test_pipeline.py b/tests/unit/passes/test_pipeline.py index e4a7ba1..4abed8a 100644 --- a/tests/unit/passes/test_pipeline.py +++ b/tests/unit/passes/test_pipeline.py @@ -2,7 +2,7 @@ import pytest -from mojo_bindgen.analysis import lower_unit, run_ir_passes +from mojo_bindgen.analysis import AnalysisOrchestrator, lower_unit, run_ir_passes from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.validate_ir import IRValidationError, ValidateIRPass from mojo_bindgen.ir import ( @@ -69,11 +69,13 @@ def test_run_ir_passes_validates_already_normalized_ir() -> None: ) normalized = run_ir_passes(unit) - assert normalized is unit field_type = normalized.decls[1].fields[0].type assert isinstance(field_type, TypeRef) assert field_type.name == "my_int" assert isinstance(field_type.canonical, IntType) + original_field_type = unit.decls[1].fields[0].type + assert isinstance(original_field_type, TypeRef) + assert original_field_type.name == "my_int" def test_validate_ir_pass_rejects_duplicate_decl_ids() -> None: @@ -94,6 +96,77 @@ def test_validate_ir_pass_rejects_duplicate_decl_ids() -> None: assert "duplicate decl_id" in str(exc_info.value) +def test_run_ir_passes_returns_fresh_unit_without_mutating_raw_unit() -> None: + i32 = _i32() + enum_ref = EnumRef( + decl_id="enum:mode", + name="mode", + c_name="mode", + underlying=i32, + ) + unit = Unit( + source_header="t.h", + library="t", + link_name="t", + target_abi=_abi(), + decls=[ + Enum( + decl_id="enum:mode", + name="mode", + c_name="mode", + underlying=i32, + enumerants=[], + ), + Typedef( + decl_id="typedef:mode_t", + name="mode_t", + aliased=enum_ref, + canonical=enum_ref, + ), + ], + ) + + normalized = run_ir_passes(unit) + + assert normalized is not unit + raw_enum = unit.decls[0] + normalized_enum = normalized.decls[0] + assert isinstance(raw_enum, Enum) + assert isinstance(normalized_enum, Enum) + assert raw_enum.name == "mode" + assert normalized_enum.name == "mode_t" + + +def test_analysis_pipeline_exposes_context_and_policy_light_module() -> None: + i32 = _i32() + struct = Struct( + decl_id="struct:holder", + name="holder", + c_name="holder", + fields=[Field(name="x", source_name="x", type=i32, byte_offset=0, size_bytes=4)], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + unit = Unit( + source_header="t.h", + library="t", + link_name="t", + target_abi=_abi(), + decls=[struct], + ) + + artifacts = AnalysisOrchestrator(MojoEmitOptions()).analyze_pipeline(unit) + + assert artifacts.raw_unit is unit + assert artifacts.normalized_unit is not unit + assert artifacts.context.unit is artifacts.normalized_unit + assert struct.decl_id in artifacts.context.records_by_decl_id + assert struct.decl_id in artifacts.context.record_layouts + assert artifacts.policy_light_module.decls + assert artifacts.mojo_module.decls + + def test_run_ir_passes_and_lower_unit_preserve_typedef_surface_name() -> None: i32 = _i32() td = Typedef(decl_id="typedef:my_int", name="my_int", aliased=i32, canonical=i32) diff --git a/tests/unit/test_analysis_context.py b/tests/unit/test_analysis_context.py new file mode 100644 index 0000000..6f43dfa --- /dev/null +++ b/tests/unit/test_analysis_context.py @@ -0,0 +1,169 @@ +from __future__ import annotations + +import pytest + +from mojo_bindgen.analysis import ( + AliasClass, + ReferenceValidationError, + ValidateReferencesPass, + build_analysis_context, +) +from mojo_bindgen.ir import ( + ByteOrder, + EnumRef, + Field, + Function, + FunctionPtr, + IntKind, + IntType, + MacroDecl, + Param, + RefExpr, + Struct, + StructRef, + TargetABI, + Typedef, + TypeRef, + Unit, + VoidType, +) + + +def _abi() -> TargetABI: + return TargetABI( + pointer_size_bytes=8, + pointer_align_bytes=8, + byte_order=ByteOrder.LITTLE, + ) + + +def _i32() -> IntType: + return IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4) + + +def test_analysis_context_classifies_aliases_and_builds_dependency_graph() -> None: + i32 = _i32() + callback = Typedef( + decl_id="typedef:callback_t", + name="callback_t", + aliased=FunctionPtr(ret=VoidType(), params=[Param(name="value", type=i32)]), + canonical=FunctionPtr(ret=VoidType(), params=[Param(name="value", type=i32)]), + ) + payload = Struct( + decl_id="struct:Payload", + name="Payload", + c_name="Payload", + fields=[Field(name="value", source_name="value", type=i32, byte_offset=0, size_bytes=4)], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + external_ref = TypeRef( + decl_id="typedef:external_size", + name="external_size", + canonical=i32, + ) + function = Function( + decl_id="fn:take", + name="take", + link_name="take", + ret=VoidType(), + params=[ + Param( + name="payload", + type=StructRef( + decl_id=payload.decl_id, + name=payload.name, + c_name=payload.c_name, + size_bytes=payload.size_bytes, + align_bytes=payload.align_bytes, + ), + ), + Param(name="size", type=external_ref), + ], + ) + unit = Unit( + source_header="demo.h", + library="demo", + link_name="demo", + target_abi=_abi(), + decls=[callback, payload, function], + ) + + context = build_analysis_context(unit) + + assert context.alias_classification.aliases_by_decl_id[callback.decl_id].alias_class == ( + AliasClass.CALLBACK + ) + assert ( + context.alias_classification.external_aliases_by_decl_id[external_ref.decl_id].alias_class + == AliasClass.EXTERNAL_TYPEDEF + ) + assert context.dependency_graph.edges_by_decl_id[function.decl_id] == frozenset( + {payload.decl_id, external_ref.decl_id} + ) + assert context.record_shapes[payload.decl_id].typed_storage_candidate is True + + +def test_validate_references_rejects_missing_enum_refs() -> None: + missing_enum = EnumRef( + decl_id="enum:Missing", + name="Missing", + c_name="Missing", + underlying=_i32(), + ) + unit = Unit( + source_header="demo.h", + library="demo", + link_name="demo", + target_abi=_abi(), + decls=[ + Function( + decl_id="fn:get", + name="get", + link_name="get", + ret=missing_enum, + params=[], + ) + ], + ) + + with pytest.raises(ReferenceValidationError, match="EnumRef"): + ValidateReferencesPass().run(unit) + + +def test_dependency_graph_tracks_const_symbol_edges() -> None: + unit = Unit( + source_header="demo.h", + library="demo", + link_name="demo", + target_abi=_abi(), + decls=[ + Typedef( + decl_id="typedef:my_int", + name="my_int", + aliased=_i32(), + canonical=_i32(), + ), + Function( + decl_id="fn:f", + name="f", + link_name="f", + ret=VoidType(), + params=[Param(name="x", type=_i32())], + ), + ], + ) + unit.decls.append( + MacroDecl( + name="ALIAS", + tokens=["OTHER"], + kind="object_like_supported", + expr=RefExpr("OTHER"), + type=_i32(), + ) + ) + + context = build_analysis_context(unit) + + assert context.dependency_graph.symbol_edges_by_decl_id["ALIAS"] == frozenset({"OTHER"}) diff --git a/tests/unit/test_analysis_record_shape.py b/tests/unit/test_analysis_record_shape.py new file mode 100644 index 0000000..51ec35d --- /dev/null +++ b/tests/unit/test_analysis_record_shape.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from mojo_bindgen.analysis.record_layout import analyze_record_layout +from mojo_bindgen.analysis.record_shape import ( + RecordStorageKind, + analyze_record_shape, + analyze_record_shapes, +) +from mojo_bindgen.ir import ( + Array, + BuiltinType, + ByteOrder, + Field, + IntKind, + IntType, + MojoBuiltin, + Struct, + StructRef, + TargetABI, +) + + +def _abi() -> TargetABI: + return TargetABI( + pointer_size_bytes=8, + pointer_align_bytes=8, + byte_order=ByteOrder.LITTLE, + ) + + +def _i32() -> IntType: + return IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4) + + +def _u8() -> IntType: + return IntType(int_kind=IntKind.UCHAR, size_bytes=1, align_bytes=1) + + +def _layout(record: Struct, records: dict[str, Struct] | None = None): + record_map = records or {record.decl_id: record} + return analyze_record_layout(record, record_map=record_map, target_abi=_abi()) + + +def test_record_shape_marks_incomplete_records_as_incomplete_storage() -> None: + opaque = Struct( + decl_id="struct:Opaque", + name="Opaque", + c_name="Opaque", + fields=[], + size_bytes=0, + align_bytes=1, + is_complete=False, + ) + + facts = analyze_record_shape(opaque, _layout(opaque)) + + assert facts.storage_kind == RecordStorageKind.INCOMPLETE + assert facts.typed_storage_candidate is False + + +def test_record_shape_marks_layout_problems_as_opaque_storage() -> None: + broken = Struct( + decl_id="struct:Broken", + name="Broken", + c_name="Broken", + fields=[Field(name="x", source_name="x", type=_i32(), byte_offset=1, size_bytes=4)], + size_bytes=8, + align_bytes=4, + is_complete=True, + ) + + facts = analyze_record_shape(broken, _layout(broken)) + + assert facts.storage_kind == RecordStorageKind.OPAQUE_STORAGE + assert facts.fallback_reasons + + +def test_record_shape_extracts_direct_flexible_tail_metadata() -> None: + packet = Struct( + decl_id="struct:Packet", + name="Packet", + c_name="Packet", + fields=[ + Field(name="tag", source_name="tag", type=_i32(), byte_offset=0, size_bytes=4), + Field( + name="payload", + source_name="payload", + type=Array( + element=_u8(), + size=None, + array_kind="flexible", + size_bytes=0, + align_bytes=1, + ), + byte_offset=4, + size_bytes=0, + fam_pattern="c99_empty", + ), + ], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + + facts = analyze_record_shape(packet, _layout(packet)) + + assert facts.storage_kind == RecordStorageKind.TYPED + assert facts.flexible_tail is not None + assert facts.flexible_tail.field_name == "payload" + assert facts.flexible_tail.element_type == BuiltinType(MojoBuiltin.C_UCHAR) + + +def test_record_shape_rejects_non_terminal_embedded_flexible_tail() -> None: + tail = Struct( + decl_id="struct:Tail", + name="Tail", + c_name="Tail", + fields=[ + Field( + name="payload", + source_name="payload", + type=Array( + element=_u8(), + size=None, + array_kind="flexible", + size_bytes=0, + align_bytes=1, + ), + byte_offset=0, + size_bytes=0, + fam_pattern="c99_empty", + ) + ], + size_bytes=0, + align_bytes=1, + is_complete=True, + ) + holder = Struct( + decl_id="struct:Holder", + name="Holder", + c_name="Holder", + fields=[ + Field( + name="tail", + source_name="tail", + type=StructRef( + decl_id=tail.decl_id, + name=tail.name, + c_name=tail.c_name, + size_bytes=tail.size_bytes, + align_bytes=tail.align_bytes, + ), + byte_offset=0, + size_bytes=0, + ), + Field(name="after", source_name="after", type=_i32(), byte_offset=0, size_bytes=4), + ], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + records = {tail.decl_id: tail, holder.decl_id: holder} + layouts = {decl_id: _layout(record, records) for decl_id, record in records.items()} + + facts = analyze_record_shapes(records, layouts)[holder.decl_id] + + assert facts.storage_kind == RecordStorageKind.OPAQUE_STORAGE + assert "embedded flexible tail is not terminal" in facts.fallback_reasons[0] diff --git a/tests/unit/test_analysis_struct_lowering.py b/tests/unit/test_analysis_struct_lowering.py index b724b3f..48a9a87 100644 --- a/tests/unit/test_analysis_struct_lowering.py +++ b/tests/unit/test_analysis_struct_lowering.py @@ -2,6 +2,7 @@ from __future__ import annotations +from mojo_bindgen.analysis.record_layout import PlainFieldFact, RecordLayoutFacts from mojo_bindgen.analysis.struct_lowering import StructLoweringContext, lower_struct from mojo_bindgen.analysis.type_lowering import LowerTypePass from mojo_bindgen.ir import ( @@ -85,6 +86,46 @@ def _context_for(*decls: Struct) -> StructLoweringContext: ) +def test_lower_struct_consumes_cached_record_layout_facts(monkeypatch) -> None: + decl = Struct( + decl_id="struct:Widget", + name="Widget", + c_name="Widget", + fields=[_field(name="value", source_name="value", type=_i32(), byte_offset=0)], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + + def fail_if_recomputed(*args, **kwargs): + raise AssertionError("record layout should come from StructLoweringContext") + + monkeypatch.setattr( + "mojo_bindgen.analysis.struct_lowering.analyze_record_layout", + fail_if_recomputed, + ) + context = StructLoweringContext( + record_map={decl.decl_id: decl}, + record_layouts={ + decl.decl_id: RecordLayoutFacts( + is_complete=True, + size_bytes=4, + align_bytes=4, + natural_typed_align_bytes=4, + plain_fields=(PlainFieldFact(index=0, byte_offset=0, size_bytes=4, align_bytes=4),), + ) + }, + target_abi=_abi(), + type_lowerer=LowerTypePass(), + ) + + lowered = lower_struct(decl, context=context) + + assert lowered.members == [ + StoredMember(index=0, name="value", type=BuiltinType(MojoBuiltin.C_INT), byte_offset=0) + ] + + def test_lower_struct_keeps_incomplete_records_opaque_with_explicit_align_none() -> None: decl = Struct( decl_id="struct:Opaque", diff --git a/tests/unit/test_cir_canonicalizer.py b/tests/unit/test_cir_canonicalizer.py index 00644dd..9a04c43 100644 --- a/tests/unit/test_cir_canonicalizer.py +++ b/tests/unit/test_cir_canonicalizer.py @@ -6,6 +6,7 @@ from mojo_bindgen.ir import ( ByteOrder, Const, + Field, Function, IntKind, IntLiteral, @@ -13,6 +14,7 @@ MacroDecl, Param, RefExpr, + Struct, TargetABI, Unit, VoidType, @@ -118,3 +120,35 @@ def test_canonicalizer_drops_self_alias_macro_when_const_claims_name() -> None: out = CIRCanonicalizer().canonicalize(unit) assert [type(decl).__name__ for decl in out.decls] == ["Const"] + + +def test_canonicalizer_prefers_complete_record_without_mutating_input_unit() -> None: + incomplete = Struct( + decl_id="struct:Widget", + name="Widget", + c_name="Widget", + fields=[], + size_bytes=0, + align_bytes=1, + is_complete=False, + ) + complete = Struct( + decl_id="struct:Widget", + name="Widget", + c_name="Widget", + fields=[Field(name="value", source_name="value", type=_i32(), byte_offset=0)], + size_bytes=4, + align_bytes=4, + is_complete=True, + ) + unit = _unit([incomplete, complete]) + + out = CIRCanonicalizer().canonicalize(unit) + + assert out is not unit + assert unit.decls == [incomplete, complete] + assert len(out.decls) == 1 + record = out.decls[0] + assert isinstance(record, Struct) + assert record.is_complete is True + assert record.fields == complete.fields From 33161a9cdde746946166a0de9385a89a077ca163 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Sat, 13 Jun 2026 08:53:09 +0200 Subject: [PATCH 2/6] Clean up analysis pass internals --- docs/cir_passes_pipeline.md | 49 ++- mojo_bindgen/analysis/alias_classification.py | 45 +-- mojo_bindgen/analysis/alias_lowering.py | 49 +++ mojo_bindgen/analysis/bitfield_layout.py | 128 ++++--- mojo_bindgen/analysis/const_value_lowering.py | 54 +++ mojo_bindgen/analysis/context.py | 49 +-- mojo_bindgen/analysis/decl_lowerer.py | 160 +-------- mojo_bindgen/analysis/dependency_graph.py | 110 ++---- mojo_bindgen/analysis/indexes.py | 82 +++++ mojo_bindgen/analysis/lowering_support.py | 12 +- mojo_bindgen/analysis/macro_lowering.py | 142 ++++++++ mojo_bindgen/analysis/reachability.py | 65 +--- mojo_bindgen/analysis/record_layout.py | 122 ++++--- mojo_bindgen/analysis/record_shape.py | 327 ++++++++++-------- mojo_bindgen/analysis/reference_validation.py | 74 +--- mojo_bindgen/analysis/struct_lowering.py | 117 ++++--- mojo_bindgen/analysis/traversal.py | 162 +++++++++ mojo_bindgen/analysis/union_lowering.py | 124 ++++--- mojo_bindgen/analysis/unit_lowering.py | 108 +----- mojo_bindgen/analysis/validate_ir.py | 43 +-- tests/unit/passes/test_pipeline.py | 56 +++ tests/unit/passes/test_traversal.py | 103 ++++++ 22 files changed, 1304 insertions(+), 877 deletions(-) create mode 100644 mojo_bindgen/analysis/alias_lowering.py create mode 100644 mojo_bindgen/analysis/const_value_lowering.py create mode 100644 mojo_bindgen/analysis/indexes.py create mode 100644 mojo_bindgen/analysis/macro_lowering.py create mode 100644 mojo_bindgen/analysis/traversal.py create mode 100644 tests/unit/passes/test_traversal.py diff --git a/docs/cir_passes_pipeline.md b/docs/cir_passes_pipeline.md index 5871ee4..46ef736 100644 --- a/docs/cir_passes_pipeline.md +++ b/docs/cir_passes_pipeline.md @@ -60,6 +60,8 @@ Purpose: - reject conflicting duplicate `decl_id` values - require `decl_id` on `Typedef`, `Struct`, `TypeRef`, `StructRef`, and `OpaqueRecordRef` positions that participate in cross-reference analysis +- validate declaration and constant-expression type slots through the shared + traversal helpers Placement: @@ -118,7 +120,8 @@ would otherwise be repeated inside lowerers. ### Declaration Indexes -`AnalysisContext` indexes normalized declarations by stable keys: +`analysis.indexes` owns declaration lookup construction. `AnalysisContext` +stores the resulting normalized declarations by stable keys: - records by `decl_id` - typedefs by `decl_id` @@ -130,6 +133,20 @@ would otherwise be repeated inside lowerers. These indexes are the basic lookup surface for later passes. +### Shared Traversal Internals + +`analysis.traversal` owns declaration and constant-expression walking: + +- direct type slots on functions, typedefs, records, globals, constants, and + macros +- type slots nested in cast and `sizeof` constant expressions +- symbol references inside constant expressions +- typedef/reference discovery across whole units + +Passes should use these helpers instead of each defining bespoke declaration or +const-expression walkers. `analysis.type_walk` remains the lower-level helper +for recursive `Type` trees only. + ### `DeclDependencyGraph` Purpose: @@ -178,7 +195,9 @@ Purpose: This is the central record analysis pass. `struct_lowering` consumes these facts and performs MojoIR member construction; it should not own recursive record -shape policy. +shape policy. Its internal helpers are split by responsibility: record +prechecks, plain-field checks, embedded-record/flexible-tail checks, and +bitfield checks. ## Stage 3: CIR To MojoIR Lowering @@ -191,7 +210,7 @@ analysis because they convert analyzed CIR into MojoIR. Purpose: - create shared lowerers for one unit -- synthesize aliases for external typedef references when needed +- synthesize aliases for external typedef references from `AliasClassification` - lower top-level declarations through `UnitDeclLowerer` - build module metadata and linking mode @@ -210,6 +229,10 @@ Purpose: This is orchestration, not whole-unit analysis. New cross-declaration facts should generally be added to `AnalysisContext`. +Typedef alias construction is centralized in `alias_lowering`, so local typedefs +and external typedef aliases share callback, exact-width stdint, and no-op +self-alias handling. + ### `LowerTypePass` Purpose: @@ -231,8 +254,19 @@ Purpose: - reject constant forms that have no direct MojoIR value form, such as null pointer literals -Macro emission policy lives in `UnitDeclLowerer`; expression lowering only -rewrites supported expression shapes. +Macro emission policy lives in `MacroLowerer`; expression lowering only rewrites +supported expression shapes. + +### Macro Lowering + +Purpose: + +- lower supported object-like macros to MojoIR value aliases +- block macro forms that would be misleading in Mojo, such as null pointer + macros and C logical operators +- preserve unsupported macro spellings as diagnostic comment aliases +- track emitted constants so self-alias and name-conflict handling is + deterministic ### Struct Lowering @@ -246,6 +280,9 @@ Purpose: - compute Mojo alignment decorator policy Struct lowering should not recompute recursive record shape decisions. +Typed-member lowering is intentionally split between plain fields, bitfield +storage, and bitfield logical fields so fallback diagnostics stay local to the +operation that failed. ### `LowerUnionPass` @@ -259,6 +296,8 @@ Purpose: Union lowering remains separate because unions lower to alias-style layout types, not `StructDecl`. +Its implementation is structured as union-arm collection followed by either +`UnsafeUnion[...]` alias construction or byte-storage fallback construction. ## Stage 4: Late MojoIR Policy Analysis diff --git a/mojo_bindgen/analysis/alias_classification.py b/mojo_bindgen/analysis/alias_classification.py index dcb6a2f..dea6e39 100644 --- a/mojo_bindgen/analysis/alias_classification.py +++ b/mojo_bindgen/analysis/alias_classification.py @@ -5,16 +5,11 @@ from dataclasses import dataclass from enum import StrEnum +from mojo_bindgen.analysis.traversal import iter_unit_typerefs from mojo_bindgen.analysis.type_lowering import exact_width_stdint_alias_type -from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( - Const, EnumRef, - Function, FunctionPtr, - GlobalVar, - MacroDecl, - Struct, StructRef, Type, Typedef, @@ -51,6 +46,7 @@ class AliasClassification: aliases_by_decl_id: dict[str, AliasInfo] external_aliases_by_decl_id: dict[str, AliasInfo] + external_type_refs_by_decl_id: dict[str, TypeRef] def classify_aliases(unit: Unit) -> AliasClassification: @@ -69,7 +65,8 @@ def classify_aliases(unit: Unit) -> AliasClassification: } external: dict[str, AliasInfo] = {} - for ref in _typeref_uses(unit): + external_refs: dict[str, TypeRef] = {} + for ref in iter_unit_typerefs(unit): if ref.decl_id in local_typedefs or ref.decl_id in external: continue external[ref.decl_id] = AliasInfo( @@ -78,10 +75,12 @@ def classify_aliases(unit: Unit) -> AliasClassification: alias_class=AliasClass.EXTERNAL_TYPEDEF, target_decl_id=_target_decl_id(ref.canonical), ) + external_refs[ref.decl_id] = ref return AliasClassification( aliases_by_decl_id=aliases, external_aliases_by_decl_id=external, + external_type_refs_by_decl_id=external_refs, ) @@ -113,38 +112,6 @@ def _target_decl_id(t: Type) -> str | None: return None if target is None else target.decl_id -def _typeref_uses(unit: Unit) -> tuple[TypeRef, ...]: - out: list[TypeRef] = [] - for decl in unit.decls: - for t in _decl_types(decl): - out.extend( - node - for node in collect_type_nodes( - t, - lambda node: isinstance(node, TypeRef), - options=TypeWalkOptions(descend_vector_element=True), - ) - if isinstance(node, TypeRef) - ) - return tuple(out) - - -def _decl_types(decl) -> tuple[Type, ...]: - if isinstance(decl, Function): - return (decl.ret, *(param.type for param in decl.params)) - if isinstance(decl, Typedef): - return (decl.aliased, decl.canonical) - if isinstance(decl, Struct): - return tuple(field.type for field in decl.fields) - if isinstance(decl, GlobalVar): - return (decl.type,) - if isinstance(decl, Const): - return (decl.type,) - if isinstance(decl, MacroDecl): - return () if decl.type is None else (decl.type,) - return () - - __all__ = [ "AliasClass", "AliasClassification", diff --git a/mojo_bindgen/analysis/alias_lowering.py b/mojo_bindgen/analysis/alias_lowering.py new file mode 100644 index 0000000..15a6433 --- /dev/null +++ b/mojo_bindgen/analysis/alias_lowering.py @@ -0,0 +1,49 @@ +"""Shared helpers for lowering typedef-like aliases into MojoIR.""" + +from __future__ import annotations + +from mojo_bindgen.analysis.common import mojo_ident +from mojo_bindgen.analysis.type_lowering import LowerTypePass, exact_width_stdint_alias_type +from mojo_bindgen.ir import AliasDecl, AliasKind, DocComment, FunctionPtr, NamedType, Type + + +def is_named_type(t: Type, name: str) -> bool: + """Return whether ``t`` is exactly the Mojo named type ``name``.""" + + return isinstance(t, NamedType) and t.name == name + + +def lower_typedef_alias( + *, + c_name: str, + aliased: Type, + type_lowerer: LowerTypePass, + doc: DocComment | None = None, +) -> AliasDecl | None: + """Lower a C typedef-like alias, skipping no-op aliases to the same name.""" + + alias_name = mojo_ident(c_name) + lowered_type = exact_width_stdint_alias_type(c_name) + if lowered_type is None: + lowered_type = type_lowerer.run(aliased) + if isinstance(lowered_type, FunctionPtr): + return AliasDecl( + name=alias_name, + kind=AliasKind.CALLBACK_SIGNATURE, + type_value=lowered_type, + doc=doc, + ) + if is_named_type(lowered_type, alias_name): + return None + return AliasDecl( + name=alias_name, + kind=AliasKind.TYPE_ALIAS, + type_value=lowered_type, + doc=doc, + ) + + +__all__ = [ + "is_named_type", + "lower_typedef_alias", +] diff --git a/mojo_bindgen/analysis/bitfield_layout.py b/mojo_bindgen/analysis/bitfield_layout.py index e6fe1ba..2b34b14 100644 --- a/mojo_bindgen/analysis/bitfield_layout.py +++ b/mojo_bindgen/analysis/bitfield_layout.py @@ -39,6 +39,12 @@ def align_bytes(self) -> int: return self.unsigned_storage_type.align_bytes or self.unsigned_storage_type.size_bytes +@dataclass(frozen=True) +class _BitfieldStorage: + width_bits: int + unsigned_type: IntType + + def analyze_bitfield_layout( decl: Struct, ) -> tuple[tuple[BitfieldRunLayout, ...], tuple[str, ...]]: @@ -54,9 +60,8 @@ def analyze_bitfield_layout( continue field_name = field_display_name(_field, index) - width_bits = bitfield_storage_width_bits(_field) - unsigned_storage_type = bitfield_unsigned_storage_type(_field) - if width_bits is None or unsigned_storage_type is None: + storage = _bitfield_storage(_field) + if storage is None: problems.append(f"bitfield `{field_name}` has unsupported backing storage") current = None continue @@ -65,49 +70,88 @@ def analyze_bitfield_layout( current = None continue - field_end_bit = _field.bit_offset + _field.bit_width - needs_new_storage = True - if current is not None: - widened_width_bits = max(current.storage_width_bits, width_bits) - needs_new_storage = ( - _field.bit_offset < current.start_bit - or field_end_bit > current.start_bit + widened_width_bits - ) - - if needs_new_storage: - storage_start_bit = (_field.bit_offset // width_bits) * width_bits - current = BitfieldRunLayout( - name=f"__bf{len(runs)}", - first_index=index, - byte_offset=storage_start_bit // 8, - start_bit=storage_start_bit, - storage_width_bits=width_bits, - unsigned_storage_type=unsigned_storage_type, - ) - runs.append(current) - elif current is not None: - if width_bits > current.storage_width_bits: - current.storage_width_bits = width_bits - current.unsigned_storage_type = unsigned_storage_type - else: - continue - - if not _field.is_anonymous: - current.fields.append( - BitfieldFieldLayout( - index=index, - field=_field, - logical_type=_field.type, - bit_offset=_field.bit_offset, - bit_width=_field.bit_width, - signed=bitfield_field_is_signed(_field), - bool_semantics=bitfield_field_is_bool(_field), - ) - ) + current = _select_bitfield_run( + runs, + current=current, + field=_field, + index=index, + storage=storage, + ) + _append_named_bitfield(current, field=_field, index=index) return tuple(runs), tuple(problems) +def _bitfield_storage(field: Field) -> _BitfieldStorage | None: + width_bits = bitfield_storage_width_bits(field) + unsigned_storage_type = bitfield_unsigned_storage_type(field) + if width_bits is None or unsigned_storage_type is None: + return None + return _BitfieldStorage(width_bits=width_bits, unsigned_type=unsigned_storage_type) + + +def _select_bitfield_run( + runs: list[BitfieldRunLayout], + *, + current: BitfieldRunLayout | None, + field: Field, + index: int, + storage: _BitfieldStorage, +) -> BitfieldRunLayout: + if current is None or _needs_new_storage_run(current, field=field, storage=storage): + storage_start_bit = (field.bit_offset // storage.width_bits) * storage.width_bits + current = BitfieldRunLayout( + name=f"__bf{len(runs)}", + first_index=index, + byte_offset=storage_start_bit // 8, + start_bit=storage_start_bit, + storage_width_bits=storage.width_bits, + unsigned_storage_type=storage.unsigned_type, + ) + runs.append(current) + return current + + if storage.width_bits > current.storage_width_bits: + current.storage_width_bits = storage.width_bits + current.unsigned_storage_type = storage.unsigned_type + return current + + +def _needs_new_storage_run( + current: BitfieldRunLayout, + *, + field: Field, + storage: _BitfieldStorage, +) -> bool: + field_end_bit = field.bit_offset + field.bit_width + widened_width_bits = max(current.storage_width_bits, storage.width_bits) + return ( + field.bit_offset < current.start_bit + or field_end_bit > current.start_bit + widened_width_bits + ) + + +def _append_named_bitfield( + current: BitfieldRunLayout, + *, + field: Field, + index: int, +) -> None: + if field.is_anonymous: + return + current.fields.append( + BitfieldFieldLayout( + index=index, + field=field, + logical_type=field.type, + bit_offset=field.bit_offset, + bit_width=field.bit_width, + signed=bitfield_field_is_signed(field), + bool_semantics=bitfield_field_is_bool(field), + ) + ) + + def bitfield_storage_width_bits(field: Field) -> int | None: core = peel_layout_wrappers(field.type) if not isinstance(core, IntType): diff --git a/mojo_bindgen/analysis/const_value_lowering.py b/mojo_bindgen/analysis/const_value_lowering.py new file mode 100644 index 0000000..ef75c4d --- /dev/null +++ b/mojo_bindgen/analysis/const_value_lowering.py @@ -0,0 +1,54 @@ +"""Helpers for typed MojoIR constant values.""" + +from __future__ import annotations + +from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.ir import ( + _MOJO_INT_TYPES, + BinaryExpr, + BuiltinType, + CastExpr, + ConstExpr, + IntLiteral, + Type, + UnaryExpr, +) + + +def typed_const_value( + value: ConstExpr, + decl_type: Type, + *, + type_lowerer: LowerTypePass, +) -> ConstExpr: + """Apply Mojo-facing type coercions needed for emitted constant aliases.""" + + lowered_type = type_lowerer.run(decl_type) + if isinstance(lowered_type, BuiltinType) and lowered_type.name in _MOJO_INT_TYPES: + return _coerce_integral_const(value, lowered_type) + return value + + +def _coerce_integral_const( + value: ConstExpr, + lowered_type: BuiltinType, +) -> ConstExpr: + if isinstance(value, IntLiteral): + return CastExpr(target=lowered_type, expr=value) + if isinstance(value, UnaryExpr): + return UnaryExpr( + op=value.op, + operand=_coerce_integral_const(value.operand, lowered_type), + ) + if isinstance(value, BinaryExpr): + return BinaryExpr( + op=value.op, + lhs=_coerce_integral_const(value.lhs, lowered_type), + rhs=_coerce_integral_const(value.rhs, lowered_type), + ) + return value + + +__all__ = [ + "typed_const_value", +] diff --git a/mojo_bindgen/analysis/context.py b/mojo_bindgen/analysis/context.py index 4f398f1..e1bd8ae 100644 --- a/mojo_bindgen/analysis/context.py +++ b/mojo_bindgen/analysis/context.py @@ -6,7 +6,7 @@ from mojo_bindgen.analysis.alias_classification import AliasClassification, classify_aliases from mojo_bindgen.analysis.dependency_graph import DeclDependencyGraph, build_decl_dependency_graph -from mojo_bindgen.analysis.lowering_support import record_by_decl_id +from mojo_bindgen.analysis.indexes import DeclIndexes, build_decl_indexes from mojo_bindgen.analysis.record_layout import RecordLayoutFacts, analyze_record_layout from mojo_bindgen.analysis.record_shape import RecordAnalysisFacts, analyze_record_shapes from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit @@ -33,33 +33,20 @@ class AnalysisContext: def build_analysis_context(unit: Unit) -> AnalysisContext: """Build shared declaration indexes and record layout facts for ``unit``.""" - records = record_by_decl_id(unit) - record_layouts = { - decl_id: analyze_record_layout( - record, - target_abi=unit.target_abi, - record_map=records, - ) - for decl_id, record in records.items() - } + indexes = build_decl_indexes(unit) + record_layouts = _build_record_layouts(unit, indexes) dependency_graph = build_decl_dependency_graph(unit) alias_classification = classify_aliases(unit) - record_shapes = analyze_record_shapes(records, record_layouts) + record_shapes = analyze_record_shapes(indexes.records_by_decl_id, record_layouts) return AnalysisContext( unit=unit, - records_by_decl_id=records, - typedefs_by_decl_id={ - decl.decl_id: decl for decl in unit.decls if isinstance(decl, Typedef) - }, - enums_by_decl_id={decl.decl_id: decl for decl in unit.decls if isinstance(decl, Enum)}, - functions_by_decl_id={ - decl.decl_id: decl for decl in unit.decls if isinstance(decl, Function) - }, - globals_by_decl_id={ - decl.decl_id: decl for decl in unit.decls if isinstance(decl, GlobalVar) - }, - consts_by_name={decl.name: decl for decl in unit.decls if isinstance(decl, Const)}, - macros_by_name={decl.name: decl for decl in unit.decls if isinstance(decl, MacroDecl)}, + records_by_decl_id=indexes.records_by_decl_id, + typedefs_by_decl_id=indexes.typedefs_by_decl_id, + enums_by_decl_id=indexes.enums_by_decl_id, + functions_by_decl_id=indexes.functions_by_decl_id, + globals_by_decl_id=indexes.globals_by_decl_id, + consts_by_name=indexes.consts_by_name, + macros_by_name=indexes.macros_by_name, dependency_graph=dependency_graph, alias_classification=alias_classification, record_layouts=record_layouts, @@ -67,6 +54,20 @@ def build_analysis_context(unit: Unit) -> AnalysisContext: ) +def _build_record_layouts( + unit: Unit, + indexes: DeclIndexes, +) -> dict[str, RecordLayoutFacts]: + return { + decl_id: analyze_record_layout( + record, + target_abi=unit.target_abi, + record_map=indexes.records_by_decl_id, + ) + for decl_id, record in indexes.records_by_decl_id.items() + } + + __all__ = [ "AnalysisContext", "build_analysis_context", diff --git a/mojo_bindgen/analysis/decl_lowerer.py b/mojo_bindgen/analysis/decl_lowerer.py index 576f0d8..3164c9c 100644 --- a/mojo_bindgen/analysis/decl_lowerer.py +++ b/mojo_bindgen/analysis/decl_lowerer.py @@ -2,28 +2,25 @@ from dataclasses import dataclass +from mojo_bindgen.analysis.alias_lowering import lower_typedef_alias from mojo_bindgen.analysis.common import mojo_ident from mojo_bindgen.analysis.const_lowering import ( ConstExprLoweringError, LowerConstExprPass, ) -from mojo_bindgen.analysis.lowering_support import ( - lowering_note, - stub_note, -) +from mojo_bindgen.analysis.const_value_lowering import typed_const_value +from mojo_bindgen.analysis.lowering_support import stub_note +from mojo_bindgen.analysis.macro_lowering import MacroLowerer from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.struct_lowering import ( StructLoweringContext, lower_struct, ) -from mojo_bindgen.analysis.type_lowering import LowerTypePass, exact_width_stdint_alias_type +from mojo_bindgen.analysis.type_lowering import LowerTypePass from mojo_bindgen.analysis.union_lowering import LowerUnionPass from mojo_bindgen.ir import ( - _MOJO_INT_TYPES, AliasDecl, AliasKind, - BinaryExpr, - BuiltinType, CallExpr, CallTarget, CastExpr, @@ -34,7 +31,6 @@ Function, FunctionDecl, FunctionKind, - FunctionPtr, GlobalDecl, GlobalKind, GlobalVar, @@ -43,15 +39,12 @@ MacroDecl, MojoDecl, NamedType, - NullPtrLiteral, Param, ParametricBase, ParametricType, RefExpr, - SizeOfExpr, Struct, Typedef, - UnaryExpr, Unit, ) @@ -82,8 +75,13 @@ class UnitDeclLowerer: def __init__(self, session: LoweringSession) -> None: self.session = session - self._union_lowerer = LowerUnionPass(type_lowerer=session.type_lowerer) self._emitted_const_names: set[str] = set() + self._union_lowerer = LowerUnionPass(type_lowerer=session.type_lowerer) + self._macro_lowerer = MacroLowerer( + const_lowerer=session.const_lowerer, + type_lowerer=session.type_lowerer, + emitted_const_names=self._emitted_const_names, + ) def lower_decl(self, decl: Decl) -> MojoDecl | list[MojoDecl] | None: if isinstance(decl, Typedef): @@ -103,23 +101,10 @@ def lower_decl(self, decl: Decl) -> MojoDecl | list[MojoDecl] | None: raise UnitLoweringError(f"unsupported CIR declaration node: {type(decl).__name__!r}") def _lower_typedef(self, decl: Typedef) -> AliasDecl | None: - alias_name = mojo_ident(decl.name) - lowered_type = exact_width_stdint_alias_type(decl.name) - if lowered_type is None: - lowered_type = self.session.type_lowerer.run(decl.aliased) - if isinstance(lowered_type, FunctionPtr): - return AliasDecl( - name=alias_name, - kind=AliasKind.CALLBACK_SIGNATURE, - type_value=lowered_type, - doc=decl.doc, - ) - if getattr(lowered_type, "name", None) == alias_name: - return None - return AliasDecl( - name=alias_name, - kind=AliasKind.TYPE_ALIAS, - type_value=lowered_type, + return lower_typedef_alias( + c_name=decl.name, + aliased=decl.aliased, + type_lowerer=self.session.type_lowerer, doc=decl.doc, ) @@ -220,78 +205,7 @@ def _lower_const(self, decl: Const) -> AliasDecl: ) def _lower_macro(self, decl: MacroDecl) -> AliasDecl | None: - if decl.kind == "empty": - return None - name = mojo_ident(decl.name) - if name in self._emitted_const_names: - if isinstance(decl.expr, RefExpr) and mojo_ident(decl.expr.name) == name: - return None - return self._macro_comment_alias( - decl, - f"macro {decl.name}: macro name conflicts with an emitted constant", - ) - if decl.expr is not None and decl.type is not None: - blocker = self._macro_emit_blocker(decl.expr) - if blocker is not None: - return self._macro_comment_alias( - decl, - f"macro {decl.name}: {blocker}", - ) - try: - value = self.session.const_lowerer.run(decl.expr) - except ConstExprLoweringError: - if isinstance(decl.expr, NullPtrLiteral): - return self._macro_comment_alias( - decl, - f"macro {decl.name}: null pointer macro is not emitted directly", - ) - value = None - else: - self._emitted_const_names.add(name) - return AliasDecl( - name=name, - kind=AliasKind.MACRO_VALUE, - const_value=self._typed_const_value(value, decl.type), - doc=decl.doc, - ) - return AliasDecl( - name=mojo_ident(decl.name), - kind=AliasKind.MACRO_VALUE, - diagnostics=( - self._macro_comment_notes( - decl, - f"macro {decl.name}: {decl.diagnostic}", - ) - if decl.diagnostic - else [stub_note("macro lowering is incomplete; placeholder alias emitted")] - ), - doc=decl.doc, - ) - - def _macro_emit_blocker(self, expr: ConstExpr) -> str | None: - if isinstance(expr, NullPtrLiteral): - return "null pointer macro is not emitted directly" - if isinstance(expr, RefExpr): - name = mojo_ident(expr.name) - if name not in self._emitted_const_names: - return ( - "macro expression references a non-emitted or later macro constant " - f"{expr.name!r}" - ) - return None - if isinstance(expr, UnaryExpr): - if expr.op == "!": - return "C logical operator '!' is not emitted directly" - return self._macro_emit_blocker(expr.operand) - if isinstance(expr, BinaryExpr): - if expr.op in {"&&", "||"}: - return f"C logical operator {expr.op!r} is not emitted directly" - return self._macro_emit_blocker(expr.lhs) or self._macro_emit_blocker(expr.rhs) - if isinstance(expr, CastExpr): - return self._macro_emit_blocker(expr.expr) - if isinstance(expr, (SizeOfExpr, CallExpr)): - return None - return None + return self._macro_lowerer.lower(decl) def _lower_struct(self, decl: Struct) -> MojoDecl: if decl.is_union: @@ -299,44 +213,4 @@ def _lower_struct(self, decl: Struct) -> MojoDecl: return lower_struct(decl, context=self.session.struct_context) def _typed_const_value(self, value: ConstExpr, decl_type) -> ConstExpr: - lowered_type = self.session.type_lowerer.run(decl_type) - if isinstance(lowered_type, BuiltinType) and lowered_type.name in _MOJO_INT_TYPES: - return self._coerce_integral_const(value, lowered_type) - return value - - def _coerce_integral_const( - self, - value: ConstExpr, - lowered_type: BuiltinType, - ) -> ConstExpr: - if isinstance(value, IntLiteral): - return CastExpr(target=lowered_type, expr=value) - if isinstance(value, UnaryExpr): - return UnaryExpr( - op=value.op, - operand=self._coerce_integral_const(value.operand, lowered_type), - ) - if isinstance(value, BinaryExpr): - return BinaryExpr( - op=value.op, - lhs=self._coerce_integral_const(value.lhs, lowered_type), - rhs=self._coerce_integral_const(value.rhs, lowered_type), - ) - return value - - def _macro_comment_alias(self, decl: MacroDecl, message: str) -> AliasDecl: - return AliasDecl( - name=mojo_ident(decl.name), - kind=AliasKind.MACRO_VALUE, - diagnostics=self._macro_comment_notes(decl, message), - doc=decl.doc, - ) - - def _macro_comment_notes(self, decl: MacroDecl, message: str): - return [ - lowering_note(message, category="macro_comment"), - lowering_note( - f"define {decl.name} {' '.join(decl.tokens)}", - category="macro_comment", - ), - ] + return typed_const_value(value, decl_type, type_lowerer=self.session.type_lowerer) diff --git a/mojo_bindgen/analysis/dependency_graph.py b/mojo_bindgen/analysis/dependency_graph.py index 1c3992d..e358f20 100644 --- a/mojo_bindgen/analysis/dependency_graph.py +++ b/mojo_bindgen/analysis/dependency_graph.py @@ -4,28 +4,16 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes -from mojo_bindgen.ir import ( - BinaryExpr, - CastExpr, - Const, - ConstExpr, - Decl, - Enum, - EnumRef, - Function, - GlobalVar, - MacroDecl, - RefExpr, - SizeOfExpr, - Struct, - StructRef, - Type, - Typedef, - TypeRef, - UnaryExpr, - Unit, +from mojo_bindgen.analysis.traversal import ( + decl_id, + iter_const_expr_refs, + iter_const_expr_types, + iter_decl_const_exprs, + iter_decl_types, + iter_type_decl_refs, ) +from mojo_bindgen.analysis.type_walk import TypeWalkOptions +from mojo_bindgen.ir import Type, Unit @dataclass(frozen=True) @@ -42,17 +30,19 @@ def build_decl_dependency_graph(unit: Unit) -> DeclDependencyGraph: edges: dict[str, set[str]] = {} symbol_edges: dict[str, set[str]] = {} for decl in unit.decls: - decl_id = _decl_id(decl) - if decl_id is None: + current_id = decl_id(decl) + if current_id is None: continue type_edges: set[str] = set() symbols: set[str] = set() - for t in _decl_types(decl): + for t in iter_decl_types(decl): _collect_type_edges(t, type_edges) - for expr in _decl_exprs(decl): - _collect_const_expr_edges(expr, type_edges, symbols) - edges[decl_id] = type_edges - symbol_edges[decl_id] = symbols + for expr in iter_decl_const_exprs(decl): + for t in iter_const_expr_types(expr): + _collect_type_edges(t, type_edges) + symbols.update(ref.name for ref in iter_const_expr_refs(expr)) + edges[current_id] = type_edges + symbol_edges[current_id] = symbols return DeclDependencyGraph( edges_by_decl_id={decl_id: frozenset(values) for decl_id, values in edges.items()}, @@ -62,72 +52,12 @@ def build_decl_dependency_graph(unit: Unit) -> DeclDependencyGraph: ) -def _decl_id(decl: Decl) -> str | None: - if isinstance(decl, (Function, Struct, Typedef, GlobalVar)): - return decl.decl_id - if isinstance(decl, Enum): - return decl.decl_id - if isinstance(decl, (Const, MacroDecl)): - return decl.name - return None - - -def _decl_types(decl: Decl) -> tuple[Type, ...]: - if isinstance(decl, Function): - return (decl.ret, *(param.type for param in decl.params)) - if isinstance(decl, Typedef): - return (decl.aliased, decl.canonical) - if isinstance(decl, Struct): - return tuple(field.type for field in decl.fields) - if isinstance(decl, GlobalVar): - return (decl.type,) - if isinstance(decl, Const): - return (decl.type,) - if isinstance(decl, MacroDecl): - return () if decl.type is None else (decl.type,) - return () - - -def _decl_exprs(decl: Decl) -> tuple[ConstExpr, ...]: - if isinstance(decl, Const): - return (decl.expr,) - if isinstance(decl, GlobalVar) and decl.initializer is not None: - return (decl.initializer,) - if isinstance(decl, MacroDecl) and decl.expr is not None: - return (decl.expr,) - return () - - def _collect_type_edges(t: Type, out: set[str]) -> None: - for node in iter_type_nodes( + for node in iter_type_decl_refs( t, options=TypeWalkOptions(descend_vector_element=True), ): - if isinstance(node, (TypeRef, StructRef, EnumRef)): - out.add(node.decl_id) - - -def _collect_const_expr_edges( - expr: ConstExpr, - type_edges: set[str], - symbol_edges: set[str], -) -> None: - if isinstance(expr, RefExpr): - symbol_edges.add(expr.name) - return - if isinstance(expr, CastExpr): - _collect_type_edges(expr.target, type_edges) - _collect_const_expr_edges(expr.expr, type_edges, symbol_edges) - return - if isinstance(expr, SizeOfExpr): - _collect_type_edges(expr.target, type_edges) - return - if isinstance(expr, UnaryExpr): - _collect_const_expr_edges(expr.operand, type_edges, symbol_edges) - return - if isinstance(expr, BinaryExpr): - _collect_const_expr_edges(expr.lhs, type_edges, symbol_edges) - _collect_const_expr_edges(expr.rhs, type_edges, symbol_edges) + out.add(node.decl_id) __all__ = [ diff --git a/mojo_bindgen/analysis/indexes.py b/mojo_bindgen/analysis/indexes.py new file mode 100644 index 0000000..a4d963e --- /dev/null +++ b/mojo_bindgen/analysis/indexes.py @@ -0,0 +1,82 @@ +"""Shared declaration indexes for normalized CIR units.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit + + +@dataclass(frozen=True) +class DeclIndexes: + """Common lookup maps for top-level declarations.""" + + records_by_decl_id: dict[str, Struct] + typedefs_by_decl_id: dict[str, Typedef] + enums_by_decl_id: dict[str, Enum] + functions_by_decl_id: dict[str, Function] + globals_by_decl_id: dict[str, GlobalVar] + consts_by_name: dict[str, Const] + macros_by_name: dict[str, MacroDecl] + + +def build_decl_indexes(unit: Unit) -> DeclIndexes: + """Build declaration lookup maps used by analysis and lowering.""" + + records_by_decl_id: dict[str, Struct] = {} + typedefs_by_decl_id: dict[str, Typedef] = {} + enums_by_decl_id: dict[str, Enum] = {} + functions_by_decl_id: dict[str, Function] = {} + globals_by_decl_id: dict[str, GlobalVar] = {} + consts_by_name: dict[str, Const] = {} + macros_by_name: dict[str, MacroDecl] = {} + + for decl in unit.decls: + if isinstance(decl, Struct): + records_by_decl_id[decl.decl_id] = decl + elif isinstance(decl, Typedef): + typedefs_by_decl_id[decl.decl_id] = decl + elif isinstance(decl, Enum): + enums_by_decl_id[decl.decl_id] = decl + elif isinstance(decl, Function): + functions_by_decl_id[decl.decl_id] = decl + elif isinstance(decl, GlobalVar): + globals_by_decl_id[decl.decl_id] = decl + elif isinstance(decl, Const): + consts_by_name[decl.name] = decl + elif isinstance(decl, MacroDecl): + macros_by_name[decl.name] = decl + + return DeclIndexes( + records_by_decl_id=records_by_decl_id, + typedefs_by_decl_id=typedefs_by_decl_id, + enums_by_decl_id=enums_by_decl_id, + functions_by_decl_id=functions_by_decl_id, + globals_by_decl_id=globals_by_decl_id, + consts_by_name=consts_by_name, + macros_by_name=macros_by_name, + ) + + +def record_by_decl_id(unit: Unit) -> dict[str, Struct]: + """Map every record ``decl_id`` to its CIR declaration, including unions.""" + + return build_decl_indexes(unit).records_by_decl_id + + +def struct_by_decl_id(unit: Unit) -> dict[str, Struct]: + """Map non-union struct ``decl_id`` to its CIR declaration.""" + + return { + decl_id: record + for decl_id, record in build_decl_indexes(unit).records_by_decl_id.items() + if not record.is_union + } + + +__all__ = [ + "DeclIndexes", + "build_decl_indexes", + "record_by_decl_id", + "struct_by_decl_id", +] diff --git a/mojo_bindgen/analysis/lowering_support.py b/mojo_bindgen/analysis/lowering_support.py index b77a5c9..c5f2e65 100644 --- a/mojo_bindgen/analysis/lowering_support.py +++ b/mojo_bindgen/analysis/lowering_support.py @@ -3,6 +3,7 @@ from __future__ import annotations from mojo_bindgen.analysis.common import mojo_ident +from mojo_bindgen.analysis.indexes import record_by_decl_id from mojo_bindgen.analysis.type_lowering import LowerTypePass, TypeLoweringError from mojo_bindgen.ir import ( BuiltinType, @@ -12,20 +13,9 @@ MojoBuiltin, Struct, Type, - Unit, ) -def record_by_decl_id(unit: Unit) -> dict[str, Struct]: - """Map every record ``decl_id`` to its CIR declaration, including unions.""" - - out: dict[str, Struct] = {} - for decl in unit.decls: - if isinstance(decl, Struct): - out[decl.decl_id] = decl - return out - - def field_display_name(field: Field, index: int) -> str: """Best-effort human-facing name for diagnostics.""" diff --git a/mojo_bindgen/analysis/macro_lowering.py b/mojo_bindgen/analysis/macro_lowering.py new file mode 100644 index 0000000..81da555 --- /dev/null +++ b/mojo_bindgen/analysis/macro_lowering.py @@ -0,0 +1,142 @@ +"""Lower C macro declarations into MojoIR aliases or diagnostic placeholders.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from mojo_bindgen.analysis.common import mojo_ident +from mojo_bindgen.analysis.const_lowering import ConstExprLoweringError, LowerConstExprPass +from mojo_bindgen.analysis.const_value_lowering import typed_const_value +from mojo_bindgen.analysis.lowering_support import lowering_note, stub_note +from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.ir import ( + AliasDecl, + AliasKind, + BinaryExpr, + CallExpr, + CastExpr, + ConstExpr, + MacroDecl, + NullPtrLiteral, + RefExpr, + SizeOfExpr, + UnaryExpr, +) + + +@dataclass +class MacroLowerer: + """Lower macro declarations while tracking already-emitted constant names.""" + + const_lowerer: LowerConstExprPass + type_lowerer: LowerTypePass + emitted_const_names: set[str] + + def lower(self, decl: MacroDecl) -> AliasDecl | None: + if decl.kind == "empty": + return None + + name = mojo_ident(decl.name) + if name in self.emitted_const_names: + if isinstance(decl.expr, RefExpr) and mojo_ident(decl.expr.name) == name: + return None + return self._comment_alias( + decl, + f"macro {decl.name}: macro name conflicts with an emitted constant", + ) + + if decl.expr is not None and decl.type is not None: + lowered = self._lower_value_macro(decl, name) + if lowered is not None: + return lowered + + return self._placeholder_alias(decl) + + def _lower_value_macro(self, decl: MacroDecl, name: str) -> AliasDecl | None: + assert decl.expr is not None + assert decl.type is not None + + blocker = self._emit_blocker(decl.expr) + if blocker is not None: + return self._comment_alias(decl, f"macro {decl.name}: {blocker}") + + try: + value = self.const_lowerer.run(decl.expr) + except ConstExprLoweringError: + if isinstance(decl.expr, NullPtrLiteral): + return self._comment_alias( + decl, + f"macro {decl.name}: null pointer macro is not emitted directly", + ) + return None + + self.emitted_const_names.add(name) + return AliasDecl( + name=name, + kind=AliasKind.MACRO_VALUE, + const_value=typed_const_value( + value, + decl.type, + type_lowerer=self.type_lowerer, + ), + doc=decl.doc, + ) + + def _emit_blocker(self, expr: ConstExpr) -> str | None: + if isinstance(expr, NullPtrLiteral): + return "null pointer macro is not emitted directly" + if isinstance(expr, RefExpr): + name = mojo_ident(expr.name) + if name not in self.emitted_const_names: + return ( + "macro expression references a non-emitted or later macro constant " + f"{expr.name!r}" + ) + return None + if isinstance(expr, UnaryExpr): + if expr.op == "!": + return "C logical operator '!' is not emitted directly" + return self._emit_blocker(expr.operand) + if isinstance(expr, BinaryExpr): + if expr.op in {"&&", "||"}: + return f"C logical operator {expr.op!r} is not emitted directly" + return self._emit_blocker(expr.lhs) or self._emit_blocker(expr.rhs) + if isinstance(expr, CastExpr): + return self._emit_blocker(expr.expr) + if isinstance(expr, (SizeOfExpr, CallExpr)): + return None + return None + + def _placeholder_alias(self, decl: MacroDecl) -> AliasDecl: + return AliasDecl( + name=mojo_ident(decl.name), + kind=AliasKind.MACRO_VALUE, + diagnostics=( + self._comment_notes(decl, f"macro {decl.name}: {decl.diagnostic}") + if decl.diagnostic + else [stub_note("macro lowering is incomplete; placeholder alias emitted")] + ), + doc=decl.doc, + ) + + def _comment_alias(self, decl: MacroDecl, message: str) -> AliasDecl: + return AliasDecl( + name=mojo_ident(decl.name), + kind=AliasKind.MACRO_VALUE, + diagnostics=self._comment_notes(decl, message), + doc=decl.doc, + ) + + def _comment_notes(self, decl: MacroDecl, message: str): + return [ + lowering_note(message, category="macro_comment"), + lowering_note( + f"define {decl.name} {' '.join(decl.tokens)}", + category="macro_comment", + ), + ] + + +__all__ = [ + "MacroLowerer", +] diff --git a/mojo_bindgen/analysis/reachability.py b/mojo_bindgen/analysis/reachability.py index 52dc47d..c03af00 100644 --- a/mojo_bindgen/analysis/reachability.py +++ b/mojo_bindgen/analysis/reachability.py @@ -12,22 +12,17 @@ from dataclasses import dataclass, replace +from mojo_bindgen.analysis.traversal import ( + iter_const_expr_types, + iter_decl_const_exprs, + iter_decl_types, +) from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( - BinaryExpr, - CastExpr, - Const, - ConstExpr, Decl, - Function, - GlobalVar, - MacroDecl, - SizeOfExpr, Struct, StructRef, Type, - Typedef, - UnaryExpr, Unit, ) @@ -71,24 +66,13 @@ def _walk_type( def _walk_const_expr( - expr: ConstExpr, + expr, out: list[StructRef], *, traverse_function_ptrs: bool, ) -> None: - if isinstance(expr, CastExpr): - _walk_type(expr.target, out, traverse_function_ptrs=traverse_function_ptrs) - _walk_const_expr(expr.expr, out, traverse_function_ptrs=traverse_function_ptrs) - return - if isinstance(expr, SizeOfExpr): - _walk_type(expr.target, out, traverse_function_ptrs=traverse_function_ptrs) - return - if isinstance(expr, UnaryExpr): - _walk_const_expr(expr.operand, out, traverse_function_ptrs=traverse_function_ptrs) - return - if isinstance(expr, BinaryExpr): - _walk_const_expr(expr.lhs, out, traverse_function_ptrs=traverse_function_ptrs) - _walk_const_expr(expr.rhs, out, traverse_function_ptrs=traverse_function_ptrs) + for t in iter_const_expr_types(expr): + _walk_type(t, out, traverse_function_ptrs=traverse_function_ptrs) def _collect_from_decl( @@ -98,35 +82,12 @@ def _collect_from_decl( options: SignatureRecordStubOptions, ) -> None: traverse_fp = options.traverse_function_ptrs - if isinstance(decl, Function): - _walk_type(decl.ret, out, traverse_function_ptrs=traverse_fp) - for p in decl.params: - _walk_type(p.type, out, traverse_function_ptrs=traverse_fp) - return - if isinstance(decl, Typedef): - _walk_type(decl.aliased, out, traverse_function_ptrs=traverse_fp) - _walk_type(decl.canonical, out, traverse_function_ptrs=traverse_fp) - return - if isinstance(decl, Struct): - for f in decl.fields: - _walk_type(f.type, out, traverse_function_ptrs=traverse_fp) - return - if isinstance(decl, GlobalVar): - _walk_type(decl.type, out, traverse_function_ptrs=traverse_fp) - if options.traverse_const_expr_types and decl.initializer is not None: - _walk_const_expr(decl.initializer, out, traverse_function_ptrs=traverse_fp) - return - if isinstance(decl, Const): - _walk_type(decl.type, out, traverse_function_ptrs=traverse_fp) - if options.traverse_const_expr_types: - _walk_const_expr(decl.expr, out, traverse_function_ptrs=traverse_fp) - return - if isinstance(decl, MacroDecl): - if decl.type is not None: - _walk_type(decl.type, out, traverse_function_ptrs=traverse_fp) - if options.traverse_const_expr_types and decl.expr is not None: - _walk_const_expr(decl.expr, out, traverse_function_ptrs=traverse_fp) + for t in iter_decl_types(decl): + _walk_type(t, out, traverse_function_ptrs=traverse_fp) + if not options.traverse_const_expr_types: return + for expr in iter_decl_const_exprs(decl): + _walk_const_expr(expr, out, traverse_function_ptrs=traverse_fp) def _struct_from_ref(ref: StructRef) -> Struct: diff --git a/mojo_bindgen/analysis/record_layout.py b/mojo_bindgen/analysis/record_layout.py index 32deed6..18dd7e8 100644 --- a/mojo_bindgen/analysis/record_layout.py +++ b/mojo_bindgen/analysis/record_layout.py @@ -5,18 +5,10 @@ from dataclasses import dataclass from mojo_bindgen.analysis.bitfield_layout import BitfieldRunLayout, analyze_bitfield_layout +from mojo_bindgen.analysis.indexes import struct_by_decl_id from mojo_bindgen.analysis.lowering_support import field_display_name from mojo_bindgen.analysis.type_layout import type_align -from mojo_bindgen.ir import Array, Struct, StructRef, TargetABI, Unit - - -def struct_by_decl_id(unit: Unit) -> dict[str, Struct]: - """Map struct ``decl_id`` to :class:`Struct`, including incomplete non-unions.""" - out: dict[str, Struct] = {} - for decl in unit.decls: - if isinstance(decl, Struct) and not decl.is_union: - out[decl.decl_id] = decl - return out +from mojo_bindgen.ir import Array, Struct, StructRef, TargetABI @dataclass(frozen=True) @@ -34,6 +26,14 @@ class PaddingSpan: size_bytes: int +@dataclass(frozen=True) +class _LayoutItem: + index: int + byte_offset: int + size_bytes: int + align_bytes: int + + @dataclass(frozen=True) class RecordLayoutFacts: is_complete: bool = False @@ -58,6 +58,8 @@ def analyze_record_layout( target_abi: TargetABI, record_map: dict[str, Struct], ) -> RecordLayoutFacts: + # Kept in the public signature for callers that pass whole-unit ABI/index + # context. The current layout facts are fully represented on each record. del target_abi, record_map if not decl.is_complete: @@ -139,47 +141,82 @@ def _synthesize_padding_and_validate_layout( problems: list[str] = [] current_offset = 0 - layout_items = sorted( - [ - *( - (field.index, field.byte_offset, field.size_bytes, field.align_bytes) - for field in plain_fields - ), - *( - (run.first_index, run.byte_offset, run.size_bytes, run.align_bytes) - for run in bitfield_runs - ), - ], - key=lambda item: (item[1], item[0]), + for item in _layout_items(plain_fields, bitfield_runs): + current_offset = _validate_layout_item( + item, + current_offset=current_offset, + padding=padding, + problems=problems, + ) + + _synthesize_trailing_padding( + decl, + current_offset=current_offset, + padding=padding, + problems=problems, ) + return padding, problems - for _, byte_offset, size_bytes, align_bytes in layout_items: - natural_offset = _align_up(current_offset, align_bytes) - if byte_offset < natural_offset: - problems.append( - f"member at byte offset {byte_offset} is before the natural typed offset {natural_offset}" - ) - continue - if align_bytes > 1 and byte_offset % align_bytes != 0: - problems.append( - f"member at byte offset {byte_offset} is not representable with typed alignment {align_bytes}" - ) - continue - if byte_offset > natural_offset: - padding.append( - PaddingSpan( - name=f"__pad{len(padding)}", - byte_offset=natural_offset, - size_bytes=byte_offset - natural_offset, - ) + +def _layout_items( + plain_fields: list[PlainFieldFact], + bitfield_runs: tuple[BitfieldRunLayout, ...], +) -> tuple[_LayoutItem, ...]: + items = [ + *( + _LayoutItem(field.index, field.byte_offset, field.size_bytes, field.align_bytes) + for field in plain_fields + ), + *( + _LayoutItem(run.first_index, run.byte_offset, run.size_bytes, run.align_bytes) + for run in bitfield_runs + ), + ] + return tuple(sorted(items, key=lambda item: (item.byte_offset, item.index))) + + +def _validate_layout_item( + item: _LayoutItem, + *, + current_offset: int, + padding: list[PaddingSpan], + problems: list[str], +) -> int: + natural_offset = _align_up(current_offset, item.align_bytes) + if item.byte_offset < natural_offset: + problems.append( + f"member at byte offset {item.byte_offset} is before the natural typed offset {natural_offset}" + ) + return current_offset + if item.align_bytes > 1 and item.byte_offset % item.align_bytes != 0: + problems.append( + f"member at byte offset {item.byte_offset} is not representable with typed alignment {item.align_bytes}" + ) + return current_offset + if item.byte_offset > natural_offset: + padding.append( + PaddingSpan( + name=f"__pad{len(padding)}", + byte_offset=natural_offset, + size_bytes=item.byte_offset - natural_offset, ) - current_offset = byte_offset + size_bytes + ) + return item.byte_offset + item.size_bytes + +def _synthesize_trailing_padding( + decl: Struct, + *, + current_offset: int, + padding: list[PaddingSpan], + problems: list[str], +) -> None: implicit_end = _align_up(current_offset, decl.align_bytes) if implicit_end > decl.size_bytes: problems.append( f"typed members consume {implicit_end} bytes after struct alignment, exceeding C size {decl.size_bytes}" ) + return if not problems and implicit_end < decl.size_bytes: padding.append( PaddingSpan( @@ -188,7 +225,6 @@ def _synthesize_padding_and_validate_layout( size_bytes=decl.size_bytes - implicit_end, ) ) - return padding, problems def _align_up(offset: int, align: int) -> int: diff --git a/mojo_bindgen/analysis/record_shape.py b/mojo_bindgen/analysis/record_shape.py index 534b3a0..34678fb 100644 --- a/mojo_bindgen/analysis/record_shape.py +++ b/mojo_bindgen/analysis/record_shape.py @@ -11,10 +11,7 @@ from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( Array, - AtomicType, FlexibleTail, - ParametricBase, - ParametricType, QualifiedType, Struct, StructRef, @@ -199,167 +196,128 @@ def _record_typed_shape_by_value( if cached is not None: return cached + decl, facts, early_shape = self._precheck_by_value_record(decl_id, active) + if early_shape is not None: + return self._cache_by_value(decl_id, early_shape) + assert decl is not None + assert facts is not None + + active.add(decl_id) + try: + field_shape = self._plain_fields_by_value_shape(decl, facts, active=active) + if not field_shape.valid: + return self._cache_by_value(decl_id, field_shape) + + bitfield_shape = self._bitfield_runs_by_value_shape(facts) + if not bitfield_shape.valid: + return self._cache_by_value(decl_id, bitfield_shape) + finally: + active.remove(decl_id) + + return self._cache_by_value( + decl_id, + ByValueRecordShape(True, flexible_tail=field_shape.flexible_tail), + ) + + def _precheck_by_value_record( + self, + decl_id: str, + active: set[str], + ) -> tuple[Struct | None, RecordLayoutFacts | None, ByValueRecordShape | None]: decl = self.records_by_decl_id.get(decl_id) if decl is None: - return self._cache_by_value( - decl_id, ByValueRecordShape(False, ("referenced definition is unavailable",)) - ) + return None, None, ByValueRecordShape(False, ("referenced definition is unavailable",)) if decl.is_union: - return self._cache_by_value(decl_id, ByValueRecordShape(True)) + return None, None, ByValueRecordShape(True) if not decl.is_complete: - return self._cache_by_value( - decl_id, ByValueRecordShape(False, ("referenced definition is incomplete",)) - ) + return None, None, ByValueRecordShape(False, ("referenced definition is incomplete",)) if decl_id in active: - return self._cache_by_value(decl_id, ByValueRecordShape(True)) + return None, None, ByValueRecordShape(True) facts = self._layout(decl) if not facts.is_complete: - return self._cache_by_value( - decl_id, ByValueRecordShape(False, ("referenced definition is incomplete",)) - ) + return None, None, ByValueRecordShape(False, ("referenced definition is incomplete",)) if facts.layout_problems: - return self._cache_by_value( - decl_id, ByValueRecordShape(False, (facts.layout_problems[0],)) - ) + return None, None, ByValueRecordShape(False, (facts.layout_problems[0],)) + return decl, facts, None - active.add(decl_id) - try: - flexible_tail: FlexibleTail | None = None - for field_fact in facts.plain_fields: - field = decl.fields[field_fact.index] - lowered_type, reason = try_lower_type( - self.type_lowerer, - field.type, - subject=f"field `{field_display_name(field, field_fact.index)}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None or lowered_type is None: - return self._cache_by_value( - decl_id, - ByValueRecordShape(False, ((reason or "field could not be lowered"),)), - ) - if isinstance(field.type, AtomicType) and not ( - isinstance(lowered_type, ParametricType) - and lowered_type.base == ParametricBase.ATOMIC - ): - # This remains typed-storage-compatible; struct lowering emits - # the user-facing note while rendering the member. - pass - if field.fam_pattern is not None: - direct_tail, tail_reason = _lower_flexible_tail_metadata( - field, - field_fact.index, - field_fact.byte_offset, - lowered_type, - ) - if tail_reason is not None or direct_tail is None: - return self._cache_by_value( - decl_id, - ByValueRecordShape( - False, ((tail_reason or "flexible tail could not lower"),) - ), - ) - flexible_tail = direct_tail - continue - - refs = _embedded_struct_refs(field.type) - if not refs: - continue - direct_ref = _direct_embedded_struct_ref(field.type) - nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]] = [] - for ref in refs: - shape = self._record_typed_shape_by_value( - ref.decl_id, - active=active, - ) - if not shape.valid: - target_name = ref.c_name or ref.name - suffix = ( - shape.detail[0] - if shape.detail - else "embedded record is not layout-emittable by value" - ) - return self._cache_by_value( - decl_id, - ByValueRecordShape( - False, - ( - f"field `{field_display_name(field, field_fact.index)}` embeds " - f"struct `{target_name}` by value, but {suffix}; opaque storage emitted", - ), - ), - ) - if shape.flexible_tail is not None: - nested_tail_shapes.append((ref, shape)) - if not nested_tail_shapes: - continue - if len(nested_tail_shapes) > 1 or direct_ref is None: - return self._cache_by_value( - decl_id, - ByValueRecordShape( - False, - ( - f"field `{field_display_name(field, field_fact.index)}` embeds a " - "flexible-tail record through a non-direct aggregate type; opaque " - "storage emitted", - ), - ), - ) - nested_ref, nested_shape = nested_tail_shapes[0] - if nested_ref.decl_id != direct_ref.decl_id: - return self._cache_by_value( - decl_id, - ByValueRecordShape( - False, - ( - f"field `{field_display_name(field, field_fact.index)}` embeds a " - "flexible-tail record through a non-direct aggregate type; opaque " - "storage emitted", - ), - ), - ) - if not _field_is_terminal_in_enclosing_record(facts, field_fact): - target_name = nested_ref.c_name or nested_ref.name - return self._cache_by_value( - decl_id, - ByValueRecordShape( - False, - ( - f"field `{field_display_name(field, field_fact.index)}` embeds struct " - f"`{target_name}` by value, but embedded flexible tail is not terminal " - "in the enclosing record; opaque storage emitted", - ), - ), - ) - flexible_tail = replace( - nested_shape.flexible_tail, # type: ignore - byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, # type: ignore - ) + def _plain_fields_by_value_shape( + self, + decl: Struct, + facts: RecordLayoutFacts, + *, + active: set[str], + ) -> ByValueRecordShape: + flexible_tail: FlexibleTail | None = None + for field_fact in facts.plain_fields: + field = decl.fields[field_fact.index] + lowered_type, reason = try_lower_type( + self.type_lowerer, + field.type, + subject=f"field `{field_display_name(field, field_fact.index)}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None or lowered_type is None: + return ByValueRecordShape(False, ((reason or "field could not be lowered"),)) + if field.fam_pattern is not None: + tail_shape = _direct_flexible_tail_shape(field, field_fact, lowered_type) + if not tail_shape.valid: + return tail_shape + flexible_tail = tail_shape.flexible_tail + continue + + nested_shape = self._nested_record_field_shape(field, field_fact, facts, active=active) + if not nested_shape.valid: + return nested_shape + if nested_shape.flexible_tail is not None: + flexible_tail = nested_shape.flexible_tail + return ByValueRecordShape(True, flexible_tail=flexible_tail) + + def _nested_record_field_shape( + self, + field, + field_fact, + facts: RecordLayoutFacts, + *, + active: set[str], + ) -> ByValueRecordShape: + refs = _embedded_struct_refs(field.type) + if not refs: + return ByValueRecordShape(True) + + nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]] = [] + for ref in refs: + shape = self._record_typed_shape_by_value(ref.decl_id, active=active) + if not shape.valid: + return _embedded_record_failure(field, field_fact, ref, shape) + if shape.flexible_tail is not None: + nested_tail_shapes.append((ref, shape)) + + if not nested_tail_shapes: + return ByValueRecordShape(True) + return _embedded_flexible_tail_shape(field, field_fact, facts, nested_tail_shapes) + + def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueRecordShape: + for run_layout in facts.bitfield_runs: + _, reason = try_lower_type( + self.type_lowerer, + run_layout.unsigned_storage_type, + subject=f"bitfield storage `{run_layout.name}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None: + return ByValueRecordShape(False, (reason,)) - for run_layout in facts.bitfield_runs: + for field_layout in run_layout.fields: _, reason = try_lower_type( self.type_lowerer, - run_layout.unsigned_storage_type, - subject=f"bitfield storage `{run_layout.name}`", + field_layout.logical_type, + subject=f"bitfield `{field_display_name(field_layout.field, field_layout.index)}`", failure_suffix="opaque storage emitted", ) if reason is not None: - return self._cache_by_value(decl_id, ByValueRecordShape(False, (reason,))) - - for field_layout in run_layout.fields: - _, reason = try_lower_type( - self.type_lowerer, - field_layout.logical_type, - subject=f"bitfield `{field_display_name(field_layout.field, field_layout.index)}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None: - return self._cache_by_value(decl_id, ByValueRecordShape(False, (reason,))) - finally: - active.remove(decl_id) - - return self._cache_by_value(decl_id, ByValueRecordShape(True, flexible_tail=flexible_tail)) + return ByValueRecordShape(False, (reason,)) + return ByValueRecordShape(True) def _layout(self, record: Struct) -> RecordLayoutFacts: return self.record_layouts[record.decl_id] @@ -398,6 +356,79 @@ def _lower_flexible_tail_metadata( ) +def _direct_flexible_tail_shape(field, field_fact, lowered_type) -> ByValueRecordShape: + direct_tail, tail_reason = _lower_flexible_tail_metadata( + field, + field_fact.index, + field_fact.byte_offset, + lowered_type, + ) + if tail_reason is not None or direct_tail is None: + return ByValueRecordShape(False, ((tail_reason or "flexible tail could not lower"),)) + return ByValueRecordShape(True, flexible_tail=direct_tail) + + +def _embedded_record_failure( + field, + field_fact, + ref: StructRef, + shape: ByValueRecordShape, +) -> ByValueRecordShape: + target_name = ref.c_name or ref.name + suffix = shape.detail[0] if shape.detail else "embedded record is not layout-emittable by value" + return ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds struct " + f"`{target_name}` by value, but {suffix}; opaque storage emitted", + ), + ) + + +def _embedded_flexible_tail_shape( + field, + field_fact, + facts: RecordLayoutFacts, + nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]], +) -> ByValueRecordShape: + direct_ref = _direct_embedded_struct_ref(field.type) + if len(nested_tail_shapes) > 1 or direct_ref is None: + return _non_direct_flexible_tail_failure(field, field_fact) + + nested_ref, nested_shape = nested_tail_shapes[0] + if nested_ref.decl_id != direct_ref.decl_id: + return _non_direct_flexible_tail_failure(field, field_fact) + if not _field_is_terminal_in_enclosing_record(facts, field_fact): + target_name = nested_ref.c_name or nested_ref.name + return ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds struct " + f"`{target_name}` by value, but embedded flexible tail is not terminal " + "in the enclosing record; opaque storage emitted", + ), + ) + + assert nested_shape.flexible_tail is not None + return ByValueRecordShape( + True, + flexible_tail=replace( + nested_shape.flexible_tail, + byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, + ), + ) + + +def _non_direct_flexible_tail_failure(field, field_fact) -> ByValueRecordShape: + return ByValueRecordShape( + False, + ( + f"field `{field_display_name(field, field_fact.index)}` embeds a flexible-tail " + "record through a non-direct aggregate type; opaque storage emitted", + ), + ) + + def _direct_embedded_struct_ref(t: Type) -> StructRef | None: while True: if isinstance(t, QualifiedType): diff --git a/mojo_bindgen/analysis/reference_validation.py b/mojo_bindgen/analysis/reference_validation.py index a88bcd7..507e17e 100644 --- a/mojo_bindgen/analysis/reference_validation.py +++ b/mojo_bindgen/analysis/reference_validation.py @@ -2,25 +2,16 @@ from __future__ import annotations +from mojo_bindgen.analysis.traversal import iter_decl_referenced_types from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes from mojo_bindgen.ir import ( - BinaryExpr, - CastExpr, - Const, - ConstExpr, Enum, EnumRef, - Function, - GlobalVar, - MacroDecl, OpaqueRecordRef, - SizeOfExpr, Struct, StructRef, Type, - Typedef, TypeRef, - UnaryExpr, Unit, ) @@ -44,19 +35,8 @@ def run(self, unit: Unit) -> Unit: enum_ids = {decl.decl_id for decl in unit.decls if isinstance(decl, Enum)} for decl in unit.decls: - for t in _decl_types(decl): - self._validate_type( - t, - struct_ids=struct_ids, - enum_ids=enum_ids, - ) - for expr in _decl_exprs(decl): - for t in _const_expr_types(expr): - self._validate_type( - t, - struct_ids=struct_ids, - enum_ids=enum_ids, - ) + for t in iter_decl_referenced_types(decl): + self._validate_type(t, struct_ids=struct_ids, enum_ids=enum_ids) return unit def _validate_type( @@ -89,54 +69,6 @@ def _validate_type( continue -def _decl_types(decl) -> tuple[Type, ...]: - if isinstance(decl, Function): - return (decl.ret, *(param.type for param in decl.params)) - if isinstance(decl, Typedef): - return (decl.aliased, decl.canonical) - if isinstance(decl, Struct): - return tuple(field.type for field in decl.fields) - if isinstance(decl, GlobalVar): - return (decl.type,) - if isinstance(decl, Const): - return (decl.type,) - if isinstance(decl, MacroDecl): - return () if decl.type is None else (decl.type,) - return () - - -def _decl_exprs(decl) -> tuple[ConstExpr, ...]: - if isinstance(decl, Const): - return (decl.expr,) - if isinstance(decl, GlobalVar) and decl.initializer is not None: - return (decl.initializer,) - if isinstance(decl, MacroDecl) and decl.expr is not None: - return (decl.expr,) - return () - - -def _const_expr_types(expr: ConstExpr) -> tuple[Type, ...]: - out: list[Type] = [] - - def walk(node: ConstExpr) -> None: - if isinstance(node, CastExpr): - out.append(node.target) - walk(node.expr) - return - if isinstance(node, SizeOfExpr): - out.append(node.target) - return - if isinstance(node, UnaryExpr): - walk(node.operand) - return - if isinstance(node, BinaryExpr): - walk(node.lhs) - walk(node.rhs) - - walk(expr) - return tuple(out) - - __all__ = [ "ReferenceValidationError", "ValidateReferencesPass", diff --git a/mojo_bindgen/analysis/struct_lowering.py b/mojo_bindgen/analysis/struct_lowering.py index fb8ecdc..678b8cc 100644 --- a/mojo_bindgen/analysis/struct_lowering.py +++ b/mojo_bindgen/analysis/struct_lowering.py @@ -120,11 +120,33 @@ def _lower_typed_members( tuple[str, ...], tuple[str, ...], ]: + plain_fields, diagnostic_notes, plain_failures = _lower_plain_fields( + decl, + facts, + context=context, + ) + lowered_bitfield_groups, bitfield_failures = _lower_bitfield_groups( + facts, + context=context, + ) + + return ( + plain_fields, + lowered_bitfield_groups, + tuple(diagnostic_notes), + (*plain_failures, *bitfield_failures), + ) + + +def _lower_plain_fields( + decl: Struct, + facts: RecordLayoutFacts, + *, + context: StructLoweringContext, +) -> tuple[list[StoredMember], tuple[str, ...], tuple[str, ...]]: plain_fields: list[StoredMember] = [] - lowered_bitfield_groups: list[BitfieldGroupMember] = [] diagnostic_notes: list[str] = [] fallback_reasons: list[str] = [] - for field_fact in facts.plain_fields: field = decl.fields[field_fact.index] display_name = field_display_name(field, field_fact.index) @@ -158,45 +180,25 @@ def _lower_typed_members( ) if field.fam_pattern is not None: continue + return plain_fields, tuple(diagnostic_notes), tuple(fallback_reasons) + +def _lower_bitfield_groups( + facts: RecordLayoutFacts, + *, + context: StructLoweringContext, +) -> tuple[list[BitfieldGroupMember], tuple[str, ...]]: + lowered_bitfield_groups: list[BitfieldGroupMember] = [] + fallback_reasons: list[str] = [] for run_layout in facts.bitfield_runs: - lowered_storage_type, reason = try_lower_type( - context.type_lowerer, - run_layout.unsigned_storage_type, - subject=f"bitfield storage `{run_layout.name}`", - failure_suffix="opaque storage emitted", - ) + lowered_storage_type, reason = _lower_bitfield_storage(run_layout, context=context) if reason is not None or lowered_storage_type is None: if reason is not None: fallback_reasons.append(reason) continue - lowered_fields: list[BitfieldField] = [] - for field_layout in run_layout.fields: - display_name = field_display_name(field_layout.field, field_layout.index) - logical_type, reason = try_lower_type( - context.type_lowerer, - field_layout.logical_type, - subject=f"bitfield `{display_name}`", - failure_suffix="opaque storage emitted", - ) - if reason is not None or logical_type is None: - if reason is not None: - fallback_reasons.append(reason) - continue - lowered_fields.append( - BitfieldField( - index=field_layout.index, - name=field_mojo_name(field_layout.field, field_layout.index), - logical_type=logical_type, - bit_offset=field_layout.bit_offset, - bit_width=field_layout.bit_width, - signed=field_layout.signed, - bool_semantics=field_layout.bool_semantics, - doc=field_layout.field.doc, - ) - ) - + lowered_fields, field_failures = _lower_bitfield_fields(run_layout, context=context) + fallback_reasons.extend(field_failures) lowered_bitfield_groups.append( BitfieldGroupMember( storage_name=run_layout.name, @@ -207,15 +209,52 @@ def _lower_typed_members( fields=lowered_fields, ) ) + return lowered_bitfield_groups, tuple(fallback_reasons) - return ( - plain_fields, - lowered_bitfield_groups, - tuple(diagnostic_notes), - tuple(fallback_reasons), + +def _lower_bitfield_storage(run_layout, *, context: StructLoweringContext): + return try_lower_type( + context.type_lowerer, + run_layout.unsigned_storage_type, + subject=f"bitfield storage `{run_layout.name}`", + failure_suffix="opaque storage emitted", ) +def _lower_bitfield_fields( + run_layout, + *, + context: StructLoweringContext, +) -> tuple[list[BitfieldField], tuple[str, ...]]: + lowered_fields: list[BitfieldField] = [] + fallback_reasons: list[str] = [] + for field_layout in run_layout.fields: + display_name = field_display_name(field_layout.field, field_layout.index) + logical_type, reason = try_lower_type( + context.type_lowerer, + field_layout.logical_type, + subject=f"bitfield `{display_name}`", + failure_suffix="opaque storage emitted", + ) + if reason is not None or logical_type is None: + if reason is not None: + fallback_reasons.append(reason) + continue + lowered_fields.append( + BitfieldField( + index=field_layout.index, + name=field_mojo_name(field_layout.field, field_layout.index), + logical_type=logical_type, + bit_offset=field_layout.bit_offset, + bit_width=field_layout.bit_width, + signed=field_layout.signed, + bool_semantics=field_layout.bool_semantics, + doc=field_layout.field.doc, + ) + ) + return lowered_fields, tuple(fallback_reasons) + + def _record_shape( decl: Struct, *, diff --git a/mojo_bindgen/analysis/traversal.py b/mojo_bindgen/analysis/traversal.py new file mode 100644 index 0000000..afa7580 --- /dev/null +++ b/mojo_bindgen/analysis/traversal.py @@ -0,0 +1,162 @@ +"""Shared traversal helpers for normalized CIR declarations and expressions.""" + +from __future__ import annotations + +from collections.abc import Iterator + +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes +from mojo_bindgen.ir import ( + BinaryExpr, + CallExpr, + CastExpr, + Const, + ConstExpr, + Decl, + Enum, + EnumRef, + Function, + GlobalVar, + MacroDecl, + RefExpr, + SizeOfExpr, + Struct, + StructRef, + Type, + Typedef, + TypeRef, + UnaryExpr, + Unit, +) + + +def decl_id(decl: Decl) -> str | None: + """Return the stable dependency-graph id for declarations that have one.""" + + if isinstance(decl, (Function, Struct, Enum, Typedef, GlobalVar)): + return decl.decl_id + if isinstance(decl, (Const, MacroDecl)): + return decl.name + return None + + +def iter_decl_types(decl: Decl) -> Iterator[Type]: + """Yield direct type slots owned by one top-level declaration.""" + + if isinstance(decl, Function): + yield decl.ret + for param in decl.params: + yield param.type + return + if isinstance(decl, Typedef): + yield decl.aliased + yield decl.canonical + return + if isinstance(decl, Struct): + for field in decl.fields: + yield field.type + return + if isinstance(decl, GlobalVar): + yield decl.type + return + if isinstance(decl, Const): + yield decl.type + return + if isinstance(decl, MacroDecl) and decl.type is not None: + yield decl.type + + +def iter_decl_const_exprs(decl: Decl) -> Iterator[ConstExpr]: + """Yield direct constant-expression slots owned by one declaration.""" + + if isinstance(decl, Const): + yield decl.expr + return + if isinstance(decl, GlobalVar) and decl.initializer is not None: + yield decl.initializer + return + if isinstance(decl, MacroDecl) and decl.expr is not None: + yield decl.expr + + +def iter_const_expr_nodes(expr: ConstExpr) -> Iterator[ConstExpr]: + """Yield a constant expression and descendants in preorder.""" + + yield expr + if isinstance(expr, CastExpr): + yield from iter_const_expr_nodes(expr.expr) + return + if isinstance(expr, UnaryExpr): + yield from iter_const_expr_nodes(expr.operand) + return + if isinstance(expr, BinaryExpr): + yield from iter_const_expr_nodes(expr.lhs) + yield from iter_const_expr_nodes(expr.rhs) + return + if isinstance(expr, CallExpr): + yield from iter_const_expr_nodes(expr.callee) + for arg in expr.args: + yield from iter_const_expr_nodes(arg) + + +def iter_const_expr_types(expr: ConstExpr) -> Iterator[Type]: + """Yield type slots referenced by a constant-expression tree.""" + + for node in iter_const_expr_nodes(expr): + if isinstance(node, CastExpr): + yield node.target + elif isinstance(node, SizeOfExpr): + yield node.target + + +def iter_const_expr_refs(expr: ConstExpr) -> Iterator[RefExpr]: + """Yield symbol references from a constant-expression tree.""" + + for node in iter_const_expr_nodes(expr): + if isinstance(node, RefExpr): + yield node + + +def iter_decl_referenced_types(decl: Decl) -> Iterator[Type]: + """Yield declaration type slots plus type slots nested in const expressions.""" + + yield from iter_decl_types(decl) + for expr in iter_decl_const_exprs(decl): + yield from iter_const_expr_types(expr) + + +def iter_unit_typerefs(unit: Unit) -> Iterator[TypeRef]: + """Yield all ``TypeRef`` nodes used by declarations and const expressions.""" + + for decl in unit.decls: + for t in iter_decl_referenced_types(decl): + for node in iter_type_nodes( + t, + options=TypeWalkOptions(descend_vector_element=True), + ): + if isinstance(node, TypeRef): + yield node + + +def iter_type_decl_refs( + t: Type, + *, + options: TypeWalkOptions | None = None, +) -> Iterator[TypeRef | StructRef | EnumRef]: + """Yield declaration-reference type nodes inside one type tree.""" + + for node in iter_type_nodes(t, options=options): + if isinstance(node, (TypeRef, StructRef, EnumRef)): + yield node + + +__all__ = [ + "decl_id", + "iter_const_expr_nodes", + "iter_const_expr_refs", + "iter_const_expr_types", + "iter_decl_const_exprs", + "iter_decl_referenced_types", + "iter_decl_types", + "iter_type_decl_refs", + "iter_unit_typerefs", +] diff --git a/mojo_bindgen/analysis/union_lowering.py b/mojo_bindgen/analysis/union_lowering.py index 6c27a8b..eac0ffb 100644 --- a/mojo_bindgen/analysis/union_lowering.py +++ b/mojo_bindgen/analysis/union_lowering.py @@ -32,6 +32,13 @@ class UnionLoweringError(ValueError): """Raised when a CIR union declaration cannot be lowered to MojoIR.""" +@dataclass(frozen=True) +class _UnionArmPlan: + arms: tuple[Type, ...] + diagnostics: tuple + eligible: bool + + @dataclass class LowerUnionPass: """Lower top-level CIR union declarations to MojoIR alias declarations.""" @@ -45,26 +52,19 @@ def run(self, decl: Struct) -> AliasDecl: ) if not decl.is_complete: - return AliasDecl( - name=record_name(decl), - kind=AliasKind.UNION_LAYOUT, - diagnostics=[stub_note("incomplete union placeholder emitted; layout not lowered")], - doc=decl.doc, - ) + return _incomplete_union_alias(decl) alias_name = record_name(decl) - diagnostics = [] + plan = self._collect_union_arms(decl, alias_name=alias_name) + if plan.eligible: + return _unsafe_union_alias(decl, alias_name=alias_name, plan=plan) + return _byte_storage_union_alias(decl, alias_name=alias_name, plan=plan) + + def _collect_union_arms(self, decl: Struct, *, alias_name: str) -> _UnionArmPlan: + diagnostics = _size_diagnostics(decl) arms: list[Type] = [] seen_keys: dict[str, str] = {} - eligible = True - - if decl.size_bytes <= 0: - eligible = False - diagnostics.append( - union_note( - f"union `{decl.c_name}` has non-representable byte size {decl.size_bytes}; using byte-storage fallback" - ) - ) + eligible = not diagnostics for index, field in enumerate(decl.fields): field_name = field_display_name(field, index) @@ -92,50 +92,76 @@ def run(self, decl: Struct) -> AliasDecl: key = self._type_key(lowered) prior_name = seen_keys.get(key) if prior_name is not None: - diagnostics.append( - union_note( - f"union member `{field_name}` duplicates lowered type of earlier member `{prior_name}`;" - ) - ) + diagnostics.append(_duplicate_member_note(field_name, prior_name)) continue seen_keys[key] = field_name arms.append(lowered) - if eligible: - return AliasDecl( - name=alias_name, - kind=AliasKind.UNION_LAYOUT, - type_value=ParametricType( - base=ParametricBase.UNSAFE_UNION, - args=[TypeArg(type=arm) for arm in arms], - ), - diagnostics=diagnostics, - doc=decl.doc, - ) - - return AliasDecl( - name=alias_name, - kind=AliasKind.UNION_LAYOUT, - type_value=Array( - element=BuiltinType(MojoBuiltin.UINT8), - size=decl.size_bytes, - array_kind="fixed", - ), - diagnostics=[ - *diagnostics, - union_note( - f"union `{decl.c_name}` lowered as InlineArray[UInt8, {decl.size_bytes}] to preserve layout" - ), - ], - doc=decl.doc, - ) + return _UnionArmPlan(arms=tuple(arms), diagnostics=tuple(diagnostics), eligible=eligible) @staticmethod def _type_key(t: Type) -> str: return json.dumps(t.to_json_dict(), sort_keys=True) +def _incomplete_union_alias(decl: Struct) -> AliasDecl: + return AliasDecl( + name=record_name(decl), + kind=AliasKind.UNION_LAYOUT, + diagnostics=[stub_note("incomplete union placeholder emitted; layout not lowered")], + doc=decl.doc, + ) + + +def _unsafe_union_alias(decl: Struct, *, alias_name: str, plan: _UnionArmPlan) -> AliasDecl: + return AliasDecl( + name=alias_name, + kind=AliasKind.UNION_LAYOUT, + type_value=ParametricType( + base=ParametricBase.UNSAFE_UNION, + args=[TypeArg(type=arm) for arm in plan.arms], + ), + diagnostics=list(plan.diagnostics), + doc=decl.doc, + ) + + +def _byte_storage_union_alias(decl: Struct, *, alias_name: str, plan: _UnionArmPlan) -> AliasDecl: + return AliasDecl( + name=alias_name, + kind=AliasKind.UNION_LAYOUT, + type_value=Array( + element=BuiltinType(MojoBuiltin.UINT8), + size=decl.size_bytes, + array_kind="fixed", + ), + diagnostics=[ + *plan.diagnostics, + union_note( + f"union `{decl.c_name}` lowered as InlineArray[UInt8, {decl.size_bytes}] to preserve layout" + ), + ], + doc=decl.doc, + ) + + +def _size_diagnostics(decl: Struct) -> list: + if decl.size_bytes > 0: + return [] + return [ + union_note( + f"union `{decl.c_name}` has non-representable byte size {decl.size_bytes}; using byte-storage fallback" + ) + ] + + +def _duplicate_member_note(field_name: str, prior_name: str): + return union_note( + f"union member `{field_name}` duplicates lowered type of earlier member `{prior_name}`;" + ) + + def lower_union(decl: Struct, *, type_lowerer: LowerTypePass | None = None) -> AliasDecl: """Lower one top-level union declaration to a MojoIR alias.""" diff --git a/mojo_bindgen/analysis/unit_lowering.py b/mojo_bindgen/analysis/unit_lowering.py index b0e9f15..78d9056 100644 --- a/mojo_bindgen/analysis/unit_lowering.py +++ b/mojo_bindgen/analysis/unit_lowering.py @@ -7,7 +7,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.common import mojo_ident +from mojo_bindgen.analysis.alias_lowering import lower_typedef_alias from mojo_bindgen.analysis.const_lowering import ( LowerConstExprPass, ) @@ -21,26 +21,10 @@ from mojo_bindgen.analysis.struct_lowering import ( StructLoweringContext, ) -from mojo_bindgen.analysis.type_lowering import LowerTypePass, exact_width_stdint_alias_type -from mojo_bindgen.analysis.type_walk import _walk_typeref_nodes +from mojo_bindgen.analysis.type_lowering import LowerTypePass from mojo_bindgen.ir import ( - AliasDecl, - AliasKind, - BinaryExpr, - CastExpr, - Const, - ConstExpr, - Function, - FunctionPtr, - GlobalVar, - MacroDecl, MojoDecl, MojoModule, - SizeOfExpr, - Struct, - Typedef, - TypeRef, - UnaryExpr, Unit, ) @@ -76,7 +60,7 @@ def run(self, unit: Unit) -> MojoModule: ), ) decl_lowerer = UnitDeclLowerer(session) - lowered_decls = self._synth_external_typedef_aliases(unit, type_lowerer) + lowered_decls = self._synth_external_typedef_aliases(context, type_lowerer) for decl in unit.decls: lowered = decl_lowerer.lower_decl(decl) if lowered is None: @@ -96,90 +80,22 @@ def run(self, unit: Unit) -> MojoModule: def _synth_external_typedef_aliases( self, - unit: Unit, + context: AnalysisContext, type_lowerer: LowerTypePass, ) -> list[MojoDecl]: - local_typedef_ids = {decl.decl_id for decl in unit.decls if isinstance(decl, Typedef)} - seen_typedef_ids: set[str] = set() lowered: list[MojoDecl] = [] - for ref in _collect_typeref_uses(unit): - if ref.decl_id in local_typedef_ids or ref.decl_id in seen_typedef_ids: - continue - seen_typedef_ids.add(ref.decl_id) - alias_name = mojo_ident(ref.name) - lowered_type = exact_width_stdint_alias_type(ref.name) - if lowered_type is None: - lowered_type = type_lowerer.run(ref.canonical) - if isinstance(lowered_type, FunctionPtr): - lowered.append( - AliasDecl( - name=alias_name, - kind=AliasKind.CALLBACK_SIGNATURE, - type_value=lowered_type, - ) - ) - continue - if getattr(lowered_type, "name", None) == alias_name: - continue - lowered.append( - AliasDecl( - name=alias_name, - kind=AliasKind.TYPE_ALIAS, - type_value=lowered_type, - ) + for ref in context.alias_classification.external_type_refs_by_decl_id.values(): + alias = lower_typedef_alias( + c_name=ref.name, + aliased=ref.canonical, + type_lowerer=type_lowerer, ) + if alias is None: + continue + lowered.append(alias) return lowered -def _walk_const_expr_typerefs(expr: ConstExpr, out: list[TypeRef]) -> None: - if isinstance(expr, CastExpr): - _walk_typeref_nodes(expr.target, out) - _walk_const_expr_typerefs(expr.expr, out) - return - if isinstance(expr, SizeOfExpr): - _walk_typeref_nodes(expr.target, out) - return - if isinstance(expr, UnaryExpr): - _walk_const_expr_typerefs(expr.operand, out) - return - if isinstance(expr, BinaryExpr): - _walk_const_expr_typerefs(expr.lhs, out) - _walk_const_expr_typerefs(expr.rhs, out) - - -def _collect_typeref_uses(unit: Unit) -> list[TypeRef]: - collected: list[TypeRef] = [] - for decl in unit.decls: - if isinstance(decl, Function): - _walk_typeref_nodes(decl.ret, collected) - for param in decl.params: - _walk_typeref_nodes(param.type, collected) - continue - if isinstance(decl, Typedef): - _walk_typeref_nodes(decl.aliased, collected) - _walk_typeref_nodes(decl.canonical, collected) - continue - if isinstance(decl, Struct): - for field in decl.fields: - _walk_typeref_nodes(field.type, collected) - continue - if isinstance(decl, GlobalVar): - _walk_typeref_nodes(decl.type, collected) - if decl.initializer is not None: - _walk_const_expr_typerefs(decl.initializer, collected) - continue - if isinstance(decl, Const): - _walk_typeref_nodes(decl.type, collected) - _walk_const_expr_typerefs(decl.expr, collected) - continue - if isinstance(decl, MacroDecl): - if decl.type is not None: - _walk_typeref_nodes(decl.type, collected) - if decl.expr is not None: - _walk_const_expr_typerefs(decl.expr, collected) - return collected - - def lower_unit( unit: Unit, *, diff --git a/mojo_bindgen/analysis/validate_ir.py b/mojo_bindgen/analysis/validate_ir.py index d1f779b..fbbe843 100644 --- a/mojo_bindgen/analysis/validate_ir.py +++ b/mojo_bindgen/analysis/validate_ir.py @@ -2,12 +2,12 @@ from __future__ import annotations -from mojo_bindgen.analysis.type_walk import iter_type_nodes +from mojo_bindgen.analysis.traversal import iter_decl_referenced_types +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes from mojo_bindgen.ir import ( - Function, - GlobalVar, + Const, + MacroDecl, OpaqueRecordRef, - Param, Struct, StructRef, Type, @@ -37,12 +37,7 @@ def run(self, unit: Unit) -> Unit: struct_ids: set[str] = set() for decl in unit.decls: - decl_id = getattr(decl, "decl_id", None) - if decl_id: - prior = decl_ids.get(decl_id) - if prior is not None and prior != decl: - raise IRValidationError(f"duplicate decl_id {decl_id!r} in Unit") - decl_ids[decl_id] = decl + self._validate_decl_identity(decl, decl_ids) if isinstance(decl, Typedef): if not decl.decl_id: raise IRValidationError(f"typedef {decl.name!r} is missing decl_id") @@ -52,26 +47,24 @@ def run(self, unit: Unit) -> Unit: struct_ids.add(decl.decl_id) for decl in unit.decls: - if isinstance(decl, Struct): - for field in decl.fields: - self._validate_type(field.type, struct_ids) - elif isinstance(decl, Typedef): - self._validate_type(decl.aliased, struct_ids) - self._validate_type(decl.canonical, struct_ids) - elif isinstance(decl, Function): - self._validate_type(decl.ret, struct_ids) - for param in decl.params: - self._validate_param(param, struct_ids) - elif isinstance(decl, GlobalVar): - self._validate_type(decl.type, struct_ids) + for t in iter_decl_referenced_types(decl): + self._validate_type(t, struct_ids) return unit - def _validate_param(self, param: Param, struct_ids: set[str]) -> None: - self._validate_type(param.type, struct_ids) + def _validate_decl_identity(self, decl, decl_ids: dict[str, object]) -> None: + if isinstance(decl, (Const, MacroDecl)): + return + decl_id = getattr(decl, "decl_id", None) + if not decl_id: + return + prior = decl_ids.get(decl_id) + if prior is not None and prior != decl: + raise IRValidationError(f"duplicate decl_id {decl_id!r} in Unit") + decl_ids[decl_id] = decl def _validate_type(self, t: Type, struct_ids: set[str]) -> None: - for node in iter_type_nodes(t): + for node in iter_type_nodes(t, options=TypeWalkOptions(descend_vector_element=True)): if isinstance(node, TypeRef) and not node.decl_id: raise IRValidationError(f"TypeRef {node.name!r} is missing decl_id") if isinstance(node, StructRef) and not node.decl_id: diff --git a/tests/unit/passes/test_pipeline.py b/tests/unit/passes/test_pipeline.py index 4abed8a..96ed977 100644 --- a/tests/unit/passes/test_pipeline.py +++ b/tests/unit/passes/test_pipeline.py @@ -12,6 +12,7 @@ ByteOrder, CallExpr, CastExpr, + Const, Enum, Enumerant, EnumRef, @@ -21,15 +22,18 @@ IntKind, IntLiteral, IntType, + MacroDecl, MojoBuiltin, NamedType, Param, RefExpr, + SizeOfExpr, Struct, TargetABI, Typedef, TypeRef, Unit, + VectorType, VoidType, ) @@ -96,6 +100,58 @@ def test_validate_ir_pass_rejects_duplicate_decl_ids() -> None: assert "duplicate decl_id" in str(exc_info.value) +@pytest.mark.parametrize( + "decl", + [ + Const( + name="BAD_CONST_TYPE", + type=TypeRef(decl_id="", name="missing_t", canonical=_i32()), + expr=IntLiteral(1), + ), + Const( + name="BAD_CAST", + type=_i32(), + expr=CastExpr( + target=TypeRef(decl_id="", name="missing_t", canonical=_i32()), + expr=IntLiteral(1), + ), + ), + Const( + name="BAD_SIZEOF", + type=_i32(), + expr=SizeOfExpr(target=TypeRef(decl_id="", name="missing_t", canonical=_i32())), + ), + Const( + name="BAD_VECTOR", + type=VectorType( + element=TypeRef(decl_id="", name="missing_t", canonical=_i32()), + count=4, + size_bytes=16, + ), + expr=IntLiteral(1), + ), + MacroDecl( + name="BAD_MACRO", + tokens=["1"], + kind="object_like_supported", + type=TypeRef(decl_id="", name="missing_t", canonical=_i32()), + expr=IntLiteral(1), + ), + ], +) +def test_validate_ir_pass_rejects_missing_decl_ids_in_all_decl_type_positions(decl) -> None: + unit = Unit( + source_header="t.h", + library="t", + link_name="t", + target_abi=_abi(), + decls=[decl], + ) + + with pytest.raises(IRValidationError, match="TypeRef"): + ValidateIRPass().run(unit) + + def test_run_ir_passes_returns_fresh_unit_without_mutating_raw_unit() -> None: i32 = _i32() enum_ref = EnumRef( diff --git a/tests/unit/passes/test_traversal.py b/tests/unit/passes/test_traversal.py new file mode 100644 index 0000000..7555ecf --- /dev/null +++ b/tests/unit/passes/test_traversal.py @@ -0,0 +1,103 @@ +from __future__ import annotations + +from mojo_bindgen.analysis.traversal import ( + decl_id, + iter_const_expr_refs, + iter_const_expr_types, + iter_decl_referenced_types, + iter_unit_typerefs, +) +from mojo_bindgen.ir import ( + BinaryExpr, + ByteOrder, + CastExpr, + Const, + Function, + IntKind, + IntLiteral, + IntType, + Param, + RefExpr, + SizeOfExpr, + TargetABI, + TypeRef, + Unit, + VoidType, +) + + +def _i32() -> IntType: + return IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4) + + +def _unit(*decls) -> Unit: + return Unit( + source_header="input.h", + library="test", + link_name="test", + target_abi=TargetABI( + pointer_size_bytes=8, + pointer_align_bytes=8, + byte_order=ByteOrder.LITTLE, + ), + decls=list(decls), + ) + + +def test_decl_id_uses_decl_ids_for_declarations_and_names_for_constants() -> None: + fn = Function( + decl_id="function:f", + name="f", + link_name="f", + ret=VoidType(), + params=[], + ) + const = Const(name="SIZE", type=_i32(), expr=IntLiteral(1)) + + assert decl_id(fn) == "function:f" + assert decl_id(const) == "SIZE" + + +def test_const_expr_traversal_yields_types_and_symbol_refs() -> None: + expr = BinaryExpr( + op="+", + lhs=CastExpr(target=_i32(), expr=RefExpr("BASE")), + rhs=SizeOfExpr(target=TypeRef("typedef:Alias", "Alias", _i32())), + ) + + assert tuple(type(t).__name__ for t in iter_const_expr_types(expr)) == ( + "IntType", + "TypeRef", + ) + assert tuple(ref.name for ref in iter_const_expr_refs(expr)) == ("BASE",) + + +def test_decl_referenced_types_include_signature_and_const_expr_type_slots() -> None: + alias = TypeRef("typedef:Alias", "Alias", _i32()) + const = Const( + name="SIZE", + type=alias, + expr=SizeOfExpr(target=alias), + ) + + assert tuple(type(t).__name__ for t in iter_decl_referenced_types(const)) == ( + "TypeRef", + "TypeRef", + ) + + +def test_iter_unit_typerefs_finds_type_refs_inside_const_expressions() -> None: + alias = TypeRef("typedef:Alias", "Alias", _i32()) + fn = Function( + decl_id="function:f", + name="f", + link_name="f", + ret=VoidType(), + params=[Param(name="x", type=alias)], + ) + const = Const(name="SIZE", type=_i32(), expr=SizeOfExpr(target=alias)) + + assert tuple(ref.decl_id for ref in iter_unit_typerefs(_unit(fn, const))) == ( + "typedef:Alias", + "typedef:Alias", + ) From 978fa8f35c01705dcdb6bfd1dc7b2800d752a0b4 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Sat, 13 Jun 2026 16:12:59 +0200 Subject: [PATCH 3/6] Upgrade CLI frontend options --- README.md | 76 +++++-- examples/common.sh | 4 +- examples/zlib/generate.sh | 4 +- mojo_bindgen/cli.py | 325 ++++++++++++++++++++------- mojo_bindgen/orchestrator.py | 46 +++- mojo_bindgen/parsing/frontend.py | 64 +++++- mojo_bindgen/utils.py | 3 +- tests/e2e/test_e2e_runtime.py | 10 +- tests/unit/test_cli.py | 198 ++++++++++++++-- tests/unit/test_codegen_generator.py | 35 +++ tests/unit/test_parser_paths.py | 41 ++++ 11 files changed, 668 insertions(+), 138 deletions(-) diff --git a/README.md b/README.md index 4bc0ffa..7f531e7 100644 --- a/README.md +++ b/README.md @@ -63,34 +63,41 @@ For development setup, checks, and Pixi workflows, see Generate bindings from a primary header: ```bash -mojo-bindgen path/to/header.h -o bindings.mojo --linking external_call|owned_dl_handle +mojo-bindgen path/to/header.h --output bindings.mojo --link-mode external-call ``` -Pass include paths and other Clang flags with repeated `--compile-arg`: +Pass common Clang inputs with structured flags: ```bash mojo-bindgen include/mylib.h \ - --compile-arg=-I./include \ - --compile-arg=-DMYLIB_FEATURE=1 \ - -o mylib_bindings.mojo + --include-dir ./include \ + --define MYLIB_FEATURE=1 \ + --std c11 \ + --output mylib_bindings.mojo ``` Emit declarations from additional include headers with repeated -`--include-header`. Dependency headers are still parsed for type information +`--emit-header`. Dependency headers are still parsed for type information and macro expansion, but are not emitted unless listed: ```bash mojo-bindgen include/mylib.h \ - --include-header include/mylib_extra.h \ - --compile-arg=-I./include \ - -o mylib_bindings.mojo + --emit-header include/mylib_extra.h \ + --include-dir ./include \ + --output mylib_bindings.mojo ``` By default the parser uses `-std=gnu11` when no C standard is provided. Pin a standard explicitly if your header requires one: ```bash -mojo-bindgen include/mylib.h --compile-arg=-std=c99 -o mylib_bindings.mojo +mojo-bindgen include/mylib.h --std c99 --output mylib_bindings.mojo +``` + +Use `--clang-arg` for raw Clang flags that do not have a structured option: + +```bash +mojo-bindgen include/mylib.h --clang-arg=-fms-extensions --output mylib_bindings.mojo ``` ## Linking modes @@ -108,13 +115,13 @@ Examples: ```bash # default -mojo-bindgen include/mylib.h --linking external_call -o mylib_bindings.mojo +mojo-bindgen include/mylib.h --link-mode external-call --output mylib_bindings.mojo # runtime-loaded shared library mojo-bindgen include/mylib.h \ - --linking owned_dl_handle \ - --library-path-hint /usr/lib/libmylib.so \ - -o mylib_bindings_dl.mojo + --link-mode owned-dl-handle \ + --library-path /usr/lib/libmylib.so \ + --output mylib_bindings_dl.mojo ``` ## What works today? @@ -125,7 +132,7 @@ generating bindings. Current support includes: -- **Parsing and lowering:** real C parsing through `libclang`, repeatable `--compile-arg` +- **Parsing and lowering:** real C parsing through `libclang`, repeatable `--clang-arg` support, and a structured IR pipeline rather than text-only generation. - **Primitive types:** scalar types, typedef chains, pointers with const-aware mutability, fixed arrays, incomplete-array decay cases, complex values, @@ -184,8 +191,8 @@ verify emitted layouts and symbols against your target toolchain. - **Linkage and compiler edge cases:** `inline`, compiler-specific linkage hints, and other extension-heavy cases can still require manual review and may lead to symbol mismatches at runtime. -- **Include-header model:** declarations are emitted from the primary header - you pass to the tool plus any headers named with `--include-header`. Other +- **Emit-header model:** declarations are emitted from the primary header + you pass to the tool plus any headers named with `--emit-header`. Other headers included by those files are parsed as dependencies but not emitted. ## Real-world examples @@ -222,14 +229,41 @@ matrix. ### The generated module is empty or missing declarations `mojo-bindgen` emits declarations from the primary header you pass in and any -headers listed with `--include-header`. If a thin wrapper only includes another +headers listed with `--emit-header`. If a thin wrapper only includes another header whose declarations should be emitted, pass that included header with -`--include-header` or use it as the primary header directly. +`--emit-header` or use it as the primary header directly. ### Parsing fails on project headers Most parser failures are missing include paths, target flags, or defines. Add -the same flags your C build uses via repeated `--compile-arg`. +the same flags your C build uses with `--include-dir`, `--define`, +`--undefine`, `--target`, `--sysroot`, `--std`, or repeated `--clang-arg`. + +### Debugging parser failures + +Print the normalized Clang arguments that `mojo-bindgen` will use: + +```bash +mojo-bindgen include/mylib.h --include-dir ./include --dump-clang-args +``` + +Write diagnostics as JSON while still generating normal output: + +```bash +mojo-bindgen include/mylib.h \ + --diagnostics json \ + --diagnostics-output diagnostics.json \ + --output mylib_bindings.mojo +``` + +Dump the preprocessed input that Clang sees: + +```bash +mojo-bindgen include/mylib.h \ + --include-dir ./include \ + --dump-preprocessed mylib.preprocessed.c \ + --output mylib_bindings.mojo +``` ### Build succeeds but symbols are missing at runtime @@ -237,7 +271,7 @@ Double-check: - `--library` and `--link-name` - your Mojo link flags for `external_call` -- your `--library-path-hint` for `owned_dl_handle` +- your `--library-path` for `owned_dl_handle` - whether the original C declaration involved tricky `inline` or exotic layout that needs manual review. ## License diff --git a/examples/common.sh b/examples/common.sh index dd99dbc..891796c 100644 --- a/examples/common.sh +++ b/examples/common.sh @@ -44,10 +44,10 @@ generate_bindings() { "${BG[@]}" "$header" \ --library "$library" \ --link-name "$link_name" \ - --layout-tests \ + --emit-layout-tests \ --layout-test-output "${output%.mojo}_layout_tests.mojo" \ "$@" \ - -o "$output" + --output "$output" } build_bindings_object() { diff --git a/examples/zlib/generate.sh b/examples/zlib/generate.sh index 6cf0c26..bf9decc 100755 --- a/examples/zlib/generate.sh +++ b/examples/zlib/generate.sh @@ -49,7 +49,7 @@ find_zlib_h() { return 1 } -# Path to the zlib shared library used as `--library-path-hint`. +# Path to the zlib shared library used as `--library-path`. find_libz_so() { local p if command -v gcc >/dev/null 2>&1; then @@ -100,7 +100,7 @@ echo "Wrote $HERE/zlib_bindings.mojo, zlib_bindings_layout_tests.mojo, and zlib_ LIBZ_SO="$(find_libz_so)" || true if [[ -n "${LIBZ_SO:-}" ]]; then generate_bindings "$ZLIB_H" z z zlib_bindings_dl.mojo \ - --linking owned_dl_handle --library-path-hint "$LIBZ_SO" + --link-mode owned-dl-handle --library-path "$LIBZ_SO" "${MOJO[@]}" build zlib_dl_smoke.mojo -I "$HERE" -o zlib_dl_smoke echo "Wrote $HERE/zlib_bindings_dl.mojo (dlopen: $LIBZ_SO) and built zlib_dl_smoke" echo "Running zlib_dl_smoke (owned_dl_handle runtime proof)" diff --git a/mojo_bindgen/cli.py b/mojo_bindgen/cli.py index f80c688..a574388 100644 --- a/mojo_bindgen/cli.py +++ b/mojo_bindgen/cli.py @@ -2,6 +2,7 @@ from __future__ import annotations +import json import sys from pathlib import Path from typing import Annotated, Literal @@ -10,33 +11,89 @@ from rich.console import Console from mojo_bindgen.orchestrator import BindgenOptions, BindgenOrchestrator +from mojo_bindgen.parsing.frontend import ClangOptions from mojo_bindgen.parsing.parser import ParseError stderr_console = Console(stderr=True) +OutputFormat = Literal["mojo", "cir-json", "mojo-ir-json"] +LinkModeOption = Literal["external-call", "owned-dl-handle"] +DiagnosticsMode = Literal["text", "json", "silent"] + def run( header: Annotated[Path, typer.Argument(help="Path to the primary C header file")], - include_header: Annotated[ + emit_header: Annotated[ list[Path] | None, typer.Option( - "--include-header", + "--emit-header", metavar="PATH", help=( - "Additional C header to emit from (repeatable). " + "Additional public C header to emit from (repeatable). " "Private dependencies remain parsed but not emitted." ), ), ] = None, + clang_arg: Annotated[ + list[str] | None, + typer.Option( + "--clang-arg", + metavar="FLAG", + help="Raw Clang flag escape hatch (repeatable).", + ), + ] = None, + include_dir: Annotated[ + list[Path] | None, + typer.Option("--include-dir", metavar="PATH", help="Add a Clang include directory."), + ] = None, + define: Annotated[ + list[str] | None, + typer.Option("--define", metavar="NAME[=VALUE]", help="Define a C preprocessor macro."), + ] = None, + undefine: Annotated[ + list[str] | None, + typer.Option("--undefine", metavar="NAME", help="Undefine a C preprocessor macro."), + ] = None, + sysroot: Annotated[ + Path | None, + typer.Option("--sysroot", metavar="PATH", help="Set the Clang sysroot."), + ] = None, + target: Annotated[ + str | None, + typer.Option("--target", metavar="TRIPLE", help="Set the Clang target triple."), + ] = None, + std: Annotated[ + str | None, + typer.Option("--std", metavar="STD", help="Set the C language standard."), + ] = None, output: Annotated[ Path | None, typer.Option( "--output", - "-o", - help="Write output to this file (default: stdout).", + help="Write primary output to this file (default: stdout).", show_default=False, ), ] = None, + output_format: Annotated[ + OutputFormat, + typer.Option("--format", help="Primary output format.", case_sensitive=False), + ] = "mojo", + layout_test_output: Annotated[ + Path | None, + typer.Option( + "--layout-test-output", + metavar="PATH", + help="Write layout-test sidecar to this path.", + show_default=False, + ), + ] = None, + emit_layout_tests: Annotated[ + bool | None, + typer.Option( + "--emit-layout-tests/--no-emit-layout-tests", + help="Write a Mojo record-layout test sidecar with file output.", + ), + ] = None, library: Annotated[ str | None, typer.Option( @@ -53,44 +110,63 @@ def run( help="Shared-library link name (default: same as --library).", ), ] = None, - compile_arg: Annotated[ - list[str] | None, + link_mode: Annotated[ + LinkModeOption, + typer.Option("--link-mode", help="FFI linking mode for Mojo output.", case_sensitive=False), + ] = "external-call", + library_path: Annotated[ + str | None, typer.Option( - "--compile-arg", - metavar="FLAG", - help="Extra clang flag (repeatable). If omitted, use built-in system include probing.", + "--library-path", + metavar="PATH", + help="Path hint for OwnedDLHandle when --link-mode owned-dl-handle.", ), ] = None, - json_output: Annotated[ - bool, - typer.Option( - "--json", - help="Emit IR as JSON instead of Mojo source.", - ), - ] = False, - linking: Annotated[ - Literal["external_call", "owned_dl_handle"], - typer.Option( - "--linking", - help="FFI linking mode for Mojo output (ignored with --json).", - case_sensitive=False, - ), - ] = "external_call", - library_path_hint: Annotated[ - str | None, + diagnostics: Annotated[ + DiagnosticsMode, + typer.Option("--diagnostics", help="Diagnostic output mode.", case_sensitive=False), + ] = "text", + diagnostics_output: Annotated[ + Path | None, typer.Option( - "--library-path-hint", + "--diagnostics-output", metavar="PATH", - help="Path hint for OwnedDLHandle when --linking owned_dl_handle.", + help="Write diagnostics to this path instead of stderr.", + show_default=False, ), ] = None, - strict_abi: Annotated[ + warnings_as_errors: Annotated[ bool, + typer.Option("--warnings-as-errors", help="Exit nonzero when warnings are reported."), + ] = False, + keep_going: Annotated[ + bool, + typer.Option("--keep-going", help="Emit partial artifacts when frontend errors permit it."), + ] = False, + dry_run: Annotated[ + bool, + typer.Option("--dry-run", help="Run the pipeline without writing files."), + ] = False, + dump_clang_args: Annotated[ + bool, + typer.Option("--dump-clang-args", help="Print normalized Clang arguments and exit."), + ] = False, + dump_cir: Annotated[ + Path | None, + typer.Option("--dump-cir", metavar="PATH", help="Write raw parsed CIR JSON sidecar."), + ] = None, + dump_mojo_ir: Annotated[ + Path | None, + typer.Option("--dump-mojo-ir", metavar="PATH", help="Write finalized MojoIR JSON sidecar."), + ] = None, + dump_preprocessed: Annotated[ + Path | None, typer.Option( - "--strict-abi", - help="Preserve parsed C alignment emission behavior. By default, omit @align for ordinary records and keep ABI comments only.", + "--dump-preprocessed", + metavar="PATH", + help="Write Clang-preprocessed source sidecar.", ), - ] = False, + ] = None, doc_comments: Annotated[ bool, typer.Option( @@ -98,33 +174,31 @@ def run( help="Emit captured C documentation comments into Mojo output.", ), ] = True, - layout_tests: Annotated[ - bool | None, + module_comment: Annotated[ + bool, typer.Option( - "--layout-tests/--no-layout-tests", - help="Write a Mojo record-layout test sidecar with file output.", + "--module-comment/--no-module-comment", + help="Emit generated-module header comments into Mojo output.", ), - ] = None, - layout_test_output: Annotated[ - Path | None, + ] = True, + strict_abi: Annotated[ + bool, typer.Option( - "--layout-test-output", - metavar="PATH", - help="Write layout-test sidecar to this path.", - show_default=False, + "--strict-abi/--no-strict-abi", + help="Preserve parsed C alignment emission behavior.", ), - ] = None, + ] = False, clang_macro_fallback: Annotated[ bool, typer.Option( - "--clang-macro-fallback", - help="Evaluate unsupported object-like integer macros through a temporary Clang translation unit.", + "--clang-macro-fallback/--no-clang-macro-fallback", + help="Evaluate unsupported object-like integer macros through Clang.", ), ] = False, - clang_macro_fallback_build_dir: Annotated[ + clang_macro_fallback_dir: Annotated[ Path | None, typer.Option( - "--clang-macro-fallback-build-dir", + "--clang-macro-fallback-dir", metavar="PATH", help="Directory for temporary Clang macro fallback probe files.", show_default=False, @@ -134,57 +208,146 @@ def run( """Generate Mojo FFI from a C header using libclang. Examples: - mojo-bindgen path/to/header.h -o bindings.mojo - mojo-bindgen path/to/header.h --json -o unit.json - mojo-bindgen include/me.h --include-header include/me_extra.h -o out.mojo - mojo-bindgen include/me.h --compile-arg=-I./include -o out.mojo + mojo-bindgen path/to/header.h --output bindings.mojo + mojo-bindgen path/to/header.h --format cir-json --output unit.json + mojo-bindgen include/me.h --emit-header include/me_extra.h --output out.mojo + mojo-bindgen include/me.h --include-dir ./include --define FEATURE=1 --std c11 """ + clang_options = ClangOptions( + std=std, + target=target, + sysroot=sysroot, + include_dirs=tuple(include_dir or ()), + defines=tuple(define or ()), + undefines=tuple(undefine or ()), + raw_args=tuple(clang_arg or ()), + ) + options = BindgenOptions( + header=header, + include_headers=emit_header, + library=library, + link_name=link_name, + clang_options=clang_options, + linking=_internal_link_mode(link_mode), + library_path_hint=library_path, + strict_abi=strict_abi, + module_comment=module_comment, + emit_doc_comments=doc_comments, + layout_tests=emit_layout_tests, + json_output=output_format != "mojo", + output=output, + layout_test_output=layout_test_output, + clang_macro_fallback=clang_macro_fallback, + clang_macro_fallback_build_dir=clang_macro_fallback_dir, + keep_going=keep_going, + ) + orchestrator = BindgenOrchestrator(options) + + if dump_clang_args: + sys.stdout.write(json.dumps(orchestrator.normalized_clang_args(), indent=2)) + sys.stdout.write("\n") + return 0 + try: - orchestrator = BindgenOrchestrator( - BindgenOptions( - header=header, - include_headers=include_header, - library=library, - link_name=link_name, - compile_args=compile_arg, - linking=linking, - library_path_hint=library_path_hint, - strict_abi=strict_abi, - module_comment=True, - emit_doc_comments=doc_comments, - layout_tests=layout_tests, - json_output=json_output, - output=output, - layout_test_output=layout_test_output, - clang_macro_fallback=clang_macro_fallback, - clang_macro_fallback_build_dir=clang_macro_fallback_build_dir, - ) - ) + if dump_preprocessed is not None: + _write_text(dump_preprocessed, orchestrator.dump_preprocessed(), dry_run=dry_run) result = orchestrator.run() - except (ParseError, FileNotFoundError, OSError) as e: - stderr_console.print(f"[bold red]mojo-bindgen error:[/bold red] {e}") + except (ParseError, FileNotFoundError, OSError, RuntimeError) as e: + args = orchestrator.normalized_clang_args() + stderr_console.print( + "[bold red]mojo-bindgen error:[/bold red] " + f"{e}\nprimary header: {header}\nclang args: {args}" + ) raise typer.Exit(code=1) from e - if json_output: - text = result.unit.to_json() - else: - text = result.bindings_source + _emit_diagnostics( + result.unit.diagnostics, + mode=diagnostics, + output=diagnostics_output, + dry_run=dry_run, + ) + if dump_cir is not None: + _write_text(dump_cir, result.unit.to_json(), dry_run=dry_run) + if dump_mojo_ir is not None: + _write_text(dump_mojo_ir, result.mojo_module.to_json(), dry_run=dry_run) + + text = _primary_output(result, output_format) if output is None: sys.stdout.write(text) if not text.endswith("\n"): sys.stdout.write("\n") else: - output.write_text(text, encoding="utf-8") + _write_text(output, text, dry_run=dry_run) + if result.layout_test_source is not None: sidecar = layout_test_output if sidecar is None and output is not None: sidecar = output.with_name(f"{output.stem}_layout_tests.mojo") if sidecar is not None: - sidecar.write_text(result.layout_test_source, encoding="utf-8") + _write_text(sidecar, result.layout_test_source, dry_run=dry_run) + + if warnings_as_errors and any(d.severity == "warning" for d in result.unit.diagnostics): + raise typer.Exit(code=1) return 0 +def _internal_link_mode(link_mode: LinkModeOption) -> Literal["external_call", "owned_dl_handle"]: + return "owned_dl_handle" if link_mode == "owned-dl-handle" else "external_call" + + +def _primary_output(result, output_format: OutputFormat) -> str: + if output_format == "cir-json": + return result.unit.to_json() + if output_format == "mojo-ir-json": + return result.mojo_module.to_json() + return result.bindings_source + + +def _write_text(path: Path, text: str, *, dry_run: bool) -> None: + if dry_run: + return + path.write_text(text, encoding="utf-8") + + +def _emit_diagnostics( + diagnostics, + *, + mode: DiagnosticsMode, + output: Path | None, + dry_run: bool, +) -> None: + if mode == "silent" or not diagnostics: + return + if mode == "json": + payload = [ + { + "stage": "frontend", + "severity": d.severity, + "message": d.message, + "file": d.file, + "line": d.line, + "col": d.col, + "decl_id": d.decl_id, + } + for d in diagnostics + ] + text = json.dumps(payload, indent=2) + else: + text = "\n".join(_format_diagnostic(d) for d in diagnostics) + if output is not None: + _write_text(output, text + "\n", dry_run=dry_run) + else: + stderr_console.print(text) + + +def _format_diagnostic(d) -> str: + file = d.file or "" + line = d.line or 0 + col = d.col or 0 + return f"{file}:{line}:{col}: {d.severity}: {d.message}" + + def main(argv: list[str] | None = None) -> int: cli_args = argv if argv is not None else sys.argv[1:] previous_argv = sys.argv[:] diff --git a/mojo_bindgen/orchestrator.py b/mojo_bindgen/orchestrator.py index de3844e..72c5fad 100644 --- a/mojo_bindgen/orchestrator.py +++ b/mojo_bindgen/orchestrator.py @@ -11,7 +11,13 @@ from mojo_bindgen.codegen.mojo_ir_printer import MojoIRPrintOptions, render_mojo_module from mojo_bindgen.ir import MojoModule, Unit from mojo_bindgen.layout_tests import render_layout_test_module -from mojo_bindgen.parsing.parser import ClangParser, _default_system_compile_args +from mojo_bindgen.parsing.frontend import ClangFrontend, ClangFrontendConfig, ClangOptions +from mojo_bindgen.parsing.parser import ( + ClangParser, + _default_system_compile_args, + _resolve_header_path, + _resolve_include_headers, +) LinkingMode = Literal["external_call", "owned_dl_handle"] @@ -22,6 +28,7 @@ class BindgenOptions: library: str | None = None link_name: str | None = None compile_args: list[str] | None = None + clang_options: ClangOptions | None = None include_headers: list[Path] | None = None linking: LinkingMode = "external_call" library_path_hint: str | None = None @@ -34,6 +41,7 @@ class BindgenOptions: layout_test_output: Path | None = None clang_macro_fallback: bool = False clang_macro_fallback_build_dir: Path | None = None + keep_going: bool = False @dataclass(frozen=True) @@ -82,12 +90,29 @@ def parse(self) -> Unit: link_name=link_name, include_headers=self._options.include_headers, compile_args=self._parse_compile_args(), - raise_on_error=True, + raise_on_error=not self._options.keep_going, clang_macro_fallback=self._options.clang_macro_fallback, clang_macro_fallback_build_dir=self._options.clang_macro_fallback_build_dir, ) return parser.run() + def normalized_clang_args(self) -> list[str]: + """Return exact normalized Clang args used for parser setup.""" + return self._parse_compile_args() + + def dump_preprocessed(self) -> str: + """Return preprocessed source using the same frontend header setup.""" + header = _resolve_header_path(self._options.header) + include_headers = _resolve_include_headers(header, self._options.include_headers) + frontend = ClangFrontend( + ClangFrontendConfig( + header=header, + compile_args=tuple(self._parse_compile_args()), + include_headers=tuple(include_headers), + ) + ) + return frontend.dump_preprocessed() + def analyze(self, unit: Unit) -> MojoModule: return AnalysisOrchestrator(self.emit_options).analyze(unit) @@ -176,18 +201,21 @@ def _library_name(self) -> str: def _link_name(self, library_name: str) -> str: return self._options.link_name if self._options.link_name is not None else library_name - def _parse_compile_args(self) -> list[str] | None: - compile_args = ( - _default_system_compile_args() - if self._options.compile_args is None - else list(self._options.compile_args) - ) + def _parse_compile_args(self) -> list[str]: + compile_args = self._base_compile_args() if not self._options.emit_doc_comments: - return None if self._options.compile_args is None else compile_args + return compile_args if self._RETAIN_SYSTEM_HEADER_COMMENTS_ARG in compile_args: return compile_args return [*compile_args, self._RETAIN_SYSTEM_HEADER_COMMENTS_ARG] + def _base_compile_args(self) -> list[str]: + if self._options.clang_options is not None: + return [*self._options.clang_options.to_args(), *_default_system_compile_args()] + if self._options.compile_args is not None: + return list(self._options.compile_args) + return _default_system_compile_args() + def bindgen(options: BindgenOptions) -> BindgenResult: return BindgenOrchestrator(options).run() diff --git a/mojo_bindgen/parsing/frontend.py b/mojo_bindgen/parsing/frontend.py index 14e67bf..3f94c87 100644 --- a/mojo_bindgen/parsing/frontend.py +++ b/mojo_bindgen/parsing/frontend.py @@ -16,7 +16,7 @@ import clang.cindex as cx -from mojo_bindgen.utils import build_c_parse_args +from mojo_bindgen.utils import build_c_parse_args, normalize_std_flag @dataclass(frozen=True) @@ -33,6 +33,38 @@ def __str__(self) -> str: return f"{self.file}:{self.line}:{self.col}: {self.severity}: {self.message}" +@dataclass(frozen=True) +class ClangOptions: + """Structured Clang argument options accepted by the public CLI.""" + + std: str | None = None + target: str | None = None + sysroot: Path | None = None + include_dirs: tuple[Path, ...] = () + defines: tuple[str, ...] = () + undefines: tuple[str, ...] = () + raw_args: tuple[str, ...] = () + + def to_args(self) -> list[str]: + """Return deterministic final Clang argv for parsing.""" + args = ["-x", "c"] + raw_args = [normalize_std_flag(arg) for arg in self.raw_args] + has_raw_std = any(arg.startswith("-std=") for arg in raw_args) + if self.std is not None: + args.append(f"-std={self.std}") + elif not has_raw_std: + args.append("-std=gnu11") + if self.target is not None: + args.append(f"--target={self.target}") + if self.sysroot is not None: + args.append(f"--sysroot={self.sysroot}") + args.extend(f"-I{path}" for path in self.include_dirs) + args.extend(f"-D{define}" for define in self.defines) + args.extend(f"-U{undefine}" for undefine in self.undefines) + args.extend(raw_args) + return args + + @dataclass(frozen=True) class ClangFrontendConfig: """Stable parser frontend configuration for one translation unit.""" @@ -117,10 +149,14 @@ def configured_headers(self) -> tuple[Path, ...]: def compile_args(self) -> tuple[str, ...]: return self.config.compile_args + def normalized_parse_args(self) -> list[str]: + """Return the exact Clang args passed to libclang.""" + return build_c_parse_args(list(self.compile_args), default_std="-std=gnu11") + def parse_translation_unit(self) -> cx.TranslationUnit: """Parse the configured header into a libclang translation unit.""" index = cx.Index.create() - args = build_c_parse_args(list(self.compile_args), default_std="-std=gnu11") + args = self.normalized_parse_args() options = _translation_unit_parse_options() if self.include_headers: umbrella_path = self._umbrella_header_path() @@ -136,6 +172,30 @@ def parse_translation_unit(self) -> cx.TranslationUnit: options=options, ) + def dump_preprocessed(self) -> str: + """Return preprocessed source for the configured primary or umbrella header.""" + if self.include_headers: + input_path = self._umbrella_header_path() + cmd = ["clang", "-E", *self.normalized_parse_args(), "-"] + source = self._umbrella_header() + proc = subprocess.run( + cmd, + input=source, + text=True, + capture_output=True, + check=False, + ) + else: + input_path = self.header + cmd = ["clang", "-E", *self.normalized_parse_args(), str(input_path)] + proc = subprocess.run(cmd, text=True, capture_output=True, check=False) + if proc.returncode != 0: + raise RuntimeError( + "clang preprocessing failed for " + f"{input_path} with args {self.normalized_parse_args()}:\n{proc.stderr}" + ) + return proc.stdout + def collect_diagnostics(self, tu: cx.TranslationUnit) -> list[FrontendDiagnostic]: """Normalize libclang diagnostics into plain dataclasses.""" out: list[FrontendDiagnostic] = [] diff --git a/mojo_bindgen/utils.py b/mojo_bindgen/utils.py index d26db0b..fd57885 100644 --- a/mojo_bindgen/utils.py +++ b/mojo_bindgen/utils.py @@ -32,7 +32,8 @@ def build_c_parse_args( """ normalized_args = [normalize_std_flag(arg) for arg in compile_args] has_std = any(arg.startswith("-std=") for arg in normalized_args) - prefix = ["-x", "c"] + has_language = "-x" in normalized_args or any(arg.startswith("-x") for arg in normalized_args) + prefix = [] if has_language else ["-x", "c"] if not has_std: prefix.append(default_std) return prefix + normalized_args diff --git a/tests/e2e/test_e2e_runtime.py b/tests/e2e/test_e2e_runtime.py index f5147d8..f026d81 100644 --- a/tests/e2e/test_e2e_runtime.py +++ b/tests/e2e/test_e2e_runtime.py @@ -185,7 +185,7 @@ def test_golden_runtime_case(case_dir: Path, tmp_path: Path) -> None: case_name, "--link-name", case_name, - "-o", + "--output", str(bindings_external), ], cwd=_REPO_ROOT, @@ -200,11 +200,11 @@ def test_golden_runtime_case(case_dir: Path, tmp_path: Path) -> None: case_name, "--link-name", case_name, - "--linking", - "owned_dl_handle", - "--library-path-hint", + "--link-mode", + "owned-dl-handle", + "--library-path", str(lib_path), - "-o", + "--output", str(bindings_dl), ], cwd=_REPO_ROOT, diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 4e128b0..441a821 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +from dataclasses import dataclass from pathlib import Path import mojo_bindgen.cli as cli @@ -16,10 +17,27 @@ def _strip_ansi(s: str) -> str: class _DummyUnit: + diagnostics = [] + def to_json(self) -> str: return '{"ok": true}' +class _DummyMojoModule: + def to_json(self) -> str: + return '{"mojo": true}' + + +@dataclass(frozen=True) +class _DummyDiagnostic: + severity: str + message: str + file: str | None = "demo.h" + line: int | None = 1 + col: int | None = 2 + decl_id: str | None = None + + def test_help_includes_examples(capsys) -> None: rc = cli.main(["--help"]) captured = capsys.readouterr() @@ -27,7 +45,7 @@ def test_help_includes_examples(capsys) -> None: plain = _strip_ansi(captured.out) assert "Generate Mojo FFI" in plain assert "Examples:" in plain - assert "--compile-arg" in plain + assert "--clang-arg" in plain def test_json_mode_uses_orchestrator_and_stdout(monkeypatch, capsys, tmp_path: Path) -> None: @@ -35,6 +53,7 @@ def test_json_mode_uses_orchestrator_and_stdout(monkeypatch, capsys, tmp_path: P class DummyResult: unit = _DummyUnit() + mojo_module = _DummyMojoModule() bindings_source = "generated" layout_test_source = None @@ -42,6 +61,9 @@ class DummyOrchestrator: def __init__(self, options) -> None: calls["options"] = options + def normalized_clang_args(self): + return self.run().unit.diagnostics + def run(self) -> DummyResult: return DummyResult() @@ -52,10 +74,19 @@ def run(self) -> DummyResult: rc = cli.main( [ str(header), - "--json", - "--include-header", + "--format", + "cir-json", + "--emit-header", str(extra), - "--compile-arg=-I./include", + "--clang-arg=-Werror", + "--include-dir", + "./include", + "--define", + "FEATURE=1", + "--undefine", + "OLD", + "--std", + "c11", ] ) captured = capsys.readouterr() @@ -66,15 +97,53 @@ def run(self) -> DummyResult: assert options.include_headers == [extra] assert options.library is None assert options.link_name is None - assert options.compile_args == ["-I./include"] + assert options.clang_options.to_args() == [ + "-x", + "c", + "-std=c11", + "-Iinclude", + "-DFEATURE=1", + "-UOLD", + "-Werror", + ] assert options.json_output is True +def test_dump_clang_args_prints_normalized_argv(monkeypatch, capsys, tmp_path: Path) -> None: + class DummyOrchestrator: + def __init__(self, options) -> None: + self.options = options + + def normalized_clang_args(self): + return self.options.clang_options.to_args() + + monkeypatch.setattr(cli, "BindgenOrchestrator", DummyOrchestrator) + + rc = cli.main( + [ + str(tmp_path / "demo.h"), + "--std", + "c99", + "--target", + "x86_64-unknown-linux-gnu", + "--sysroot", + "/sdk", + "--include-dir", + "/sdk/include", + "--dump-clang-args", + ] + ) + + assert rc == 0 + assert '"-std=c99"' in capsys.readouterr().out + + def test_cli_passes_clang_macro_fallback_options(monkeypatch, capsys, tmp_path: Path) -> None: calls: dict[str, object] = {} class DummyResult: unit = _DummyUnit() + mojo_module = _DummyMojoModule() bindings_source = "generated" layout_test_source = None @@ -92,7 +161,7 @@ def run(self) -> DummyResult: [ str(tmp_path / "demo.h"), "--clang-macro-fallback", - "--clang-macro-fallback-build-dir", + "--clang-macro-fallback-dir", str(build_dir), ] ) @@ -105,11 +174,12 @@ def run(self) -> DummyResult: assert options.clang_macro_fallback_build_dir == build_dir -def test_non_json_mode_passes_emit_options(monkeypatch, capsys, tmp_path: Path) -> None: +def test_mojo_mode_passes_emit_options(monkeypatch, capsys, tmp_path: Path) -> None: calls: dict[str, object] = {} class DummyResult: unit = _DummyUnit() + mojo_module = _DummyMojoModule() bindings_source = "generated" layout_test_source = None @@ -129,11 +199,12 @@ def run(self) -> DummyResult: rc = cli.main( [ str(header), - "--linking", - "owned_dl_handle", - "--library-path-hint", + "--link-mode", + "owned-dl-handle", + "--library-path", "/tmp/libsample.so", "--strict-abi", + "--no-module-comment", ] ) captured = capsys.readouterr() @@ -143,6 +214,7 @@ def run(self) -> DummyResult: assert calls["library_path_hint"] == "/tmp/libsample.so" assert calls["strict_abi"] is True assert calls["options"].json_output is False + assert calls["options"].module_comment is False def test_output_mode_writes_default_layout_test_sidecar(monkeypatch, tmp_path: Path) -> None: @@ -150,6 +222,7 @@ class DummyResult: bindings_source = "bindings" layout_test_source = "layout tests" unit = _DummyUnit() + mojo_module = _DummyMojoModule() class DummyOrchestrator: def __init__(self, _options) -> None: @@ -161,7 +234,7 @@ def run(self) -> DummyResult: monkeypatch.setattr(cli, "BindgenOrchestrator", DummyOrchestrator) output = tmp_path / "demo_bindings.mojo" - rc = cli.main([str(tmp_path / "demo.h"), "-o", str(output)]) + rc = cli.main([str(tmp_path / "demo.h"), "--output", str(output)]) assert rc == 0 assert output.read_text(encoding="utf-8") == "bindings" @@ -173,6 +246,7 @@ def run(self) -> DummyResult: def test_no_layout_tests_suppresses_default_sidecar(monkeypatch, tmp_path: Path) -> None: class DummyResult: unit = _DummyUnit() + mojo_module = _DummyMojoModule() bindings_source = "bindings" layout_test_source = None @@ -186,7 +260,7 @@ def run(self) -> DummyResult: monkeypatch.setattr(cli, "BindgenOrchestrator", DummyOrchestrator) output = tmp_path / "demo.mojo" - rc = cli.main([str(tmp_path / "demo.h"), "-o", str(output), "--no-layout-tests"]) + rc = cli.main([str(tmp_path / "demo.h"), "--output", str(output), "--no-emit-layout-tests"]) assert rc == 0 assert output.read_text(encoding="utf-8") == "bindings" @@ -198,6 +272,7 @@ class DummyResult: bindings_source = "bindings" layout_test_source = "custom layout" unit = _DummyUnit() + mojo_module = _DummyMojoModule() class DummyOrchestrator: def __init__(self, _options) -> None: @@ -213,7 +288,7 @@ def run(self) -> DummyResult: rc = cli.main( [ str(tmp_path / "demo.h"), - "-o", + "--output", str(output), "--layout-test-output", str(sidecar), @@ -224,9 +299,98 @@ def run(self) -> DummyResult: assert sidecar.read_text(encoding="utf-8") == "custom layout" +def test_mojo_ir_format_and_dump_sidecars(monkeypatch, tmp_path: Path) -> None: + class DummyResult: + bindings_source = "bindings" + layout_test_source = None + unit = _DummyUnit() + mojo_module = _DummyMojoModule() + + class DummyOrchestrator: + def __init__(self, _options) -> None: + pass + + def run(self) -> DummyResult: + return DummyResult() + + def dump_preprocessed(self) -> str: + return "preprocessed" + + monkeypatch.setattr(cli, "BindgenOrchestrator", DummyOrchestrator) + + output = tmp_path / "module.json" + cir = tmp_path / "cir.json" + mojo_ir = tmp_path / "mojo-ir.json" + preprocessed = tmp_path / "demo.i" + rc = cli.main( + [ + str(tmp_path / "demo.h"), + "--format", + "mojo-ir-json", + "--output", + str(output), + "--dump-cir", + str(cir), + "--dump-mojo-ir", + str(mojo_ir), + "--dump-preprocessed", + str(preprocessed), + ] + ) + + assert rc == 0 + assert output.read_text(encoding="utf-8") == '{"mojo": true}' + assert cir.read_text(encoding="utf-8") == '{"ok": true}' + assert mojo_ir.read_text(encoding="utf-8") == '{"mojo": true}' + assert preprocessed.read_text(encoding="utf-8") == "preprocessed" + + +def test_dry_run_writes_no_files_and_warnings_as_errors_exits_1( + monkeypatch, tmp_path: Path +) -> None: + class DummyUnit(_DummyUnit): + diagnostics = [_DummyDiagnostic("warning", "careful")] + + class DummyResult: + bindings_source = "bindings" + layout_test_source = "layout" + unit = DummyUnit() + mojo_module = _DummyMojoModule() + + class DummyOrchestrator: + def __init__(self, _options) -> None: + pass + + def run(self) -> DummyResult: + return DummyResult() + + monkeypatch.setattr(cli, "BindgenOrchestrator", DummyOrchestrator) + + output = tmp_path / "demo.mojo" + diagnostics = tmp_path / "diagnostics.json" + rc = cli.main( + [ + str(tmp_path / "demo.h"), + "--output", + str(output), + "--diagnostics", + "json", + "--diagnostics-output", + str(diagnostics), + "--warnings-as-errors", + "--dry-run", + ] + ) + + assert rc == 1 + assert not output.exists() + assert not diagnostics.exists() + + def test_stdout_does_not_write_layout_tests_by_default(monkeypatch, capsys, tmp_path: Path) -> None: class DummyResult: unit = _DummyUnit() + mojo_module = _DummyMojoModule() bindings_source = "bindings" layout_test_source = None @@ -249,8 +413,11 @@ def run(self) -> DummyResult: def test_orchestrator_failures_return_exit_code_1(monkeypatch, capsys, tmp_path: Path) -> None: class DummyOrchestrator: - def __init__(self, *_args: object, **_kwargs: object) -> None: - pass + def __init__(self, options) -> None: + self.options = options + + def normalized_clang_args(self): + return self.options.clang_options.to_args() def run(self): raise FileNotFoundError("header not found") @@ -262,3 +429,4 @@ def run(self): assert rc == 1 assert "mojo-bindgen error:" in captured.err assert "header not found" in captured.err + assert "clang args:" in captured.err diff --git a/tests/unit/test_codegen_generator.py b/tests/unit/test_codegen_generator.py index 7707a2a..b65cc19 100644 --- a/tests/unit/test_codegen_generator.py +++ b/tests/unit/test_codegen_generator.py @@ -9,6 +9,7 @@ from mojo_bindgen import BindgenOptions, BindgenOrchestrator, bindgen from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ByteOrder, Function, LinkMode, MojoModule, TargetABI, Unit, VoidType +from mojo_bindgen.parsing.frontend import ClangOptions def _abi() -> TargetABI: @@ -129,6 +130,40 @@ def run(self) -> Unit: assert "-fretain-comments-from-system-headers" in calls["compile_args"] +def test_parse_adds_system_includes_to_structured_clang_options(monkeypatch) -> None: + calls: dict[str, object] = {} + + class DummyParser: + def __init__( + self, + header: Path, + *, + library: str, + link_name: str, + include_headers, + compile_args, + raise_on_error, + **_kwargs, + ): + calls["compile_args"] = compile_args + + def run(self) -> Unit: + return _demo_unit() + + monkeypatch.setattr(orchestrator_mod, "ClangParser", DummyParser) + + options = BindgenOptions( + header=Path("demo.h"), + clang_options=ClangOptions(std="c11", include_dirs=(Path("./include"),)), + ) + BindgenOrchestrator(options).parse() + + assert calls["compile_args"] is not None + assert calls["compile_args"][:4] == ["-x", "c", "-std=c11", "-Iinclude"] + assert "-I/usr/include" in calls["compile_args"] + assert "-fretain-comments-from-system-headers" in calls["compile_args"] + + def test_parse_omits_system_comment_retention_when_doc_comments_disabled(monkeypatch) -> None: calls: dict[str, object] = {} diff --git a/tests/unit/test_parser_paths.py b/tests/unit/test_parser_paths.py index 70d1477..5eb6526 100644 --- a/tests/unit/test_parser_paths.py +++ b/tests/unit/test_parser_paths.py @@ -9,6 +9,7 @@ import pytest +from mojo_bindgen.parsing.frontend import ClangOptions from mojo_bindgen.parsing.parser import _default_system_compile_args, _resolve_header_path from mojo_bindgen.utils import build_c_parse_args, normalize_std_flag @@ -91,3 +92,43 @@ def test_build_c_parse_args_accepts_bare_std_equals() -> None: default_std="-std=gnu11", ) assert args == ["-x", "c", "-std=c17", "-DVALUE=1"] + + +def test_build_c_parse_args_does_not_duplicate_normalized_language_and_std() -> None: + args = build_c_parse_args( + ["-x", "c", "-std=c11", "-I./include"], + default_std="-std=gnu11", + ) + assert args == ["-x", "c", "-std=c11", "-I./include"] + + +def test_clang_options_normalizes_structured_flags_in_deterministic_order() -> None: + options = ClangOptions( + std="c23", + target="wasm32-unknown-unknown", + sysroot=Path("/sdk"), + include_dirs=(Path("include"), Path("vendor/include")), + defines=("FEATURE=1", "NAME"), + undefines=("OLD",), + raw_args=("-Wno-unused",), + ) + + assert options.to_args() == [ + "-x", + "c", + "-std=c23", + "--target=wasm32-unknown-unknown", + "--sysroot=/sdk", + "-Iinclude", + "-Ivendor/include", + "-DFEATURE=1", + "-DNAME", + "-UOLD", + "-Wno-unused", + ] + + +def test_clang_options_raw_std_suppresses_default_gnu11() -> None: + options = ClangOptions(raw_args=("--std=c99", "-DVALUE=1")) + + assert options.to_args() == ["-x", "c", "-std=c99", "-DVALUE=1"] From 5ea9a0e42af69d0ca04bcbbc9efe2241a89f1230 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Sat, 13 Jun 2026 16:26:03 +0200 Subject: [PATCH 4/6] Restructure analysis pipeline package --- docs/analysis_type_lowering_pipeline.md | 12 +-- docs/cir_passes_pipeline.md | 2 +- mojo_bindgen/analysis/__init__.py | 76 +++++++++---------- mojo_bindgen/analysis/cir/__init__.py | 1 + .../analysis/{ => cir}/cir_canonicalizer.py | 0 .../analysis/{ => cir}/reachability.py | 0 .../{ => cir}/reference_validation.py | 0 .../analysis/{ => cir}/validate_ir.py | 0 mojo_bindgen/analysis/facts/__init__.py | 1 + .../{ => facts}/alias_classification.py | 2 +- .../analysis/{ => facts}/bitfield_layout.py | 4 +- mojo_bindgen/analysis/{ => facts}/context.py | 13 ++-- .../analysis/{ => facts}/dependency_graph.py | 0 mojo_bindgen/analysis/{ => facts}/indexes.py | 0 .../analysis/{ => facts}/record_layout.py | 8 +- .../analysis/{ => facts}/record_shape.py | 6 +- .../analysis/{ => facts}/type_layout.py | 0 mojo_bindgen/analysis/mojo/__init__.py | 1 + .../analysis/{ => mojo}/alias_lowering.py | 2 +- .../analysis/{ => mojo}/const_lowering.py | 2 +- .../{ => mojo}/const_value_lowering.py | 2 +- .../analysis/{ => mojo}/decl_lowerer.py | 18 ++--- .../analysis/{ => mojo}/lowering_support.py | 4 +- .../analysis/{ => mojo}/macro_lowering.py | 8 +- .../analysis/{ => mojo}/mojo_emit_options.py | 0 .../analysis/{ => mojo}/record_policies.py | 0 .../analysis/{ => mojo}/struct_lowering.py | 16 ++-- .../analysis/{ => mojo}/type_lowering.py | 0 .../analysis/{ => mojo}/union_lowering.py | 4 +- .../analysis/{ => mojo}/unit_lowering.py | 14 ++-- .../analysis/{orchestrator.py => pipeline.py} | 16 ++-- mojo_bindgen/layout_tests/generator.py | 8 +- mojo_bindgen/orchestrator.py | 4 +- tests/bindgen_helpers.py | 2 +- tests/corpus/test_header_corpus.py | 2 +- tests/stress/test_stress_emit.py | 2 +- .../alignment/test_alignment_goldens.py | 2 +- tests/surface/test_surface_goldens.py | 2 +- tests/unit/passes/test_pipeline.py | 4 +- tests/unit/test_analysis_const_lowering.py | 2 +- tests/unit/test_analysis_record_layout.py | 4 +- tests/unit/test_analysis_record_shape.py | 4 +- tests/unit/test_analysis_struct_lowering.py | 8 +- tests/unit/test_analysis_type_lowering.py | 2 +- tests/unit/test_analysis_union_lowering.py | 2 +- tests/unit/test_analysis_unit_lowering.py | 2 +- tests/unit/test_cir_canonicalizer.py | 2 +- tests/unit/test_codegen_generator.py | 2 +- tests/unit/test_doc_comments.py | 2 +- tests/unit/test_ir_codegen_edges.py | 2 +- tests/unit/test_reachability_pass.py | 4 +- tests/unit/test_type_lowering.py | 2 +- 52 files changed, 143 insertions(+), 133 deletions(-) create mode 100644 mojo_bindgen/analysis/cir/__init__.py rename mojo_bindgen/analysis/{ => cir}/cir_canonicalizer.py (100%) rename mojo_bindgen/analysis/{ => cir}/reachability.py (100%) rename mojo_bindgen/analysis/{ => cir}/reference_validation.py (100%) rename mojo_bindgen/analysis/{ => cir}/validate_ir.py (100%) create mode 100644 mojo_bindgen/analysis/facts/__init__.py rename mojo_bindgen/analysis/{ => facts}/alias_classification.py (97%) rename mojo_bindgen/analysis/{ => facts}/bitfield_layout.py (97%) rename mojo_bindgen/analysis/{ => facts}/context.py (82%) rename mojo_bindgen/analysis/{ => facts}/dependency_graph.py (100%) rename mojo_bindgen/analysis/{ => facts}/indexes.py (100%) rename mojo_bindgen/analysis/{ => facts}/record_layout.py (95%) rename mojo_bindgen/analysis/{ => facts}/record_shape.py (98%) rename mojo_bindgen/analysis/{ => facts}/type_layout.py (100%) create mode 100644 mojo_bindgen/analysis/mojo/__init__.py rename mojo_bindgen/analysis/{ => mojo}/alias_lowering.py (93%) rename mojo_bindgen/analysis/{ => mojo}/const_lowering.py (97%) rename mojo_bindgen/analysis/{ => mojo}/const_value_lowering.py (95%) rename mojo_bindgen/analysis/{ => mojo}/decl_lowerer.py (91%) rename mojo_bindgen/analysis/{ => mojo}/lowering_support.py (93%) rename mojo_bindgen/analysis/{ => mojo}/macro_lowering.py (93%) rename mojo_bindgen/analysis/{ => mojo}/mojo_emit_options.py (100%) rename mojo_bindgen/analysis/{ => mojo}/record_policies.py (100%) rename mojo_bindgen/analysis/{ => mojo}/struct_lowering.py (97%) rename mojo_bindgen/analysis/{ => mojo}/type_lowering.py (100%) rename mojo_bindgen/analysis/{ => mojo}/union_lowering.py (97%) rename mojo_bindgen/analysis/{ => mojo}/unit_lowering.py (87%) rename mojo_bindgen/analysis/{orchestrator.py => pipeline.py} (86%) diff --git a/docs/analysis_type_lowering_pipeline.md b/docs/analysis_type_lowering_pipeline.md index 97f4aa1..2d9e884 100644 --- a/docs/analysis_type_lowering_pipeline.md +++ b/docs/analysis_type_lowering_pipeline.md @@ -60,7 +60,7 @@ Responsibilities: - delegate each top-level declaration to `UnitDeclLowerer` Source: -- [unit_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/unit_lowering.py:45) +- [unit_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/unit_lowering.py:45) ### `LowerTypePass` @@ -78,7 +78,7 @@ Important rules: - unsupported unsized types fall back to opaque external pointers Source: -- [type_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/type_lowering.py:132) +- [type_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/type_lowering.py:132) ### `UnitDeclLowerer` @@ -94,7 +94,7 @@ Responsibilities: - lower CIR constant expressions and macro expressions through `LowerConstExprPass` Source: -- [decl_lowerer.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/decl_lowerer.py:93) +- [decl_lowerer.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/decl_lowerer.py:93) ### `lower_struct(...)` @@ -110,8 +110,8 @@ Responsibilities: - attach fallback diagnostics when a record must collapse to opaque storage Source: -- [struct_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/struct_lowering.py:56) -- [record_layout.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/record_layout.py:48) +- [struct_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/struct_lowering.py:56) +- [record_layout.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/facts/record_layout.py:48) ### `lower_union(...)` @@ -119,7 +119,7 @@ Union lowering decides whether a union can stay as a typed `UnsafeUnion[...]` surface or must fall back to a conservative layout alias. Source: -- [union_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/union_lowering.py:25) +- [union_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/union_lowering.py:25) ## Type-Level Fallback Rules diff --git a/docs/cir_passes_pipeline.md b/docs/cir_passes_pipeline.md index 46ef736..76622ca 100644 --- a/docs/cir_passes_pipeline.md +++ b/docs/cir_passes_pipeline.md @@ -120,7 +120,7 @@ would otherwise be repeated inside lowerers. ### Declaration Indexes -`analysis.indexes` owns declaration lookup construction. `AnalysisContext` +`analysis.facts.indexes` owns declaration lookup construction. `AnalysisContext` stores the resulting normalized declarations by stable keys: - records by `decl_id` diff --git a/mojo_bindgen/analysis/__init__.py b/mojo_bindgen/analysis/__init__.py index dec6f59..e9044ce 100644 --- a/mojo_bindgen/analysis/__init__.py +++ b/mojo_bindgen/analysis/__init__.py @@ -1,42 +1,28 @@ """Public analysis API for CIR passes and CIR -> MojoIR lowering.""" -from mojo_bindgen.analysis.alias_classification import ( +from mojo_bindgen.analysis.cir.reachability import ( + SignatureRecordStubOptions, + SignatureRecordStubPass, + SignatureRecordStubResult, + materialize_signature_record_stubs, +) +from mojo_bindgen.analysis.cir.reference_validation import ( + ReferenceValidationError, + ValidateReferencesPass, +) +from mojo_bindgen.analysis.cir.validate_ir import IRValidationError, ValidateIRPass +from mojo_bindgen.analysis.facts.alias_classification import ( AliasClass, AliasClassification, AliasInfo, classify_aliases, ) -from mojo_bindgen.analysis.const_lowering import ( - ConstExprLoweringError, - LowerConstExprPass, - lower_const_expr, -) -from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context -from mojo_bindgen.analysis.decl_lowerer import UnitLoweringError -from mojo_bindgen.analysis.dependency_graph import ( +from mojo_bindgen.analysis.facts.context import AnalysisContext, build_analysis_context +from mojo_bindgen.analysis.facts.dependency_graph import ( DeclDependencyGraph, build_decl_dependency_graph, ) -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.orchestrator import ( - AnalysisArtifacts, - AnalysisOrchestrator, - AnalysisResult, - run_ir_passes, -) -from mojo_bindgen.analysis.reachability import ( - SignatureRecordStubOptions, - SignatureRecordStubPass, - SignatureRecordStubResult, - materialize_signature_record_stubs, -) -from mojo_bindgen.analysis.record_policies import ( - AssignRecordPoliciesError, - AssignRecordPoliciesPass, - PolicyInferencePass, - assign_record_policies, -) -from mojo_bindgen.analysis.record_shape import ( +from mojo_bindgen.analysis.facts.record_shape import ( ByValueRecordShape, RecordAnalysisFacts, RecordShapeFacts, @@ -44,31 +30,45 @@ analyze_record_shape, analyze_record_shapes, ) -from mojo_bindgen.analysis.reference_validation import ( - ReferenceValidationError, - ValidateReferencesPass, +from mojo_bindgen.analysis.mojo.const_lowering import ( + ConstExprLoweringError, + LowerConstExprPass, + lower_const_expr, +) +from mojo_bindgen.analysis.mojo.decl_lowerer import UnitLoweringError +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.record_policies import ( + AssignRecordPoliciesError, + AssignRecordPoliciesPass, + PolicyInferencePass, + assign_record_policies, ) -from mojo_bindgen.analysis.struct_lowering import ( +from mojo_bindgen.analysis.mojo.struct_lowering import ( StructLoweringContext, StructLoweringError, lower_struct, ) -from mojo_bindgen.analysis.type_lowering import ( +from mojo_bindgen.analysis.mojo.type_lowering import ( LowerTypePass, TypeLoweringError, lower_type, ) -from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes -from mojo_bindgen.analysis.union_lowering import ( +from mojo_bindgen.analysis.mojo.union_lowering import ( LowerUnionPass, UnionLoweringError, lower_union, ) -from mojo_bindgen.analysis.unit_lowering import ( +from mojo_bindgen.analysis.mojo.unit_lowering import ( LowerUnitPass, lower_unit, ) -from mojo_bindgen.analysis.validate_ir import IRValidationError, ValidateIRPass +from mojo_bindgen.analysis.pipeline import ( + AnalysisArtifacts, + AnalysisOrchestrator, + AnalysisResult, + run_ir_passes, +) +from mojo_bindgen.analysis.type_walk import TypeWalkOptions, iter_type_nodes __all__ = [ "AssignRecordPoliciesError", diff --git a/mojo_bindgen/analysis/cir/__init__.py b/mojo_bindgen/analysis/cir/__init__.py new file mode 100644 index 0000000..cc10666 --- /dev/null +++ b/mojo_bindgen/analysis/cir/__init__.py @@ -0,0 +1 @@ +"""CIR validation and normalization passes.""" diff --git a/mojo_bindgen/analysis/cir_canonicalizer.py b/mojo_bindgen/analysis/cir/cir_canonicalizer.py similarity index 100% rename from mojo_bindgen/analysis/cir_canonicalizer.py rename to mojo_bindgen/analysis/cir/cir_canonicalizer.py diff --git a/mojo_bindgen/analysis/reachability.py b/mojo_bindgen/analysis/cir/reachability.py similarity index 100% rename from mojo_bindgen/analysis/reachability.py rename to mojo_bindgen/analysis/cir/reachability.py diff --git a/mojo_bindgen/analysis/reference_validation.py b/mojo_bindgen/analysis/cir/reference_validation.py similarity index 100% rename from mojo_bindgen/analysis/reference_validation.py rename to mojo_bindgen/analysis/cir/reference_validation.py diff --git a/mojo_bindgen/analysis/validate_ir.py b/mojo_bindgen/analysis/cir/validate_ir.py similarity index 100% rename from mojo_bindgen/analysis/validate_ir.py rename to mojo_bindgen/analysis/cir/validate_ir.py diff --git a/mojo_bindgen/analysis/facts/__init__.py b/mojo_bindgen/analysis/facts/__init__.py new file mode 100644 index 0000000..60868bf --- /dev/null +++ b/mojo_bindgen/analysis/facts/__init__.py @@ -0,0 +1 @@ +"""Shared analysis facts computed from normalized CIR.""" diff --git a/mojo_bindgen/analysis/alias_classification.py b/mojo_bindgen/analysis/facts/alias_classification.py similarity index 97% rename from mojo_bindgen/analysis/alias_classification.py rename to mojo_bindgen/analysis/facts/alias_classification.py index dea6e39..529bdad 100644 --- a/mojo_bindgen/analysis/alias_classification.py +++ b/mojo_bindgen/analysis/facts/alias_classification.py @@ -5,8 +5,8 @@ from dataclasses import dataclass from enum import StrEnum +from mojo_bindgen.analysis.mojo.type_lowering import exact_width_stdint_alias_type from mojo_bindgen.analysis.traversal import iter_unit_typerefs -from mojo_bindgen.analysis.type_lowering import exact_width_stdint_alias_type from mojo_bindgen.ir import ( EnumRef, FunctionPtr, diff --git a/mojo_bindgen/analysis/bitfield_layout.py b/mojo_bindgen/analysis/facts/bitfield_layout.py similarity index 97% rename from mojo_bindgen/analysis/bitfield_layout.py rename to mojo_bindgen/analysis/facts/bitfield_layout.py index 2b34b14..2c5b8ae 100644 --- a/mojo_bindgen/analysis/bitfield_layout.py +++ b/mojo_bindgen/analysis/facts/bitfield_layout.py @@ -4,8 +4,8 @@ from dataclasses import dataclass, field -from mojo_bindgen.analysis.lowering_support import field_display_name -from mojo_bindgen.analysis.type_layout import peel_layout_wrappers +from mojo_bindgen.analysis.facts.type_layout import peel_layout_wrappers +from mojo_bindgen.analysis.mojo.lowering_support import field_display_name from mojo_bindgen.ir import Field, IntKind, IntType, Struct, Type diff --git a/mojo_bindgen/analysis/context.py b/mojo_bindgen/analysis/facts/context.py similarity index 82% rename from mojo_bindgen/analysis/context.py rename to mojo_bindgen/analysis/facts/context.py index e1bd8ae..ceb5792 100644 --- a/mojo_bindgen/analysis/context.py +++ b/mojo_bindgen/analysis/facts/context.py @@ -4,11 +4,14 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.alias_classification import AliasClassification, classify_aliases -from mojo_bindgen.analysis.dependency_graph import DeclDependencyGraph, build_decl_dependency_graph -from mojo_bindgen.analysis.indexes import DeclIndexes, build_decl_indexes -from mojo_bindgen.analysis.record_layout import RecordLayoutFacts, analyze_record_layout -from mojo_bindgen.analysis.record_shape import RecordAnalysisFacts, analyze_record_shapes +from mojo_bindgen.analysis.facts.alias_classification import AliasClassification, classify_aliases +from mojo_bindgen.analysis.facts.dependency_graph import ( + DeclDependencyGraph, + build_decl_dependency_graph, +) +from mojo_bindgen.analysis.facts.indexes import DeclIndexes, build_decl_indexes +from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts, analyze_record_layout +from mojo_bindgen.analysis.facts.record_shape import RecordAnalysisFacts, analyze_record_shapes from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit diff --git a/mojo_bindgen/analysis/dependency_graph.py b/mojo_bindgen/analysis/facts/dependency_graph.py similarity index 100% rename from mojo_bindgen/analysis/dependency_graph.py rename to mojo_bindgen/analysis/facts/dependency_graph.py diff --git a/mojo_bindgen/analysis/indexes.py b/mojo_bindgen/analysis/facts/indexes.py similarity index 100% rename from mojo_bindgen/analysis/indexes.py rename to mojo_bindgen/analysis/facts/indexes.py diff --git a/mojo_bindgen/analysis/record_layout.py b/mojo_bindgen/analysis/facts/record_layout.py similarity index 95% rename from mojo_bindgen/analysis/record_layout.py rename to mojo_bindgen/analysis/facts/record_layout.py index 18dd7e8..6df308d 100644 --- a/mojo_bindgen/analysis/record_layout.py +++ b/mojo_bindgen/analysis/facts/record_layout.py @@ -4,10 +4,10 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.bitfield_layout import BitfieldRunLayout, analyze_bitfield_layout -from mojo_bindgen.analysis.indexes import struct_by_decl_id -from mojo_bindgen.analysis.lowering_support import field_display_name -from mojo_bindgen.analysis.type_layout import type_align +from mojo_bindgen.analysis.facts.bitfield_layout import BitfieldRunLayout, analyze_bitfield_layout +from mojo_bindgen.analysis.facts.indexes import struct_by_decl_id +from mojo_bindgen.analysis.facts.type_layout import type_align +from mojo_bindgen.analysis.mojo.lowering_support import field_display_name from mojo_bindgen.ir import Array, Struct, StructRef, TargetABI diff --git a/mojo_bindgen/analysis/record_shape.py b/mojo_bindgen/analysis/facts/record_shape.py similarity index 98% rename from mojo_bindgen/analysis/record_shape.py rename to mojo_bindgen/analysis/facts/record_shape.py index 34678fb..8e1ce66 100644 --- a/mojo_bindgen/analysis/record_shape.py +++ b/mojo_bindgen/analysis/facts/record_shape.py @@ -5,9 +5,9 @@ from dataclasses import dataclass, replace from enum import StrEnum -from mojo_bindgen.analysis.lowering_support import field_display_name, try_lower_type -from mojo_bindgen.analysis.record_layout import RecordLayoutFacts -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts +from mojo_bindgen.analysis.mojo.lowering_support import field_display_name, try_lower_type +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( Array, diff --git a/mojo_bindgen/analysis/type_layout.py b/mojo_bindgen/analysis/facts/type_layout.py similarity index 100% rename from mojo_bindgen/analysis/type_layout.py rename to mojo_bindgen/analysis/facts/type_layout.py diff --git a/mojo_bindgen/analysis/mojo/__init__.py b/mojo_bindgen/analysis/mojo/__init__.py new file mode 100644 index 0000000..b3b241f --- /dev/null +++ b/mojo_bindgen/analysis/mojo/__init__.py @@ -0,0 +1 @@ +"""CIR-to-MojoIR lowering and MojoIR policy passes.""" diff --git a/mojo_bindgen/analysis/alias_lowering.py b/mojo_bindgen/analysis/mojo/alias_lowering.py similarity index 93% rename from mojo_bindgen/analysis/alias_lowering.py rename to mojo_bindgen/analysis/mojo/alias_lowering.py index 15a6433..882559e 100644 --- a/mojo_bindgen/analysis/alias_lowering.py +++ b/mojo_bindgen/analysis/mojo/alias_lowering.py @@ -3,7 +3,7 @@ from __future__ import annotations from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.type_lowering import LowerTypePass, exact_width_stdint_alias_type +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, exact_width_stdint_alias_type from mojo_bindgen.ir import AliasDecl, AliasKind, DocComment, FunctionPtr, NamedType, Type diff --git a/mojo_bindgen/analysis/const_lowering.py b/mojo_bindgen/analysis/mojo/const_lowering.py similarity index 97% rename from mojo_bindgen/analysis/const_lowering.py rename to mojo_bindgen/analysis/mojo/const_lowering.py index 59b20ef..33d38c8 100644 --- a/mojo_bindgen/analysis/const_lowering.py +++ b/mojo_bindgen/analysis/mojo/const_lowering.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from mojo_bindgen.analysis.common import mojo_float_literal_text, mojo_ident -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( BinaryExpr, CallExpr, diff --git a/mojo_bindgen/analysis/const_value_lowering.py b/mojo_bindgen/analysis/mojo/const_value_lowering.py similarity index 95% rename from mojo_bindgen/analysis/const_value_lowering.py rename to mojo_bindgen/analysis/mojo/const_value_lowering.py index ef75c4d..b206d05 100644 --- a/mojo_bindgen/analysis/const_value_lowering.py +++ b/mojo_bindgen/analysis/mojo/const_value_lowering.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( _MOJO_INT_TYPES, BinaryExpr, diff --git a/mojo_bindgen/analysis/decl_lowerer.py b/mojo_bindgen/analysis/mojo/decl_lowerer.py similarity index 91% rename from mojo_bindgen/analysis/decl_lowerer.py rename to mojo_bindgen/analysis/mojo/decl_lowerer.py index 3164c9c..c9fc31c 100644 --- a/mojo_bindgen/analysis/decl_lowerer.py +++ b/mojo_bindgen/analysis/mojo/decl_lowerer.py @@ -2,22 +2,22 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.alias_lowering import lower_typedef_alias from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.const_lowering import ( +from mojo_bindgen.analysis.mojo.alias_lowering import lower_typedef_alias +from mojo_bindgen.analysis.mojo.const_lowering import ( ConstExprLoweringError, LowerConstExprPass, ) -from mojo_bindgen.analysis.const_value_lowering import typed_const_value -from mojo_bindgen.analysis.lowering_support import stub_note -from mojo_bindgen.analysis.macro_lowering import MacroLowerer -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.struct_lowering import ( +from mojo_bindgen.analysis.mojo.const_value_lowering import typed_const_value +from mojo_bindgen.analysis.mojo.lowering_support import stub_note +from mojo_bindgen.analysis.mojo.macro_lowering import MacroLowerer +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.struct_lowering import ( StructLoweringContext, lower_struct, ) -from mojo_bindgen.analysis.type_lowering import LowerTypePass -from mojo_bindgen.analysis.union_lowering import LowerUnionPass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.union_lowering import LowerUnionPass from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/mojo_bindgen/analysis/lowering_support.py b/mojo_bindgen/analysis/mojo/lowering_support.py similarity index 93% rename from mojo_bindgen/analysis/lowering_support.py rename to mojo_bindgen/analysis/mojo/lowering_support.py index c5f2e65..b7315c4 100644 --- a/mojo_bindgen/analysis/lowering_support.py +++ b/mojo_bindgen/analysis/mojo/lowering_support.py @@ -3,8 +3,8 @@ from __future__ import annotations from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.indexes import record_by_decl_id -from mojo_bindgen.analysis.type_lowering import LowerTypePass, TypeLoweringError +from mojo_bindgen.analysis.facts.indexes import record_by_decl_id +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, TypeLoweringError from mojo_bindgen.ir import ( BuiltinType, Field, diff --git a/mojo_bindgen/analysis/macro_lowering.py b/mojo_bindgen/analysis/mojo/macro_lowering.py similarity index 93% rename from mojo_bindgen/analysis/macro_lowering.py rename to mojo_bindgen/analysis/mojo/macro_lowering.py index 81da555..67a53f6 100644 --- a/mojo_bindgen/analysis/macro_lowering.py +++ b/mojo_bindgen/analysis/mojo/macro_lowering.py @@ -5,10 +5,10 @@ from dataclasses import dataclass from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.const_lowering import ConstExprLoweringError, LowerConstExprPass -from mojo_bindgen.analysis.const_value_lowering import typed_const_value -from mojo_bindgen.analysis.lowering_support import lowering_note, stub_note -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.const_lowering import ConstExprLoweringError, LowerConstExprPass +from mojo_bindgen.analysis.mojo.const_value_lowering import typed_const_value +from mojo_bindgen.analysis.mojo.lowering_support import lowering_note, stub_note +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/mojo_bindgen/analysis/mojo_emit_options.py b/mojo_bindgen/analysis/mojo/mojo_emit_options.py similarity index 100% rename from mojo_bindgen/analysis/mojo_emit_options.py rename to mojo_bindgen/analysis/mojo/mojo_emit_options.py diff --git a/mojo_bindgen/analysis/record_policies.py b/mojo_bindgen/analysis/mojo/record_policies.py similarity index 100% rename from mojo_bindgen/analysis/record_policies.py rename to mojo_bindgen/analysis/mojo/record_policies.py diff --git a/mojo_bindgen/analysis/struct_lowering.py b/mojo_bindgen/analysis/mojo/struct_lowering.py similarity index 97% rename from mojo_bindgen/analysis/struct_lowering.py rename to mojo_bindgen/analysis/mojo/struct_lowering.py index 678b8cc..cc8168b 100644 --- a/mojo_bindgen/analysis/struct_lowering.py +++ b/mojo_bindgen/analysis/mojo/struct_lowering.py @@ -5,20 +5,20 @@ from dataclasses import dataclass, field from mojo_bindgen.analysis.common import _mojo_align_decorator_ok -from mojo_bindgen.analysis.lowering_support import ( +from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts, analyze_record_layout +from mojo_bindgen.analysis.facts.record_shape import ( + RecordAnalysisFacts, + RecordStorageKind, + analyze_record_shapes, +) +from mojo_bindgen.analysis.mojo.lowering_support import ( field_display_name, field_mojo_name, record_name, struct_note, try_lower_type, ) -from mojo_bindgen.analysis.record_layout import RecordLayoutFacts, analyze_record_layout -from mojo_bindgen.analysis.record_shape import ( - RecordAnalysisFacts, - RecordStorageKind, - analyze_record_shapes, -) -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( AtomicType, BitfieldField, diff --git a/mojo_bindgen/analysis/type_lowering.py b/mojo_bindgen/analysis/mojo/type_lowering.py similarity index 100% rename from mojo_bindgen/analysis/type_lowering.py rename to mojo_bindgen/analysis/mojo/type_lowering.py diff --git a/mojo_bindgen/analysis/union_lowering.py b/mojo_bindgen/analysis/mojo/union_lowering.py similarity index 97% rename from mojo_bindgen/analysis/union_lowering.py rename to mojo_bindgen/analysis/mojo/union_lowering.py index eac0ffb..d999649 100644 --- a/mojo_bindgen/analysis/union_lowering.py +++ b/mojo_bindgen/analysis/mojo/union_lowering.py @@ -5,14 +5,14 @@ import json from dataclasses import dataclass -from mojo_bindgen.analysis.lowering_support import ( +from mojo_bindgen.analysis.mojo.lowering_support import ( field_display_name, record_name, stub_note, try_lower_type, union_note, ) -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/mojo_bindgen/analysis/unit_lowering.py b/mojo_bindgen/analysis/mojo/unit_lowering.py similarity index 87% rename from mojo_bindgen/analysis/unit_lowering.py rename to mojo_bindgen/analysis/mojo/unit_lowering.py index 78d9056..90f3c4a 100644 --- a/mojo_bindgen/analysis/unit_lowering.py +++ b/mojo_bindgen/analysis/mojo/unit_lowering.py @@ -7,21 +7,21 @@ from __future__ import annotations -from mojo_bindgen.analysis.alias_lowering import lower_typedef_alias -from mojo_bindgen.analysis.const_lowering import ( +from mojo_bindgen.analysis.facts.context import AnalysisContext, build_analysis_context +from mojo_bindgen.analysis.mojo.alias_lowering import lower_typedef_alias +from mojo_bindgen.analysis.mojo.const_lowering import ( LowerConstExprPass, ) -from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context -from mojo_bindgen.analysis.decl_lowerer import ( +from mojo_bindgen.analysis.mojo.decl_lowerer import ( LoweringSession, UnitDeclLowerer, _link_mode_for_options, ) -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.struct_lowering import ( +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.struct_lowering import ( StructLoweringContext, ) -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( MojoDecl, MojoModule, diff --git a/mojo_bindgen/analysis/orchestrator.py b/mojo_bindgen/analysis/pipeline.py similarity index 86% rename from mojo_bindgen/analysis/orchestrator.py rename to mojo_bindgen/analysis/pipeline.py index 72a9fca..e472a2b 100644 --- a/mojo_bindgen/analysis/orchestrator.py +++ b/mojo_bindgen/analysis/pipeline.py @@ -4,14 +4,14 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.cir_canonicalizer import CIRCanonicalizer -from mojo_bindgen.analysis.context import AnalysisContext, build_analysis_context -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.reachability import SignatureRecordStubPass -from mojo_bindgen.analysis.record_policies import assign_record_policies -from mojo_bindgen.analysis.reference_validation import ValidateReferencesPass -from mojo_bindgen.analysis.unit_lowering import lower_unit -from mojo_bindgen.analysis.validate_ir import ValidateIRPass +from mojo_bindgen.analysis.cir.cir_canonicalizer import CIRCanonicalizer +from mojo_bindgen.analysis.cir.reachability import SignatureRecordStubPass +from mojo_bindgen.analysis.cir.reference_validation import ValidateReferencesPass +from mojo_bindgen.analysis.cir.validate_ir import ValidateIRPass +from mojo_bindgen.analysis.facts.context import AnalysisContext, build_analysis_context +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.record_policies import assign_record_policies +from mojo_bindgen.analysis.mojo.unit_lowering import lower_unit from mojo_bindgen.codegen.normalize_mojo_module import normalize_mojo_module from mojo_bindgen.ir import MojoModule, Unit diff --git a/mojo_bindgen/layout_tests/generator.py b/mojo_bindgen/layout_tests/generator.py index 5be078a..77c4204 100644 --- a/mojo_bindgen/layout_tests/generator.py +++ b/mojo_bindgen/layout_tests/generator.py @@ -4,8 +4,12 @@ from dataclasses import dataclass -from mojo_bindgen.analysis.lowering_support import field_mojo_name, record_by_decl_id, record_name -from mojo_bindgen.analysis.record_layout import analyze_record_layout +from mojo_bindgen.analysis.facts.record_layout import analyze_record_layout +from mojo_bindgen.analysis.mojo.lowering_support import ( + field_mojo_name, + record_by_decl_id, + record_name, +) from mojo_bindgen.ir import ( BitfieldGroupMember, MojoModule, diff --git a/mojo_bindgen/orchestrator.py b/mojo_bindgen/orchestrator.py index 72c5fad..7294687 100644 --- a/mojo_bindgen/orchestrator.py +++ b/mojo_bindgen/orchestrator.py @@ -6,8 +6,8 @@ from pathlib import Path from typing import Literal -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.orchestrator import AnalysisOrchestrator +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.pipeline import AnalysisOrchestrator from mojo_bindgen.codegen.mojo_ir_printer import MojoIRPrintOptions, render_mojo_module from mojo_bindgen.ir import MojoModule, Unit from mojo_bindgen.layout_tests import render_layout_test_module diff --git a/tests/bindgen_helpers.py b/tests/bindgen_helpers.py index bdab7e4..ab2d102 100644 --- a/tests/bindgen_helpers.py +++ b/tests/bindgen_helpers.py @@ -5,7 +5,7 @@ from pathlib import Path from mojo_bindgen import BindgenOptions, BindgenOrchestrator -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.codegen.mojo_ir_printer import MojoIRPrintOptions, render_mojo_module from mojo_bindgen.ir import MojoModule, Unit diff --git a/tests/corpus/test_header_corpus.py b/tests/corpus/test_header_corpus.py index 84bc5ed..eea316c 100644 --- a/tests/corpus/test_header_corpus.py +++ b/tests/corpus/test_header_corpus.py @@ -9,7 +9,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.parsing.parser import ClangParser, ParseError from tests.bindgen_helpers import MojoGenerator diff --git a/tests/stress/test_stress_emit.py b/tests/stress/test_stress_emit.py index 35e5b9b..0099da2 100644 --- a/tests/stress/test_stress_emit.py +++ b/tests/stress/test_stress_emit.py @@ -6,7 +6,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from tests.bindgen_helpers import MojoGenerator from tests.stress._helpers import case_dirs, has_libclang, parse_case diff --git a/tests/surface/alignment/test_alignment_goldens.py b/tests/surface/alignment/test_alignment_goldens.py index a3c69a7..5fd7d96 100644 --- a/tests/surface/alignment/test_alignment_goldens.py +++ b/tests/surface/alignment/test_alignment_goldens.py @@ -7,7 +7,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.parsing.parser import ClangParser from tests.bindgen_helpers import MojoGenerator diff --git a/tests/surface/test_surface_goldens.py b/tests/surface/test_surface_goldens.py index c8f7af9..c99f6cb 100644 --- a/tests/surface/test_surface_goldens.py +++ b/tests/surface/test_surface_goldens.py @@ -7,7 +7,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.parsing.parser import ClangParser from tests.bindgen_helpers import MojoGenerator diff --git a/tests/unit/passes/test_pipeline.py b/tests/unit/passes/test_pipeline.py index 96ed977..a678f7d 100644 --- a/tests/unit/passes/test_pipeline.py +++ b/tests/unit/passes/test_pipeline.py @@ -3,8 +3,8 @@ import pytest from mojo_bindgen.analysis import AnalysisOrchestrator, lower_unit, run_ir_passes -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.validate_ir import IRValidationError, ValidateIRPass +from mojo_bindgen.analysis.cir.validate_ir import IRValidationError, ValidateIRPass +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/tests/unit/test_analysis_const_lowering.py b/tests/unit/test_analysis_const_lowering.py index c091df3..cca6895 100644 --- a/tests/unit/test_analysis_const_lowering.py +++ b/tests/unit/test_analysis_const_lowering.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.const_lowering import lower_const_expr +from mojo_bindgen.analysis.mojo.const_lowering import lower_const_expr from mojo_bindgen.ir import ( BinaryExpr, BuiltinType, diff --git a/tests/unit/test_analysis_record_layout.py b/tests/unit/test_analysis_record_layout.py index 84f9634..2cca111 100644 --- a/tests/unit/test_analysis_record_layout.py +++ b/tests/unit/test_analysis_record_layout.py @@ -2,8 +2,8 @@ from __future__ import annotations -from mojo_bindgen.analysis.record_layout import analyze_record_layout -from mojo_bindgen.analysis.type_layout import type_layout +from mojo_bindgen.analysis.facts.record_layout import analyze_record_layout +from mojo_bindgen.analysis.facts.type_layout import type_layout from mojo_bindgen.ir import ( Array, ByteOrder, diff --git a/tests/unit/test_analysis_record_shape.py b/tests/unit/test_analysis_record_shape.py index 51ec35d..fb47b2a 100644 --- a/tests/unit/test_analysis_record_shape.py +++ b/tests/unit/test_analysis_record_shape.py @@ -1,7 +1,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.record_layout import analyze_record_layout -from mojo_bindgen.analysis.record_shape import ( +from mojo_bindgen.analysis.facts.record_layout import analyze_record_layout +from mojo_bindgen.analysis.facts.record_shape import ( RecordStorageKind, analyze_record_shape, analyze_record_shapes, diff --git a/tests/unit/test_analysis_struct_lowering.py b/tests/unit/test_analysis_struct_lowering.py index 48a9a87..e75df0c 100644 --- a/tests/unit/test_analysis_struct_lowering.py +++ b/tests/unit/test_analysis_struct_lowering.py @@ -2,9 +2,9 @@ from __future__ import annotations -from mojo_bindgen.analysis.record_layout import PlainFieldFact, RecordLayoutFacts -from mojo_bindgen.analysis.struct_lowering import StructLoweringContext, lower_struct -from mojo_bindgen.analysis.type_lowering import LowerTypePass +from mojo_bindgen.analysis.facts.record_layout import PlainFieldFact, RecordLayoutFacts +from mojo_bindgen.analysis.mojo.struct_lowering import StructLoweringContext, lower_struct +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass from mojo_bindgen.ir import ( Array, AtomicType, @@ -101,7 +101,7 @@ def fail_if_recomputed(*args, **kwargs): raise AssertionError("record layout should come from StructLoweringContext") monkeypatch.setattr( - "mojo_bindgen.analysis.struct_lowering.analyze_record_layout", + "mojo_bindgen.analysis.mojo.struct_lowering.analyze_record_layout", fail_if_recomputed, ) context = StructLoweringContext( diff --git a/tests/unit/test_analysis_type_lowering.py b/tests/unit/test_analysis_type_lowering.py index ccfddaa..da651a4 100644 --- a/tests/unit/test_analysis_type_lowering.py +++ b/tests/unit/test_analysis_type_lowering.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.type_lowering import LowerTypePass, lower_type +from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, lower_type from mojo_bindgen.ir import ( Array, AtomicType, diff --git a/tests/unit/test_analysis_union_lowering.py b/tests/unit/test_analysis_union_lowering.py index 8b60f52..57c373b 100644 --- a/tests/unit/test_analysis_union_lowering.py +++ b/tests/unit/test_analysis_union_lowering.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.union_lowering import lower_union +from mojo_bindgen.analysis.mojo.union_lowering import lower_union from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/tests/unit/test_analysis_unit_lowering.py b/tests/unit/test_analysis_unit_lowering.py index f72e11a..72b84f2 100644 --- a/tests/unit/test_analysis_unit_lowering.py +++ b/tests/unit/test_analysis_unit_lowering.py @@ -3,7 +3,7 @@ from __future__ import annotations from mojo_bindgen.analysis import lower_unit -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( AliasDecl, AliasKind, diff --git a/tests/unit/test_cir_canonicalizer.py b/tests/unit/test_cir_canonicalizer.py index 9a04c43..46c70de 100644 --- a/tests/unit/test_cir_canonicalizer.py +++ b/tests/unit/test_cir_canonicalizer.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.cir_canonicalizer import CIRCanonicalizer +from mojo_bindgen.analysis.cir.cir_canonicalizer import CIRCanonicalizer from mojo_bindgen.ir import ( ByteOrder, Const, diff --git a/tests/unit/test_codegen_generator.py b/tests/unit/test_codegen_generator.py index b65cc19..daae876 100644 --- a/tests/unit/test_codegen_generator.py +++ b/tests/unit/test_codegen_generator.py @@ -7,7 +7,7 @@ import mojo_bindgen.orchestrator as orchestrator_mod from mojo_bindgen import BindgenOptions, BindgenOrchestrator, bindgen -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ByteOrder, Function, LinkMode, MojoModule, TargetABI, Unit, VoidType from mojo_bindgen.parsing.frontend import ClangOptions diff --git a/tests/unit/test_doc_comments.py b/tests/unit/test_doc_comments.py index 480afbc..8da39bd 100644 --- a/tests/unit/test_doc_comments.py +++ b/tests/unit/test_doc_comments.py @@ -6,7 +6,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.codegen.mojo_ir_printer import _clean_doc_comment from mojo_bindgen.ir import DocComment, Enum, Function, GlobalVar, Struct from mojo_bindgen.parsing.parser import ClangParser diff --git a/tests/unit/test_ir_codegen_edges.py b/tests/unit/test_ir_codegen_edges.py index 55a9cbe..8c1fc0f 100644 --- a/tests/unit/test_ir_codegen_edges.py +++ b/tests/unit/test_ir_codegen_edges.py @@ -3,7 +3,7 @@ from __future__ import annotations from mojo_bindgen.analysis import lower_unit -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( AtomicType, BinaryExpr, diff --git a/tests/unit/test_reachability_pass.py b/tests/unit/test_reachability_pass.py index 115c357..84a5f24 100644 --- a/tests/unit/test_reachability_pass.py +++ b/tests/unit/test_reachability_pass.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.reachability import ( +from mojo_bindgen.analysis.cir.reachability import ( SignatureRecordStubOptions, SignatureRecordStubPass, materialize_signature_record_stubs, @@ -174,7 +174,7 @@ def test_materialize_signature_record_stubs_wrapper_returns_unit_only() -> None: def test_run_ir_passes_includes_signature_record_stubs() -> None: - from mojo_bindgen.analysis.orchestrator import run_ir_passes + from mojo_bindgen.analysis.pipeline import run_ir_passes orphan = StructRef( decl_id="c:@S@pipe", diff --git a/tests/unit/test_type_lowering.py b/tests/unit/test_type_lowering.py index d023e0f..ef06ff3 100644 --- a/tests/unit/test_type_lowering.py +++ b/tests/unit/test_type_lowering.py @@ -6,7 +6,7 @@ import pytest -from mojo_bindgen.analysis.mojo_emit_options import MojoEmitOptions +from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( Array, AtomicType, From 9a8237fcb2b8eb08620e085b3f97ee90b71c0533 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Sat, 13 Jun 2026 16:41:04 +0200 Subject: [PATCH 5/6] Rename MojoIR lowering to mapping --- CONTRIBUTING.md | 2 +- README.md | 12 +- docs/analysis_type_lowering_pipeline.md | 149 ---------- docs/analysis_type_mapping_pipeline.md | 149 ++++++++++ docs/cir_passes_pipeline.md | 86 +++--- docs/mojo_ir_normalization_pipeline.md | 4 +- docs/parsing_c_to_cir_pipeline.md | 4 +- mojo_bindgen/analysis/__init__.py | 72 ++--- .../analysis/cir/cir_canonicalizer.py | 2 +- .../analysis/cir/reference_validation.py | 4 +- mojo_bindgen/analysis/cir/validate_ir.py | 4 +- .../analysis/facts/alias_classification.py | 2 +- .../analysis/facts/bitfield_layout.py | 2 +- mojo_bindgen/analysis/facts/context.py | 2 +- mojo_bindgen/analysis/facts/indexes.py | 2 +- mojo_bindgen/analysis/facts/record_layout.py | 2 +- mojo_bindgen/analysis/facts/record_shape.py | 54 ++-- mojo_bindgen/analysis/mojo/__init__.py | 2 +- .../{alias_lowering.py => alias_mapping.py} | 26 +- ...onst_lowering.py => const_expr_mapping.py} | 40 ++- ...lue_lowering.py => const_value_mapping.py} | 20 +- .../mojo/{decl_lowerer.py => decl_mapping.py} | 114 ++++---- .../{macro_lowering.py => macro_mapping.py} | 42 +-- ...lowering_support.py => mapping_support.py} | 50 ++-- .../analysis/mojo/mojo_emit_options.py | 2 +- mojo_bindgen/analysis/mojo/record_policies.py | 4 +- .../{struct_lowering.py => struct_mapping.py} | 120 ++++---- .../{type_lowering.py => type_mapping.py} | 66 ++--- .../{union_lowering.py => union_mapping.py} | 52 ++-- .../{unit_lowering.py => unit_mapping.py} | 76 +++--- mojo_bindgen/analysis/pipeline.py | 24 +- mojo_bindgen/codegen/mojo_ir_printer.py | 4 +- mojo_bindgen/ir.py | 36 +-- mojo_bindgen/layout_tests/generator.py | 2 +- .../expect.portable.external.mojo | 4 +- .../expect.strict.external.mojo | 4 +- .../expect.portable.external.mojo | 14 +- .../policy_matrix/expect.strict.external.mojo | 14 +- tests/unit/passes/test_pipeline.py | 36 +-- tests/unit/test_analysis_const_lowering.py | 38 --- tests/unit/test_analysis_const_mapping.py | 38 +++ tests/unit/test_analysis_record_policies.py | 4 +- ...ing.py => test_analysis_struct_mapping.py} | 258 +++++++++--------- ...ering.py => test_analysis_type_mapping.py} | 94 +++---- ...ring.py => test_analysis_union_mapping.py} | 56 ++-- ...ering.py => test_analysis_unit_mapping.py} | 162 +++++------ tests/unit/test_ir_codegen_edges.py | 8 +- 47 files changed, 979 insertions(+), 983 deletions(-) delete mode 100644 docs/analysis_type_lowering_pipeline.md create mode 100644 docs/analysis_type_mapping_pipeline.md rename mojo_bindgen/analysis/mojo/{alias_lowering.py => alias_mapping.py} (55%) rename mojo_bindgen/analysis/mojo/{const_lowering.py => const_expr_mapping.py} (63%) rename mojo_bindgen/analysis/mojo/{const_value_lowering.py => const_value_mapping.py} (57%) rename mojo_bindgen/analysis/mojo/{decl_lowerer.py => decl_mapping.py} (58%) rename mojo_bindgen/analysis/mojo/{macro_lowering.py => macro_mapping.py} (75%) rename mojo_bindgen/analysis/mojo/{lowering_support.py => mapping_support.py} (52%) rename mojo_bindgen/analysis/mojo/{struct_lowering.py => struct_mapping.py} (78%) rename mojo_bindgen/analysis/mojo/{type_lowering.py => type_mapping.py} (86%) rename mojo_bindgen/analysis/mojo/{union_lowering.py => union_mapping.py} (72%) rename mojo_bindgen/analysis/mojo/{unit_lowering.py => unit_mapping.py} (54%) delete mode 100644 tests/unit/test_analysis_const_lowering.py create mode 100644 tests/unit/test_analysis_const_mapping.py rename tests/unit/{test_analysis_struct_lowering.py => test_analysis_struct_mapping.py} (76%) rename tests/unit/{test_analysis_type_lowering.py => test_analysis_type_mapping.py} (61%) rename tests/unit/{test_analysis_union_lowering.py => test_analysis_union_mapping.py} (71%) rename tests/unit/{test_analysis_unit_lowering.py => test_analysis_unit_mapping.py} (85%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4fee668..6d63735 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ Thanks for contributing to `mojo-bindgen`. This repository is primarily a Python project that parses C with `libclang` and emits Mojo FFI bindings. Most contribution work falls into one of three areas: -- parser / lowering changes under `mojo_bindgen/parsing/` and `mojo_bindgen/analysis/` +- parser changes under `mojo_bindgen/parsing/` and CIR-to-MojoIR mapping under `mojo_bindgen/analysis/` - Mojo IR emission changes under `mojo_bindgen/codegen/` - test and fixture updates under `tests/` diff --git a/README.md b/README.md index 7f531e7..50f6e14 100644 --- a/README.md +++ b/README.md @@ -132,20 +132,20 @@ generating bindings. Current support includes: -- **Parsing and lowering:** real C parsing through `libclang`, repeatable `--clang-arg` +- **Parsing and mapping:** real C parsing through `libclang`, repeatable `--clang-arg` support, and a structured IR pipeline rather than text-only generation. - **Primitive types:** scalar types, typedef chains, pointers with const-aware mutability, fixed arrays, incomplete-array decay cases, complex values, vector extension types, and representable atomics. -- **Mojo-native numeric lowering:** vector types lower to `SIMD[...]`, complex - values lower to `ComplexSIMD[...]`, and representable atomics lower to +- **Mojo-native numeric mapping:** vector types map to `SIMD[...]`, complex + values map to `ComplexSIMD[...]`, and representable atomics map to `Atomic[...]`. - **Records:** structs, anonymous members, mixed layouts that combine plain fields and bitfields, synthesized padding, and custom alignment emission where Mojo can represent the layout faithfully. - **Bitfields:** bitfields are emitted through explicit storage fields plus synthesized getter and setter methods. -- **Unions:** eligible unions lower to `UnsafeUnion[...]`; unions that cannot +- **Unions:** eligible unions map to `UnsafeUnion[...]`; unions that cannot be represented safely fall back to opaque `InlineArray[...]` storage with diagnostics to preserve layout. - **Opaque and difficult layouts:** incomplete records, packed layouts, and @@ -157,9 +157,9 @@ Current support includes: - **Functions:** thin wrappers are generated for non-variadic functions under both `external_call` and `owned_dl_handle` link modes. - **Globals and constants:** because Mojo does not currently expose native C - globals directly, supported globals lower through generated `GlobalVar` / + globals directly, supported globals map through generated `GlobalVar` / `GlobalConst` helper structs with synthesized `load()` / `store()` methods; - constants and supported object-like macros lower to `comptime` declarations. + constants and supported object-like macros map to `comptime` declarations. - **Macros:** integer, float, string, and char literal macros, foldable macro chains, supported casts, and `sizeof(type)` expressions are emitted as Mojo code. diff --git a/docs/analysis_type_lowering_pipeline.md b/docs/analysis_type_lowering_pipeline.md deleted file mode 100644 index 2d9e884..0000000 --- a/docs/analysis_type_lowering_pipeline.md +++ /dev/null @@ -1,149 +0,0 @@ -# Analysis Type Lowering Pipeline - -This document shows how normalized CIR lowers into MojoIR in the `analysis` -layer. - -## Overview - -```mermaid -flowchart TD - A[normalized CIR Unit] --> B[LowerUnitPass.run] - B --> C[build LoweringSession] - C --> D[UnitDeclLowerer] - - B --> E[synthesize external typedef aliases] - - D --> F{Decl kind} - F -->|Typedef| G[type alias lowering] - F -->|Enum| H[enum alias + constant lowering] - F -->|Function| I[function signature lowering] - F -->|GlobalVar| J[global helper lowering] - F -->|Const or MacroDecl| K[const expression lowering] - F -->|Struct| L[lower_struct] - F -->|Union| M[lower_union] - - G --> N[LowerTypePass] - H --> N - I --> N - J --> N - K --> O[LowerConstExprPass] - O --> N - L --> P[analyze_record_layout] - P --> Q[_lower_typed_members] - Q --> N - M --> N - - E --> R[MojoDecl list] - G --> R - H --> R - I --> R - J --> R - K --> R - L --> R - M --> R - - R --> S[policy-light MojoModule] -``` - -## Main Components - -### `LowerUnitPass` - -`LowerUnitPass` owns the top-level walk over a normalized CIR `Unit`. - -Responsibilities: -- create one shared `LowerTypePass` -- create one shared `LowerConstExprPass` -- build `StructLoweringContext` from the normalized record map and target ABI -- synthesize aliases for externally referenced typedefs that are used but not - declared locally in the current `Unit` -- delegate each top-level declaration to `UnitDeclLowerer` - -Source: -- [unit_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/unit_lowering.py:45) - -### `LowerTypePass` - -`LowerTypePass` converts one parser-facing `Type` into one valid Mojo-facing `Type`. - -Important rules: -- primitive C scalars lower to `BuiltinType` or fixed-width `NamedType` -- exact-width aliases such as `int32_t` and `uint64_t` lower to Mojo fixed-width names -- `size_t` and `ssize_t` lower to `UInt` and `Int` -- representable atomics lower to `Atomic[...]` -- data pointers lower to nullable `Pointer` -- vectors lower to `SIMD[...]` when lane metadata is usable -- complex values lower to `ComplexSIMD[...]` -- unsupported fixed-size types fall back to byte arrays -- unsupported unsized types fall back to opaque external pointers - -Source: -- [type_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/type_lowering.py:132) - -### `UnitDeclLowerer` - -`UnitDeclLowerer` is the declaration-family dispatcher inside the lowering -session. - -Responsibilities: -- lower typedefs and enums into aliases and constants -- lower functions into `FunctionDecl` -- lower globals into `GlobalDecl` -- lower structs through `lower_struct(...)` -- lower unions through `lower_union(...)` -- lower CIR constant expressions and macro expressions through `LowerConstExprPass` - -Source: -- [decl_lowerer.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/decl_lowerer.py:93) - -### `lower_struct(...)` - -Struct lowering is factored around real record layout analysis plus typed member -planning. - -Responsibilities: -- compute pure layout facts with `analyze_record_layout(...)` -- reject incomplete or non-representable layouts onto opaque storage -- lower plain stored fields and bitfield storage/logical field types -- synthesize padding members and initializers from analyzed layout facts -- preserve flexible-tail metadata when the enclosing shape stays representable -- attach fallback diagnostics when a record must collapse to opaque storage - -Source: -- [struct_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/struct_lowering.py:56) -- [record_layout.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/facts/record_layout.py:48) - -### `lower_union(...)` - -Union lowering decides whether a union can stay as a typed `UnsafeUnion[...]` -surface or must fall back to a conservative layout alias. - -Source: -- [union_lowering.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/union_lowering.py:25) - -## Type-Level Fallback Rules - -These are the current "always lower to valid MojoIR" escape hatches: - -| CIR type shape | MojoIR fallback | -| --- | --- | -| `UnsupportedType` with size | `Array(UInt8, size)` | -| `UnsupportedType` without size | opaque external `Pointer` | -| `VectorType` with unknown lane count | `Array(UInt8, size_bytes)` | -| `FloatKind.FLOAT128` | `Array(UInt8, size_bytes)` | -| `WCHAR` / `CHAR16` / `CHAR32` / `EXT_INT` | fixed-width `NamedType` | - -These rules keep lowering printable and prevent unsupported placeholders from -leaking into emitted MojoIR. - -## What This Stage Produces - -`LowerUnitPass.run(...)` returns a `MojoModule` with: - -- top-level declarations lowered into MojoIR -- module-level link metadata copied from the CIR `Unit` -- no final record trait policy yet -- no final import/support-dependency computation yet - -Those later concerns are handled by record-policy assignment and MojoIR -normalization. diff --git a/docs/analysis_type_mapping_pipeline.md b/docs/analysis_type_mapping_pipeline.md new file mode 100644 index 0000000..659e492 --- /dev/null +++ b/docs/analysis_type_mapping_pipeline.md @@ -0,0 +1,149 @@ +# Analysis Type Mapping Pipeline + +This document shows how normalized CIR maps into MojoIR in the `analysis` +layer. + +## Overview + +```mermaid +flowchart TD + A[normalized CIR Unit] --> B[MapUnitPass.run] + B --> C[build MappingSession] + C --> D[UnitDeclMapper] + + B --> E[synthesize external typedef aliases] + + D --> F{Decl kind} + F -->|Typedef| G[type alias mapping] + F -->|Enum| H[enum alias + constant mapping] + F -->|Function| I[function signature mapping] + F -->|GlobalVar| J[global helper mapping] + F -->|Const or MacroDecl| K[const expression mapping] + F -->|Struct| L[map_struct] + F -->|Union| M[map_union] + + G --> N[MapTypePass] + H --> N + I --> N + J --> N + K --> O[MapConstExprPass] + O --> N + L --> P[analyze_record_layout] + P --> Q[_map_typed_members] + Q --> N + M --> N + + E --> R[MojoDecl list] + G --> R + H --> R + I --> R + J --> R + K --> R + L --> R + M --> R + + R --> S[policy-light MojoModule] +``` + +## Main Components + +### `MapUnitPass` + +`MapUnitPass` owns the top-level walk over a normalized CIR `Unit`. + +Responsibilities: +- create one shared `MapTypePass` +- create one shared `MapConstExprPass` +- build `StructMappingContext` from the normalized record map and target ABI +- synthesize aliases for externally referenced typedefs that are used but not + declared locally in the current `Unit` +- delegate each top-level declaration to `UnitDeclMapper` + +Source: +- [unit_mapping.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/unit_mapping.py:45) + +### `MapTypePass` + +`MapTypePass` converts one parser-facing `Type` into one valid Mojo-facing `Type`. + +Important rules: +- primitive C scalars map to `BuiltinType` or fixed-width `NamedType` +- exact-width aliases such as `int32_t` and `uint64_t` map to Mojo fixed-width names +- `size_t` and `ssize_t` map to `UInt` and `Int` +- representable atomics map to `Atomic[...]` +- data pointers map to nullable `Pointer` +- vectors map to `SIMD[...]` when lane metadata is usable +- complex values map to `ComplexSIMD[...]` +- unsupported fixed-size types fall back to byte arrays +- unsupported unsized types fall back to opaque external pointers + +Source: +- [type_mapping.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/type_mapping.py:132) + +### `UnitDeclMapper` + +`UnitDeclMapper` is the declaration-family dispatcher inside the mapping +session. + +Responsibilities: +- map typedefs and enums into aliases and constants +- map functions into `FunctionDecl` +- map globals into `GlobalDecl` +- map structs through `map_struct(...)` +- map unions through `map_union(...)` +- map CIR constant expressions and macro expressions through `MapConstExprPass` + +Source: +- [decl_mapping.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/decl_mapping.py:93) + +### `map_struct(...)` + +Struct mapping is factored around real record layout analysis plus typed member +planning. + +Responsibilities: +- compute pure layout facts with `analyze_record_layout(...)` +- reject incomplete or non-representable layouts onto opaque storage +- map plain stored fields and bitfield storage/logical field types +- synthesize padding members and initializers from analyzed layout facts +- preserve flexible-tail metadata when the enclosing shape stays representable +- attach fallback diagnostics when a record must collapse to opaque storage + +Source: +- [struct_mapping.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/struct_mapping.py:56) +- [record_layout.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/facts/record_layout.py:48) + +### `map_union(...)` + +Union mapping decides whether a union can stay as a typed `UnsafeUnion[...]` +surface or must fall back to a conservative layout alias. + +Source: +- [union_mapping.py](/home/mohamed/Documents/Projects/mojo_bindgen/mojo_bindgen/analysis/mojo/union_mapping.py:25) + +## Type-Level Fallback Rules + +These are the current "always map to valid MojoIR" escape hatches: + +| CIR type shape | MojoIR fallback | +| --- | --- | +| `UnsupportedType` with size | `Array(UInt8, size)` | +| `UnsupportedType` without size | opaque external `Pointer` | +| `VectorType` with unknown lane count | `Array(UInt8, size_bytes)` | +| `FloatKind.FLOAT128` | `Array(UInt8, size_bytes)` | +| `WCHAR` / `CHAR16` / `CHAR32` / `EXT_INT` | fixed-width `NamedType` | + +These rules keep mapping printable and prevent unsupported placeholders from +leaking into emitted MojoIR. + +## What This Stage Produces + +`MapUnitPass.run(...)` returns a `MojoModule` with: + +- top-level declarations mapped into MojoIR +- module-level link metadata copied from the CIR `Unit` +- no final record trait policy yet +- no final import/support-dependency computation yet + +Those later concerns are handled by record-policy assignment and MojoIR +normalization. diff --git a/docs/cir_passes_pipeline.md b/docs/cir_passes_pipeline.md index 76622ca..95a33a8 100644 --- a/docs/cir_passes_pipeline.md +++ b/docs/cir_passes_pipeline.md @@ -2,7 +2,7 @@ This is the main reference for analysis-owned passes in `mojo-bindgen`. Analysis starts with a raw parser `Unit`, validates and normalizes CIR, computes -shared facts, lowers into policy-light MojoIR, and assigns late Mojo record +shared facts, maps into policy-light MojoIR, and assigns late Mojo record policies. `NormalizeMojoModulePass` is intentionally not documented here in detail. It is @@ -26,12 +26,12 @@ flowchart TD G --> K[RecordLayoutFacts] G --> L[RecordAnalysisFacts] - G --> M[LowerUnitPass] - M --> N[LowerTypePass] - M --> O[LowerConstExprPass] - M --> P[UnitDeclLowerer] - P --> Q[struct_lowering] - P --> R[LowerUnionPass] + G --> M[MapUnitPass] + M --> N[MapTypePass] + M --> O[MapConstExprPass] + M --> P[UnitDeclMapper] + P --> Q[struct_mapping] + P --> R[MapUnionPass] M --> S[policy-light MojoModule] S --> T[PolicyInferencePass] @@ -42,7 +42,7 @@ flowchart TD The compatibility entry points are: - `run_ir_passes(unit)`: validates and normalizes CIR. -- `lower_unit(unit)`: lowers normalized CIR, building fallback context if needed. +- `map_unit(unit)`: maps normalized CIR, building fallback context if needed. - `AnalysisOrchestrator.analyze_with_artifacts(unit)`: returns the stable public `AnalysisResult`. - `AnalysisOrchestrator.analyze_pipeline(unit)`: returns `AnalysisArtifacts` @@ -81,7 +81,7 @@ Purpose: - synthesize incomplete `Struct` declarations for signature-only `StructRef` uses that have no top-level record declaration -This keeps signatures such as `int f(struct opaque *p);` lowerable as opaque +This keeps signatures such as `int f(struct opaque *p);` mappable as opaque Mojo record stubs. ### `CIRCanonicalizer` @@ -106,7 +106,7 @@ Purpose: - require `EnumRef` declarations to exist in the normalized `Unit` - require concrete non-union `StructRef` declarations to exist after signature stub materialization -- allow external typedef references, because `LowerUnitPass` can synthesize +- allow external typedef references, because `MapUnitPass` can synthesize external typedef aliases - allow `OpaqueRecordRef` to remain opaque/external @@ -116,7 +116,7 @@ This is the dangling-reference gate for normalized CIR. `build_analysis_context(unit)` computes reusable whole-unit facts after CIR is normalized and reference-validated. This stage should absorb analysis work that -would otherwise be repeated inside lowerers. +would otherwise be repeated inside mappers. ### Declaration Indexes @@ -168,7 +168,7 @@ Purpose: aliases, ordinary typedefs, and external typedefs This keeps alias meaning available as shared facts instead of rediscovering it -inside lowerers. +inside mappers. ### `RecordLayoutFacts` @@ -177,7 +177,7 @@ Purpose: - analyze physical record layout from CIR offsets/sizes - compute plain field facts, bitfield storage runs, padding spans, natural typed alignment, and layout problems -- preserve incomplete-record facts without forcing a lowering decision +- preserve incomplete-record facts without forcing a mapping decision `analyze_record_layout()` owns pure C-layout checks. It does not decide how a Mojo `StructDecl` should be emitted. @@ -190,111 +190,111 @@ Purpose: - classify record storage as incomplete, union, typed, or opaque storage - validate direct and embedded flexible-tail patterns - analyze recursive by-value record shapes -- carry flexible-tail metadata for struct lowering +- carry flexible-tail metadata for struct mapping - carry fallback reasons for opaque-storage diagnostics -This is the central record analysis pass. `struct_lowering` consumes these facts +This is the central record analysis pass. `struct_mapping` consumes these facts and performs MojoIR member construction; it should not own recursive record shape policy. Its internal helpers are split by responsibility: record prechecks, plain-field checks, embedded-record/flexible-tail checks, and bitfield checks. -## Stage 3: CIR To MojoIR Lowering +## Stage 3: CIR To MojoIR Mapping -Lowering consumes normalized CIR plus `AnalysisContext` and produces a +Mapping consumes normalized CIR plus `AnalysisContext` and produces a policy-light `MojoModule`. These passes are Mojo-facing, but still live in analysis because they convert analyzed CIR into MojoIR. -### `LowerUnitPass` +### `MapUnitPass` Purpose: -- create shared lowerers for one unit +- create shared mappers for one unit - synthesize aliases for external typedef references from `AliasClassification` -- lower top-level declarations through `UnitDeclLowerer` +- map top-level declarations through `UnitDeclMapper` - build module metadata and linking mode -`lower_unit(unit)` remains a compatibility helper. If no `AnalysisContext` is +`map_unit(unit)` remains a compatibility helper. If no `AnalysisContext` is provided, it builds one internally. -### `UnitDeclLowerer` +### `UnitDeclMapper` Purpose: -- dispatch each top-level CIR declaration to the correct lowerer -- lower typedefs, enums, functions, globals, constants, macros, structs, and +- dispatch each top-level CIR declaration to the correct mapper +- map typedefs, enums, functions, globals, constants, macros, structs, and unions - preserve declaration order except for intentionally synthesized aliases This is orchestration, not whole-unit analysis. New cross-declaration facts should generally be added to `AnalysisContext`. -Typedef alias construction is centralized in `alias_lowering`, so local typedefs +Typedef alias construction is centralized in `alias_mapping`, so local typedefs and external typedef aliases share callback, exact-width stdint, and no-op self-alias handling. -### `LowerTypePass` +### `MapTypePass` Purpose: - map CIR types to MojoIR type nodes - preserve named typedef/record/enum surfaces as `NamedType` -- lower pointers, arrays, function pointers, atomics, vectors, complex values, +- map pointers, arrays, function pointers, atomics, vectors, complex values, and unsupported sized types -This is a recursive type lowerer. It does not decide declaration reachability or +This is a recursive type mapper. It does not decide declaration reachability or record storage policy. -### `LowerConstExprPass` +### `MapConstExprPass` Purpose: -- lower CIR constant-expression nodes to MojoIR constant-expression nodes -- lower cast and `sizeof` target types through `LowerTypePass` +- map CIR constant-expression nodes to MojoIR constant-expression nodes +- map cast and `sizeof` target types through `MapTypePass` - reject constant forms that have no direct MojoIR value form, such as null pointer literals -Macro emission policy lives in `MacroLowerer`; expression lowering only rewrites +Macro emission policy lives in `MacroMapper`; expression mapping only rewrites supported expression shapes. -### Macro Lowering +### Macro Mapping Purpose: -- lower supported object-like macros to MojoIR value aliases +- map supported object-like macros to MojoIR value aliases - block macro forms that would be misleading in Mojo, such as null pointer macros and C logical operators - preserve unsupported macro spellings as diagnostic comment aliases - track emitted constants so self-alias and name-conflict handling is deterministic -### Struct Lowering +### Struct Mapping Purpose: - consume `RecordAnalysisFacts` and `RecordLayoutFacts` - emit opaque declarations for incomplete records - emit byte-storage structs for opaque-storage decisions -- lower typed plain fields and bitfield groups to MojoIR members +- map typed plain fields and bitfield groups to MojoIR members - attach flexible-tail metadata computed by record analysis - compute Mojo alignment decorator policy -Struct lowering should not recompute recursive record shape decisions. -Typed-member lowering is intentionally split between plain fields, bitfield +Struct mapping should not recompute recursive record shape decisions. +Typed-member mapping is intentionally split between plain fields, bitfield storage, and bitfield logical fields so fallback diagnostics stay local to the operation that failed. -### `LowerUnionPass` +### `MapUnionPass` Purpose: -- lower complete unions to `UnsafeUnion[...]` when member types are distinct and +- map complete unions to `UnsafeUnion[...]` when member types are distinct and representable -- fall back to `InlineArray[UInt8, size]` when union lowering would be unsafe or +- fall back to `InlineArray[UInt8, size]` when union mapping would be unsafe or ambiguous - emit placeholder aliases for incomplete unions -Union lowering remains separate because unions lower to alias-style layout +Union mapping remains separate because unions map to alias-style layout types, not `StructDecl`. Its implementation is structured as union-arm collection followed by either `UnsafeUnion[...]` alias construction or byte-storage fallback construction. @@ -311,7 +311,7 @@ Purpose: - handle recursion, opaque storage, arrays, atomics, pointers, aliases, and nested records -This pass runs after CIR-to-MojoIR lowering because it reasons over final MojoIR +This pass runs after CIR-to-MojoIR mapping because it reasons over final MojoIR types and declaration relationships. ## Codegen Boundary diff --git a/docs/mojo_ir_normalization_pipeline.md b/docs/mojo_ir_normalization_pipeline.md index b826185..e740b99 100644 --- a/docs/mojo_ir_normalization_pipeline.md +++ b/docs/mojo_ir_normalization_pipeline.md @@ -1,6 +1,6 @@ # MojoIR Normalization Workflow -This document shows how `normalize_mojo_module()` turns lowered MojoIR into the +This document shows how `normalize_mojo_module()` turns mapped MojoIR into the printer-ready form consumed by the final rendering step. ## Overview @@ -107,7 +107,7 @@ The current public render path is: 1. `AnalysisOrchestrator.run_ir_passes(unit)` validates and normalizes raw CIR 2. `build_analysis_context(unit)` computes shared declaration and layout facts -3. `LowerUnitPass.run(unit)` produces a policy-light `MojoModule` +3. `MapUnitPass.run(unit)` produces a policy-light `MojoModule` 4. `PolicyInferencePass` derives late record passability, traits, and fieldwise-init policy 5. `normalize_mojo_module(module)` makes printer-facing facts explicit 6. `render_mojo_module(module, options)` emits Mojo source diff --git a/docs/parsing_c_to_cir_pipeline.md b/docs/parsing_c_to_cir_pipeline.md index b80d2da..2a2c5f0 100644 --- a/docs/parsing_c_to_cir_pipeline.md +++ b/docs/parsing_c_to_cir_pipeline.md @@ -96,14 +96,14 @@ This produces a source-faithful but not yet normalized `Unit`. ### 5. Hand-off boundary -`ClangParser.run()` stops at raw CIR. Any later CIR normalization or lowering +`ClangParser.run()` stops at raw CIR. Any later CIR normalization or mapping belongs to the analysis layer, not the parser. In the current code shape that means `AnalysisOrchestrator` owns: - CIR validation - signature-only record stub materialization - CIR canonicalization -- CIR -> MojoIR lowering +- CIR -> MojoIR mapping - late record policy assignment - MojoIR normalization diff --git a/mojo_bindgen/analysis/__init__.py b/mojo_bindgen/analysis/__init__.py index e9044ce..79e076d 100644 --- a/mojo_bindgen/analysis/__init__.py +++ b/mojo_bindgen/analysis/__init__.py @@ -1,4 +1,4 @@ -"""Public analysis API for CIR passes and CIR -> MojoIR lowering.""" +"""Public analysis API for CIR passes and CIR -> MojoIR mapping.""" from mojo_bindgen.analysis.cir.reachability import ( SignatureRecordStubOptions, @@ -30,12 +30,12 @@ analyze_record_shape, analyze_record_shapes, ) -from mojo_bindgen.analysis.mojo.const_lowering import ( - ConstExprLoweringError, - LowerConstExprPass, - lower_const_expr, +from mojo_bindgen.analysis.mojo.const_expr_mapping import ( + ConstExprMappingError, + MapConstExprPass, + map_const_expr, ) -from mojo_bindgen.analysis.mojo.decl_lowerer import UnitLoweringError +from mojo_bindgen.analysis.mojo.decl_mapping import UnitMappingError from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.mojo.record_policies import ( AssignRecordPoliciesError, @@ -43,24 +43,24 @@ PolicyInferencePass, assign_record_policies, ) -from mojo_bindgen.analysis.mojo.struct_lowering import ( - StructLoweringContext, - StructLoweringError, - lower_struct, +from mojo_bindgen.analysis.mojo.struct_mapping import ( + StructMappingContext, + StructMappingError, + map_struct, ) -from mojo_bindgen.analysis.mojo.type_lowering import ( - LowerTypePass, - TypeLoweringError, - lower_type, +from mojo_bindgen.analysis.mojo.type_mapping import ( + MapTypePass, + TypeMappingError, + map_type, ) -from mojo_bindgen.analysis.mojo.union_lowering import ( - LowerUnionPass, - UnionLoweringError, - lower_union, +from mojo_bindgen.analysis.mojo.union_mapping import ( + MapUnionPass, + UnionMappingError, + map_union, ) -from mojo_bindgen.analysis.mojo.unit_lowering import ( - LowerUnitPass, - lower_unit, +from mojo_bindgen.analysis.mojo.unit_mapping import ( + MapUnitPass, + map_unit, ) from mojo_bindgen.analysis.pipeline import ( AnalysisArtifacts, @@ -80,12 +80,12 @@ "AliasClass", "AliasClassification", "AliasInfo", - "ConstExprLoweringError", + "ConstExprMappingError", "IRValidationError", - "LowerConstExprPass", - "LowerTypePass", - "LowerUnionPass", - "LowerUnitPass", + "MapConstExprPass", + "MapTypePass", + "MapUnionPass", + "MapUnitPass", "MojoEmitOptions", "PolicyInferencePass", "DeclDependencyGraph", @@ -97,13 +97,13 @@ "SignatureRecordStubOptions", "SignatureRecordStubPass", "SignatureRecordStubResult", - "StructLoweringContext", - "StructLoweringError", - "TypeLoweringError", + "StructMappingContext", + "StructMappingError", + "TypeMappingError", "ValidateIRPass", "ValidateReferencesPass", - "UnionLoweringError", - "UnitLoweringError", + "UnionMappingError", + "UnitMappingError", "TypeWalkOptions", "assign_record_policies", "analyze_record_shape", @@ -112,11 +112,11 @@ "build_decl_dependency_graph", "classify_aliases", "iter_type_nodes", - "lower_const_expr", - "lower_struct", - "lower_type", - "lower_union", - "lower_unit", + "map_const_expr", + "map_struct", + "map_type", + "map_union", + "map_unit", "materialize_signature_record_stubs", "run_ir_passes", ] diff --git a/mojo_bindgen/analysis/cir/cir_canonicalizer.py b/mojo_bindgen/analysis/cir/cir_canonicalizer.py index e426e6b..151b53c 100644 --- a/mojo_bindgen/analysis/cir/cir_canonicalizer.py +++ b/mojo_bindgen/analysis/cir/cir_canonicalizer.py @@ -36,7 +36,7 @@ class CIRCanonicalizer: - """Canonicalize cross-declaration CIR facts before Mojo lowering. + """Canonicalize cross-declaration CIR facts before Mojo mapping. This pass repairs parser-local declaration quirks without changing the caller-owned ``Unit``. Its ordered steps are: diff --git a/mojo_bindgen/analysis/cir/reference_validation.py b/mojo_bindgen/analysis/cir/reference_validation.py index 507e17e..f062a7b 100644 --- a/mojo_bindgen/analysis/cir/reference_validation.py +++ b/mojo_bindgen/analysis/cir/reference_validation.py @@ -24,7 +24,7 @@ class ValidateReferencesPass: """Validate cross-declaration references that must resolve inside the ``Unit``. Missing ``TypeRef`` declarations are treated as external typedef uses because - ``LowerUnitPass`` can synthesize aliases for those. ``OpaqueRecordRef`` is + ``MapUnitPass`` can synthesize aliases for those. ``OpaqueRecordRef`` is also allowed to remain external. Concrete non-union ``StructRef`` and all ``EnumRef`` nodes must resolve after signature-stub materialization and enum canonicalization. @@ -53,7 +53,7 @@ def _validate_type( if isinstance(node, TypeRef): if not node.decl_id: raise ReferenceValidationError(f"TypeRef {node.name!r} is missing decl_id") - # External typedef refs are supported by synthetic alias lowering. + # External typedef refs are supported by synthetic alias mapping. continue if isinstance(node, StructRef): if node.decl_id not in struct_ids and not node.is_union: diff --git a/mojo_bindgen/analysis/cir/validate_ir.py b/mojo_bindgen/analysis/cir/validate_ir.py index fbbe843..e50e7e8 100644 --- a/mojo_bindgen/analysis/cir/validate_ir.py +++ b/mojo_bindgen/analysis/cir/validate_ir.py @@ -27,9 +27,9 @@ class ValidateIRPass: The pass is intentionally fail-fast and non-mutating. It guarantees stable declaration identities for declarations that participate in cross-reference analysis, and it verifies nested record/type references carry the identity - fields needed by canonicalization, reachability, layout, and lowering. It + fields needed by canonicalization, reachability, layout, and mapping. It does not prove ABI correctness or Mojo representability; later layout and - lowering passes own those decisions. + mapping passes own those decisions. """ def run(self, unit: Unit) -> Unit: diff --git a/mojo_bindgen/analysis/facts/alias_classification.py b/mojo_bindgen/analysis/facts/alias_classification.py index 529bdad..2e837a0 100644 --- a/mojo_bindgen/analysis/facts/alias_classification.py +++ b/mojo_bindgen/analysis/facts/alias_classification.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from enum import StrEnum -from mojo_bindgen.analysis.mojo.type_lowering import exact_width_stdint_alias_type +from mojo_bindgen.analysis.mojo.type_mapping import exact_width_stdint_alias_type from mojo_bindgen.analysis.traversal import iter_unit_typerefs from mojo_bindgen.ir import ( EnumRef, diff --git a/mojo_bindgen/analysis/facts/bitfield_layout.py b/mojo_bindgen/analysis/facts/bitfield_layout.py index 2c5b8ae..0b3303f 100644 --- a/mojo_bindgen/analysis/facts/bitfield_layout.py +++ b/mojo_bindgen/analysis/facts/bitfield_layout.py @@ -5,7 +5,7 @@ from dataclasses import dataclass, field from mojo_bindgen.analysis.facts.type_layout import peel_layout_wrappers -from mojo_bindgen.analysis.mojo.lowering_support import field_display_name +from mojo_bindgen.analysis.mojo.mapping_support import field_display_name from mojo_bindgen.ir import Field, IntKind, IntType, Struct, Type diff --git a/mojo_bindgen/analysis/facts/context.py b/mojo_bindgen/analysis/facts/context.py index ceb5792..c1f0fc9 100644 --- a/mojo_bindgen/analysis/facts/context.py +++ b/mojo_bindgen/analysis/facts/context.py @@ -17,7 +17,7 @@ @dataclass(frozen=True) class AnalysisContext: - """Reusable facts computed after CIR normalization and before MojoIR lowering.""" + """Reusable facts computed after CIR normalization and before MojoIR mapping.""" unit: Unit records_by_decl_id: dict[str, Struct] diff --git a/mojo_bindgen/analysis/facts/indexes.py b/mojo_bindgen/analysis/facts/indexes.py index a4d963e..e22694c 100644 --- a/mojo_bindgen/analysis/facts/indexes.py +++ b/mojo_bindgen/analysis/facts/indexes.py @@ -21,7 +21,7 @@ class DeclIndexes: def build_decl_indexes(unit: Unit) -> DeclIndexes: - """Build declaration lookup maps used by analysis and lowering.""" + """Build declaration lookup maps used by analysis and mapping.""" records_by_decl_id: dict[str, Struct] = {} typedefs_by_decl_id: dict[str, Typedef] = {} diff --git a/mojo_bindgen/analysis/facts/record_layout.py b/mojo_bindgen/analysis/facts/record_layout.py index 6df308d..ab8ddba 100644 --- a/mojo_bindgen/analysis/facts/record_layout.py +++ b/mojo_bindgen/analysis/facts/record_layout.py @@ -7,7 +7,7 @@ from mojo_bindgen.analysis.facts.bitfield_layout import BitfieldRunLayout, analyze_bitfield_layout from mojo_bindgen.analysis.facts.indexes import struct_by_decl_id from mojo_bindgen.analysis.facts.type_layout import type_align -from mojo_bindgen.analysis.mojo.lowering_support import field_display_name +from mojo_bindgen.analysis.mojo.mapping_support import field_display_name from mojo_bindgen.ir import Array, Struct, StructRef, TargetABI diff --git a/mojo_bindgen/analysis/facts/record_shape.py b/mojo_bindgen/analysis/facts/record_shape.py index 8e1ce66..6b6300d 100644 --- a/mojo_bindgen/analysis/facts/record_shape.py +++ b/mojo_bindgen/analysis/facts/record_shape.py @@ -6,8 +6,8 @@ from enum import StrEnum from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts -from mojo_bindgen.analysis.mojo.lowering_support import field_display_name, try_lower_type -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.mapping_support import field_display_name, try_map_type +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.analysis.type_walk import TypeWalkOptions, collect_type_nodes from mojo_bindgen.ir import ( Array, @@ -40,7 +40,7 @@ class ByValueRecordShape: @dataclass(frozen=True) class RecordAnalysisFacts: - """Central per-record facts used by struct lowering and later policy passes.""" + """Central per-record facts used by struct mapping and later policy passes.""" decl_id: str is_union: bool @@ -77,14 +77,14 @@ def analyze_record_shapes( records_by_decl_id: dict[str, Struct], record_layouts: dict[str, RecordLayoutFacts], *, - type_lowerer: LowerTypePass | None = None, + type_mapper: MapTypePass | None = None, ) -> dict[str, RecordAnalysisFacts]: """Analyze every CIR record's layout shape and typed-storage eligibility.""" analyzer = RecordShapeAnalyzer( records_by_decl_id=records_by_decl_id, record_layouts=record_layouts, - type_lowerer=type_lowerer or LowerTypePass(), + type_mapper=type_mapper or MapTypePass(), ) return { decl_id: analyzer.analyze_record(record) for decl_id, record in records_by_decl_id.items() @@ -97,7 +97,7 @@ def analyze_record_shape( *, records_by_decl_id: dict[str, Struct] | None = None, record_layouts: dict[str, RecordLayoutFacts] | None = None, - type_lowerer: LowerTypePass | None = None, + type_mapper: MapTypePass | None = None, ) -> RecordAnalysisFacts: """Analyze one CIR record. @@ -110,7 +110,7 @@ def analyze_record_shape( return RecordShapeAnalyzer( records_by_decl_id=records, record_layouts=layouts, - type_lowerer=type_lowerer or LowerTypePass(), + type_mapper=type_mapper or MapTypePass(), ).analyze_record(record) @@ -120,7 +120,7 @@ class RecordShapeAnalyzer: records_by_decl_id: dict[str, Struct] record_layouts: dict[str, RecordLayoutFacts] - type_lowerer: LowerTypePass + type_mapper: MapTypePass def __post_init__(self) -> None: self._record_cache: dict[str, RecordAnalysisFacts] = {} @@ -251,16 +251,16 @@ def _plain_fields_by_value_shape( flexible_tail: FlexibleTail | None = None for field_fact in facts.plain_fields: field = decl.fields[field_fact.index] - lowered_type, reason = try_lower_type( - self.type_lowerer, + mapped_type, reason = try_map_type( + self.type_mapper, field.type, subject=f"field `{field_display_name(field, field_fact.index)}`", failure_suffix="opaque storage emitted", ) - if reason is not None or lowered_type is None: - return ByValueRecordShape(False, ((reason or "field could not be lowered"),)) + if reason is not None or mapped_type is None: + return ByValueRecordShape(False, ((reason or "field could not be mapped"),)) if field.fam_pattern is not None: - tail_shape = _direct_flexible_tail_shape(field, field_fact, lowered_type) + tail_shape = _direct_flexible_tail_shape(field, field_fact, mapped_type) if not tail_shape.valid: return tail_shape flexible_tail = tail_shape.flexible_tail @@ -299,8 +299,8 @@ def _nested_record_field_shape( def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueRecordShape: for run_layout in facts.bitfield_runs: - _, reason = try_lower_type( - self.type_lowerer, + _, reason = try_map_type( + self.type_mapper, run_layout.unsigned_storage_type, subject=f"bitfield storage `{run_layout.name}`", failure_suffix="opaque storage emitted", @@ -309,8 +309,8 @@ def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueReco return ByValueRecordShape(False, (reason,)) for field_layout in run_layout.fields: - _, reason = try_lower_type( - self.type_lowerer, + _, reason = try_map_type( + self.type_mapper, field_layout.logical_type, subject=f"bitfield `{field_display_name(field_layout.field, field_layout.index)}`", failure_suffix="opaque storage emitted", @@ -331,24 +331,24 @@ def _cache_by_value( return shape -def _lower_flexible_tail_metadata( +def _map_flexible_tail_metadata( field, index: int, byte_offset: int, - lowered_type, + mapped_type, ) -> tuple[FlexibleTail | None, str | None]: if not isinstance(field.type, Array): - return None, "flexible tail field did not lower from an array type; opaque storage emitted" - if not isinstance(lowered_type, Array) or lowered_type.size != 0: + return None, "flexible tail field did not map from an array type; opaque storage emitted" + if not isinstance(mapped_type, Array) or mapped_type.size != 0: return ( None, - f"field `{field_display_name(field, index)}` did not lower to InlineArray[..., 0]; " + f"field `{field_display_name(field, index)}` did not map to InlineArray[..., 0]; " "opaque storage emitted", ) return ( FlexibleTail( field_name=field.name, - element_type=lowered_type.element, + element_type=mapped_type.element, pattern=field.fam_pattern, byte_offset=byte_offset, ), @@ -356,15 +356,15 @@ def _lower_flexible_tail_metadata( ) -def _direct_flexible_tail_shape(field, field_fact, lowered_type) -> ByValueRecordShape: - direct_tail, tail_reason = _lower_flexible_tail_metadata( +def _direct_flexible_tail_shape(field, field_fact, mapped_type) -> ByValueRecordShape: + direct_tail, tail_reason = _map_flexible_tail_metadata( field, field_fact.index, field_fact.byte_offset, - lowered_type, + mapped_type, ) if tail_reason is not None or direct_tail is None: - return ByValueRecordShape(False, ((tail_reason or "flexible tail could not lower"),)) + return ByValueRecordShape(False, ((tail_reason or "flexible tail could not map"),)) return ByValueRecordShape(True, flexible_tail=direct_tail) diff --git a/mojo_bindgen/analysis/mojo/__init__.py b/mojo_bindgen/analysis/mojo/__init__.py index b3b241f..27a46c4 100644 --- a/mojo_bindgen/analysis/mojo/__init__.py +++ b/mojo_bindgen/analysis/mojo/__init__.py @@ -1 +1 @@ -"""CIR-to-MojoIR lowering and MojoIR policy passes.""" +"""CIR-to-MojoIR mapping and MojoIR policy passes.""" diff --git a/mojo_bindgen/analysis/mojo/alias_lowering.py b/mojo_bindgen/analysis/mojo/alias_mapping.py similarity index 55% rename from mojo_bindgen/analysis/mojo/alias_lowering.py rename to mojo_bindgen/analysis/mojo/alias_mapping.py index 882559e..d35007f 100644 --- a/mojo_bindgen/analysis/mojo/alias_lowering.py +++ b/mojo_bindgen/analysis/mojo/alias_mapping.py @@ -1,9 +1,9 @@ -"""Shared helpers for lowering typedef-like aliases into MojoIR.""" +"""Shared helpers for mapping typedef-like aliases into MojoIR.""" from __future__ import annotations from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, exact_width_stdint_alias_type +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass, exact_width_stdint_alias_type from mojo_bindgen.ir import AliasDecl, AliasKind, DocComment, FunctionPtr, NamedType, Type @@ -13,37 +13,37 @@ def is_named_type(t: Type, name: str) -> bool: return isinstance(t, NamedType) and t.name == name -def lower_typedef_alias( +def map_typedef_alias( *, c_name: str, aliased: Type, - type_lowerer: LowerTypePass, + type_mapper: MapTypePass, doc: DocComment | None = None, ) -> AliasDecl | None: - """Lower a C typedef-like alias, skipping no-op aliases to the same name.""" + """Map a C typedef-like alias, skipping no-op aliases to the same name.""" alias_name = mojo_ident(c_name) - lowered_type = exact_width_stdint_alias_type(c_name) - if lowered_type is None: - lowered_type = type_lowerer.run(aliased) - if isinstance(lowered_type, FunctionPtr): + mapped_type = exact_width_stdint_alias_type(c_name) + if mapped_type is None: + mapped_type = type_mapper.run(aliased) + if isinstance(mapped_type, FunctionPtr): return AliasDecl( name=alias_name, kind=AliasKind.CALLBACK_SIGNATURE, - type_value=lowered_type, + type_value=mapped_type, doc=doc, ) - if is_named_type(lowered_type, alias_name): + if is_named_type(mapped_type, alias_name): return None return AliasDecl( name=alias_name, kind=AliasKind.TYPE_ALIAS, - type_value=lowered_type, + type_value=mapped_type, doc=doc, ) __all__ = [ "is_named_type", - "lower_typedef_alias", + "map_typedef_alias", ] diff --git a/mojo_bindgen/analysis/mojo/const_lowering.py b/mojo_bindgen/analysis/mojo/const_expr_mapping.py similarity index 63% rename from mojo_bindgen/analysis/mojo/const_lowering.py rename to mojo_bindgen/analysis/mojo/const_expr_mapping.py index 33d38c8..92bbba9 100644 --- a/mojo_bindgen/analysis/mojo/const_lowering.py +++ b/mojo_bindgen/analysis/mojo/const_expr_mapping.py @@ -1,11 +1,11 @@ -"""Lower CIR constant expressions into Mojo-facing constant-expression IR.""" +"""Map CIR constant expressions into Mojo-facing constant-expression IR.""" from __future__ import annotations from dataclasses import dataclass from mojo_bindgen.analysis.common import mojo_float_literal_text, mojo_ident -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( BinaryExpr, CallExpr, @@ -22,21 +22,21 @@ ) -class ConstExprLoweringError(ValueError): - """Raised when a CIR constant expression cannot be lowered to MojoIR.""" +class ConstExprMappingError(ValueError): + """Raised when a CIR constant expression cannot be mapped to MojoIR.""" @dataclass -class LowerConstExprPass: - """Lower CIR constant expressions into Mojo-facing constant expressions.""" +class MapConstExprPass: + """Map CIR constant expressions into Mojo-facing constant expressions.""" - type_lowering: LowerTypePass + type_mapping: MapTypePass @staticmethod def _parse_float_literal(value: str | float) -> float: text = mojo_float_literal_text(str(value)) - lowered = text.lower() - if lowered.startswith(("0x", "+0x", "-0x")): + mapped = text.lower() + if mapped.startswith(("0x", "+0x", "-0x")): return float.fromhex(text) return float(text) @@ -57,33 +57,31 @@ def run(self, expr: ConstExpr) -> ConstExpr: return BinaryExpr(op=expr.op, lhs=self.run(expr.lhs), rhs=self.run(expr.rhs)) if isinstance(expr, CastExpr): return CastExpr( - target=self.type_lowering.run(expr.target), + target=self.type_mapping.run(expr.target), expr=self.run(expr.expr), ) if isinstance(expr, SizeOfExpr): - return SizeOfExpr(target=self.type_lowering.run(expr.target)) + return SizeOfExpr(target=self.type_mapping.run(expr.target)) if isinstance(expr, CallExpr): return CallExpr( callee=self.run(expr.callee), args=[self.run(arg) for arg in expr.args], ) if isinstance(expr, NullPtrLiteral): - raise ConstExprLoweringError( - "nullptr constants do not have a valid MojoIR literal form" - ) - raise ConstExprLoweringError( + raise ConstExprMappingError("nullptr constants do not have a valid MojoIR literal form") + raise ConstExprMappingError( f"unsupported CIR constant-expression node: {type(expr).__name__!r}" ) -def lower_const_expr(expr: ConstExpr) -> ConstExpr: - """Lower one CIR constant expression to MojoIR.""" +def map_const_expr(expr: ConstExpr) -> ConstExpr: + """Map one CIR constant expression to MojoIR.""" - return LowerConstExprPass(type_lowering=LowerTypePass()).run(expr) + return MapConstExprPass(type_mapping=MapTypePass()).run(expr) __all__ = [ - "ConstExprLoweringError", - "LowerConstExprPass", - "lower_const_expr", + "ConstExprMappingError", + "MapConstExprPass", + "map_const_expr", ] diff --git a/mojo_bindgen/analysis/mojo/const_value_lowering.py b/mojo_bindgen/analysis/mojo/const_value_mapping.py similarity index 57% rename from mojo_bindgen/analysis/mojo/const_value_lowering.py rename to mojo_bindgen/analysis/mojo/const_value_mapping.py index b206d05..e6bcd97 100644 --- a/mojo_bindgen/analysis/mojo/const_value_lowering.py +++ b/mojo_bindgen/analysis/mojo/const_value_mapping.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( _MOJO_INT_TYPES, BinaryExpr, @@ -19,32 +19,32 @@ def typed_const_value( value: ConstExpr, decl_type: Type, *, - type_lowerer: LowerTypePass, + type_mapper: MapTypePass, ) -> ConstExpr: """Apply Mojo-facing type coercions needed for emitted constant aliases.""" - lowered_type = type_lowerer.run(decl_type) - if isinstance(lowered_type, BuiltinType) and lowered_type.name in _MOJO_INT_TYPES: - return _coerce_integral_const(value, lowered_type) + mapped_type = type_mapper.run(decl_type) + if isinstance(mapped_type, BuiltinType) and mapped_type.name in _MOJO_INT_TYPES: + return _coerce_integral_const(value, mapped_type) return value def _coerce_integral_const( value: ConstExpr, - lowered_type: BuiltinType, + mapped_type: BuiltinType, ) -> ConstExpr: if isinstance(value, IntLiteral): - return CastExpr(target=lowered_type, expr=value) + return CastExpr(target=mapped_type, expr=value) if isinstance(value, UnaryExpr): return UnaryExpr( op=value.op, - operand=_coerce_integral_const(value.operand, lowered_type), + operand=_coerce_integral_const(value.operand, mapped_type), ) if isinstance(value, BinaryExpr): return BinaryExpr( op=value.op, - lhs=_coerce_integral_const(value.lhs, lowered_type), - rhs=_coerce_integral_const(value.rhs, lowered_type), + lhs=_coerce_integral_const(value.lhs, mapped_type), + rhs=_coerce_integral_const(value.rhs, mapped_type), ) return value diff --git a/mojo_bindgen/analysis/mojo/decl_lowerer.py b/mojo_bindgen/analysis/mojo/decl_mapping.py similarity index 58% rename from mojo_bindgen/analysis/mojo/decl_lowerer.py rename to mojo_bindgen/analysis/mojo/decl_mapping.py index c9fc31c..16f7b20 100644 --- a/mojo_bindgen/analysis/mojo/decl_lowerer.py +++ b/mojo_bindgen/analysis/mojo/decl_mapping.py @@ -3,21 +3,21 @@ from dataclasses import dataclass from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.mojo.alias_lowering import lower_typedef_alias -from mojo_bindgen.analysis.mojo.const_lowering import ( - ConstExprLoweringError, - LowerConstExprPass, +from mojo_bindgen.analysis.mojo.alias_mapping import map_typedef_alias +from mojo_bindgen.analysis.mojo.const_expr_mapping import ( + ConstExprMappingError, + MapConstExprPass, ) -from mojo_bindgen.analysis.mojo.const_value_lowering import typed_const_value -from mojo_bindgen.analysis.mojo.lowering_support import stub_note -from mojo_bindgen.analysis.mojo.macro_lowering import MacroLowerer +from mojo_bindgen.analysis.mojo.const_value_mapping import typed_const_value +from mojo_bindgen.analysis.mojo.macro_mapping import MacroMapper +from mojo_bindgen.analysis.mojo.mapping_support import stub_note from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.mojo.struct_lowering import ( - StructLoweringContext, - lower_struct, +from mojo_bindgen.analysis.mojo.struct_mapping import ( + StructMappingContext, + map_struct, ) -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass -from mojo_bindgen.analysis.mojo.union_lowering import LowerUnionPass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass +from mojo_bindgen.analysis.mojo.union_mapping import MapUnionPass from mojo_bindgen.ir import ( AliasDecl, AliasKind, @@ -49,8 +49,8 @@ ) -class UnitLoweringError(ValueError): - """Raised when a CIR declaration cannot be lowered to MojoIR.""" +class UnitMappingError(ValueError): + """Raised when a CIR declaration cannot be mapped to MojoIR.""" def _link_mode_for_options(options: MojoEmitOptions) -> LinkMode: @@ -60,56 +60,56 @@ def _link_mode_for_options(options: MojoEmitOptions) -> LinkMode: @dataclass(frozen=True) -class LoweringSession: - """Immutable collaborators shared across one ``Unit`` lowering run.""" +class MappingSession: + """Immutable collaborators shared across one ``Unit`` mapping run.""" unit: Unit options: MojoEmitOptions - type_lowerer: LowerTypePass - const_lowerer: LowerConstExprPass - struct_context: StructLoweringContext + type_mapper: MapTypePass + const_expr_mapper: MapConstExprPass + struct_context: StructMappingContext -class UnitDeclLowerer: - """Lower one top-level CIR declaration into one or more MojoIR declarations.""" +class UnitDeclMapper: + """Map one top-level CIR declaration into one or more MojoIR declarations.""" - def __init__(self, session: LoweringSession) -> None: + def __init__(self, session: MappingSession) -> None: self.session = session self._emitted_const_names: set[str] = set() - self._union_lowerer = LowerUnionPass(type_lowerer=session.type_lowerer) - self._macro_lowerer = MacroLowerer( - const_lowerer=session.const_lowerer, - type_lowerer=session.type_lowerer, + self._union_mapper = MapUnionPass(type_mapper=session.type_mapper) + self._macro_mapper = MacroMapper( + const_expr_mapper=session.const_expr_mapper, + type_mapper=session.type_mapper, emitted_const_names=self._emitted_const_names, ) - def lower_decl(self, decl: Decl) -> MojoDecl | list[MojoDecl] | None: + def map_decl(self, decl: Decl) -> MojoDecl | list[MojoDecl] | None: if isinstance(decl, Typedef): - return self._lower_typedef(decl) + return self._map_typedef(decl) if isinstance(decl, Enum): - return self._lower_enum(decl) + return self._map_enum(decl) if isinstance(decl, Function): - return self._lower_function(decl) + return self._map_function(decl) if isinstance(decl, GlobalVar): - return self._lower_global(decl) + return self._map_global(decl) if isinstance(decl, Const): - return self._lower_const(decl) + return self._map_const(decl) if isinstance(decl, MacroDecl): - return self._lower_macro(decl) + return self._map_macro(decl) if isinstance(decl, Struct): - return self._lower_struct(decl) - raise UnitLoweringError(f"unsupported CIR declaration node: {type(decl).__name__!r}") + return self._map_struct(decl) + raise UnitMappingError(f"unsupported CIR declaration node: {type(decl).__name__!r}") - def _lower_typedef(self, decl: Typedef) -> AliasDecl | None: - return lower_typedef_alias( + def _map_typedef(self, decl: Typedef) -> AliasDecl | None: + return map_typedef_alias( c_name=decl.name, aliased=decl.aliased, - type_lowerer=self.session.type_lowerer, + type_mapper=self.session.type_mapper, doc=decl.doc, ) - def _lower_enum(self, decl: Enum) -> list[MojoDecl]: - underlying = self.session.type_lowerer.run(decl.underlying) + def _map_enum(self, decl: Enum) -> list[MojoDecl]: + underlying = self.session.type_mapper.run(decl.underlying) enum_name = mojo_ident(decl.name) enum_decl = AliasDecl( name=enum_name, @@ -148,19 +148,19 @@ def _lower_enum(self, decl: Enum) -> list[MojoDecl]: self._emitted_const_names.update(mojo_ident(alias_name) for alias_name in decl.alias_names) return [enum_decl, *aliases, *enumerants] - def _lower_function(self, decl: Function) -> FunctionDecl: + def _map_function(self, decl: Function) -> FunctionDecl: return FunctionDecl( name=mojo_ident(decl.name), link_name=decl.link_name, params=[ Param( name=(mojo_ident(param.name, fallback=f"a{i}") if param.name else f"a{i}"), - type=self.session.type_lowerer.run(param.type), + type=self.session.type_mapper.run(param.type), doc=param.doc, ) for i, param in enumerate(decl.params) ], - return_type=self.session.type_lowerer.run(decl.ret), + return_type=self.session.type_mapper.run(decl.ret), kind=(FunctionKind.VARIADIC_STUB if decl.is_variadic else FunctionKind.WRAPPER), call_target=CallTarget( link_mode=_link_mode_for_options(self.session.options), @@ -169,29 +169,29 @@ def _lower_function(self, decl: Function) -> FunctionDecl: doc=decl.doc, ) - def _lower_global(self, decl: GlobalVar) -> GlobalDecl: - lowered_type = self.session.type_lowerer.run(decl.type) + def _map_global(self, decl: GlobalVar) -> GlobalDecl: + mapped_type = self.session.type_mapper.run(decl.type) is_atomic = ( - isinstance(lowered_type, ParametricType) and lowered_type.base == ParametricBase.ATOMIC + isinstance(mapped_type, ParametricType) and mapped_type.base == ParametricBase.ATOMIC ) return GlobalDecl( name=mojo_ident(decl.name), link_name=decl.link_name, - value_type=lowered_type, + value_type=mapped_type, is_const=decl.is_const, kind=(GlobalKind.STUB if is_atomic else GlobalKind.WRAPPER), doc=decl.doc, ) - def _lower_const(self, decl: Const) -> AliasDecl: + def _map_const(self, decl: Const) -> AliasDecl: try: - value = self.session.const_lowerer.run(decl.expr) - except ConstExprLoweringError: + value = self.session.const_expr_mapper.run(decl.expr) + except ConstExprMappingError: return AliasDecl( name=mojo_ident(decl.name), kind=AliasKind.CONST_VALUE, diagnostics=[ - stub_note("constant expression could not be lowered; placeholder alias emitted") + stub_note("constant expression could not be mapped; placeholder alias emitted") ], doc=decl.doc, ) @@ -204,13 +204,13 @@ def _lower_const(self, decl: Const) -> AliasDecl: doc=decl.doc, ) - def _lower_macro(self, decl: MacroDecl) -> AliasDecl | None: - return self._macro_lowerer.lower(decl) + def _map_macro(self, decl: MacroDecl) -> AliasDecl | None: + return self._macro_mapper.map(decl) - def _lower_struct(self, decl: Struct) -> MojoDecl: + def _map_struct(self, decl: Struct) -> MojoDecl: if decl.is_union: - return self._union_lowerer.run(decl) - return lower_struct(decl, context=self.session.struct_context) + return self._union_mapper.run(decl) + return map_struct(decl, context=self.session.struct_context) def _typed_const_value(self, value: ConstExpr, decl_type) -> ConstExpr: - return typed_const_value(value, decl_type, type_lowerer=self.session.type_lowerer) + return typed_const_value(value, decl_type, type_mapper=self.session.type_mapper) diff --git a/mojo_bindgen/analysis/mojo/macro_lowering.py b/mojo_bindgen/analysis/mojo/macro_mapping.py similarity index 75% rename from mojo_bindgen/analysis/mojo/macro_lowering.py rename to mojo_bindgen/analysis/mojo/macro_mapping.py index 67a53f6..a69b5a2 100644 --- a/mojo_bindgen/analysis/mojo/macro_lowering.py +++ b/mojo_bindgen/analysis/mojo/macro_mapping.py @@ -1,14 +1,14 @@ -"""Lower C macro declarations into MojoIR aliases or diagnostic placeholders.""" +"""Map C macro declarations into MojoIR aliases or diagnostic placeholders.""" from __future__ import annotations from dataclasses import dataclass from mojo_bindgen.analysis.common import mojo_ident -from mojo_bindgen.analysis.mojo.const_lowering import ConstExprLoweringError, LowerConstExprPass -from mojo_bindgen.analysis.mojo.const_value_lowering import typed_const_value -from mojo_bindgen.analysis.mojo.lowering_support import lowering_note, stub_note -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.const_expr_mapping import ConstExprMappingError, MapConstExprPass +from mojo_bindgen.analysis.mojo.const_value_mapping import typed_const_value +from mojo_bindgen.analysis.mojo.mapping_support import mapping_note, stub_note +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( AliasDecl, AliasKind, @@ -25,14 +25,14 @@ @dataclass -class MacroLowerer: - """Lower macro declarations while tracking already-emitted constant names.""" +class MacroMapper: + """Map macro declarations while tracking already-emitted constant names.""" - const_lowerer: LowerConstExprPass - type_lowerer: LowerTypePass + const_expr_mapper: MapConstExprPass + type_mapper: MapTypePass emitted_const_names: set[str] - def lower(self, decl: MacroDecl) -> AliasDecl | None: + def map(self, decl: MacroDecl) -> AliasDecl | None: if decl.kind == "empty": return None @@ -46,13 +46,13 @@ def lower(self, decl: MacroDecl) -> AliasDecl | None: ) if decl.expr is not None and decl.type is not None: - lowered = self._lower_value_macro(decl, name) - if lowered is not None: - return lowered + mapped = self._map_value_macro(decl, name) + if mapped is not None: + return mapped return self._placeholder_alias(decl) - def _lower_value_macro(self, decl: MacroDecl, name: str) -> AliasDecl | None: + def _map_value_macro(self, decl: MacroDecl, name: str) -> AliasDecl | None: assert decl.expr is not None assert decl.type is not None @@ -61,8 +61,8 @@ def _lower_value_macro(self, decl: MacroDecl, name: str) -> AliasDecl | None: return self._comment_alias(decl, f"macro {decl.name}: {blocker}") try: - value = self.const_lowerer.run(decl.expr) - except ConstExprLoweringError: + value = self.const_expr_mapper.run(decl.expr) + except ConstExprMappingError: if isinstance(decl.expr, NullPtrLiteral): return self._comment_alias( decl, @@ -77,7 +77,7 @@ def _lower_value_macro(self, decl: MacroDecl, name: str) -> AliasDecl | None: const_value=typed_const_value( value, decl.type, - type_lowerer=self.type_lowerer, + type_mapper=self.type_mapper, ), doc=decl.doc, ) @@ -114,7 +114,7 @@ def _placeholder_alias(self, decl: MacroDecl) -> AliasDecl: diagnostics=( self._comment_notes(decl, f"macro {decl.name}: {decl.diagnostic}") if decl.diagnostic - else [stub_note("macro lowering is incomplete; placeholder alias emitted")] + else [stub_note("macro mapping is incomplete; placeholder alias emitted")] ), doc=decl.doc, ) @@ -129,8 +129,8 @@ def _comment_alias(self, decl: MacroDecl, message: str) -> AliasDecl: def _comment_notes(self, decl: MacroDecl, message: str): return [ - lowering_note(message, category="macro_comment"), - lowering_note( + mapping_note(message, category="macro_comment"), + mapping_note( f"define {decl.name} {' '.join(decl.tokens)}", category="macro_comment", ), @@ -138,5 +138,5 @@ def _comment_notes(self, decl: MacroDecl, message: str): __all__ = [ - "MacroLowerer", + "MacroMapper", ] diff --git a/mojo_bindgen/analysis/mojo/lowering_support.py b/mojo_bindgen/analysis/mojo/mapping_support.py similarity index 52% rename from mojo_bindgen/analysis/mojo/lowering_support.py rename to mojo_bindgen/analysis/mojo/mapping_support.py index b7315c4..04a9697 100644 --- a/mojo_bindgen/analysis/mojo/lowering_support.py +++ b/mojo_bindgen/analysis/mojo/mapping_support.py @@ -1,15 +1,15 @@ -"""Small shared helpers for the active CIR -> MojoIR lowering pipeline.""" +"""Small shared helpers for the active CIR -> MojoIR mapping pipeline.""" from __future__ import annotations from mojo_bindgen.analysis.common import mojo_ident from mojo_bindgen.analysis.facts.indexes import record_by_decl_id -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, TypeLoweringError +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass, TypeMappingError from mojo_bindgen.ir import ( BuiltinType, Field, - LoweringNote, - LoweringSeverity, + MappingNote, + MappingSeverity, MojoBuiltin, Struct, Type, @@ -37,59 +37,59 @@ def field_mojo_name(field: Field, index: int) -> str: def record_name(decl: Struct) -> str: - """Lowered Mojo-facing record name.""" + """Mapped Mojo-facing record name.""" raw_name = decl.name.strip() or decl.c_name.strip() fallback = "anonymous_union" if decl.is_union else "anonymous_struct" return mojo_ident(raw_name, fallback=fallback) -def lowering_note(message: str, *, category: str) -> LoweringNote: - return LoweringNote( - severity=LoweringSeverity.NOTE, +def mapping_note(message: str, *, category: str) -> MappingNote: + return MappingNote( + severity=MappingSeverity.NOTE, message=message, category=category, ) -def stub_note(message: str) -> LoweringNote: - return lowering_note(message, category="stub_lowering") +def stub_note(message: str) -> MappingNote: + return mapping_note(message, category="stub_mapping") -def struct_note(message: str) -> LoweringNote: - return lowering_note(message, category="struct_lowering") +def struct_note(message: str) -> MappingNote: + return mapping_note(message, category="struct_mapping") -def union_note(message: str) -> LoweringNote: - return lowering_note(message, category="union_lowering") +def union_note(message: str) -> MappingNote: + return mapping_note(message, category="union_mapping") -def try_lower_type( - type_lowerer: LowerTypePass, +def try_map_type( + type_mapper: MapTypePass, t: Type, *, subject: str, failure_suffix: str, ) -> tuple[Type | None, str | None]: - """Lower one CIR type and turn lowering failure into a diagnostic reason.""" + """Map one CIR type and turn mapping failure into a diagnostic reason.""" try: - lowered = type_lowerer.run(t) - except TypeLoweringError as exc: - return None, f"{subject} could not be lowered ({exc}); {failure_suffix}" - if lowered == BuiltinType(MojoBuiltin.UNSUPPORTED): - return None, f"{subject} lowered to an unsupported Mojo type; {failure_suffix}" - return lowered, None + mapped = type_mapper.run(t) + except TypeMappingError as exc: + return None, f"{subject} could not be mapped ({exc}); {failure_suffix}" + if mapped == BuiltinType(MojoBuiltin.UNSUPPORTED): + return None, f"{subject} mapped to an unsupported Mojo type; {failure_suffix}" + return mapped, None __all__ = [ "field_display_name", "field_mojo_name", - "lowering_note", + "mapping_note", "record_by_decl_id", "record_name", "struct_note", "stub_note", - "try_lower_type", + "try_map_type", "union_note", ] diff --git a/mojo_bindgen/analysis/mojo/mojo_emit_options.py b/mojo_bindgen/analysis/mojo/mojo_emit_options.py index 3d16037..ee3af30 100644 --- a/mojo_bindgen/analysis/mojo/mojo_emit_options.py +++ b/mojo_bindgen/analysis/mojo/mojo_emit_options.py @@ -24,7 +24,7 @@ class MojoEmitOptions: linking: LinkingMode = "external_call" """external_call: link C symbols at mojo build time; emitted wrappers use ``abi("C")``. owned_dl_handle: resolve via ``OwnedDLHandle.call`` (raises); wrappers omit ``abi("C")`` on - the ``def`` line because ``abi("C")`` combined with ``raises`` currently fails LLVM lowering.""" + the ``def`` line because ``abi("C")`` combined with ``raises`` currently fails LLVM mapping.""" library_path_hint: str | None = None """If set with owned_dl_handle, pass this path to OwnedDLHandle(...). If None, use DEFAULT_RTLD (symbols must be linked into the process).""" diff --git a/mojo_bindgen/analysis/mojo/record_policies.py b/mojo_bindgen/analysis/mojo/record_policies.py index 92c5c4b..0705708 100644 --- a/mojo_bindgen/analysis/mojo/record_policies.py +++ b/mojo_bindgen/analysis/mojo/record_policies.py @@ -1,4 +1,4 @@ -"""Late record-policy assignment on lowered MojoIR.""" +"""Late record-policy assignment on mapped MojoIR.""" from __future__ import annotations @@ -229,7 +229,7 @@ def _is_pure_bitfield(decl: StructDecl) -> bool: def assign_record_policies(module: MojoModule) -> MojoModule: - """Assign final record policy bits to a fully lowered ``MojoModule``.""" + """Assign final record policy bits to a fully mapped ``MojoModule``.""" return PolicyInferencePass().run(module) diff --git a/mojo_bindgen/analysis/mojo/struct_lowering.py b/mojo_bindgen/analysis/mojo/struct_mapping.py similarity index 78% rename from mojo_bindgen/analysis/mojo/struct_lowering.py rename to mojo_bindgen/analysis/mojo/struct_mapping.py index cc8168b..d4ae3f7 100644 --- a/mojo_bindgen/analysis/mojo/struct_lowering.py +++ b/mojo_bindgen/analysis/mojo/struct_mapping.py @@ -1,4 +1,4 @@ -"""Lower CIR structs into MojoIR structs using pure layout facts plus Mojo planning.""" +"""Map CIR structs into MojoIR structs using pure layout facts plus Mojo planning.""" from __future__ import annotations @@ -11,14 +11,14 @@ RecordStorageKind, analyze_record_shapes, ) -from mojo_bindgen.analysis.mojo.lowering_support import ( +from mojo_bindgen.analysis.mojo.mapping_support import ( field_display_name, field_mojo_name, record_name, struct_note, - try_lower_type, + try_map_type, ) -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( AtomicType, BitfieldField, @@ -37,24 +37,24 @@ ) -class StructLoweringError(ValueError): - """Raised when a CIR struct declaration cannot be lowered to MojoIR.""" +class StructMappingError(ValueError): + """Raised when a CIR struct declaration cannot be mapped to MojoIR.""" @dataclass -class StructLoweringContext: +class StructMappingContext: record_map: dict[str, Struct] target_abi: TargetABI - type_lowerer: LowerTypePass + type_mapper: MapTypePass record_layouts: dict[str, RecordLayoutFacts] = field(default_factory=dict) record_shapes: dict[str, RecordAnalysisFacts] = field(default_factory=dict) -def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: - """Lower one top-level CIR struct declaration to MojoIR.""" +def map_struct(decl: Struct, *, context: StructMappingContext) -> StructDecl: + """Map one top-level CIR struct declaration to MojoIR.""" if decl.is_union: - raise StructLoweringError( + raise StructMappingError( f"expected non-union Struct declaration, got union {decl.decl_id!r}" ) @@ -70,12 +70,10 @@ def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: fallback_reasons=shape.fallback_reasons, ) - plain_fields, lowered_bitfield_groups, diagnostic_notes, fallback_reasons = ( - _lower_typed_members( - decl, - facts, - context=context, - ) + plain_fields, mapped_bitfield_groups, diagnostic_notes, fallback_reasons = _map_typed_members( + decl, + facts, + context=context, ) if fallback_reasons: return _opaque_storage_struct_decl( @@ -93,10 +91,10 @@ def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: align=align, align_decorator=align_decorator, fieldwise_init=False, - members=_build_members(decl, facts, plain_fields, lowered_bitfield_groups), + members=_build_members(decl, facts, plain_fields, mapped_bitfield_groups), initializers=_build_initializers( facts, - lowered_bitfield_groups, + mapped_bitfield_groups, uses_opaque_storage=False, ), flexible_tail=shape.flexible_tail, @@ -109,40 +107,40 @@ def lower_struct(decl: Struct, *, context: StructLoweringContext) -> StructDecl: ) -def _lower_typed_members( +def _map_typed_members( decl: Struct, facts: RecordLayoutFacts, *, - context: StructLoweringContext, + context: StructMappingContext, ) -> tuple[ list[StoredMember], list[BitfieldGroupMember], tuple[str, ...], tuple[str, ...], ]: - plain_fields, diagnostic_notes, plain_failures = _lower_plain_fields( + plain_fields, diagnostic_notes, plain_failures = _map_plain_fields( decl, facts, context=context, ) - lowered_bitfield_groups, bitfield_failures = _lower_bitfield_groups( + mapped_bitfield_groups, bitfield_failures = _map_bitfield_groups( facts, context=context, ) return ( plain_fields, - lowered_bitfield_groups, + mapped_bitfield_groups, tuple(diagnostic_notes), (*plain_failures, *bitfield_failures), ) -def _lower_plain_fields( +def _map_plain_fields( decl: Struct, facts: RecordLayoutFacts, *, - context: StructLoweringContext, + context: StructMappingContext, ) -> tuple[list[StoredMember], tuple[str, ...], tuple[str, ...]]: plain_fields: list[StoredMember] = [] diagnostic_notes: list[str] = [] @@ -150,18 +148,18 @@ def _lower_plain_fields( for field_fact in facts.plain_fields: field = decl.fields[field_fact.index] display_name = field_display_name(field, field_fact.index) - lowered_type, reason = try_lower_type( - context.type_lowerer, + mapped_type, reason = try_map_type( + context.type_mapper, field.type, subject=f"field `{display_name}`", failure_suffix="opaque storage emitted", ) - if reason is not None or lowered_type is None: + if reason is not None or mapped_type is None: if reason is not None: fallback_reasons.append(reason) continue if isinstance(field.type, AtomicType) and not ( - isinstance(lowered_type, ParametricType) and lowered_type.base == ParametricBase.ATOMIC + isinstance(mapped_type, ParametricType) and mapped_type.base == ParametricBase.ATOMIC ): note = ( "some atomic types were mapped to their underlying non-atomic Mojo type " @@ -173,7 +171,7 @@ def _lower_plain_fields( StoredMember( index=field_fact.index, name=field_mojo_name(field, field_fact.index), - type=lowered_type, + type=mapped_type, byte_offset=field_fact.byte_offset, doc=field.doc, ) @@ -183,55 +181,55 @@ def _lower_plain_fields( return plain_fields, tuple(diagnostic_notes), tuple(fallback_reasons) -def _lower_bitfield_groups( +def _map_bitfield_groups( facts: RecordLayoutFacts, *, - context: StructLoweringContext, + context: StructMappingContext, ) -> tuple[list[BitfieldGroupMember], tuple[str, ...]]: - lowered_bitfield_groups: list[BitfieldGroupMember] = [] + mapped_bitfield_groups: list[BitfieldGroupMember] = [] fallback_reasons: list[str] = [] for run_layout in facts.bitfield_runs: - lowered_storage_type, reason = _lower_bitfield_storage(run_layout, context=context) - if reason is not None or lowered_storage_type is None: + mapped_storage_type, reason = _map_bitfield_storage(run_layout, context=context) + if reason is not None or mapped_storage_type is None: if reason is not None: fallback_reasons.append(reason) continue - lowered_fields, field_failures = _lower_bitfield_fields(run_layout, context=context) + mapped_fields, field_failures = _map_bitfield_fields(run_layout, context=context) fallback_reasons.extend(field_failures) - lowered_bitfield_groups.append( + mapped_bitfield_groups.append( BitfieldGroupMember( storage_name=run_layout.name, - storage_type=lowered_storage_type, + storage_type=mapped_storage_type, byte_offset=run_layout.byte_offset, first_index=run_layout.first_index, storage_width_bits=run_layout.storage_width_bits, - fields=lowered_fields, + fields=mapped_fields, ) ) - return lowered_bitfield_groups, tuple(fallback_reasons) + return mapped_bitfield_groups, tuple(fallback_reasons) -def _lower_bitfield_storage(run_layout, *, context: StructLoweringContext): - return try_lower_type( - context.type_lowerer, +def _map_bitfield_storage(run_layout, *, context: StructMappingContext): + return try_map_type( + context.type_mapper, run_layout.unsigned_storage_type, subject=f"bitfield storage `{run_layout.name}`", failure_suffix="opaque storage emitted", ) -def _lower_bitfield_fields( +def _map_bitfield_fields( run_layout, *, - context: StructLoweringContext, + context: StructMappingContext, ) -> tuple[list[BitfieldField], tuple[str, ...]]: - lowered_fields: list[BitfieldField] = [] + mapped_fields: list[BitfieldField] = [] fallback_reasons: list[str] = [] for field_layout in run_layout.fields: display_name = field_display_name(field_layout.field, field_layout.index) - logical_type, reason = try_lower_type( - context.type_lowerer, + logical_type, reason = try_map_type( + context.type_mapper, field_layout.logical_type, subject=f"bitfield `{display_name}`", failure_suffix="opaque storage emitted", @@ -240,7 +238,7 @@ def _lower_bitfield_fields( if reason is not None: fallback_reasons.append(reason) continue - lowered_fields.append( + mapped_fields.append( BitfieldField( index=field_layout.index, name=field_mojo_name(field_layout.field, field_layout.index), @@ -252,13 +250,13 @@ def _lower_bitfield_fields( doc=field_layout.field.doc, ) ) - return lowered_fields, tuple(fallback_reasons) + return mapped_fields, tuple(fallback_reasons) def _record_shape( decl: Struct, *, - context: StructLoweringContext, + context: StructMappingContext, ) -> RecordAnalysisFacts: cached = context.record_shapes.get(decl.decl_id) if cached is not None: @@ -276,7 +274,7 @@ def _record_shape( context.record_shapes = analyze_record_shapes( context.record_map, layouts, - type_lowerer=context.type_lowerer, + type_mapper=context.type_mapper, ) return context.record_shapes[decl.decl_id] @@ -285,14 +283,14 @@ def _build_members( decl: Struct, facts: RecordLayoutFacts, plain_fields: list[StoredMember], - lowered_bitfield_groups: list[BitfieldGroupMember], + mapped_bitfield_groups: list[BitfieldGroupMember], ) -> list[StoredMember | PaddingMember | OpaqueStorageMember | BitfieldGroupMember]: members_with_offsets: list[ tuple[int, int, StoredMember | PaddingMember | OpaqueStorageMember | BitfieldGroupMember] ] = [(field.byte_offset, field.index, field) for field in plain_fields] if not facts.is_pure_bitfield: - pad_order_base = len(decl.fields) + len(lowered_bitfield_groups) + pad_order_base = len(decl.fields) + len(mapped_bitfield_groups) for i, padding in enumerate(facts.padding_spans): members_with_offsets.append( ( @@ -306,7 +304,7 @@ def _build_members( ) ) - for group in lowered_bitfield_groups: + for group in mapped_bitfield_groups: members_with_offsets.append((group.byte_offset, group.first_index, group)) members_with_offsets.sort(key=lambda item: (item[0], item[1])) @@ -345,7 +343,7 @@ def _compute_align_policy( def _build_initializers( facts: RecordLayoutFacts, - lowered_bitfield_groups: list[BitfieldGroupMember], + mapped_bitfield_groups: list[BitfieldGroupMember], *, uses_opaque_storage: bool, ) -> list[Initializer]: @@ -354,7 +352,7 @@ def _build_initializers( named_members = [ member - for group in lowered_bitfield_groups + for group in mapped_bitfield_groups for member in sorted(group.fields, key=lambda item: item.index) ] initializers = [Initializer(params=[])] @@ -412,7 +410,7 @@ def _opaque_storage_struct_decl( __all__ = [ - "StructLoweringContext", - "StructLoweringError", - "lower_struct", + "StructMappingContext", + "StructMappingError", + "map_struct", ] diff --git a/mojo_bindgen/analysis/mojo/type_lowering.py b/mojo_bindgen/analysis/mojo/type_mapping.py similarity index 86% rename from mojo_bindgen/analysis/mojo/type_lowering.py rename to mojo_bindgen/analysis/mojo/type_mapping.py index 518cddc..557886f 100644 --- a/mojo_bindgen/analysis/mojo/type_lowering.py +++ b/mojo_bindgen/analysis/mojo/type_mapping.py @@ -1,4 +1,4 @@ -"""Lower CIR types into surface-oriented MojoIR type nodes.""" +"""Map CIR types into surface-oriented MojoIR type nodes.""" from __future__ import annotations @@ -42,8 +42,8 @@ ) -class TypeLoweringError(ValueError): - """Raised when a CIR type cannot be lowered to a MojoIR type.""" +class TypeMappingError(ValueError): + """Raised when a CIR type cannot be mapped to a MojoIR type.""" PrimitiveCIRType = VoidType | IntType | FloatType @@ -143,8 +143,8 @@ def exact_width_stdint_alias_type(name: str) -> NamedType | None: @dataclass -class LowerTypePass: - """Lower CIR types into surface-oriented MojoIR type nodes.""" +class MapTypePass: + """Map CIR types into surface-oriented MojoIR type nodes.""" _cache: dict[int, Type] = field(default_factory=dict, init=False, repr=False) _active: set[int] = field(default_factory=set, init=False, repr=False) @@ -157,11 +157,11 @@ def run(self, t: Type) -> Type: if key in self._cache: return self._cache[key] if key in self._active: - raise TypeLoweringError(f"recursive CIR type cycle while lowering {type(t).__name__}") + raise TypeMappingError(f"recursive CIR type cycle while mapping {type(t).__name__}") self._active.add(key) try: t_norm = _strip_transparent_wrappers(t) - result = self._lower(t_norm) + result = self._map(t_norm) finally: self._active.remove(key) @@ -169,14 +169,14 @@ def run(self, t: Type) -> Type: return result # ------------------------ - # Core lowering dispatcher + # Core mapping dispatcher # ------------------------ - def _lower(self, t: Type) -> Type: + def _map(self, t: Type) -> Type: if isinstance(t, (VoidType, IntType, FloatType)): - return self._lower_primitive(t) + return self._map_primitive(t) if isinstance(t, AtomicType): - return self._lower_atomic(t) + return self._map_atomic(t) if isinstance(t, (TypeRef, EnumRef, StructRef)): return self._named(t.name) if isinstance(t, OpaqueRecordRef): @@ -186,23 +186,23 @@ def _lower(self, t: Type) -> Type: origin=PointerOrigin.EXTERNAL, ) if isinstance(t, Pointer): - return self._lower_pointer(t) + return self._map_pointer(t) if isinstance(t, Array): - return self._lower_array(t) + return self._map_array(t) if isinstance(t, FunctionPtr): - return self._lower_function_ptr(t) + return self._map_function_ptr(t) if isinstance(t, ComplexType): - return self._lower_complex(t) + return self._map_complex(t) if isinstance(t, VectorType): - return self._lower_vector(t) + return self._map_vector(t) if isinstance(t, UnsupportedType): return self._unsupported_type(t) - raise TypeLoweringError(f"unsupported CIR type node: {type(t).__name__!r}") + raise TypeMappingError(f"unsupported CIR type node: {type(t).__name__!r}") # ------------------------ # Named types # ------------------------ - def _lower_primitive(self, t: PrimitiveCIRType) -> Type: + def _map_primitive(self, t: PrimitiveCIRType) -> Type: if isinstance(t, VoidType): key = "void" elif isinstance(t, IntType): @@ -219,17 +219,17 @@ def _lower_primitive(self, t: PrimitiveCIRType) -> Type: return self._opaque_bytes(t.size_bytes) key = t.float_kind else: - raise TypeLoweringError(f"expected CIR primitive type, got {type(t).__name__!r}") + raise TypeMappingError(f"expected CIR primitive type, got {type(t).__name__!r}") try: builtin = PRIMITIVE_BUILTINS[key] except KeyError as exc: - raise TypeLoweringError( + raise TypeMappingError( f"no Mojo primitive builtin registered for {type(t).__name__} key {key!r}" ) from exc return BuiltinType(name=builtin) - def _lower_atomic(self, t: AtomicType) -> Type: + def _map_atomic(self, t: AtomicType) -> Type: dtype = self._dtype_arg(t.value_type) if dtype is not None: return ParametricType( @@ -242,9 +242,9 @@ def _named(self, name: str) -> NamedType: return NamedType(name=mojo_ident(name.strip())) # ------------------------ - # Pointer lowering + # Pointer mapping # ------------------------ - def _lower_pointer(self, t: Pointer) -> Pointer: + def _map_pointer(self, t: Pointer) -> Pointer: pointee, mutability = _unwrap_pointer_pointee(t.pointee) if pointee is None or isinstance(pointee, VoidType): return Pointer( @@ -263,7 +263,7 @@ def _lower_pointer(self, t: Pointer) -> Pointer: # ------------------------ # Arrays # ------------------------ - def _lower_array(self, t: Array) -> Type: + def _map_array(self, t: Array) -> Type: if t.array_kind == "fixed" and t.size is not None: return Array(element=self.run(t.element), size=t.size, array_kind="fixed") @@ -280,7 +280,7 @@ def _lower_array(self, t: Array) -> Type: # ------------------------ # Function pointers # ------------------------ - def _lower_function_ptr(self, t: FunctionPtr) -> FunctionPtr: + def _map_function_ptr(self, t: FunctionPtr) -> FunctionPtr: param_names = t.param_names or [] params = [ Param( @@ -301,7 +301,7 @@ def _lower_function_ptr(self, t: FunctionPtr) -> FunctionPtr: # Vector & complex # ------------------------ - def _lower_vector(self, t: VectorType) -> Type: + def _map_vector(self, t: VectorType) -> Type: if t.count is None: return self._opaque_bytes(t.size_bytes) dtype = self._dtype_arg(t.element) @@ -312,7 +312,7 @@ def _lower_vector(self, t: VectorType) -> Type: ) return Array(element=self.run(t.element), size=t.count, array_kind="fixed") - def _lower_complex(self, t: ComplexType) -> Type: + def _map_complex(self, t: ComplexType) -> Type: dtype = self._dtype_arg(t.element) if dtype is not None: return ParametricType( @@ -362,13 +362,13 @@ def _scalar_for_dtype(self, t: Type) -> PrimitiveCIRType | None: # ------------------------ -def lower_type(t: Type) -> Type: - """Lower a CIR type to MojoIR.""" - return LowerTypePass().run(t) +def map_type(t: Type) -> Type: + """Map a CIR type to MojoIR.""" + return MapTypePass().run(t) __all__ = [ - "LowerTypePass", - "TypeLoweringError", - "lower_type", + "MapTypePass", + "TypeMappingError", + "map_type", ] diff --git a/mojo_bindgen/analysis/mojo/union_lowering.py b/mojo_bindgen/analysis/mojo/union_mapping.py similarity index 72% rename from mojo_bindgen/analysis/mojo/union_lowering.py rename to mojo_bindgen/analysis/mojo/union_mapping.py index d999649..fb9557c 100644 --- a/mojo_bindgen/analysis/mojo/union_lowering.py +++ b/mojo_bindgen/analysis/mojo/union_mapping.py @@ -1,18 +1,18 @@ -"""Lower CIR unions into MojoIR union layout aliases.""" +"""Map CIR unions into MojoIR union layout aliases.""" from __future__ import annotations import json from dataclasses import dataclass -from mojo_bindgen.analysis.mojo.lowering_support import ( +from mojo_bindgen.analysis.mojo.mapping_support import ( field_display_name, record_name, stub_note, - try_lower_type, + try_map_type, union_note, ) -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( AliasDecl, AliasKind, @@ -28,8 +28,8 @@ ) -class UnionLoweringError(ValueError): - """Raised when a CIR union declaration cannot be lowered to MojoIR.""" +class UnionMappingError(ValueError): + """Raised when a CIR union declaration cannot be mapped to MojoIR.""" @dataclass(frozen=True) @@ -40,14 +40,14 @@ class _UnionArmPlan: @dataclass -class LowerUnionPass: - """Lower top-level CIR union declarations to MojoIR alias declarations.""" +class MapUnionPass: + """Map top-level CIR union declarations to MojoIR alias declarations.""" - type_lowerer: LowerTypePass + type_mapper: MapTypePass def run(self, decl: Struct) -> AliasDecl: if not decl.is_union: - raise UnionLoweringError( + raise UnionMappingError( f"expected union Struct declaration, got non-union {decl.decl_id!r}" ) @@ -68,35 +68,35 @@ def _collect_union_arms(self, decl: Struct, *, alias_name: str) -> _UnionArmPlan for index, field in enumerate(decl.fields): field_name = field_display_name(field, index) - lowered, reason = try_lower_type( - self.type_lowerer, + mapped, reason = try_map_type( + self.type_mapper, field.type, subject=f"union member `{field_name}`", failure_suffix="using byte-storage fallback", ) - if reason is not None or lowered is None: + if reason is not None or mapped is None: eligible = False if reason is not None: diagnostics.append(union_note(reason)) continue - if isinstance(lowered, NamedType) and lowered.name == alias_name: + if isinstance(mapped, NamedType) and mapped.name == alias_name: eligible = False diagnostics.append( union_note( - f"union member `{field_name}` lowered to self-referential type `{alias_name}`; using byte-storage fallback" + f"union member `{field_name}` mapped to self-referential type `{alias_name}`; using byte-storage fallback" ) ) continue - key = self._type_key(lowered) + key = self._type_key(mapped) prior_name = seen_keys.get(key) if prior_name is not None: diagnostics.append(_duplicate_member_note(field_name, prior_name)) continue seen_keys[key] = field_name - arms.append(lowered) + arms.append(mapped) return _UnionArmPlan(arms=tuple(arms), diagnostics=tuple(diagnostics), eligible=eligible) @@ -109,7 +109,7 @@ def _incomplete_union_alias(decl: Struct) -> AliasDecl: return AliasDecl( name=record_name(decl), kind=AliasKind.UNION_LAYOUT, - diagnostics=[stub_note("incomplete union placeholder emitted; layout not lowered")], + diagnostics=[stub_note("incomplete union placeholder emitted; layout not mapped")], doc=decl.doc, ) @@ -139,7 +139,7 @@ def _byte_storage_union_alias(decl: Struct, *, alias_name: str, plan: _UnionArmP diagnostics=[ *plan.diagnostics, union_note( - f"union `{decl.c_name}` lowered as InlineArray[UInt8, {decl.size_bytes}] to preserve layout" + f"union `{decl.c_name}` mapped as InlineArray[UInt8, {decl.size_bytes}] to preserve layout" ), ], doc=decl.doc, @@ -158,18 +158,18 @@ def _size_diagnostics(decl: Struct) -> list: def _duplicate_member_note(field_name: str, prior_name: str): return union_note( - f"union member `{field_name}` duplicates lowered type of earlier member `{prior_name}`;" + f"union member `{field_name}` duplicates mapped type of earlier member `{prior_name}`;" ) -def lower_union(decl: Struct, *, type_lowerer: LowerTypePass | None = None) -> AliasDecl: - """Lower one top-level union declaration to a MojoIR alias.""" +def map_union(decl: Struct, *, type_mapper: MapTypePass | None = None) -> AliasDecl: + """Map one top-level union declaration to a MojoIR alias.""" - return LowerUnionPass(type_lowerer=type_lowerer or LowerTypePass()).run(decl) + return MapUnionPass(type_mapper=type_mapper or MapTypePass()).run(decl) __all__ = [ - "LowerUnionPass", - "UnionLoweringError", - "lower_union", + "MapUnionPass", + "UnionMappingError", + "map_union", ] diff --git a/mojo_bindgen/analysis/mojo/unit_lowering.py b/mojo_bindgen/analysis/mojo/unit_mapping.py similarity index 54% rename from mojo_bindgen/analysis/mojo/unit_lowering.py rename to mojo_bindgen/analysis/mojo/unit_mapping.py index 90f3c4a..04707fa 100644 --- a/mojo_bindgen/analysis/mojo/unit_lowering.py +++ b/mojo_bindgen/analysis/mojo/unit_mapping.py @@ -1,27 +1,27 @@ -"""High-level lowering entrypoint from CIR ``Unit`` to MojoIR ``MojoModule``. +"""High-level mapping entrypoint from CIR ``Unit`` to MojoIR ``MojoModule``. This module is intentionally thin. It mirrors the parser's orchestration shape: one pass owns the overall walk over top-level declarations and delegates -declaration-family lowering to narrower helpers. +declaration-family mapping to narrower helpers. """ from __future__ import annotations from mojo_bindgen.analysis.facts.context import AnalysisContext, build_analysis_context -from mojo_bindgen.analysis.mojo.alias_lowering import lower_typedef_alias -from mojo_bindgen.analysis.mojo.const_lowering import ( - LowerConstExprPass, +from mojo_bindgen.analysis.mojo.alias_mapping import map_typedef_alias +from mojo_bindgen.analysis.mojo.const_expr_mapping import ( + MapConstExprPass, ) -from mojo_bindgen.analysis.mojo.decl_lowerer import ( - LoweringSession, - UnitDeclLowerer, +from mojo_bindgen.analysis.mojo.decl_mapping import ( + MappingSession, + UnitDeclMapper, _link_mode_for_options, ) from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions -from mojo_bindgen.analysis.mojo.struct_lowering import ( - StructLoweringContext, +from mojo_bindgen.analysis.mojo.struct_mapping import ( + StructMappingContext, ) -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( MojoDecl, MojoModule, @@ -29,8 +29,8 @@ ) -class LowerUnitPass: - """Lower an already-normalized CIR ``Unit`` into a MojoIR ``MojoModule``.""" +class MapUnitPass: + """Map an already-normalized CIR ``Unit`` into a MojoIR ``MojoModule``.""" def __init__( self, @@ -45,69 +45,69 @@ def run(self, unit: Unit) -> MojoModule: context = self._context or build_analysis_context(unit) if context.unit is not unit: context = build_analysis_context(unit) - type_lowerer = LowerTypePass() - session = LoweringSession( + type_mapper = MapTypePass() + session = MappingSession( unit=unit, options=self._options, - type_lowerer=type_lowerer, - const_lowerer=LowerConstExprPass(type_lowering=type_lowerer), - struct_context=StructLoweringContext( + type_mapper=type_mapper, + const_expr_mapper=MapConstExprPass(type_mapping=type_mapper), + struct_context=StructMappingContext( record_map=context.records_by_decl_id, record_layouts=context.record_layouts, record_shapes=context.record_shapes, target_abi=unit.target_abi, - type_lowerer=type_lowerer, + type_mapper=type_mapper, ), ) - decl_lowerer = UnitDeclLowerer(session) - lowered_decls = self._synth_external_typedef_aliases(context, type_lowerer) + decl_mapping = UnitDeclMapper(session) + mapped_decls = self._synth_external_typedef_aliases(context, type_mapper) for decl in unit.decls: - lowered = decl_lowerer.lower_decl(decl) - if lowered is None: + mapped = decl_mapping.map_decl(decl) + if mapped is None: continue - if isinstance(lowered, list): - lowered_decls.extend(lowered) + if isinstance(mapped, list): + mapped_decls.extend(mapped) else: - lowered_decls.append(lowered) + mapped_decls.append(mapped) return MojoModule( source_header=unit.source_header, library=unit.library, link_name=unit.link_name, link_mode=_link_mode_for_options(self._options), library_path_hint=self._options.library_path_hint, - decls=lowered_decls, + decls=mapped_decls, ) def _synth_external_typedef_aliases( self, context: AnalysisContext, - type_lowerer: LowerTypePass, + type_mapper: MapTypePass, ) -> list[MojoDecl]: - lowered: list[MojoDecl] = [] + mapped: list[MojoDecl] = [] for ref in context.alias_classification.external_type_refs_by_decl_id.values(): - alias = lower_typedef_alias( + alias = map_typedef_alias( c_name=ref.name, aliased=ref.canonical, - type_lowerer=type_lowerer, + type_mapper=type_mapper, ) if alias is None: continue - lowered.append(alias) - return lowered + mapped.append(alias) + return mapped -def lower_unit( +def map_unit( unit: Unit, *, options: MojoEmitOptions | None = None, context: AnalysisContext | None = None, ) -> MojoModule: - """Lower one CIR ``Unit`` into a standalone ``MojoModule``.""" + """Map one CIR ``Unit`` into a standalone ``MojoModule``.""" - return LowerUnitPass(options=options, context=context).run(unit) + return MapUnitPass(options=options, context=context).run(unit) __all__ = [ - "LowerUnitPass", - "lower_unit", + "MapUnitPass", + "map_unit", ] diff --git a/mojo_bindgen/analysis/pipeline.py b/mojo_bindgen/analysis/pipeline.py index e472a2b..9480ea3 100644 --- a/mojo_bindgen/analysis/pipeline.py +++ b/mojo_bindgen/analysis/pipeline.py @@ -11,7 +11,7 @@ from mojo_bindgen.analysis.facts.context import AnalysisContext, build_analysis_context from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.analysis.mojo.record_policies import assign_record_policies -from mojo_bindgen.analysis.mojo.unit_lowering import lower_unit +from mojo_bindgen.analysis.mojo.unit_mapping import map_unit from mojo_bindgen.codegen.normalize_mojo_module import normalize_mojo_module from mojo_bindgen.ir import MojoModule, Unit @@ -45,7 +45,7 @@ def options(self) -> MojoEmitOptions: return self._options def normalize_cir(self, unit: Unit) -> Unit: - """Run the ordered, pure CIR repair sequence before MojoIR lowering.""" + """Run the ordered, pure CIR repair sequence before MojoIR mapping.""" validated = ValidateIRPass().run(unit) with_signature_stubs = SignatureRecordStubPass().run(validated).unit normalized = CIRCanonicalizer().canonicalize(with_signature_stubs) @@ -56,18 +56,18 @@ def run_ir_passes(self, unit: Unit) -> Unit: """Compatibility alias for the CIR normalization pass sequence.""" return self.normalize_cir(unit) - def lower_normalized( + def map_normalized( self, unit: Unit, *, context: AnalysisContext | None = None, ) -> MojoModule: - """Lower already-normalized CIR into policy-free MojoIR.""" - return lower_unit(unit, options=self._options, context=context) + """Map already-normalized CIR into policy-free MojoIR.""" + return map_unit(unit, options=self._options, context=context) - def lower(self, unit: Unit) -> MojoModule: - """Lower validated CIR into policy-free MojoIR.""" - return self.lower_normalized(self.normalize_cir(unit)) + def map(self, unit: Unit) -> MojoModule: + """Map validated CIR into policy-free MojoIR.""" + return self.map_normalized(self.normalize_cir(unit)) def finalize(self, module: MojoModule) -> MojoModule: """Apply late record policy decisions and final normalization.""" @@ -93,19 +93,19 @@ def analyze_pipeline(self, unit: Unit) -> AnalysisArtifacts: ) normalized_unit = ValidateReferencesPass().run(normalized_unit) context = build_analysis_context(normalized_unit) - lowered = self.lower_normalized(normalized_unit, context=context) + mapped = self.map_normalized(normalized_unit, context=context) return AnalysisArtifacts( raw_unit=unit, validated_unit=validated_unit, normalized_unit=normalized_unit, context=context, - policy_light_module=lowered, - mojo_module=self.finalize(lowered), + policy_light_module=mapped, + mojo_module=self.finalize(mapped), ) def run_ir_passes(unit: Unit) -> Unit: - """Run the analysis-owned CIR pass sequence before MojoIR lowering.""" + """Run the analysis-owned CIR pass sequence before MojoIR mapping.""" return AnalysisOrchestrator().run_ir_passes(unit) diff --git a/mojo_bindgen/codegen/mojo_ir_printer.py b/mojo_bindgen/codegen/mojo_ir_printer.py index 41d8b48..9e3c4f8 100644 --- a/mojo_bindgen/codegen/mojo_ir_printer.py +++ b/mojo_bindgen/codegen/mojo_ir_printer.py @@ -31,7 +31,7 @@ Initializer, IntLiteral, LinkMode, - LoweringNote, + MappingNote, ModuleImport, MojoBuiltin, MojoDecl, @@ -797,7 +797,7 @@ def _render_const_expr(self, expr: ConstExpr) -> str: raise MojoIRPrintError(f"unsupported ConstExpr node: {type(expr).__name__!r}") @staticmethod - def _diagnostic_lines(notes: Iterable[LoweringNote]) -> list[str]: + def _diagnostic_lines(notes: Iterable[MappingNote]) -> list[str]: return [ f"# {note.severity.value.upper()}[{note.category}]: {note.message}" for note in notes ] diff --git a/mojo_bindgen/ir.py b/mojo_bindgen/ir.py index e2f7548..a4d6b24 100644 --- a/mojo_bindgen/ir.py +++ b/mojo_bindgen/ir.py @@ -102,10 +102,10 @@ class FloatKind(StrEnum): ArrayKind = Literal["fixed", "incomplete", "flexible", "variable"] -# Array-shape categories that matter for ABI-faithful lowering. +# Array-shape categories that matter for ABI-faithful mapping. FamPattern = Literal["c99_empty", "gnu_zero"] -# Flexible-tail field patterns recognized by parser record lowering. +# Flexible-tail field patterns recognized by parser record mapping. MacroDeclKind = Literal[ @@ -328,7 +328,7 @@ class UnsupportedType(SerDeMixin): """A type that the parser recognized but cannot model faithfully yet. Unlike :class:`OpaqueRecordRef`, this means the source type is known but - unsupported for precise lowering. The category and reason fields make the + unsupported for precise mapping. The category and reason fields make the fallback explicit for later diagnostics and renderer policy. """ @@ -377,7 +377,7 @@ class StructRef(SerDeMixin): layouts. ``name`` and ``c_name`` are usually the same C tag; anonymous record bodies use a stable parent-scoped synthetic name from the parser. - Unions carry ``is_union=True`` and ``size_bytes`` so the emitter can lower + Unions carry ``is_union=True`` and ``size_bytes`` so the emitter can map by-value unions to ``InlineArray[UInt8, size]`` without a separate lookup. """ @@ -418,7 +418,7 @@ class TypeRef(SerDeMixin): A named reference to a C typedef where the typedef name appears in a type position (parameter, field, pointer target, etc.). - ``canonical`` is the fully resolved :class:`Type` for ABI lowering; the + ``canonical`` is the fully resolved :class:`Type` for ABI mapping; the typedef ``name`` preserves the C API spelling for readable emission. """ @@ -612,11 +612,11 @@ class Enumerant(SerDeMixin): @dataclass class Enum(SerDeMixin): """ - C enum lowered to a scalar type alias plus typed enumerator constants. + C enum mapped to a scalar type alias plus typed enumerator constants. ``name`` is the primary emitted alias chosen by CIR canonicalization. ``alias_names`` are additional collision-free names that should alias the - primary name at Mojo lowering time. + primary name at Mojo mapping time. ``underlying`` is always IntType (C enum base type is integer). """ @@ -645,7 +645,7 @@ class Typedef(SerDeMixin): ``aliased`` is the direct underlying type (one typedef step), often a :class:`TypeRef` when the underlying names another typedef. - ``canonical`` is the fully unrolled type for ABI layout and for lowering + ``canonical`` is the fully unrolled type for ABI layout and for mapping inside compound positions (struct fields, function pointer signatures). """ @@ -733,7 +733,7 @@ class Const(SerDeMixin): class MacroDecl(SerDeMixin): """Source-backed preprocessor macro preserved from the parsed translation unit. - Macros are preserved even when their replacement list cannot be lowered to + Macros are preserved even when their replacement list cannot be mapped to the supported :class:`ConstExpr` subset. ``tokens`` keeps the original replacement spelling, while ``expr`` and ``type`` are populated only for macros the parser can structurally understand today. @@ -1021,15 +1021,15 @@ class LinkMode(StrEnum): OWNED_DL_HANDLE = "owned_dl_handle" -class LoweringSeverity(StrEnum): +class MappingSeverity(StrEnum): NOTE = "note" WARNING = "warning" ERROR = "error" @dataclass(frozen=True) -class LoweringNote(SerDeMixin): - severity: LoweringSeverity +class MappingNote(SerDeMixin): + severity: MappingSeverity message: str category: str @@ -1268,7 +1268,7 @@ class StructDecl(SerDeMixin): comptime_members: list[ComptimeMember] = field(default_factory=list) initializers: list[Initializer] = field(default_factory=list) flexible_tail: FlexibleTail | None = None - diagnostics: list[LoweringNote] = field(default_factory=list) + diagnostics: list[MappingNote] = field(default_factory=list) doc: DocComment | None = None @@ -1286,7 +1286,7 @@ class AliasDecl(SerDeMixin): type_value: Type | None = None const_type: Type | None = None const_value: ConstExpr | None = None - diagnostics: list[LoweringNote] = field(default_factory=list) + diagnostics: list[MappingNote] = field(default_factory=list) doc: DocComment | None = None def has_payload(self) -> bool: @@ -1322,7 +1322,7 @@ class FunctionDecl(SerDeMixin): call_target: CallTarget = field( default_factory=lambda: CallTarget(link_mode=LinkMode.EXTERNAL_CALL, symbol="") ) - diagnostics: list[LoweringNote] = field(default_factory=list) + diagnostics: list[MappingNote] = field(default_factory=list) doc: DocComment | None = None @@ -1340,7 +1340,7 @@ class GlobalDecl(SerDeMixin): value_type: Type is_const: bool = False kind: GlobalKind = GlobalKind.WRAPPER - diagnostics: list[LoweringNote] = field(default_factory=list) + diagnostics: list[MappingNote] = field(default_factory=list) doc: DocComment | None = None @@ -1504,8 +1504,8 @@ def mojo_decl_from_json(d: dict[str, object]) -> MojoDecl: "IntLiteral", "IntType", "LinkMode", - "LoweringNote", - "LoweringSeverity", + "MappingNote", + "MappingSeverity", "MacroDecl", "MacroDeclKind", "ModuleDependencies", diff --git a/mojo_bindgen/layout_tests/generator.py b/mojo_bindgen/layout_tests/generator.py index 77c4204..07510a2 100644 --- a/mojo_bindgen/layout_tests/generator.py +++ b/mojo_bindgen/layout_tests/generator.py @@ -5,7 +5,7 @@ from dataclasses import dataclass from mojo_bindgen.analysis.facts.record_layout import analyze_record_layout -from mojo_bindgen.analysis.mojo.lowering_support import ( +from mojo_bindgen.analysis.mojo.mapping_support import ( field_mojo_name, record_by_decl_id, record_name, diff --git a/tests/surface/alignment/fixtures/packed_struct_comment/expect.portable.external.mojo b/tests/surface/alignment/fixtures/packed_struct_comment/expect.portable.external.mojo index f354952..3c28afb 100644 --- a/tests/surface/alignment/fixtures/packed_struct_comment/expect.portable.external.mojo +++ b/tests/surface/alignment/fixtures/packed_struct_comment/expect.portable.external.mojo @@ -9,7 +9,7 @@ comptime uint8_t = UInt8 comptime uint32_t = UInt32 -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct psc_header(Copyable, Movable): var storage: InlineArray[UInt8, 5] diff --git a/tests/surface/alignment/fixtures/packed_struct_comment/expect.strict.external.mojo b/tests/surface/alignment/fixtures/packed_struct_comment/expect.strict.external.mojo index f354952..3c28afb 100644 --- a/tests/surface/alignment/fixtures/packed_struct_comment/expect.strict.external.mojo +++ b/tests/surface/alignment/fixtures/packed_struct_comment/expect.strict.external.mojo @@ -9,7 +9,7 @@ comptime uint8_t = UInt8 comptime uint32_t = UInt32 -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct psc_header(Copyable, Movable): var storage: InlineArray[UInt8, 5] diff --git a/tests/surface/alignment/fixtures/policy_matrix/expect.portable.external.mojo b/tests/surface/alignment/fixtures/policy_matrix/expect.portable.external.mojo index c4211a7..f313849 100644 --- a/tests/surface/alignment/fixtures/policy_matrix/expect.portable.external.mojo +++ b/tests/surface/alignment/fixtures/policy_matrix/expect.portable.external.mojo @@ -45,23 +45,23 @@ struct ag_field_align(TrivialRegisterPassable): var __pad0_1: UInt64 var value: c_int -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct ag_packed(Copyable, Movable): var storage: InlineArray[UInt8, 5] -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted @align(16) struct ag_packed_aligned(Copyable, Movable): var storage: InlineArray[UInt8, 16] -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct ag_pragma_packed(Copyable, Movable): var storage: InlineArray[UInt8, 5] -# NOTE[struct_lowering]: member at byte offset 2 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 2 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 2 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 2 is smaller than the natural typed Mojo alignment 4; opaque storage emitted @align(2) struct ag_pragma_pack2(Copyable, Movable): var storage: InlineArray[UInt8, 6] diff --git a/tests/surface/alignment/fixtures/policy_matrix/expect.strict.external.mojo b/tests/surface/alignment/fixtures/policy_matrix/expect.strict.external.mojo index c4211a7..f313849 100644 --- a/tests/surface/alignment/fixtures/policy_matrix/expect.strict.external.mojo +++ b/tests/surface/alignment/fixtures/policy_matrix/expect.strict.external.mojo @@ -45,23 +45,23 @@ struct ag_field_align(TrivialRegisterPassable): var __pad0_1: UInt64 var value: c_int -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct ag_packed(Copyable, Movable): var storage: InlineArray[UInt8, 5] -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted @align(16) struct ag_packed_aligned(Copyable, Movable): var storage: InlineArray[UInt8, 16] -# NOTE[struct_lowering]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 1 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 1 is smaller than the natural typed Mojo alignment 4; opaque storage emitted struct ag_pragma_packed(Copyable, Movable): var storage: InlineArray[UInt8, 5] -# NOTE[struct_lowering]: member at byte offset 2 is before the natural typed offset 4; opaque storage emitted -# NOTE[struct_lowering]: C base alignment 2 is smaller than the natural typed Mojo alignment 4; opaque storage emitted +# NOTE[struct_mapping]: member at byte offset 2 is before the natural typed offset 4; opaque storage emitted +# NOTE[struct_mapping]: C base alignment 2 is smaller than the natural typed Mojo alignment 4; opaque storage emitted @align(2) struct ag_pragma_pack2(Copyable, Movable): var storage: InlineArray[UInt8, 6] diff --git a/tests/unit/passes/test_pipeline.py b/tests/unit/passes/test_pipeline.py index a678f7d..0e846fe 100644 --- a/tests/unit/passes/test_pipeline.py +++ b/tests/unit/passes/test_pipeline.py @@ -2,7 +2,7 @@ import pytest -from mojo_bindgen.analysis import AnalysisOrchestrator, lower_unit, run_ir_passes +from mojo_bindgen.analysis import AnalysisOrchestrator, map_unit, run_ir_passes from mojo_bindgen.analysis.cir.validate_ir import IRValidationError, ValidateIRPass from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( @@ -223,7 +223,7 @@ def test_analysis_pipeline_exposes_context_and_policy_light_module() -> None: assert artifacts.mojo_module.decls -def test_run_ir_passes_and_lower_unit_preserve_typedef_surface_name() -> None: +def test_run_ir_passes_and_map_unit_preserve_typedef_surface_name() -> None: i32 = _i32() td = Typedef(decl_id="typedef:my_int", name="my_int", aliased=i32, canonical=i32) tr = TypeRef(decl_id=td.decl_id, name=td.name, canonical=i32) @@ -237,9 +237,9 @@ def test_run_ir_passes_and_lower_unit_preserve_typedef_surface_name() -> None: unit = Unit(source_header="t.h", library="t", link_name="t", target_abi=_abi(), decls=[td, fn]) normalized = run_ir_passes(unit) - lowered = lower_unit(normalized, options=MojoEmitOptions()) - typedef_decl = lowered.decls[0] - fn_decl = lowered.decls[1] + mapped = map_unit(normalized, options=MojoEmitOptions()) + typedef_decl = mapped.decls[0] + fn_decl = mapped.decls[1] assert typedef_decl == AliasDecl( name="my_int", @@ -303,18 +303,18 @@ def test_run_ir_passes_prefers_typedef_name_for_named_enum_and_emits_tag_alias() assert enum_decl.name == "typedef_name" assert enum_decl.alias_names == ["tag_name"] - lowered = lower_unit(normalized, options=MojoEmitOptions()) - assert lowered.decls[0] == AliasDecl( + mapped = map_unit(normalized, options=MojoEmitOptions()) + assert mapped.decls[0] == AliasDecl( name="typedef_name", kind=AliasKind.TYPE_ALIAS, type_value=BuiltinType(MojoBuiltin.C_UINT), ) - assert lowered.decls[1] == AliasDecl( + assert mapped.decls[1] == AliasDecl( name="tag_name", kind=AliasKind.TYPE_ALIAS, type_value=NamedType("typedef_name"), ) - assert lowered.decls[2] == AliasDecl( + assert mapped.decls[2] == AliasDecl( name="TAG_A", kind=AliasKind.CONST_VALUE, const_type=NamedType("typedef_name"), @@ -328,8 +328,8 @@ def test_run_ir_passes_prefers_typedef_name_for_named_enum_and_emits_tag_alias() ], ), ) - assert isinstance(lowered.decls[3], FunctionDecl) - assert lowered.decls[3].params[0].type == NamedType("typedef_name") + assert isinstance(mapped.decls[3], FunctionDecl) + assert mapped.decls[3].params[0].type == NamedType("typedef_name") def test_run_ir_passes_uses_tag_name_for_tag_only_enum() -> None: @@ -369,13 +369,13 @@ def test_run_ir_passes_uses_tag_name_for_tag_only_enum() -> None: assert enum_decl.name == "mode_tag" assert enum_decl.alias_names == [] - lowered = lower_unit(normalized, options=MojoEmitOptions()) - assert lowered.decls[0] == AliasDecl( + mapped = map_unit(normalized, options=MojoEmitOptions()) + assert mapped.decls[0] == AliasDecl( name="mode_tag", kind=AliasKind.TYPE_ALIAS, type_value=BuiltinType(MojoBuiltin.C_UINT), ) - assert lowered.decls[1] == AliasDecl( + assert mapped.decls[1] == AliasDecl( name="MODE_A", kind=AliasKind.CONST_VALUE, const_type=NamedType("mode_tag"), @@ -389,8 +389,8 @@ def test_run_ir_passes_uses_tag_name_for_tag_only_enum() -> None: ], ), ) - assert isinstance(lowered.decls[2], FunctionDecl) - assert lowered.decls[2].return_type == NamedType("mode_tag") + assert isinstance(mapped.decls[2], FunctionDecl) + assert mapped.decls[2].return_type == NamedType("mode_tag") def test_run_ir_passes_drops_colliding_tag_alias_for_typedef_enum() -> None: @@ -429,8 +429,8 @@ def test_run_ir_passes_drops_colliding_tag_alias_for_typedef_enum() -> None: assert enum_decl.name == "mode_t" assert enum_decl.alias_names == [] - lowered = lower_unit(normalized, options=MojoEmitOptions()) - assert lowered.decls == [ + mapped = map_unit(normalized, options=MojoEmitOptions()) + assert mapped.decls == [ AliasDecl( name="mode_t", kind=AliasKind.TYPE_ALIAS, diff --git a/tests/unit/test_analysis_const_lowering.py b/tests/unit/test_analysis_const_lowering.py deleted file mode 100644 index cca6895..0000000 --- a/tests/unit/test_analysis_const_lowering.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Unit tests for CIR -> MojoIR constant-expression lowering.""" - -from __future__ import annotations - -from mojo_bindgen.analysis.mojo.const_lowering import lower_const_expr -from mojo_bindgen.ir import ( - BinaryExpr, - BuiltinType, - CastExpr, - FloatLiteral, - IntKind, - IntLiteral, - IntType, - MojoBuiltin, - RefExpr, - SizeOfExpr, -) - - -def test_lower_const_expr_maps_binary_ref_cast_and_sizeof_nodes() -> None: - c_int = IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4) - - assert lower_const_expr(BinaryExpr(op="+", lhs=IntLiteral(1), rhs=IntLiteral(2))) == ( - BinaryExpr(op="+", lhs=IntLiteral(1), rhs=IntLiteral(2)) - ) - assert lower_const_expr(RefExpr(name="VALUE")) == RefExpr(name="VALUE") - assert lower_const_expr(CastExpr(target=c_int, expr=IntLiteral(7))) == CastExpr( - target=BuiltinType(MojoBuiltin.C_INT), - expr=IntLiteral(7), - ) - assert lower_const_expr(SizeOfExpr(target=c_int)) == SizeOfExpr( - target=BuiltinType(MojoBuiltin.C_INT) - ) - - -def test_lower_const_expr_parses_decimal_and_hex_float_literals() -> None: - assert lower_const_expr(FloatLiteral("1.25f")) == FloatLiteral(1.25) - assert lower_const_expr(FloatLiteral("0x1.0p4")) == FloatLiteral(16.0) diff --git a/tests/unit/test_analysis_const_mapping.py b/tests/unit/test_analysis_const_mapping.py new file mode 100644 index 0000000..f51a38c --- /dev/null +++ b/tests/unit/test_analysis_const_mapping.py @@ -0,0 +1,38 @@ +"""Unit tests for CIR -> MojoIR constant-expression mapping.""" + +from __future__ import annotations + +from mojo_bindgen.analysis.mojo.const_expr_mapping import map_const_expr +from mojo_bindgen.ir import ( + BinaryExpr, + BuiltinType, + CastExpr, + FloatLiteral, + IntKind, + IntLiteral, + IntType, + MojoBuiltin, + RefExpr, + SizeOfExpr, +) + + +def test_map_const_expr_maps_binary_ref_cast_and_sizeof_nodes() -> None: + c_int = IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4) + + assert map_const_expr(BinaryExpr(op="+", lhs=IntLiteral(1), rhs=IntLiteral(2))) == ( + BinaryExpr(op="+", lhs=IntLiteral(1), rhs=IntLiteral(2)) + ) + assert map_const_expr(RefExpr(name="VALUE")) == RefExpr(name="VALUE") + assert map_const_expr(CastExpr(target=c_int, expr=IntLiteral(7))) == CastExpr( + target=BuiltinType(MojoBuiltin.C_INT), + expr=IntLiteral(7), + ) + assert map_const_expr(SizeOfExpr(target=c_int)) == SizeOfExpr( + target=BuiltinType(MojoBuiltin.C_INT) + ) + + +def test_map_const_expr_parses_decimal_and_hex_float_literals() -> None: + assert map_const_expr(FloatLiteral("1.25f")) == FloatLiteral(1.25) + assert map_const_expr(FloatLiteral("0x1.0p4")) == FloatLiteral(16.0) diff --git a/tests/unit/test_analysis_record_policies.py b/tests/unit/test_analysis_record_policies.py index 4878b35..47ffbc8 100644 --- a/tests/unit/test_analysis_record_policies.py +++ b/tests/unit/test_analysis_record_policies.py @@ -1,6 +1,6 @@ from __future__ import annotations -from mojo_bindgen.analysis import assign_record_policies, lower_unit +from mojo_bindgen.analysis import assign_record_policies, map_unit from mojo_bindgen.ir import ( Array, AtomicType, @@ -73,7 +73,7 @@ def _field( def _lowered_structs(*decls: Struct) -> list[StructDecl]: - lowered = lower_unit( + lowered = map_unit( Unit( source_header="demo.h", library="demo", diff --git a/tests/unit/test_analysis_struct_lowering.py b/tests/unit/test_analysis_struct_mapping.py similarity index 76% rename from tests/unit/test_analysis_struct_lowering.py rename to tests/unit/test_analysis_struct_mapping.py index e75df0c..5963e28 100644 --- a/tests/unit/test_analysis_struct_lowering.py +++ b/tests/unit/test_analysis_struct_mapping.py @@ -1,10 +1,10 @@ -"""Unit tests for CIR struct -> MojoIR struct lowering.""" +"""Unit tests for CIR struct -> MojoIR struct mapping.""" from __future__ import annotations from mojo_bindgen.analysis.facts.record_layout import PlainFieldFact, RecordLayoutFacts -from mojo_bindgen.analysis.mojo.struct_lowering import StructLoweringContext, lower_struct -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass +from mojo_bindgen.analysis.mojo.struct_mapping import StructMappingContext, map_struct +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass from mojo_bindgen.ir import ( Array, AtomicType, @@ -77,16 +77,16 @@ def _field( ) -def _context_for(*decls: Struct) -> StructLoweringContext: +def _context_for(*decls: Struct) -> StructMappingContext: record_map = {decl.decl_id: decl for decl in decls} - return StructLoweringContext( + return StructMappingContext( record_map=record_map, target_abi=_abi(), - type_lowerer=LowerTypePass(), + type_mapper=MapTypePass(), ) -def test_lower_struct_consumes_cached_record_layout_facts(monkeypatch) -> None: +def test_map_struct_consumes_cached_record_layout_facts(monkeypatch) -> None: decl = Struct( decl_id="struct:Widget", name="Widget", @@ -98,13 +98,13 @@ def test_lower_struct_consumes_cached_record_layout_facts(monkeypatch) -> None: ) def fail_if_recomputed(*args, **kwargs): - raise AssertionError("record layout should come from StructLoweringContext") + raise AssertionError("record layout should come from StructMappingContext") monkeypatch.setattr( - "mojo_bindgen.analysis.mojo.struct_lowering.analyze_record_layout", + "mojo_bindgen.analysis.mojo.struct_mapping.analyze_record_layout", fail_if_recomputed, ) - context = StructLoweringContext( + context = StructMappingContext( record_map={decl.decl_id: decl}, record_layouts={ decl.decl_id: RecordLayoutFacts( @@ -116,17 +116,17 @@ def fail_if_recomputed(*args, **kwargs): ) }, target_abi=_abi(), - type_lowerer=LowerTypePass(), + type_mapper=MapTypePass(), ) - lowered = lower_struct(decl, context=context) + mapped = map_struct(decl, context=context) - assert lowered.members == [ + assert mapped.members == [ StoredMember(index=0, name="value", type=BuiltinType(MojoBuiltin.C_INT), byte_offset=0) ] -def test_lower_struct_keeps_incomplete_records_opaque_with_explicit_align_none() -> None: +def test_map_struct_keeps_incomplete_records_opaque_with_explicit_align_none() -> None: decl = Struct( decl_id="struct:Opaque", name="Opaque", @@ -137,20 +137,20 @@ def test_lower_struct_keeps_incomplete_records_opaque_with_explicit_align_none() is_complete=False, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.name == "Opaque" - assert lowered.kind == StructKind.OPAQUE - assert lowered.align is None - assert lowered.align_decorator is None - assert lowered.fieldwise_init is False - assert lowered.traits == [] - assert lowered.members == [] - assert lowered.initializers == [] - assert lowered.diagnostics == [] + assert mapped.name == "Opaque" + assert mapped.kind == StructKind.OPAQUE + assert mapped.align is None + assert mapped.align_decorator is None + assert mapped.fieldwise_init is False + assert mapped.traits == [] + assert mapped.members == [] + assert mapped.initializers == [] + assert mapped.diagnostics == [] -def test_lower_struct_lowers_fieldwise_exact_structs_without_policies() -> None: +def test_map_struct_maps_fieldwise_exact_structs_without_policies() -> None: decl = Struct( decl_id="struct:Widget", name="Widget", @@ -164,20 +164,20 @@ def test_lower_struct_lowers_fieldwise_exact_structs_without_policies() -> None: is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.kind == StructKind.PLAIN - assert lowered.align == 4 - assert lowered.align_decorator is None - assert lowered.fieldwise_init is False - assert lowered.traits == [] - assert lowered.members == [ + assert mapped.kind == StructKind.PLAIN + assert mapped.align == 4 + assert mapped.align_decorator is None + assert mapped.fieldwise_init is False + assert mapped.traits == [] + assert mapped.members == [ StoredMember(index=0, name="size", type=BuiltinType(MojoBuiltin.C_INT), byte_offset=0), StoredMember(index=1, name="flags", type=BuiltinType(MojoBuiltin.C_UINT), byte_offset=4), ] -def test_lower_struct_synthesizes_padding_members_for_exact_layout_gaps() -> None: +def test_map_struct_synthesizes_padding_members_for_exact_layout_gaps() -> None: decl = Struct( decl_id="struct:Padded", name="Padded", @@ -196,17 +196,17 @@ def test_lower_struct_synthesizes_padding_members_for_exact_layout_gaps() -> Non is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.members == [ StoredMember(index=0, name="tag", type=BuiltinType(MojoBuiltin.C_UCHAR), byte_offset=0), PaddingMember(name="__pad0", size_bytes=4, byte_offset=4), StoredMember(index=1, name="value", type=BuiltinType(MojoBuiltin.C_INT), byte_offset=8), ] -def test_lower_struct_keeps_union_members_typed_and_preserves_padding() -> None: +def test_map_struct_keeps_union_members_typed_and_preserves_padding() -> None: union_decl = Struct( decl_id="union:Payload", name="Payload", @@ -248,17 +248,17 @@ def test_lower_struct_keeps_union_members_typed_and_preserves_padding() -> None: is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl, union_decl)) + mapped = map_struct(decl, context=_context_for(decl, union_decl)) - assert lowered.fieldwise_init is False - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.members == [ StoredMember(index=0, name="tag", type=BuiltinType(MojoBuiltin.C_UCHAR), byte_offset=0), StoredMember(index=1, name="payload", type=NamedType("Payload"), byte_offset=8), StoredMember(index=2, name="tail", type=BuiltinType(MojoBuiltin.C_UINT), byte_offset=16), ] -def test_lower_struct_uses_opaque_storage_for_embedded_incomplete_struct() -> None: +def test_map_struct_uses_opaque_storage_for_embedded_incomplete_struct() -> None: inner = Struct( decl_id="struct:Inner", name="Inner", @@ -293,12 +293,12 @@ def test_lower_struct_uses_opaque_storage_for_embedded_incomplete_struct() -> No is_complete=True, ) - lowered = lower_struct(outer, context=_context_for(outer, inner)) + mapped = map_struct(outer, context=_context_for(outer, inner)) - assert lowered.members == [OpaqueStorageMember(name="storage", size_bytes=24)] + assert mapped.members == [OpaqueStorageMember(name="storage", size_bytes=24)] -def test_lower_struct_keeps_array_of_embedded_empty_struct_typed() -> None: +def test_map_struct_keeps_array_of_embedded_empty_struct_typed() -> None: inner = Struct( decl_id="struct:Inner", name="Inner", @@ -338,16 +338,16 @@ def test_lower_struct_keeps_array_of_embedded_empty_struct_typed() -> None: is_complete=True, ) - lowered = lower_struct(outer, context=_context_for(outer, inner)) + mapped = map_struct(outer, context=_context_for(outer, inner)) - assert lowered.kind == StructKind.PLAIN - assert [member.name for member in lowered.members if isinstance(member, StoredMember)] == [ + assert mapped.kind == StructKind.PLAIN + assert [member.name for member in mapped.members if isinstance(member, StoredMember)] == [ "items", "tail", ] -def test_lower_struct_keeps_pointer_to_incomplete_struct_typed() -> None: +def test_map_struct_keeps_pointer_to_incomplete_struct_typed() -> None: inner = Struct( decl_id="struct:Inner", name="Inner", @@ -387,16 +387,16 @@ def test_lower_struct_keeps_pointer_to_incomplete_struct_typed() -> None: is_complete=True, ) - lowered = lower_struct(outer, context=_context_for(outer, inner)) + mapped = map_struct(outer, context=_context_for(outer, inner)) - assert lowered.kind == StructKind.PLAIN - assert [member.name for member in lowered.members if isinstance(member, StoredMember)] == [ + assert mapped.kind == StructKind.PLAIN + assert [member.name for member in mapped.members if isinstance(member, StoredMember)] == [ "ptr", "tail", ] -def test_lower_struct_omits_trailing_padding_explained_by_struct_alignment() -> None: +def test_map_struct_omits_trailing_padding_explained_by_struct_alignment() -> None: decl = Struct( decl_id="struct:Aligned", name="Aligned", @@ -407,14 +407,14 @@ def test_lower_struct_omits_trailing_padding_explained_by_struct_alignment() -> is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.members == [ + assert mapped.members == [ StoredMember(index=0, name="value", type=BuiltinType(MojoBuiltin.C_INT), byte_offset=0), ] -def test_lower_struct_falls_back_to_opaque_storage_for_unrepresentable_layout() -> None: +def test_map_struct_falls_back_to_opaque_storage_for_unrepresentable_layout() -> None: decl = Struct( decl_id="struct:Packed", name="Packed", @@ -434,18 +434,18 @@ def test_lower_struct_falls_back_to_opaque_storage_for_unrepresentable_layout() is_packed=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert len(lowered.members) == 1 - assert lowered.members[0].name == "storage" - assert lowered.members[0].size_bytes == 5 - assert lowered.diagnostics - assert all(note.category == "struct_lowering" for note in lowered.diagnostics) - assert "opaque storage emitted" in lowered.diagnostics[0].message + assert mapped.fieldwise_init is False + assert len(mapped.members) == 1 + assert mapped.members[0].name == "storage" + assert mapped.members[0].size_bytes == 5 + assert mapped.diagnostics + assert all(note.category == "struct_mapping" for note in mapped.diagnostics) + assert "opaque storage emitted" in mapped.diagnostics[0].message -def test_lower_struct_keeps_atomic_field_types_and_drops_traits() -> None: +def test_map_struct_keeps_atomic_field_types_and_drops_traits() -> None: decl = Struct( decl_id="struct:AtomicHolder", name="AtomicHolder", @@ -465,11 +465,11 @@ def test_lower_struct_keeps_atomic_field_types_and_drops_traits() -> None: is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.traits == [] - assert lowered.fieldwise_init is False - assert lowered.members[0] == StoredMember( + assert mapped.traits == [] + assert mapped.fieldwise_init is False + assert mapped.members[0] == StoredMember( index=0, name="counter", type=ParametricType(base=ParametricBase.ATOMIC, args=[DTypeArg("DType.int32")]), @@ -477,7 +477,7 @@ def test_lower_struct_keeps_atomic_field_types_and_drops_traits() -> None: ) -def test_lower_struct_lowers_pure_bitfield_structs_with_synthesized_initializers() -> None: +def test_map_struct_maps_pure_bitfield_structs_with_synthesized_initializers() -> None: decl = Struct( decl_id="struct:Bits", name="Bits", @@ -526,10 +526,10 @@ def test_lower_struct_lowers_pure_bitfield_structs_with_synthesized_initializers is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.members == [ BitfieldGroupMember( storage_name="__bf0", storage_type=BuiltinType(MojoBuiltin.C_UINT), @@ -574,7 +574,7 @@ def test_lower_struct_lowers_pure_bitfield_structs_with_synthesized_initializers ], ), ] - assert lowered.initializers == [ + assert mapped.initializers == [ Initializer(params=[]), Initializer( params=[ @@ -586,7 +586,7 @@ def test_lower_struct_lowers_pure_bitfield_structs_with_synthesized_initializers ] -def test_lower_struct_lowers_mixed_bitfield_structs_in_byte_offset_order() -> None: +def test_map_struct_maps_mixed_bitfield_structs_in_byte_offset_order() -> None: decl = Struct( decl_id="struct:MixedBits", name="MixedBits", @@ -617,18 +617,18 @@ def test_lower_struct_lowers_mixed_bitfield_structs_in_byte_offset_order() -> No is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert lowered.initializers == [] - assert lowered.members[0] == StoredMember( + assert mapped.fieldwise_init is False + assert mapped.initializers == [] + assert mapped.members[0] == StoredMember( index=0, name="tag", type=BuiltinType(MojoBuiltin.C_UINT), byte_offset=0, ) - assert isinstance(lowered.members[1], BitfieldGroupMember) - group = lowered.members[1] + assert isinstance(mapped.members[1], BitfieldGroupMember) + group = mapped.members[1] assert group.storage_name == "__bf0" assert group.byte_offset == 4 assert [field.name for field in group.fields] == ["signed_bits", "enabled"] @@ -636,7 +636,7 @@ def test_lower_struct_lowers_mixed_bitfield_structs_in_byte_offset_order() -> No assert group.fields[1].bool_semantics is True -def test_lower_struct_splits_bitfield_groups_on_zero_width_barriers() -> None: +def test_map_struct_splits_bitfield_groups_on_zero_width_barriers() -> None: decl = Struct( decl_id="struct:SplitBits", name="SplitBits", @@ -676,13 +676,13 @@ def test_lower_struct_splits_bitfield_groups_on_zero_width_barriers() -> None: is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert [member.storage_name for member in lowered.members] == ["__bf0", "__bf1"] - assert [member.byte_offset for member in lowered.members] == [0, 4] + assert [member.storage_name for member in mapped.members] == ["__bf0", "__bf1"] + assert [member.byte_offset for member in mapped.members] == [0, 4] -def test_lower_struct_emits_zero_init_only_for_anonymous_only_bitfield_struct() -> None: +def test_map_struct_emits_zero_init_only_for_anonymous_only_bitfield_struct() -> None: decl = Struct( decl_id="struct:AnonBits", name="AnonBits", @@ -704,14 +704,14 @@ def test_lower_struct_emits_zero_init_only_for_anonymous_only_bitfield_struct() is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert len(lowered.initializers) == 1 - assert lowered.initializers[0] == Initializer(params=[]) + assert mapped.fieldwise_init is False + assert len(mapped.initializers) == 1 + assert mapped.initializers[0] == Initializer(params=[]) -def test_lower_struct_omits_register_passable_when_field_references_incomplete_struct() -> None: +def test_map_struct_omits_register_passable_when_field_references_incomplete_struct() -> None: child = Struct( decl_id="struct:Child", name="Child", @@ -745,13 +745,13 @@ def test_lower_struct_omits_register_passable_when_field_references_incomplete_s is_complete=True, ) - lowered = lower_struct(holder, context=_context_for(holder, child)) + mapped = map_struct(holder, context=_context_for(holder, child)) - assert lowered.fieldwise_init is False - assert lowered.traits == [] + assert mapped.fieldwise_init is False + assert mapped.traits == [] -def test_lower_struct_omits_register_passable_for_recursive_struct_cycle() -> None: +def test_map_struct_omits_register_passable_for_recursive_struct_cycle() -> None: left = Struct( decl_id="struct:Left", name="Left", @@ -801,17 +801,17 @@ def test_lower_struct_omits_register_passable_for_recursive_struct_cycle() -> No ) ] - lowered = lower_struct(left, context=_context_for(left, right)) + mapped = map_struct(left, context=_context_for(left, right)) - assert lowered.fieldwise_init is False - assert lowered.traits == [] + assert mapped.fieldwise_init is False + assert mapped.traits == [] -def test_lower_struct_falls_back_only_after_mojo_type_lowering_failure() -> None: +def test_map_struct_falls_back_only_after_mojo_type_mapping_failure() -> None: decl = Struct( - decl_id="struct:LoweringFallback", - name="LoweringFallback", - c_name="LoweringFallback", + decl_id="struct:MappingFallback", + name="MappingFallback", + c_name="MappingFallback", fields=[ _field( name="mystery", @@ -831,11 +831,11 @@ def test_lower_struct_falls_back_only_after_mojo_type_lowering_failure() -> None is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert lowered.diagnostics == [] - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.diagnostics == [] + assert mapped.members == [ StoredMember( index=0, name="mystery", @@ -845,7 +845,7 @@ def test_lower_struct_falls_back_only_after_mojo_type_lowering_failure() -> None ] -def test_lower_struct_tracks_flexible_tail_metadata() -> None: +def test_map_struct_tracks_flexible_tail_metadata() -> None: decl = Struct( decl_id="struct:Packet", name="Packet", @@ -871,14 +871,14 @@ def test_lower_struct_tracks_flexible_tail_metadata() -> None: is_complete=True, ) - lowered = lower_struct(decl, context=_context_for(decl)) + mapped = map_struct(decl, context=_context_for(decl)) - assert lowered.fieldwise_init is False - assert lowered.flexible_tail is not None - assert lowered.flexible_tail.field_name == "payload" - assert lowered.flexible_tail.pattern == "c99_empty" - assert lowered.flexible_tail.byte_offset == 4 - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.flexible_tail is not None + assert mapped.flexible_tail.field_name == "payload" + assert mapped.flexible_tail.pattern == "c99_empty" + assert mapped.flexible_tail.byte_offset == 4 + assert mapped.members == [ StoredMember(index=0, name="tag", type=BuiltinType(MojoBuiltin.C_UINT), byte_offset=0), StoredMember( index=1, @@ -889,7 +889,7 @@ def test_lower_struct_tracks_flexible_tail_metadata() -> None: ] -def test_lower_struct_propagates_terminal_by_value_embedded_fam_struct() -> None: +def test_map_struct_propagates_terminal_by_value_embedded_fam_struct() -> None: packet = Struct( decl_id="struct:Packet", name="Packet", @@ -938,15 +938,15 @@ def test_lower_struct_propagates_terminal_by_value_embedded_fam_struct() -> None is_complete=True, ) - lowered = lower_struct(wrapper, context=_context_for(packet, wrapper)) + mapped = map_struct(wrapper, context=_context_for(packet, wrapper)) - assert lowered.fieldwise_init is False - assert lowered.diagnostics == [] - assert lowered.flexible_tail is not None - assert lowered.flexible_tail.field_name == "payload" - assert lowered.flexible_tail.pattern == "c99_empty" - assert lowered.flexible_tail.byte_offset == 4 - assert lowered.members == [ + assert mapped.fieldwise_init is False + assert mapped.diagnostics == [] + assert mapped.flexible_tail is not None + assert mapped.flexible_tail.field_name == "payload" + assert mapped.flexible_tail.pattern == "c99_empty" + assert mapped.flexible_tail.byte_offset == 4 + assert mapped.members == [ StoredMember( index=0, name="packet", @@ -956,7 +956,7 @@ def test_lower_struct_propagates_terminal_by_value_embedded_fam_struct() -> None ] -def test_lower_struct_falls_back_for_nonterminal_by_value_embedded_fam_struct() -> None: +def test_map_struct_falls_back_for_nonterminal_by_value_embedded_fam_struct() -> None: packet = Struct( decl_id="struct:Packet", name="Packet", @@ -1006,13 +1006,13 @@ def test_lower_struct_falls_back_for_nonterminal_by_value_embedded_fam_struct() is_complete=True, ) - lowered = lower_struct(wrapper, context=_context_for(packet, wrapper)) + mapped = map_struct(wrapper, context=_context_for(packet, wrapper)) - assert lowered.fieldwise_init is False - assert lowered.flexible_tail is None - assert lowered.members == [OpaqueStorageMember(name="storage", size_bytes=8)] - assert lowered.diagnostics + assert mapped.fieldwise_init is False + assert mapped.flexible_tail is None + assert mapped.members == [OpaqueStorageMember(name="storage", size_bytes=8)] + assert mapped.diagnostics assert ( "embedded flexible tail is not terminal in the enclosing record" - in lowered.diagnostics[0].message + in mapped.diagnostics[0].message ) diff --git a/tests/unit/test_analysis_type_lowering.py b/tests/unit/test_analysis_type_mapping.py similarity index 61% rename from tests/unit/test_analysis_type_lowering.py rename to tests/unit/test_analysis_type_mapping.py index da651a4..3544e97 100644 --- a/tests/unit/test_analysis_type_lowering.py +++ b/tests/unit/test_analysis_type_mapping.py @@ -1,8 +1,8 @@ -"""Unit tests for surface-oriented CIR -> MojoIR type lowering.""" +"""Unit tests for surface-oriented CIR -> MojoIR type mapping.""" from __future__ import annotations -from mojo_bindgen.analysis.mojo.type_lowering import LowerTypePass, lower_type +from mojo_bindgen.analysis.mojo.type_mapping import MapTypePass, map_type from mojo_bindgen.ir import ( Array, AtomicType, @@ -35,26 +35,26 @@ ) -def test_lower_type_maps_primitives_to_builtins() -> None: - lowered = lower_type(VoidType()) +def test_map_type_maps_primitives_to_builtins() -> None: + mapped = map_type(VoidType()) - assert lowered == BuiltinType(name=MojoBuiltin.NONE) - assert lower_type(IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4)) == BuiltinType( + assert mapped == BuiltinType(name=MojoBuiltin.NONE) + assert map_type(IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4)) == BuiltinType( name=MojoBuiltin.C_INT ) -def test_lower_type_preserves_typeref_name_as_named_type() -> None: +def test_map_type_preserves_typeref_name_as_named_type() -> None: t = TypeRef( decl_id="typedef:my_uint", name="my_uint", canonical=IntType(int_kind=IntKind.UINT, size_bytes=4, align_bytes=4), ) - assert lower_type(t) == NamedType(name="my_uint") + assert map_type(t) == NamedType(name="my_uint") -def test_lower_type_maps_named_record_and_enum_refs() -> None: +def test_map_type_maps_named_record_and_enum_refs() -> None: enum_ref = EnumRef( decl_id="enum:mode_t", name="mode_t", @@ -67,11 +67,11 @@ def test_lower_type_maps_named_record_and_enum_refs() -> None: c_name="payload_t", ) - assert lower_type(enum_ref) == NamedType(name="mode_t") - assert lower_type(struct_ref) == NamedType(name="payload_t") + assert map_type(enum_ref) == NamedType(name="mode_t") + assert map_type(struct_ref) == NamedType(name="payload_t") -def test_lower_type_uses_pointer_pointee_constness_for_mutability() -> None: +def test_map_type_uses_pointer_pointee_constness_for_mutability() -> None: const_int_ptr = Pointer( pointee=QualifiedType( unqualified=IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4), @@ -80,36 +80,36 @@ def test_lower_type_uses_pointer_pointee_constness_for_mutability() -> None: ) void_ptr = Pointer(pointee=None) - assert lower_type(const_int_ptr) == Pointer( + assert map_type(const_int_ptr) == Pointer( pointee=BuiltinType(name=MojoBuiltin.C_INT), mutability=PointerMutability.IMMUT, nullable=True, ) - assert lower_type(void_ptr) == Pointer( + assert map_type(void_ptr) == Pointer( pointee=None, mutability=PointerMutability.MUT, nullable=True, ) -def test_lower_type_maps_fixed_arrays_and_pointer_falls_back_for_flexible_arrays() -> None: +def test_map_type_maps_fixed_arrays_and_pointer_falls_back_for_flexible_arrays() -> None: element = IntType(int_kind=IntKind.SHORT, size_bytes=2, align_bytes=2) - assert lower_type(Array(element=element, size=4, array_kind="fixed")) == Array( + assert map_type(Array(element=element, size=4, array_kind="fixed")) == Array( element=BuiltinType(name=MojoBuiltin.C_SHORT), size=4, ) - assert lower_type(Array(element=element, size=4, array_kind="flexible")) == Pointer( + assert map_type(Array(element=element, size=4, array_kind="flexible")) == Pointer( pointee=BuiltinType(name=MojoBuiltin.C_SHORT), mutability=PointerMutability.MUT, ) - assert lower_type(Array(element=element, size=None, array_kind="flexible")) == Array( + assert map_type(Array(element=element, size=None, array_kind="flexible")) == Array( element=BuiltinType(name=MojoBuiltin.C_SHORT), size=0, ) -def test_lower_type_keeps_raw_function_pointer_signature_shape() -> None: +def test_map_type_keeps_raw_function_pointer_signature_shape() -> None: fn_ptr = FunctionPtr( ret=IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4), params=[ @@ -127,7 +127,7 @@ def test_lower_type_keeps_raw_function_pointer_signature_shape() -> None: calling_convention="c", ) - assert lower_type(fn_ptr) == FunctionPtr( + assert map_type(fn_ptr) == FunctionPtr( params=[ Param( name="", @@ -142,42 +142,42 @@ def test_lower_type_keeps_raw_function_pointer_signature_shape() -> None: ) -def test_lower_type_maps_representable_atomic_to_atomic_surface_type() -> None: +def test_map_type_maps_representable_atomic_to_atomic_surface_type() -> None: atomic = AtomicType(value_type=IntType(int_kind=IntKind.UINT, size_bytes=4, align_bytes=4)) - assert lower_type(atomic) == ParametricType( + assert map_type(atomic) == ParametricType( base=ParametricBase.ATOMIC, args=[DTypeArg("DType.uint32")], ) -def test_lower_type_maps_complex_and_vector_surface_forms_and_fallbacks() -> None: +def test_map_type_maps_complex_and_vector_surface_forms_and_fallbacks() -> None: f32 = FloatType(float_kind=FloatKind.FLOAT, size_bytes=4, align_bytes=4) long_double = FloatType(float_kind=FloatKind.LONG_DOUBLE, size_bytes=16, align_bytes=16) - assert lower_type(ComplexType(element=f32, size_bytes=8)) == ParametricType( + assert map_type(ComplexType(element=f32, size_bytes=8)) == ParametricType( base=ParametricBase.COMPLEX_SIMD, args=[DTypeArg("DType.float32"), ConstArg(1)], ) - assert lower_type(ComplexType(element=long_double, size_bytes=32)) == Array( + assert map_type(ComplexType(element=long_double, size_bytes=32)) == Array( element=BuiltinType(name=MojoBuiltin.C_DOUBLE), size=2, ) - assert lower_type(VectorType(element=f32, count=4, size_bytes=16)) == ParametricType( + assert map_type(VectorType(element=f32, count=4, size_bytes=16)) == ParametricType( base=ParametricBase.SIMD, args=[DTypeArg("DType.float32"), ConstArg(4)], ) - assert lower_type(VectorType(element=long_double, count=2, size_bytes=32)) == Array( + assert map_type(VectorType(element=long_double, count=2, size_bytes=32)) == Array( element=BuiltinType(name=MojoBuiltin.C_DOUBLE), size=2, ) - assert lower_type(VectorType(element=f32, count=None, size_bytes=16)) == Array( + assert map_type(VectorType(element=f32, count=None, size_bytes=16)) == Array( element=BuiltinType(name=MojoBuiltin.UINT8), size=16, ) -def test_lower_type_maps_unsupported_type_to_inline_bytes_when_sized() -> None: +def test_map_type_maps_unsupported_type_to_inline_bytes_when_sized() -> None: unsupported = UnsupportedType( category="unknown", spelling="mystery_t", @@ -186,48 +186,48 @@ def test_lower_type_maps_unsupported_type_to_inline_bytes_when_sized() -> None: align_bytes=8, ) - assert lower_type(unsupported) == Array( + assert map_type(unsupported) == Array( element=BuiltinType(name=MojoBuiltin.UINT8), size=16, ) -def test_lower_type_maps_unsized_unsupported_type_to_opaque_pointer() -> None: +def test_map_type_maps_unsized_unsupported_type_to_opaque_pointer() -> None: unsupported = UnsupportedType( category="unknown", spelling="mystery_t", reason="not modeled", ) - assert lower_type(unsupported) == Pointer( + assert map_type(unsupported) == Pointer( pointee=None, mutability=PointerMutability.MUT, ) -def test_lower_type_maps_missing_primitive_kinds_to_valid_surface_types() -> None: - assert lower_type(IntType(int_kind=IntKind.WCHAR, size_bytes=4, align_bytes=4)) == NamedType( +def test_map_type_maps_missing_primitive_kinds_to_valid_surface_types() -> None: + assert map_type(IntType(int_kind=IntKind.WCHAR, size_bytes=4, align_bytes=4)) == NamedType( name="Int32" ) - assert lower_type(IntType(int_kind=IntKind.CHAR16, size_bytes=2, align_bytes=2)) == NamedType( + assert map_type(IntType(int_kind=IntKind.CHAR16, size_bytes=2, align_bytes=2)) == NamedType( name="UInt16" ) - assert lower_type(IntType(int_kind=IntKind.CHAR32, size_bytes=4, align_bytes=4)) == NamedType( + assert map_type(IntType(int_kind=IntKind.CHAR32, size_bytes=4, align_bytes=4)) == NamedType( name="UInt32" ) - assert lower_type( + assert map_type( IntType(int_kind=IntKind.EXT_INT, size_bytes=8, align_bytes=8, ext_bits=33) ) == NamedType(name="Int64") - assert lower_type( - IntType(int_kind=IntKind.INT128, size_bytes=16, align_bytes=16) - ) == BuiltinType(name=MojoBuiltin.INT128) - assert lower_type( + assert map_type(IntType(int_kind=IntKind.INT128, size_bytes=16, align_bytes=16)) == BuiltinType( + name=MojoBuiltin.INT128 + ) + assert map_type( IntType(int_kind=IntKind.UINT128, size_bytes=16, align_bytes=16) ) == BuiltinType(name=MojoBuiltin.UINT128) -def test_lower_type_maps_float128_to_inline_bytes() -> None: - assert lower_type( +def test_map_type_maps_float128_to_inline_bytes() -> None: + assert map_type( FloatType(float_kind=FloatKind.FLOAT128, size_bytes=16, align_bytes=16) ) == Array( element=BuiltinType(name=MojoBuiltin.UINT8), @@ -235,8 +235,8 @@ def test_lower_type_maps_float128_to_inline_bytes() -> None: ) -def test_lower_type_caches_recursively_without_looping_for_nested_typeref_shapes() -> None: - lowerer = LowerTypePass() +def test_map_type_caches_recursively_without_looping_for_nested_typeref_shapes() -> None: + mapper = MapTypePass() alias = TypeRef( decl_id="typedef:node_handle_t", name="node_handle_t", @@ -249,8 +249,8 @@ def test_lower_type_caches_recursively_without_looping_for_nested_typeref_shapes ), ) - first = lowerer.run(alias) - second = lowerer.run(alias) + first = mapper.run(alias) + second = mapper.run(alias) assert first == NamedType(name="node_handle_t") assert second == first diff --git a/tests/unit/test_analysis_union_lowering.py b/tests/unit/test_analysis_union_mapping.py similarity index 71% rename from tests/unit/test_analysis_union_lowering.py rename to tests/unit/test_analysis_union_mapping.py index 57c373b..91d451e 100644 --- a/tests/unit/test_analysis_union_lowering.py +++ b/tests/unit/test_analysis_union_mapping.py @@ -1,8 +1,8 @@ -"""Unit tests for CIR union -> MojoIR union layout lowering.""" +"""Unit tests for CIR union -> MojoIR union layout mapping.""" from __future__ import annotations -from mojo_bindgen.analysis.mojo.union_lowering import lower_union +from mojo_bindgen.analysis.mojo.union_mapping import map_union from mojo_bindgen.ir import ( AliasDecl, AliasKind, @@ -34,7 +34,7 @@ def _u32() -> IntType: return IntType(int_kind=IntKind.UINT, size_bytes=4, align_bytes=4) -def test_lower_union_uses_unsafe_union_for_distinct_supported_member_types() -> None: +def test_map_union_uses_unsafe_union_for_distinct_supported_member_types() -> None: decl = Struct( decl_id="union:Payload", name="Payload", @@ -54,9 +54,9 @@ def test_lower_union_uses_unsafe_union_for_distinct_supported_member_types() -> is_complete=True, ) - lowered = lower_union(decl) + mapped = map_union(decl) - assert lowered == AliasDecl( + assert mapped == AliasDecl( name="Payload", kind=AliasKind.UNION_LAYOUT, type_value=ParametricType( @@ -69,7 +69,7 @@ def test_lower_union_uses_unsafe_union_for_distinct_supported_member_types() -> ) -def test_lower_union_falls_back_for_duplicate_member_types() -> None: +def test_map_union_falls_back_for_duplicate_member_types() -> None: decl = Struct( decl_id="union:Dup", name="Dup", @@ -85,20 +85,20 @@ def test_lower_union_falls_back_for_duplicate_member_types() -> None: is_complete=True, ) - lowered = lower_union(decl) + mapped = map_union(decl) - assert lowered.type_value == ParametricType( + assert mapped.type_value == ParametricType( base=ParametricBase.UNSAFE_UNION, args=[ TypeArg(type=BuiltinType(MojoBuiltin.C_INT)), TypeArg(type=BuiltinType(MojoBuiltin.C_SHORT)), ], ) - assert lowered.diagnostics[0].category == "union_lowering" - assert "duplicates lowered type of earlier member" in lowered.diagnostics[0].message + assert mapped.diagnostics[0].category == "union_mapping" + assert "duplicates mapped type of earlier member" in mapped.diagnostics[0].message -def test_lower_union_falls_back_for_unsupported_member_type() -> None: +def test_map_union_falls_back_for_unsupported_member_type() -> None: decl = Struct( decl_id="union:Odd", name="Odd", @@ -123,9 +123,9 @@ def test_lower_union_falls_back_for_unsupported_member_type() -> None: is_complete=True, ) - lowered = lower_union(decl) + mapped = map_union(decl) - assert lowered.type_value == ParametricType( + assert mapped.type_value == ParametricType( base=ParametricBase.UNSAFE_UNION, args=[ TypeArg( @@ -136,10 +136,10 @@ def test_lower_union_falls_back_for_unsupported_member_type() -> None: ) ], ) - assert lowered.diagnostics == [] + assert mapped.diagnostics == [] -def test_lower_union_falls_back_for_self_referential_member_type() -> None: +def test_map_union_falls_back_for_self_referential_member_type() -> None: decl = Struct( decl_id="union:Node", name="Node", @@ -164,17 +164,17 @@ def test_lower_union_falls_back_for_self_referential_member_type() -> None: is_complete=True, ) - lowered = lower_union(decl) + mapped = map_union(decl) - assert lowered.type_value == Array( + assert mapped.type_value == Array( element=BuiltinType(MojoBuiltin.UINT8), size=8, ) - assert lowered.diagnostics[0].category == "union_lowering" - assert "self-referential type `Node`" in lowered.diagnostics[0].message + assert mapped.diagnostics[0].category == "union_mapping" + assert "self-referential type `Node`" in mapped.diagnostics[0].message -def test_lower_union_keeps_incomplete_unions_as_placeholder_aliases() -> None: +def test_map_union_keeps_incomplete_unions_as_placeholder_aliases() -> None: decl = Struct( decl_id="union:Opaque", name="Opaque", @@ -186,14 +186,14 @@ def test_lower_union_keeps_incomplete_unions_as_placeholder_aliases() -> None: is_complete=False, ) - lowered = lower_union(decl) + mapped = map_union(decl) - assert lowered.name == "Opaque" - assert lowered.kind == AliasKind.UNION_LAYOUT - assert lowered.type_value is None - assert lowered.const_value is None - assert len(lowered.diagnostics) == 1 - assert lowered.diagnostics[0].category == "stub_lowering" + assert mapped.name == "Opaque" + assert mapped.kind == AliasKind.UNION_LAYOUT + assert mapped.type_value is None + assert mapped.const_value is None + assert len(mapped.diagnostics) == 1 + assert mapped.diagnostics[0].category == "stub_mapping" assert ( - lowered.diagnostics[0].message == "incomplete union placeholder emitted; layout not lowered" + mapped.diagnostics[0].message == "incomplete union placeholder emitted; layout not mapped" ) diff --git a/tests/unit/test_analysis_unit_lowering.py b/tests/unit/test_analysis_unit_mapping.py similarity index 85% rename from tests/unit/test_analysis_unit_lowering.py rename to tests/unit/test_analysis_unit_mapping.py index 72b84f2..fecc2de 100644 --- a/tests/unit/test_analysis_unit_lowering.py +++ b/tests/unit/test_analysis_unit_mapping.py @@ -1,8 +1,8 @@ -"""Unit tests for the high-level CIR ``Unit`` -> MojoIR lowering entrypoint.""" +"""Unit tests for the high-level CIR ``Unit`` -> MojoIR mapping entrypoint.""" from __future__ import annotations -from mojo_bindgen.analysis import lower_unit +from mojo_bindgen.analysis import map_unit from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( AliasDecl, @@ -95,7 +95,7 @@ def _field( ) -def test_lower_unit_builds_module_metadata_and_preserves_decl_order() -> None: +def test_map_unit_builds_module_metadata_and_preserves_decl_order() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -132,14 +132,14 @@ def test_lower_unit_builds_module_metadata_and_preserves_decl_order() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - assert lowered.source_header == "demo.h" - assert lowered.library == "demo" - assert lowered.link_name == "demo" - assert lowered.link_mode == LinkMode.EXTERNAL_CALL - assert lowered.library_path_hint is None - assert [type(decl) for decl in lowered.decls] == [ + assert mapped.source_header == "demo.h" + assert mapped.library == "demo" + assert mapped.link_name == "demo" + assert mapped.link_mode == LinkMode.EXTERNAL_CALL + assert mapped.library_path_hint is None + assert [type(decl) for decl in mapped.decls] == [ AliasDecl, AliasDecl, AliasDecl, @@ -148,7 +148,7 @@ def test_lower_unit_builds_module_metadata_and_preserves_decl_order() -> None: ] -def test_lower_unit_uses_owned_dl_handle_module_policy_for_wrappers() -> None: +def test_map_unit_uses_owned_dl_handle_module_policy_for_wrappers() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -165,7 +165,7 @@ def test_lower_unit_uses_owned_dl_handle_module_policy_for_wrappers() -> None: ], ) - lowered = lower_unit( + mapped = map_unit( unit, options=MojoEmitOptions( linking="owned_dl_handle", @@ -174,15 +174,15 @@ def test_lower_unit_uses_owned_dl_handle_module_policy_for_wrappers() -> None: ), ) - fn_decl = lowered.decls[0] + fn_decl = mapped.decls[0] - assert lowered.link_mode == LinkMode.OWNED_DL_HANDLE - assert lowered.library_path_hint == "/tmp/libdemo.so" + assert mapped.link_mode == LinkMode.OWNED_DL_HANDLE + assert mapped.library_path_hint == "/tmp/libdemo.so" assert isinstance(fn_decl, FunctionDecl) assert fn_decl.call_target.link_mode == LinkMode.OWNED_DL_HANDLE -def test_lower_unit_lowers_typedef_and_enum_surface_forms() -> None: +def test_map_unit_maps_typedef_and_enum_surface_forms() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -211,12 +211,12 @@ def test_lower_unit_lowers_typedef_and_enum_surface_forms() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - typedef_decl = lowered.decls[0] - signed_typedef_decl = lowered.decls[1] - enum_decl = lowered.decls[2] - enumerant_decl = lowered.decls[3] + typedef_decl = mapped.decls[0] + signed_typedef_decl = mapped.decls[1] + enum_decl = mapped.decls[2] + enumerant_decl = mapped.decls[3] assert typedef_decl == AliasDecl( name="size_t", @@ -249,7 +249,7 @@ def test_lower_unit_lowers_typedef_and_enum_surface_forms() -> None: ) -def test_lower_unit_lowers_function_and_global() -> None: +def test_map_unit_maps_function_and_global() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -274,10 +274,10 @@ def test_lower_unit_lowers_function_and_global() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - fn_decl = lowered.decls[0] - global_decl = lowered.decls[1] + fn_decl = mapped.decls[0] + global_decl = mapped.decls[1] assert isinstance(fn_decl, FunctionDecl) assert fn_decl.kind == FunctionKind.VARIADIC_STUB @@ -287,7 +287,7 @@ def test_lower_unit_lowers_function_and_global() -> None: assert global_decl.is_const is True -def test_lower_unit_lowers_const_and_supported_macro_to_aliases() -> None: +def test_map_unit_maps_const_and_supported_macro_to_aliases() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -309,10 +309,10 @@ def test_lower_unit_lowers_const_and_supported_macro_to_aliases() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - const_decl = lowered.decls[0] - macro_decl = lowered.decls[1] + const_decl = mapped.decls[0] + macro_decl = mapped.decls[1] assert isinstance(const_decl, AliasDecl) assert const_decl.kind == AliasKind.CONST_VALUE @@ -322,7 +322,7 @@ def test_lower_unit_lowers_const_and_supported_macro_to_aliases() -> None: assert macro_decl.const_value is not None -def test_lower_unit_emits_placeholder_macro_when_not_parsed() -> None: +def test_map_unit_emits_placeholder_macro_when_not_parsed() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -338,8 +338,8 @@ def test_lower_unit_emits_placeholder_macro_when_not_parsed() -> None: ], ) - lowered = lower_unit(unit) - decl = lowered.decls[0] + mapped = map_unit(unit) + decl = mapped.decls[0] assert isinstance(decl, AliasDecl) assert decl.kind == AliasKind.MACRO_VALUE @@ -351,7 +351,7 @@ def test_lower_unit_emits_placeholder_macro_when_not_parsed() -> None: assert decl.diagnostics[1].message == "define BROKEN foo ( )" -def test_lower_unit_preserves_macro_with_unemitted_reference() -> None: +def test_map_unit_preserves_macro_with_unemitted_reference() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -368,8 +368,8 @@ def test_lower_unit_preserves_macro_with_unemitted_reference() -> None: ], ) - lowered = lower_unit(unit) - decl = lowered.decls[0] + mapped = map_unit(unit) + decl = mapped.decls[0] assert isinstance(decl, AliasDecl) assert decl.kind == AliasKind.MACRO_VALUE @@ -378,7 +378,7 @@ def test_lower_unit_preserves_macro_with_unemitted_reference() -> None: assert "non-emitted" in decl.diagnostics[0].message -def test_lower_unit_preserves_macro_with_unfolded_c_logical_operator() -> None: +def test_map_unit_preserves_macro_with_unfolded_c_logical_operator() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -395,8 +395,8 @@ def test_lower_unit_preserves_macro_with_unfolded_c_logical_operator() -> None: ], ) - lowered = lower_unit(unit) - decl = lowered.decls[0] + mapped = map_unit(unit) + decl = mapped.decls[0] assert isinstance(decl, AliasDecl) assert decl.kind == AliasKind.MACRO_VALUE @@ -404,7 +404,7 @@ def test_lower_unit_preserves_macro_with_unfolded_c_logical_operator() -> None: assert "logical operator" in decl.diagnostics[0].message -def test_lower_unit_lowers_structs_and_unions_with_real_record_layouts() -> None: +def test_map_unit_maps_structs_and_unions_with_real_record_layouts() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -456,11 +456,11 @@ def test_lower_unit_lowers_structs_and_unions_with_real_record_layouts() -> None ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - opaque_decl = lowered.decls[0] - plain_decl = lowered.decls[1] - union_decl = lowered.decls[2] + opaque_decl = mapped.decls[0] + plain_decl = mapped.decls[1] + union_decl = mapped.decls[2] assert isinstance(opaque_decl, StructDecl) assert opaque_decl.kind == StructKind.OPAQUE @@ -489,7 +489,7 @@ def test_lower_unit_lowers_structs_and_unions_with_real_record_layouts() -> None assert union_decl.diagnostics == [] -def test_lower_unit_uses_unsafe_union_for_repeated_types_union() -> None: +def test_map_unit_uses_unsafe_union_for_repeated_types_union() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -512,8 +512,8 @@ def test_lower_unit_uses_unsafe_union_for_repeated_types_union() -> None: ], ) - lowered = lower_unit(unit) - union_decl = lowered.decls[0] + mapped = map_unit(unit) + union_decl = mapped.decls[0] assert isinstance(union_decl, AliasDecl) assert union_decl.kind == AliasKind.UNION_LAYOUT @@ -521,10 +521,10 @@ def test_lower_unit_uses_unsafe_union_for_repeated_types_union() -> None: base=ParametricBase.UNSAFE_UNION, args=[TypeArg(type=BuiltinType(MojoBuiltin.C_INT))], ) - assert union_decl.diagnostics[0].category == "union_lowering" + assert union_decl.diagnostics[0].category == "union_mapping" -def test_lower_unit_keeps_incomplete_union_as_placeholder_alias() -> None: +def test_map_unit_keeps_incomplete_union_as_placeholder_alias() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -544,20 +544,20 @@ def test_lower_unit_keeps_incomplete_union_as_placeholder_alias() -> None: ], ) - lowered = lower_unit(unit) - union_decl = lowered.decls[0] + mapped = map_unit(unit) + union_decl = mapped.decls[0] assert isinstance(union_decl, AliasDecl) assert union_decl.kind == AliasKind.UNION_LAYOUT assert union_decl.type_value is None - assert union_decl.diagnostics[0].category == "stub_lowering" + assert union_decl.diagnostics[0].category == "stub_mapping" assert ( union_decl.diagnostics[0].message - == "incomplete union placeholder emitted; layout not lowered" + == "incomplete union placeholder emitted; layout not mapped" ) -def test_lower_unit_lowers_structs_that_store_union_members_by_named_alias() -> None: +def test_map_unit_maps_structs_that_store_union_members_by_named_alias() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -601,9 +601,9 @@ def test_lower_unit_lowers_structs_that_store_union_members_by_named_alias() -> ], ) - lowered = lower_unit(unit) - union_decl = lowered.decls[0] - holder_decl = lowered.decls[1] + mapped = map_unit(unit) + union_decl = mapped.decls[0] + holder_decl = mapped.decls[1] assert isinstance(union_decl, AliasDecl) assert union_decl.kind == AliasKind.UNION_LAYOUT @@ -620,7 +620,7 @@ def test_lower_unit_lowers_structs_that_store_union_members_by_named_alias() -> assert holder_decl.diagnostics == [] -def test_lower_unit_keeps_raw_callback_types_inline() -> None: +def test_map_unit_keeps_raw_callback_types_inline() -> None: callback = FunctionPtr( ret=IntType(int_kind=IntKind.INT, size_bytes=4, align_bytes=4), params=[CIRParam(name="", type=Pointer(pointee=None))], @@ -647,10 +647,10 @@ def test_lower_unit_keeps_raw_callback_types_inline() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - typedef_decl = lowered.decls[0] - fn_decl = lowered.decls[1] + typedef_decl = mapped.decls[0] + fn_decl = mapped.decls[1] assert isinstance(typedef_decl, AliasDecl) assert isinstance(typedef_decl.type_value, FunctionPtr) @@ -663,7 +663,7 @@ def test_lower_unit_keeps_raw_callback_types_inline() -> None: ) -def test_lower_unit_synthesizes_aliases_for_external_typeref_uses() -> None: +def test_map_unit_synthesizes_aliases_for_external_typeref_uses() -> None: int32_ref = TypeRef( decl_id="typedef:int32_t", name="int32_t", @@ -688,9 +688,9 @@ def test_lower_unit_synthesizes_aliases_for_external_typeref_uses() -> None: ], ) - lowered = lower_unit(unit) - typedef_decl = lowered.decls[0] - fn_decl = lowered.decls[1] + mapped = map_unit(unit) + typedef_decl = mapped.decls[0] + fn_decl = mapped.decls[1] assert typedef_decl == AliasDecl( name="int32_t", @@ -702,7 +702,7 @@ def test_lower_unit_synthesizes_aliases_for_external_typeref_uses() -> None: assert fn_decl.return_type == NamedType("int32_t") -def test_lower_unit_synthesizes_exact_width_stdint_aliases_as_fixed_width() -> None: +def test_map_unit_synthesizes_exact_width_stdint_aliases_as_fixed_width() -> None: aliases = { "int8_t": (IntType(int_kind=IntKind.SCHAR, size_bytes=1, align_bytes=1), "Int8"), "uint8_t": (IntType(int_kind=IntKind.UCHAR, size_bytes=1, align_bytes=1), "UInt8"), @@ -733,9 +733,9 @@ def test_lower_unit_synthesizes_exact_width_stdint_aliases_as_fixed_width() -> N ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - assert lowered.decls[: len(aliases)] == [ + assert mapped.decls[: len(aliases)] == [ AliasDecl( name=name, kind=AliasKind.TYPE_ALIAS, @@ -745,7 +745,7 @@ def test_lower_unit_synthesizes_exact_width_stdint_aliases_as_fixed_width() -> N ] -def test_lower_unit_lowers_local_exact_width_typedef_and_chain() -> None: +def test_map_unit_maps_local_exact_width_typedef_and_chain() -> None: int64_ref = TypeRef( decl_id="typedef:int64_t", name="int64_t", @@ -784,21 +784,21 @@ def test_lower_unit_lowers_local_exact_width_typedef_and_chain() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - assert lowered.decls[0] == AliasDecl( + assert mapped.decls[0] == AliasDecl( name="int64_t", kind=AliasKind.TYPE_ALIAS, type_value=NamedType("Int64"), ) - assert lowered.decls[1] == AliasDecl( + assert mapped.decls[1] == AliasDecl( name="my_i64", kind=AliasKind.TYPE_ALIAS, type_value=NamedType("int64_t"), ) -def test_lower_unit_keeps_unrelated_unsigned_int_typedef_as_c_uint() -> None: +def test_map_unit_keeps_unrelated_unsigned_int_typedef_as_c_uint() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -814,16 +814,16 @@ def test_lower_unit_keeps_unrelated_unsigned_int_typedef_as_c_uint() -> None: ], ) - lowered = lower_unit(unit) + mapped = map_unit(unit) - assert lowered.decls[0] == AliasDecl( + assert mapped.decls[0] == AliasDecl( name="raw_uint_t", kind=AliasKind.TYPE_ALIAS, type_value=BuiltinType(MojoBuiltin.C_UINT), ) -def test_lower_unit_keeps_ordinary_long_as_c_long() -> None: +def test_map_unit_keeps_ordinary_long_as_c_long() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -840,15 +840,15 @@ def test_lower_unit_keeps_ordinary_long_as_c_long() -> None: ], ) - lowered = lower_unit(unit) - fn_decl = lowered.decls[0] + mapped = map_unit(unit) + fn_decl = mapped.decls[0] assert isinstance(fn_decl, FunctionDecl) assert fn_decl.return_type == BuiltinType(MojoBuiltin.C_LONG) assert fn_decl.params[0].type == BuiltinType(MojoBuiltin.C_LONG) -def test_lower_unit_keeps_placeholder_const_when_const_expr_lowering_fails() -> None: +def test_map_unit_keeps_placeholder_const_when_const_expr_mapping_fails() -> None: unit = Unit( source_header="demo.h", library="demo", @@ -863,13 +863,13 @@ def test_lower_unit_keeps_placeholder_const_when_const_expr_lowering_fails() -> ], ) - lowered = lower_unit(unit) - decl = lowered.decls[0] + mapped = map_unit(unit) + decl = mapped.decls[0] assert isinstance(decl, AliasDecl) assert decl.kind == AliasKind.CONST_VALUE assert decl.const_value is None assert decl.type_value is None assert decl.diagnostics[0].message == ( - "constant expression could not be lowered; placeholder alias emitted" + "constant expression could not be mapped; placeholder alias emitted" ) diff --git a/tests/unit/test_ir_codegen_edges.py b/tests/unit/test_ir_codegen_edges.py index 8c1fc0f..cb2e580 100644 --- a/tests/unit/test_ir_codegen_edges.py +++ b/tests/unit/test_ir_codegen_edges.py @@ -2,7 +2,7 @@ from __future__ import annotations -from mojo_bindgen.analysis import lower_unit +from mojo_bindgen.analysis import map_unit from mojo_bindgen.analysis.mojo.mojo_emit_options import MojoEmitOptions from mojo_bindgen.ir import ( AtomicType, @@ -164,7 +164,7 @@ def test_incomplete_struct_emitted_as_opaque_stub_not_as_layout_struct() -> None align_bytes=0, is_complete=False, ) - lowered = lower_unit( + mapped = map_unit( Unit( source_header="t.h", library="t", @@ -174,7 +174,7 @@ def test_incomplete_struct_emitted_as_opaque_stub_not_as_layout_struct() -> None ), options=MojoEmitOptions(), ) - structs = [decl for decl in lowered.decls if isinstance(decl, StructDecl)] + structs = [decl for decl in mapped.decls if isinstance(decl, StructDecl)] assert len(structs) == 1 assert structs[0].name == "foo" assert structs[0].kind == StructKind.OPAQUE @@ -1080,7 +1080,7 @@ def test_generator_emits_documented_inline_array_fallback_for_duplicated_type_un decls=[union_decl, holder], ) ) - assert "duplicates lowered type of earlier member `a`" in out + assert "duplicates mapped type of earlier member `a`" in out assert "comptime Dup = UnsafeUnion[c_int]" in out assert "var payload: Dup" in out From 94915c8edadc9a54c31a5f729c8764104b35f448 Mon Sep 17 00:00:00 2001 From: MoSafi2 Date: Sat, 13 Jun 2026 16:48:46 +0200 Subject: [PATCH 6/6] Clarify record layout and storage boundary --- README.md | 2 +- docs/analysis_type_mapping_pipeline.md | 2 +- docs/cir_passes_pipeline.md | 20 +- mojo_bindgen/analysis/__init__.py | 20 +- mojo_bindgen/analysis/facts/context.py | 17 +- mojo_bindgen/analysis/facts/record_layout.py | 9 +- .../{record_shape.py => record_storage.py} | 253 ++++++++++-------- mojo_bindgen/analysis/mojo/struct_mapping.py | 32 +-- mojo_bindgen/analysis/mojo/unit_mapping.py | 2 +- tests/unit/test_analysis_context.py | 2 +- ...ape.py => test_analysis_record_storage.py} | 24 +- 11 files changed, 205 insertions(+), 178 deletions(-) rename mojo_bindgen/analysis/facts/{record_shape.py => record_storage.py} (62%) rename tests/unit/{test_analysis_record_shape.py => test_analysis_record_storage.py} (85%) diff --git a/README.md b/README.md index 50f6e14..8160ce4 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ Current support includes: be represented safely fall back to opaque `InlineArray[...]` storage with diagnostics to preserve layout. - **Opaque and difficult layouts:** incomplete records, packed layouts, and - alignment-sensitive record shapes are preserved conservatively as opaque byte + alignment-sensitive record storages are preserved conservatively as opaque byte storage when a faithful typed layout is not possible. - **Callbacks and function pointers:** callback typedefs, function-pointer fields, and function-pointer parameters and returns are preserved in Mojo via diff --git a/docs/analysis_type_mapping_pipeline.md b/docs/analysis_type_mapping_pipeline.md index 659e492..4886107 100644 --- a/docs/analysis_type_mapping_pipeline.md +++ b/docs/analysis_type_mapping_pipeline.md @@ -106,7 +106,7 @@ Responsibilities: - reject incomplete or non-representable layouts onto opaque storage - map plain stored fields and bitfield storage/logical field types - synthesize padding members and initializers from analyzed layout facts -- preserve flexible-tail metadata when the enclosing shape stays representable +- preserve flexible-tail metadata when the enclosing record remains typed-storage eligible - attach fallback diagnostics when a record must collapse to opaque storage Source: diff --git a/docs/cir_passes_pipeline.md b/docs/cir_passes_pipeline.md index 95a33a8..39a572d 100644 --- a/docs/cir_passes_pipeline.md +++ b/docs/cir_passes_pipeline.md @@ -24,7 +24,7 @@ flowchart TD G --> I[DeclDependencyGraph] G --> J[AliasClassification] G --> K[RecordLayoutFacts] - G --> L[RecordAnalysisFacts] + G --> L[RecordStorageFacts] G --> M[MapUnitPass] M --> N[MapTypePass] @@ -182,21 +182,21 @@ Purpose: `analyze_record_layout()` owns pure C-layout checks. It does not decide how a Mojo `StructDecl` should be emitted. -### `RecordAnalysisFacts` / `RecordShapeAnalyzer` +### `RecordStorageFacts` / `RecordStorageAnalyzer` Purpose: -- centralize record shape and representability decisions +- centralize record storage and representability decisions - classify record storage as incomplete, union, typed, or opaque storage - validate direct and embedded flexible-tail patterns -- analyze recursive by-value record shapes +- analyze recursive by-value record storages - carry flexible-tail metadata for struct mapping - carry fallback reasons for opaque-storage diagnostics -This is the central record analysis pass. `struct_mapping` consumes these facts -and performs MojoIR member construction; it should not own recursive record -shape policy. Its internal helpers are split by responsibility: record -prechecks, plain-field checks, embedded-record/flexible-tail checks, and +This is the central Mojo storage-decision pass. `struct_mapping` consumes these +facts and performs MojoIR member construction; it should not own recursive +typed-vs-opaque policy. Its internal helpers are split by responsibility: +record prechecks, plain-field checks, embedded-record/flexible-tail checks, and bitfield checks. ## Stage 3: CIR To MojoIR Mapping @@ -272,14 +272,14 @@ Purpose: Purpose: -- consume `RecordAnalysisFacts` and `RecordLayoutFacts` +- consume `RecordStorageFacts` and `RecordLayoutFacts` - emit opaque declarations for incomplete records - emit byte-storage structs for opaque-storage decisions - map typed plain fields and bitfield groups to MojoIR members - attach flexible-tail metadata computed by record analysis - compute Mojo alignment decorator policy -Struct mapping should not recompute recursive record shape decisions. +Struct mapping should not recompute recursive record storage decisions. Typed-member mapping is intentionally split between plain fields, bitfield storage, and bitfield logical fields so fallback diagnostics stay local to the operation that failed. diff --git a/mojo_bindgen/analysis/__init__.py b/mojo_bindgen/analysis/__init__.py index 79e076d..dd2f8db 100644 --- a/mojo_bindgen/analysis/__init__.py +++ b/mojo_bindgen/analysis/__init__.py @@ -22,13 +22,12 @@ DeclDependencyGraph, build_decl_dependency_graph, ) -from mojo_bindgen.analysis.facts.record_shape import ( - ByValueRecordShape, - RecordAnalysisFacts, - RecordShapeFacts, +from mojo_bindgen.analysis.facts.record_storage import ( + ByValueEmbeddingDecision, + RecordStorageFacts, RecordStorageKind, - analyze_record_shape, - analyze_record_shapes, + analyze_record_storage, + analyze_record_storage_facts, ) from mojo_bindgen.analysis.mojo.const_expr_mapping import ( ConstExprMappingError, @@ -90,9 +89,8 @@ "PolicyInferencePass", "DeclDependencyGraph", "ReferenceValidationError", - "ByValueRecordShape", - "RecordAnalysisFacts", - "RecordShapeFacts", + "ByValueEmbeddingDecision", + "RecordStorageFacts", "RecordStorageKind", "SignatureRecordStubOptions", "SignatureRecordStubPass", @@ -106,8 +104,8 @@ "UnitMappingError", "TypeWalkOptions", "assign_record_policies", - "analyze_record_shape", - "analyze_record_shapes", + "analyze_record_storage", + "analyze_record_storage_facts", "build_analysis_context", "build_decl_dependency_graph", "classify_aliases", diff --git a/mojo_bindgen/analysis/facts/context.py b/mojo_bindgen/analysis/facts/context.py index c1f0fc9..7457245 100644 --- a/mojo_bindgen/analysis/facts/context.py +++ b/mojo_bindgen/analysis/facts/context.py @@ -11,7 +11,10 @@ ) from mojo_bindgen.analysis.facts.indexes import DeclIndexes, build_decl_indexes from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts, analyze_record_layout -from mojo_bindgen.analysis.facts.record_shape import RecordAnalysisFacts, analyze_record_shapes +from mojo_bindgen.analysis.facts.record_storage import ( + RecordStorageFacts, + analyze_record_storage_facts, +) from mojo_bindgen.ir import Const, Enum, Function, GlobalVar, MacroDecl, Struct, Typedef, Unit @@ -30,17 +33,21 @@ class AnalysisContext: dependency_graph: DeclDependencyGraph alias_classification: AliasClassification record_layouts: dict[str, RecordLayoutFacts] - record_shapes: dict[str, RecordAnalysisFacts] + record_storage: dict[str, RecordStorageFacts] def build_analysis_context(unit: Unit) -> AnalysisContext: - """Build shared declaration indexes and record layout facts for ``unit``.""" + """Build shared declaration indexes and analysis facts for ``unit``.""" indexes = build_decl_indexes(unit) + # First compute C ABI geometry: offsets, sizes, alignment, padding, and + # bitfield runs. record_layouts = _build_record_layouts(unit, indexes) dependency_graph = build_decl_dependency_graph(unit) alias_classification = classify_aliases(unit) - record_shapes = analyze_record_shapes(indexes.records_by_decl_id, record_layouts) + # Then decide Mojo storage eligibility using layout facts plus recursive + # record/type context. + record_storage = analyze_record_storage_facts(indexes.records_by_decl_id, record_layouts) return AnalysisContext( unit=unit, records_by_decl_id=indexes.records_by_decl_id, @@ -53,7 +60,7 @@ def build_analysis_context(unit: Unit) -> AnalysisContext: dependency_graph=dependency_graph, alias_classification=alias_classification, record_layouts=record_layouts, - record_shapes=record_shapes, + record_storage=record_storage, ) diff --git a/mojo_bindgen/analysis/facts/record_layout.py b/mojo_bindgen/analysis/facts/record_layout.py index ab8ddba..c6c2730 100644 --- a/mojo_bindgen/analysis/facts/record_layout.py +++ b/mojo_bindgen/analysis/facts/record_layout.py @@ -1,4 +1,9 @@ -"""Pure record layout and representability analysis over CIR.""" +"""C ABI record layout facts for normalized CIR records. + +This module reports physical geometry: completion, size, alignment, field +offsets, bitfield runs, padding spans, and layout problems. It does not decide +whether a record should use typed Mojo fields or opaque byte storage. +""" from __future__ import annotations @@ -59,7 +64,7 @@ def analyze_record_layout( record_map: dict[str, Struct], ) -> RecordLayoutFacts: # Kept in the public signature for callers that pass whole-unit ABI/index - # context. The current layout facts are fully represented on each record. + # context. The current physical layout facts are represented on each record. del target_abi, record_map if not decl.is_complete: diff --git a/mojo_bindgen/analysis/facts/record_shape.py b/mojo_bindgen/analysis/facts/record_storage.py similarity index 62% rename from mojo_bindgen/analysis/facts/record_shape.py rename to mojo_bindgen/analysis/facts/record_storage.py index 6b6300d..35c8187 100644 --- a/mojo_bindgen/analysis/facts/record_shape.py +++ b/mojo_bindgen/analysis/facts/record_storage.py @@ -1,4 +1,9 @@ -"""Central record layout and shape analysis for normalized CIR.""" +"""Mojo storage decisions for normalized CIR records. + +This module consumes :class:`RecordLayoutFacts` plus whole-unit record/type +context. It decides typed-vs-opaque storage, by-value embeddability, and +flexible-tail propagation. It does not recompute C ABI geometry. +""" from __future__ import annotations @@ -30,7 +35,7 @@ class RecordStorageKind(StrEnum): @dataclass(frozen=True) -class ByValueRecordShape: +class ByValueEmbeddingDecision: """Whether a record can be embedded by value in another typed record.""" valid: bool @@ -39,7 +44,7 @@ class ByValueRecordShape: @dataclass(frozen=True) -class RecordAnalysisFacts: +class RecordStorageFacts: """Central per-record facts used by struct mapping and later policy passes.""" decl_id: str @@ -47,41 +52,33 @@ class RecordAnalysisFacts: is_complete: bool layout: RecordLayoutFacts storage_kind: RecordStorageKind - by_value_shape: ByValueRecordShape + by_value_embedding: ByValueEmbeddingDecision flexible_tail: FlexibleTail | None = None diagnostic_notes: tuple[str, ...] = () fallback_reasons: tuple[str, ...] = () @property - def typed_storage_candidate(self) -> bool: + def uses_typed_storage(self) -> bool: return self.storage_kind == RecordStorageKind.TYPED @property - def opaque_storage_required(self) -> bool: + def requires_opaque_storage(self) -> bool: return self.storage_kind == RecordStorageKind.OPAQUE_STORAGE @property def layout_known(self) -> bool: return self.layout.is_complete and not self.layout.layout_problems - @property - def flexible_tail_field_index(self) -> int | None: - if self.flexible_tail is None: - return None - # Kept for compatibility with the first shape-facts API. Prefer - # ``flexible_tail`` for new callers. - return None - -def analyze_record_shapes( +def analyze_record_storage_facts( records_by_decl_id: dict[str, Struct], record_layouts: dict[str, RecordLayoutFacts], *, type_mapper: MapTypePass | None = None, -) -> dict[str, RecordAnalysisFacts]: - """Analyze every CIR record's layout shape and typed-storage eligibility.""" +) -> dict[str, RecordStorageFacts]: + """Analyze every CIR record's Mojo storage eligibility.""" - analyzer = RecordShapeAnalyzer( + analyzer = RecordStorageAnalyzer( records_by_decl_id=records_by_decl_id, record_layouts=record_layouts, type_mapper=type_mapper or MapTypePass(), @@ -91,14 +88,14 @@ def analyze_record_shapes( } -def analyze_record_shape( +def analyze_record_storage( record: Struct, layout: RecordLayoutFacts, *, records_by_decl_id: dict[str, Struct] | None = None, record_layouts: dict[str, RecordLayoutFacts] | None = None, type_mapper: MapTypePass | None = None, -) -> RecordAnalysisFacts: +) -> RecordStorageFacts: """Analyze one CIR record. Direct callers can pass only ``record`` and ``layout`` for isolated tests; @@ -107,7 +104,7 @@ def analyze_record_shape( records = records_by_decl_id or {record.decl_id: record} layouts = record_layouts or {record.decl_id: layout} - return RecordShapeAnalyzer( + return RecordStorageAnalyzer( records_by_decl_id=records, record_layouts=layouts, type_mapper=type_mapper or MapTypePass(), @@ -115,139 +112,153 @@ def analyze_record_shape( @dataclass -class RecordShapeAnalyzer: - """Compute reusable record shape facts for a normalized CIR unit.""" +class RecordStorageAnalyzer: + """Compute reusable record storage facts for a normalized CIR unit.""" records_by_decl_id: dict[str, Struct] record_layouts: dict[str, RecordLayoutFacts] type_mapper: MapTypePass def __post_init__(self) -> None: - self._record_cache: dict[str, RecordAnalysisFacts] = {} - self._by_value_cache: dict[str, ByValueRecordShape] = {} + self._record_cache: dict[str, RecordStorageFacts] = {} + self._by_value_cache: dict[str, ByValueEmbeddingDecision] = {} - def analyze_record(self, record: Struct) -> RecordAnalysisFacts: + def analyze_record(self, record: Struct) -> RecordStorageFacts: cached = self._record_cache.get(record.decl_id) if cached is not None: return cached layout = self._layout(record) if record.is_union: - facts = RecordAnalysisFacts( + facts = RecordStorageFacts( decl_id=record.decl_id, is_union=True, is_complete=record.is_complete, layout=layout, storage_kind=RecordStorageKind.UNION, - by_value_shape=ByValueRecordShape(True), + by_value_embedding=ByValueEmbeddingDecision(True), ) self._record_cache[record.decl_id] = facts return facts if not layout.is_complete: - facts = RecordAnalysisFacts( + facts = RecordStorageFacts( decl_id=record.decl_id, is_union=False, is_complete=False, layout=layout, storage_kind=RecordStorageKind.INCOMPLETE, - by_value_shape=ByValueRecordShape(False, ("referenced definition is incomplete",)), + by_value_embedding=ByValueEmbeddingDecision( + False, ("referenced definition is incomplete",) + ), ) self._record_cache[record.decl_id] = facts return facts if layout.layout_problems: - facts = RecordAnalysisFacts( + facts = RecordStorageFacts( decl_id=record.decl_id, is_union=False, is_complete=True, layout=layout, storage_kind=RecordStorageKind.OPAQUE_STORAGE, - by_value_shape=ByValueRecordShape(False, (layout.layout_problems[0],)), + by_value_embedding=ByValueEmbeddingDecision(False, (layout.layout_problems[0],)), fallback_reasons=layout.layout_problems, ) self._record_cache[record.decl_id] = facts return facts - by_value = self._record_typed_shape_by_value(record.decl_id, active=set()) + by_value = self._record_typed_storage_by_value(record.decl_id, active=set()) storage_kind = ( RecordStorageKind.TYPED if by_value.valid else RecordStorageKind.OPAQUE_STORAGE ) - facts = RecordAnalysisFacts( + facts = RecordStorageFacts( decl_id=record.decl_id, is_union=False, is_complete=True, layout=layout, storage_kind=storage_kind, - by_value_shape=by_value, + by_value_embedding=by_value, flexible_tail=by_value.flexible_tail, fallback_reasons=() if by_value.valid else by_value.detail, ) self._record_cache[record.decl_id] = facts return facts - def _record_typed_shape_by_value( + def _record_typed_storage_by_value( self, decl_id: str, *, active: set[str], - ) -> ByValueRecordShape: + ) -> ByValueEmbeddingDecision: cached = self._by_value_cache.get(decl_id) if cached is not None: return cached - decl, facts, early_shape = self._precheck_by_value_record(decl_id, active) - if early_shape is not None: - return self._cache_by_value(decl_id, early_shape) + decl, facts, early_decision = self._precheck_by_value_record(decl_id, active) + if early_decision is not None: + return self._cache_by_value(decl_id, early_decision) assert decl is not None assert facts is not None active.add(decl_id) try: - field_shape = self._plain_fields_by_value_shape(decl, facts, active=active) - if not field_shape.valid: - return self._cache_by_value(decl_id, field_shape) + field_decision = self._plain_fields_by_value_embedding(decl, facts, active=active) + if not field_decision.valid: + return self._cache_by_value(decl_id, field_decision) - bitfield_shape = self._bitfield_runs_by_value_shape(facts) - if not bitfield_shape.valid: - return self._cache_by_value(decl_id, bitfield_shape) + bitfield_decision = self._bitfield_runs_by_value_embedding(facts) + if not bitfield_decision.valid: + return self._cache_by_value(decl_id, bitfield_decision) finally: active.remove(decl_id) return self._cache_by_value( decl_id, - ByValueRecordShape(True, flexible_tail=field_shape.flexible_tail), + ByValueEmbeddingDecision(True, flexible_tail=field_decision.flexible_tail), ) def _precheck_by_value_record( self, decl_id: str, active: set[str], - ) -> tuple[Struct | None, RecordLayoutFacts | None, ByValueRecordShape | None]: + ) -> tuple[Struct | None, RecordLayoutFacts | None, ByValueEmbeddingDecision | None]: decl = self.records_by_decl_id.get(decl_id) if decl is None: - return None, None, ByValueRecordShape(False, ("referenced definition is unavailable",)) + return ( + None, + None, + ByValueEmbeddingDecision(False, ("referenced definition is unavailable",)), + ) if decl.is_union: - return None, None, ByValueRecordShape(True) + return None, None, ByValueEmbeddingDecision(True) if not decl.is_complete: - return None, None, ByValueRecordShape(False, ("referenced definition is incomplete",)) + return ( + None, + None, + ByValueEmbeddingDecision(False, ("referenced definition is incomplete",)), + ) if decl_id in active: - return None, None, ByValueRecordShape(True) + return None, None, ByValueEmbeddingDecision(True) facts = self._layout(decl) if not facts.is_complete: - return None, None, ByValueRecordShape(False, ("referenced definition is incomplete",)) + return ( + None, + None, + ByValueEmbeddingDecision(False, ("referenced definition is incomplete",)), + ) if facts.layout_problems: - return None, None, ByValueRecordShape(False, (facts.layout_problems[0],)) + return None, None, ByValueEmbeddingDecision(False, (facts.layout_problems[0],)) return decl, facts, None - def _plain_fields_by_value_shape( + def _plain_fields_by_value_embedding( self, decl: Struct, facts: RecordLayoutFacts, *, active: set[str], - ) -> ByValueRecordShape: + ) -> ByValueEmbeddingDecision: flexible_tail: FlexibleTail | None = None for field_fact in facts.plain_fields: field = decl.fields[field_fact.index] @@ -258,46 +269,53 @@ def _plain_fields_by_value_shape( failure_suffix="opaque storage emitted", ) if reason is not None or mapped_type is None: - return ByValueRecordShape(False, ((reason or "field could not be mapped"),)) + return ByValueEmbeddingDecision(False, ((reason or "field could not be mapped"),)) if field.fam_pattern is not None: - tail_shape = _direct_flexible_tail_shape(field, field_fact, mapped_type) - if not tail_shape.valid: - return tail_shape - flexible_tail = tail_shape.flexible_tail + tail_decision = _direct_flexible_tail_decision(field, field_fact, mapped_type) + if not tail_decision.valid: + return tail_decision + flexible_tail = tail_decision.flexible_tail continue - nested_shape = self._nested_record_field_shape(field, field_fact, facts, active=active) - if not nested_shape.valid: - return nested_shape - if nested_shape.flexible_tail is not None: - flexible_tail = nested_shape.flexible_tail - return ByValueRecordShape(True, flexible_tail=flexible_tail) + nested_decision = self._nested_record_field_decision( + field, + field_fact, + facts, + active=active, + ) + if not nested_decision.valid: + return nested_decision + if nested_decision.flexible_tail is not None: + flexible_tail = nested_decision.flexible_tail + return ByValueEmbeddingDecision(True, flexible_tail=flexible_tail) - def _nested_record_field_shape( + def _nested_record_field_decision( self, field, field_fact, facts: RecordLayoutFacts, *, active: set[str], - ) -> ByValueRecordShape: + ) -> ByValueEmbeddingDecision: refs = _embedded_struct_refs(field.type) if not refs: - return ByValueRecordShape(True) + return ByValueEmbeddingDecision(True) - nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]] = [] + nested_tail_decisions: list[tuple[StructRef, ByValueEmbeddingDecision]] = [] for ref in refs: - shape = self._record_typed_shape_by_value(ref.decl_id, active=active) - if not shape.valid: - return _embedded_record_failure(field, field_fact, ref, shape) - if shape.flexible_tail is not None: - nested_tail_shapes.append((ref, shape)) - - if not nested_tail_shapes: - return ByValueRecordShape(True) - return _embedded_flexible_tail_shape(field, field_fact, facts, nested_tail_shapes) - - def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueRecordShape: + decision = self._record_typed_storage_by_value(ref.decl_id, active=active) + if not decision.valid: + return _embedded_record_failure(field, field_fact, ref, decision) + if decision.flexible_tail is not None: + nested_tail_decisions.append((ref, decision)) + + if not nested_tail_decisions: + return ByValueEmbeddingDecision(True) + return _embedded_flexible_tail_decision(field, field_fact, facts, nested_tail_decisions) + + def _bitfield_runs_by_value_embedding( + self, facts: RecordLayoutFacts + ) -> ByValueEmbeddingDecision: for run_layout in facts.bitfield_runs: _, reason = try_map_type( self.type_mapper, @@ -306,7 +324,7 @@ def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueReco failure_suffix="opaque storage emitted", ) if reason is not None: - return ByValueRecordShape(False, (reason,)) + return ByValueEmbeddingDecision(False, (reason,)) for field_layout in run_layout.fields: _, reason = try_map_type( @@ -316,8 +334,8 @@ def _bitfield_runs_by_value_shape(self, facts: RecordLayoutFacts) -> ByValueReco failure_suffix="opaque storage emitted", ) if reason is not None: - return ByValueRecordShape(False, (reason,)) - return ByValueRecordShape(True) + return ByValueEmbeddingDecision(False, (reason,)) + return ByValueEmbeddingDecision(True) def _layout(self, record: Struct) -> RecordLayoutFacts: return self.record_layouts[record.decl_id] @@ -325,10 +343,10 @@ def _layout(self, record: Struct) -> RecordLayoutFacts: def _cache_by_value( self, decl_id: str, - shape: ByValueRecordShape, - ) -> ByValueRecordShape: - self._by_value_cache[decl_id] = shape - return shape + decision: ByValueEmbeddingDecision, + ) -> ByValueEmbeddingDecision: + self._by_value_cache[decl_id] = decision + return decision def _map_flexible_tail_metadata( @@ -356,7 +374,7 @@ def _map_flexible_tail_metadata( ) -def _direct_flexible_tail_shape(field, field_fact, mapped_type) -> ByValueRecordShape: +def _direct_flexible_tail_decision(field, field_fact, mapped_type) -> ByValueEmbeddingDecision: direct_tail, tail_reason = _map_flexible_tail_metadata( field, field_fact.index, @@ -364,19 +382,23 @@ def _direct_flexible_tail_shape(field, field_fact, mapped_type) -> ByValueRecord mapped_type, ) if tail_reason is not None or direct_tail is None: - return ByValueRecordShape(False, ((tail_reason or "flexible tail could not map"),)) - return ByValueRecordShape(True, flexible_tail=direct_tail) + return ByValueEmbeddingDecision(False, ((tail_reason or "flexible tail could not map"),)) + return ByValueEmbeddingDecision(True, flexible_tail=direct_tail) def _embedded_record_failure( field, field_fact, ref: StructRef, - shape: ByValueRecordShape, -) -> ByValueRecordShape: + decision: ByValueEmbeddingDecision, +) -> ByValueEmbeddingDecision: target_name = ref.c_name or ref.name - suffix = shape.detail[0] if shape.detail else "embedded record is not layout-emittable by value" - return ByValueRecordShape( + suffix = ( + decision.detail[0] + if decision.detail + else "embedded record is not layout-emittable by value" + ) + return ByValueEmbeddingDecision( False, ( f"field `{field_display_name(field, field_fact.index)}` embeds struct " @@ -385,22 +407,22 @@ def _embedded_record_failure( ) -def _embedded_flexible_tail_shape( +def _embedded_flexible_tail_decision( field, field_fact, facts: RecordLayoutFacts, - nested_tail_shapes: list[tuple[StructRef, ByValueRecordShape]], -) -> ByValueRecordShape: + nested_tail_decisions: list[tuple[StructRef, ByValueEmbeddingDecision]], +) -> ByValueEmbeddingDecision: direct_ref = _direct_embedded_struct_ref(field.type) - if len(nested_tail_shapes) > 1 or direct_ref is None: + if len(nested_tail_decisions) > 1 or direct_ref is None: return _non_direct_flexible_tail_failure(field, field_fact) - nested_ref, nested_shape = nested_tail_shapes[0] + nested_ref, nested_decision = nested_tail_decisions[0] if nested_ref.decl_id != direct_ref.decl_id: return _non_direct_flexible_tail_failure(field, field_fact) if not _field_is_terminal_in_enclosing_record(facts, field_fact): target_name = nested_ref.c_name or nested_ref.name - return ByValueRecordShape( + return ByValueEmbeddingDecision( False, ( f"field `{field_display_name(field, field_fact.index)}` embeds struct " @@ -409,18 +431,18 @@ def _embedded_flexible_tail_shape( ), ) - assert nested_shape.flexible_tail is not None - return ByValueRecordShape( + assert nested_decision.flexible_tail is not None + return ByValueEmbeddingDecision( True, flexible_tail=replace( - nested_shape.flexible_tail, - byte_offset=field_fact.byte_offset + nested_shape.flexible_tail.byte_offset, + nested_decision.flexible_tail, + byte_offset=field_fact.byte_offset + nested_decision.flexible_tail.byte_offset, ), ) -def _non_direct_flexible_tail_failure(field, field_fact) -> ByValueRecordShape: - return ByValueRecordShape( +def _non_direct_flexible_tail_failure(field, field_fact) -> ByValueEmbeddingDecision: + return ByValueEmbeddingDecision( False, ( f"field `{field_display_name(field, field_fact.index)}` embeds a flexible-tail " @@ -475,16 +497,11 @@ def _embedded_struct_refs(t: Type) -> tuple[StructRef, ...]: ) -# Compatibility aliases for the initial lightweight shape API. -RecordShapeFacts = RecordAnalysisFacts - - __all__ = [ - "ByValueRecordShape", - "RecordAnalysisFacts", - "RecordShapeFacts", - "RecordShapeAnalyzer", + "ByValueEmbeddingDecision", + "RecordStorageFacts", + "RecordStorageAnalyzer", "RecordStorageKind", - "analyze_record_shape", - "analyze_record_shapes", + "analyze_record_storage", + "analyze_record_storage_facts", ] diff --git a/mojo_bindgen/analysis/mojo/struct_mapping.py b/mojo_bindgen/analysis/mojo/struct_mapping.py index d4ae3f7..726f149 100644 --- a/mojo_bindgen/analysis/mojo/struct_mapping.py +++ b/mojo_bindgen/analysis/mojo/struct_mapping.py @@ -6,10 +6,10 @@ from mojo_bindgen.analysis.common import _mojo_align_decorator_ok from mojo_bindgen.analysis.facts.record_layout import RecordLayoutFacts, analyze_record_layout -from mojo_bindgen.analysis.facts.record_shape import ( - RecordAnalysisFacts, +from mojo_bindgen.analysis.facts.record_storage import ( + RecordStorageFacts, RecordStorageKind, - analyze_record_shapes, + analyze_record_storage_facts, ) from mojo_bindgen.analysis.mojo.mapping_support import ( field_display_name, @@ -47,7 +47,7 @@ class StructMappingContext: target_abi: TargetABI type_mapper: MapTypePass record_layouts: dict[str, RecordLayoutFacts] = field(default_factory=dict) - record_shapes: dict[str, RecordAnalysisFacts] = field(default_factory=dict) + record_storage: dict[str, RecordStorageFacts] = field(default_factory=dict) def map_struct(decl: Struct, *, context: StructMappingContext) -> StructDecl: @@ -58,16 +58,16 @@ def map_struct(decl: Struct, *, context: StructMappingContext) -> StructDecl: f"expected non-union Struct declaration, got union {decl.decl_id!r}" ) - shape = _record_shape(decl, context=context) - facts = shape.layout - if shape.storage_kind == RecordStorageKind.INCOMPLETE: + storage = _record_storage(decl, context=context) + facts = storage.layout + if storage.storage_kind == RecordStorageKind.INCOMPLETE: return _incomplete_struct_decl(decl) - if shape.storage_kind == RecordStorageKind.OPAQUE_STORAGE: + if storage.storage_kind == RecordStorageKind.OPAQUE_STORAGE: return _opaque_storage_struct_decl( decl, facts, - diagnostic_notes=shape.diagnostic_notes, - fallback_reasons=shape.fallback_reasons, + diagnostic_notes=storage.diagnostic_notes, + fallback_reasons=storage.fallback_reasons, ) plain_fields, mapped_bitfield_groups, diagnostic_notes, fallback_reasons = _map_typed_members( @@ -97,7 +97,7 @@ def map_struct(decl: Struct, *, context: StructMappingContext) -> StructDecl: mapped_bitfield_groups, uses_opaque_storage=False, ), - flexible_tail=shape.flexible_tail, + flexible_tail=storage.flexible_tail, diagnostics=_build_diagnostics( facts.layout_problems, diagnostic_notes=diagnostic_notes, @@ -253,12 +253,12 @@ def _map_bitfield_fields( return mapped_fields, tuple(fallback_reasons) -def _record_shape( +def _record_storage( decl: Struct, *, context: StructMappingContext, -) -> RecordAnalysisFacts: - cached = context.record_shapes.get(decl.decl_id) +) -> RecordStorageFacts: + cached = context.record_storage.get(decl.decl_id) if cached is not None: return cached @@ -271,12 +271,12 @@ def _record_shape( target_abi=context.target_abi, ) context.record_layouts = layouts - context.record_shapes = analyze_record_shapes( + context.record_storage = analyze_record_storage_facts( context.record_map, layouts, type_mapper=context.type_mapper, ) - return context.record_shapes[decl.decl_id] + return context.record_storage[decl.decl_id] def _build_members( diff --git a/mojo_bindgen/analysis/mojo/unit_mapping.py b/mojo_bindgen/analysis/mojo/unit_mapping.py index 04707fa..52048e0 100644 --- a/mojo_bindgen/analysis/mojo/unit_mapping.py +++ b/mojo_bindgen/analysis/mojo/unit_mapping.py @@ -54,7 +54,7 @@ def run(self, unit: Unit) -> MojoModule: struct_context=StructMappingContext( record_map=context.records_by_decl_id, record_layouts=context.record_layouts, - record_shapes=context.record_shapes, + record_storage=context.record_storage, target_abi=unit.target_abi, type_mapper=type_mapper, ), diff --git a/tests/unit/test_analysis_context.py b/tests/unit/test_analysis_context.py index 6f43dfa..6874847 100644 --- a/tests/unit/test_analysis_context.py +++ b/tests/unit/test_analysis_context.py @@ -102,7 +102,7 @@ def test_analysis_context_classifies_aliases_and_builds_dependency_graph() -> No assert context.dependency_graph.edges_by_decl_id[function.decl_id] == frozenset( {payload.decl_id, external_ref.decl_id} ) - assert context.record_shapes[payload.decl_id].typed_storage_candidate is True + assert context.record_storage[payload.decl_id].uses_typed_storage is True def test_validate_references_rejects_missing_enum_refs() -> None: diff --git a/tests/unit/test_analysis_record_shape.py b/tests/unit/test_analysis_record_storage.py similarity index 85% rename from tests/unit/test_analysis_record_shape.py rename to tests/unit/test_analysis_record_storage.py index fb47b2a..eb3bf80 100644 --- a/tests/unit/test_analysis_record_shape.py +++ b/tests/unit/test_analysis_record_storage.py @@ -1,10 +1,10 @@ from __future__ import annotations from mojo_bindgen.analysis.facts.record_layout import analyze_record_layout -from mojo_bindgen.analysis.facts.record_shape import ( +from mojo_bindgen.analysis.facts.record_storage import ( RecordStorageKind, - analyze_record_shape, - analyze_record_shapes, + analyze_record_storage, + analyze_record_storage_facts, ) from mojo_bindgen.ir import ( Array, @@ -41,7 +41,7 @@ def _layout(record: Struct, records: dict[str, Struct] | None = None): return analyze_record_layout(record, record_map=record_map, target_abi=_abi()) -def test_record_shape_marks_incomplete_records_as_incomplete_storage() -> None: +def test_record_storage_marks_incomplete_records_as_incomplete_storage() -> None: opaque = Struct( decl_id="struct:Opaque", name="Opaque", @@ -52,13 +52,13 @@ def test_record_shape_marks_incomplete_records_as_incomplete_storage() -> None: is_complete=False, ) - facts = analyze_record_shape(opaque, _layout(opaque)) + facts = analyze_record_storage(opaque, _layout(opaque)) assert facts.storage_kind == RecordStorageKind.INCOMPLETE - assert facts.typed_storage_candidate is False + assert facts.uses_typed_storage is False -def test_record_shape_marks_layout_problems_as_opaque_storage() -> None: +def test_record_storage_marks_layout_problems_as_opaque_storage() -> None: broken = Struct( decl_id="struct:Broken", name="Broken", @@ -69,13 +69,13 @@ def test_record_shape_marks_layout_problems_as_opaque_storage() -> None: is_complete=True, ) - facts = analyze_record_shape(broken, _layout(broken)) + facts = analyze_record_storage(broken, _layout(broken)) assert facts.storage_kind == RecordStorageKind.OPAQUE_STORAGE assert facts.fallback_reasons -def test_record_shape_extracts_direct_flexible_tail_metadata() -> None: +def test_record_storage_extracts_direct_flexible_tail_metadata() -> None: packet = Struct( decl_id="struct:Packet", name="Packet", @@ -102,7 +102,7 @@ def test_record_shape_extracts_direct_flexible_tail_metadata() -> None: is_complete=True, ) - facts = analyze_record_shape(packet, _layout(packet)) + facts = analyze_record_storage(packet, _layout(packet)) assert facts.storage_kind == RecordStorageKind.TYPED assert facts.flexible_tail is not None @@ -110,7 +110,7 @@ def test_record_shape_extracts_direct_flexible_tail_metadata() -> None: assert facts.flexible_tail.element_type == BuiltinType(MojoBuiltin.C_UCHAR) -def test_record_shape_rejects_non_terminal_embedded_flexible_tail() -> None: +def test_record_storage_rejects_non_terminal_embedded_flexible_tail() -> None: tail = Struct( decl_id="struct:Tail", name="Tail", @@ -162,7 +162,7 @@ def test_record_shape_rejects_non_terminal_embedded_flexible_tail() -> None: records = {tail.decl_id: tail, holder.decl_id: holder} layouts = {decl_id: _layout(record, records) for decl_id, record in records.items()} - facts = analyze_record_shapes(records, layouts)[holder.decl_id] + facts = analyze_record_storage_facts(records, layouts)[holder.decl_id] assert facts.storage_kind == RecordStorageKind.OPAQUE_STORAGE assert "embedded flexible tail is not terminal" in facts.fallback_reasons[0]