Runtime features and debugging
Language reference · Previous: Collections and Enumerable
Fibers
f = Fiber.new(callable)
f.resume(0) # runs/resumes; returns the yielded or completed value
f.status() # "runnable" / "suspended" / "completed" / "failed"
f.alive?()
Inside a callable body that does not declare an &block parameter,
yield(value) suspends and sends value out
to whoever resumes; the next .resume(v) delivers v back in as
yield's own expression result. Fiber.new's argument must be a
zero-argument callable (captures are fine — only nested defs produce a
referenceable one; see Functions, closures, and calls).
Fiber.yield(value) is the explicit equivalent and remains unambiguous inside
a callable that also declares █ Fiber.yield() sends nil.
I/O
puts("hello, #{name}") # print(value) has no trailing newline
warn("disk almost full") # stderr, with a newline
line = gets() # one line from stdin, nil at EOF
f = File.open("data.txt", "w")
f.write("some text")
f.close()
server = TCPServer.listen(8080)
conn = server.accept() # blocks until a client connects
conn.gets()
conn.write("response\n")
conn.close()
client = TCPSocket.connect("example.com", 8080)
A connected socket (from .connect or .accept()) is a File under the
hood, so .read()/.read(n)/.gets()/.write(value)/.close() work
identically on both.
File also has a small family of path utilities needing no open handle:
File.join("a", "b") (=> "a/b"), .dirname(path), .basename(path, suffix = nil), .extname(path), .absolute?(path), and
.expand_path(path, base = nil) (resolves and lexically normalizes
path against base, or the current working directory when base is
omitted) are all pure string manipulation, needing no filesystem access.
.directory?(path) is the one exception — real stat()-backed I/O, true
only if path exists and is a directory; any stat() failure (missing
path, permission denied, ...) reads as an ordinary false rather than
raising, matching Ruby's File.directory? so a recursive directory walk
can use it in a plain condition with nothing to rescue. See Local
I/O
for the full rules.
ARGV and ENV are plain global values, not calls — ARGV is an
Array of Strings, the script's own trailing command-line arguments
(d6 script.d6 one two → ARGV == ["one", "two"]; [] for -e/a
file run with no trailing args, or from the REPL). ENV is a Hash of
String to String, a snapshot of the process environment taken at
startup — ENV["PATH"], ENV["HOME"], etc.; a missing key is nil,
same as any other Hash. ENV["K"] = "v" and ENV.delete("K") change that
Hash, and every Process.run/Process.spawn after that starts the child with
the current contents (values must be Strings, or the spawn raises TypeError).
The running process's own C-level environment is not changed.
A constant or user-defined function named ARGV/ENV shadows it.
Constant definitions belong at top level or inside a class or module.
__FILE__ and __dir__ are the absolute path of the source file they are
written in and its directory (-e and the REPL have no file: __FILE__ is the
name given and __dir__ the working directory). Each file gets its own: one
that is required reports itself, not the file that required it, which makes
"#{__dir__}/data.json" the way to find files next to a script whatever the
working directory is. They are substituted when the program is loaded, so a
bytecode cache or a d6 build binary keeps the path it was compiled
from. Text in a comment or string that only mentions them is left alone;
inside a string they work in #{...} interpolation.
exit(code = 0) immediately terminates the whole process with the given
status (0–255; anything else raises ArgumentError, a non-Int raises
TypeError). This is a hard exit, not a raised/rescuable control-flow
value like Ruby's plain exit — no ensure block anywhere on the call
stack runs, and every other Thread.new-spawned OS thread stops too,
since they all share this one process. A validation failure (bad code)
is an ordinary catchable exception; only a valid code actually exits:
begin
exit(-1)
rescue error: ArgumentError
puts("bad exit code: #{error.message()}")
end
exit(1) # this one actually terminates the process
Like every other built-in name, a local variable or user-defined
function named exit shadows it.
Debugging
def compute(x)
y = x * 2
debugger() # breakpoint() is the same thing, either name works
y + 1
end
compute(5)
--- paused at compute:3:3 ---
locals:
x = 5
y = 10
(press Enter to continue)
debugger()/breakpoint() pauses execution, prints where it was called
from and every currently-live local (name and value — parameters count
as locals too), then waits for one line of input on stdin before
resuming normally. Read-only inspection, not a live REPL: there is no
way to evaluate a new expression or reassign a local from the pause — it
prints what's already there and continues, deliberately scoped short of
a Ruby binding.pry/debug-style interactive session. Stdin at EOF
(closed, redirected from /dev/null — the ordinary case under a non-
interactive script or test run) continues immediately rather than
hanging, so it's always safe to leave a debugger() call in code that
might run non-interactively.
Like puts/gets/Time/every other built-in name, a local variable or
a user-defined function named debugger/breakpoint shadows it —
def debugger(); ...; end makes debugger() call that instead, never
the built-in.
An editor's own gutter breakpoints reuse this exact same pause, inserted
at compile time rather than written into the source — see
Debugging for the DAP integration (dap/,
editors/vscode's "Debugging" section), its limitations, and the
DIAMOND_DEBUG_FD/DIAMOND_DEBUG_BREAKPOINTS env-var contract behind it.
Regexp
re = Regexp.new("(\\d+)-(\\d+)")
m = re.match("id:42-99") # => ["42-99", "42", "99"]
m[0] # full match
m[1] # first capture group
re.match?("id:42-99") # => true, no captures allocated
re.match("no digits") # => nil, no match
Regexp.new("foo", 1) # 1 = case-insensitive (see options below)
Backed by reginold, a companion regex engine vendored in-repo under
reginold/ and built as a static archive — compiled under Ruby regex
syntax. Regexp.new(pattern, options = 0) —
the options argument is a plain Int bitmask: 1 = ignore case, 2 = .
matches newline, 4 = extended (whitespace and # comments ignored in
the pattern) — combine flags with | (or plain addition, since they're
disjoint bits and the two coincide): 1 | 2 for case-insensitive and
dot-matches-newline together. See Classes and modules
for |/&/^/>>'s own Int-only bitwise semantics.
.match(string) returns an Array — index 0 is the full match, indices
1.. are capture groups in order, nil at any index for an unmatched
optional group ((a)|(b) matched against "b" gives
["b", nil, "b"]) — or nil if the pattern didn't match at all.
.match?(string) is the same search without allocating capture data, for
a plain yes/no check. An invalid pattern raises a rescuable RegexpError
at Regexp.new time. /pattern/imx literals are equivalent (see the
Collections guide). "text".match(re) and "text".match?(re) (a String pattern is compiled first)
behave like re.match("text") and re.match?("text").
This is deliberately a small first cut: no =~ operator and no richer
MatchData object (pre_match, named captures) — the plain-Array
result covers the common case. String#split/#sub/#gsub/#scan do
accept a Regexp ("a,b,c".split(re), "abc123".sub(re, "X"),
"abc123".gsub(re, "X"), "abc123".scan(re)), each with backreference
and capture-group handling of its own — see the
Collections guide for those; Regexp.new +
.match/.match? plus that String quartet is the whole surface.
JSON
text = JSON.stringify({"name": "Ada", "scores": [98, 91.5], "admin": false, "note": nil})
# => {"name":"Ada","scores":[98,91.5],"admin":false,"note":null}
data = JSON.parse(text)
data["scores"][1] # => 91.5
JSON.stringify(value) accepts nil, Bool, Int (including big
integers), finite Float, String, and any Array or Hash built from
those. Hash keys that aren't Strings are written as their string form
({1: 2} becomes {"1":2}); Hash entries keep their insertion order.
Strings are escaped as JSON requires (quotes, backslashes, and control
characters) and otherwise written byte for byte. Anything else -- a
Symbol, an instance, NaN, Infinity -- raises a rescuable JSONError.
Output is compact, with no whitespace unless you ask for it: JSON.stringify(value, options) takes a Hash of
options (String or Symbol keys). "indent" is a number of spaces (0 to 16; 0 is compact) or a String of
spaces and tabs, and puts each array element and object entry on its own line, with ": " after a
key; empty arrays and objects stay [] and {}. "sort_keys": true writes every Hash's keys in
ascending bytewise order (equal text keeps insertion order). Any other option, or a value of the wrong
type, raises TypeError.
JSON.stringify({"b": [1, 2], "a": {}}, {"indent": 2, "sort_keys": true})
# {
# "a": {},
# "b": [
# 1,
# 2
# ]
# }
Nesting is limited to 149 levels, the same
bound JSON.parse uses; a deeper document, or an Array/Hash that contains
itself, raises SystemStackError rather than recursing without end.
JSON.parse(text) (also text.parse_json()) returns the value a JSON
document describes: objects become Hashes with String keys, arrays become
Arrays, null becomes nil, numbers without a fraction or exponent become
Int (promoting past 64 bits as needed), other numbers become Float, and
\uXXXX escapes are decoded to UTF-8. Invalid input raises JSONError
(for example "unexpected end of input").
No AST
The compiler is a Pratt parser that emits register bytecode directly;
there is no retained AST. diamond_compile actually runs this parser
twice per compile, not once: a throwaway first "discovery" pass walks the
whole source (tolerating a forward reference to a not-yet-declared
class/module/interface just long enough to keep going) purely to
register every declaration regardless of textual order, then a real
second pass emits the bytecode that actually runs, with every
declaration already known from the start -- see docs/roadmap.md's
"Compiler representation" section. This is what lets a class construct
or call a singleton method on another class declared later in the same
file (SomeClass.new(...), OtherClass.someMethod(...)), and lets a
type annotation name a class declared later too. A superclass still has
to be declared first (class B < A needs A's complete, already-fully-
compiled field table, not just its name), and there's still no retained
AST for either pass to share — each is a full, independent walk of the
token stream. Pass --dump-bytecode on the CLI to see how any construct
in this document actually lowers (that dump reflects only the second,
real pass's output).
Targeted bytecode dumps
d6 --dump-bytecode=user FILE [ARGS...] (or -e CODE [ARGS...])
prints the application's bytecode and its require/require_cut imports,
excluding the bundled core/JSON preamble. Instruction offsets, registers,
function IDs, and jump targets retain their values in the complete program.
The program still runs afterward, with its usual output and exit status.
Plain --dump-bytecode keeps the complete dump; --dump-bytecode=all is an
explicit equivalent. User-only dumps recompile instead of reading or updating
the .d6c cache so the preamble boundary is known even for cached programs.
This is a preamble filter, not a single-file or single-function selector.
Diagnostic environment variables
These switches report what the interpreter did; none change what a program
means. A flag is on when it is set to anything other than an empty value or 0.
A numeric one takes a positive integer and is ignored otherwise. Every report
goes to standard error, and the counter reports print once, after the program
has finished without a runtime error; a server stopped by a signal never
prints them. They apply to the d6 command.
Documented with their features instead: DIAMOND_JIT, DIAMOND_JIT_THRESHOLD
and DIAMOND_TRACE_JIT (deployment); DIAMOND_NO_CACHE and
DIAMOND_TRACE_CACHE (caching); DIAMOND_SANDBOX,
DIAMOND_SANDBOX_ALLOW and the three DIAMOND_MAX_* budgets
(sandbox); DIAMOND_DEBUG_FD, DIAMOND_DEBUG_BREAKPOINTS and
DIAMOND_DEBUG_BREAKPOINT_OFFSETS (debugging); DIAMOND_FORCE_REPL
(repl); DIAMOND_AOT_KIT (deployment).
Where the time goes
DIAMOND_TRACE_STARTUP--startup: load Xs, compile Xs (N bytes: P prelude + U user), run Xs, total Xs. The prelude figure is 0 when the prelude comes precompiled instead of being compiled with the program.DIAMOND_TRACE_COMPILE--compile: discovery Xs, real Xs, total Xs, source N bytes, once per compile: the throwaway declaration-discovery pass, then the pass that emits bytecode (see "No AST" above).DIAMOND_TRACE_GC--GC: M major (Xs), m minor (Xs): collection counts and total pause time.DIAMOND_TRACE_TAIL_CALLS--tail calls: N optimized in place: how many tail calls ran as a jump rather than a nested call (callables).0for a program whose recursion did not qualify.DIAMOND_REPEAT=N-- run the compiled programNtimes in one VM, stopping at the first failure. Later runs start with warm caches, which is what a steady-state measurement wants; the program's own output repeats every time.DIAMOND_TRACE_OPCODES-- oneopcode[N]: countline per opcode that ran, whereNis the opcode's ordinal inDiamondOpCode(src/vm.h).--dump-bytecodeprints the names.
Inline caches and shapes
Method calls and field reads are cached per call site. A site that keeps hitting one class is rewritten to dispatch directly.
DIAMOND_TRACE_IC--inline caches: H hits, M misses.DIAMOND_TRACE_IC_SITES-- oneinline cache site[i]: H hits, M misses, C classesline for every site that was ever used.DIAMOND_TRACE_IC_FAST--monomorphic dispatches: N, the calls that took the single-class fast path.DIAMOND_TRACE_IC_PROBES--method cache probes: N.DIAMOND_TRACE_IC_REWRITES--direct dispatch rewrites: N, the call sites rewritten to direct dispatch.DIAMOND_TRACE_IC_POLICY-- the two thresholds in effect,quicken thresholdandmonomorphic threshold.DIAMOND_IC_MONO_THRESHOLD=N-- how many cache hits a site needs before it counts as monomorphic and may be rewritten. Default 1.DIAMOND_TRACE_SHAPES--shape transitions: N, the times an instance's field layout changed.DIAMOND_INVALIDATE_IC_EACH_RUN-- withDIAMOND_REPEAT, clear the method caches before every run after the first, so each run measures a cold cache.DIAMOND_TRACE_IC_EACH_RUN-- withDIAMOND_REPEAT, arun N: inline caches: H hits, M misses, rewrites: Rline after every run instead of only a final total.
Quickening
DIAMOND_QUICKEN-- rewrite a generic+,-,*,/,==,!=,<,<=,>or>=site to anInt-specialized opcode once it has seenIntoperands. A site whose operands later stop beingIntfalls back to the generic opcode, which is how mixedInt/Floatarithmetic keeps working after a rewrite. Off by default.DIAMOND_QUICKEN_THRESHOLD=N--Intobservations, counted across the whole VM, needed before a rewrite. Default 1.DIAMOND_TRACE_QUICKEN--quickened sites: N, deoptimized sites: M.
Stress and verification
DIAMOND_STRESS_GC/DIAMOND_STRESS_MINOR_GC-- force a major or a minor collection before every allocation that could trigger one. Programs run far slower; this exists to expose a value that is not rooted when the collector runs (see GC design).DIAMOND_DEBUG_VERIFY-- the bytecode verifier (run on everyProgramBuilder#runprogram and on the embedded prelude) disassembles what it checks and normally discards the text. With this set the disassembly goes to standard error.