Ruby stdlib facades let Haxe-authored Ruby code use real Ruby libraries without
falling back to raw __ruby__, broad Dynamic, or wrapper runtimes. They live
under std/ruby/** and should make the generated Ruby look like ordinary
hand-written Ruby wherever the Ruby API already has the desired behavior.
- Put Ruby-owned library surfaces under the
rubypackage:ruby.Dir,ruby.File,ruby.FileUtils,ruby.Json,ruby.Kernel,ruby.Pathname,ruby.Tempfile,ruby.URI,ruby.CSV,ruby.Open3,ruby.Set<T>,ruby.Regexp,ruby.MatchData,ruby.Time,ruby.TimeParsing, andruby.Datefacades. - Keep the Haxe class name Haxe-idiomatic when RubyHx owns the authoring
surface. Use
@:nativeto point at Ruby constants with different spelling, for example@:native("JSON") extern class Json. - Preserve exact Ruby names through Haxe's built-in
@:nativemetadata when modeling an existing Ruby API. Do not introduce public lowercase Haxe class names just because the Ruby constant is lowercase or acronym-heavy. - Add
@:rubyRequire("...")on externs that need a Ruby stdlib require, such asruby.Json, so generated output records the dependency instead of relying on user glue. - Keep internal compiler/runtime adapters clearly internal.
ruby.NativeHashandruby.NativeIteratorare implementation helpers for target std overrides; they are not the model for public app-facing stdlib facades.
Prefer the narrowest typed surface that still feels useful:
| Need | Shape |
|---|---|
| Existing Ruby constant or module method | extern class with @:native and typed static methods |
| Existing Ruby receiver method | extern, typed patch contract, or compiler direct lowering when it is a Haxe std method |
| Small Haxe-owned convenience | normal Haxe class that delegates to Sys/typed std APIs, such as ruby.Prelude |
| Ruby value with a small representation boundary | abstract such as ruby.Symbol |
| Ruby behavior needed by Haxe std semantics | std override plus direct Ruby or compact HXRuby helper only for the semantic gap |
| Framework/gem-specific behavior | RailsHx/gem-layer package, not std/ruby core |
A public facade should expose typed parameters and return values whenever the
contract is known. Use Dynamic only at a real Ruby boundary, such as
JSON.parse before a typed decoder exists, and document or narrow the value
before app logic depends on its shape.
RubyHx should ultimately provide broad typed access to the Ruby core and
standard-library APIs that are available across its supported Ruby matrix. In
Haxe source, these are imported as ruby.* types such as ruby.Pathname; the
physical std/ruby directory is an implementation and packaging detail, not a
package named @std/ruby.
Broad coverage does not mean copying every Ruby method by hand or pretending that every open Ruby contract is statically sound. The coverage program should:
- inventory Ruby core APIs, standard libraries, default gems, bundled gems, and platform-specific libraries separately;
- establish the common contract for the supported Ruby versions before adding a version-specific API;
- generate conservative low-level contracts from deterministic RBS where that is mechanical, then compile, review, document, and runtime-test them;
- curate Haxe-idiomatic names, overloads, blocks, keyword arguments, nullability, and return types where a mechanical signature is not sufficient;
- omit an uncertain operation or expose an explicitly named narrow unchecked
boundary instead of widening a whole facade to
Dynamic; - keep Rails and third-party gem APIs in RailsHx, companion packages, or generated app-local contracts rather than treating them as Ruby core.
The complete inventory is a useful long-term goal. Stable releases should state which domains and Ruby versions are covered instead of making an unqualified "whole stdlib" claim. A smaller precise facade is more valuable than a large surface whose types do not describe Ruby's actual behavior.
The packaged
lib/hxruby/stdlib_coverage.json
catalog is the current domain-level contract. It distinguishes core,
standard-library, default-gem, bundled-gem, and platform-specific availability
for every supported MRI branch and accounts for every maintained std/ruby
facade. See Ruby Stdlib Coverage Catalog for its
schema, runtime checks, and deliberately bounded claims.
The public Ruby-native surface and the Haxe std compatibility surface have different semantic owners:
native Ruby constants and methods
|
typed externs and narrow target primitives
/ \
public ruby.* std/ruby/_std
Ruby semantics Haxe semantics
|
semantic adapters only
where Ruby behavior differs
std/ruby/_std is not a second Ruby library API. It implements portable Haxe
types such as haxe.Json, haxe.ds.Map, sys.FileSystem, and sys.io.File on
the Ruby target. Those APIs must retain their Haxe contracts even when the most
direct implementation uses Ruby's JSON, Hash, File, Dir, or
FileUtils underneath.
The preferred implementation rule is:
- Model a reusable native operation with a precise typed
ruby.*extern or a narrow internal target primitive. - Let Haxe std overrides consume that typed operation when its contract is an exact fit.
- Add a small Haxe-semantic adapter when return values, indexing, mutation, exceptions, encodings, nullability, blocks, or other behavior differs.
- Keep the adapter's public result in Haxe std types. Do not leak a
ruby.Pathname, native status value, or Ruby-only exception contract through a portable Haxe API.
This means a typed-native-first implementation is desirable for new reusable
domains, but it is not a rigid prerequisite for every Haxe std parity fix. A
private native carrier can be the correct boundary when no useful public Ruby
facade exists, and a compiler lowering can be better than introducing a public
wrapper solely for _std. Conversely, a completed public ruby.* facade must
not wait for a Haxe std consumer: Ruby-first code is itself a first-class use
case.
The two branches should share typed native contracts where doing so removes raw
target access and duplication. They should not be collapsed into one public API.
ruby.* answers "what does Ruby do?" while Haxe std answers "what does this
Haxe API promise on every target?"
When Ruby behavior matches the Haxe/RubyHx contract, emit the Ruby call directly:
import ruby.Kernel;
Kernel.puts("ready");This should lower to an ordinary Ruby call against Kernel, not an HXRuby
wrapper. The same rule applies to compiler-special lowerings for Haxe std
methods: Array.concat can become left + right, Array.contains can become
include?, and Array.copy can become dup because those Ruby calls preserve
the required behavior.
Kernel.puts and Kernel.print use method-level generics instead of
Dynamic: each call preserves the caller's precise Haxe value type while the
extern still maps directly to Ruby's open-value Kernel API. The generic does
not grant field access or leak an unchecked value into application code.
Use an HXRuby helper only when Ruby would drift from the documented Haxe
semantics. Examples include numeric-prefix parsing for Std.parseInt,
Haxe-specific stringification for Std.string, portable Array.join, string
UTF-16 overlap behavior, enum/type reflection, and array boundary methods such
as slice, splice, indexOf, and lastIndexOf.
Direct Ruby extern:
package ruby;
@:native("File")
extern class File {
public static function read(path:String):String;
}Ruby stdlib require:
package ruby;
@:rubyRequire("json")
@:native("JSON")
extern class Json {
public static function parse(input:String):Dynamic;
}Opt-in Ruby-flavored convenience:
import ruby.Prelude.puts;
puts("typed output");ruby.Prelude.puts intentionally delegates to Sys.println, so it keeps
RubyHx/Haxe stringification semantics. Use ruby.Kernel.puts when the goal is
exact Ruby Kernel interop.
ruby.Pathname is the canonical typed facade for Ruby's stdlib-owned Pathname
value. It exposes Haxe-idiomatic names for construction, path composition and
decomposition, cleaning/expansion, relative-path calculation, parent/children,
read-only filesystem predicates, and bounded reads:
var base = new ruby.Pathname("/srv/app");
var entry = base.join("lib").join("entry.rb");
ruby.Kernel.puts(entry.relativeTo(base).toPath()); // lib/entry.rb
ruby.Kernel.puts(entry.baseName().toPath()); // entry.rb
ruby.Kernel.puts(entry.extension()); // .rbThe generated Ruby stays target-native:
require "pathname"
base = Pathname.new("/srv/app")
entry = base.join("lib").join("entry.rb")
Kernel.puts(entry.relative_path_from(base).to_path)Ruby's constructor and variadic methods can accept arbitrary to_path objects,
but the canonical Haxe surface deliberately accepts String or Pathname
where modeled. Multiple segments use chained join(...) calls. This keeps
completion and diagnostics precise and avoids introducing Dynamic, casts, raw
Ruby, splat lowering, or a wrapper solely to mirror an open Ruby argument list.
ruby.Pathname is Ruby-shaped interop; it does not replace portable
haxe.io.Path, whose parsing/normalization contract remains Haxe-owned.
ruby.URI and ruby.URIValue form a bounded typed facade over Ruby's URI
module and the shared URI::Generic value contract. Parsing, two-reference
joining, form/URI component encoding, common nullable components, predicates,
merging, relative routing, normalization, and string conversion remain nominal
and chainable:
var base = ruby.URI.parse("https://example.com/app/");
var endpoint = base.merge("api/items?q=typed");
ruby.Kernel.puts(endpoint.host());
ruby.Kernel.puts(endpoint.toString());
ruby.Kernel.puts(ruby.URI.encodeComponent("a b/c"));Generated output requires and calls the real Ruby library directly:
require "uri"
base = URI.parse("https://example.com/app/")
endpoint = base.merge("api/items?q=typed")
Kernel.puts(endpoint.host)
Kernel.puts(endpoint.to_s)
Kernel.puts(URI.encode_uri_component("a b/c"))The facade was reviewed against official ruby/rbs v4.0.3 URI signatures;
the exact source hashes and curation statement live in the packaged coverage
catalog. Ruby's open conversion protocols, enumerable form encoding, optional
encoding parameters, variadic joins, mutation, and scheme-specific APIs remain
excluded rather than represented loosely. URI.parse returns scheme-specific
subclasses at runtime, and ruby.URIValue models their sound shared base rather
than pretending every parsed value is HTTP-specific.
ruby.CSV is the canonical typed facade for Ruby's header-free CSV singleton
API. ruby.CSVRow keeps fields as Null<String> so unquoted missing fields do
not collapse into quoted empty strings. The concise methods use Ruby defaults;
the With variants accept typed option carriers with completion and emit native
keyword arguments:
var rows = ruby.CSV.parseRowsWith(" name ; value \n alpha ; 1 \n", {
columnSeparator: ";",
stripFields: true,
maxFieldSize: 4096
});
ruby.CSV.forEachRow("imports/users.csv", row -> {
ruby.Kernel.puts(row[0]);
});
var output = ruby.CSV.generateRowsWith(rows, {
columnSeparator: ";",
rowSeparator: "\n"
});Generated Ruby requires and calls the real library, preserving native keyword and block syntax:
require "csv"
rows = CSV.parse(input, col_sep: ";", strip: true, max_field_size: 4096)
CSV.foreach("imports/users.csv") { |row| Kernel.puts(row[0]) }
output = CSV.generate_lines(rows, col_sep: ";", row_sep: "\n")Header/table modes and converters are deliberately absent because they return
CSV::Row/CSV::Table or arbitrary converter values rather than CSVRow.
Open IO, file modes, encodings, arbitrary output objects, custom nil/empty
replacements, and unchecked keyword bags are likewise omitted instead of
widening the facade. Applications parsing untrusted input should set
maxFieldSize; Ruby otherwise has no field-size limit. CSV is a default gem on
Ruby 3.3 and a bundled gem on Ruby 3.4/4.0, so the facade contract applies to
the tested distributions and does not claim availability in every minimal Ruby
installation.
ruby.Open3 is the canonical typed capture surface for child processes whose
executable and arguments are already known separately. Open3Executable
encodes Ruby's [path, argv0] process form, which selects the executable
directly even when the argument list is empty. A Haxe rest argument becomes a
native Ruby splat:
var arguments = ["-e", "STDOUT.write(ARGV.fetch(0))", "literal;$(not-run)"];
var capture = ruby.Open3.capture(ruby.Open3Executable.of("ruby"), ...arguments);
if (!capture.status.succeeded()) {
throw capture.standardError;
}
ruby.Kernel.puts(capture.standardOutput);Generated Ruby calls the real default gem directly. The argument containing shell metacharacters remains one literal child argument:
require "open3"
capture = Open3.capture3(["ruby", "ruby"], *arguments)
unless capture.last.success?
raise capture.fetch(1)
end
Kernel.puts(capture.first)Open3Capture is deliberately property-only. Its private target adapter reads
the official fixed [String, String, Process::Status] tuple through native
first, fetch(1), and last calls, while Haxe callers see only
standardOutput, standardError, and status. Open3Status exposes the
completed process result without collapsing nullable exit and signal codes.
The facade does not accept a shell command-line string. It also omits environment and process-option hashes, stdin/binmode keywords, duplicate capture variants, live streams, and pipelines rather than weakening them into unchecked bags or leaking lifecycle obligations. Use a separate future typed stream/lifecycle contract when interactive IO is genuinely required.
ruby.Set<T> is a generic facade over Ruby's native Set, not a portable
haxe.ds collection. Haxe-authored code keeps one precise element type while
Ruby owns membership and duplicate elimination through eql? and hash:
var permissions = new ruby.Set<String>(["read", "write", "read"]);
permissions.add("publish");
var elevated = permissions.union(new ruby.Set<String>(["admin"]));
if (elevated.contains("admin")) {
elevated.forEach(permission -> ruby.Kernel.puts(permission));
}The generated Ruby is an ordinary Set allocation plus receiver calls and a native block:
require "set"
permissions = Set.new(["read", "write", "read"])
permissions.add("publish")
elevated = permissions.union(Set.new(["admin"]))
elevated.each { |permission| Kernel.puts(permission) }addIfAbsent(...) and deleteIfPresent(...) preserve Ruby's nullable
changed-result contract. union, intersection, and difference return new
sets; merge, replace, subtract, filters, and element add/delete methods
mutate the receiver. Call toArray() when an Array is required. The facade does
not expose iterator(), so a Ruby-semantic set cannot silently masquerade as a
portable Haxe iterable.
Ruby assumes an element's eql?/hash identity remains stable while stored and
may store a frozen copy of a mutable String. Open Enumerable inputs, variadic
construction/merge, type-changing transforms, classification/division,
flattening, identity-comparison mode, mutable-element reset, subclass/CoreSet
behavior, raw operators, and unchecked values remain outside this bounded
contract. require "set" is retained for Ruby 3.3/3.4; Ruby 4.0 promotes Set
to a core class while preserving the tested common surface.
ruby.Regexp is the require-free native pattern facade, and
ruby.MatchData is its read-only result. The constructor accepts only a
String pattern and the closed, composable RegexpOptions values. Use
matches(...) for Ruby's side-effect-free match? predicate and match(...)
when the native MatchData value is required:
var options = ruby.RegexpOptions.ignoreCase | ruby.RegexpOptions.multiline;
var expression = new ruby.Regexp("(?<word>r.by)", options);
if (expression.matches("Ruby")) {
var match = expression.match("Ruby");
if (match != null) {
ruby.Kernel.puts(match.capture(1));
ruby.Kernel.puts(match.offset(1).start());
}
}The generated Ruby uses the core constants directly and adds no require or wrapper:
expression = Regexp.new("(?<word>r.by)", 1 | 4)
if expression.match?("Ruby")
match = expression.match("Ruby")
unless match.nil?
Kernel.puts(match.match(1))
Kernel.puts(match.offset(1)[0])
end
endmatch(...) has Ruby's normal match-global side effect; the facade does not
expose that process/thread-local global state. matches(...) maps to match?
specifically so a predicate does not update it. Indexed captures are
Null<String> because an optional group can be unmatched. MatchOffset
names the two values returned by MatchData#offset; they are character
positions, can both be null for an unmatched group, and erase to the native
two-element Array without allocating a wrapper. Capture names are available
as typed inventories through names(), namedCaptureIndexes(), and
namedCaptures(). Unchecked lookup by a caller-provided name is omitted because
Ruby raises when that name is unknown.
Regular expressions are executable matching programs. Quote literal fragments
with Regexp.escape(...), but do not mistake quoting one fragment for a bound
on the complete pattern's cost. Keep untrusted patterns and target strings
size-bounded, and give untrusted or complex patterns a workload-appropriate
per-instance timeout:
var bounded = ruby.Regexp.compileWith(
"(?:a+)+$",
ruby.RegexpOptions.none,
{timeoutSeconds: 0.05}
);The timeout is attached to that native Regexp instance; this slice does not mutate Ruby's class-wide timeout policy. Arbitrary integer/encoding flags, byte offsets, ranges, heterogeneous union and value-list APIs, block-return overloads, global last-match access, and unchecked values stay outside the bounded surface.
Native ruby.Regexp is not a replacement for portable Haxe EReg. EReg
owns stateful matched* accessors, matchSub offsets, the g option, and
Haxe-specific split/replace/map and $1/$$ replacement semantics. Its Ruby
override therefore remains a semantic adapter. It reuses the typed native
Regexp.escape operation and types its internal replacement result as
ruby.MatchData because those two contracts are exact; broad delegation would
silently change Haxe behavior.
ruby.Time and ruby.Date expose Ruby's native temporal semantics without
changing the portable Haxe Date contract. Ruby months remain one-based,
Time measures Unix time in seconds, and Date remains a civil calendar value:
import ruby.Date as RubyDate;
import ruby.Time as RubyTime;
import ruby.TimeParsing;
var publishedAt = RubyTime.utc(2024, 2, 29, 12, 0, 0);
var expiresAt = publishedAt.addSeconds(3600);
var importedAt = TimeParsing.parseIso8601("2024-02-29T12:00:00Z");
var billingDay = RubyDate.parseIso8601("2024-02-29").nextMonth();
ruby.Kernel.puts(expiresAt.strftime("%Y-%m-%d %H:%M:%S %z"));
ruby.Kernel.puts(importedAt.strftime("%Y-%m-%d %H:%M:%S %z"));
ruby.Kernel.puts(billingDay.toIso8601());The generated Ruby uses the core Time constant directly, renders native
arithmetic in infix form, requires time only for parsing, and requires date
only because Date is used:
require "date"
require "time"
published_at = Time.utc(2024, 2, 29, 12, 0, 0)
expires_at = published_at + 3600
imported_at = Time.iso8601("2024-02-29T12:00:00Z")
billing_day = Date.iso8601("2024-02-29").next_month
Kernel.puts(expires_at.strftime("%Y-%m-%d %H:%M:%S %z"))
Kernel.puts(imported_at.strftime("%Y-%m-%d %H:%M:%S %z"))
Kernel.puts(billing_day.iso8601)ruby.Time covers now, epoch/local/UTC construction, calendar components,
daylight-saving and fixed-offset reads, non-mutating local/UTC/offset copies,
epoch conversion, formatting, and precisely typed seconds arithmetic and
difference. A program that uses only this facade adds no require.
ruby.TimeParsing is a separate native view of the same Time constant. It
adds restricted ISO 8601 and explicit-format parsing behind one deduplicated
require "time", without making every core Time consumer load the parser.
Heuristic Time.parse and parser blocks remain omitted.
ruby.Date covers civil construction, today, strict ISO 8601 and
explicit-format parsing, calendar and ISO-week components, leap-year queries,
formatting, and integer day/month/year movement. It emits one deduplicated
require "date".
These facades are intentionally distinct from std/ruby/_std/Date.hx, which
owns Haxe's zero-based-month, millisecond-epoch, parsing, and string-format
semantics. The compiler emits that portable owned type as HxDate from
hx_date.rb, so the normal load-path-first runner can coexist with
require "date" without replacing or shadowing Ruby's Date. Open Numeric
coercions, subsecond units and Rational values,
permissive parsing, named timezone objects/databases, mutating zone conversion,
calendar-reform starts, enumerators, and unchecked options remain omitted.
Ruby documents DateTime as deprecated in favor of Time; no ruby.DateTime
surface is claimed by this bounded slice. Rails applications should use
RailsTime.current(), RailsTime.zone(), TimeZone, and TimeWithZone for
application-zone behavior. A Rails migration datetime column is a storage
type, not a request to use Ruby DateTime. See
Modern RubyHx And RailsHx Temporal APIs for the complete
selection and load-ownership contract.
ruby.Dir is the canonical typed facade for Ruby's core Dir class. It covers
current-directory lookup and explicit changes, home-directory lookup, entries,
children, one-pattern globbing, and directory existence/emptiness predicates:
var original = ruby.Dir.current();
var sources = ruby.Dir.glob("std/ruby/*.hx");
if (ruby.Dir.exists("std")) {
ruby.Kernel.puts(sources.length);
}
ruby.Dir.changeCurrent("std");
ruby.Dir.changeCurrent(original);The generated Ruby calls the core constant directly and adds no require:
original = Dir.pwd()
sources = Dir.glob("std/ruby/*.hx")
if Dir.exist?("std")
Kernel.puts(sources.length)
end
Dir.chdir("std")
Dir.chdir(original)changeCurrent(...) is intentionally named as a process operation: Ruby
Dir.chdir changes process-wide state when it is called without a block. The
facade returns Ruby's integer status and does not pretend to provide scoped
restoration; callers must save current() and restore it explicitly.
Ruby also accepts block-returning chdir, encoding and keyword options,
multiple glob patterns, and other open forms. They are excluded from this
bounded surface rather than represented with Dynamic, casts, raw Ruby, or a
wrapper. Future additions should introduce distinct typed contracts for those
shapes. ruby.Dir is Ruby-shaped interop and stays separate from Haxe-owned
sys.FileSystem semantics.
ruby.FileUtils is the canonical typed facade for Ruby's standard-library
FileUtils module. Its first contract deliberately accepts one String path
per source/destination slot and exposes Haxe-idiomatic names for copying,
moving, directory creation, file and empty-directory removal, secure recursive
removal, touching, content comparison, and freshness checks:
var created = ruby.FileUtils.makeDirectories("tmp/build/assets");
ruby.FileUtils.copyFile("README.md", "tmp/build/README.md");
if (ruby.FileUtils.sameContents("README.md", "tmp/build/README.md")) {
ruby.Kernel.puts(created[0]);
}
ruby.FileUtils.secureRemoveTree("tmp/build");The generated Ruby requires the real stdlib module and dispatches directly:
require "fileutils"
created = FileUtils.mkdir_p("tmp/build/assets")
FileUtils.cp("README.md", "tmp/build/README.md")
if FileUtils.compare_file("README.md", "tmp/build/README.md")
Kernel.puts(created[0])
end
FileUtils.remove_entry_secure("tmp/build")Creation, touch, and non-recursive removal methods return Array<String>
because Ruby normalizes a single path into a one-element path list. Copy and
move operations intentionally return Void: their native return values are
either nil or undocumented implementation status, so app code should depend
on the filesystem result rather than an unstable value. sameContents(...)
and isUpToDate(...) retain their native Bool contracts.
Recursive deletion is security-sensitive. Ruby documents a local TOCTTOU risk
for rm_r/rm_rf under attacker-writable parent directories, so the canonical
facade omits those shortcuts and exposes
secureRemoveTree(path, ?ignoreErrors) over
FileUtils.remove_entry_secure. Passing true explicitly requests Ruby's
force behavior and can suppress errors beyond a missing path.
forceRemoveFile(...) similarly makes rm_f error suppression visible in the
Haxe name. Use force only when intentionally accepting that loss of diagnostic
information.
Ruby's list-input, keyword, symlink, ownership, permission, install, and block
forms remain excluded rather than represented through Dynamic, casts, raw
Ruby, or a wrapper. Future additions should use distinct typed option records
or methods where their semantics justify the extra surface. As with ruby.Dir,
this is Ruby-shaped interop and does not replace Haxe sys.FileSystem.
ruby.Tempfile is the canonical typed lifecycle facade for Ruby's
standard-library Tempfile. Normal code should use createDefault(...),
create(...), or createIn(...): each accepts a typed ruby.File -> T
callback, returns that callback's T, and uses @:rubyBlockArg to emit Ruby's
recommended native block form:
var size = ruby.Tempfile.create("report-", function(file) {
file.write("typed report");
file.flush();
return file.size();
});Generated Ruby remains recognizable and lets Ruby own the ensure cleanup:
require "tempfile"
size = Tempfile.create("report-") do |file|
file.write("typed report")
file.flush()
file.size()
endRuby closes and removes the scoped file when the block exits, including when
the callback raises. The callback receives a typed ruby.File, not Dynamic.
Inline Haxe function expressions emit normal Ruby blocks; a callback stored in
a typed Haxe function value is forwarded as &callback, preserving its arity.
To support that boundary, ruby.File.open(...) now returns ruby.File and the
nominal instance exposes bounded path, write, readAll, length-bounded
read, rewind, flush, close, isClosed, and size operations. A
length-bounded read returns Null<String> because Ruby returns nil at EOF;
the all-content form remains a non-null String.
The new ruby.Tempfile(...) constructor remains available for code that must
retain a nominal temporary-file object outside a callback. That form is an
explicit lifecycle responsibility: call closeAndUnlink() deterministically.
Ruby's GC finalizer is a fallback, not a resource-management contract, and may
leave files present for an unbounded interval. path() is therefore
Null<String> because successful unlinking removes the path.
Open-ended basename arrays, mode/options keyword bags, anonymous-file keyword
forms, and general delegated IO are intentionally excluded. They should gain
separate typed contracts where needed rather than widening this lifecycle seam
through Dynamic, casts, raw Ruby, or a wrapper runtime.
- Check whether the API belongs in
std/ruby, Haxe std, RailsHx, or a gem layer. Ruby stdlib modules belong instd/ruby; Rails and gem APIs do not. - Prefer an extern over a wrapper class when the Ruby API is already the runtime owner and the Haxe surface can be typed directly.
- Add
@:rubyRequireif Ruby needs a stdlib require. - Keep raw
__ruby__out of public facades. If a tiny implementation helper needs raw Ruby, mark it explicitly with@:rubyAllowRaw, keep it narrow, and explain why a direct extern or compiler lowering was not enough. - Avoid hiding Ruby magic behind compiler globals. Use explicit imports for
convenience names, and keep exact Ruby interop under the
ruby.*package. - Add or update inventory when adding new std/runtime files:
docs/stdlib-inventory.jsonmust represent newstd/**andruntime/hxruby/**ownership. Add everystd/rubyfacade tolib/hxruby/stdlib_coverage.jsonwith its per-Ruby distribution and evidence.
Choose the smallest gate set that proves both authoring and emitted shape:
npm run test:examples-compilewhen a public example imports the facade.- Focused smoke tests such as
test:ruby-interop,test:ruby-call-shapes, or a new facade-specific smoke when behavior executes at runtime. npm run test:compiler-metadata-docswhen adding or changing compiler metadata;docs/compiler-metadata.mdis the canonical target metadata index.UPDATE_SNAPSHOTS=1 npm run test:snapshots && npm run test:snapshotswhen generated Ruby shape or requires change.npm run test:runtime-minitestwhenruntime/hxruby/**changes.npm run test:stdlib-inventory && npm run test:gap-reportwhen inventory or std ownership changes.npm run test:ruby-stdlib-coveragewhen a Ruby facade, supported branch, distribution classification, or catalog claim changes.npm run public:precommitand GitHub CI before considering the slice done.
If a facade starts as a deliberately loose boundary, add a follow-up bead for
typed decoding or stronger contracts instead of letting Dynamic spread into
canonical examples.