mojo-bindgen 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. mojo_bindgen/__init__.py +20 -0
  2. mojo_bindgen/cli.py +138 -0
  3. mojo_bindgen/codegen/__init__.py +15 -0
  4. mojo_bindgen/codegen/_struct_order.py +108 -0
  5. mojo_bindgen/codegen/generator.py +48 -0
  6. mojo_bindgen/codegen/mojo_emit_options.py +40 -0
  7. mojo_bindgen/codegen/mojo_mapper.py +635 -0
  8. mojo_bindgen/codegen/render.py +628 -0
  9. mojo_bindgen/ir.py +750 -0
  10. mojo_bindgen/parsing/__init__.py +9 -0
  11. mojo_bindgen/parsing/diagnostics.py +65 -0
  12. mojo_bindgen/parsing/frontend.py +233 -0
  13. mojo_bindgen/parsing/lowering/__init__.py +34 -0
  14. mojo_bindgen/parsing/lowering/const_expr.py +494 -0
  15. mojo_bindgen/parsing/lowering/decl_lowering.py +200 -0
  16. mojo_bindgen/parsing/lowering/literal_resolver.py +126 -0
  17. mojo_bindgen/parsing/lowering/macro_env.py +92 -0
  18. mojo_bindgen/parsing/lowering/primitive.py +118 -0
  19. mojo_bindgen/parsing/lowering/record_lowering.py +325 -0
  20. mojo_bindgen/parsing/lowering/type_lowering.py +301 -0
  21. mojo_bindgen/parsing/parser.py +183 -0
  22. mojo_bindgen/parsing/registry.py +259 -0
  23. mojo_bindgen/passes/__init__.py +52 -0
  24. mojo_bindgen/passes/analyze_for_mojo.py +957 -0
  25. mojo_bindgen/passes/pipeline.py +14 -0
  26. mojo_bindgen/passes/reachability.py +217 -0
  27. mojo_bindgen/passes/validate_ir.py +99 -0
  28. mojo_bindgen/py.typed +0 -0
  29. mojo_bindgen/serde.py +213 -0
  30. mojo_bindgen/utils.py +38 -0
  31. mojo_bindgen-0.1.0.dist-info/METADATA +189 -0
  32. mojo_bindgen-0.1.0.dist-info/RECORD +35 -0
  33. mojo_bindgen-0.1.0.dist-info/WHEEL +4 -0
  34. mojo_bindgen-0.1.0.dist-info/entry_points.txt +2 -0
  35. mojo_bindgen-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,20 @@
1
+ """C header → Mojo FFI bindings via libclang."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+
7
+ from mojo_bindgen.codegen import FFIScalarStyle, MojoEmitOptions, MojoGenerator, generate_mojo
8
+
9
+ try:
10
+ __version__ = version("mojo-bindgen")
11
+ except PackageNotFoundError:
12
+ __version__ = "0.1.0"
13
+
14
+ __all__ = [
15
+ "FFIScalarStyle",
16
+ "MojoEmitOptions",
17
+ "MojoGenerator",
18
+ "__version__",
19
+ "generate_mojo",
20
+ ]
mojo_bindgen/cli.py ADDED
@@ -0,0 +1,138 @@
1
+ """Command-line interface for mojo-bindgen."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+ from typing import Annotated, Literal
8
+
9
+ import typer
10
+ from rich.console import Console
11
+
12
+ from mojo_bindgen.codegen.generator import MojoGenerator
13
+ from mojo_bindgen.codegen.mojo_emit_options import MojoEmitOptions
14
+ from mojo_bindgen.parsing.parser import ClangParser, ParseError
15
+
16
+ stderr_console = Console(stderr=True)
17
+
18
+
19
+ def run(
20
+ header: Annotated[Path, typer.Argument(help="Path to the primary C header file")],
21
+ output: Annotated[
22
+ Path | None,
23
+ typer.Option(
24
+ "--output",
25
+ "-o",
26
+ help="Write output to this file (default: stdout).",
27
+ show_default=False,
28
+ ),
29
+ ] = None,
30
+ library: Annotated[
31
+ str | None,
32
+ typer.Option(
33
+ "--library",
34
+ metavar="NAME",
35
+ help="Logical library name in the IR (default: stem of the header path).",
36
+ ),
37
+ ] = None,
38
+ link_name: Annotated[
39
+ str | None,
40
+ typer.Option(
41
+ "--link-name",
42
+ metavar="NAME",
43
+ help="Shared-library link name (default: same as --library).",
44
+ ),
45
+ ] = None,
46
+ compile_arg: Annotated[
47
+ list[str] | None,
48
+ typer.Option(
49
+ "--compile-arg",
50
+ metavar="FLAG",
51
+ help="Extra clang flag (repeatable). If omitted, use built-in system include probing.",
52
+ ),
53
+ ] = None,
54
+ json_output: Annotated[
55
+ bool,
56
+ typer.Option(
57
+ "--json",
58
+ help="Emit IR as JSON instead of Mojo source.",
59
+ ),
60
+ ] = False,
61
+ linking: Annotated[
62
+ Literal["external_call", "owned_dl_handle"],
63
+ typer.Option(
64
+ "--linking",
65
+ help="FFI linking mode for Mojo output (ignored with --json).",
66
+ case_sensitive=False,
67
+ ),
68
+ ] = "external_call",
69
+ library_path_hint: Annotated[
70
+ str | None,
71
+ typer.Option(
72
+ "--library-path-hint",
73
+ metavar="PATH",
74
+ help="Path hint for OwnedDLHandle when --linking owned_dl_handle.",
75
+ ),
76
+ ] = None,
77
+ ) -> int:
78
+ """Generate Mojo FFI from a C header using libclang.
79
+
80
+ Examples:
81
+ mojo-bindgen path/to/header.h -o bindings.mojo
82
+ mojo-bindgen path/to/header.h --json -o unit.json
83
+ mojo-bindgen include/me.h --compile-arg=-I./include -o out.mojo
84
+ """
85
+ stem = header.stem if header.suffix else str(header)
86
+ library_name = library if library is not None else stem
87
+ link_name_value = link_name if link_name is not None else library_name
88
+ compile_args = compile_arg
89
+
90
+ try:
91
+ parser = ClangParser(
92
+ header,
93
+ library=library_name,
94
+ link_name=link_name_value,
95
+ compile_args=compile_args,
96
+ raise_on_error=True,
97
+ )
98
+ unit = parser.run()
99
+ except (ParseError, FileNotFoundError, OSError) as e:
100
+ stderr_console.print(f"[bold red]mojo-bindgen error:[/bold red] {e}")
101
+ raise typer.Exit(code=1) from e
102
+
103
+ if json_output:
104
+ text = unit.to_json()
105
+ else:
106
+ opts = MojoEmitOptions(
107
+ linking=linking,
108
+ library_path_hint=library_path_hint,
109
+ )
110
+ text = MojoGenerator(opts).generate(unit)
111
+
112
+ if output is None:
113
+ sys.stdout.write(text)
114
+ if not text.endswith("\n"):
115
+ sys.stdout.write("\n")
116
+ else:
117
+ output.write_text(text, encoding="utf-8")
118
+ return 0
119
+
120
+
121
+ def main(argv: list[str] | None = None) -> int:
122
+ cli_args = argv if argv is not None else sys.argv[1:]
123
+ previous_argv = sys.argv[:]
124
+ sys.argv = ["mojo-bindgen", *cli_args]
125
+ try:
126
+ typer.run(run)
127
+ return 0
128
+ except typer.Exit as e:
129
+ return e.exit_code
130
+ except SystemExit as e:
131
+ code = e.code
132
+ return code if isinstance(code, int) else 0
133
+ finally:
134
+ sys.argv = previous_argv
135
+
136
+
137
+ if __name__ == "__main__":
138
+ raise SystemExit(main())
@@ -0,0 +1,15 @@
1
+ """Public Mojo code generation API.
2
+
3
+ Import from this package when you want the stable, higher-level codegen
4
+ surface rather than the individual implementation modules.
5
+ """
6
+
7
+ from mojo_bindgen.codegen.generator import MojoGenerator, generate_mojo
8
+ from mojo_bindgen.codegen.mojo_emit_options import FFIScalarStyle, MojoEmitOptions
9
+
10
+ __all__ = [
11
+ "FFIScalarStyle",
12
+ "MojoGenerator",
13
+ "MojoEmitOptions",
14
+ "generate_mojo",
15
+ ]
@@ -0,0 +1,108 @@
1
+ """Value-embedding struct dependency ordering for code generation.
2
+
3
+ These helpers compute an emission order for structs that embed other structs
4
+ by value, so referenced layouts appear before the records that depend on them.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections import defaultdict, deque
10
+
11
+ from mojo_bindgen.codegen.mojo_mapper import mojo_ident
12
+ from mojo_bindgen.ir import (
13
+ Array,
14
+ AtomicType,
15
+ EnumRef,
16
+ QualifiedType,
17
+ Struct,
18
+ StructRef,
19
+ Type,
20
+ TypeRef,
21
+ )
22
+
23
+
24
+ def struct_dependency_edges(s: Struct) -> list[tuple[str, str]]:
25
+ """Return (successor, predecessor) pairs: `succ` depends on `pred` (emit `pred` first).
26
+
27
+ Only **by-value** struct references create ordering edges. Pointer and function-pointer
28
+ fields do not (C allows pointers to incomplete types); nested struct layout is still
29
+ captured via ``Array``/``StructRef`` for value-embedded arrays of structs.
30
+ """
31
+ me = mojo_ident(s.name.strip() or s.c_name.strip())
32
+ edges: list[tuple[str, str]] = []
33
+
34
+ def walk(ty: Type) -> None:
35
+ """Collect by-value struct dependencies reachable from ``ty``."""
36
+ if isinstance(ty, TypeRef):
37
+ walk(ty.canonical)
38
+ return
39
+ if isinstance(ty, QualifiedType):
40
+ walk(ty.unqualified)
41
+ return
42
+ if isinstance(ty, AtomicType):
43
+ walk(ty.value_type)
44
+ return
45
+ if isinstance(ty, EnumRef):
46
+ return
47
+ if isinstance(ty, StructRef):
48
+ if ty.is_union:
49
+ return
50
+ pred = mojo_ident(ty.name.strip())
51
+ if pred != me:
52
+ edges.append((me, pred))
53
+ return
54
+ if isinstance(ty, Array):
55
+ walk(ty.element)
56
+
57
+ for f in s.fields:
58
+ walk(f.type)
59
+ return edges
60
+
61
+
62
+ def toposort_structs(structs: list[Struct]) -> list[Struct]:
63
+ """Return structs in an order suitable for emission.
64
+
65
+ Value-embedded :class:`~mojo_bindgen.ir.StructRef` dependencies are emitted
66
+ before the records that use them. Cycles fall back to the original input
67
+ order once the acyclic portion of the graph is exhausted.
68
+ """
69
+ if not structs:
70
+ return []
71
+
72
+ def name_of(s: Struct) -> str:
73
+ """Return the sanitized emitted name for ``s``."""
74
+ return mojo_ident(s.name.strip() or s.c_name.strip())
75
+
76
+ names = [name_of(s) for s in structs]
77
+ known = set(names)
78
+ name_to_struct = {name_of(s): s for s in structs}
79
+
80
+ # graph[pred] = successors that must appear after pred
81
+ graph: dict[str, set[str]] = defaultdict(set)
82
+ indegree: dict[str, int] = {n: 0 for n in names}
83
+
84
+ for s in structs:
85
+ succ = name_of(s)
86
+ for succ2, pred in struct_dependency_edges(s):
87
+ if succ2 != succ:
88
+ continue
89
+ if pred in known and pred != succ:
90
+ if succ not in graph[pred]:
91
+ graph[pred].add(succ)
92
+ indegree[succ] += 1
93
+
94
+ q = deque(sorted([n for n in names if indegree[n] == 0], key=lambda x: names.index(x)))
95
+ order: list[str] = []
96
+ while q:
97
+ n = q.popleft()
98
+ order.append(n)
99
+ for succ in graph.get(n, ()):
100
+ indegree[succ] -= 1
101
+ if indegree[succ] == 0:
102
+ q.append(succ)
103
+
104
+ for n in names:
105
+ if n not in order:
106
+ order.append(n)
107
+
108
+ return [name_to_struct[n] for n in order]
@@ -0,0 +1,48 @@
1
+ """Public orchestration API for Mojo code generation.
2
+
3
+ Higher-level callers should use :class:`MojoGenerator` rather than stitching
4
+ analysis and rendering together manually.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from mojo_bindgen.codegen.mojo_emit_options import MojoEmitOptions
10
+ from mojo_bindgen.codegen.render import MojoRenderer
11
+ from mojo_bindgen.ir import Unit
12
+ from mojo_bindgen.passes import AnalyzeForMojoPass, run_ir_passes
13
+ from mojo_bindgen.passes.analyze_for_mojo import AnalyzedUnit
14
+
15
+
16
+ class MojoGenerator:
17
+ """Orchestrate analysis and rendering for Mojo bindings.
18
+
19
+ The generator owns a stable codegen interface for callers that want either
20
+ one-shot generation or explicit access to intermediate analysis results.
21
+ """
22
+
23
+ def __init__(self, options: MojoEmitOptions | None = None) -> None:
24
+ """Store the codegen options used for all subsequent operations."""
25
+ self._options = options or MojoEmitOptions()
26
+
27
+ @property
28
+ def options(self) -> MojoEmitOptions:
29
+ """Return the options currently bound to this generator."""
30
+ return self._options
31
+
32
+ def analyze(self, unit: Unit) -> AnalyzedUnit:
33
+ """Run semantic codegen analysis over ``unit``."""
34
+ normalized = run_ir_passes(unit)
35
+ return AnalyzeForMojoPass(self._options).run(normalized)
36
+
37
+ def render(self, analyzed: AnalyzedUnit) -> str:
38
+ """Render previously analyzed codegen state to Mojo source."""
39
+ return MojoRenderer(analyzed).render()
40
+
41
+ def generate(self, unit: Unit) -> str:
42
+ """Analyze and render ``unit`` in one step."""
43
+ return self.render(self.analyze(unit))
44
+
45
+
46
+ def generate_mojo(unit: Unit, options: MojoEmitOptions | None = None) -> str:
47
+ """Generate a Mojo module from parsed IR in one convenience call."""
48
+ return MojoGenerator(options).generate(unit)
@@ -0,0 +1,40 @@
1
+ """Options that configure Mojo code generation and rendering."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Literal
7
+
8
+ from mojo_bindgen.codegen.mojo_mapper import FFIOriginStyle, FFIScalarStyle
9
+
10
+ LinkingMode = Literal["external_call", "owned_dl_handle"]
11
+ """Supported linking strategies for generated wrappers."""
12
+
13
+
14
+ @dataclass
15
+ class MojoEmitOptions:
16
+ """Controls Mojo code generation and rendering behavior.
17
+
18
+ The options are shared by analysis and rendering. They shape link strategy,
19
+ pointer provenance, and generated comments.
20
+ """
21
+
22
+ linking: LinkingMode = "external_call"
23
+ """external_call: link C symbols at mojo build time; emitted wrappers use ``abi("C")``.
24
+ owned_dl_handle: resolve via ``OwnedDLHandle.call`` (raises); wrappers omit ``abi("C")`` on
25
+ the ``def`` line because ``abi("C")`` combined with ``raises`` currently fails LLVM lowering."""
26
+
27
+ library_path_hint: str | None = None
28
+ """If set with owned_dl_handle, pass this path to OwnedDLHandle(...). If None, use DEFAULT_RTLD (symbols must be linked into the process)."""
29
+
30
+ module_comment: bool = True
31
+ """Emit a leading comment with source header and library metadata."""
32
+
33
+ warn_abi: bool = True
34
+ """Emit comments reminding that packed/aligned layouts need verification."""
35
+
36
+ ffi_origin: FFIOriginStyle = "external"
37
+ """Pointer provenance for mapped types: ``external`` → Mut/Immut*ExternalOrigin (recommended for C FFI); ``any`` → *AnyOrigin."""
38
+
39
+ ffi_scalar_style: FFIScalarStyle = "std_ffi_aliases"
40
+ """``std_ffi_aliases`` (default): ``c_int``, ``c_long``, … from ``std.ffi``. ``fixed_width``: ``Int32`` / ``Int64`` / … from Clang sizes."""