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
12 changes: 7 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,11 +515,13 @@ 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'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
mypyc now compiles the whole game: module-level values are annotated (another module's reads were
"Cannot determine type"; a generalized value like `Map.empty` spells its type variables `Any`),
and a unit match in statement position runs its arms in place (a temp assigned only unit calls was
"inferred type None"). Compiled on Windows (MSVC, CPython 3.14), every replay prints the same, and
its move-generation bench runs about **1.5x** faster than plain Python (generation up to 2x on
the heaviest position). The replays themselves gain only 1.1–1.2x, since loading the dictionary
takes most of each run. **Remaining:** 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
Expand Down
38 changes: 35 additions & 3 deletions src/lowering/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -950,7 +950,8 @@ 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.
// once gets its inferred type when Python can spell it, with `Any` for a
// type variable.
let mut value_types: HashMap<String, String> = HashMap::new();
let mut bound_count: HashMap<String, usize> = HashMap::new();
for item in &module.items {
Expand All @@ -961,7 +962,7 @@ impl Lowerer {
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)
&& let Some(ann) = py_value_annotation(ty, &spell)
{
value_types.insert(py_value_name(name), ann);
}
Expand All @@ -970,6 +971,7 @@ impl Lowerer {
// Only a name's first assignment carries the annotation (a top-level
// `let mut` is reassigned by `<-`).
let mut annotated: HashSet<String> = HashSet::new();
let mut needs_any = false;
for stmt in &mut py.body {
let PyStmt::Assign { target, value } = stmt else {
continue;
Expand All @@ -993,9 +995,23 @@ impl Lowerer {
} else if bound_count.get(target.as_str()) == Some(&1)
&& let Some(ann) = value_types.get(target.as_str())
{
needs_any |= ann.contains("Any");
*target = format!("{target}: {ann}");
}
}
let has_any = py.body.iter().any(|s| {
matches!(s, PyStmt::ImportFrom { module, names }
if module == "typing" && names.iter().any(|n| n == "Any"))
});
if needs_any && !has_any {
py.body.insert(
0,
PyStmt::ImportFrom {
module: "typing".to_string(),
names: vec!["Any".to_string()],
},
);
}
// 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 @@ -6699,8 +6715,23 @@ fn return_annotation(ty: &crate::types::Ty, spell: &HashMap<String, String>) ->
/// functions (`DESIGN.md` §5.6); `None` when Python has no spelling for it here.
/// `spell` maps a user type name to its class or union alias.
fn py_type_annotation(ty: &crate::types::Ty, spell: &HashMap<String, String>) -> Option<String> {
spell_type(ty, spell, false)
}

/// A module-level value's annotation: as [`py_type_annotation`], except that a
/// type variable is `Any`. A generalized value (`let empty = Map.empty`) is
/// used at many types, which is what `Any` says.
fn py_value_annotation(ty: &crate::types::Ty, spell: &HashMap<String, String>) -> Option<String> {
spell_type(ty, spell, true)
}

fn spell_type(
ty: &crate::types::Ty,
spell: &HashMap<String, String>,
vars_any: bool,
) -> Option<String> {
use crate::types::Ty;
let inner = |t: &Ty| py_type_annotation(t, spell);
let inner = |t: &Ty| spell_type(t, spell, vars_any);
Some(match ty {
Ty::Int(_) | Ty::Num(..) => "int".to_string(),
Ty::Float(_) => "float".to_string(),
Expand All @@ -6717,6 +6748,7 @@ fn py_type_annotation(ty: &crate::types::Ty, spell: &HashMap<String, String>) ->
("Map", [k, v]) => format!("dict[{}, {}]", inner(k)?, inner(v)?),
(other, _) => spell.get(other)?.clone(),
},
Ty::Var(_) if vars_any => "Any".to_string(),
Ty::Var(_) | Ty::Fun(..) => return None,
})
}
Expand Down
9 changes: 7 additions & 2 deletions tests/compile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4023,14 +4023,19 @@ fn native_annotates_module_level_values_for_other_modules() {
let src = "type Cell = Plain | Double\n\
let layout = [Plain, Double]\n\
let size = List.len layout\n\
print size";
let empty = Map.empty\n\
print size\n\
print (Map.len (Map.add 1 \"a\" empty))";
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"]);
// A generalized value spells its type variables as `Any`.
assert!(py.contains("empty: dict[Any, Any] = "), "{py}");
assert!(py.contains("from typing import Any"), "{py}");
assert_eq!(out, ["2", "1"]);
}

#[test]
Expand Down
Loading