Spinel is a subset of Ruby: a program that compiles and runs under Spinel
should behave the same under CRuby. One place Spinel used to be a superset
instead was require. Some stdlib that CRuby gates behind a require --
StringIO, IO#winsize, and friends -- was always available in Spinel, so code
that forgot the require ran under Spinel but raised NameError /
NoMethodError under CRuby. The require-gate closes that gap.
This document describes how require works today, which stdlib needs which
require, and how features are resolved. The planned package system is sketched
under Planned at the end.
They are different mechanisms and stay distinct:
| form | resolves to | when |
|---|---|---|
require_relative "path" |
a project-local file, spliced into the program | always; a missing file is a compile error |
require "name" |
a named feature (bundled stdlib, native, or -- planned -- a package) | the feature must exist, else a compile error |
Kernel.require "name" / ::Kernel.require "name" |
the same as the bare require |
as above; any other receiver (obj.require) is an ordinary method call |
require is not a runtime file load. Spinel is a whole-program
ahead-of-time compiler, so every require is known at compile time and there is
no $LOAD_PATH to mutate at runtime. A require that names something Spinel
cannot provide is reported when you compile, not when you run.
The require-gate makes require-gated stdlib unavailable unless you require it,
matching CRuby. It is currently opt-in: pass --require-gate, or set the
environment variable SPINEL_REQUIRE_GATE=1, when compiling. With it off (the
default today) the old always-available behaviour is preserved. The gate is
expected to become the default at a future release boundary.
spinel --require-gate myprogram.rb
SPINEL_REQUIRE_GATE=1 spinel myprogram.rb # the same switchspin build always compiles with the gate on: inside a resolved dependency set
the universe is known, so a require that resolves to nothing is a bug rather
than something to warn past. spin flags hands the flag to a build driven from
outside spin, which is why it has a flag spelling at all -- an environment
assignment cannot ride inside a flag string.
These are provided by Spinel but, like CRuby, only after their require:
require |
what it enables | without the require (gate on) |
|---|---|---|
require "stringio" |
StringIO |
uninitialized constant |
require "strscan" |
StringScanner |
uninitialized constant |
require "json" |
JSON.generate, JSON.dump |
uninitialized constant |
require "csv" |
CSV (with CSV::Row / CSV::Table) |
uninitialized constant |
require "monitor" |
Monitor (#synchronize) |
NameError (uninitialized constant) |
require "socket" |
TCPServer, TCPSocket, UDPSocket, UNIXServer, UNIXSocket, Socket, Addrinfo |
NameError (uninitialized constant) |
require "pathname" |
Pathname |
uninitialized constant |
require "pathname" |
Pathname() (the Kernel conversion method) |
NoMethodError |
require "io/console" |
IO#winsize |
NoMethodError |
require "time" |
Time#iso8601 |
NoMethodError |
require "bigdecimal" |
BigDecimal, BigDecimal() (a minimal subset: see packages/bigdecimal/bigdecimal.rb) |
NoMethodError (BigDecimal()) |
socket is the strictest of these: its require is mandatory even with the
gate off, because CRuby itself only defines TCPServer / TCPSocket after
the require, and growing the constants unconditionally would diverge from
CRuby's NameError. What the two classes actually cover is in
limitations.md.
A library loads itself and nothing else. CRuby's stdlib files often require
others as an implementation detail (csv pulls in stringio there, so a
program that requires csv can name StringIO without saying so), and Spinel's
version of the same library has no reason to make the same internal choice.
Require what the program actually uses.
Two shapes, which set what the failure looks like:
- A feature that defines a class/module (
stringio,strscan,json,monitor): without therequirethe constant is undefined. - A feature that extends a core class (
io/consoleaddsIO#winsize,timeaddsTime#iso8601): without therequirethe method is undefined. The rest ofIOandTimeare core and always available -- only the gated method needs therequire.
# gate on, no require:
StringIO.new("x") # error: uninitialized constant StringIO
STDOUT.winsize # error: undefined method 'winsize'
# correct:
require "stringio"
require "io/console"
StringIO.new("x").read
STDOUT.winsizeA require that Spinel cannot satisfy at all -- a stdlib Spinel does not
implement (date, pp, securerandom, ...) or an unknown name -- is a compile
error under the gate, the same as a missing require_relative:
spinel: cannot load such file -- date (require "date")
This is honest about the subset: if Spinel cannot provide date, the program
genuinely cannot run, and you learn it when you compile rather than via a
confusing failure later. With the gate off this is a warning instead.
A few requires name a capability Spinel already provides as core, and are
tolerated as a no-op (modern CRuby treats them as already-loaded too):
require "thread"(Thread, Mutex, Queue are core)require "enumerator"(Enumerator is core)require "fiber"(Fiber is core)
Some stdlib ships with Spinel as Ruby source and is spliced when required --
set, forwardable, optparse, erb, csv, pathname, digest, base64,
fileutils, tmpdir, zlib, open3 (the capture forms), pty (PTY.spawn; not on
wasm32-wasi, which has no fork), fiddle (where libffi is,
like ffi),
benchmark (CRuby's own gem, unmodified), shellwords (likewise), find (likewise, but for one line; see its note),
fileutils, tmpdir, zlib, logger (a compact implementation of its API),
benchmark (CRuby's own gem, unmodified),
bigdecimal (a minimal subset, see packages/bigdecimal/bigdecimal.rb)
(plus the stringio/strscan/json marker shims for their C-backed
features). net/http and uri are there, and so
is openssl -- the one that is conditional: it is glue over the system libssl, so it exists only where those
headers did at build time, and require "openssl" is otherwise the
unsatisfiable require it is for any library Spinel does not carry. Where the
build found a keg-only OpenSSL (Homebrew's openssl@3), the compiler records
that library directory and links a program's -lssl -lcrypto against it, so
no LIBRARY_PATH is needed after make. Each lives as an ordinary
spinelgem under packages/<name>/ beside the compiler (packages/set/set.rb with
its spin.toml); lib/ holds only the C runtime. The require pulls in
the package's file like any other package -- pre-installed just means no fetch.
Two of these are also spliced implicitly, because CRuby provides them with
no require at all: a program that references Set (or calls .to_set)
anywhere, in the entry file or in a file it requires, gets require "set"
prepended, and one that references IO::Buffer gets
require "io/buffer" (packages/io) the same way. Writing the require
explicitly is fine too -- the splice only fills it in when absent.
You can provide a feature yourself and have require "name" resolve it, through
the same mechanism as bundled stdlib. Pass -I <dir> (like ruby -I) to add a
feature search root, then a require resolves against it:
spinel -I mylibs main.rbThe -I roots are searched before the packages bundled with the compiler (packages/), so a project's own package of the same name as a bundled one (an openssl of its own, say) is the one a require reaches. The compiler's lib/ comes first and is not shadowed.
A feature name is a path, looked up in each -I root in two forms:
- single file --
require "thing"→mylibs/thing.rb(the CRuby form); - colocated directory --
require "thing"→mylibs/thing/thing.rb, so a feature's sources (.rb, later its.c/.rbs) share one directory.require "my/thing"→mylibs/my/thing.rbormylibs/my/thing/thing.rb.
Pure Ruby needs only the .rb: it is spliced into the whole-program compile and
Spinel infers types from it like your own code, so no manifest or .rbs is
required (an .rbs is optional, to pin the public surface). A require that no
root satisfies is the compile error from the previous section.
A spin package is already in the colocated form. A package named curses
lives at <pkgs>/curses/curses.rb, so -I <pkgs> resolves require "curses"
against it -- with no project file and no spin involved -- and one root serves
every package under it. Its carried C reaches the link line through the
compiler's repeatable --link:
spinel -I spin/packages --link ~/.cache/spin/native/curses-0.1.0-cc/sp_curses.o app.rbThe root has to be the directory the package sits in. -I . at a repository
root whose packages live under spin/packages/ looks for ./curses.rb and
./curses/curses.rb, finds neither, and the require is ignored with a
warning (or refused, under the gate). Working out which roots and which objects
a dependency set implies is what spin flags
does for you.
- C in a feature is reached through FFI today (wrapping an existing library); a planned in-TU mode will let a feature's own C be inlined without a link boundary.
- A default vendored root and a project manifest will sit on top of
-I, so packages resolve without passing-Iby hand. - Feature directories (a
-Iroot, a vendored package) are read-only inputs: Spinel never writes build artifacts next to a feature's sources. A C-backed feature's compiled objects go to a separate build cache, so a feature tree can be shared or mounted read-only. - Spinel's own require-gated stdlib will eventually be carved out of the compiler into ordinary feature packages, resolved exactly like a third-party one.