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
2 changes: 1 addition & 1 deletion docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,7 +689,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
| Array parameters only indexed, stored, `len`-ed; no aliasing (entry guard) | not compiled; diagnostic |
| Integer ops proven safe by the verifier's intervals (constants, `len`, `range` variables, guards such as `if x < e` / `while lo < hi`, joined local assignments, `//` and `%` by constants) | emitted unchecked (a shift/mask for `//`/`%` by a power of two of a non-negative value); the sanitizer CI run builds with `TP_CHECK_PROVEN` and turns a wrong proof into a SystemError (#148) |
| `array.array[float]` / `array.array[int]` parameters (exact `array.array`, typecode `d` or `q`/`l`, 8-byte items) are read and written in place: the buffer export is held for the call and released on every exit, and every element access is one relaxed atomic 8-byte load or store, so each access is atomic like the interpreter's `a[i]`; no copy-in and no limit on the number of buffers. Open (#149): while the export is held, another thread's resize of that array raises BufferError. numpy arrays: see the next row | anything else: object path |
| numpy `npt.NDArray[np.float64]` / `NDArray[np.int64]` parameters (exact `numpy.ndarray`, 1-D, C-contiguous, native byte order, writable when stored to) take the same buffer path (#220, step 1) when the function accesses the array purely by element (`x[i]`, `len(x)`); a function that uses it as a whole object (`np.sum(x)`, `x.T`, passing `x` on, a slice) compiles as before with the parameter an opaque object. Other dtypes, including bool, are opaque objects. Results follow the interpreter: a value derived from an element is a numpy scalar there, so it is boxed as `np.float64` / `np.int64` / `np.bool` when it escapes (return, `Box`), decided per value at run time by a taint flag; a function declared `-> float` / `int` / `bool` that holds a numpy array returns the interpreter's object. `float(v)` of such a value is a plain float. An out-of-range index raises numpy's `IndexError("index i is out of bounds for axis 0 with size n")`. Arithmetic on a value derived from an element follows numpy's scalar semantics (#220, step 2): float `/`, `//`, `%` never raise ZeroDivisionError (inf / nan, with numpy's result bits and sign rules, `//` and `%` as numpy's `divmod`), int64 `//` and `%` by zero give 0, int64 `/` converts both operands to double. numpy's RuntimeWarning (category and text exactly as numpy's, e.g. `overflow encountered in scalar add`, `divide by zero encountered in scalar divide`, `invalid value encountered in scalar floor_divide`) is emitted once per flagged operation, with the source file and line of the operation. A pure function does not emit it itself: on any floating-point exception (overflow, invalid, divide by zero) or int64 wrap or division by zero it deopts, so the interpreter emits it; an impure function cannot deopt after an effect, so the compiled code emits the warning (`PyErr_WarnExplicitObject`, honouring `warnings` filters and the module's `__warningregistry__`). Float `+ - * / // %` on such values is compiled in both kinds of function, including a store of the result into the array (`x[i] -= d * m`); int64 `/ // %` is compiled in a pure function only. Int64 `+ - *` and `// %` of an element in an impure function (a native INT would have to be a numpy scalar) stay on the object path | anything else: object path |
| numpy `npt.NDArray[np.float64]` / `NDArray[np.int64]` parameters (exact `numpy.ndarray`, 1-D, C-contiguous, native byte order, writable when stored to) take the same buffer path (#220, step 1) when the function accesses the array purely by element (`x[i]`, `len(x)`); a function that uses it as a whole object (`np.sum(x)`, `x.T`, passing `x` on, a slice) compiles as before with the parameter an opaque object. `NDArray[np.bool_]` (#261) is the same path with 1-byte items (buffer format `?`; a `uint8` or other array deopts): an element read is a native bool, boxed as `np.bool` when it escapes; a store compiles only for a bool value (`x[i] = v`, `v: bool`), because numpy converts an int or float by truthiness and that function stays interpreted. Other dtypes are opaque objects. Results follow the interpreter: a value derived from an element is a numpy scalar there, so it is boxed as `np.float64` / `np.int64` / `np.bool` when it escapes (return, `Box`), decided per value at run time by a taint flag; a function declared `-> float` / `int` / `bool` that holds a numpy array returns the interpreter's object. `float(v)` of such a value is a plain float. An out-of-range index raises numpy's `IndexError("index i is out of bounds for axis 0 with size n")`. Arithmetic on a value derived from an element follows numpy's scalar semantics (#220, step 2): float `/`, `//`, `%` never raise ZeroDivisionError (inf / nan, with numpy's result bits and sign rules, `//` and `%` as numpy's `divmod`), int64 `//` and `%` by zero give 0, int64 `/` converts both operands to double. numpy's RuntimeWarning (category and text exactly as numpy's, e.g. `overflow encountered in scalar add`, `divide by zero encountered in scalar divide`, `invalid value encountered in scalar floor_divide`) is emitted once per flagged operation, with the source file and line of the operation. A pure function does not emit it itself: on any floating-point exception (overflow, invalid, divide by zero) or int64 wrap or division by zero it deopts, so the interpreter emits it; an impure function cannot deopt after an effect, so the compiled code emits the warning (`PyErr_WarnExplicitObject`, honouring `warnings` filters and the module's `__warningregistry__`). Float `+ - * / // %` on such values is compiled in both kinds of function, including a store of the result into the array (`x[i] -= d * m`); int64 `/ // %` is compiled in a pure function only. Int64 `+ - *` and `// %` of an element in an impure function (a native INT would have to be a numpy scalar) stay on the object path | anything else: object path |
| A function holding an array parameter runs no user code: closed, and no slot read or `C(args)` whose slow path runs user code, in its whole call tree (#203) | not compiled; diagnostic |
| OBJ references follow one ownership rule; no IR form for free, borrowed alias or NULL read | not compiled; diagnostic |
| Integer overflow checked and promoted (deopt); an op marked `proven` must re-prove its interval fits i64 | not compiled; diagnostic `verify/proven-overflow` |
Expand Down
6 changes: 3 additions & 3 deletions python-multiplatform-ksp/src/main/python/typedpython/cgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,10 +188,10 @@ class CGenError(Exception):
_BUFFER_KINDS = {None: None, "array": 0, "numpy": 1}
# buf_types slots 2..4: the numpy scalar types a Box.np makes (np.float64, np.int64, np.bool_)
_NP_SLOT = {Type.F64: 2, Type.I64: 3, Type.BOOL: 4}
_BUFFER_FMT = {Type.F64_ARRAY: ('"d"', "NULL"), Type.I64_ARRAY: ('"q"', '"l"')}
_BUFFER_SFX = {Type.F64_ARRAY: "f64", Type.I64_ARRAY: "i64"}
_BUFFER_FMT = {Type.F64_ARRAY: ('"d"', "NULL"), Type.I64_ARRAY: ('"q"', '"l"'), Type.BOOL_ARRAY: ('"?"', "NULL")}
_BUFFER_SFX = {Type.F64_ARRAY: "f64", Type.I64_ARRAY: "i64", Type.BOOL_ARRAY: "bool"}
_CT_ARRAY_REV = {"tp_f64_array": Type.F64_ARRAY, "tp_i64_array": Type.I64_ARRAY}
_ARRAY_ELEM = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64}
_ARRAY_ELEM = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64, Type.BOOL_ARRAY: Type.BOOL}
_ARRAY_PFX = {Type.F64_ARRAY: "tp_f64_array", Type.I64_ARRAY: "tp_i64_array"}
_UNBOX = {Type.I64: "tp_unbox_i64", Type.F64: "tp_unbox_f64", Type.BOOL: "tp_unbox_bool"}
_BOX = {Type.I64: "tp_box_i64", Type.F64: "tp_box_f64", Type.BOOL: "tp_box_bool"}
Expand Down
12 changes: 7 additions & 5 deletions python-multiplatform-ksp/src/main/python/typedpython/frontend.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
TYPE_NAME = {
Type.I64: "int", Type.F64: "float", Type.BOOL: "bool", Type.NONE: "None", Type.OBJ: "object",
Type.INT: "int",
Type.F64_ARRAY: "list[float]", Type.I64_ARRAY: "list[int]",
Type.F64_ARRAY: "list[float]", Type.I64_ARRAY: "list[int]", Type.BOOL_ARRAY: "NDArray[np.bool_]",
}
SCALAR_KIND = {"int": Type.I64, "float": Type.F64, "bool": Type.BOOL}
ARRAY_KIND = {"list[float]": Type.F64_ARRAY, "list[int]": Type.I64_ARRAY}
Expand Down Expand Up @@ -341,7 +341,8 @@ def _element_only(fn: ast.FunctionDef, name: str) -> bool:


# numpy dtypes that are 8-byte buffer arrays (#220): the scalar type annotation they spell
_NUMPY_DTYPE = {"numpy.float64": Type.F64_ARRAY, "numpy.int64": Type.I64_ARRAY}
_NUMPY_DTYPE = {"numpy.float64": Type.F64_ARRAY, "numpy.int64": Type.I64_ARRAY,
"numpy.bool_": Type.BOOL_ARRAY, "numpy.bool": Type.BOOL_ARRAY}


def _scalar_globals(tree: ast.Module, bindings: dict[str, list[tuple[int, str]]]) -> dict[str, Type]:
Expand Down Expand Up @@ -801,7 +802,8 @@ def _numpy_kinds(sig: _Signature, inferred: dict[str, list[tuple[int, str]]],
nodes = {n.lineno: n for n in ast.walk(sig.node)
if isinstance(n, (ast.Assign, ast.AugAssign)) and len(getattr(n, "targets", [0])) == 1}

out = {name: [(line, _NUMPY_SPELLED.get(k, k)) for line, k in entries]
# newer numpy stubs type an element `typing.Any`: the same "source settles it" case as `Unknown`
out = {name: [(line, _NUMPY_SPELLED.get(k, "Unknown" if k == "typing.Any" else k)) for line, k in entries]
for name, entries in inferred.items()}

def known(name: str) -> str | None:
Expand All @@ -816,7 +818,7 @@ def kind(e: ast.expr) -> str | None:
if isinstance(e, ast.Name):
return known(e.id)
if isinstance(e, ast.Subscript) and isinstance(e.value, ast.Name) and e.value.id in arrays:
return "float" if arrays[e.value.id] is Type.F64_ARRAY else "int"
return {Type.F64_ARRAY: "float", Type.BOOL_ARRAY: "bool"}.get(arrays[e.value.id], "int")
if isinstance(e, ast.UnaryOp):
return "bool" if isinstance(e.op, ast.Not) else kind(e.operand)
if isinstance(e, ast.BinOp):
Expand Down Expand Up @@ -1800,7 +1802,7 @@ def array_of(self, node: ast.Subscript) -> tuple[str, Type]:
if not (isinstance(node.value, ast.Name) and node.value.id in self.arrays):
raise Skip(node.lineno, f"subscript `{_src(node)}` of an object is not supported")
name = node.value.id
return name, Type.F64 if self.arrays[name] == Type.F64_ARRAY else Type.I64
return name, {Type.F64_ARRAY: Type.F64, Type.BOOL_ARRAY: Type.BOOL}.get(self.arrays[name], Type.I64)

def index(self, node: ast.expr) -> ir.Expr:
if isinstance(node, ast.Slice):
Expand Down
3 changes: 2 additions & 1 deletion python-multiplatform-ksp/src/main/python/typedpython/ir.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ class Type(Enum):
INT = "int" # a Python int that is a native i64 or, once promoted, a PyLong (SPEC N-13)
F64_ARRAY = "f64[]" # a list[float] parameter (ArrayParam) or a local array (NewArray/CopyArray)
I64_ARRAY = "i64[]" # a list[int] parameter (ArrayParam) or a local array (NewArray/CopyArray)
BOOL_ARRAY = "bool[]" # a numpy np.bool_ buffer parameter only (1-byte items, #261)


SCALARS = (Type.I64, Type.F64, Type.BOOL)
# INT is not a scalar: it owns a reference when promoted. It is created only by `ToInt`, by arithmetic
# on INTs, and by a redo `Call` of an i64-returning callee, and it becomes an object only through `Box`.
ARRAYS = (Type.F64_ARRAY, Type.I64_ARRAY)
ARRAYS = (Type.F64_ARRAY, Type.I64_ARRAY, Type.BOOL_ARRAY)


# --- expressions ---------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1115,8 +1115,8 @@ static inline void tp_buf_exit(tp_buf *b)
memset(b, 0, sizeof(*b));
}

static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2,
int writable, tp_buf *b)
static inline int tp__buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2,
int writable, Py_ssize_t isz, tp_buf *b)
{
int flags = PyBUF_C_CONTIGUOUS | PyBUF_FORMAT;
memset(b, 0, sizeof(*b));
Expand All @@ -1135,12 +1135,12 @@ static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1,
return 1;
}
b->held = 1;
if (b->view.ndim != 1 || b->view.itemsize != 8 || b->view.shape == NULL
if (b->view.ndim != 1 || b->view.itemsize != isz || b->view.shape == NULL
|| b->view.suboffsets != NULL || b->view.shape[0] < 0
|| b->view.shape[0] * 8 != b->view.len || b->view.format == NULL
|| b->view.shape[0] * isz != b->view.len || b->view.format == NULL
|| !(strcmp(b->view.format, fmt1) == 0 || (fmt2 != NULL && strcmp(b->view.format, fmt2) == 0))
|| (writable && b->view.readonly)
|| (b->view.shape[0] > 0 && (b->view.buf == NULL || ((uintptr_t)b->view.buf & 7) != 0))) {
|| (b->view.shape[0] > 0 && (b->view.buf == NULL || ((uintptr_t)b->view.buf & (uintptr_t)(isz - 1)) != 0))) {
tp_buf_exit(b);
return 1;
}
Expand All @@ -1149,6 +1149,13 @@ static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1,
return 0;
}

/* The item size follows the format: "?" (numpy bool_, #261) has 1-byte items, every other format 8. */
static inline int tp_buf_enter(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2,
int writable, tp_buf *b)
{
return tp__buf_enter(obj, exact, fmt1, fmt2, writable, strcmp(fmt1, "?") == 0 ? 1 : 8, b);
}

/* A numpy array (#220): the same guard on the exact `numpy.ndarray`; `np` selects numpy's IndexError
* text. The format check already refuses a byte-order prefix and every dtype but float64 / int64. */
static inline int tp_buf_enter_np(PyObject *obj, PyObject *exact, const char *fmt1, const char *fmt2,
Expand Down Expand Up @@ -1191,7 +1198,19 @@ static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i) { return ((con
static inline int64_t tp_buf_ld_i64(const tp_buf *b, Py_ssize_t i) { return ((const int64_t *)b->data)[i]; }
static inline void tp_buf_st_f64(const tp_buf *b, Py_ssize_t i, double v) { ((double *)b->data)[i] = v; }
static inline void tp_buf_st_i64(const tp_buf *b, Py_ssize_t i, int64_t v) { ((int64_t *)b->data)[i] = v; }
static inline int tp_buf_ld_bool(const tp_buf *b, Py_ssize_t i) { return ((const uint8_t *)b->data)[i] != 0; }
static inline void tp_buf_st_bool(const tp_buf *b, Py_ssize_t i, int v) { ((uint8_t *)b->data)[i] = v != 0; }
#else
static inline int tp_buf_ld_bool(const tp_buf *b, Py_ssize_t i)
{
return __atomic_load_n((const uint8_t *)b->data + i, __ATOMIC_RELAXED) != 0;
}

static inline void tp_buf_st_bool(const tp_buf *b, Py_ssize_t i, int v)
{
__atomic_store_n((uint8_t *)b->data + i, (uint8_t)(v != 0), __ATOMIC_RELAXED);
}

static inline double tp_buf_ld_f64(const tp_buf *b, Py_ssize_t i)
{
uint64_t u = __atomic_load_n((const uint64_t *)b->data + i, __ATOMIC_RELAXED);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ class Diagnostic:
_INT_PRODUCERS = frozenset({ToInt, BinOp, UnaryOp, Local, Call})
_INT_OPS = frozenset({BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.FLOORDIV,
BinOpKind.MOD, BinOpKind.LSHIFT, BinOpKind.RSHIFT})
_ELEMENT = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64}
_ELEMENT = {Type.F64_ARRAY: Type.F64, Type.I64_ARRAY: Type.I64, Type.BOOL_ARRAY: Type.BOOL}
_MATH_ARITY = {MathFunc.ATAN2: 2, MathFunc.HYPOT: 2} # every other MathFunc: 1
_I64_DEOPT_OPS = frozenset({BinOpKind.ADD, BinOpKind.SUB, BinOpKind.MUL, BinOpKind.FLOORDIV,
BinOpKind.MOD, BinOpKind.TRUEDIV})
Expand Down
Loading