diff --git a/LANGUAGE.md b/LANGUAGE.md index 9de9648..391b35c 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -177,7 +177,7 @@ as `math.divfloor(a, b)` resolve to ordinary functions. - root `std` re-exports `ArrayList(T)` while its operations remain in `std/arraylist` - `std/mem` generic slice equality, allocator contract with raw byte operations, typed `empty` / `alloc` / `realloc` / `free`, overflow checks, zero-sized-type support, and failure-preserving reallocation - `std/arraylist` generic `ArrayList(T)` with direct `items` slice access, explicit capacity, allocator ownership, fallible reserve/append, clear, and deinit -- `std/io` explicit `Io` capabilities, `Reader`/`Writer` stream values, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime +- `std/io` explicit `Io` capabilities, provider-bound `Reader`/`Writer` handles, existing-file open/close operations, allocation-free `write_all`, and comptime-expanded writer-first `print`; formatting supports natural `{}`, byte `{s}`, decimal `{d}`, integer `{b}` / `{o}` / `{x}` / `{X}`, byte-character `{c}`, scientific float `{e}`, and `{{` / `}}`, with malformed formats and incompatible tuple fields rejected at comptime - entry points are either `main func() ...` or `main func(init process.Init) ...`; `std/process.Init` carries startup capabilities, currently only `io`, while the system provider remains hidden inside `std/io` - `std/debug.print` is an allocation-free, failure-ignoring stderr escape hatch independent of `process.Init` diff --git a/README.md b/README.md index 57a309a..2de3ada 100644 --- a/README.md +++ b/README.md @@ -9,18 +9,15 @@ odin build . -out:build/brolang ./build/prototype ``` -Programs may receive the system I/O capability explicitly. Readers and writers -pair that implementation with a stream; `main func() ...` remains valid. +Programs may receive the system I/O capability explicitly. Standard-stream +helpers bind the provider, handle, and callback; `main func() ...` remains valid. ```bro io :: import "@std/io" process :: import "@std/process" main func(init process.Init) void { - io.print(io.Writer { - impl = init.io, - stream = .stdout, - }, "hello {s} {d}\n", {"bro", 37}) catch |_| { + io.print(io.stdout(init.io), "hello {s} {d}\n", {"bro", 37}) catch |_| { return } } diff --git a/TODO.md b/TODO.md index f16999d..9e5d86a 100644 --- a/TODO.md +++ b/TODO.md @@ -627,7 +627,7 @@ parameters 25. dynamic heap allocation (implemented; v1) - - `std/mem` exposes a plain-data `Allocator` contract with `?*mut anyopaque` context and `alloc`, `realloc`, and `free` `@func` pointers + - `std/mem` exposes a plain-data `Allocator` contract with `?@mut anyopaque` context and `alloc`, `realloc`, and `free` `@func` pointers - `mem.c_allocator` is the libc-backed allocator; `mem.alloc(mem.c_allocator, size, alignment)` returns nullable mutable byte memory - `mem.free(mem.c_allocator, ptr, size, alignment)` frees with the same allocator; `malloc` handles default-aligned requests and `posix_memalign` handles larger power-of-two alignments - `mem.realloc` preserves alignment and the original allocation on failure; zero size frees, and over-aligned blocks use allocate/copy/free @@ -811,9 +811,11 @@ 33. explicit I/O provider (implemented) - `main` may take one canonical `@std/process Init`; parameterless entry points remain valid - the compiler supplies a `hide system` macOS provider and constructs `Init` in the external C wrapper - - readers and writers pair an explicit provider with `stdin`, `stdout`, or `stderr` + - readers and writers bind provider context, a generalized handle, and a direct callback + - standard-stream helpers and existing-file operations use the injected provider; files retain it - `read` and `write` validate provider counts; `write_all` handles partial writes and no progress - - the system provider uses unbuffered libc `read`/`write`, retries interruption, and allocates nothing + - the system provider uses unbuffered POSIX file descriptors, retries interrupted open/read/write, + and allocates nothing 34. package declaration aliases and root `std.ArrayList` (implemented) - bare qualified aliases use `Name :: alias package.Member` without adding a keyword diff --git a/compiler_tests.odin b/compiler_tests.odin index 8137d1e..73c548f 100644 --- a/compiler_tests.odin +++ b/compiler_tests.odin @@ -2266,6 +2266,120 @@ milestone_33_injects_explicit_io_provider_and_runs_std_io :: proc(t: ^testing.T) ) } +@(test) +std_io_opens_existing_files_through_the_captured_provider :: proc(t: ^testing.T) { + directory := "/tmp/brolang-test-file-io" + main_path := "/tmp/brolang-test-file-io/main.bro" + data_path := "/tmp/brolang-test-file-io/data" + output := "/tmp/brolang-test-file-io/app" + seed := "xxxxx" + text := `io :: import "@std/io" +process :: import "@std/process" + +missing_fails func(system io.Io) bool { + _ = io.open(system, "/tmp/brolang-test-file-io/missing", .read_only) catch |err| { + return err == .open_failed + } + return false +} + +wrong_read_fails func(file io.File) bool { + buffer [1]mut u8 = [0] + _ = io.read(io.reader(file), buffer[..]) catch |err| { + return err == .not_open_for_reading + } + return false +} + +wrong_write_fails func(file io.File) bool { + _ = io.write(io.writer(file), "x") catch |err| { + return err == .not_open_for_writing + } + return false +} + +main func(init process.Init) i32 { + if !missing_fails(init.io) { + return 1 + } + + read_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_only) catch |_| { + return 2 + } + buffer [5]mut u8 = [0, 0, 0, 0, 0] + count usize = io.read(io.reader(read_file), buffer[..]) catch |_| { + return 4 + } + if count != 5 or buffer[0] != 'x' or !wrong_write_fails(read_file) { + return 5 + } + io.close(read_file) catch |_| { + return 6 + } + + write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .write_only) catch |_| { + return 7 + } + if !wrong_read_fails(write_file) { + return 8 + } + io.write_all(io.writer(write_file), "bro") catch |_| { + return 9 + } + io.close(write_file) catch |_| { + return 10 + } + + read_write_file io.File :: io.open(init.io, "/tmp/brolang-test-file-io/data", .read_write) catch |_| { + return 11 + } + count = io.read(io.reader(read_write_file), buffer[..]) catch |_| { + return 12 + } + if count != 5 or buffer[0] != 'b' or buffer[1] != 'r' or buffer[2] != 'o' { + return 13 + } + io.write_all(io.writer(read_write_file), "!") catch |_| { + return 14 + } + io.close(read_write_file) catch |_| { + return 15 + } + return 0 +} +` + + _ = os2.remove_all(directory) + defer _ = os2.remove_all(directory) + testing.expect(t, os.make_directory(directory) == nil) + testing.expect(t, os.write_entire_file(main_path, transmute([]byte)text)) + testing.expect(t, os.write_entire_file(data_path, transmute([]byte)seed)) + + status := compiler_core.compile_package( + directory, + output, + nil, + target.DEFAULT, + cimport.Options{}, + ".", + ) + testing.expect_value(t, status, 0) + state, stdout, stderr, _ := os2.process_exec( + os2.Process_Desc{command=[]string{output}}, + context.allocator, + ) + defer delete(stdout) + defer delete(stderr) + testing.expect_value(t, state.exit_code, 0) + testing.expect_value(t, len(stdout), 0) + testing.expect_value(t, len(stderr), 0) + + data, ok := os.read_entire_file(data_path) + defer delete(data) + testing.expect(t, ok) + testing.expect_value(t, string(data), "broxx!") +} + @(test) milestone_33_rejects_non_io_and_extra_main_parameters :: proc(t: ^testing.T) { cases := [?]string{ @@ -2549,7 +2663,7 @@ milestone_37_format_errors_are_reported_at_comptime :: proc(t: ^testing.T) { process :: import "@std/process" main func(init process.Init) void { - writer io.Writer :: io.Writer {impl = init.io, stream = .stdout} + writer io.Writer :: io.stdout(init.io) stored :: typeinfo!(i32) _ = stored io.print(writer, "", 1) catch |_| {} @@ -4582,7 +4696,7 @@ allocator_contract_c_allocator_global_lowers :: proc(t: ^testing.T) { found_anyopaque_context = pointer_ok && pointer_item.kind == .Pointer && pointer_item.mutable && - pointer_item.many && + !pointer_item.many && pointer_item.child == types.ANYOPAQUE } } else if name == "vtable" { diff --git a/examples/programs/arraylist/main.bro b/examples/programs/arraylist/main.bro index e58c7d1..c5818c8 100644 --- a/examples/programs/arraylist/main.bro +++ b/examples/programs/arraylist/main.bro @@ -2,15 +2,15 @@ arraylist :: import "@std/arraylist" mem :: import "@std/mem" std :: import "@std" -hide fail_alloc func(_ ?*mut anyopaque, _ usize, _ usize) ?*mut u8 { +hide fail_alloc func(_ ?@mut anyopaque, _ usize, _ usize) ?*mut u8 { return none } -hide fail_realloc func(_ ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { +hide fail_realloc func(_ ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { return none } -hide fail_free func(_ ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize) void {} +hide fail_free func(_ ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize) void {} hide fail_vtable mem.AllocatorVTable :: mem.AllocatorVTable { alloc = fail_alloc, diff --git a/examples/programs/io/main.bro b/examples/programs/io/main.bro index 31a1b33..c3868c9 100644 --- a/examples/programs/io/main.bro +++ b/examples/programs/io/main.bro @@ -1,7 +1,7 @@ io :: import "@std/io" process :: import "@std/process" -hide read_ok func(_ ?*mut anyopaque, _ io.ReadStream, buffer []mut u8) usize ! io.ReadError { +hide read_ok func(_ ?@mut anyopaque, _ io.Handle, buffer []mut u8) usize ! io.ReadError { if buffer.len == 0 { return 0 } @@ -13,85 +13,94 @@ hide read_ok func(_ ?*mut anyopaque, _ io.ReadStream, buffer []mut u8) usize ! i return 2 } -hide read_too_much func(_ ?*mut anyopaque, _ io.ReadStream, buffer []mut u8) usize ! io.ReadError { +hide read_too_much func(_ ?@mut anyopaque, _ io.Handle, buffer []mut u8) usize ! io.ReadError { return buffer.len + 1 } -hide read_eof func(_ ?*mut anyopaque, _ io.ReadStream, _ []mut u8) usize ! io.ReadError { +hide read_eof func(_ ?@mut anyopaque, _ io.Handle, _ []mut u8) usize ! io.ReadError { return 0 } -hide write_short func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { +hide write_short func(_ ?@mut anyopaque, _ io.Handle, bytes []u8) usize ! io.WriteError { if bytes.len > 2 { return 2 } return bytes.len } -hide write_none func(_ ?*mut anyopaque, _ io.WriteStream, _ []u8) usize ! io.WriteError { +hide write_none func(_ ?@mut anyopaque, _ io.Handle, _ []u8) usize ! io.WriteError { return 0 } -hide write_too_much func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { +hide write_too_much func(_ ?@mut anyopaque, _ io.Handle, bytes []u8) usize ! io.WriteError { return bytes.len + 1 } -hide ok_vtable io.IoVTable :: io.IoVTable { - read = read_ok, - write = write_short, -} - -hide read_bad_vtable io.IoVTable :: io.IoVTable { - read = read_too_much, - write = write_short, -} - -hide eof_vtable io.IoVTable :: io.IoVTable { - read = read_eof, - write = write_short, -} - -hide write_none_vtable io.IoVTable :: io.IoVTable { - read = read_ok, - write = write_none, -} - -hide write_bad_vtable io.IoVTable :: io.IoVTable { - read = read_ok, - write = write_too_much, -} - -reader_for func(vtable @io.IoVTable) io.Reader { +ok_reader func() io.Reader { return io.Reader { - impl = io.Io {context = none, vtable = vtable}, - stream = .stdin, + context = none, + handle = io.Handle {file_desc = 0}, + read = read_ok, } } -writer_for func(vtable @io.IoVTable) io.Writer { +bad_reader func() io.Reader { + return io.Reader { + context = none, + handle = io.Handle {file_desc = 0}, + read = read_too_much, + } +} + +eof_reader func() io.Reader { + return io.Reader { + context = none, + handle = io.Handle {file_desc = 0}, + read = read_eof, + } +} + +short_writer func() io.Writer { return io.Writer { - impl = io.Io {context = none, vtable = vtable}, - stream = .stdout, + context = none, + handle = io.Handle {file_desc = 0}, + write = write_short, + } +} + +none_writer func() io.Writer { + return io.Writer { + context = none, + handle = io.Handle {file_desc = 0}, + write = write_none, + } +} + +bad_writer func() io.Writer { + return io.Writer { + context = none, + handle = io.Handle {file_desc = 0}, + write = write_too_much, } } rejects_bad_read func() bool { buffer [1]mut u8 = [0] - _ = io.read(reader_for(&read_bad_vtable), buffer[..]) catch |err| { + _ = io.read(bad_reader(), buffer[..]) catch |err| { return err == .read_failed } return false } rejects_no_progress func() bool { - io.print(writer_for(&write_none_vtable), "{s}", {"x",}) catch |err| { + io.print(none_writer(), "{s}", {"x",}) catch |err| { return err == .no_progress } return false } rejects_bad_write func() bool { - _ = io.write(writer_for(&write_bad_vtable), "x") catch |err| { + _ = io.write(bad_writer(), "x") catch |err| { return err == .write_failed } return false @@ -100,32 +109,26 @@ rejects_bad_write func() bool { main func(init process.Init) i32 { system io.Io :: init.io buffer [2]mut u8 = [0, 0] - count usize :: io.read(reader_for(&ok_vtable), buffer[..]) catch 0 + count usize :: io.read(ok_reader(), buffer[..]) catch 0 if count != 2 or buffer[0] != 'o' or buffer[1] != 'k' { return 1 } - eof usize :: io.read(reader_for(&eof_vtable), buffer[..]) catch 1 - empty_read usize :: io.read(reader_for(&read_bad_vtable), buffer[0..0]) catch 1 - empty_write usize :: io.write(writer_for(&write_bad_vtable), "") catch 1 + eof usize :: io.read(eof_reader(), buffer[..]) catch 1 + empty_read usize :: io.read(bad_reader(), buffer[0..0]) catch 1 + empty_write usize :: io.write(bad_writer(), "") catch 1 if eof != 0 or empty_read != 0 or empty_write != 0 { return 5 } - io.print(writer_for(&ok_vtable), "{s}{d}", {"partial", 37}) catch |_| { + io.print(short_writer(), "{s}{d}", {"partial", 37}) catch |_| { return 2 } if !rejects_bad_read() or !rejects_no_progress() or !rejects_bad_write() { return 3 } - io.print(io.Writer { - impl = system, - stream = .stdout, - }, "io-ok {d} {{bro}}\n", {37,}) catch |_| { + io.print(io.stdout(system), "io-ok {d} {{bro}}\n", {37,}) catch |_| { return 4 } - io.print(io.Writer { - impl = system, - stream = .stdout, - }, "bounds={d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d}\n", { + io.print(io.stdout(system), "bounds={d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d} {d}/{d}\n", { minval!(i8), maxval!(u8), minval!(i16), maxval!(u16), minval!(i32), maxval!(u32), diff --git a/examples/programs/mem_allocator/typed_alloc.bro b/examples/programs/mem_allocator/typed_alloc.bro index fb15b03..b5e7682 100644 --- a/examples/programs/mem_allocator/typed_alloc.bro +++ b/examples/programs/mem_allocator/typed_alloc.bro @@ -1,23 +1,23 @@ mem :: import "@std/mem" -hide probe_count func(context ?*mut anyopaque) void { +hide probe_count func(context ?@mut anyopaque) void { if context |raw| { - counts *mut usize :: ptrcast!(usize, raw) - counts[0] += 1 + count @mut usize :: ptrcast!(usize, raw) + count^ += 1 } } -hide probe_alloc func(context ?*mut anyopaque, _ usize, _ usize) ?*mut u8 { +hide probe_alloc func(context ?@mut anyopaque, _ usize, _ usize) ?*mut u8 { probe_count(context) return none } -hide probe_realloc func(context ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { +hide probe_realloc func(context ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize, _ usize) ?*mut u8 { probe_count(context) return none } -hide probe_free func(context ?*mut anyopaque, _ ?*mut u8, _ usize, _ usize) void { +hide probe_free func(context ?@mut anyopaque, _ ?*mut u8, _ usize, _ usize) void { probe_count(context) } @@ -33,11 +33,11 @@ typed_allocator_test func() i32 { first_calls [1]mut usize = [0] second_calls [1]mut usize = [0] first_allocator mem.Allocator :: mem.Allocator { - context = (&first_calls).ptr, + context = &first_calls, vtable = &probe_vtable, } second_allocator mem.Allocator :: mem.Allocator { - context = (&second_calls).ptr, + context = &second_calls, vtable = &probe_vtable, } diff --git a/examples/programs/milestone_39/main.bro b/examples/programs/milestone_39/main.bro index a1de9cd..8b784e6 100644 --- a/examples/programs/milestone_39/main.bro +++ b/examples/programs/milestone_39/main.bro @@ -79,7 +79,7 @@ main func(init process.Init) i32 { return 2 } - writer io.Writer :: io.Writer {impl = init.io, stream = .stdout} + writer io.Writer :: io.stdout(init.io) positive f64 = 1.0 zero f64 = 0.0 infinity f64 :: positive / zero diff --git a/ffi/c/posix.bro b/ffi/c/posix.bro index 7b4a190..7df17b3 100644 --- a/ffi/c/posix.bro +++ b/ffi/c/posix.bro @@ -1,3 +1,16 @@ +O_RDONLY c_int :: 0 +O_WRONLY c_int :: 1 +O_RDWR c_int :: 2 + +STDIN_FILENO c_int :: 0 +STDOUT_FILENO c_int :: 1 +STDERR_FILENO c_int :: 2 + +EINTR c_int :: 4 +EBADF c_int :: 9 + +open c_func(_ *c_char, _ c_int, ...) c_int +close c_func(_ c_int) c_int read c_func(_ c_int, _ ?*mut anyopaque, _ c_ulong) c_long write c_func(_ c_int, _ ?*anyopaque, _ c_ulong) c_long __error c_func() *mut c_int diff --git a/std/debug/debug.bro b/std/debug/debug.bro index ecaaeeb..a7a69d8 100644 --- a/std/debug/debug.bro +++ b/std/debug/debug.bro @@ -1,36 +1,28 @@ import "@ffi/c" import "@std/io" -hide write func(_ ?*mut anyopaque, _ io.WriteStream, bytes []u8) usize ! io.WriteError { +print func($format []u8, $Args type, args Args) void { + writer io.Writer :: io.Writer{ + context = none, + handle = io.Handle{ file_desc = c_int(io.Stream.stderr) }, + write = write, + } + io.print(writer, format, Args, args) catch |_| {} +} + +hide write func(_ ?@mut anyopaque, handle io.Handle, bytes []u8) usize ! io.WriteError { request usize = bytes.len maximum usize :: usize(maxval!(c_long)) if request > maximum { request = maximum } while true { - count c_long :: c.write(2, bytes.ptr, c_ulong(request)) + count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request)) if count >= 0 { return usize(count) } - if c.__error()^ != 4 { + if c.__error()^ != c.EINTR { return .write_failed } } } - -hide read func(_ ?*mut anyopaque, _ io.ReadStream, _ []mut u8) usize ! io.ReadError { - return .read_failed -} - -hide vtable io.IoVTable :: io.IoVTable { - read = read, - write = write, -} - -print func($format []u8, $Args type, args Args) void { - writer io.Writer :: io.Writer { - impl = io.Io {context = none, vtable = &vtable}, - stream = .stderr, - } - io.print(writer, format, Args, args) catch |_| {} -} diff --git a/std/io/file.bro b/std/io/file.bro new file mode 100644 index 0000000..141a862 --- /dev/null +++ b/std/io/file.bro @@ -0,0 +1,144 @@ +import "@ffi/c" + +File :: struct { + io Io + handle Handle +} + +FileMode :: enum { + read_only + write_only + read_write +} + +OpenError :: enum { + open_failed +} + +CloseError :: enum { + close_failed +} + +open func(io Io, path [;0]u8, mode FileMode) File ! OpenError { + handle Handle :: io.vtable.open(io.context, path, mode) catch |err| { + return err + } + return File {io = io, handle = handle} +} + +close func(file File) void ! CloseError { + try file.io.vtable.close(file.io.context, file.handle) +} + +reader func(file File) Reader { + return Reader { + context = file.io.context, + handle = file.handle, + read = file.io.vtable.read, + } +} + +writer func(file File) Writer { + return Writer { + context = file.io.context, + handle = file.handle, + write = file.io.vtable.write, + } +} + +hide system_read func(_ ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError { + request usize = buffer.len + maximum usize :: usize(maxval!(c_long)) + if request > maximum { + request = maximum + } + while true { + count c_long :: c.read(handle.file_desc, buffer.ptr, c_ulong(request)) + if count >= 0 { + return usize(count) + } + errno c_int :: c.__error()^ + if errno == c.EINTR { + continue + } + if errno == c.EBADF { + return .not_open_for_reading + } + return .read_failed + } +} + +hide system_write func(_ ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError { + request usize = bytes.len + maximum usize :: usize(maxval!(c_long)) + if request > maximum { + request = maximum + } + while true { + count c_long :: c.write(handle.file_desc, bytes.ptr, c_ulong(request)) + if count >= 0 { + return usize(count) + } + errno c_int :: c.__error()^ + if errno == c.EINTR { + continue + } + if errno == c.EBADF { + return .not_open_for_writing + } + return .write_failed + } +} + +hide system_open func(_ ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError { + flags c_int = c.O_RDONLY + match mode { + .read_only: flags = c.O_RDONLY + .write_only: flags = c.O_WRONLY + .read_write: flags = c.O_RDWR + } + while true { + fd c_int :: c.open(ptrcast!(c_char, path.ptr), flags) + if fd >= 0 { + return Handle {file_desc = fd} + } + if c.__error()^ != c.EINTR { + return .open_failed + } + } +} + +hide system_close func(_ ?@mut anyopaque, handle Handle) void ! CloseError { + if c.close(handle.file_desc) != 0 { + return .close_failed + } +} + +hide system_stdin func(_ ?@mut anyopaque) Handle { + return Handle {file_desc = c_int(Stream.stdin)} +} + +hide system_stdout func(_ ?@mut anyopaque) Handle { + return Handle {file_desc = c_int(Stream.stdout)} +} + +hide system_stderr func(_ ?@mut anyopaque) Handle { + return Handle {file_desc = c_int(Stream.stderr)} +} + +hide system_vtable IoVTable :: IoVTable { + read = system_read, + write = system_write, + open = system_open, + close = system_close, + stdin = system_stdin, + stdout = system_stdout, + stderr = system_stderr, +} + +hide system func() Io { + return Io { + context = none, + vtable = &system_vtable, + } +} diff --git a/std/io/io.bro b/std/io/io.bro index cb7911f..11e2c4b 100644 --- a/std/io/io.bro +++ b/std/io/io.bro @@ -2,69 +2,80 @@ import "@ffi/c" import "@std/meta" ReadError :: enum { + not_open_for_reading read_failed } WriteError :: enum { + not_open_for_writing write_failed no_progress } +Handle :: union { + file_desc c_int + ptr @mut anyopaque +} + +Stream :: enum(c_int) { + stdin = c.STDIN_FILENO + stdout = c.STDOUT_FILENO + stderr = c.STDERR_FILENO +} + Io :: struct { - context ?*mut anyopaque + context ?@mut anyopaque vtable @IoVTable } IoVTable :: struct { - read @func(context ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError - write @func(context ?*mut anyopaque, stream WriteStream, bytes []u8) usize ! WriteError -} - -ReadStream :: enum(c_int) { - stdin = 0 -} - -WriteStream :: enum(c_int) { - stdout = 1 - stderr = 2 + read @func(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError + write @func(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError + open @func(context ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError + close @func(context ?@mut anyopaque, handle Handle) void ! CloseError + stdin @func(context ?@mut anyopaque) Handle + stdout @func(context ?@mut anyopaque) Handle + stderr @func(context ?@mut anyopaque) Handle } Reader :: struct { - impl Io - stream ReadStream + context ?@mut anyopaque + handle Handle + read @func(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError } Writer :: struct { - impl Io - stream WriteStream + context ?@mut anyopaque + handle Handle + write @func(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError } -read func(reader Reader, buffer []mut u8) usize ! ReadError { +read func(input Reader, buffer []mut u8) usize ! ReadError { if buffer.len == 0 { return 0 } - count usize :: try reader.impl.vtable.read(reader.impl.context, reader.stream, buffer) + count usize :: try input.read(input.context, input.handle, buffer) if count > buffer.len { return .read_failed } return count } -write func(writer Writer, bytes []u8) usize ! WriteError { +write func(output Writer, bytes []u8) usize ! WriteError { if bytes.len == 0 { return 0 } - count usize :: try writer.impl.vtable.write(writer.impl.context, writer.stream, bytes) + count usize :: try output.write(output.context, output.handle, bytes) if count > bytes.len { return .write_failed } return count } -write_all func(writer Writer, bytes []u8) void ! WriteError { +write_all func(output Writer, bytes []u8) void ! WriteError { offset usize = 0 while offset < bytes.len { - count usize :: write(writer, bytes[offset..]) catch |err| { + count usize :: write(output, bytes[offset..]) catch |err| { return err } if count == 0 { @@ -75,7 +86,49 @@ write_all func(writer Writer, bytes []u8) void ! WriteError { return } -hide write_integer_signed func(writer Writer, value i64, base u64, uppercase bool) void ! WriteError { +stdin func(io Io) Reader { + return Reader { + context = io.context, + handle = io.vtable.stdin(io.context), + read = io.vtable.read, + } +} + +stdout func(io Io) Writer { + return Writer { + context = io.context, + handle = io.vtable.stdout(io.context), + write = io.vtable.write, + } +} + +stderr func(io Io) Writer { + return Writer { + context = io.context, + handle = io.vtable.stderr(io.context), + write = io.vtable.write, + } +} + +print func(output Writer, $format []u8, $Args type, args Args) void ! WriteError { + expand for parse_format(format.len, format, Args) |token| { + match token.kind { + .unused: break + .literal: try write_all(output, format[token.start..token.end]) + .string: try write_all(output, field!(args, token.field)) + .default: try write_default(output, field!(args, token.field)) + .decimal: try write_decimal(output, field!(args, token.field)) + .binary: try write_integer(output, field!(args, token.field), 2, false) + .octal: try write_integer(output, field!(args, token.field), 8, false) + .hex_lower: try write_integer(output, field!(args, token.field), 16, false) + .hex_upper: try write_integer(output, field!(args, token.field), 16, true) + .character: try write_character(output, field!(args, token.field)) + else: try write_float(output, field!(args, token.field), true) + } + } +} + +hide write_integer_signed func(output Writer, value i64, base u64, uppercase bool) void ! WriteError { buffer [65]mut u8 = undefined end usize = buffer.len current i64 = value @@ -104,11 +157,11 @@ hide write_integer_signed func(writer Writer, value i64, base u64, uppercase boo end -= 1 buffer[end] = '-' } - try write_all(writer, buffer[end..]) + try write_all(output, buffer[end..]) return } -hide write_integer_unsigned func(writer Writer, value u64, base u64, uppercase bool) void ! WriteError { +hide write_integer_unsigned func(output Writer, value u64, base u64, uppercase bool) void ! WriteError { buffer [65]mut u8 = undefined end usize = buffer.len current u64 = value @@ -127,7 +180,7 @@ hide write_integer_unsigned func(writer Writer, value u64, base u64, uppercase b break } } - try write_all(writer, buffer[end..]) + try write_all(output, buffer[end..]) return } @@ -264,12 +317,12 @@ hide format_field_name func($T type, index usize) []u8 { } } -hide write_integer func(writer Writer, $T type, value T, base u64, uppercase bool) void ! WriteError { +hide write_integer func(output Writer, $T type, value T, base u64, uppercase bool) void ! WriteError { match typeinfo!(T) { .integer: if minval!(T) < 0 { - try write_integer_signed(writer, i64(value), base, uppercase) + try write_integer_signed(output, i64(value), base, uppercase) } else { - try write_integer_unsigned(writer, u64(value), base, uppercase) + try write_integer_unsigned(output, u64(value), base, uppercase) } else: compile_error!("io.print integer format requires an integer argument") } @@ -277,7 +330,7 @@ hide write_integer func(writer Writer, $T type, value T, base u64, uppercase boo } # note: libc keeps float formatting small; replace it with a native shortest-roundtrip writer if locale independence matters. -hide write_float func(writer Writer, $T type, value T, scientific bool) void ! WriteError { +hide write_float func(output Writer, $T type, value T, scientific bool) void ! WriteError { match typeinfo!(T) { .float: { buffer [64]mut u8 = undefined @@ -296,53 +349,53 @@ hide write_float func(writer Writer, $T type, value T, scientific bool) void ! W if count < 0 or usize(count) >= buffer.len { return .write_failed } - try write_all(writer, buffer[0..usize(count)]) + try write_all(output, buffer[0..usize(count)]) } else: compile_error!("io.print float format requires a float argument") } return } -hide write_decimal func(writer Writer, $T type, value T) void ! WriteError { +hide write_decimal func(output Writer, $T type, value T) void ! WriteError { match typeinfo!(T) { - .integer: try write_integer(writer, value, 10, false) - .float: try write_float(writer, value, false) + .integer: try write_integer(output, value, 10, false) + .float: try write_float(output, value, false) else: compile_error!("io.print '{d}' requires an integer or float argument") } return } -hide write_character func(writer Writer, $T type, value T) void ! WriteError { +hide write_character func(output Writer, $T type, value T) void ! WriteError { match typeinfo!(T) { .integer: { if minval!(T) < 0 or maxval!(T) > 255 { compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") } buffer [1]u8 = [u8(value)] - try write_all(writer, buffer[..]) + try write_all(output, buffer[..]) } else: compile_error!("io.print '{c}' requires an unsigned integer that fits in u8") } return } -hide write_default func(writer Writer, $T type, value T) void ! WriteError { +hide write_default func(output Writer, $T type, value T) void ! WriteError { match typeinfo!(T) { .bool: if value { - try write_all(writer, "true") + try write_all(output, "true") } else { - try write_all(writer, "false") + try write_all(output, "false") } - .integer: try write_integer(writer, value, 10, false) - .float: try write_float(writer, value, false) - .array: try write_all(writer, value) - .pointer: try write_all(writer, value) - .slice: try write_all(writer, value) + .integer: try write_integer(output, value, 10, false) + .float: try write_float(output, value, false) + .array: try write_all(output, value) + .pointer: try write_all(output, value) + .slice: try write_all(output, value) .enum |enum_info|: { expand for enum_info.fields |field| { if value == field!(T, field.name) { - try write_all(writer, ".") - try write_all(writer, field.name) + try write_all(output, ".") + try write_all(output, field.name) return } } @@ -352,68 +405,3 @@ hide write_default func(writer Writer, $T type, value T) void ! WriteError { } return } - -print func(writer Writer, $format []u8, $Args type, args Args) void ! WriteError { - expand for parse_format(format.len, format, Args) |token| { - match token.kind { - .unused: break - .literal: try write_all(writer, format[token.start..token.end]) - .string: try write_all(writer, field!(args, token.field)) - .default: try write_default(writer, field!(args, token.field)) - .decimal: try write_decimal(writer, field!(args, token.field)) - .binary: try write_integer(writer, field!(args, token.field), 2, false) - .octal: try write_integer(writer, field!(args, token.field), 8, false) - .hex_lower: try write_integer(writer, field!(args, token.field), 16, false) - .hex_upper: try write_integer(writer, field!(args, token.field), 16, true) - .character: try write_character(writer, field!(args, token.field)) - else: try write_float(writer, field!(args, token.field), true) - } - } -} - -hide system_read func(_ ?*mut anyopaque, stream ReadStream, buffer []mut u8) usize ! ReadError { - request usize = buffer.len - maximum usize :: usize(maxval!(c_long)) - if request > maximum { - request = maximum - } - while true { - count c_long :: c.read(c_int(stream), buffer.ptr, c_ulong(request)) - if count >= 0 { - return usize(count) - } - if c.__error()^ != 4 { - return .read_failed - } - } -} - -hide system_write func(_ ?*mut anyopaque, stream WriteStream, bytes []u8) usize ! WriteError { - fd c_int :: c_int(stream) - request usize = bytes.len - maximum usize :: usize(maxval!(c_long)) - if request > maximum { - request = maximum - } - while true { - count c_long :: c.write(fd, bytes.ptr, c_ulong(request)) - if count >= 0 { - return usize(count) - } - if c.__error()^ != 4 { - return .write_failed - } - } -} - -hide system_vtable IoVTable :: IoVTable { - read = system_read, - write = system_write, -} - -hide system func() Io { - return Io { - context = none, - vtable = &system_vtable, - } -} diff --git a/std/mem/mem.bro b/std/mem/mem.bro index 12a75dd..6f97d01 100644 --- a/std/mem/mem.bro +++ b/std/mem/mem.bro @@ -5,14 +5,14 @@ AllocError :: enum { } Allocator :: struct { - context ?*mut anyopaque + context ?@mut anyopaque vtable @AllocatorVTable } AllocatorVTable :: struct { - alloc @func(context ?*mut anyopaque, size usize, alignment usize) ?*mut u8 - realloc @func(context ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 - free @func(context ?*mut anyopaque, memory ?*mut u8, size usize, alignment usize) void + alloc @func(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 + realloc @func(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 + free @func(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void } raw_alloc func(allocator Allocator, size usize, alignment usize) ?*mut u8 { @@ -135,7 +135,7 @@ hide power_of_two func(value usize) bool { return true } -hide c_alloc func(_ ?*mut anyopaque, size usize, alignment usize) ?*mut u8 { +hide c_alloc func(_ ?@mut anyopaque, size usize, alignment usize) ?*mut u8 { if power_of_two(alignment) == false { return none } @@ -153,7 +153,7 @@ hide c_alloc func(_ ?*mut anyopaque, size usize, alignment usize) ?*mut u8 { return ptrcast!(u8, memory[0]) } -hide c_realloc func(_ ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { +hide c_realloc func(_ ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 { if power_of_two(alignment) == false { return none } @@ -186,7 +186,7 @@ hide c_realloc func(_ ?*mut anyopaque, memory ?*mut u8, old_size usize, new_size return c_alloc(none, new_size, alignment) } -hide c_free func(_ ?*mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { +hide c_free func(_ ?@mut anyopaque, memory ?*mut u8, _ usize, _ usize) void { c.free(memory) }