Skip to content

Commit 5da719e

Browse files
committed
WIP
1 parent 94cda04 commit 5da719e

6 files changed

Lines changed: 222 additions & 44 deletions

File tree

src/elz/core.zig

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,8 @@ pub const Port = struct {
237237
is_open: bool,
238238
/// Name of the file (for error messages).
239239
name: []const u8,
240+
/// One-byte lookahead buffer used by `peekChar`. Empty when no char has been peeked.
241+
peek_buffer: ?u8 = null,
240242

241243
pub fn openInput(allocator: std.mem.Allocator, io: std.Io, name: []const u8) !Port {
242244
const file = try std.Io.Dir.cwd().openFile(io, name, .{});
@@ -246,6 +248,7 @@ pub const Port = struct {
246248
.is_input = true,
247249
.is_open = true,
248250
.name = try allocator.dupe(u8, name),
251+
.peek_buffer = null,
249252
};
250253
}
251254

@@ -257,6 +260,7 @@ pub const Port = struct {
257260
.is_input = false,
258261
.is_open = true,
259262
.name = try allocator.dupe(u8, name),
263+
.peek_buffer = null,
260264
};
261265
}
262266

@@ -270,20 +274,17 @@ pub const Port = struct {
270274
pub fn readLine(self: *Port, allocator: std.mem.Allocator) !?[]const u8 {
271275
if (!self.is_input or !self.is_open) return null;
272276
var buf: [4096]u8 = undefined;
273-
var read_buf: [1]u8 = undefined;
274277
var len: usize = 0;
275278

276279
while (len < buf.len - 1) {
277-
const bytes_read = self.file.readStreaming(self.io, &.{&read_buf}) catch |err| switch (err) {
278-
error.EndOfStream => 0,
279-
else => return null,
280-
};
281-
if (bytes_read == 0) {
280+
const c_opt = try self.readChar();
281+
if (c_opt == null) {
282282
if (len == 0) return null;
283283
break;
284284
}
285-
if (read_buf[0] == '\n') break;
286-
buf[len] = read_buf[0];
285+
const c = c_opt.?;
286+
if (c == '\n') break;
287+
buf[len] = c;
287288
len += 1;
288289
}
289290

@@ -293,6 +294,10 @@ pub const Port = struct {
293294

294295
pub fn readChar(self: *Port) !?u8 {
295296
if (!self.is_input or !self.is_open) return null;
297+
if (self.peek_buffer) |c| {
298+
self.peek_buffer = null;
299+
return c;
300+
}
296301
var buf: [1]u8 = undefined;
297302
const bytes_read = self.file.readStreaming(self.io, &.{&buf}) catch |err| switch (err) {
298303
error.EndOfStream => return null,
@@ -302,6 +307,17 @@ pub const Port = struct {
302307
return buf[0];
303308
}
304309

310+
pub fn peekChar(self: *Port) !?u8 {
311+
if (!self.is_input or !self.is_open) return null;
312+
if (self.peek_buffer) |c| return c;
313+
const c_opt = try self.readChar();
314+
if (c_opt) |c| {
315+
self.peek_buffer = c;
316+
return c;
317+
}
318+
return null;
319+
}
320+
305321
pub fn writeString(self: *Port, str: []const u8) !void {
306322
if (self.is_input or !self.is_open) return error.InvalidPort;
307323
try self.file.writeStreamingAll(self.io, str);

src/elz/env_setup.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,9 @@ pub fn populate_ports(interp: *interpreter.Interpreter) !void {
301301
try interp.root_env.set(interp, "close-output-port", core.Value{ .procedure = ports.close_output_port });
302302
try interp.root_env.set(interp, "read-line", core.Value{ .procedure = ports.read_line });
303303
try interp.root_env.set(interp, "read-char", core.Value{ .procedure = ports.read_char });
304+
try interp.root_env.set(interp, "peek-char", core.Value{ .procedure = ports.peek_char });
305+
try interp.root_env.set(interp, "char-ready?", core.Value{ .procedure = ports.char_ready_p });
306+
try interp.root_env.set(interp, "write-char", core.Value{ .procedure = ports.write_char });
304307
try interp.root_env.set(interp, "write-port", core.Value{ .procedure = ports.write_to_port });
305308
try interp.root_env.set(interp, "input-port?", core.Value{ .procedure = ports.is_input_port });
306309
try interp.root_env.set(interp, "output-port?", core.Value{ .procedure = ports.is_output_port });

src/elz/primitives/io.zig

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,49 @@ const interpreter = @import("../interpreter.zig");
1616
///
1717
/// Returns:
1818
/// An unspecified value, or an error if writing to stdout fails.
19-
pub fn display(interp: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
20-
if (args.items.len != 1) return ElzError.WrongArgumentCount;
21-
var buffer: [4096]u8 = undefined;
22-
const stdout_file = std.Io.File.stdout();
23-
var stdout_writer = stdout_file.writer(interp.io, &buffer);
24-
const aw = &stdout_writer.interface;
25-
const value = args.items[0];
26-
19+
/// Renders a value in display mode (strings unquoted, chars as raw codepoints).
20+
fn render_display(value: Value, w: *std.Io.Writer) !void {
2721
switch (value) {
28-
.string => |s| aw.writeAll(s) catch return ElzError.ForeignFunctionError,
22+
.string => |s| try w.writeAll(s),
2923
.character => |c| {
3024
if (c > 0x10FFFF) return ElzError.InvalidArgument;
31-
3225
const codepoint: u21 = @intCast(c);
33-
if (!std.unicode.utf8ValidCodepoint(codepoint)) {
34-
return ElzError.InvalidArgument;
35-
}
36-
26+
if (!std.unicode.utf8ValidCodepoint(codepoint)) return ElzError.InvalidArgument;
3727
var buf: [4]u8 = undefined;
38-
const len = std.unicode.utf8Encode(codepoint, &buf) catch {
39-
return ElzError.InvalidArgument;
40-
};
41-
aw.writeAll(buf[0..@as(usize, @intCast(len))]) catch return ElzError.ForeignFunctionError;
28+
const len = std.unicode.utf8Encode(codepoint, &buf) catch return ElzError.InvalidArgument;
29+
try w.writeAll(buf[0..@as(usize, @intCast(len))]);
4230
},
43-
else => writer.write(value, aw) catch return ElzError.ForeignFunctionError,
31+
else => try writer.write(value, w),
32+
}
33+
}
34+
35+
/// Writes the rendered bytes from `aw` either to stdout (when `port_opt` is null) or to
36+
/// the supplied port. The output is fully buffered and flushed at the end.
37+
fn flush_to_destination(interp: *interpreter.Interpreter, aw: *std.Io.Writer.Allocating, port_opt: ?Value) ElzError!void {
38+
const bytes = aw.written();
39+
if (port_opt) |port_val| {
40+
if (port_val != .port) return ElzError.InvalidArgument;
41+
port_val.port.writeString(bytes) catch return ElzError.ForeignFunctionError;
42+
return;
4443
}
45-
aw.flush() catch return ElzError.ForeignFunctionError;
44+
var buffer: [4096]u8 = undefined;
45+
const stdout_file = std.Io.File.stdout();
46+
var stdout_writer = stdout_file.writer(interp.io, &buffer);
47+
const out = &stdout_writer.interface;
48+
out.writeAll(bytes) catch return ElzError.ForeignFunctionError;
49+
out.flush() catch return ElzError.ForeignFunctionError;
50+
}
51+
52+
pub fn display(interp: *interpreter.Interpreter, env: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
53+
if (args.items.len < 1 or args.items.len > 2) return ElzError.WrongArgumentCount;
54+
var aw: std.Io.Writer.Allocating = .init(env.allocator);
55+
defer aw.deinit();
56+
render_display(args.items[0], &aw.writer) catch |err| switch (err) {
57+
ElzError.InvalidArgument => return ElzError.InvalidArgument,
58+
else => return ElzError.ForeignFunctionError,
59+
};
60+
const port_opt: ?Value = if (args.items.len == 2) args.items[1] else null;
61+
try flush_to_destination(interp, &aw, port_opt);
4662
return Value.unspecified;
4763
}
4864

@@ -54,14 +70,13 @@ pub fn display(interp: *interpreter.Interpreter, _: *core.Environment, args: cor
5470
///
5571
/// Returns:
5672
/// An unspecified value, or an error if writing to stdout fails.
57-
pub fn write_proc(interp: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
58-
if (args.items.len != 1) return ElzError.WrongArgumentCount;
59-
var buffer: [4096]u8 = undefined;
60-
const stdout_file = std.Io.File.stdout();
61-
var stdout_writer = stdout_file.writer(interp.io, &buffer);
62-
const aw = &stdout_writer.interface;
63-
writer.write(args.items[0], aw) catch return ElzError.ForeignFunctionError;
64-
aw.flush() catch return ElzError.ForeignFunctionError;
73+
pub fn write_proc(interp: *interpreter.Interpreter, env: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
74+
if (args.items.len < 1 or args.items.len > 2) return ElzError.WrongArgumentCount;
75+
var aw: std.Io.Writer.Allocating = .init(env.allocator);
76+
defer aw.deinit();
77+
writer.write(args.items[0], &aw.writer) catch return ElzError.ForeignFunctionError;
78+
const port_opt: ?Value = if (args.items.len == 2) args.items[1] else null;
79+
try flush_to_destination(interp, &aw, port_opt);
6580
return Value.unspecified;
6681
}
6782

@@ -73,14 +88,13 @@ pub fn write_proc(interp: *interpreter.Interpreter, _: *core.Environment, args:
7388
///
7489
/// Returns:
7590
/// An unspecified value, or an error if writing to stdout fails.
76-
pub fn newline(interp: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
77-
if (args.items.len != 0) return ElzError.WrongArgumentCount;
78-
var buffer: [4096]u8 = undefined;
79-
const stdout_file = std.Io.File.stdout();
80-
var stdout_writer = stdout_file.writer(interp.io, &buffer);
81-
const aw = &stdout_writer.interface;
82-
aw.writeAll("\n") catch return ElzError.ForeignFunctionError;
83-
aw.flush() catch return ElzError.ForeignFunctionError;
91+
pub fn newline(interp: *interpreter.Interpreter, env: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
92+
if (args.items.len > 1) return ElzError.WrongArgumentCount;
93+
var aw: std.Io.Writer.Allocating = .init(env.allocator);
94+
defer aw.deinit();
95+
aw.writer.writeAll("\n") catch return ElzError.ForeignFunctionError;
96+
const port_opt: ?Value = if (args.items.len == 1) args.items[0] else null;
97+
try flush_to_destination(interp, &aw, port_opt);
8498
return Value.unspecified;
8599
}
86100

src/elz/primitives/ports.zig

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,54 @@ pub fn read_char(_: *interpreter.Interpreter, _: *core.Environment, args: core.V
8888
return Value{ .symbol = "eof" };
8989
}
9090

91+
/// `peek_char` returns the next character on an input port without consuming it.
92+
/// Syntax: (peek-char port)
93+
pub fn peek_char(_: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
94+
if (args.items.len != 1) return ElzError.WrongArgumentCount;
95+
96+
const port_val = args.items[0];
97+
if (port_val != .port) return ElzError.InvalidArgument;
98+
99+
const char = port_val.port.peekChar() catch return ElzError.IOError;
100+
if (char) |c| {
101+
return Value{ .character = c };
102+
}
103+
return Value{ .symbol = "eof" };
104+
}
105+
106+
/// `char_ready_p` reports whether a character is available on an input port.
107+
/// File-backed ports always have a character available until end-of-file, so this
108+
/// simply returns `#t` for any open input port.
109+
/// Syntax: (char-ready? port)
110+
pub fn char_ready_p(_: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
111+
if (args.items.len != 1) return ElzError.WrongArgumentCount;
112+
113+
const port_val = args.items[0];
114+
if (port_val != .port) return ElzError.InvalidArgument;
115+
return Value{ .boolean = port_val.port.is_input and port_val.port.is_open };
116+
}
117+
118+
/// `write_char` writes a single character to an output port as UTF-8.
119+
/// Syntax: (write-char char port)
120+
pub fn write_char(_: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {
121+
if (args.items.len != 2) return ElzError.WrongArgumentCount;
122+
123+
const char_val = args.items[0];
124+
const port_val = args.items[1];
125+
if (char_val != .character) return ElzError.InvalidArgument;
126+
if (port_val != .port) return ElzError.InvalidArgument;
127+
128+
const cp = char_val.character;
129+
if (cp > 0x10FFFF) return ElzError.InvalidArgument;
130+
const codepoint: u21 = @intCast(cp);
131+
if (!std.unicode.utf8ValidCodepoint(codepoint)) return ElzError.InvalidArgument;
132+
133+
var buf: [4]u8 = undefined;
134+
const len = std.unicode.utf8Encode(codepoint, &buf) catch return ElzError.InvalidArgument;
135+
port_val.port.writeString(buf[0..@as(usize, @intCast(len))]) catch return ElzError.IOError;
136+
return Value.unspecified;
137+
}
138+
91139
/// `write_string_to_port` writes a string to an output port.
92140
/// Syntax: (write-port str port)
93141
pub fn write_to_port(_: *interpreter.Interpreter, _: *core.Environment, args: core.ValueList, _: *u64) ElzError!Value {

src/stdlib/std.elz

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,28 @@
518518
((lambda (g) (g g))
519519
(lambda (g)
520520
(f (lambda (a) ((g g) a)))))))
521+
522+
;;; ---------------------------------
523+
;;; File Combinators
524+
;;; ---------------------------------
525+
526+
;;; (call-with-input-file path proc)
527+
;;;
528+
;;; Opens path for reading, applies proc to the resulting port, closes the port,
529+
;;; and returns the value proc produced. The port is closed on a normal return
530+
;;; only; if proc raises an error the port leaks.
531+
(define (call-with-input-file path proc)
532+
(let ((port (open-input-file path)))
533+
(let ((result (proc port)))
534+
(close-input-port port)
535+
result)))
536+
537+
;;; (call-with-output-file path proc)
538+
;;;
539+
;;; Opens path for writing, applies proc to the resulting port, closes the port,
540+
;;; and returns the value proc produced.
541+
(define (call-with-output-file path proc)
542+
(let ((port (open-output-file path)))
543+
(let ((result (proc port)))
544+
(close-output-port port)
545+
result)))

tests/test_stdlib.elz

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,78 @@
257257
(test-case "string list roundtrip"
258258
(string=? (list->string (string->list "round-trip")) "round-trip"))
259259

260+
;; --- I/O Tests ---
261+
(test-case "call-with-output-file then call-with-input-file roundtrips bytes"
262+
(let ((path "/tmp/elz-iotest.txt"))
263+
(call-with-output-file path
264+
(lambda (port)
265+
(write-port "hello" port)))
266+
(let ((content (call-with-input-file path
267+
(lambda (port) (read-line port)))))
268+
(delete-file path)
269+
(string=? content "hello"))))
270+
(test-case "peek-char does not consume"
271+
(let ((path "/tmp/elz-peek.txt"))
272+
(call-with-output-file path
273+
(lambda (port) (write-port "abc" port)))
274+
(let ((result (call-with-input-file path
275+
(lambda (port)
276+
(let ((peeked (peek-char port))
277+
(read-1 (read-char port))
278+
(read-2 (read-char port)))
279+
(and (char=? peeked #\a)
280+
(char=? read-1 #\a)
281+
(char=? read-2 #\b)))))))
282+
(delete-file path)
283+
result)))
284+
(test-case "char-ready? on open input port"
285+
(let ((path "/tmp/elz-ready.txt"))
286+
(call-with-output-file path
287+
(lambda (port) (write-port "x" port)))
288+
(let ((result (call-with-input-file path
289+
(lambda (port) (char-ready? port)))))
290+
(delete-file path)
291+
result)))
292+
(test-case "write-char writes a single byte"
293+
(let ((path "/tmp/elz-wc.txt"))
294+
(call-with-output-file path
295+
(lambda (port)
296+
(write-char #\Z port)
297+
(write-char #\! port)))
298+
(let ((content (call-with-input-file path
299+
(lambda (port) (read-line port)))))
300+
(delete-file path)
301+
(string=? content "Z!"))))
302+
(test-case "display with port writes display form"
303+
(let ((path "/tmp/elz-disp.txt"))
304+
(call-with-output-file path
305+
(lambda (port)
306+
(display "no quotes" port)))
307+
(let ((content (call-with-input-file path
308+
(lambda (port) (read-line port)))))
309+
(delete-file path)
310+
(string=? content "no quotes"))))
311+
(test-case "write with port writes machine-readable form"
312+
(let ((path "/tmp/elz-wr.txt"))
313+
(call-with-output-file path
314+
(lambda (port)
315+
(write "with quotes" port)))
316+
(let ((content (call-with-input-file path
317+
(lambda (port) (read-line port)))))
318+
(delete-file path)
319+
(string=? content "\"with quotes\""))))
320+
(test-case "newline with port appends newline"
321+
(let ((path "/tmp/elz-nl.txt"))
322+
(call-with-output-file path
323+
(lambda (port)
324+
(display "line" port)
325+
(newline port)
326+
(display "two" port)))
327+
(let ((first (call-with-input-file path
328+
(lambda (port) (read-line port)))))
329+
(delete-file path)
330+
(string=? first "line"))))
331+
260332
;; --- Multiple Values Tests ---
261333
(test-case "values single arg returns the arg"
262334
(= 42 (values 42)))

0 commit comments

Comments
 (0)