Skip to content
Merged

Fixes #213

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
2849f78
write a module-level `private` class under the name it is emitted as …
KotlinIsland Oct 7, 2026
7434631
parenthesize an expression written over several lines wherever a lowe…
KotlinIsland Oct 7, 2026
a0bdf8f
lower every `??` by an edit of its own, so one inside an operand of a…
KotlinIsland Oct 7, 2026
8bbb8df
lower a `?.` chain inside the receiver of another, behind a call (`n?…
KotlinIsland Oct 7, 2026
4918d05
bind the receiver of a grapheme `first` / `last` as a comprehension v…
KotlinIsland Oct 7, 2026
1e22869
parenthesize a `^` operand written over several lines where the guard…
KotlinIsland Oct 7, 2026
c58d967
run python's compile-time checks over the transpiled module, on every…
KotlinIsland Oct 7, 2026
c2c74cf
read a soft keyword after a parameter's modifiers as the parameter's …
KotlinIsland Oct 8, 2026
193a72f
read a soft keyword after a callable-type parameter's `local` / `once…
KotlinIsland Oct 8, 2026
7a1212b
read a soft keyword after `protocol`, `extension`, `newtype`, `implem…
KotlinIsland Oct 8, 2026
1081361
report a misplaced `some` before a soft keyword as one error, through…
KotlinIsland Oct 8, 2026
52ef3ea
stop the parser panicking on a decorator over a modifier word that st…
KotlinIsland Oct 8, 2026
d35592e
spell a `float` type argument as `float` in a `.by` module wherever t…
KotlinIsland Oct 8, 2026
b27411d
transpile a checked cast or a type test whose target another lowering…
KotlinIsland Oct 8, 2026
aa5099c
build a union of methods bound to different literals without comparin…
KotlinIsland Oct 8, 2026
78f8250
add a `ty_micro[enum_name_method]` benchmark calling `lower` and `sta…
KotlinIsland Oct 8, 2026
4218c42
record during inference which modules' extensions and conformances it…
KotlinIsland Oct 9, 2026
0108e6d
add a per-file query listing each import that makes another module's …
KotlinIsland Oct 9, 2026
bdcc01d
record during inference the names it reads as every binding their sco…
KotlinIsland Oct 9, 2026
8bc0bdd
add an `unused-import` lint for `.by` files with F401's rules, where …
KotlinIsland Oct 9, 2026
358e3cb
move ruff's `delete_stmt` fix helper into `ruff_python_edits` so ty c…
KotlinIsland Oct 9, 2026
d2b5221
fix an unused import with `by check --fix`: remove the statement, or …
KotlinIsland Oct 9, 2026
a5e916d
stop buff's F401 checking `.by` and `.byi` files, whose unused import…
KotlinIsland Oct 9, 2026
2b3ea61
document `unused-import`, F401 leaving `.by` files to it, and that an…
KotlinIsland Oct 9, 2026
b25fc64
move the choice of where runtime soundness checks go into the checker…
KotlinIsland Oct 9, 2026
01f51f2
count an import read only by a type the transpiled program writes out…
KotlinIsland Oct 9, 2026
ac4927c
run every checker-clean mdtest block again with its unused imports re…
KotlinIsland Oct 9, 2026
93f311c
construct a plain tuple literal as the anonymous named tuple the chec…
KotlinIsland Oct 9, 2026
9432693
leave a function to its interpreted definition in `by compile` when i…
KotlinIsland Oct 9, 2026
b50bb3f
document that a plain tuple literal is constructed as the anonymous n…
KotlinIsland Oct 9, 2026
0f5be36
write a module-level name into code the transpiled program runs only …
KotlinIsland Oct 9, 2026
5b12c7b
make a soundness check in a function on a class the module binds furt…
KotlinIsland Oct 10, 2026
c309eba
show a parameter default that is re-evaluated per call as a marker wh…
KotlinIsland Oct 10, 2026
296d86a
make a compiled function hand a call to its interpreted definition wh…
KotlinIsland Oct 10, 2026
c0f1e66
add `SemanticModel::deferrable_from_imports`: the module-level `from`…
KotlinIsland Oct 11, 2026
2ab311f
below python 3.15, a `from` import binds nothing where it is written:…
KotlinIsland Oct 11, 2026
660dcc8
a string in a `.by` type expression is emitted as the literal type th…
KotlinIsland Oct 11, 2026
c4beb25
let a type argument written as a value name a class defined further d…
KotlinIsland Oct 21, 2026
c08473d
quote an anonymous named tuple's field type when it names something t…
KotlinIsland Oct 21, 2026
7a5b186
record each type the transpiler writes as a string where it writes it…
KotlinIsland Oct 21, 2026
f5d623c
`by build`, `by run`, `by transpile <dir>` and `by compile` transpile…
KotlinIsland Oct 22, 2026
291cc9f
the db `by build`, `by run`, `by transpile <dir>`, `by compile` and `…
KotlinIsland Oct 22, 2026
0c52b69
a tuple type with a labelled `name: T` field and no `/`, `*` or varia…
KotlinIsland Oct 22, 2026
2744854
the checker tells an anonymous named tuple class by its own anchor, `…
KotlinIsland Oct 22, 2026
a72a605
a `.py` file that spells `===` or `!==` gets a syntax error, "identit…
KotlinIsland Oct 22, 2026
b2a2cf4
the anonymous-named-tuple cleanup that runs over a file's transpiled …
KotlinIsland Oct 22, 2026
ba56e91
ask whether a module-level name a lowering writes into code holds the…
KotlinIsland Oct 22, 2026
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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions crates/basedpython/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 7 additions & 3 deletions crates/by_build/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -390,9 +390,10 @@ fn finish(
anyhow::anyhow!("the `{pass}` pass produced ill-formed IR:\n {detail}")
})?;

// the default config on purpose, except for the target version: the
// lazy-import pass is what binds `JustFloat = float` locally instead of
// emitting a `from ty_extensions import ...` that has no module behind it.
// the default config on purpose, except for the target version and the
// deferral of `from` imports: the lazy-import pass is what binds
// `JustFloat = float` locally instead of emitting a
// `from ty_extensions import ...` that has no module behind it
//
// the version has to be the *interpreter's*, because this python runs inside
// the extension at import time — emitting syntax the interpreter cannot parse
Expand All @@ -402,6 +403,9 @@ fn finish(
source.to_string()
} else {
let mut config = options.fallback.clone().unwrap_or_default();
// the compiled functions read the module's globals by name, and a deferred
// `from` import is in them only once something has read it
config.defer_from_imports = false;
if let Some((major, minor)) = toolchain.and_then(|toolchain| toolchain.version)
&& let Ok(parsed) = format!("{major}.{minor}").parse()
{
Expand Down
191 changes: 188 additions & 3 deletions crates/by_build/tests/differential.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11561,6 +11561,24 @@ fn a_float_module_imports_without_any_extra_runtime_module() {
assert_eq!(out, "12.0");
}

#[test]
fn a_compiled_function_reads_a_name_its_module_imports_with_from() {
// below 3.15 the lazy-import polyfill defers a `from` import by binding nothing where
// it is written, and a compiled function reads the module's globals by name. so the
// fallback a compiled module embeds keeps its `from` imports ordinary ones
agree(
"fromglobal",
"\
from json import dumps
from os.path import basename as base

def f(a: int) -> str:
return dumps([a]) + base('/x/y')
",
&["m.f(1)"],
);
}

#[test]
fn a_compiled_function_is_a_c_function_object() {
// one of the few places the two builds are *meant* to differ: a natively
Expand Down Expand Up @@ -22840,10 +22858,64 @@ def widened(value: object) -> str:
// the class reports the module it was written in, not `builtins`
"(m.Node.__module__, m.Keyed.__module__)",
"(m.Node.__name__, m.Node.__qualname__)",
// an imported base reaches this module as a lazy proxy, so `__mro_entries__`
// is what turns it into the class — and python records what was written
"repr(m.Node.__orig_bases__)",
// an imported base is the class itself, with no `__mro_entries__` to
// record what was written in its place
"'__orig_bases__' in vars(m.Node)",
],
);
}

#[test]
fn a_class_on_a_base_that_stands_for_others_agrees() {
// a base that is not a class may stand for a tuple of them through `__mro_entries__`,
// which python resolves before it works out a metaclass, recording what was written in
// `__orig_bases__`. the checker is shown the class the stand-in stands for, so the class
// compiles, and the metaclass here is `ABCMeta`, so the type is built by calling it
let Some(compiled) = agree_in(
"mroentries",
"\
from abc import ABC
from typing import TYPE_CHECKING


class StandIn:
def __mro_entries__(self, bases: tuple[object, ...]) -> tuple[type, ...]:
return (ABC,)


if TYPE_CHECKING:
Base = ABC
else:
Base = StandIn()


class Node(Base):
def kind(self) -> str:
return \"node\"
",
&[
"m.Node().kind()",
"[base.__name__ for base in m.Node.__mro__]",
"type(m.Node.__orig_bases__[0]).__name__",
"(type(m.Node).__name__, issubclass(m.Node, __import__('abc').ABC))",
],
false,
by_irbuild::Language::BasedPython,
) else {
return;
};
// and the class is the one module init built, not the interpreted definition left
// standing because the build gave up on it
let Some((python, _)) = environment() else {
return;
};
assert_eq!(
run(
&python,
&compiled,
"import by_diff_mroentries as m\nprint(type(m.Node.__dict__['kind']).__name__)\n",
),
"method_descriptor"
);
}

Expand Down Expand Up @@ -29062,6 +29134,95 @@ class Joiner:
);
}

/// `inspect.signature` reads a compiled function's defaults off its interpreted definition,
/// and in `.by` a default re-evaluated per call is a marker there. a tool that binds a call
/// from the signature and calls with `apply_defaults` hands the markers back, and the
/// compiled boundary hands such a call to the definition, whose guard reads each one as the
/// argument not given — the boundary was unboxing the marker as the argument instead
#[test]
fn a_default_marker_handed_back_agrees() {
const HANDED_BACK: &str = "(lambda f: (lambda b: (b.apply_defaults(), f(*b.args, **b.kwargs))[1])(__import__('inspect').signature(f).bind()))";
let calls = [
format!("[{HANDED_BACK}(m.fresh), {HANDED_BACK}(m.fresh)]"),
format!("[{HANDED_BACK}(m.limit), setattr(m, 'LIMIT', 5), {HANDED_BACK}(m.limit)]"),
format!("_warnings_from({HANDED_BACK}, m.relaxed)"),
format!("[{HANDED_BACK}(m.keyword), {HANDED_BACK}(m.keyword)]"),
"(lambda d: [m.Bag([1]).add(d), m.Bag([1]).add(d)])(__import__('inspect').signature(m.Bag.add).parameters['more'].default)".to_owned(),
];
agree(
"default_markers",
"\
LIMIT = 2


def fresh(xs: list[int] = []) -> int:
xs.append(1)
return len(xs)


def limit(x: int = LIMIT) -> int:
return x


def relaxed(x: int = 1, a: int) -> int:
return x + a


def keyword(*, xs: list[int] = []) -> int:
xs.append(1)
return len(xs)


class Bag:
def __init__(self, items: list[int]) -> None:
self.items = items

def add(self, more: list[int] = [2]) -> int:
more.append(3)
return len(self.items) + len(more)
",
&calls.iter().map(String::as_str).collect::<Vec<_>>(),
);
}

/// the marker a compiled method compares an argument against is read off its interpreted
/// definition at module init, through `__wrapped__` where a decorator wrapped it. a
/// `__wrapped__` that leads back round stops the walk, as `inspect.unwrap` stops, rather
/// than holding the import forever. the decorated method itself is left to the class the
/// interpreted definition decorated, which is the one reading its handle at init
#[test]
fn a_wrapped_chain_that_leads_back_round_agrees() {
agree_with_declines(
"wrapped_cycle",
"\
class Looped:
def __init__(self, fn: (...) -> int):
self.fn = fn
self.__wrapped__ = self

def __get__(self, instance: object, owner: object = None) -> object:
if instance === None:
return self
return lambda xs: self.fn(instance, xs)


def looped(fn: (...) -> int) -> Looped:
return Looped(fn)


class Bag:
def __init__(self):
self.n = 1

@looped
def fresh(self, xs: list[int] = []) -> int:
xs.append(1)
return len(xs) + self.n
",
&["[m.Bag().fresh([5]), m.Bag().fresh([5])]"],
);
}

#[test]
fn the_numeric_slots_agree() {
// every binary numeric dunder fills a `nb_*` slot, and python never looks one up by
Expand Down Expand Up @@ -44945,6 +45106,30 @@ def show() -> str:
);
}

#[test]
fn a_plain_tuple_built_as_an_anonymous_named_tuple_agrees() {
// the checker reads each literal as an instance of the named tuple expected where it
// stands, and the fields are read by name: a plain tuple the lowering built has none
agree_with_declines(
"anonbuilt",
"\
def take(p: (a: int, b: int)) -> int:
return p.a + p.b


def call() -> int:
return take((7, 8))


def second() -> int:
items: list[(a: int, b: int)] = []
items.append((1, 2))
return items[0].b
",
&["m.call()", "m.second()"],
);
}

#[test]
fn a_checked_cast_agrees_on_the_value_it_refuses() {
// the refusal is the point of the form, so the raise is part of the answer
Expand Down
Loading
Loading