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 depend
Original file line number Diff line number Diff line change
Expand Up @@ -7809,6 +7809,7 @@ jit.$(OBJEXT): {$(VPATH)}internal/error.h
jit.$(OBJEXT): {$(VPATH)}internal/eval.h
jit.$(OBJEXT): {$(VPATH)}internal/event.h
jit.$(OBJEXT): {$(VPATH)}internal/fl_type.h
jit.$(OBJEXT): {$(VPATH)}internal/core/rtypeddata.h
jit.$(OBJEXT): {$(VPATH)}internal/gc.h
jit.$(OBJEXT): {$(VPATH)}internal/glob.h
jit.$(OBJEXT): {$(VPATH)}internal/globals.h
Expand Down
11 changes: 11 additions & 0 deletions jit.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "internal/string.h"
#include "internal/class.h"
#include "internal/imemo.h"
#include "ruby/internal/core/rtypeddata.h"

enum jit_bindgen_constants {
// Field offsets for the RObject struct
Expand All @@ -27,6 +28,9 @@ enum jit_bindgen_constants {
// Field offset for prime classext's fields_obj from a class pointer
RCLASS_OFFSET_PRIME_FIELDS_OBJ = offsetof(struct RClass_and_rb_classext_t, classext.fields_obj),

// Field offset for fields_obj in RTypedData
RTYPEDDATA_OFFSET_FIELDS_OBJ = offsetof(struct RTypedData, fields_obj),

// Field offsets for the RString struct
RUBY_OFFSET_RSTRING_LEN = offsetof(struct RString, len),

Expand Down Expand Up @@ -541,6 +545,13 @@ rb_jit_class_fields_embedded_p(VALUE klass)
return !fields_obj || !FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP);
}

bool
rb_jit_typed_data_fields_embedded_p(VALUE obj)
{
VALUE fields_obj = RTYPEDDATA(obj)->fields_obj;
return !fields_obj || !FL_TEST_RAW(fields_obj, OBJ_FIELD_HEAP);
}

// Acquire the VM lock and then signal all other Ruby threads (ractors) to
// contend for the VM lock, putting them to sleep. ZJIT and YJIT use this to
// evict threads running inside generated code so among other things, it can
Expand Down
72 changes: 72 additions & 0 deletions lib/prism/parse_result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,78 @@ def failure?
!success?
end

# Returns true if the parsed source is an incomplete expression that could
# become valid with additional input. This is useful for REPL contexts (such
# as IRB) where the user may be entering a multi-line expression one line at
# a time and the implementation needs to determine whether to wait for more
# input or to evaluate what has been entered so far.
#
# Concretely, this returns true when every error present is caused by the
# parser reaching the end of the input before a construct was closed (e.g.
# an unclosed string, array, block, or keyword), and returns false when any
# error is caused by a token that makes the input structurally invalid
# regardless of what might follow (e.g. a stray `end`, `]`, or `)` with no
# matching opener).
#
# Examples:
#
# Prism.parse("1 + [").continuable? #=> true (unclosed array)
# Prism.parse("1 + ]").continuable? #=> false (stray ])
# Prism.parse("tap do").continuable? #=> true (unclosed block)
# Prism.parse("end.tap do").continuable? #=> false (stray end)
#
#--
#: () -> bool
def continuable?
return false if errors.empty?

offset = source.source.bytesize
errors.all? { |error| CONTINUABLE.include?(error.type) || error.location.start_offset == offset }
end

# The set of error types whose location the parser places at the opening
# token of an unclosed construct rather than at the end of the source. These
# errors always indicate incomplete input regardless of their byte position,
# so they are checked by type rather than by location.
#--
#: Array[Symbol]
CONTINUABLE = %i[
begin_term
begin_upcase_term
block_param_pipe_term
block_term_brace
block_term_end
case_missing_conditions
case_term
class_term
conditional_term
conditional_term_else
def_term
embdoc_term
end_upcase_term
for_term
hash_term
heredoc_term
lambda_term_brace
lambda_term_end
list_i_lower_term
list_i_upper_term
list_w_lower_term
list_w_upper_term
module_term
regexp_term
rescue_term
string_interpolated_term
string_literal_eof
symbol_term_dynamic
symbol_term_interpolated
until_term
while_term
xstring_term
].freeze

private_constant :CONTINUABLE

# Create a code units cache for the given encoding.
#--
#: (Encoding encoding) -> _CodeUnitsCache
Expand Down
1 change: 1 addition & 0 deletions prism/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ errors:
- DEF_ENDLESS
- DEF_ENDLESS_PARAMETERS
- DEF_ENDLESS_SETTER
- DEF_ENDLESS_DO_BLOCK
- DEF_NAME
- DEF_PARAMS_TERM
- DEF_PARAMS_TERM_PAREN
Expand Down
14 changes: 14 additions & 0 deletions prism/prism.c
Original file line number Diff line number Diff line change
Expand Up @@ -18998,6 +18998,20 @@ parse_expression_prefix(pm_parser_t *parser, pm_binding_power_t binding_power, b

pm_node_t *statement = parse_expression(parser, PM_BINDING_POWER_DEFINED + 1, allow_command_call, false, PM_ERR_DEF_ENDLESS, (uint16_t) (depth + 1));

// In an endless method definition, the body is not allowed to
// be a command with a do..end block.
if (PM_NODE_TYPE_P(statement, PM_CALL_NODE)) {
pm_call_node_t *call = (pm_call_node_t *) statement;

if (call->arguments != NULL && call->block != NULL && PM_NODE_TYPE_P(call->block, PM_BLOCK_NODE)) {
pm_block_node_t *block = (pm_block_node_t *) call->block;

if (parser->start[block->opening_loc.start] != '{') {
pm_parser_err_node(parser, call->block, PM_ERR_DEF_ENDLESS_DO_BLOCK);
}
}
}

if (accept1(parser, PM_TOKEN_KEYWORD_RESCUE_MODIFIER)) {
context_push(parser, PM_CONTEXT_RESCUE_MODIFIER);

Expand Down
1 change: 1 addition & 0 deletions prism/templates/src/diagnostic.c.erb
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ static const pm_diagnostic_data_t diagnostic_messages[PM_DIAGNOSTIC_ID_MAX] = {
[PM_ERR_DEF_ENDLESS] = { "could not parse the endless method body", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_ENDLESS_PARAMETERS] = { "could not parse the endless method parameters", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_ENDLESS_SETTER] = { "invalid method name; a setter method cannot be defined in an endless method definition", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_ENDLESS_DO_BLOCK] = { "unexpected `do` for block in an endless method definition", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_NAME] = { "unexpected %s; expected a method name", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_PARAMS_TERM] = { "expected a delimiter to close the parameters", PM_ERROR_LEVEL_SYNTAX },
[PM_ERR_DEF_PARAMS_TERM_PAREN] = { "unexpected %s; expected a `)` to close the parameters", PM_ERROR_LEVEL_SYNTAX },
Expand Down
3 changes: 3 additions & 0 deletions test/prism/errors/def_endless_do.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def a = a b do 1 end
^~~~~~~~ unexpected `do` for block in an endless method definition

35 changes: 35 additions & 0 deletions test/prism/errors_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,41 @@ def test_unclosed_heredoc_and_interpolation
assert_nil(statement.parts[0].statements)
end

def test_continuable
# Valid input is not continuable (nothing to continue).
refute_predicate Prism.parse("1 + 1"), :continuable?
refute_predicate Prism.parse(""), :continuable?

# Stray closing tokens make input non-continuable regardless of what
# follows (matches the feature-request examples exactly).
refute_predicate Prism.parse("1 + ]"), :continuable?
refute_predicate Prism.parse("end.tap do"), :continuable?

# Unclosed constructs are continuable.
assert_predicate Prism.parse("1 + ["), :continuable?
assert_predicate Prism.parse("tap do"), :continuable?

# Unclosed keywords.
assert_predicate Prism.parse("def foo"), :continuable?
assert_predicate Prism.parse("class Foo"), :continuable?
assert_predicate Prism.parse("module Foo"), :continuable?
assert_predicate Prism.parse("if true"), :continuable?
assert_predicate Prism.parse("while true"), :continuable?
assert_predicate Prism.parse("begin"), :continuable?
assert_predicate Prism.parse("for x in [1]"), :continuable?

# Unclosed delimiters.
assert_predicate Prism.parse("{"), :continuable?
assert_predicate Prism.parse("foo("), :continuable?
assert_predicate Prism.parse('"hello'), :continuable?
assert_predicate Prism.parse("'hello"), :continuable?
assert_predicate Prism.parse("<<~HEREDOC\nhello"), :continuable?

# A mix: stray end plus an unclosed block is not continuable because the
# stray end cannot be fixed by appending more input.
refute_predicate Prism.parse("end\ntap do"), :continuable?
end

private

def assert_errors(filepath, version)
Expand Down
1 change: 1 addition & 0 deletions yjit/src/cruby_bindings.inc.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions zjit/bindgen/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ fn main() {
.allowlist_type("RBasic")

.allowlist_type("ruby_rstring_flags")
.allowlist_type("rbimpl_typeddata_flags")

// This function prints info about a value and is useful for debugging
.allowlist_function("rb_raw_obj_info")
Expand Down Expand Up @@ -310,6 +311,8 @@ fn main() {
.allowlist_function("rb_jit_shape_too_complex_p")
.allowlist_function("rb_jit_multi_ractor_p")
.allowlist_function("rb_jit_class_fields_embedded_p")
.allowlist_function("rb_jit_typed_data_p")
.allowlist_function("rb_jit_typed_data_fields_embedded_p")
.allowlist_function("rb_jit_vm_lock_then_barrier")
.allowlist_function("rb_jit_vm_unlock")
.allowlist_function("rb_jit_for_each_iseq")
Expand Down
12 changes: 12 additions & 0 deletions zjit/src/cruby.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ use std::fmt::{Debug, Display, Formatter};
use std::os::raw::{c_char, c_int, c_uint};
use std::panic::{catch_unwind, UnwindSafe};

use crate::cast::IntoUsize as _;

// We check that we can do this with the configure script and a couple of
// static asserts. u64 and not usize to play nice with lowering to x86.
pub type size_t = u64;
Expand Down Expand Up @@ -602,6 +604,16 @@ impl VALUE {
unsafe { rb_jit_class_fields_embedded_p(self) }
}

pub fn typed_data_p(self) -> bool {
!self.special_const_p() &&
self.builtin_type() == RUBY_T_DATA &&
0 != (self.builtin_flags() & RUBY_TYPED_FL_IS_TYPED_DATA.to_usize())
}

pub fn typed_data_fields_embedded_p(self) -> bool {
unsafe { rb_jit_typed_data_fields_embedded_p(self) }
}

pub fn as_fixnum(self) -> i64 {
assert!(self.fixnum_p());
(self.0 as i64) >> 1
Expand Down
9 changes: 9 additions & 0 deletions zjit/src/cruby_bindings.inc.rs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

59 changes: 46 additions & 13 deletions zjit/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2699,7 +2699,9 @@ impl Function {
let newly_reachable = reachable.insert($target.target);
let mut target_changed = newly_reachable;
for (idx, arg) in $target.args.iter().enumerate() {
let arg = self.union_find.borrow().find_const(*arg);
let param = $self.blocks[$target.target.0].params[idx];
let param = self.union_find.borrow().find_const(param);
let new = self.insn_types[param.0].union(self.insn_types[arg.0]);
if !self.insn_types[param.0].bit_equal(new) {
self.insn_types[param.0] = new;
Expand All @@ -2717,37 +2719,38 @@ impl Function {
in_worklist.remove(block);
if !reachable.get(block) { continue; }
for insn_id in &self.blocks[block.0].insns {
let insn_type = match self.find(*insn_id) {
Insn::IfTrue { val, target } => {
let insn_id = self.union_find.borrow().find_const(*insn_id);
let insn_type = match &self.insns[insn_id.0] {
&Insn::IfTrue { val, ref target } => {
assert!(!self.type_of(val).bit_equal(types::Empty));
if self.type_of(val).could_be(Type::from_cbool(true)) {
enqueue!(self, target);
}
continue;
}
Insn::IfFalse { val, target } => {
&Insn::IfFalse { val, ref target } => {
assert!(!self.type_of(val).bit_equal(types::Empty));
if self.type_of(val).could_be(Type::from_cbool(false)) {
enqueue!(self, target);
}
continue;
}
Insn::Jump(target) => {
&Insn::Jump(ref target) => {
enqueue!(self, target);
continue;
}
Insn::Entries { targets } => {
for target in &targets {
&Insn::Entries { ref targets } => {
for target in targets {
if reachable.insert(*target) {
worklist_add!(*target);
}
}
continue;
}
insn if insn.has_output() => self.infer_type(*insn_id),
insn if insn.has_output() => self.infer_type(insn_id),
_ => continue,
};
if !self.type_of(*insn_id).bit_equal(insn_type) {
if !self.type_of(insn_id).bit_equal(insn_type) {
self.insn_types[insn_id.0] = insn_type;
}
}
Expand Down Expand Up @@ -3949,8 +3952,34 @@ impl Function {
return_type: types::BasicObject,
elidable: true })
}
} else if recv_type.flags().is_typed_data() {
// Typed T_DATA: load from fields_obj at fixed offset in RTypedData
let fields_obj = self.push_insn(block, Insn::LoadField {
recv: self_val, id: ID!(_fields_obj),
offset: RTYPEDDATA_OFFSET_FIELDS_OBJ as i32,
return_type: types::RubyValue,
});
if recv_type.flags().is_fields_embedded() {
let offset = ROBJECT_OFFSET_AS_ARY as i32
+ (SIZEOF_VALUE * ivar_index.to_usize()) as i32;
self.push_insn(block, Insn::LoadField {
recv: fields_obj, id, offset,
return_type: types::BasicObject,
})
} else {
let ptr = self.push_insn(block, Insn::LoadField {
recv: fields_obj, id: ID!(_as_heap),
offset: ROBJECT_OFFSET_AS_HEAP_FIELDS as i32,
return_type: types::CPtr,
});
let offset = SIZEOF_VALUE_I32 * ivar_index as i32;
self.push_insn(block, Insn::LoadField {
recv: ptr, id, offset,
return_type: types::BasicObject,
})
}
} else if !recv_type.flags().is_t_object() {
// Non-T_OBJECT, non-class/module (e.g. T_DATA): fall back to C call
// Non-T_OBJECT, non-class/module, non-typed-data: fall back to C call
// NOTE: it's fine to use rb_ivar_get_at_no_ractor_check because
// getinstancevariable does assume_single_ractor_mode()
let ivar_index_insn = self.push_insn(block, Insn::Const { val: Const::CUInt16(ivar_index as u16) });
Expand Down Expand Up @@ -5112,11 +5141,15 @@ impl Function {
let mut num_in_edges = vec![0; self.blocks.len()];
for block in self.rpo() {
for &insn in &self.blocks[block.0].insns {
match self.find(insn) {
Insn::IfTrue { target, .. } | Insn::IfFalse { target, .. } | Insn::Jump(target) => {
num_in_edges[target.target.0] += 1;
// Instructions without output, including branch instructions, can't be targets of
// make_equal_to, so we don't need find() here.
match &self.insns[insn.0] {
Insn::IfTrue { target: BranchEdge { target, .. }, .. }
| Insn::IfFalse { target: BranchEdge { target, .. }, .. }
| Insn::Jump(BranchEdge { target, .. }) => {
num_in_edges[target.0] += 1;
}
Insn::Entries { ref targets } => {
Insn::Entries { targets } => {
for target in targets {
num_in_edges[target.0] += 1;
}
Expand Down
Loading