Skip to content
Open
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
16 changes: 10 additions & 6 deletions docs/src/api_reference/python_builtins.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ h3 {
}
</style>

## Implemented CPython-Like Built-ins
## Implemented CPython-Like Built-ins

The following built-in functions work similarly to their equivalents in CPython; see the specific functions below for notes

Expand Down Expand Up @@ -43,7 +43,7 @@ The following built-in functions work similarly to their equivalents in CPython;
### __hash__(object) { data-toc-label='hash()' }
: Currently implemented for types: `i8`,`i32`, `u8`, `bool`, `str`.

: By default, instances of SPy structs are not hashable. As a planned future feature, structs will have auto-generated `__hash__` by default, but this is awaiting implementation. Currently, users can implement the `__hash__` function to permit hashing.
: By default, instances of SPy structs are not hashable. As a planned future feature, structs will have auto-generated `__hash__` by default, but this is awaiting implementation. Currently, users can implement the `__hash__` function to permit hashing.

### __int__(object) { data-toc-label='int()' }

Expand All @@ -57,7 +57,7 @@ The following built-in functions work similarly to their equivalents in CPython;

### __list__\[type\]() { data-toc-label='list()' }

: The syntax `list[type]()` can be used to create a new empty list of the given type. The simpler syntax `l: list[membertype] = []` can also be used. Unlike CPython, this does not (currently) accept an Iterable to create a new list from.
: The syntax `list[type]()` can be used to create a new empty list of the given type. The simpler syntax `l: list[membertype] = []` can also be used. Unlike CPython, this does not (currently) accept an Iterable to create a new list from.

: The implementation (in SPy) of `list` can be [viewed here](https://github.com/spylang/spy/blob/main/stdlib/_list.spy).

Expand All @@ -73,6 +73,10 @@ The following built-in functions work similarly to their equivalents in CPython;

: `object` is implemented as a type, and can be used as a parameter or return type. "Plain" objects (i.e. `x = object()`) are not supported.

### __open__(file: str, mode: str) { data-toc-label='open()' }

: Open file and return a `_io.FileIO`. Raise OSError upon failure.

### __print__(obj) { data-toc-label='print()' }

: The print function is currently not variadic, in the sense that it only accepts a single argument. The built-in types are special-cased, and SPy can always print blue objects by pre-computing their string representation
Expand Down Expand Up @@ -103,15 +107,15 @@ The following built-in functions work similarly to their equivalents in CPython;

### __tuple__()

: The syntax `tuple[t1, t2, ...](val1, val2 ...)` can be used to create a new tuple, with `t1` as the type of `val1`, etc. unlike CPython, this does not (currently) accept an Iterable to create a new tuple from.
: The syntax `tuple[t1, t2, ...](val1, val2 ...)` can be used to create a new tuple, with `t1` as the type of `val1`, etc. unlike CPython, this does not (currently) accept an Iterable to create a new tuple from.

: The implementation (in SPy) of `tuple` can be [viewed here](https://github.com/spylang/spy/blob/main/stdlib/_tuple.spy).

### __type__(object) { data-toc-label='type()' }

: Returns the type (i.e. the dynamic type at runtime) of an object

## Not-Implemented CPython Built-ins
## Not-Implemented CPython Built-ins

The following CPython built-ins are not currently implemented in SPy. Each category has a brief note about the current state of that category of object or function - some require additional internal mechanics, others are simply lower priority that other facets of the language to this point.

Expand Down Expand Up @@ -151,4 +155,4 @@ Many of these types are not implemented yet; others are in active development.

The I/O story is currently a high priority and is in active development.

: input(), open()
: input()
21 changes: 21 additions & 0 deletions examples/io_low_level.spy
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from posix import open, write, read, close, O_WRONLY, O_CREAT, O_TRUNC, O_RDONLY


def write_file(path: str, data: str) -> i32:
fd: i32 = open(path, O_WRONLY | O_CREAT | O_TRUNC)
n: i32 = write(fd, data)
close(fd)
return n


def read_file(path: str, n: i32) -> str:
fd: i32 = open(path, O_RDONLY)
result = read(fd, n)
close(fd)
return result


def main() -> None:
path = "/tmp/spy-io_low_level.txt"
write_file(path, "hello world")
print(read_file(path, 64))
17 changes: 17 additions & 0 deletions examples/io_open.spy
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def write(path: str, data: str, mode: str) -> None:
file = open(path, mode)
file.write(data)
file.close()


def main() -> None:
path = "/tmp/spy-io_open.txt"

write(path, "Like Python", "w")
write(path, ", but static\n", "a")

file = open(path, "r")
txt = file.read(11)
file.close()

assert txt == "Like Python"
1 change: 1 addition & 0 deletions spy/analyze/symtable.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ def add_sym(attr: str, impref: ImportRef, loc: Optional[Loc] = None) -> None:
add_sym("tuple", ImportRef("_tuple", "tuple"))
add_sym("slice", ImportRef("_slice", "Slice"))
add_sym("dict", ImportRef("_dict", "dict"))
add_sym("open", ImportRef("_io", "open"))
return scope

def __repr__(self) -> str:
Expand Down
46 changes: 46 additions & 0 deletions spy/libspy/include/spy/posix.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#ifndef SPY_TARGET_WASI
# include <sys/ioctl.h>
#endif
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>

// NOTE: this struct is also defined in vm/modules/posix.py, the two definitions must be
Expand Down Expand Up @@ -36,4 +39,47 @@ spy_posix$get_terminal_size(void) {
return result;
}

static inline int32_t
spy_posix$open(spy_Str *path, int32_t flags) {
// path->utf8 is NOT null-terminated, so we need a temporary copy
char *cpath = (char *)malloc(path->length + 1);
memcpy(cpath, path->utf8, path->length);
cpath[path->length] = '\0';
int fd = open(cpath, flags);
free(cpath);
if (fd == -1) {
spy_panic("OSError", strerror(errno), __FILE__, __LINE__);
}
return (int32_t)fd;
}

static inline spy_Str *
spy_posix$read(int32_t fd, int32_t count) {
spy_Str *result = spy_str_alloc((size_t)count);
ssize_t n = read(fd, (void *)result->utf8, (size_t)count);
if (n == -1) {
spy_panic("OSError", strerror(errno), __FILE__, __LINE__);
}
// Adjust the length to what was actually read
result->length = (size_t)n;
result->hash = -1;
return result;
}

static inline int32_t
spy_posix$write(int32_t fd, spy_Str *data) {
ssize_t n = write(fd, data->utf8, data->length);
if (n == -1) {
spy_panic("OSError", strerror(errno), __FILE__, __LINE__);
}
return (int32_t)n;
}

static inline void
spy_posix$close(int32_t fd) {
if (close(fd) == -1) {
spy_panic("OSError", strerror(errno), __FILE__, __LINE__);
}
}

#endif /* SPY_POSIX_H */
98 changes: 97 additions & 1 deletion spy/tests/compiler/test_posix.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from spy.tests.support import CompilerTest
import os

from spy.errors import SPyError
from spy.tests.support import CompilerTest, skip_backends


class TestPosix(CompilerTest):
Expand All @@ -18,3 +21,96 @@ def foo() -> str:
# When running in pytest without a terminal, we get fallback values
assert columns >= 80
assert lines >= 24

@skip_backends("C", reason="WASI sandbox cannot access host filesystem paths")
def test_open_read(self, tmp_path):
fpath = tmp_path / "hello.txt"
fpath.write_text("hello world", encoding="utf-8")
mod = self.compile("""
from posix import open, read, close, O_RDONLY

def read_file(path: str, n: i32) -> str:
fd: i32 = open(path, O_RDONLY)
result: str = read(fd, n)
close(fd)
return result
""")
assert mod.read_file(str(fpath), 11) == "hello world"
# Requesting fewer bytes than available
assert mod.read_file(str(fpath), 5) == "hello"

@skip_backends("C", reason="WASI sandbox cannot access host filesystem paths")
def test_open_write(self, tmp_path):
fpath = tmp_path / "out.txt"
mod = self.compile("""
from posix import open, write, close, O_WRONLY, O_CREAT, O_TRUNC

def write_file(path: str, data: str) -> i32:
fd: i32 = open(path, O_WRONLY | O_CREAT | O_TRUNC)
n: i32 = write(fd, data)
close(fd)
return n
""")
n = mod.write_file(str(fpath), "spy rocks")
assert n == 9
assert fpath.read_text(encoding="utf-8") == "spy rocks"

def test_o_flags_constants(self):
mod = self.compile("""
from posix import O_RDONLY, O_WRONLY, O_RDWR, O_CREAT, O_TRUNC, O_APPEND, O_EXCL

def get_rdonly() -> i32: return O_RDONLY
def get_wronly() -> i32: return O_WRONLY
def get_rdwr() -> i32: return O_RDWR
def get_creat() -> i32: return O_CREAT
def get_trunc() -> i32: return O_TRUNC
def get_append() -> i32: return O_APPEND
def get_excl() -> i32: return O_EXCL
""")
assert mod.get_rdonly() == os.O_RDONLY
assert mod.get_wronly() == os.O_WRONLY
assert mod.get_rdwr() == os.O_RDWR
assert mod.get_creat() == os.O_CREAT
assert mod.get_trunc() == os.O_TRUNC
assert mod.get_append() == os.O_APPEND
assert mod.get_excl() == os.O_EXCL

def test_open_nonexistent(self, tmp_path):
mod = self.compile("""
from posix import open, O_RDONLY

def foo(path: str) -> i32:
return open(path, O_RDONLY)
""")
with SPyError.raises("W_OSError", match="No such file or directory"):
mod.foo(str(tmp_path / "nonexistent.txt"))

def test_read_invalid_fd(self):
mod = self.compile("""
from posix import read

def foo() -> str:
return read(-1, 64)
""")
with SPyError.raises("W_OSError", match="Bad file descriptor"):
mod.foo()

def test_write_invalid_fd(self):
mod = self.compile("""
from posix import write

def foo() -> i32:
return write(-1, "data")
""")
with SPyError.raises("W_OSError", match="Bad file descriptor"):
mod.foo()

def test_close_invalid_fd(self):
mod = self.compile("""
from posix import close

def foo() -> None:
close(-1)
""")
with SPyError.raises("W_OSError", match="Bad file descriptor"):
mod.foo()
101 changes: 101 additions & 0 deletions spy/tests/stdlib/test__io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
from spy.errors import SPyError
from spy.tests.support import CompilerTest, skip_backends


@skip_backends("C", reason="WASI sandbox cannot access host filesystem paths")
class TestIO(CompilerTest):
def test_read(self, tmp_path):
fpath = tmp_path / "hello.txt"
fpath.write_text("hello world", encoding="utf-8")
mod = self.compile("""
from _io import FileIO

def read_file(path: str, n: i32) -> str:
f = FileIO(path, "r")
result: str = f.read(n)
f.close()
return result
""")
assert mod.read_file(str(fpath), 11) == "hello world"
assert mod.read_file(str(fpath), 5) == "hello"

def test_write(self, tmp_path):
fpath = tmp_path / "out.txt"
mod = self.compile("""
from _io import FileIO

def write_file(path: str, data: str) -> i32:
f = FileIO(path, "w")
n: i32 = f.write(data)
f.close()
return n
""")
n = mod.write_file(str(fpath), "spy rocks")
assert n == 9
assert fpath.read_text(encoding="utf-8") == "spy rocks"

def test_write_truncates(self, tmp_path):
fpath = tmp_path / "out.txt"
fpath.write_text("old content here", encoding="utf-8")
mod = self.compile("""
from _io import FileIO

def write_file(path: str, data: str) -> None:
f = FileIO(path, "w")
f.write(data)
f.close()
""")
mod.write_file(str(fpath), "new")
assert fpath.read_text(encoding="utf-8") == "new"

def test_append(self, tmp_path):
fpath = tmp_path / "out.txt"
fpath.write_text("hello ", encoding="utf-8")
mod = self.compile("""
from _io import FileIO

def append_file(path: str, data: str) -> None:
f = FileIO(path, "a")
f.write(data)
f.close()
""")
mod.append_file(str(fpath), "world")
assert fpath.read_text(encoding="utf-8") == "hello world"

def test_open_nonexistent(self, tmp_path):
mod = self.compile("""
from _io import FileIO

def open_file(path: str) -> FileIO:
return FileIO(path, "r")
""")
with SPyError.raises("W_OSError", match="No such file or directory"):
mod.open_file(str(tmp_path / "nonexistent.txt"))

def test_invalid_mode(self, tmp_path):
fpath = tmp_path / "f.txt"
fpath.write_text("x", encoding="utf-8")
mod = self.compile("""
from _io import FileIO

def open_file(path: str) -> FileIO:
return FileIO(path, "x")
""")
with SPyError.raises("W_ValueError", match="invalid mode"):
mod.open_file(str(fpath))


@skip_backends("C", reason="WASI sandbox cannot access host filesystem paths")
class TestOpen(CompilerTest):
def test_read(self, tmp_path):
fpath = tmp_path / "hello.txt"
fpath.write_text("hello world", encoding="utf-8")
mod = self.compile("""
def read_file(path: str, n: i32) -> str:
f = open(path, "r")
result: str = f.read(n)
f.close()
return result
""")
assert mod.read_file(str(fpath), 11) == "hello world"
assert mod.read_file(str(fpath), 5) == "hello"
5 changes: 5 additions & 0 deletions spy/vm/exc.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@ class W_KeyError(W_Exception):
pass


@BUILTINS.builtin_type("OSError")
class W_OSError(W_Exception):
pass


@BUILTINS.builtin_type("WIP")
class W_WIP(W_Exception):
"""
Expand Down
Loading
Loading