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
86 changes: 61 additions & 25 deletions src/pyscriptc/lower/_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -1242,22 +1242,34 @@ def _fill_args(self, node: ast.Call, params: list[dict[str, Any]], what: str) ->
)
if var_index is not None:
return self._fill_varargs(node, params, var_index, what)
if len(node.args) > len(params):
self.fail(node, "PS1003", f"{what} expects {len(params)} argument(s)")
kwonly_start = next((index for index, param in enumerate(params) if param.get("kwonly")), len(params))
positional = params[:kwonly_start]
if len(node.args) > len(positional):
self.fail(node, "PS1003", f"{what} expects {len(positional)} argument(s)")
slots: list[dict[str, Any] | None] = [None] * len(params)
names = [param["name"] for param in params]
pos_names = [param["name"] for param in positional]
for index, arg_node in enumerate(node.args):
slots[index] = self._expr(arg_node, params[index]["type"])
seen: set[str] = set()
for kw in node.keywords:
assert kw.arg is not None
if kw.arg not in names:
if kw.arg in pos_names:
index = names.index(kw.arg)
if params[index].get("posonly"):
self.fail(kw, "PS2003", f"{what} got {kw.arg} as a keyword for a positional-only parameter")
if index < len(node.args) or kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
slots[index] = self._expr(kw.value, params[index]["type"])
elif kw.arg in names:
index = names.index(kw.arg)
if kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
slots[index] = self._expr(kw.value, params[index]["type"])
else:
self.fail(kw, "PS2003", f"unexpected keyword argument {kw.arg}")
index = names.index(kw.arg)
if index < len(node.args) or kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
slots[index] = self._expr(kw.value, params[index]["type"])
args: list[dict[str, Any]] = []
for index, param in enumerate(params):
got = slots[index]
Expand All @@ -1282,34 +1294,49 @@ def _fill_varargs(
fixed = params[:var_index]
varparam = params[var_index] if params[var_index].get("vararg") else None
kwparam = params[-1] if params[-1].get("kwarg") else None
tail = params[var_index + 1 :]
# Keyword-only parameters can sit before the `**kwargs` capture, so
# split them out of the fixed prefix rather than assuming a layout.
positional_params = [param for param in fixed if not param.get("kwonly")]
kwonly = [param for param in fixed if param.get("kwonly")] + [param for param in tail if param.get("kwonly")]
if varparam is not None and any(kw.arg == varparam["name"] for kw in node.keywords):
self.fail(node, "PS2002", "a starred parameter is not passed by keyword")
positional = list(node.args[: len(fixed)])
extra = list(node.args[len(fixed) :])
slots: list[dict[str, Any] | None] = [None] * len(fixed)
names = [param["name"] for param in fixed]
positional = list(node.args[: len(positional_params)])
extra = list(node.args[len(positional_params) :])
slots: list[dict[str, Any] | None] = [None] * len(positional_params)
kw_slots: list[dict[str, Any] | None] = [None] * len(kwonly)
names = [param["name"] for param in positional_params]
kw_names = [param["name"] for param in kwonly]
for index, arg_node in enumerate(positional):
slots[index] = self._expr(arg_node, fixed[index]["type"])
slots[index] = self._expr(arg_node, positional_params[index]["type"])
seen: set[str] = set()
leftover: list[ast.keyword] = []
for kw in node.keywords:
assert kw.arg is not None
if kw.arg not in names:
if kwparam is None:
self.fail(kw, "PS2003", f"unexpected keyword argument {kw.arg}")
if kw.arg in names:
index = names.index(kw.arg)
if positional_params[index].get("posonly"):
self.fail(kw, "PS2003", f"{what} got {kw.arg} as a keyword for a positional-only parameter")
if index < len(positional) or kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
slots[index] = self._expr(kw.value, positional_params[index]["type"])
elif kw.arg in kw_names:
index = kw_names.index(kw.arg)
if kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
kw_slots[index] = self._expr(kw.value, kwonly[index]["type"])
elif kwparam is not None:
leftover.append(kw)
continue
index = names.index(kw.arg)
if index < len(positional) or kw.arg in seen:
self.fail(kw, "PS2003", f"{kw.arg} given more than once")
seen.add(kw.arg)
slots[index] = self._expr(kw.value, fixed[index]["type"])
else:
self.fail(kw, "PS2003", f"unexpected keyword argument {kw.arg}")
args: list[dict[str, Any]] = []
for index, param in enumerate(fixed):
for index, param in enumerate(positional_params):
got = slots[index]
if got is None:
if "default" not in param:
self.fail(node, "PS1003", f"{what} expects {len(fixed)} argument(s)")
self.fail(node, "PS1003", f"{what} expects {len(positional_params)} argument(s)")
got = param["default"]
if not types_equal(got["type"], param["type"]):
self.fail(node, "PS1003", f"argument {param['name']} of {what} has the wrong type")
Expand All @@ -1325,7 +1352,16 @@ def _fill_varargs(
elems.append(value)
args.append({"kind": "list", "elems": elems, "type": varparam["type"], "span": span})
elif extra:
self.fail(node, "PS1003", f"{what} expects {len(fixed)} argument(s)")
self.fail(node, "PS1003", f"{what} expects {len(positional_params)} argument(s)")
for index, param in enumerate(kwonly):
got = kw_slots[index]
if got is None:
if "default" not in param:
self.fail(node, "PS1003", f"{what} expects keyword argument {param['name']}")
got = param["default"]
if not types_equal(got["type"], param["type"]):
self.fail(node, "PS1003", f"argument {param['name']} of {what} has the wrong type")
args.append(got)
if kwparam is not None:
pairs = []
for kw in leftover:
Expand Down
55 changes: 35 additions & 20 deletions src/pyscriptc/lower/_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -658,9 +658,13 @@ def _generated_method(self, node: ast.ClassDef, info: dict[str, Any]) -> dict[st
def _signature(
self, fn: ast.FunctionDef, class_name: str | None, kind: str = "method"
) -> tuple[list[dict[str, Any]], dict[str, Any]]:
if fn.args.kwonlyargs or fn.args.posonlyargs:
self.fail(fn, "PS1202", f"{fn.name} uses a parameter form that is not lowered")
args = list(fn.args.args)
# `**kwargs` capture stays lowered (packed into dict[str, T] by
# `_fill_varargs`); `/`-positional and `*`-keyword parameters normalize
# into the named-parameter machinery instead: positional-only
# parameters refuse keywords, keyword-only ones must be passed by
# name. See `_fill_args`/`_fill_varargs` for the binding rules.
pos_marks = [True] * len(fn.args.posonlyargs) + [False] * len(fn.args.args)
args = list(fn.args.posonlyargs) + list(fn.args.args)
params: list[dict[str, Any]] = []
if kind == "property":
if fn.args.vararg is not None or fn.args.kwarg is not None or len(args) != 1 or args[0].arg != "self":
Expand All @@ -671,13 +675,15 @@ def _signature(
self.fail(fn, "PS1201", "decorators are not lowered")
params.append({"name": "self", "type": {"kind": "class", "name": class_name}})
args = []
pos_marks = []
elif kind == "class":
if not args or args[0].arg != "cls":
self.fail(fn, "PS2002", "a classmethod's first parameter must be cls")
if args[0].annotation is not None:
self.fail(args[0], "PS2002", "cls is not annotated")
# cls is not a value. Class.method(...) does not pass the class object.
args = args[1:]
pos_marks = pos_marks[1:]
elif kind == "static":
pass
elif class_name is not None:
Expand All @@ -687,28 +693,15 @@ def _signature(
self.fail(args[0], "PS2002", "self is not annotated")
params.append({"name": "self", "type": {"kind": "class", "name": class_name}})
args = args[1:]
pos_marks = pos_marks[1:]
defaults = list(fn.args.defaults)
missing = len(args) - len(defaults)
for index, arg in enumerate(args):
param = _param(self, arg)
if pos_marks[index]:
param["posonly"] = True
if index >= missing:
default_node = defaults[index - missing]
if param["type"].get("optional"):
value = self._expr(default_node)
if value["kind"] != "const":
self.fail(default_node, "PS2002", "a default must be a constant")
if value["type"]["kind"] != "none" and not types_equal(value["type"], param["type"]["elem"]):
self.fail(
default_node, "PS1003", f"{arg.arg} is {_show(value['type'])}, not {_show(param['type'])}"
)
param["default"] = value
param["optdefault"] = True
else:
value = self._expr(default_node, param["type"])
self._expect(default_node, value["type"], param["type"], arg.arg)
if value["kind"] != "const":
self.fail(default_node, "PS2002", "a default must be a constant")
param["default"] = value
self._param_default(arg, defaults[index - missing], param)
params.append(param)
if fn.args.vararg is not None:
vararg = fn.args.vararg
Expand All @@ -718,6 +711,12 @@ def _signature(
if elem["kind"] == "bool":
self.fail(vararg, "PS2002", "list[bool] is not lowered")
params.append({"name": vararg.arg, "type": {"kind": "list", "elem": elem}, "vararg": True})
for karg, kdef in zip(fn.args.kwonlyargs, fn.args.kw_defaults, strict=True):
param = _param(self, karg)
param["kwonly"] = True
if kdef is not None:
self._param_default(karg, kdef, param)
params.append(param)
if fn.args.kwarg is not None:
kwarg = fn.args.kwarg
if kwarg.annotation is None:
Expand All @@ -741,6 +740,22 @@ def _signature(
self.fail(fn, "PS1003", "__init__ returns None")
return params, ret

def _param_default(self, arg: ast.arg, default_node: ast.expr, param: dict[str, Any]) -> None:
if param["type"].get("optional"):
value = self._expr(default_node)
if value["kind"] != "const":
self.fail(default_node, "PS2002", "a default must be a constant")
if value["type"]["kind"] != "none" and not types_equal(value["type"], param["type"]["elem"]):
self.fail(default_node, "PS1003", f"{arg.arg} is {_show(value['type'])}, not {_show(param['type'])}")
param["default"] = value
param["optdefault"] = True
else:
value = self._expr(default_node, param["type"])
self._expect(default_node, value["type"], param["type"], arg.arg)
if value["kind"] != "const":
self.fail(default_node, "PS2002", "a default must be a constant")
param["default"] = value

def _construct(self, node: ast.Call, span: dict[str, Any], name: str) -> dict[str, Any]:
info = self.classes[name]
if info.get("namedtuple"):
Expand Down
6 changes: 4 additions & 2 deletions tests/test_census_self.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ def test_self_census_ballpark() -> None:
# First-run fix: node_text/op_text diagnostics helpers and CLI --run entry handling (+7 fences).
# Optional widening (Stage U): total stays 1434; 2 typefacts `X | Y | None` fences
# move from union-types (11 to 9) into optional-types (0 to 2, PS1102).
assert census["fences"] == 1434
# T2-3 parameter forms normalize into the named-parameter machinery (+1
# _classes fence for the kwarg-dict binding path).
assert census["fences"] == 1435
# 22,164 lines after the `--facts` lane landed in typefacts.py.
assert 11_000 <= census["lines"] <= 25_000
assert sum(p["fences"] for p in census["packages"]) == 1434
assert sum(p["fences"] for p in census["packages"]) == 1435
9 changes: 5 additions & 4 deletions tests/test_pyjwt_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ def test_real_pyjwt_hmac_tree_still_fences() -> None:

proc = run_cli("compile", str(FIXTURE / "jwt"), "--emit", "ir", "-o", os.devnull, "--skip-check")
assert proc.returncode == 3
# 103 fences after R26 (`Optional` / `x | None` annotations lowered).
# Shrinking means a backlog lowering cleared fences here; update the pin
# when that happens.
assert proc.stderr.count("PS") == 103
# 107 fences after T2-3 (kw-only parameters normalize): the removed
# PS1202 signature fences let those defs lower further and report
# deeper, more accurate fences instead. Shrinking means a backlog
# lowering cleared fences here; update the pin when that happens.
assert proc.stderr.count("PS") == 107


@pytest.mark.linux_container # HEAVY=1 .agents/docker/run.sh
Expand Down
Loading
Loading