This document catalogs differences between the current implementation and the R7RS-small specification. These are semantic differences where the implementation produces results but may not match R7RS behavior for certain inputs.
Reference: R7RS-small Specification
Last Updated: 2026-02-12
Three known differences exist:
- Non-blocking I/O detection (
char-ready?,u8-ready?) always returns#t. Conservative safe behavior with minimal practical impact. parameterizeuses continuation marks instead ofdynamic-wind. This fixes composable continuation bugs at the cost of a minor semantic difference when mutating parameters via(p val)insideparameterize.set-current-directory!changes the process-global working directory viaos.Chdir, which is inherently shared across all Wile engines and goroutines in the same OS process.
Affected Primitives: char-ready?, u8-ready?
R7RS §6.13.2 Requirement:
Returns
#tif a character (or byte) is ready on the input port and returns#fotherwise. Ifchar-ready?returns#tthen the nextread-char(orread-u8) operation on the given port is guaranteed not to hang.
Wile Behavior: Always returns #t.
Rationale:
Go's io.Reader interface does not expose readiness status or non-blocking I/O semantics. Implementing true non-blocking detection would require:
- OS-specific syscalls (
select/pollon Unix, overlapped I/O on Windows) - Platform-specific build tags and dependencies (
golang.org/x/sys/unix,golang.org/x/sys/windows) - Handling buffered readers (
bufio.Reader) where buffered data makes reads non-blocking even when the underlying descriptor would block - Significant complexity in the I/O layer with cross-platform maintenance burden
The conservative behavior (always returning #t) is safe: it may cause blocking where R7RS code expected non-blocking, but never claims data is available when it isn't (which would violate R7RS guarantees).
Workaround:
Use Go channels or goroutines for non-blocking I/O patterns:
;; Instead of polling with char-ready?:
(if (char-ready? port)
(read-char port)
'not-ready)
;; Use a thread to read asynchronously:
(let ((ch (make-channel)))
(thread-start!
(make-thread
(lambda ()
(channel-send! ch (read-char port)))))
(channel-receive ch))Impact: LOW — char-ready? and u8-ready? are rarely used in modern Scheme code. These predicates were designed for select-style event loops, a pattern largely superseded by async/await and channel-based concurrency. Most I/O in Wile is either:
- File-based (always ready, blocking is acceptable)
- Network streams where blocking semantics are expected
- Interactive REPL input where immediate blocking is desired
Estimated implementation effort: 4-8 hours including cross-platform support and testing.
ROI analysis: Documentation (15 minutes) provides clear expectations at far better ROI than implementation (4-8 hours) for an exotic edge case.
Affected Form: parameterize
R7RS §4.2.6: The R7RS reference implementation uses dynamic-wind to save/restore parameter values. Wile uses with-continuation-mark instead, storing parameter bindings as continuation marks keyed by the parameter object.
Why: The dynamic-wind approach has bugs when composable continuations (call-with-composable-continuation) cross parameterize boundaries. The after-thunk captures the "old" value at definition time. When a composable continuation is invoked from a different parameterize context, the stale old value clobbers the outer binding. Marks-based parameterize eliminates this class of bugs because bindings ride on the continuation frames structurally.
Semantic difference: (p val) (calling a parameter with 1 argument) inside parameterize sets the parameter's base value. With dynamic-wind-based parameterize, the mutation is visible within the extent and undone on exit. With marks-based parameterize, the mark shadows the base value, so the mutation is invisible while the parameterize is active but persists after it exits.
This difference is observable only when code mutates a parameter via (p val) inside a parameterize body — a rare pattern. The standard pattern of reading (p) inside parameterize is unaffected.
Impact: LOW — standard R7RS programs use parameterize for scoped binding, not direct mutation. The marks-based approach matches Racket's semantics and is correct for composable continuations.
These are Wile-specific features that extend R7RS. They do not conflict with R7RS behavior — standard Scheme programs behave identically. These extensions use reader prefixes in the # dispatch space that R7RS leaves implementation-defined.
Wile provides reader syntax for explicitly constructing arbitrary-precision numbers. These are not part of any Scheme standard (R5RS, R6RS, R7RS, or SRFIs).
| Prefix | Type | Exactness | Backed by | Examples |
|---|---|---|---|---|
#z |
BigInteger | exact | math/big.Int |
#z12345678901234567890, #z-42, #z+7 |
#m |
BigFloat | inexact | math/big.Float (256-bit) |
#m3.14159265358979323846, #m1.5e-10, #m.5 |
Both prefixes are case-insensitive (#Z, #M also work), following R7RS §7.1.1 conventions.
BigInteger (#z) supports radix prefixes: #z#b101 (binary), #z#o77 (octal), #z#x1F (hex).
BigFloat (#m) supports optional sign, decimal point, and exponent markers (e, s, f, d, l).
Note: R7RS requires implementations to support arbitrarily large exact integers (§6.2.3). Wile satisfies this via automatic overflow promotion from Integer (int64) to BigInteger — the #z prefix is a convenience for explicit construction, not a conformance requirement. Standard R7RS programs never need #z or #m.
Primitive: set-current-directory!
Behavior: Calls os.Chdir, which changes the working directory for the entire OS process. Multiple Wile engines in the same Go process share one working directory. Concurrent calls from different goroutines race on the same OS state. This is inherent to POSIX — there is no per-thread working directory.
Mitigation: The primitive is gated by security.ResourceProcess / security.ActionWrite / target "cwd", so embedders can deny it via their authorizer. When denied, all file operations should use absolute paths.
R7RS does not specify directory operations. This follows SRFI-170 conventions.
R7RS §7.3's reference implementation of guard uses (let ((result (begin e1 e2 ...))) ...), which binds a single value. If the body produces multiple values via (values v1 v2 ...), the let binding triggers an arity mismatch.
Wile's guard uses call-with-values to capture all values from the body, then re-emits them via (apply values results). This means (guard (e (#f)) (values 1 2)) correctly propagates both values, whereas the R7RS reference implementation would signal an error.