diff --git a/ROADMAP.md b/ROADMAP.md index 8b912c0..ed9c444 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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 diff --git a/src/lowering/mod.rs b/src/lowering/mod.rs index 1128ab1..8c46a23 100644 --- a/src/lowering/mod.rs +++ b/src/lowering/mod.rs @@ -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 = HashMap::new(); let mut bound_count: HashMap = HashMap::new(); for item in &module.items { @@ -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); } @@ -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 = HashSet::new(); + let mut needs_any = false; for stmt in &mut py.body { let PyStmt::Assign { target, value } = stmt else { continue; @@ -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 = HashMap::new(); for item in &module.items { @@ -6699,8 +6715,23 @@ fn return_annotation(ty: &crate::types::Ty, spell: &HashMap) -> /// 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) -> Option { + 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) -> Option { + spell_type(ty, spell, true) +} + +fn spell_type( + ty: &crate::types::Ty, + spell: &HashMap, + vars_any: bool, +) -> Option { 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(), @@ -6717,6 +6748,7 @@ fn py_type_annotation(ty: &crate::types::Ty, spell: &HashMap) -> ("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, }) } diff --git a/tests/compile.rs b/tests/compile.rs index 836b1a4..3dd2d1b 100644 --- a/tests/compile.rs +++ b/tests/compile.rs @@ -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]