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.
- mojo_bindgen/__init__.py +20 -0
- mojo_bindgen/cli.py +138 -0
- mojo_bindgen/codegen/__init__.py +15 -0
- mojo_bindgen/codegen/_struct_order.py +108 -0
- mojo_bindgen/codegen/generator.py +48 -0
- mojo_bindgen/codegen/mojo_emit_options.py +40 -0
- mojo_bindgen/codegen/mojo_mapper.py +635 -0
- mojo_bindgen/codegen/render.py +628 -0
- mojo_bindgen/ir.py +750 -0
- mojo_bindgen/parsing/__init__.py +9 -0
- mojo_bindgen/parsing/diagnostics.py +65 -0
- mojo_bindgen/parsing/frontend.py +233 -0
- mojo_bindgen/parsing/lowering/__init__.py +34 -0
- mojo_bindgen/parsing/lowering/const_expr.py +494 -0
- mojo_bindgen/parsing/lowering/decl_lowering.py +200 -0
- mojo_bindgen/parsing/lowering/literal_resolver.py +126 -0
- mojo_bindgen/parsing/lowering/macro_env.py +92 -0
- mojo_bindgen/parsing/lowering/primitive.py +118 -0
- mojo_bindgen/parsing/lowering/record_lowering.py +325 -0
- mojo_bindgen/parsing/lowering/type_lowering.py +301 -0
- mojo_bindgen/parsing/parser.py +183 -0
- mojo_bindgen/parsing/registry.py +259 -0
- mojo_bindgen/passes/__init__.py +52 -0
- mojo_bindgen/passes/analyze_for_mojo.py +957 -0
- mojo_bindgen/passes/pipeline.py +14 -0
- mojo_bindgen/passes/reachability.py +217 -0
- mojo_bindgen/passes/validate_ir.py +99 -0
- mojo_bindgen/py.typed +0 -0
- mojo_bindgen/serde.py +213 -0
- mojo_bindgen/utils.py +38 -0
- mojo_bindgen-0.1.0.dist-info/METADATA +189 -0
- mojo_bindgen-0.1.0.dist-info/RECORD +35 -0
- mojo_bindgen-0.1.0.dist-info/WHEEL +4 -0
- mojo_bindgen-0.1.0.dist-info/entry_points.txt +2 -0
- mojo_bindgen-0.1.0.dist-info/licenses/LICENSE +21 -0
mojo_bindgen/__init__.py
ADDED
|
@@ -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."""
|