Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/limitations.md
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ Limited today, but additively fixable; listed roughly easiest-first.
| `StringIO#readpartial` / `#sysread` / `#read_nonblock` with a buffer argument | the data comes back as the result, but the caller's own buffer variable is not changed | the methods are plain Ruby in the stringio package, and a String parameter a package class's method changes is not yet passed by reference the way a user class's is. Through a value that may be a File or a StringIO (`def rd(io) = io.readpartial(n)` called with both) the three methods raise NoMethodError: that receiver takes the built-in IO arms, which don't see a package's plain-Ruby methods |
| `IO::Buffer` | the full in-memory API, CRuby-faithful: `new` (INTERNAL/MAPPED flags), `get_value`/`set_value`/`get_values`/`set_values` over all 18 type symbols (little/big-endian, `u64` round-trips Bignums under `--int-overflow=promote`), `get_string`/`set_string` (NUL-safe binary), `resize`/`clear`/`copy`/`size`, `slice` (live views, safe across a source `resize`), `transfer`/`free`/`dup`, `<=>`/`==`, `hexdump`/`inspect`/`to_s`, the predicates, the tiling bitwise family (`&` `\|` `^` `~` and `and!`/`or!`/`xor!`/`not!`), `locked`, `IO::Buffer.for(string)`/.string/.size_of, the CRuby exception classes (`IO::Buffer::AccessError` etc.), and the IO integration: `#read`/`#write`/`#pread`/`#pwrite` against an IO (one syscall each, answering the count, 0 at EOF or -errno; a blocking read on a socket or pipe parks the green thread, and the buffer is locked for the duration) and `IO::Buffer.map(file, size, offset, flags)` as an mmap view (READONLY / SHARED / PRIVATE, default 0: writable and shared, as in CRuby; munmap'd by the finalizer; `resize` refused as for EXTERNAL). Passed to an `ffi_func` pointer argument, a buffer hands C its base address for the call ([FFI.md](FFI.md)). No `require` needed, as in CRuby. A literal type symbol compiles to a direct typed accessor (the wasm-runtime / binary-protocol hot path) | `#read` serves the bytes the IO's own stream already buffered before reading the descriptor (a `getc` followed by a `read` sees the next bytes); CRuby's reads the descriptor directly and can skip what its buffer holds. Three more deliberate divergences: `IO::Buffer.for(string)` copies (Spinel strings are immutable, so unobservable) and its write-through BLOCK form raises `NotImplementedError`; `each`/`each_byte`/`values` are block-form only (no Enumerator, as with StringIO); `get_string`'s third (encoding) argument is not accepted |
| The value of `super` in `initialize` (`c = super`, `super.frozen?`, `c = if f then super else [] end`) when the parent's `initialize` is the program's own | refused at compile time, naming the line | an `initialize` is compiled to return nothing, since `new` drops its value; one whose value a subclass's `super` asks for would return its last value instead |
| A call the program writes of a method an Array, a Hash, a String, an Integer, a Float, a Symbol or a Range has, on a value of that type, that the compiler has no route for (`h.transform_values` with no block) | refused at compile time, naming the class and the method; with `--defer-refusals` the call raises `NotImplementedError` where it runs. It used to raise `NoMethodError`, which CRuby does not | each such call is its own missing route, additive |
| `Array#hash` (and arrays as hash keys) | unsupported | a builtin is additive, but array *keys* need the fundamental key-dispatch above |
| `IO.popen` | refused at compile time, naming it | the bundled `open3` package's `Open3.capture2` / `capture3` (with `stdin_data:`) and `Process.spawn` with pipes cover what it is used for; the method itself is a stream held open over a child, which the open3 package does not model yet |
| `File.read` with an options hash other than an encoding (`mode:`, `open_args:`, a transcoding encoding) | refused at compile time, naming it | `encoding:` / `external_encoding:` of UTF-8 reads as the call without it, and of BINARY / ASCII-8BIT as `File.binread`; `File.open` takes a mode |
Expand Down
5 changes: 5 additions & 0 deletions src/analyze.c
Original file line number Diff line number Diff line change
Expand Up @@ -53388,8 +53388,13 @@ static void an_phase_nullable_returns(Compiler *c, const NodeTypes *promoted) {
free(rc.seen);
g_ret_no_new_poly = 0;
}
/* the node table's size when the analysis began: the calls the program
wrote are below it, the ones a rewrite made above (an_written_call) */
static int g_an_src_nodes = -1;
int an_written_call(int id) { return g_an_src_nodes < 0 || id < g_an_src_nodes; }
void analyze_program(Compiler *c) {
double tm_an = sp_timing_now();
g_an_src_nodes = c->nt->count;
an_note_written_names(c->nt);
AN_PHASE("desugar_register", an_phase_desugar_register(c));
AN_PHASE("class_structure", an_phase_class_structure(c));
Expand Down
35 changes: 35 additions & 0 deletions src/codegen_call.c
Original file line number Diff line number Diff line change
Expand Up @@ -21754,6 +21754,38 @@ int call_misses_self_method(Compiler *c, int id) {
g_emitting_class_id >= 0 && comp_method_in_chain(c, g_emitting_class_id, name, NULL) < 0 &&
an_user_defines_method(c, name);
}
/* The builtin class a receiver of type t is an instance of, for the
unresolved-call refusal below, or NULL. */
static const char *unresolved_builtin_class(TyKind t) {
if (ty_is_array(t)) return "Array";
if (ty_is_hash(t)) return "Hash";
if (t == TY_STRING || t == TY_STRBUF) return "String";
if (t == TY_INT) return "Integer";
if (t == TY_FLOAT) return "Float";
if (t == TY_SYMBOL) return "Symbol";
if (t == TY_RANGE) return "Range";
return NULL;
}
/* A call on a receiver of a builtin type known here, whose class has the
method (CRuby's own table, builtin_arity.inc), that no emitter took: the
analysis missed a route for it, and the NoMethodError the gate below
would raise is no answer CRuby gives. Refused instead, naming the method;
under --defer-refusals it raises NotImplementedError where it runs. Only
a call the program wrote: an arm a rewrite builds is guarded by a test
that may never pass here (desugar_builtin_enum_calls' `is_a?(K) ?
recv.m : ...` for a class of the program's own, the arities and block
forms of desugar_builtin_splat_forward, desugar_unpack_block's block
test), and is left to the gate. */
int an_written_call(int id);
static void unresolved_builtin_refusal(Compiler *c, int id, int recv, const char *nm, TyKind t) {
if (recv < 0 || !nm || !an_written_call(id)) return;
const char *cls = unresolved_builtin_class(t);
if (!cls || !builtin_method_known(cls, nm)) return;
char msg[256];
snprintf(msg, sizeof msg, "%s#%s on a value typed as %s, a call no route of the compiler took "
"(CRuby calls the method; this would raise NoMethodError)", cls, nm, cls);
unsupported(c, id, msg);
}
int emit_unresolved_call(Compiler *c, int id, Buf *b) {
const NodeTable *nt = c->nt;
const char *name = nt_str(nt, id, "name");
Expand Down Expand Up @@ -22465,6 +22497,9 @@ if (recv >= 0 && nm && (grt == TY_RATIONAL || grt == TY_COMPLEX)) {
unsupported(c, id, "a lazy chain held in an instance, class or global variable or a "
"constant (an Enumerator::Lazy is fused into the call that forces it; "
"keep the chain in a local or force it where it is built)");
/* after the refusals of particular shapes above, whose words say what
to do instead: this one only names the method */
unresolved_builtin_refusal(c, id, recv, nm, grt);
if (g_gate_raise) {
/* Scalar slot: the comma-expr yields a typed default the surrounding C
accepts directly. Poly slot: emit the recognizable sp_raise_nomethod
Expand Down
12 changes: 12 additions & 0 deletions test/collect/refusals.expected
Original file line number Diff line number Diff line change
Expand Up @@ -1778,6 +1778,18 @@ spinel: 1 refusal, nothing written
== test/reject/typed_array_kept_by_struct_into_mutated_param.rb [promote] rc=1
spinel: test/reject/typed_array_kept_by_struct_into_mutated_param.rb:10: an Array[Integer] is passed to `add`'s parameter `out`, which the method mutates: the parameter is a general Array and the argument would be copied into it, so the mutation would not reach the caller's array. Give the parameter the argument's kind (an rbs seed, or callers that all pass Array[Integer]), or build the argument as a general Array.
spinel: 1 refusal, nothing written
== test/reject/unresolved_builtin_method.rb [default] rc=1
spinel: test/reject/unresolved_builtin_method.rb:7: unsupported Hash#transform_values on a value typed as Hash, a call no route of the compiler took (CRuby calls the method; this would raise NoMethodError): node N (CallNode `transform_values`) recv=LocalVariableReadNode/ty28 argc=0
spinel: 1 refusal, nothing written
== test/reject/unresolved_builtin_method.rb [promote] rc=1
spinel: test/reject/unresolved_builtin_method.rb:7: unsupported Hash#transform_values on a value typed as Hash, a call no route of the compiler took (CRuby calls the method; this would raise NoMethodError): node N (CallNode `transform_values`) recv=LocalVariableReadNode/ty28 argc=0
spinel: 1 refusal, nothing written
== test/reject/unresolved_builtin_method_sym_block.rb [default] rc=1
spinel: test/reject/unresolved_builtin_method_sym_block.rb:17: unsupported Array#drop_while on a value typed as Array, a call no route of the compiler took (CRuby calls the method; this would raise NoMethodError): node N (CallNode `drop_while`) recv=CallNode/ty22 argc=0
spinel: 1 refusal, nothing written
== test/reject/unresolved_builtin_method_sym_block.rb [promote] rc=1
spinel: test/reject/unresolved_builtin_method_sym_block.rb:17: unsupported Array#drop_while on a value typed as Array, a call no route of the compiler took (CRuby calls the method; this would raise NoMethodError): node N (CallNode `drop_while`) recv=CallNode/ty22 argc=0
spinel: 1 refusal, nothing written
== test/reject/unsupported_node_kind.rb [default] rc=1
spinel: test/reject/unsupported_node_kind.rb:5: unsupported condition (non-bool): FlipFlopNode at test/reject/unsupported_node_kind.rb:5:8
spinel: test/reject/unsupported_node_kind.rb:10: unsupported condition (non-bool): MatchLastLineNode at test/reject/unsupported_node_kind.rb:10:8
Expand Down
8 changes: 8 additions & 0 deletions test/reject/unresolved_builtin_method.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# spinel: reject-subclass: Hash#transform_values on a value typed as Hash, a call no route of the compiler took
# A call of a method a Hash has, on a value typed as a Hash, that no route
# of the compiler takes (here the blockless form, which answers an
# Enumerator in CRuby): refused at compile time, naming the method, rather
# than raising NoMethodError for a method CRuby calls.
h = { a: 1, b: 2 }
e = h.transform_values
p e.class
17 changes: 17 additions & 0 deletions test/reject/unresolved_builtin_method_sym_block.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# spinel: reject-subclass: Array#drop_while on a value typed as Array, a call no route of the compiler took
# An Array's own method given `&:name`, on the answer of a method that
# returns a local assigned only `[]` (an Array the analysis types only
# after its main pass): no route of the compiler takes the call, so it is
# refused at compile time, naming the method, rather than raising
# NoMethodError for a method CRuby calls.
class Item
def initialize(s) = @s = s
def label = @s
end

def none_yet
out = []
out
end

p none_yet.drop_while(&:label)
Loading