Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion INTERNALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,13 @@ A `Lowerer.native` flag, set by `lowering::lower_native_typed` (single file, cal
spells data class fields that name a user type (`"Expr"`), and rewrites each top-level
`PyStmt::FuncDef` whose binding type is known into a `PyStmt::TypedFuncDef` using
`py_type_annotation` over `Lowerer.binding_types` (span of the binding target → `types::Ty`, from
`TypeSpan::raw`). An unspellable type leaves its position unannotated.
`TypeSpan::raw`). An unspellable type leaves its position unannotated. The same pass annotates
module-level values, which mypyc cannot type when another module reads them: a nullary singleton
`_X: X = X()`, and the first assignment of a top-level value bound once.
- **Discarded matches.** `Lowerer::push_discarded` emits a statement-position expression. In
native mode, when its value is a temp set by an `if` ladder, `discard_temp` turns each assignment
of the temp into a bare evaluation, since mypyc rejects a local whose every assignment is a unit
call.
- **Build.** `src/native_build.rs` writes the native output to a directory, runs `python -m mypyc
--allow-redefinition-new --local-partial-types` over every module but `_pyfun_rt.py`, removes the
sources and writes `__main__.py`. `bench/mypyc_run.py` and `.github/workflows/native-bench.yml`
Expand Down
10 changes: 6 additions & 4 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,10 +515,12 @@ map_build 1.64× vs hand-written.
**The real-program audit (the Scrabble game, locally):** its native output passes plain mypy with
the build's flags (64 errors down to 0 after the shared runtime's fields became `Any`, unit
returns went bare, and three boundary error codes were disabled), and every replay prints the same.
mypyc itself still stops on **"Cannot determine type" for module-level values another module reads
at import time** (nullary singletons like `board._QW`, layout tables): that is the next native
blocker. **Remaining:** that, or-patterns whose alternatives bind names, and the closure/`seq`
audit under mypyc.
mypyc's front end now accepts the whole game too: module-level values are annotated (another
module's reads were "Cannot determine type"), and a unit match in statement position runs its arms
in place (a temp assigned only unit calls was "inferred type None"). It stops only at `build_ext`
on a machine without a C compiler, so the compiled game hasn't been timed. **Remaining:** a
compiled run of a real program on a machine with a C toolchain, or-patterns whose alternatives
bind names, and the closure/`seq` audit under mypyc.
The original reasoning, kept for the record:
the checker knows every binding's inferred type, so the emitter could produce fully annotated
Python whose annotations cannot lie, then compile it with mypyc into a C extension — native speed
Expand Down
113 changes: 105 additions & 8 deletions src/lowering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -947,6 +947,55 @@ impl Lowerer {
}
}
}
// Module-level values another module reads need their type written down:
// mypyc cannot infer it across modules ("Cannot determine type"). Each
// nullary singleton names its class, and a top-level value binding bound
// once gets its inferred type when Python can spell it.
let mut value_types: HashMap<String, String> = HashMap::new();
let mut bound_count: HashMap<String, usize> = HashMap::new();
for item in &module.items {
if let Item::Let(b) = item {
for name in b.bound_names() {
*bound_count.entry(py_value_name(&name)).or_default() += 1;
}
if b.params.is_empty()
&& let Some(name) = b.name()
&& let Some(ty) = self.binding_types.get(&b.target_span.span())
&& let Some(ann) = py_type_annotation(ty, &spell)
{
value_types.insert(py_value_name(name), ann);
}
}
}
// Only a name's first assignment carries the annotation (a top-level
// `let mut` is reassigned by `<-`).
let mut annotated: HashSet<String> = HashSet::new();
for stmt in &mut py.body {
let PyStmt::Assign { target, value } = stmt else {
continue;
};
if !annotated.insert(target.clone()) {
continue;
}
let singleton_class = match value {
PyExpr::Call { func, args } if args.is_empty() => match func.as_ref() {
PyExpr::Name(class)
if class_names.contains(class) && *target == format!("_{class}") =>
{
Some(class.clone())
}
_ => None,
},
_ => None,
};
if let Some(class) = singleton_class {
*target = format!("{target}: {class}");
} else if bound_count.get(target.as_str()) == Some(&1)
&& let Some(ann) = value_types.get(target.as_str())
{
*target = format!("{target}: {ann}");
}
}
// The top-level function bindings and their types.
let mut fn_types: HashMap<String, (usize, crate::types::Ty)> = HashMap::new();
for item in &module.items {
Expand Down Expand Up @@ -2067,11 +2116,8 @@ impl Lowerer {
}
BlockStmt::Expr(e) if i == last => out.extend(self.lower_return(e, &locals)?),
BlockStmt::Expr(e) => {
let (mut s, v) = self.lower_value(e, &locals)?;
out.append(&mut s);
if !matches!(v, PyExpr::NoneLit) {
out.push(PyStmt::Expr(v));
}
let (s, v) = self.lower_value(e, &locals)?;
self.push_discarded(&mut out, s, v);
}
}
}
Expand Down Expand Up @@ -2438,11 +2484,11 @@ impl Lowerer {
}
BlockStmt::Expr(e) => {
let (mut s, v) = self.lower_value(e, &locals)?;
out.append(&mut s);
if i == last {
out.append(&mut s);
value = v;
} else if !matches!(v, PyExpr::NoneLit) {
out.push(PyStmt::Expr(v));
} else {
self.push_discarded(&mut out, s, v);
}
}
}
Expand Down Expand Up @@ -5569,6 +5615,26 @@ impl Lowerer {
///
/// The checker has proved the unguarded arms cover the scrutinee's type, so
/// the last arm, when unguarded, is a plain `else`.
/// Emit a statement-position expression whose value is dropped. In native
/// mode a value match hoisted into a temp that nothing reads evaluates its
/// arms in place instead: mypyc rejects a local whose every assignment is a
/// unit call ("has inferred type None").
fn push_discarded(&self, out: &mut Vec<PyStmt>, mut stmts: Vec<PyStmt>, value: PyExpr) {
if self.native
&& let PyExpr::Name(tmp) = &value
&& tmp.starts_with("_pf_t")
&& let Some(PyStmt::If { .. }) = stmts.last()
{
discard_temp(&mut stmts, tmp);
out.append(&mut stmts);
return;
}
out.append(&mut stmts);
if !matches!(value, PyExpr::NoneLit) {
out.push(PyStmt::Expr(value));
}
}

fn try_lower_native_match(
&mut self,
scrutinee: &Expr,
Expand Down Expand Up @@ -11092,6 +11158,37 @@ fn py_param_names(names: &[String]) -> Vec<String> {
}

/// Append `target = value` to a (possibly empty) statement list.
/// Turn every assignment of `tmp` in an `if` ladder (as built by
/// `try_lower_native_match`) into a bare evaluation of its value, dropping it
/// when the value is a bare name (evaluating one does nothing).
fn discard_temp(stmts: &mut Vec<PyStmt>, tmp: &str) {
let mut i = 0;
while i < stmts.len() {
match &mut stmts[i] {
PyStmt::If { body, orelse, .. } => {
discard_temp(body, tmp);
discard_temp(orelse, tmp);
}
PyStmt::Assign { target, value } if target == tmp => {
let value = std::mem::replace(value, PyExpr::NoneLit);
match value {
PyExpr::NoneLit => {
stmts.remove(i);
continue;
}
PyExpr::Name(_) => {
stmts.remove(i);
continue;
}
other => stmts[i] = PyStmt::Expr(other),
}
}
_ => {}
}
i += 1;
}
}

fn with_assign(mut stmts: Vec<PyStmt>, target: &str, value: PyExpr) -> Vec<PyStmt> {
stmts.push(PyStmt::Assign {
target: target.to_string(),
Expand Down
34 changes: 34 additions & 0 deletions tests/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4016,6 +4016,40 @@ fn native_annotates_top_level_functions_it_can_spell() {
assert_eq!(out, ["4.0", "4", "['A']", "Some(1)", "2"]);
}

#[test]
fn native_annotates_module_level_values_for_other_modules() {
// mypyc cannot infer a module-level value's type from another module, so
// native mode writes singletons' and top-level values' types down.
let src = "type Cell = Plain | Double\n\
let layout = [Plain, Double]\n\
let size = List.len layout\n\
print size";
let Some((py, out)) = run_native(src) else {
return;
};
assert!(py.contains("_Double: Double = Double()"), "{py}");
assert!(py.contains("layout: list[Cell] = "), "{py}");
assert!(py.contains("size: int = "), "{py}");
assert_eq!(out, ["2"]);
}

#[test]
fn native_evaluates_a_discarded_match_in_place() {
// A unit match in statement position runs its arms directly: a temp
// assigned only unit calls is one mypyc rejects ("inferred type None").
let src = "let report o =\n \
match o:\n \
case Some n: print n\n \
case None: print \"none\"\n \
print \"done\"\n\
report (Some 1)";
let Some((py, out)) = run_native(src) else {
return;
};
assert!(!py.contains("_pf_t"), "{py}");
assert_eq!(out, ["1", "done"]);
}

#[test]
fn native_annotates_block_local_functions_and_leaves_unit_returns_bare() {
let src = "type Shape = Circle float | Square float\n\
Expand Down
Loading