Skip to content

Commit

Permalink
Merge branch 'master' into feature/slice-literal-infer
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Mar 3, 2025
2 parents e358595 + 12f5656 commit a290cbe
Show file tree
Hide file tree
Showing 15 changed files with 213 additions and 45 deletions.
4 changes: 2 additions & 2 deletions spec/primitives/slice_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ describe "Primitives: Slice" do
describe ".literal" do
# TODO: implement in the interpreter
{% for num in BUILTIN_NUMBER_TYPES %}
pending_interpreted {{ "creates a read-only Slice(#{num})" }} do
it {{ "creates a read-only Slice(#{num})" }} do
slice = Slice({{ num }}).literal(0, 1, 4, 9, 16, 25)
slice.should be_a(Slice({{ num }}))
slice.to_a.should eq([0, 1, 4, 9, 16, 25] of {{ num }})
slice.read_only?.should be_true
end

# TODO: these should probably return the same pointers
pending_interpreted "creates multiple literals" do
it "creates multiple literals" do
slice1 = Slice({{ num }}).literal(1, 2, 3)
slice2 = Slice({{ num }}).literal(1, 2, 3)
slice1.should eq(slice2)
Expand Down
10 changes: 10 additions & 0 deletions spec/std/process/exit_reason_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
require "spec"

describe Process::ExitReason do
describe "#description" do
it "with exit status" do
Process::ExitReason::Normal.description.should eq "Process exited normally"
Process::ExitReason::Unknown.description.should eq "Process terminated abnormally, the cause is unknown"
end
end
end
24 changes: 24 additions & 0 deletions spec/std/process/status_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -329,4 +329,28 @@ describe Process::Status do
end
{% end %}
end

describe "#description" do
it "with exit status" do
Process::Status.new(exit_status(0)).description.should eq "Process exited normally"
Process::Status.new(exit_status(255)).description.should eq "Process exited normally"
end

it "on interrupt" do
status_for(:interrupted).description.should eq "Process was interrupted"
end

{% if flag?(:unix) && !flag?(:wasi) %}
it "with exit signal" do
Process::Status.new(Signal::HUP.value).description.should eq "Process terminated abnormally"
Process::Status.new(Signal::KILL.value).description.should eq "Process terminated abnormally"
Process::Status.new(Signal::STOP.value).description.should eq "Process received and didn't handle signal STOP"
last_signal = Signal.values[-1]
Process::Status.new(last_signal.value).description.should eq "Process received and didn't handle signal #{last_signal}"

unknown_signal = Signal.new(126)
Process::Status.new(unknown_signal.value).description.should eq "Process received and didn't handle signal 126"
end
{% end %}
end
end
47 changes: 43 additions & 4 deletions spec/std/range_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,49 @@ describe "Range" do
end

describe "#size" do
it "optimizes for int range" do
(5..12).size.should eq(8)
(5...12).size.should eq(7)
(5..4).size.should eq(0)
describe "Int" do
it { (5..12).size.should eq(8) }
it { (5...12).size.should eq(7) }
it { (5..4).size.should eq(0) }
it { (0..0).size.should eq(1) }
it { (0...0).size.should eq(0) }
it { (1..1).size.should eq(1) }
it { (1...1).size.should eq(0) }

it { (-12..-5).size.should eq(8) }
it { (-12...-5).size.should eq(7) }
it { (-4..-5).size.should eq(0) }
it { (-1..-1).size.should eq(1) }
it { (-1...-1).size.should eq(0) }

it { (-3..3).size.should eq(7) }
it { (-3...3).size.should eq(6) }
it { (-3..0).size.should eq(4) }
it { (-3...0).size.should eq(3) }
it { (3..-3).size.should eq(0) }
it { (3...-3).size.should eq(0) }
it { (-128_i8..0_i8).size.should eq(129) }
it { (-128_i8...0_i8).size.should eq(128) }

it { (Int32::MAX..Int32::MAX).size.should eq(1) }
it { (Int32::MAX...Int32::MAX).size.should eq(0) }
it { (-Int32::MAX..-Int32::MAX).size.should eq(1) }
it { (-Int32::MAX...-Int32::MAX).size.should eq(0) }

it { (5_u8..12_u8).size.should eq(8) }
it { (5_u8...12_u8).size.should eq(7) }
it { (5_u8..4_u8).size.should eq(0) }
it { (0_u8..0_u8).size.should eq(1) }
it { (0_u8...0_u8).size.should eq(0) }
it { (1_u8..1_u8).size.should eq(1) }
it { (1_u8...1_u8).size.should eq(0) }
it { (UInt8::MAX..UInt8::MAX).size.should eq(1) }
it { (UInt8::MAX...UInt8::MAX).size.should eq(0) }

it { (-32768_i16..254_u8).size.should eq(33023) }
it { (-128_i8..127_i8).size.should eq(256) }
it { (Int32::MIN..-127_i8).size.should eq(2_147_483_522) }
it { ((Int16::MIN.to_i32 - 1)..127_i16).size.should eq(32897) }
end

it "works for other types" do
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ module Crystal
symbol_table.initializer = llvm_type(@program.string).const_array(@symbol_table_values)
end

program.const_slices.each do |info|
program.const_slices.each_value do |info|
define_slice_constant(info)
end

Expand Down
32 changes: 2 additions & 30 deletions src/compiler/crystal/command.cr
Original file line number Diff line number Diff line change
Expand Up @@ -305,42 +305,14 @@ class Crystal::Command
exit exit_code
end

if message = exit_message(status)
STDERR.puts message
unless status.exit_reason.normal?
STDERR.puts status.description
STDERR.flush
end

exit 1
end

private def exit_message(status)
case status.exit_reason
when .aborted?, .session_ended?, .terminal_disconnected?
if signal = status.exit_signal?
if signal.kill?
"Program was killed"
else
"Program received and didn't handle signal #{signal} (#{signal.value})"
end
else
"Program exited abnormally"
end
when .breakpoint?
"Program hit a breakpoint and no debugger was attached"
when .access_violation?, .bad_memory_access?
# NOTE: this only happens with the empty prelude, because the stdlib
# runtime catches those exceptions and then exits _normally_ with exit
# code 11 or 1
"Program exited because of an invalid memory access"
when .bad_instruction?
"Program exited because of an invalid instruction"
when .float_exception?
"Program exited because of a floating-point system exception"
when .unknown?
"Program exited abnormally, the cause is unknown"
end
end

record CompilerConfig,
compiler : Compiler,
sources : Array(Compiler::Source),
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/ffi/lib_ffi.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#
# See https://crystal-lang.org/reference/man/required_libraries.html#compiler-dependencies
module Crystal
@[Link("ffi")]
@[Link("ffi", pkg_config: "libffi")]
{% if compare_versions(Crystal::VERSION, "1.11.0-dev") >= 0 %}
@[Link(dll: "libffi.dll")]
{% end %}
Expand Down
21 changes: 21 additions & 0 deletions src/compiler/crystal/interpreter/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1223,6 +1223,19 @@ class Crystal::Repl::Compiler < Crystal::Visitor
end

private def compile_pointerof_read_instance_var(obj, obj_type, name)
# Special handling for slice literals which have been expanded into
# `::Slice.new(pointerof($Slice:n.@buffer), ...)`,
# where `$Slice:n` is a `StaticArray`; we will build the literal contents
# in the context directly instead of using interpreter bytecode
if obj.is_a?(Path) && obj_type.is_a?(StaticArrayInstanceType) && name == "@buffer"
if buffer_name = obj.single_name?
if info = @context.program.const_slices[buffer_name]?
compile_pointerof_slice_literal_buffer(obj, info)
return false
end
end
end

ivar = obj_type.lookup_instance_var(name)
ivar_offset = ivar_offset(obj_type, name)
ivar_size = inner_sizeof_type(ivar)
Expand Down Expand Up @@ -2537,6 +2550,10 @@ class Crystal::Repl::Compiler < Crystal::Visitor
assign_to_temporary_and_return_pointer(obj)
end

private def compile_pointerof_slice_literal_buffer(obj : Path, info : Program::ConstSliceInfo) : Nil
put_ptr @context.const_slice_buffer(info), node: obj
end

# Assigns the object's value to a temporary
# local variable, and then produces a pointer to that local variable.
# In this way we make sure that the memory the pointer is pointing
Expand Down Expand Up @@ -3248,6 +3265,10 @@ class Crystal::Repl::Compiler < Crystal::Visitor
put_i128 value.to_i128!, node: node
end

private def put_ptr(value : Pointer, *, node : ASTNode?)
put_i64 value.address.to_i64!, node: node
end

private def put_string(value : String, *, node : ASTNode?)
cached_string = @context.program.string_pool.get(value)

Expand Down
35 changes: 35 additions & 0 deletions src/compiler/crystal/interpreter/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class Crystal::Repl::Context
# the proc in this Hash.
getter ffi_closure_to_compiled_def : Hash(Void*, CompiledDef)

# Cached underlying buffers for constant slices constructed via the
# `Slice.literal` compiler built-in, indexed by the internal buffer name
# (e.g. `$Slice:0`).
@const_slice_buffers = {} of String => UInt8*

def initialize(@program : Program)
@program.flags << "interpreted"

Expand Down Expand Up @@ -287,6 +292,36 @@ class Crystal::Repl::Context
end
end

def const_slice_buffer(info : Program::ConstSliceInfo) : UInt8*
@const_slice_buffers.put_if_absent(info.name) do
kind = info.element_type
element_size = kind.bytesize // 8
buffer = Pointer(UInt8).malloc(info.args.size * element_size)
ptr = buffer

info.args.each do |arg|
num = arg.as(NumberLiteral)
case kind
in .i8? then ptr.as(Int8*).value = num.value.to_i8
in .i16? then ptr.as(Int16*).value = num.value.to_i16
in .i32? then ptr.as(Int32*).value = num.value.to_i32
in .i64? then ptr.as(Int64*).value = num.value.to_i64
in .i128? then ptr.as(Int128*).value = num.value.to_i128
in .u8? then ptr.as(UInt8*).value = num.value.to_u8
in .u16? then ptr.as(UInt16*).value = num.value.to_u16
in .u32? then ptr.as(UInt32*).value = num.value.to_u32
in .u64? then ptr.as(UInt64*).value = num.value.to_u64
in .u128? then ptr.as(UInt128*).value = num.value.to_u128
in .f32? then ptr.as(Float32*).value = num.value.to_f32
in .f64? then ptr.as(Float64*).value = num.value.to_f64
end
ptr += element_size
end

buffer
end
end

def aligned_sizeof_type(node : ASTNode) : Int32
aligned_sizeof_type(node.type?)
end
Expand Down
5 changes: 3 additions & 2 deletions src/compiler/crystal/program.cr
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ module Crystal
element_type : NumberKind,
args : Array(ASTNode)

# All constant slices constructed via the `Slice.literal` primitive.
getter const_slices = [] of ConstSliceInfo
# All constant slices constructed via the `Slice.literal` compiler built-in,
# indexed by their buffers' internal names (e.g. `$Slice:0`).
getter const_slices = {} of String => ConstSliceInfo

# Here we store constants, in the
# order that they are used. They will be initialized as soon
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1626,7 +1626,7 @@ module Crystal
const_value.type = @program.static_array_of(element_type, node.args.size)
const = Const.new(@program, @program, const_name, const_value)
@program.types[const_name] = const
@program.const_slices << Program::ConstSliceInfo.new(const_name, kind, node.args)
@program.const_slices[const_name] = Program::ConstSliceInfo.new(const_name, kind, node.args)

# ::Slice.new(pointerof($Slice:n.@buffer), {{ args.size }}, read_only: true)
pointer_node = PointerOf.new(ReadInstanceVar.new(Path.new(const_name).at(node), "@buffer").at(node)).at(node)
Expand Down
1 change: 1 addition & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ module Crystal
super.downcase
end

# TODO: rename to `bit_width`
def bytesize
case self
in .i8? then 8
Expand Down
10 changes: 9 additions & 1 deletion src/gc/boehm.cr
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,15 @@ require "crystal/tracing"
{% end %}

{% if flag?(:freebsd) || flag?(:dragonfly) %}
@[Link("gc-threaded")]
{% if flag?(:interpreted) %}
# FIXME: We're not using the pkg-config name here because that would resolve the
# lib flags for libgc including `-lpthread` which the interpreter is not able
# to load on systems with modern libc where libpthread is only available as an
# (empty) static library.
@[Link("gc-threaded")]
{% else %}
@[Link("gc-threaded", pkg_config: "bdw-gc-threaded")]
{% end %}
{% elsif flag?(:interpreted) %}
# FIXME: We're not using the pkg-config name here because that would resolve the
# lib flags for libgc including `-lpthread` which the interpreter is not able
Expand Down
49 changes: 49 additions & 0 deletions src/process/status.cr
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,37 @@ enum Process::ExitReason
def abnormal?
!normal?
end

# Returns a textual description of this exit reason.
#
# ```
# Process::ExitReason::Normal.description # => "Process exited normally"
# Process::ExitReason::Aborted.description # => "Process terminated abnormally"
# ```
#
# `Status#description` provides more detail for a specific process status.
def description
case self
in .normal?
"Process exited normally"
in .aborted?, .session_ended?, .terminal_disconnected?
"Process terminated abnormally"
in .interrupted?
"Process was interrupted"
in .breakpoint?
"Process hit a breakpoint and no debugger was attached"
in .access_violation?, .bad_memory_access?
"Process terminated because of an invalid memory access"
in .bad_instruction?
"Process terminated because of an invalid instruction"
in .float_exception?
"Process terminated because of a floating-point system exception"
in .signal?
"Process terminated because of an unhandled signal"
in .unknown?
"Process terminated abnormally, the cause is unknown"
end
end
end

# The status of a terminated process. Returned by `Process#wait`.
Expand Down Expand Up @@ -371,6 +402,24 @@ class Process::Status
{% end %}
end

# Returns a textual description of this process status.
#
# ```
# Process::Status.new(0).description # => "Process exited normally"
# process = Process.new("sleep", ["10"])
# process.terminate
# process.wait.description # => "Process received and didn't handle signal TERM (15)"
# ```
#
# `ExitReason#description` provides the specific messages for non-signal exits.
def description
if exit_reason.signal? && (signal = exit_signal?)
"Process received and didn't handle signal #{signal}"
else
exit_reason.description
end
end

private def stringify_exit_status_windows(io)
# On Windows large status codes are typically expressed in hexadecimal
if @exit_status >= UInt16::MAX
Expand Down
14 changes: 11 additions & 3 deletions src/range.cr
Original file line number Diff line number Diff line change
Expand Up @@ -489,9 +489,17 @@ struct Range(B, E)

# Optimized implementation for int range
if b.is_a?(Int) && e.is_a?(Int)
e -= 1 if @exclusive
n = e - b + 1
n < 0 ? 0 : n.to_i32
return 0 if e < b

# Convert `e` to `Int32` in order to ensure that `e &- b` doesn't get
# truncated due to the smaller type of `e`.
if e.is_a?(UInt8 | Int8 | UInt16 | Int16)
e = e.to_i32!
end

diff = (e &- b).to_i32.abs
diff &+= 1 unless @exclusive
diff
else
if b.nil? || e.nil?
raise ArgumentError.new("Can't calculate size of an open range")
Expand Down

0 comments on commit a290cbe

Please sign in to comment.