<% if type.program? %> - <%= type.full_name %> + <%= type.full_name.gsub("::", "::") %> <% else %> - <%= type.abstract? ? "abstract " : ""%><%= type.kind %> <%= type.full_name %> + <%= type.abstract? ? "abstract " : ""%><%= type.kind %> <%= type.full_name.gsub("::", "::") %> <% end %>

From 05209443a1ddd9e2b9b677bdc2b9e71537fa69af Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Mon, 25 Sep 2023 20:28:46 +0800 Subject: [PATCH 13/37] Fix incorrect overflow in `UInt64.from_yaml` (#13829) --- spec/std/yaml/serializable_spec.cr | 8 ++++---- spec/std/yaml/serialization_spec.cr | 26 ++++++++++++++++++++++---- src/yaml/from_yaml.cr | 14 ++++++++++---- src/yaml/schema/core.cr | 21 ++++++++++++++++++++- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/spec/std/yaml/serializable_spec.cr b/spec/std/yaml/serializable_spec.cr index 91596523644f..7d13f4318350 100644 --- a/spec/std/yaml/serializable_spec.cr +++ b/spec/std/yaml/serializable_spec.cr @@ -746,21 +746,21 @@ describe "YAML::Serializable" do end it "checks that values fit into integer types" do - expect_raises(YAML::ParseException, /Expected Int16/) do + expect_raises(YAML::ParseException, /Can't read Int16/) do YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21000000, "bar": 7})) end - expect_raises(YAML::ParseException, /Expected Int8/) do + expect_raises(YAML::ParseException, /Can't read Int8/) do YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21, "bar": 7000})) end end it "checks that non-integer values for integer fields report the expected type" do - expect_raises(YAML::ParseException, /Expected Int16, not "a"/) do + expect_raises(YAML::ParseException, /Can't read Int16/) do YAMLAttrWithSmallIntegers.from_yaml(%({"foo": "a", "bar": 7})) end - expect_raises(YAML::ParseException, /Expected Int8, not "a"/) do + expect_raises(YAML::ParseException, /Can't read Int8/) do YAMLAttrWithSmallIntegers.from_yaml(%({"foo": 21, "bar": "a"})) end end diff --git a/spec/std/yaml/serialization_spec.cr b/spec/std/yaml/serialization_spec.cr index b65667d8a504..6907bdbec594 100644 --- a/spec/std/yaml/serialization_spec.cr +++ b/spec/std/yaml/serialization_spec.cr @@ -53,14 +53,32 @@ describe "YAML serialization" do end end - it "does Int32#from_yaml" do - Int32.from_yaml("123").should eq(123) + {% for int in [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64] %} + it "does {{ int }}.from_yaml" do + {{ int }}.from_yaml("0").should(be_a({{ int }})).should eq(0) + {{ int }}.from_yaml("123").should(be_a({{ int }})).should eq(123) + {{ int }}.from_yaml({{ int }}::MIN.to_s).should(be_a({{ int }})).should eq({{ int }}::MIN) + {{ int }}.from_yaml({{ int }}::MAX.to_s).should(be_a({{ int }})).should eq({{ int }}::MAX) + end + + it "raises if {{ int }}.from_yaml overflows" do + expect_raises(YAML::ParseException, "Can't read {{ int }}") do + {{ int }}.from_yaml(({{ int }}::MIN.to_big_i - 1).to_s) + end + expect_raises(YAML::ParseException, "Can't read {{ int }}") do + {{ int }}.from_yaml(({{ int }}::MAX.to_big_i + 1).to_s) + end + end + {% end %} + + it "does Int.from_yaml with prefixes" do Int32.from_yaml("0xabc").should eq(0xabc) Int32.from_yaml("0b10110").should eq(0b10110) + Int32.from_yaml("0777").should eq(0o777) end - it "does Int64#from_yaml" do - Int64.from_yaml("123456789123456789").should eq(123456789123456789) + it "does Int.from_yaml with underscores" do + Int32.from_yaml("1_2_34").should eq(1_2_34) end it "does String#from_yaml" do diff --git a/src/yaml/from_yaml.cr b/src/yaml/from_yaml.cr index f40fcfb2a402..e6f11e1924ba 100644 --- a/src/yaml/from_yaml.cr +++ b/src/yaml/from_yaml.cr @@ -59,10 +59,16 @@ end {% for type in %w(Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64) %} def {{type.id}}.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) - begin - {{type.id}}.new parse_scalar(ctx, node, Int64, {{type.id}}) - rescue err : OverflowError | ArgumentError - node.raise "Expected {{type.id}}" + ctx.read_alias(node, {{type.id}}) do |obj| + return obj + end + + if node.is_a?(YAML::Nodes::Scalar) + value = YAML::Schema::Core.parse_int(node, {{type.id}}) + ctx.record_anchor(node, value) + value + else + node.raise "Expected scalar, not #{node.kind}" end end {% end %} diff --git a/src/yaml/schema/core.cr b/src/yaml/schema/core.cr index 0add1e3e5bda..d8ca344ab92a 100644 --- a/src/yaml/schema/core.cr +++ b/src/yaml/schema/core.cr @@ -238,7 +238,7 @@ module YAML::Schema::Core raise YAML::ParseException.new("Invalid bool", *location) end - protected def self.parse_int(string, location) : Int64 + protected def self.parse_int(string : String, location) : Int64 return 0_i64 if string == "0" string.to_i64?(underscore: true, prefix: true, leading_zero_is_octal: true) || @@ -324,6 +324,25 @@ module YAML::Schema::Core parse_int?(string) || parse_float?(string) end + # Parses an integer of the given *type* according to the core schema. + # + # *type* must be a primitive integer type. Raises `YAML::ParseException` if + # *node* is not a valid integer or its value is outside *type*'s range. + def self.parse_int(node : Nodes::Node, type : T.class) : T forall T + {% unless Int::Primitive.union_types.includes?(T) %} + {% raise "Expected `type` to be a primitive integer type, not #{T}" %} + {% end %} + + string = node.value + return T.zero if string == "0" + + begin + T.new(string, underscore: true, prefix: true, leading_zero_is_octal: true) + rescue ex : ArgumentError + raise YAML::ParseException.new("Can't read #{T}", *node.location) + end + end + private def self.parse_int?(string) string.to_i64?(underscore: true, leading_zero_is_octal: true) end From b5065f05308e22929b795de2d38cf69554ce71e9 Mon Sep 17 00:00:00 2001 From: Margret Riegert Date: Tue, 26 Sep 2023 12:23:54 -0400 Subject: [PATCH 14/37] Add CSS for tables (#13822) --- .../crystal/tools/doc/html/css/style.css | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/src/compiler/crystal/tools/doc/html/css/style.css b/src/compiler/crystal/tools/doc/html/css/style.css index 2b91cf4ca7d9..9198b29b0d5e 100644 --- a/src/compiler/crystal/tools/doc/html/css/style.css +++ b/src/compiler/crystal/tools/doc/html/css/style.css @@ -729,6 +729,30 @@ img { max-width: 100%; } +table { + font-size: 14px; + display: block; + max-width: -moz-fit-content; + max-width: fit-content; + overflow-x: auto; + white-space: nowrap; + background: #fdfdfd; + text-align: center; + border: 1px solid #eee; + border-collapse: collapse; + padding: 0px 5px 0px 5px; +} + +table th { + padding: 10px; + letter-spacing: 1px; + border-bottom: 1px solid #eee; +} + +table td { + padding: 10px; +} + #sidebar-btn { height: 32px; width: 32px; @@ -935,13 +959,18 @@ img { color: white; } - pre { + pre, + table { color: white; background: #202020; border: 1px solid #353535; } + table th { + border-bottom: 1px solid #353535; + } + #sidebar-btn, #sidebar-btn-label { - color: white; + color: white; } } From a5e9ac803af2ba1fd47d3eb45609b08e920f89af Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 27 Sep 2023 00:24:04 +0800 Subject: [PATCH 15/37] Update specs for `Int::Primitive.from_json` (#13835) --- spec/std/json/serialization_spec.cr | 37 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/spec/std/json/serialization_spec.cr b/spec/std/json/serialization_spec.cr index 4657759481ea..34734b97d076 100644 --- a/spec/std/json/serialization_spec.cr +++ b/spec/std/json/serialization_spec.cr @@ -1,4 +1,5 @@ require "../spec_helper" +require "../../support/number" require "spec/helpers/iterate" require "json" require "big" @@ -36,22 +37,34 @@ describe "JSON serialization" do Path.from_json(%("foo/bar")).should eq(Path.new("foo/bar")) end - it "does UInt64.from_json" do - UInt64.from_json(UInt64::MAX.to_s).should eq(UInt64::MAX) - end + {% for int in BUILTIN_INTEGER_TYPES %} + it "does {{ int }}.from_json" do + {{ int }}.from_json("0").should(be_a({{ int }})).should eq(0) + {{ int }}.from_json("123").should(be_a({{ int }})).should eq(123) + {{ int }}.from_json({{ int }}::MIN.to_s).should(be_a({{ int }})).should eq({{ int }}::MIN) + {{ int }}.from_json({{ int }}::MAX.to_s).should(be_a({{ int }})).should eq({{ int }}::MAX) + end - it "does UInt128.from_json" do - UInt128.from_json(UInt128::MAX.to_s).should eq(UInt128::MAX) - end + # NOTE: "Invalid" shows up only for `Int64` + it "raises if {{ int }}.from_json overflows" do + expect_raises(JSON::ParseException, /(Can't read|Invalid) {{ int }}/) do + {{ int }}.from_json(({{ int }}::MIN.to_big_i - 1).to_s) + end + expect_raises(JSON::ParseException, /(Can't read|Invalid) {{ int }}/) do + {{ int }}.from_json(({{ int }}::MAX.to_big_i + 1).to_s) + end + end + {% end %} - it "does Int128.from_json" do - Int128.from_json(Int128::MAX.to_s).should eq(Int128::MAX) + it "errors on non-base-10 ints" do + expect_raises(JSON::ParseException) { Int32.from_json "0b1" } + expect_raises(JSON::ParseException) { Int32.from_json "0o1" } + expect_raises(JSON::ParseException) { Int32.from_json "0x1" } + expect_raises(JSON::ParseException) { Int32.from_json "01" } end - it "raises ParserException for overflow UInt64.from_json" do - expect_raises(JSON::ParseException, "Can't read UInt64 at line 0, column 0") do - UInt64.from_json("1#{UInt64::MAX}") - end + it "errors on underscores inside ints" do + expect_raises(JSON::ParseException) { Int32.from_json "1_2" } end it "does Array(Nil)#from_json" do From dcc5158705b31d58a8d04c9e42b442d6ecadc83b Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 27 Sep 2023 00:24:12 +0800 Subject: [PATCH 16/37] Support YAML deserialization of 128-bit integers (#13834) --- spec/std/yaml/serialization_spec.cr | 3 ++- src/yaml/from_yaml.cr | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/std/yaml/serialization_spec.cr b/spec/std/yaml/serialization_spec.cr index 6907bdbec594..414d44541fab 100644 --- a/spec/std/yaml/serialization_spec.cr +++ b/spec/std/yaml/serialization_spec.cr @@ -1,4 +1,5 @@ require "../spec_helper" +require "../../support/number" require "yaml" require "big" require "big/yaml" @@ -53,7 +54,7 @@ describe "YAML serialization" do end end - {% for int in [Int8, UInt8, Int16, UInt16, Int32, UInt32, Int64, UInt64] %} + {% for int in BUILTIN_INTEGER_TYPES %} it "does {{ int }}.from_yaml" do {{ int }}.from_yaml("0").should(be_a({{ int }})).should eq(0) {{ int }}.from_yaml("123").should(be_a({{ int }})).should eq(123) diff --git a/src/yaml/from_yaml.cr b/src/yaml/from_yaml.cr index e6f11e1924ba..b9b6e7fae45c 100644 --- a/src/yaml/from_yaml.cr +++ b/src/yaml/from_yaml.cr @@ -57,7 +57,7 @@ def Bool.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) parse_scalar(ctx, node, self) end -{% for type in %w(Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64) %} +{% for type in %w(Int8 Int16 Int32 Int64 Int128 UInt8 UInt16 UInt32 UInt64 UInt128) %} def {{type.id}}.new(ctx : YAML::ParseContext, node : YAML::Nodes::Node) ctx.read_alias(node, {{type.id}}) do |obj| return obj From 0914f732c8f86ce6d4af66b37ab3e1524a9503f1 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 27 Sep 2023 04:50:42 +0800 Subject: [PATCH 17/37] Support 128-bit integers in `JSON::PullParser#read?` (#13837) --- spec/std/json/pull_parser_spec.cr | 77 +++++++++-------------------- spec/std/json/serialization_spec.cr | 10 ++-- src/json/from_json.cr | 3 +- src/json/pull_parser.cr | 38 +++++++------- 4 files changed, 49 insertions(+), 79 deletions(-) diff --git a/spec/std/json/pull_parser_spec.cr b/spec/std/json/pull_parser_spec.cr index 8cf4127237bf..8de524e86c87 100644 --- a/spec/std/json/pull_parser_spec.cr +++ b/spec/std/json/pull_parser_spec.cr @@ -343,15 +343,17 @@ describe JSON::PullParser do assert_raw %({"foo":"bar"}) end - describe "read?" do + describe "#read?" do {% for pair in [[Int8, 1_i8], [Int16, 1_i16], [Int32, 1_i32], [Int64, 1_i64], + [Int128, "Int128.new(1)".id], [UInt8, 1_u8], [UInt16, 1_u16], [UInt32, 1_u32], [UInt64, 1_u64], + [UInt128, "UInt128.new(1)".id], [Float32, 1.0_f32], [Float64, 1.0], [String, "foo"], @@ -370,33 +372,27 @@ describe JSON::PullParser do end {% end %} - it_reads Float32::MIN - it_reads -10_f32 - it_reads 0_f32 - it_reads 10_f32 - it_reads Float32::MAX - - it_reads Float64::MIN - it_reads -10_f64 - it_reads 0_f64 - it_reads 10_f64 - it_reads Float64::MAX - - it_reads Int64::MIN - it_reads -10_i64 - it_reads 0_i64 - it_reads 10_i64 - it_reads Int64::MAX - - it "reads > Int64::MAX" do - pull = JSON::PullParser.new(Int64::MAX.to_s + "0") - pull.read?(Int64).should be_nil - end + {% for num in Int::Primitive.union_types %} + it_reads {{ num }}::MIN + {% unless num < Int::Unsigned %} + it_reads {{ num }}.new(-10) + it_reads {{ num }}.zero + {% end %} + it_reads {{ num }}.new(10) + it_reads {{ num }}::MAX + {% end %} - it "reads < Int64::MIN" do - pull = JSON::PullParser.new(Int64::MIN.to_s + "0") - pull.read?(Int64).should be_nil - end + {% for i in [8, 16, 32, 64, 128] %} + it "returns nil in place of Int{{i}} when an overflow occurs" do + JSON::PullParser.new(Int{{i}}::MAX.to_s + "0").read?(Int{{i}}).should be_nil + JSON::PullParser.new(Int{{i}}::MIN.to_s + "0").read?(Int{{i}}).should be_nil + end + + it "returns nil in place of UInt{{i}} when an overflow occurs" do + JSON::PullParser.new(UInt{{i}}::MAX.to_s + "0").read?(UInt{{i}}).should be_nil + JSON::PullParser.new("-1").read?(UInt{{i}}).should be_nil + end + {% end %} it "reads > Float32::MAX" do pull = JSON::PullParser.new(Float64::MAX.to_s) @@ -408,16 +404,6 @@ describe JSON::PullParser do pull.read?(Float32).should be_nil end - it "reads > UInt64::MAX" do - pull = JSON::PullParser.new(UInt64::MAX.to_s + "0") - pull.read?(UInt64).should be_nil - end - - it "reads == UInt64::MAX" do - pull = JSON::PullParser.new(UInt64::MAX.to_s) - pull.read?(UInt64).should eq(UInt64::MAX) - end - it "reads > Float64::MAX" do pull = JSON::PullParser.new("1" + Float64::MAX.to_s) pull.read?(Float64).should be_nil @@ -428,23 +414,6 @@ describe JSON::PullParser do pull.read?(Float64).should be_nil end - {% for pair in [[Int8, Int64::MAX], - [Int16, Int64::MAX], - [Int32, Int64::MAX], - [UInt8, -1], - [UInt16, -1], - [UInt32, -1], - [UInt64, -1], - [Float32, Float64::MAX]] %} - {% type = pair[0] %} - {% value = pair[1] %} - - it "returns nil in place of {{type}} when an overflow occurs" do - pull = JSON::PullParser.new({{value}}.to_json) - pull.read?({{type}}).should be_nil - end - {% end %} - it "doesn't accept nan or infinity" do pull = JSON::PullParser.new(%("nan")) pull.read?(Float64).should be_nil diff --git a/spec/std/json/serialization_spec.cr b/spec/std/json/serialization_spec.cr index 34734b97d076..7e320b9f81cd 100644 --- a/spec/std/json/serialization_spec.cr +++ b/spec/std/json/serialization_spec.cr @@ -437,11 +437,11 @@ describe "JSON serialization" do Union(Bool, Array(Int32)).from_json(%(true)).should be_true end - {% for type in %w(Int8 Int16 Int32 Int64 UInt8 UInt16 UInt32 UInt64).map(&.id) %} - it "deserializes union with {{type}} (fast path)" do - Union({{type}}, Array(Int32)).from_json(%(#{ {{type}}::MAX })).should eq({{type}}::MAX) - end - {% end %} + {% for type in Int::Primitive.union_types %} + it "deserializes union with {{type}} (fast path)" do + Union({{type}}, Array(Int32)).from_json({{type}}::MAX.to_s).should eq({{type}}::MAX) + end + {% end %} it "deserializes union with Float32 (fast path)" do Union(Float32, Array(Int32)).from_json(%(1)).should eq(1) diff --git a/src/json/from_json.cr b/src/json/from_json.cr index fdc18b3a9171..1c6a9e3c9c29 100644 --- a/src/json/from_json.cr +++ b/src/json/from_json.cr @@ -137,6 +137,7 @@ end "UInt128" => "u128", } %} def {{type.id}}.new(pull : JSON::PullParser) + # TODO: use `PullParser#read?` instead location = pull.location value = {% if type == "UInt64" || type == "UInt128" || type == "Int128" %} @@ -401,7 +402,7 @@ def Union.new(pull : JSON::PullParser) return pull.read_string {% end %} when .int? - {% type_order = [Int64, UInt64, Int32, UInt32, Int16, UInt16, Int8, UInt8, Float64, Float32] %} + {% type_order = [Int128, UInt128, Int64, UInt64, Int32, UInt32, Int16, UInt16, Int8, UInt8, Float64, Float32] %} {% for type in type_order.select { |t| T.includes? t } %} value = pull.read?({{type}}) return value unless value.nil? diff --git a/src/json/pull_parser.cr b/src/json/pull_parser.cr index 40bc190a72d9..a4c5b4797bd4 100644 --- a/src/json/pull_parser.cr +++ b/src/json/pull_parser.cr @@ -418,27 +418,27 @@ class JSON::PullParser read_bool if kind.bool? end - {% for type in [Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32] %} - # Reads an {{type}} value and returns it. - # - # If the value is not an integer or does not fit in a {{type}} variable, it returns `nil`. - def read?(klass : {{type}}.class) - {{type}}.new(int_value).tap { read_next } if kind.int? - rescue JSON::ParseException | OverflowError - nil - end + {% begin %} + # types that don't fit into `Int64` (integer type for `JSON::Any`)'s range + {% large_ints = [UInt64, Int128, UInt128] %} + + {% for int in Int::Primitive.union_types %} + {% is_large_int = large_ints.includes?(int) %} + + # Reads an `{{int}}` value and returns it. + # + # If the value is not an integer or does not fit in an `{{int}}`, it + # returns `nil`. + def read?(klass : {{int}}.class) : {{int}}? + if kind.int? + {{int}}.new({{ is_large_int ? "raw_value".id : "int_value".id }}).tap { read_next } + end + rescue JSON::ParseException | {{ is_large_int ? ArgumentError : OverflowError }} + nil + end + {% end %} {% end %} - # Reads an `Int64` value and returns it. - # - # If the value is not an integer or does not fin in an `Int64` variable, it returns `nil`. - def read?(klass : UInt64.class) : UInt64? - # UInt64 is a special case due to exceeding bounds of @int_value - UInt64.new(raw_value).tap { read_next } if kind.int? - rescue JSON::ParseException | ArgumentError - nil - end - # Reads an `Float32` value and returns it. # # If the value is not an integer or does not fit in an `Float32`, it returns `nil`. From 8e15d169542e94a1488bf6a8e6a802a83a1c8e07 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Wed, 27 Sep 2023 04:50:53 +0800 Subject: [PATCH 18/37] Add `Complex#to_i128`, `Complex#to_u128` (#13838) --- spec/std/complex_spec.cr | 5 ++--- src/complex.cr | 24 +++--------------------- 2 files changed, 5 insertions(+), 24 deletions(-) diff --git a/spec/std/complex_spec.cr b/spec/std/complex_spec.cr index 0524f912aa6d..fe0f1ac628ac 100644 --- a/spec/std/complex_spec.cr +++ b/spec/std/complex_spec.cr @@ -17,9 +17,8 @@ end describe "Complex" do describe "as numbers" do it_can_convert_between([Complex], [Complex]) - it_can_convert_between({{BUILTIN_NUMBER_TYPES_LTE_64}}, [Complex]) - it_can_convert_between([Complex], {{BUILTIN_NUMBER_TYPES_LTE_64}}) - # TODO pending conversion between Int128 + it_can_convert_between({{BUILTIN_NUMBER_TYPES}}, [Complex]) + it_can_convert_between([Complex], {{BUILTIN_NUMBER_TYPES}}) division_between_returns {{BUILTIN_NUMBER_TYPES}}, [Complex], Complex division_between_returns [Complex], {{BUILTIN_NUMBER_TYPES}}, Complex diff --git a/src/complex.cr b/src/complex.cr index d4e648e9c8fe..bbe7eb54e921 100644 --- a/src/complex.cr +++ b/src/complex.cr @@ -59,32 +59,14 @@ struct Complex @real end - # Returns the value as a `Float32` if possible (the imaginary part should be exactly zero), - # raises otherwise. - def to_f32 : Float32 - to_f64.to_f32 - end - # See `#to_f64`. def to_f to_f64 end - # Returns the value as an `Int64` if possible (the imaginary part should be exactly zero), - # raises otherwise. - def to_i64 : Int64 - to_f64.to_i64 - end - - delegate to_i32, to_i16, to_i8, to: to_i64 - - # Returns the value as an `UInt64` if possible (the imaginary part should be exactly zero), - # raises otherwise. - def to_u64 : UInt64 - to_f64.to_u64 - end - - delegate to_u32, to_u16, to_u8, to: to_u64 + delegate to_i128, to_i64, to_i32, to_i16, to_i8, to: to_f64 + delegate to_u128, to_u64, to_u32, to_u16, to_u8, to: to_f64 + delegate to_f32, to: to_f64 # See `#to_i32`. def to_i From 2d8ff9cb94a211c5beef1217f8b7b804cc3b8644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 26 Sep 2023 22:51:16 +0200 Subject: [PATCH 19/37] Add `tool unreachable` (#13783) Co-authored-by: Brian J. Cardiff Co-authored-by: Sijawusz Pur Rahnama Co-authored-by: Beta Ziliani --- man/crystal.1 | 4 + .../crystal/tools/unreachable_spec.cr | 429 ++++++++++++++++++ src/compiler/crystal/command.cr | 28 +- src/compiler/crystal/syntax/ast.cr | 9 + src/compiler/crystal/syntax/location.cr | 8 + src/compiler/crystal/tools/unreachable.cr | 180 ++++++++ 6 files changed, 653 insertions(+), 5 deletions(-) create mode 100644 spec/compiler/crystal/tools/unreachable_spec.cr create mode 100644 src/compiler/crystal/tools/unreachable.cr diff --git a/man/crystal.1 b/man/crystal.1 index cad5824c502a..5b2f2b247a3d 100644 --- a/man/crystal.1 +++ b/man/crystal.1 @@ -368,6 +368,10 @@ Show implementations for a given call. Use to specify the cursor position. The format for the cursor position is file:line:column. .It Cm types Show type of main variables of file. +.It Cm unreachable +Show methods that are never called. The output is a list of lines with columns +separated by tab. The first column is the location of the def, the second column +its reference name and the third column is the length in lines. .El .Pp .It diff --git a/spec/compiler/crystal/tools/unreachable_spec.cr b/spec/compiler/crystal/tools/unreachable_spec.cr new file mode 100644 index 000000000000..46a297e8359b --- /dev/null +++ b/spec/compiler/crystal/tools/unreachable_spec.cr @@ -0,0 +1,429 @@ +require "../../../spec_helper" +include Crystal + +def processed_unreachable_visitor(code) + compiler = Compiler.new + compiler.prelude = "empty" + compiler.no_codegen = true + result = compiler.compile(Compiler::Source.new(".", code), "fake-no-build") + + visitor = UnreachableVisitor.new + visitor.excludes << Path[Dir.current].to_posix.to_s + visitor.includes << "." + + process_result = visitor.process(result) + + {visitor, process_result} +end + +private def assert_unreachable(code, file = __FILE__, line = __LINE__) + expected_locations = [] of Location + + code.lines.each_with_index do |line, line_number_0| + if column_number = line.index('༓') + expected_locations << Location.new(".", line_number_0 + 1, column_number + 1) + end + end + + code = code.gsub('༓', "") + + visitor, result = processed_unreachable_visitor(code) + + result_location = result.defs.try &.compact_map(&.location).sort_by! do |loc| + {loc.filename.as(String), loc.line_number, loc.column_number} + end.map(&.to_s) + + result_location.should eq(expected_locations.map(&.to_s)), file: file, line: line +end + +# References +# +# ༓ marks the expected unreachable code to be found +# +describe "unreachable" do + it "finds top level methods" do + assert_unreachable <<-CRYSTAL + ༓def foo + 1 + end + + def bar + 2 + end + + bar + CRYSTAL + end + + it "finds instance methods" do + assert_unreachable <<-CRYSTAL + class Foo + ༓def foo + 1 + end + + def bar + 2 + end + end + + Foo.new.bar + CRYSTAL + end + + it "finds class methods" do + assert_unreachable <<-CRYSTAL + class Foo + ༓def self.foo + 1 + end + + def self.bar + 2 + end + end + + Foo.bar + CRYSTAL + end + + it "finds instance methods in nested types" do + assert_unreachable <<-CRYSTAL + module Mod + class Foo + ༓def foo + 1 + end + + def bar + 2 + end + end + end + + Mod::Foo.new.bar + CRYSTAL + end + + it "finds initializer" do + assert_unreachable <<-CRYSTAL + class Foo + ༓def initialize + end + end + + class Bar + def initialize + end + end + + Bar.new + CRYSTAL + end + + it "finds method with free variable" do + assert_unreachable <<-CRYSTAL + ༓def foo(u : U) forall U + end + + def bar(u : U) forall U + end + + bar(1) + CRYSTAL + end + + it "finds yielding methods" do + assert_unreachable <<-CRYSTAL + ༓def foo + yield + end + + def bar + yield + end + + bar {} + CRYSTAL + end + + it "finds method called from block" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + def bar + end + + def baz + yield + end + + baz do + bar + end + CRYSTAL + end + + it "finds method called from proc" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + def bar + end + + def baz(&proc : ->) + proc.call + end + + baz do + bar + end + CRYSTAL + end + + it "finds methods with proc parameter" do + assert_unreachable <<-CRYSTAL + ༓def foo(&proc : ->) + proc.call + end + + def bar(&proc : ->) + proc.call + end + + bar {} + CRYSTAL + end + + it "finds shadowed method" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + ༓def foo + end + + ༓def bar + end + + def bar + end + + bar + CRYSTAL + end + + it "finds method with `previous_def`" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + ༓def foo + previous_def + end + + def bar + end + + def bar + previous_def + end + + bar + CRYSTAL + end + + it "finds methods called from reachable code" do + assert_unreachable <<-CRYSTAL + ༓def qux_foo + end + + ༓def foo + qux_foo + end + + def qux_bar + end + + def bar + qux_bar + end + + bar + CRYSTAL + end + + it "finds method with `super`" do + assert_unreachable <<-CRYSTAL + class Foo + ༓def foo + end + + def bar + end + end + + class Qux < Foo + ༓def foo + super + end + + def bar + super + end + end + + Qux.new.bar + CRYSTAL + end + + it "finds methods in generic type" do + assert_unreachable <<-CRYSTAL + class Foo(T) + ༓def foo + 1 + end + + def bar + 2 + end + end + + Foo(Int32).new.bar + CRYSTAL + end + + it "finds method in abstract type" do + assert_unreachable <<-CRYSTAL + abstract class Foo + ༓def foo + end + + def bar + end + end + + class Baz < Foo + end + + Baz.new.bar + CRYSTAL + end + + # TODO: Should abstract Foo#bar be reported as well? + it "finds abstract method" do + assert_unreachable <<-CRYSTAL + abstract class Foo + abstract def foo + + abstract def bar + end + + class Baz < Foo + ༓def foo + end + + def bar + end + end + + Baz.new.bar + CRYSTAL + end + + it "finds virtual method" do + assert_unreachable <<-CRYSTAL + abstract class Foo + ༓def foo + end + + def bar + end + end + + class Baz < Foo + end + + class Qux < Foo + ༓def foo + end + + def bar + end + end + + Baz.new.as(Baz | Qux).bar + CRYSTAL + end + + it "ignores autogenerated enum predicates" do + assert_unreachable <<-CRYSTAL + enum Foo + BAR + BAZ + + ༓def foo + end + end + CRYSTAL + end + + it "finds method called from instance variable initializer" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + def bar + 1 + end + + class Foo + @status = Bar.new + @other : Int32 = bar + end + + class Bar + def initialize + end + end + + Foo.new + CRYSTAL + end + + it "finds method called from expanded macro" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + def bar + end + + macro bar_macro + bar + end + + def go(&block) + block.call + end + + go { bar_macro } + CRYSTAL + end + + it "finds method called from expanded macro expression" do + assert_unreachable <<-CRYSTAL + ༓def foo + end + + def bar + end + + {% begin %} + bar + {% end %} + CRYSTAL + end +end diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 64622c37a315..c8a743a1800f 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -44,6 +44,7 @@ class Crystal::Command format format project, directories and/or files hierarchy show type hierarchy implementations show implementations for given call in location + unreachable show methods that are never called types show type of main variables --help, -h show this help USAGE @@ -187,6 +188,9 @@ class Crystal::Command when "types".starts_with?(tool) options.shift types + when "unreachable".starts_with?(tool) + options.shift + unreachable when "--help" == tool, "-h" == tool puts COMMANDS_USAGE exit @@ -239,8 +243,8 @@ class Crystal::Command end end - private def compile_no_codegen(command, wants_doc = false, hierarchy = false, no_cleanup = false, cursor_command = false, top_level = false) - config = create_compiler command, no_codegen: true, hierarchy: hierarchy, cursor_command: cursor_command + private def compile_no_codegen(command, wants_doc = false, hierarchy = false, no_cleanup = false, cursor_command = false, top_level = false, path_filter = false) + config = create_compiler command, no_codegen: true, hierarchy: hierarchy, cursor_command: cursor_command, path_filter: path_filter config.compiler.no_codegen = true config.compiler.no_cleanup = no_cleanup config.compiler.wants_doc = wants_doc @@ -337,7 +341,9 @@ class Crystal::Command hierarchy_exp : String?, cursor_location : String?, output_format : String?, - combine_rpath : Bool do + combine_rpath : Bool, + includes : Array(String), + excludes : Array(String) do def compile(output_filename = self.output_filename) compiler.emit_base_filename = emit_base_filename || output_filename.rchop(File.extname(output_filename)) compiler.compile sources, output_filename, combine_rpath: combine_rpath @@ -350,7 +356,7 @@ class Crystal::Command private def create_compiler(command, no_codegen = false, run = false, hierarchy = false, cursor_command = false, - single_file = false) + single_file = false, path_filter = false) compiler = new_compiler compiler.progress_tracker = @progress_tracker link_flags = [] of String @@ -363,6 +369,8 @@ class Crystal::Command hierarchy_exp = nil cursor_location = nil output_format = nil + excludes = [] of String + includes = [] of String option_parser = parse_with_crystal_opts do |opts| opts.banner = "Usage: crystal #{command} [options] [programfile] [--] [arguments]\n\nOptions:" @@ -420,6 +428,16 @@ class Crystal::Command exit end + if path_filter + opts.on("-i ", "--include ", "Include path") do |f| + includes << f + end + + opts.on("-e ", "--exclude ", "Exclude path (default: lib)") do |f| + excludes << f + end + end + unless no_codegen opts.on("--ll", "Dump ll to Crystal's cache directory") do compiler.dump_ll = true @@ -559,7 +577,7 @@ class Crystal::Command end combine_rpath = run && !no_codegen - @config = CompilerConfig.new compiler, sources, output_filename, emit_base_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format, combine_rpath + @config = CompilerConfig.new compiler, sources, output_filename, emit_base_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format, combine_rpath, includes, excludes end private def gather_sources(filenames) diff --git a/src/compiler/crystal/syntax/ast.cr b/src/compiler/crystal/syntax/ast.cr index f94685f1f597..7b37c369927f 100644 --- a/src/compiler/crystal/syntax/ast.cr +++ b/src/compiler/crystal/syntax/ast.cr @@ -34,6 +34,11 @@ module Crystal self end + # Returns the number of lines between start and end locations + def length : Int32? + Location.lines(location, end_location) + end + # Returns a deep copy of this node. Copied nodes retain # the location and end location of the original nodes. def clone @@ -1115,6 +1120,10 @@ module Crystal end def_equals_and_hash @name, @args, @body, @receiver, @block_arg, @return_type, @macro_def, @block_arity, @abstract, @splat_index, @double_splat + + def autogenerated? + location == body.location + end end class Macro < ASTNode diff --git a/src/compiler/crystal/syntax/location.cr b/src/compiler/crystal/syntax/location.cr index d035177d9d76..de40526faae7 100644 --- a/src/compiler/crystal/syntax/location.cr +++ b/src/compiler/crystal/syntax/location.cr @@ -97,4 +97,12 @@ class Crystal::Location nil end end + + # Returns the number of lines between start and finish locations. + def self.lines(start, finish) + return unless start && finish && start.filename == finish.filename + start, finish = finish, start if finish < start + + finish.line_number - start.line_number + 1 + end end diff --git a/src/compiler/crystal/tools/unreachable.cr b/src/compiler/crystal/tools/unreachable.cr new file mode 100644 index 000000000000..f9e80db7bc66 --- /dev/null +++ b/src/compiler/crystal/tools/unreachable.cr @@ -0,0 +1,180 @@ +require "../syntax/ast" +require "../compiler" +require "json" + +module Crystal + class Command + private def unreachable + config, result = compile_no_codegen "tool unreachable", path_filter: true + format = config.output_format + + unreachable = UnreachableVisitor.new + + unreachable.includes.concat config.includes.map { |path| ::Path[path].expand.to_posix.to_s } + + unreachable.excludes.concat CrystalPath.default_paths.map { |path| ::Path[path].expand.to_posix.to_s } + unreachable.excludes.concat config.excludes.map { |path| ::Path[path].expand.to_posix.to_s } + + defs = unreachable.process(result) + defs.defs.sort_by! do |a_def| + location = a_def.location.not_nil! + { + location.filename.as(String), + location.line_number, + location.column_number, + } + end + + case format + when "json" + defs.to_json(STDOUT) + else + defs.to_text(STDOUT) + end + end + end + + record UnreachableResult, defs : Array(Def) do + include JSON::Serializable + + def to_text(io) + defs.each do |a_def| + io << a_def.location << "\t" + io << a_def.short_reference << "\t" + io << a_def.length << " lines" + io.puts + end + end + + def to_json(builder : JSON::Builder) + builder.array do + defs.each do |a_def| + builder.object do + builder.field "name", a_def.short_reference + builder.field "location", a_def.location.to_s + if lines = a_def.length + builder.field "lines", lines + end + end + end + end + end + end + + # This visitor walks the entire reachable code tree and collect locations + # of all defs that are a target of a call into `@used_def_locations`. + # The locations are filtered to only those that we're interested in per + # `@includes` and `@excludes`. + # Then it traverses all types and their defs and reports those that are not + # in `@used_def_locations` (and match the filter). + class UnreachableVisitor < Visitor + @used_def_locations = Set(Location).new + @defs : Set(Def) = Set(Def).new.compare_by_identity + @visited_defs : Set(Def) = Set(Def).new.compare_by_identity + + property includes = [] of String + property excludes = [] of String + + def process_type(type) + if type.is_a?(ModuleType) + track_unused_defs type + end + + type.types?.try &.each_value do |inner_type| + process_type(inner_type) + end + + process_type(type.metaclass) if type.metaclass != type + end + + def process(result : Compiler::Result) + @defs.clear + + result.node.accept(self) + + process_type(result.program) + + UnreachableResult.new @defs.to_a + end + + def visit(node) + true + end + + def visit(node : ExpandableNode) + if expanded = node.expanded + expanded.accept self + end + + true + end + + def visit(node : Call) + if expanded = node.expanded + expanded.accept(self) + + return true + end + + node.target_defs.try &.each do |a_def| + if (location = a_def.location) + @used_def_locations << location if interested_in(location) + end + + if @visited_defs.add?(a_def) + a_def.body.accept(self) + end + end + + true + end + + def visit(node : ClassDef) + node.resolved_type.instance_vars_initializers.try &.each do |initializer| + initializer.value.accept(self) + end + + true + end + + private def track_unused_defs(module_type : ModuleType) + module_type.defs.try &.each_value.each do |defs_with_meta| + defs_with_meta.each do |def_with_meta| + check_def(def_with_meta.def) + end + end + end + + private def check_def(a_def : Def) + return if a_def.abstract? + return if a_def.autogenerated? + return unless interested_in(a_def.location) + + previous = a_def.previous.try(&.def) + + check_def(previous) if previous && !a_def.calls_previous_def? + + return if @used_def_locations.includes?(a_def.location) + + check_def(previous) if previous && a_def.calls_previous_def? + + @defs << a_def + end + + private def interested_in(location) + if filename = location.try(&.filename).as?(String) + match_path?(filename) + end + end + + def match_path?(path) + paths = ::Path[path].parents << ::Path[path] + + match_any_pattern?(includes, paths) || !match_any_pattern?(excludes, paths) + end + + private def match_any_pattern?(patterns, paths) + patterns.any? { |pattern| paths.any? { |path| path == pattern || File.match?(pattern, path.to_posix) } } + end + end +end From 09d71d482bf4bad2e931d6dce332666c7e18b72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 27 Sep 2023 10:26:59 +0200 Subject: [PATCH 20/37] Feature: Add `crystal tool dependencies` (#13631) Co-authored-by: Quinton Miller Co-authored-by: Sijawusz Pur Rahnama Co-authored-by: Caspian Baska --- etc/completion.bash | 2 +- etc/completion.fish | 12 ++ etc/completion.zsh | 17 ++ man/crystal.1 | 23 ++- src/compiler/crystal/command.cr | 42 ++++- src/compiler/crystal/compiler.cr | 2 + src/compiler/crystal/program.cr | 16 ++ .../crystal/semantic/semantic_visitor.cr | 41 ++-- src/compiler/crystal/tools/dependencies.cr | 178 ++++++++++++++++++ 9 files changed, 308 insertions(+), 25 deletions(-) create mode 100644 src/compiler/crystal/tools/dependencies.cr diff --git a/etc/completion.bash b/etc/completion.bash index d36a6d282a07..d8731c65bff7 100644 --- a/etc/completion.bash +++ b/etc/completion.bash @@ -66,7 +66,7 @@ _crystal() _crystal_compgen_options "${opts}" "${cur}" else if [[ "${prev}" == "tool" ]] ; then - local subcommands="context format hierarchy implementations types" + local subcommands="context dependencies format hierarchy implementations types" _crystal_compgen_options "${subcommands}" "${cur}" else _crystal_compgen_sources "${cur}" diff --git a/etc/completion.fish b/etc/completion.fish index 5bdb8532fec7..150dd37108d8 100644 --- a/etc/completion.fish +++ b/etc/completion.fish @@ -149,6 +149,18 @@ complete -c crystal -n "__fish_seen_subcommand_from context" -s p -l progress -d complete -c crystal -n "__fish_seen_subcommand_from context" -s t -l time -d "Enable execution time output" complete -c crystal -n "__fish_seen_subcommand_from context" -l stdin-filename -d "Source file name to be read from STDIN" +complete -c crystal -n "__fish_seen_subcommand_from tool; and not __fish_seen_subcommand_from $tool_subcommands" -a "dependencies" -d "show tree of required source files" -x +complete -c crystal -n "__fish_seen_subcommand_from context" -s i -l include -d "Include path in output" +complete -c crystal -n "__fish_seen_subcommand_from context" -s e -l exclude -d "Exclude path in output" +complete -c crystal -n "__fish_seen_subcommand_from context" -s D -l define -d "Define a compile-time flag" +complete -c crystal -n "__fish_seen_subcommand_from context" -s f -l format -d "Output format 'tree' (default), 'flat', 'dot', or 'mermaid'." -a "tree flat dot mermaid" -f +complete -c crystal -n "__fish_seen_subcommand_from context" -l error-trace -d "Show full error trace" +complete -c crystal -n "__fish_seen_subcommand_from context" -l no-color -d "Disable colored output" +complete -c crystal -n "__fish_seen_subcommand_from context" -l prelude -d "Use given file as prelude" +complete -c crystal -n "__fish_seen_subcommand_from context" -s s -l stats -d "Enable statistics output" +complete -c crystal -n "__fish_seen_subcommand_from context" -s p -l progress -d "Enable progress output" +complete -c crystal -n "__fish_seen_subcommand_from context" -s t -l time -d "Enable execution time output" + complete -c crystal -n "__fish_seen_subcommand_from tool; and not __fish_seen_subcommand_from $tool_subcommands" -a "expand" -d "show macro expansion for given location" -x complete -c crystal -n "__fish_seen_subcommand_from expand" -s D -l define -d "Define a compile-time flag" complete -c crystal -n "__fish_seen_subcommand_from expand" -s c -l cursor -d "Cursor location with LOC as path/to/file.cr:line:column" diff --git a/etc/completion.zsh b/etc/completion.zsh index 783b9f3cd295..bf57d80208df 100644 --- a/etc/completion.zsh +++ b/etc/completion.zsh @@ -61,6 +61,11 @@ local -a cursor_args; cursor_args=( '(-c --cursor)'{-c,--cursor}'[cursor location with LOC as path/to/file.cr:line:column]:LOC' ) +local -a include_exclude_args; cursor_args=( + '(-i --include)'{-i,--include}'[Include path in output]' \ + '(-i --exclude)'{-i,--exclude}'[Exclude path in output]' +) + local -a programfile; programfile='*:Crystal File:_files -g "*.cr(.)"' # TODO make 'emit' allow completion with more than one @@ -158,6 +163,7 @@ _crystal-tool() { commands=( "context:show context for given location" + "dependencies:show tree of required source files" "expand:show macro expansion for given location" "format:format project, directories and/or files" "hierarchy:show type hierarchy" @@ -183,6 +189,17 @@ _crystal-tool() { $cursor_args ;; + (dependencies) + _arguments \ + $programfile \ + $help_args \ + $no_color_args \ + $exec_args \ + '(-f --format)'{-f,--format}'[output format 'tree' (default), 'flat', 'dot', or 'mermaid']:' \ + $prelude_args \ + $include_exclude_args + ;; + (expand) _arguments \ $programfile \ diff --git a/man/crystal.1 b/man/crystal.1 index 5b2f2b247a3d..5f08ce29d07e 100644 --- a/man/crystal.1 +++ b/man/crystal.1 @@ -346,12 +346,33 @@ Disable colored output. .Op -- .Op arguments .Pp -Run a tool. The available tools are: context, format, hierarchy, implementations, and types. +Run a tool. The available tools are: context, dependencies, format, hierarchy, implementations, and types. .Pp Tools: .Bl -tag -offset indent .It Cm context Show context for given location. +.It Cm dependencies +Show tree of required source files. +.Pp +Options: +.Bl -tag -width "12345678" -compact +.Pp +.It Fl D Ar FLAG, Fl -define= Ar FLAG +Define a compile-time flag. This is useful to conditionally define types, methods, or commands based on flags available at compile time. The default flags are from the target triple given with --target-triple or the hosts default, if none is given. +.It Fl f Ar FORMAT, Fl -format= Ar FORMAT +Output format 'tree' (default), 'flat', 'dot', or 'mermaid'. +.It Fl i Ar PATH, Fl -include= Ar PATH +Include path in output. +.It Fl e Ar PATH, Fl -exclude= Ar PATH +Exclude path in output. +.It Fl -error-trace +Show full error trace. +.It Fl -prelude +Specify prelude to use. The default one initializes the garbage collector. You can also use --prelude=empty to use no preludes. This can be useful for checking code generation for a specific source code file. +.It Fl -verbose +Show skipped and heads of filtered paths +.El .It Cm expand Show macro expansion for given location. .It Cm format diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index c8a743a1800f..1540aafa9a46 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -43,6 +43,7 @@ class Crystal::Command expand show macro expansion for given location format format project, directories and/or files hierarchy show type hierarchy + dependencies show file dependency tree implementations show implementations for given call in location unreachable show methods that are never called types show type of main variables @@ -182,6 +183,9 @@ class Crystal::Command when "hierarchy".starts_with?(tool) options.shift hierarchy + when "dependencies".starts_with?(tool) + options.shift + dependencies when "implementations".starts_with?(tool) options.shift implementations @@ -341,9 +345,11 @@ class Crystal::Command hierarchy_exp : String?, cursor_location : String?, output_format : String?, + dependency_output_format : DependencyPrinter::Format, combine_rpath : Bool, includes : Array(String), - excludes : Array(String) do + excludes : Array(String), + verbose : Bool do def compile(output_filename = self.output_filename) compiler.emit_base_filename = emit_base_filename || output_filename.rchop(File.extname(output_filename)) compiler.compile sources, output_filename, combine_rpath: combine_rpath @@ -356,7 +362,8 @@ class Crystal::Command private def create_compiler(command, no_codegen = false, run = false, hierarchy = false, cursor_command = false, - single_file = false, path_filter = false) + single_file = false, dependencies = false, + path_filter = false) compiler = new_compiler compiler.progress_tracker = @progress_tracker link_flags = [] of String @@ -369,8 +376,10 @@ class Crystal::Command hierarchy_exp = nil cursor_location = nil output_format = nil + dependency_output_format = nil excludes = [] of String includes = [] of String + verbose = false option_parser = parse_with_crystal_opts do |opts| opts.banner = "Usage: crystal #{command} [options] [programfile] [--] [arguments]\n\nOptions:" @@ -414,8 +423,27 @@ class Crystal::Command end end - opts.on("-f text|json", "--format text|json", "Output format text (default) or json") do |f| - output_format = f + if dependencies + opts.on("-f tree|flat|dot|mermaid", "--format tree|flat|dot|mermaid", "Output format tree (default), flat, dot, or mermaid") do |f| + dependency_output_format = DependencyPrinter::Format.parse?(f) + error "Invalid format: #{f}. Options are: tree, flat, dot, or mermaid" unless dependency_output_format + end + + opts.on("-i ", "--include ", "Include path") do |f| + includes << f + end + + opts.on("-e ", "--exclude ", "Exclude path (default: lib)") do |f| + excludes << f + end + + opts.on("--verbose", "Show skipped and filtered paths") do + verbose = true + end + else + opts.on("-f text|json", "--format text|json", "Output format text (default) or json") do |f| + output_format = f + end end opts.on("--error-trace", "Show full error trace") do @@ -561,6 +589,8 @@ class Crystal::Command end end + dependency_output_format ||= DependencyPrinter::Format::Tree + output_format ||= "text" unless output_format.in?("text", "json") error "You have input an invalid format, only text and JSON are supported" @@ -577,7 +607,9 @@ class Crystal::Command end combine_rpath = run && !no_codegen - @config = CompilerConfig.new compiler, sources, output_filename, emit_base_filename, arguments, specified_output, hierarchy_exp, cursor_location, output_format, combine_rpath, includes, excludes + @config = CompilerConfig.new compiler, sources, output_filename, emit_base_filename, + arguments, specified_output, hierarchy_exp, cursor_location, output_format, + dependency_output_format.not_nil!, combine_rpath, includes, excludes, verbose end private def gather_sources(filenames) diff --git a/src/compiler/crystal/compiler.cr b/src/compiler/crystal/compiler.cr index f32ecd344be1..1099569c97aa 100644 --- a/src/compiler/crystal/compiler.cr +++ b/src/compiler/crystal/compiler.cr @@ -144,6 +144,8 @@ module Crystal # Whether to link statically property? static = false + property dependency_printer : DependencyPrinter? = nil + # Program that was created for the last compilation. property! program : Program diff --git a/src/compiler/crystal/program.cr b/src/compiler/crystal/program.cr index d1bf702d70b1..97808279061c 100644 --- a/src/compiler/crystal/program.cr +++ b/src/compiler/crystal/program.cr @@ -462,6 +462,22 @@ module Crystal recorded_requires << RecordedRequire.new(filename, relative_to) end + def run_requires(node : Require, filenames) : Nil + dependency_printer = compiler.try(&.dependency_printer) + + filenames.each do |filename| + unseen_file = requires.add?(filename) + + dependency_printer.try(&.enter_file(filename, unseen_file)) + + if unseen_file + yield filename + end + + dependency_printer.try(&.leave_file) + end + end + # Finds *filename* in the configured CRYSTAL_PATH for this program, # relative to *relative_to*. def find_in_path(filename, relative_to = nil) : Array(String)? diff --git a/src/compiler/crystal/semantic/semantic_visitor.cr b/src/compiler/crystal/semantic/semantic_visitor.cr index 0b686db9dc40..b85fdba37109 100644 --- a/src/compiler/crystal/semantic/semantic_visitor.cr +++ b/src/compiler/crystal/semantic/semantic_visitor.cr @@ -69,25 +69,11 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor if filenames nodes = Array(ASTNode).new(filenames.size) - filenames.each do |filename| - if @program.requires.add?(filename) - parser = @program.new_parser(File.read(filename)) - parser.filename = filename - parser.wants_doc = @program.wants_doc? - begin - parsed_nodes = parser.parse - parsed_nodes = @program.normalize(parsed_nodes, inside_exp: inside_exp?) - # We must type the node immediately, in case a file requires another - # *before* one of the files in `filenames` - parsed_nodes.accept self - rescue ex : CodeError - node.raise "while requiring \"#{node.string}\"", ex - rescue ex - raise Error.new "while requiring \"#{node.string}\"", ex - end - nodes << FileNode.new(parsed_nodes, filename) - end + + @program.run_requires(node, filenames) do |filename| + nodes << require_file(node, filename) end + expanded = Expressions.from(nodes) else expanded = Nop.new @@ -98,6 +84,25 @@ abstract class Crystal::SemanticVisitor < Crystal::Visitor false end + private def require_file(node : Require, filename : String) + parser = @program.new_parser(File.read(filename)) + parser.filename = filename + parser.wants_doc = @program.wants_doc? + begin + parsed_nodes = parser.parse + parsed_nodes = @program.normalize(parsed_nodes, inside_exp: inside_exp?) + # We must type the node immediately, in case a file requires another + # *before* one of the files in `filenames` + parsed_nodes.accept self + rescue ex : CodeError + node.raise "while requiring \"#{node.string}\"", ex + rescue ex + raise Error.new "while requiring \"#{node.string}\"", ex + end + + FileNode.new(parsed_nodes, filename) + end + def visit(node : ClassDef) check_outside_exp node, "declare class" pushing_type(node.resolved_type) do diff --git a/src/compiler/crystal/tools/dependencies.cr b/src/compiler/crystal/tools/dependencies.cr new file mode 100644 index 000000000000..745e7339f0ec --- /dev/null +++ b/src/compiler/crystal/tools/dependencies.cr @@ -0,0 +1,178 @@ +require "set" +require "colorize" +require "../syntax/ast" + +class Crystal::Command + private def dependencies + config = create_compiler "tool dependencies", no_codegen: true, dependencies: true + + dependency_printer = DependencyPrinter.create(STDOUT, format: config.dependency_output_format, verbose: config.verbose) + + dependency_printer.includes.concat config.includes.map { |path| ::Path[path].expand.to_s } + dependency_printer.excludes.concat config.excludes.map { |path| ::Path[path].expand.to_s } + config.compiler.dependency_printer = dependency_printer + + dependency_printer.start_format + config.compiler.top_level_semantic config.sources + dependency_printer.end_format + end +end + +module Crystal + abstract class DependencyPrinter + enum Format + Flat + Tree + Dot + Mermaid + end + + @stack = [] of String + @filter_depth = Int32::MAX + + @format : Format + + property includes = [] of String + property excludes = [] of String + + getter default_paths : Array(::Path) = CrystalPath.default_paths.map { |path| ::Path[path].expand } + + def self.create(io : IO, format : Format = Format::Tree, verbose : Bool = false) + case format + in .flat?, .tree? + List.new(io, format, verbose) + in .dot? + Dot.new(io, format, verbose) + in .mermaid? + Mermaid.new(io, format, verbose) + end + end + + def initialize(@io : IO, @format : Format = Format::Tree, @verbose : Bool = false) + end + + def enter_file(filename : String, unseen : Bool) + if @stack.size <= @filter_depth + filter = filter?(filename) + + if filter + @filter_depth = @stack.size + else + @filter_depth = Int32::MAX + end + + if (unseen && !filter) || @verbose + print_indent + + print_file(filename, @stack.last?, filter, unseen) + end + end + + @stack << filename + end + + def leave_file + @stack.pop + end + + def start_format + end + + private def print_indent + end + + private abstract def print_file(filename, parent, filter, unseen) + + def end_format + end + + private def edge_comment(filter = false, unseen = false) + if unseen + "filtered" if filter + else + "duplicate skipped" + end + end + + private def path(filename) + ::Path[filename].relative_to?(Dir.current) || filename + end + + private def filter?(filename) + paths = ::Path[filename].parents + paths << ::Path[filename] + + return false if match_patterns?(includes, paths) + + return true if default_paths.any? { |path| paths.includes?(path) } + + match_patterns?(excludes, paths) + end + + private def match_patterns?(patterns, paths) + patterns.any? { |pattern| paths.any? { |path| File.match?(pattern, path) } } + end + + class List < DependencyPrinter + private def print_file(filename, parent, filter, unseen) + @io.print path(filename) + if comment = edge_comment(filter, unseen) + @io.print " " + @io.print comment + end + @io.puts + end + + private def print_indent + @io.print " " * @stack.size unless @stack.empty? + end + end + + class Dot < DependencyPrinter + def start_format + @io.puts "digraph G {" + end + + def end_format + @io.puts "}" + end + + private def print_file(filename, parent, filter, unseen) + return unless parent + + @io.print " " + @io.print path(parent) + @io.print " -> " + @io.print path(filename) + if comment = edge_comment(filter, unseen) + @io.print %( [label="#{comment}"]) + end + @io.puts + end + + private def path(filename) + super.to_s.inspect + end + end + + class Mermaid < DependencyPrinter + def start_format + @io.puts "graph LR" + end + + private def print_file(filename, parent, filter, unseen) + return unless parent + + @io.print " " + @io.print path(parent) + @io.print " -->" + if comment = edge_comment(filter, unseen) + @io.print "|#{comment}|" + end + @io.print " " + @io.print path(filename) + @io.puts + end + end + end +end From e291c62c1418ee840811208282feb327433dea82 Mon Sep 17 00:00:00 2001 From: Evan Paterakis Date: Wed, 27 Sep 2023 11:27:14 +0300 Subject: [PATCH 21/37] Fix docs dark mode dropdown background on blink (#13840) --- src/compiler/crystal/tools/doc/html/css/style.css | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/compiler/crystal/tools/doc/html/css/style.css b/src/compiler/crystal/tools/doc/html/css/style.css index 9198b29b0d5e..3d0a8a5f3b2c 100644 --- a/src/compiler/crystal/tools/doc/html/css/style.css +++ b/src/compiler/crystal/tools/doc/html/css/style.css @@ -1,3 +1,7 @@ +:root { + color-scheme: light dark; +} + html, body { background: #FFFFFF; position: relative; @@ -818,10 +822,6 @@ table td { } @media (prefers-color-scheme: dark) { - :root { - color-scheme: dark; - } - html, body { background: #1b1b1b; } @@ -848,6 +848,10 @@ table td { border: 1px solid #353535; } + .project-versions-nav > option { + background-color: #222; + } + .superclass-hierarchy .superclass a, .superclass-hierarchy .superclass a:visited, .other-type a, From 6b9ad16c98f0289a90a6cc74493a83898050af6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Wed, 27 Sep 2023 10:27:23 +0200 Subject: [PATCH 22/37] Minor fixup for `HTML.decode_codepoint` (#13843) --- src/html.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/html.cr b/src/html.cr index b3943fb82cb9..75ff1c246f02 100644 --- a/src/html.cr +++ b/src/html.cr @@ -162,7 +162,7 @@ module HTML case codepoint when 0x80..0x9F # Replace characters from Windows-1252 with UTF-8 equivalents. - CHARACTER_REPLACEMENTS[codepoint - 0x80].to_s + CHARACTER_REPLACEMENTS[codepoint - 0x80] when 0, .>(Char::MAX_CODEPOINT), 0xD800..0xDFFF # unicode surrogate characters @@ -175,7 +175,7 @@ module HTML (0xFDD0..0xFDEF).includes?(codepoint) || # last two of each plane (nonchars) disallowed codepoint & 0xFFFF >= 0xFFFE || - # unicode control characters expect space + # unicode control characters except space (codepoint < 0x0020 && !codepoint.in?(0x0009, 0x000A, 0x000C)) codepoint.unsafe_chr end From 95747506d08de43e70b26c20785409e6f778d450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Thu, 28 Sep 2023 14:56:55 +0200 Subject: [PATCH 23/37] [CI] Refactor `crystal_bootstrap_version` (#13845) --- .github/workflows/linux.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index c9d00e9084d0..8be77f17ff05 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -19,20 +19,24 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - crystal_bootstrap_version: [1.2.2, 1.3.2, 1.4.1, 1.5.1, 1.6.2] - flags: ["USE_PCRE1=true"] + crystal_bootstrap_version: [1.7.3, 1.8.2, 1.9.2] + flags: [""] include: # libffi is only available starting from the 1.2.2 build images - crystal_bootstrap_version: 1.0.0 flags: "FLAGS=-Dwithout_ffi USE_PCRE1=true" - crystal_bootstrap_version: 1.1.1 flags: "FLAGS=-Dwithout_ffi USE_PCRE1=true" - - crystal_bootstrap_version: 1.7.3 - flags: "" - - crystal_bootstrap_version: 1.8.2 - flags: "" - - crystal_bootstrap_version: 1.9.2 - flags: "" + - crystal_bootstrap_version: 1.2.2 + flags: "USE_PCRE1=true" + - crystal_bootstrap_version: 1.3.2 + flags: "USE_PCRE1=true" + - crystal_bootstrap_version: 1.4.1 + flags: "USE_PCRE1=true" + - crystal_bootstrap_version: 1.5.1 + flags: "USE_PCRE1=true" + - crystal_bootstrap_version: 1.6.2 + flags: "USE_PCRE1=true" steps: - name: Download Crystal source uses: actions/checkout@v4 From ca0dc199179676dc0d55da1b5a288817632c0ee7 Mon Sep 17 00:00:00 2001 From: kojix2 <2xijok@gmail.com> Date: Wed, 4 Oct 2023 20:44:12 +0900 Subject: [PATCH 24/37] Fix typo in unistd.cr (#13850) --- src/lib_c/aarch64-android/c/unistd.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib_c/aarch64-android/c/unistd.cr b/src/lib_c/aarch64-android/c/unistd.cr index c2e22a6ad0fa..bb8f78e8d1d7 100644 --- a/src/lib_c/aarch64-android/c/unistd.cr +++ b/src/lib_c/aarch64-android/c/unistd.cr @@ -41,7 +41,7 @@ lib LibC fun lseek(__fd : Int, __offset : OffT, __whence : Int) : OffT fun pipe(__fds : Int[2]) : Int fun read(__fd : Int, __buf : Void*, __count : SizeT) : SSizeT - fun pread(__fd : Int, __buf : Void*, __count : SizeT, __offest : OffT) : SSizeT + fun pread(__fd : Int, __buf : Void*, __count : SizeT, __offset : OffT) : SSizeT fun rmdir(__path : Char*) : Int fun symlink(__old_path : Char*, __new_path : Char*) : Int fun readlink(__path : Char*, __buf : Char*, __buf_size : SizeT) : SSizeT From 672389d23ba53b96be6af34aaea7680507240c5f Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Thu, 5 Oct 2023 17:40:12 +0800 Subject: [PATCH 25/37] Support LLVM 17 (#13782) --- .github/workflows/llvm.yml | 2 ++ .github/workflows/win.yml | 22 ++++++++++++++-------- .github/workflows/win_build_portable.yml | 5 ++++- src/llvm/ext/llvm-versions.txt | 2 +- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/llvm.yml b/.github/workflows/llvm.yml index bf8660be39bd..ee07fdb39b04 100644 --- a/.github/workflows/llvm.yml +++ b/.github/workflows/llvm.yml @@ -24,6 +24,8 @@ jobs: llvm_ubuntu_version: "18.04" - llvm_version: "16.0.3" llvm_ubuntu_version: "22.04" + - llvm_version: "17.0.2" + llvm_ubuntu_version: "22.04" name: "LLVM ${{ matrix.llvm_version }}" steps: - name: Checkout Crystal source diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index d4d3df576ef9..45a87b90f04e 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -6,6 +6,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} +env: + CI_LLVM_VERSION: "17.0.2" + jobs: x86_64-windows-libs: runs-on: windows-2022 @@ -172,12 +175,12 @@ jobs: uses: actions/cache@v3 with: path: llvm - key: llvm-libs-16.0.3-msvc + key: llvm-libs-${{ env.CI_LLVM_VERSION }}-msvc - name: Download LLVM if: steps.cache-llvm.outputs.cache-hit != 'true' run: | - iwr https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.3/llvm-16.0.3.src.tar.xz -OutFile llvm.tar.xz + iwr https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.CI_LLVM_VERSION }}/llvm-${{ env.CI_LLVM_VERSION }}.src.tar.xz -OutFile llvm.tar.xz (Get-FileHash -Algorithm SHA256 .\llvm.tar.xz).hash -eq "D820E63BC3A6F4F833EC69A1EF49A2E81992E90BC23989F98946914B061AB6C7" 7z x llvm.tar.xz 7z x llvm.tar @@ -186,7 +189,7 @@ jobs: - name: Download LLVM's CMake files if: steps.cache-llvm.outputs.cache-hit != 'true' run: | - iwr https://github.com/llvm/llvm-project/releases/download/llvmorg-16.0.3/cmake-16.0.3.src.tar.xz -OutFile cmake.tar.xz + iwr https://github.com/llvm/llvm-project/releases/download/llvmorg-${{ env.CI_LLVM_VERSION }}/cmake-${{ env.CI_LLVM_VERSION }}.src.tar.xz -OutFile cmake.tar.xz (Get-FileHash -Algorithm SHA256 .\cmake.tar.xz).hash -eq "B6D83C91F12757030D8361DEDC5DD84357B3EDB8DA406B5D0850DF8B6F7798B1" 7z x cmake.tar.xz 7z x cmake.tar @@ -194,17 +197,19 @@ jobs: - name: Build LLVM if: steps.cache-llvm.outputs.cache-hit != 'true' - working-directory: ./llvm-src run: | - cmake . -Thost=x64 -DLLVM_TARGETS_TO_BUILD="X86;AArch64" -DLLVM_USE_CRT_RELEASE=MT -DBUILD_SHARED_LIBS=OFF -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_ZSTD=OFF + mkdir llvm-build + cd llvm-build + cmake ..\llvm-src -Thost=x64 -DLLVM_TARGETS_TO_BUILD="X86;AArch64" -DLLVM_USE_CRT_RELEASE=MT -DBUILD_SHARED_LIBS=OFF -DCMAKE_FIND_USE_SYSTEM_ENVIRONMENT_PATH=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF -DLLVM_INCLUDE_TESTS=OFF -DLLVM_ENABLE_ZSTD=OFF cmake --build . --config Release - cmake -DCMAKE_INSTALL_PREFIX=$(pwd)\llvm -P cmake_install.cmake + cmake "-DCMAKE_INSTALL_PREFIX=$(pwd)\..\llvm" -P cmake_install.cmake x86_64-windows: needs: [x86_64-windows-libs, x86_64-windows-dlls, x86_64-windows-llvm] uses: ./.github/workflows/win_build_portable.yml with: release: false + llvm_version: ${{ env.CI_LLVM_VERSION }} x86_64-windows-test: runs-on: windows-2022 @@ -230,7 +235,7 @@ jobs: uses: actions/cache/restore@v3 with: path: llvm - key: llvm-libs-16.0.3-msvc + key: llvm-libs-${{ env.CI_LLVM_VERSION }}-msvc fail-on-cache-miss: true - name: Set up environment @@ -257,6 +262,7 @@ jobs: uses: ./.github/workflows/win_build_portable.yml with: release: true + llvm_version: ${{ env.CI_LLVM_VERSION }} x86_64-windows-installer: if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/')) @@ -280,7 +286,7 @@ jobs: uses: actions/cache/restore@v3 with: path: llvm - key: llvm-libs-16.0.3-msvc + key: llvm-libs-${{ env.CI_LLVM_VERSION }}-msvc fail-on-cache-miss: true - name: Set up environment diff --git a/.github/workflows/win_build_portable.yml b/.github/workflows/win_build_portable.yml index 7b784df7c871..fd3bb1da3120 100644 --- a/.github/workflows/win_build_portable.yml +++ b/.github/workflows/win_build_portable.yml @@ -6,6 +6,9 @@ on: release: required: true type: boolean + llvm_version: + required: true + type: string jobs: build: @@ -88,7 +91,7 @@ jobs: uses: actions/cache/restore@v3 with: path: llvm - key: llvm-libs-16.0.3-msvc + key: llvm-libs-${{ inputs.llvm_version }}-msvc fail-on-cache-miss: true - name: Set up environment diff --git a/src/llvm/ext/llvm-versions.txt b/src/llvm/ext/llvm-versions.txt index eee7a8d7f17d..7c8773c02212 100644 --- a/src/llvm/ext/llvm-versions.txt +++ b/src/llvm/ext/llvm-versions.txt @@ -1 +1 @@ -16.0 15.0 14.0 13.0 12.0 11.1 11.0 10.0 9.0 8.0 +17.0 16.0 15.0 14.0 13.0 12.0 11.1 11.0 10.0 9.0 8.0 From 5ed476a41adf0719fc540e6c072a521b4c8ed3ec Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Fri, 6 Oct 2023 05:09:33 +0800 Subject: [PATCH 26/37] Change `IO::Buffered#peek`'s return type to `Bytes` (#13863) --- src/io/buffered.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/buffered.cr b/src/io/buffered.cr index df66fca6dc19..11d9d15827e8 100644 --- a/src/io/buffered.cr +++ b/src/io/buffered.cr @@ -98,7 +98,7 @@ module IO::Buffered # peek data if the current buffer is empty: # otherwise no read is performed and whatever # is in the buffer is returned. - def peek : Bytes? + def peek : Bytes check_open if @in_buffer_rem.empty? From 9c011d77d660a5a7441370dce9e101b3ca094d8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 9 Oct 2023 18:02:40 +0200 Subject: [PATCH 27/37] Add changelog for 1.10.0 (#13864) --- CHANGELOG.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/VERSION | 2 +- 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de572ab822a5..04f4eef15188 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,140 @@ +# 1.10.0 (2023-10-09) + +### Features + +#### lang + +- Add unlimited block unpacking ([#11597](https://github.com/crystal-lang/crystal/pull/11597), thanks @asterite) + +#### stdlib + +- Add more `Colorize::Mode` flags ([#13745](https://github.com/crystal-lang/crystal/pull/13745), thanks @HertzDevil) +- *(collection)* Add `Hash#put_if_absent` ([#13590](https://github.com/crystal-lang/crystal/pull/13590), thanks @HertzDevil) +- *(collection)* Add `Set#rehash` ([#13630](https://github.com/crystal-lang/crystal/pull/13630), thanks @HertzDevil) +- *(collection)* Add yield `key` in `Hash#transform_values` and `value` in `#transform_keys` ([#13608](https://github.com/crystal-lang/crystal/pull/13608), thanks @baseballlover723) +- *(crypto)* Upgrade SSL defaults to Mozilla guidelines version 5.7 ([#13685](https://github.com/crystal-lang/crystal/pull/13685), thanks @straight-shoota) +- *(crypto)* **[security]** Allow OpenSSL clients to choose cipher ([#13695](https://github.com/crystal-lang/crystal/pull/13695), thanks @carlhoerberg) +- *(files)* Add `File#rename` ([#13640](https://github.com/crystal-lang/crystal/pull/13640), thanks @carlhoerberg) +- *(llvm)* Support LLVM 17 ([#13782](https://github.com/crystal-lang/crystal/pull/13782), thanks @HertzDevil) +- *(networking)* Add overloads for `URI::Params.encode` with `IO` parameter ([#13798](https://github.com/crystal-lang/crystal/pull/13798), thanks @jwoertink) +- *(numeric)* Add `Complex#to_i128`, `Complex#to_u128` ([#13838](https://github.com/crystal-lang/crystal/pull/13838), thanks @HertzDevil) +- *(runtime)* Add additional fields to `GC:ProfStats` ([#13734](https://github.com/crystal-lang/crystal/pull/13734), thanks @carlhoerberg) +- *(serialization)* Support YAML deserialization of 128-bit integers ([#13834](https://github.com/crystal-lang/crystal/pull/13834), thanks @HertzDevil) +- *(serialization)* Support 128-bit integers in `JSON::PullParser#read?` ([#13837](https://github.com/crystal-lang/crystal/pull/13837), thanks @HertzDevil) +- *(specs)* **[breaking]** Change spec runner to exit with failure for `focus: true` ([#13653](https://github.com/crystal-lang/crystal/pull/13653), thanks @straight-shoota) +- *(text)* Add `String#byte_index(Char)` ([#13819](https://github.com/crystal-lang/crystal/pull/13819), thanks @funny-falcon) +- *(time)* Support Android's system timezone database ([#13666](https://github.com/crystal-lang/crystal/pull/13666), thanks @HertzDevil) + +#### compiler + +- Experimental: Add `Slice.literal` for numeric slice constants ([#13716](https://github.com/crystal-lang/crystal/pull/13716), thanks @HertzDevil) + +#### tools + +- Add `tool unreachable` ([#13783](https://github.com/crystal-lang/crystal/pull/13783), thanks @straight-shoota) +- *(dependencies)* Add `crystal tool dependencies` ([#13631](https://github.com/crystal-lang/crystal/pull/13631), thanks @straight-shoota) +- *(docs-generator)* Add CSS for tables ([#13822](https://github.com/crystal-lang/crystal/pull/13822), thanks @nobodywasishere) +- *(hierarchy)* Support generic types in `crystal tool hierarchy` ([#13715](https://github.com/crystal-lang/crystal/pull/13715), thanks @HertzDevil) +- *(playground)* Update octicons to v19.5.0 ([#13738](https://github.com/crystal-lang/crystal/pull/13738), thanks @GeopJr) + +### Bugfixes + +#### lang + +- *(macros)* Fix missing normalization of macro expressions (and others) ([#13709](https://github.com/crystal-lang/crystal/pull/13709), thanks @asterite) +- *(macros)* Fix block parameter unpacking inside macros ([#13813](https://github.com/crystal-lang/crystal/pull/13813), thanks @HertzDevil) + +#### stdlib + +- *(collection)* **[breaking]** Mark the return type of methods such as `Slice#copy_to` as `Nil` ([#13774](https://github.com/crystal-lang/crystal/pull/13774), thanks @erdian718) +- *(files)* Change `IO::Buffered#peek`'s return type to `Bytes` ([#13863](https://github.com/crystal-lang/crystal/pull/13863), thanks @HertzDevil) +- *(llvm)* Chop git suffix from `LibLLVM::VERSION` ([#13699](https://github.com/crystal-lang/crystal/pull/13699), thanks @HOMODELUNA) +- *(macros)* Do not add trailing `+` in `TypeNode#id` for virtual types ([#13708](https://github.com/crystal-lang/crystal/pull/13708), thanks @HertzDevil) +- *(numeric)* Fix `BigDecimal#round` for large digit counts in base 10 ([#13811](https://github.com/crystal-lang/crystal/pull/13811), thanks @HertzDevil) +- *(serialization)* Set encoding in `XML.parse_html` explicitly to UTF-8 ([#13705](https://github.com/crystal-lang/crystal/pull/13705), thanks @straight-shoota) +- *(serialization)* Fix error message when parsing unknown JSON enum value ([#13728](https://github.com/crystal-lang/crystal/pull/13728), thanks @willhbr) +- *(serialization)* Fix YAML scalar type validation error message ([#13771](https://github.com/crystal-lang/crystal/pull/13771), thanks @MistressRemilia) +- *(serialization)* Fix incorrect overflow in `UInt64.from_yaml` ([#13829](https://github.com/crystal-lang/crystal/pull/13829), thanks @HertzDevil) +- *(system)* Fix `Process.new` with nilable chdir parameter on Windows ([#13768](https://github.com/crystal-lang/crystal/pull/13768), thanks @straight-shoota) +- *(system)* Fix typo in unistd.cr ([#13850](https://github.com/crystal-lang/crystal/pull/13850), thanks @kojix2) +- *(text)* Fix `Char::Reader#each` bounds check after block ([#13817](https://github.com/crystal-lang/crystal/pull/13817), thanks @straight-shoota) +- *(text)* Minor fixup for `HTML.decode_codepoint` ([#13843](https://github.com/crystal-lang/crystal/pull/13843), thanks @straight-shoota) + +#### compiler + +- **[breaking]** Remove double `.cr.cr` extension in `require` path lookup ([#13749](https://github.com/crystal-lang/crystal/pull/13749), thanks @straight-shoota) +- *(parser)* Fix end location for `FunDef` ([#13789](https://github.com/crystal-lang/crystal/pull/13789), thanks @straight-shoota) +- *(semantic)* Fix lookup scope for `@[Primitive]` def's return type ([#13658](https://github.com/crystal-lang/crystal/pull/13658), thanks @HertzDevil) +- *(semantic)* Fix typo in call_error.cr ([#13764](https://github.com/crystal-lang/crystal/pull/13764), thanks @kojix2) + +#### tools + +- *(docs-generator)* Fix octicon-link icon color on dark mode ([#13670](https://github.com/crystal-lang/crystal/pull/13670), thanks @GeopJr) +- *(docs-generator)* Allow word breaks between module names in docs ([#13827](https://github.com/crystal-lang/crystal/pull/13827), thanks @nobodywasishere) +- *(docs-generator)* Fix docs dark mode dropdown background on blink ([#13840](https://github.com/crystal-lang/crystal/pull/13840), thanks @GeopJr) +- *(init)* Fix shard crystal version in `crystal init` ([#13730](https://github.com/crystal-lang/crystal/pull/13730), thanks @xendk) +- *(hierarchy)*: Fix byte sizes for `Proc`s inside extern structs ([#13711](https://github.com/crystal-lang/crystal/pull/13711), thanks @HertzDevil) + +### Performance + +#### stdlib + +- Optimize `IO::Delimited` ([#11242](https://github.com/crystal-lang/crystal/pull/11242), thanks @asterite) +- *(crypto)* Use `IO::DEFAULT_BUFFER_SIZE` in `Digest#update` ([#13635](https://github.com/crystal-lang/crystal/pull/13635), thanks @carlhoerberg) +- *(crypto)* Fix memory leak in `OpenSSL::SSL::Socket#peer_certificate` ([#13785](https://github.com/crystal-lang/crystal/pull/13785), thanks @compumike) +- *(files)* Optimize `IO#read_string(0)` ([#13732](https://github.com/crystal-lang/crystal/pull/13732), thanks @jgaskins) +- *(files)* Avoid double file buffering ([#13780](https://github.com/crystal-lang/crystal/pull/13780), thanks @carlhoerberg) +- *(llvm)* Refactor `LLVM.default_target_triple` to avoid regex ([#13659](https://github.com/crystal-lang/crystal/pull/13659), thanks @straight-shoota) +- *(numeric)* Pre-allocate Dragonbox cache array ([#13649](https://github.com/crystal-lang/crystal/pull/13649), thanks @HertzDevil) +- *(runtime)* Avoid realloc callstack array when unwinding ([#13781](https://github.com/crystal-lang/crystal/pull/13781), thanks @carlhoerberg) +- *(time)* Optimize the constructors of `Time::Span` ([#13807](https://github.com/crystal-lang/crystal/pull/13807), thanks @erdian718) + +### Refactor + +#### stdlib + +- Do not use nilable `Pointer`s ([#13710](https://github.com/crystal-lang/crystal/pull/13710), thanks @HertzDevil) +- *(collection)* Use `Set(T)` instead of `Hash(T, Bool)` ([#13611](https://github.com/crystal-lang/crystal/pull/13611), thanks @HertzDevil) +- *(concurrency)* Use `Fiber.inactive` inside `Fiber#run`'s `ensure` block ([#13701](https://github.com/crystal-lang/crystal/pull/13701), thanks @HertzDevil) +- *(crypto)* Use `JSON::Serializable` in `scripts/generate_ssl_server_defaults.cr` ([#13667](https://github.com/crystal-lang/crystal/pull/13667), thanks @HertzDevil) +- *(crypto)* Refactor narrow OpenSSL requires for digest implementations ([#13818](https://github.com/crystal-lang/crystal/pull/13818), thanks @straight-shoota) +- *(networking)* **[deprecation]** Add types to `HTTP::StaticFileHandler` ([#13778](https://github.com/crystal-lang/crystal/pull/13778), thanks @jkthorne) + +#### compiler + +- Restrict some boolean properties to `Bool` in the compiler ([#13614](https://github.com/crystal-lang/crystal/pull/13614), thanks @HertzDevil) + +### Documentation + +#### stdlib + +- *(crypto)* Fix docs for `Digest::SHA512` ([#13796](https://github.com/crystal-lang/crystal/pull/13796), thanks @jgaskins) +- *(files)* Document `Dir#mkdir`, `Dir#exists?` ([#13795](https://github.com/crystal-lang/crystal/pull/13795), thanks @jkthorne) +- *(networking)* Add documentation for `HTTP::Headers#add` ([#13762](https://github.com/crystal-lang/crystal/pull/13762), thanks @jkthorne) +- *(text)* Fix typo in regex.cr ([#13751](https://github.com/crystal-lang/crystal/pull/13751), thanks @beta-ziliani) + +### Specs + +#### stdlib + +- *(numeric)* Update specs for `Int::Primitive.from_json` ([#13835](https://github.com/crystal-lang/crystal/pull/13835), thanks @HertzDevil) +- *(numeric)* Remove overflowing `Float#to_u!` interpreter primitive specs ([#13737](https://github.com/crystal-lang/crystal/pull/13737), thanks @HertzDevil) +- *(time)* Clear `Time::Location` cache before `.load_android` specs ([#13718](https://github.com/crystal-lang/crystal/pull/13718), thanks @HertzDevil) + +### Infrastructure + +- Update previous Crystal release - 1.9.2 ([#13650](https://github.com/crystal-lang/crystal/pull/13650), thanks @straight-shoota) +- Update distribution-scripts ([#13776](https://github.com/crystal-lang/crystal/pull/13776), thanks @straight-shoota) +- make: Add `generate_data` target for running generator scripts ([#13700](https://github.com/crystal-lang/crystal/pull/13700), thanks @straight-shoota) +- Add shell completions for `clear_cache` ([#13636](https://github.com/crystal-lang/crystal/pull/13636), thanks @straight-shoota) +- New changelog format ([#13662](https://github.com/crystal-lang/crystal/pull/13662), thanks @straight-shoota) +- Detect developer mode in Windows installer ([#13681](https://github.com/crystal-lang/crystal/pull/13681), thanks @HertzDevil) +- Update PGP key link ([#13754](https://github.com/crystal-lang/crystal/pull/13754), thanks @syeopite) +- Fix log format in update-distribution-scripts.sh ([#13777](https://github.com/crystal-lang/crystal/pull/13777), thanks @straight-shoota) +- *(ci)* Trigger windows release jobs on tag ([#13683](https://github.com/crystal-lang/crystal/pull/13683), thanks @straight-shoota) +- *(ci)* Update GH Actions ([#13748](https://github.com/crystal-lang/crystal/pull/13748), thanks @renovate) +- *(ci)* Refactor `crystal_bootstrap_version` ([#13845](https://github.com/crystal-lang/crystal/pull/13845), thanks @straight-shoota) + # 1.9.2 (2023-07-19) ### Bugfixes diff --git a/src/VERSION b/src/VERSION index a01185b4d67a..81c871de46b3 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.10.0-dev +1.10.0 From 8cd1481925aba5bf5d9d7c2b08b81b1f79b8c534 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 10 Oct 2023 20:49:36 +0200 Subject: [PATCH 28/37] Fix `win.yml` (#13876) --- .github/workflows/win.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/win.yml b/.github/workflows/win.yml index 45a87b90f04e..a98e0dcb7c7e 100644 --- a/.github/workflows/win.yml +++ b/.github/workflows/win.yml @@ -209,7 +209,7 @@ jobs: uses: ./.github/workflows/win_build_portable.yml with: release: false - llvm_version: ${{ env.CI_LLVM_VERSION }} + llvm_version: "17.0.2" x86_64-windows-test: runs-on: windows-2022 @@ -262,7 +262,7 @@ jobs: uses: ./.github/workflows/win_build_portable.yml with: release: true - llvm_version: ${{ env.CI_LLVM_VERSION }} + llvm_version: "17.0.2" x86_64-windows-installer: if: github.repository_owner == 'crystal-lang' && (startsWith(github.ref, 'refs/tags/') || startsWith(github.ref, 'refs/heads/ci/')) From 4867d81e6a993c2395d32e6038a23c489e1736c2 Mon Sep 17 00:00:00 2001 From: Mike Robbins Date: Thu, 12 Oct 2023 08:57:24 -0400 Subject: [PATCH 29/37] `IO#gets` should have same result regardless of #peek availability (#13882) Co-authored-by: Sijawusz Pur Rahnama --- spec/std/io/io_spec.cr | 34 ++++++++++++++++++++++++++++++++++ src/io.cr | 10 +++++++--- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/spec/std/io/io_spec.cr b/spec/std/io/io_spec.cr index ac51d4bddaea..e2c065ecebf0 100644 --- a/spec/std/io/io_spec.cr +++ b/spec/std/io/io_spec.cr @@ -183,6 +183,40 @@ describe IO do io.gets(chomp: false).should be_nil end + it "does gets with empty string (no peek)" do + io = SimpleIOMemory.new("") + io.gets(chomp: true).should be_nil + end + + it "does gets with empty string (with peek)" do + io = IO::Memory.new("") + io.gets(chomp: true).should be_nil + end + + it "does gets with \\n (no peek)" do + io = SimpleIOMemory.new("\n") + io.gets(chomp: true).should eq("") + io.gets(chomp: true).should be_nil + end + + it "does gets with \\n (with peek)" do + io = IO::Memory.new("\n") + io.gets(chomp: true).should eq("") + io.gets(chomp: true).should be_nil + end + + it "does gets with \\r\\n (no peek)" do + io = SimpleIOMemory.new("\r\n") + io.gets(chomp: true).should eq("") + io.gets(chomp: true).should be_nil + end + + it "does gets with \\r\\n (with peek)" do + io = IO::Memory.new("\r\n") + io.gets(chomp: true).should eq("") + io.gets(chomp: true).should be_nil + end + it "does gets with big line" do big_line = "a" * 20_000 io = SimpleIOMemory.new("#{big_line}\nworld\n") diff --git a/src/io.cr b/src/io.cr index 0294ec0272b7..28a3652c9e4c 100644 --- a/src/io.cr +++ b/src/io.cr @@ -752,11 +752,12 @@ abstract class IO private def gets_slow(delimiter : Char, limit, chomp) buffer = String::Builder.new - gets_slow(delimiter, limit, chomp, buffer) - buffer.empty? ? nil : buffer.to_s + bytes_read = gets_slow(delimiter, limit, chomp, buffer) + buffer.to_s if bytes_read end - private def gets_slow(delimiter : Char, limit, chomp, buffer : String::Builder) : Nil + private def gets_slow(delimiter : Char, limit, chomp, buffer : String::Builder) : Bool + bytes_read = false chomp_rn = delimiter == '\n' && chomp while true @@ -766,6 +767,7 @@ abstract class IO end char, char_bytesize = info + bytes_read = true # Consider the case of \r\n when the delimiter is \n and chomp = true if chomp_rn && char == '\r' @@ -801,6 +803,8 @@ abstract class IO break if char_bytesize >= limit limit -= char_bytesize end + + bytes_read end # Reads until *delimiter* is found or the end of the `IO` is reached. From 1d0a7ab02320349ea9db5a178c45c9fdacf742a6 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Thu, 12 Oct 2023 20:57:44 +0800 Subject: [PATCH 30/37] Support Android API levels 24 - 27 (#13884) --- lib/reply/src/term_size.cr | 6 +- src/crystal/iconv.cr | 2 +- src/crystal/system/unix/file_descriptor.cr | 68 +++++++++++++++++++--- src/io/console.cr | 8 +-- src/lib_c.cr | 2 +- src/lib_c/aarch64-android/c/iconv.cr | 4 +- src/lib_c/aarch64-android/c/sys/ioctl.cr | 8 +++ src/lib_c/aarch64-android/c/termios.cr | 2 - 8 files changed, 80 insertions(+), 20 deletions(-) create mode 100644 src/lib_c/aarch64-android/c/sys/ioctl.cr diff --git a/lib/reply/src/term_size.cr b/lib/reply/src/term_size.cr index d0a4c2e79699..fd0c60421c4f 100644 --- a/lib/reply/src/term_size.cr +++ b/lib/reply/src/term_size.cr @@ -145,6 +145,10 @@ end {% end %} {% end %} - fun ioctl(fd : Int, request : ULong, ...) : Int + {% if flag?(:android) %} + fun ioctl(__fd : Int, __request : Int, ...) : Int + {% else %} + fun ioctl(fd : Int, request : ULong, ...) : Int + {% end %} end {% end %} diff --git a/src/crystal/iconv.cr b/src/crystal/iconv.cr index 64d4e17f8112..593c492d4ce3 100644 --- a/src/crystal/iconv.cr +++ b/src/crystal/iconv.cr @@ -1,4 +1,4 @@ -{% if flag?(:use_libiconv) || flag?(:win32) %} +{% if flag?(:use_libiconv) || flag?(:win32) || (flag?(:android) && LibC::ANDROID_API < 28) %} require "./lib_iconv" private USE_LIBICONV = true {% else %} diff --git a/src/crystal/system/unix/file_descriptor.cr b/src/crystal/system/unix/file_descriptor.cr index 8a5c01cff44e..d77708f314bb 100644 --- a/src/crystal/system/unix/file_descriptor.cr +++ b/src/crystal/system/unix/file_descriptor.cr @@ -1,6 +1,9 @@ require "c/fcntl" require "io/evented" require "termios" +{% if flag?(:android) && LibC::ANDROID_API < 28 %} + require "c/sys/ioctl" +{% end %} # :nodoc: module Crystal::System::FileDescriptor @@ -198,7 +201,7 @@ module Crystal::System::FileDescriptor system_console_mode do |mode| flags = LibC::ECHO | LibC::ECHOE | LibC::ECHOK | LibC::ECHONL mode.c_lflag = enable ? (mode.c_lflag | flags) : (mode.c_lflag & ~flags) - if LibC.tcsetattr(fd, LibC::TCSANOW, pointerof(mode)) != 0 + if FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(mode)) != 0 raise IO::Error.from_errno("tcsetattr") end yield @@ -208,13 +211,13 @@ module Crystal::System::FileDescriptor private def system_raw(enable : Bool, & : ->) system_console_mode do |mode| if enable - LibC.cfmakeraw(pointerof(mode)) + FileDescriptor.cfmakeraw(pointerof(mode)) else mode.c_iflag |= LibC::BRKINT | LibC::ISTRIP | LibC::ICRNL | LibC::IXON mode.c_oflag |= LibC::OPOST mode.c_lflag |= LibC::ECHO | LibC::ECHOE | LibC::ECHOK | LibC::ECHONL | LibC::ICANON | LibC::ISIG | LibC::IEXTEN end - if LibC.tcsetattr(fd, LibC::TCSANOW, pointerof(mode)) != 0 + if FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(mode)) != 0 raise IO::Error.from_errno("tcsetattr") end yield @@ -223,13 +226,60 @@ module Crystal::System::FileDescriptor @[AlwaysInline] private def system_console_mode(&) - if LibC.tcgetattr(fd, out mode) != 0 - raise IO::Error.from_errno("tcgetattr") + before = FileDescriptor.tcgetattr(fd) + begin + yield before + ensure + FileDescriptor.tcsetattr(fd, LibC::TCSANOW, pointerof(before)) end + end + + @[AlwaysInline] + def self.tcgetattr(fd) + termios = uninitialized LibC::Termios + ret = {% if flag?(:android) && !LibC.has_method?(:tcgetattr) %} + LibC.ioctl(fd, LibC::TCGETS, pointerof(termios)) + {% else %} + LibC.tcgetattr(fd, pointerof(termios)) + {% end %} + raise IO::Error.from_errno("tcgetattr") if ret != 0 + termios + end + + @[AlwaysInline] + def self.tcsetattr(fd, optional_actions, termios_p) + {% if flag?(:android) && !LibC.has_method?(:tcsetattr) %} + optional_actions = optional_actions.value if optional_actions.is_a?(Termios::LineControl) + cmd = case optional_actions + when LibC::TCSANOW + LibC::TCSETS + when LibC::TCSADRAIN + LibC::TCSETSW + when LibC::TCSAFLUSH + LibC::TCSETSF + else + Errno.value = Errno::EINVAL + return LibC::Int.new(-1) + end + + LibC.ioctl(fd, cmd, termios_p) + {% else %} + LibC.tcsetattr(fd, optional_actions, termios_p) + {% end %} + end - before = mode - ret = yield mode - LibC.tcsetattr(fd, LibC::TCSANOW, pointerof(before)) - ret + @[AlwaysInline] + def self.cfmakeraw(termios_p) + {% if flag?(:android) && !LibC.has_method?(:cfmakeraw) %} + s.value.c_iflag &= ~(LibC::IGNBRK | LibC::BRKINT | LibC::PARMRK | LibC::ISTRIP | LibC::INLCR | LibC::IGNCR | LibC::ICRNL | LibC::IXON) + s.value.c_oflag &= ~LibC::OPOST + s.value.c_lflag &= ~(LibC::ECHO | LibC::ECHONL | LibC::ICANON | LibC::ISIG | LibC::IEXTEN) + s.value.c_cflag &= ~(LibC::CSIZE | LibC::PARENB) + s.value.c_cflag |= LibC::CS8 + s.value.c_cc[LibC::VMIN] = 1 + s.value.c_cc[LibC::VTIME] = 0 + {% else %} + LibC.cfmakeraw(termios_p) + {% end %} end end diff --git a/src/io/console.cr b/src/io/console.cr index d5c756edc7e8..5ac51b497c29 100644 --- a/src/io/console.cr +++ b/src/io/console.cr @@ -92,7 +92,7 @@ class IO::FileDescriptor < IO @[Deprecated] macro noecho_from_tc_mode! mode.c_lflag &= ~(Termios::LocalMode.flags(ECHO, ECHOE, ECHOK, ECHONL).value) - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) end @[Deprecated] @@ -109,12 +109,12 @@ class IO::FileDescriptor < IO Termios::LocalMode::ICANON | Termios::LocalMode::ISIG | Termios::LocalMode::IEXTEN).value - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) end @[Deprecated] macro raw_from_tc_mode! - LibC.cfmakeraw(pointerof(mode)) - LibC.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) + Crystal::System::FileDescriptor.cfmakeraw(pointerof(mode)) + Crystal::System::FileDescriptor.tcsetattr(fd, Termios::LineControl::TCSANOW, pointerof(mode)) end end diff --git a/src/lib_c.cr b/src/lib_c.cr index b707d18a3a9d..b859a4c85061 100644 --- a/src/lib_c.cr +++ b/src/lib_c.cr @@ -27,7 +27,7 @@ lib LibC {% if flag?(:android) %} {% default_api_version = 31 %} - {% min_supported_version = 28 %} + {% min_supported_version = 24 %} {% api_version_var = env("ANDROID_PLATFORM") || env("ANDROID_NATIVE_API_LEVEL") %} {% api_version = api_version_var ? api_version_var.gsub(/^android-/, "").to_i : default_api_version %} {% raise "TODO: Support Android API level below #{min_supported_version}" unless api_version >= min_supported_version %} diff --git a/src/lib_c/aarch64-android/c/iconv.cr b/src/lib_c/aarch64-android/c/iconv.cr index 6a9a20a6eb7a..ea48b1122c32 100644 --- a/src/lib_c/aarch64-android/c/iconv.cr +++ b/src/lib_c/aarch64-android/c/iconv.cr @@ -1,9 +1,9 @@ require "./stddef" lib LibC - type IconvT = Void* - {% if ANDROID_API >= 28 %} + type IconvT = Void* + fun iconv(__converter : IconvT, __src_buf : Char**, __src_bytes_left : SizeT*, __dst_buf : Char**, __dst_bytes_left : SizeT*) : SizeT fun iconv_close(__converter : IconvT) : Int fun iconv_open(__src_encoding : Char*, __dst_encoding : Char*) : IconvT diff --git a/src/lib_c/aarch64-android/c/sys/ioctl.cr b/src/lib_c/aarch64-android/c/sys/ioctl.cr new file mode 100644 index 000000000000..4667c87864a4 --- /dev/null +++ b/src/lib_c/aarch64-android/c/sys/ioctl.cr @@ -0,0 +1,8 @@ +lib LibC + TCGETS = 0x5401 + TCSETS = 0x5402 + TCSETSW = 0x5403 + TCSETSF = 0x5404 + + fun ioctl(__fd : Int, __request : Int, ...) : Int +end diff --git a/src/lib_c/aarch64-android/c/termios.cr b/src/lib_c/aarch64-android/c/termios.cr index 01f5a2831a0b..cf96bf3eb3c7 100644 --- a/src/lib_c/aarch64-android/c/termios.cr +++ b/src/lib_c/aarch64-android/c/termios.cr @@ -168,8 +168,6 @@ lib LibC c_cc : StaticArray(CcT, 19) # cc_t[NCCS] end - # TODO: defined inline for `21 <= ANDROID_API < 28` in terms of `ioctl`, but - # `lib/reply/src/term_size.cr` contains an incompatible definition of it {% if ANDROID_API >= 28 %} fun tcgetattr(__fd : Int, __t : Termios*) : Int fun tcsetattr(__fd : Int, __optional_actions : Int, __t : Termios*) : Int From c6f3552f5be159eb06c8f348c6b9e23ff7f17dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Fri, 13 Oct 2023 09:24:41 +0200 Subject: [PATCH 31/37] Add changelog for 1.10.1 (#13886) Co-authored-by: Sijawusz Pur Rahnama --- CHANGELOG.md | 13 +++++++++++++ src/VERSION | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04f4eef15188..02e481aa1d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# 1.10.1 (2023-10-13) + +### Bugfixes + +#### stdlib + +- `IO#gets` should have same result regardless of `#peek` availability ([#13882](https://github.com/crystal-lang/crystal/pull/13882), thanks @compumike) +- Support Android API levels 24 - 27 ([#13884](https://github.com/crystal-lang/crystal/pull/13884), thanks @HertzDevil) + +### Infrastructure + +- *(ci)* Fix `win.yml` ([#13876](https://github.com/crystal-lang/crystal/pull/13876), thanks @straight-shoota) + # 1.10.0 (2023-10-09) ### Features diff --git a/src/VERSION b/src/VERSION index 81c871de46b3..4dae2985b58c 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.10.0 +1.10.1 From edb0b5be7714a769b77b2ec22e5a366f642eb321 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 16 Oct 2023 19:15:41 +0200 Subject: [PATCH 32/37] Update previous Crystal release - 1.10.0 (#13878) --- .circleci/config.yml | 2 +- .github/workflows/interpreter.yml | 6 +++--- .github/workflows/linux.yml | 2 +- .github/workflows/llvm.yml | 2 +- .github/workflows/openssl.yml | 6 +++--- .github/workflows/regex-engine.yml | 4 ++-- .github/workflows/wasm32.yml | 2 +- .github/workflows/win_build_portable.yml | 2 +- bin/ci | 6 +++--- shell.nix | 12 ++++++------ src/VERSION | 2 +- 11 files changed, 23 insertions(+), 23 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 33fe27c0540c..840e1a974f21 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ parameters: previous_crystal_base_url: description: "Prefix for URLs to Crystal bootstrap compiler" type: string - default: "https://github.com/crystal-lang/crystal/releases/download/1.9.2/crystal-1.9.2-1" + default: "https://github.com/crystal-lang/crystal/releases/download/1.10.1/crystal-1.10.1-1" defaults: environment: &env diff --git a/.github/workflows/interpreter.yml b/.github/workflows/interpreter.yml index 949c43a307a8..a034a9f5b410 100644 --- a/.github/workflows/interpreter.yml +++ b/.github/workflows/interpreter.yml @@ -13,7 +13,7 @@ jobs: test-interpreter_spec: runs-on: ubuntu-22.04 container: - image: crystallang/crystal:1.9.2-build + image: crystallang/crystal:1.10.1-build name: "Test Interpreter" steps: - uses: actions/checkout@v4 @@ -24,7 +24,7 @@ jobs: build-interpreter: runs-on: ubuntu-22.04 container: - image: crystallang/crystal:1.9.2-build + image: crystallang/crystal:1.10.1-build name: Build interpreter steps: - uses: actions/checkout@v4 @@ -43,7 +43,7 @@ jobs: needs: build-interpreter runs-on: ubuntu-22.04 container: - image: crystallang/crystal:1.9.2-build + image: crystallang/crystal:1.10.1-build strategy: matrix: part: [0, 1, 2, 3] diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 8be77f17ff05..f34bf180bd98 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - crystal_bootstrap_version: [1.7.3, 1.8.2, 1.9.2] + crystal_bootstrap_version: [1.7.3, 1.8.2, 1.9.2, 1.10.1] flags: [""] include: # libffi is only available starting from the 1.2.2 build images diff --git a/.github/workflows/llvm.yml b/.github/workflows/llvm.yml index ee07fdb39b04..883302cf64a6 100644 --- a/.github/workflows/llvm.yml +++ b/.github/workflows/llvm.yml @@ -56,7 +56,7 @@ jobs: - name: Install Crystal uses: crystal-lang/install-crystal@v1 with: - crystal: "1.9.2" + crystal: "1.10.1" - name: Build libllvm_ext run: make -B deps diff --git a/.github/workflows/openssl.yml b/.github/workflows/openssl.yml index 7c4c60ced711..07e2d7e94558 100644 --- a/.github/workflows/openssl.yml +++ b/.github/workflows/openssl.yml @@ -10,7 +10,7 @@ jobs: openssl3: runs-on: ubuntu-latest name: "OpenSSL 3.0" - container: crystallang/crystal:1.9.2-alpine + container: crystallang/crystal:1.10.1-alpine steps: - name: Download Crystal source uses: actions/checkout@v4 @@ -27,7 +27,7 @@ jobs: openssl111: runs-on: ubuntu-latest name: "OpenSSL 1.1.1" - container: crystallang/crystal:1.9.2-alpine + container: crystallang/crystal:1.10.1-alpine steps: - name: Download Crystal source uses: actions/checkout@v4 @@ -42,7 +42,7 @@ jobs: libressl34: runs-on: ubuntu-latest name: "LibreSSL 3.4" - container: crystallang/crystal:1.9.2-alpine + container: crystallang/crystal:1.10.1-alpine steps: - name: Download Crystal source uses: actions/checkout@v4 diff --git a/.github/workflows/regex-engine.yml b/.github/workflows/regex-engine.yml index d5120a378640..a7ad3afbbb65 100644 --- a/.github/workflows/regex-engine.yml +++ b/.github/workflows/regex-engine.yml @@ -10,7 +10,7 @@ jobs: pcre: runs-on: ubuntu-latest name: "PCRE" - container: crystallang/crystal:1.9.2-alpine + container: crystallang/crystal:1.10.1-alpine steps: - name: Download Crystal source uses: actions/checkout@v4 @@ -25,7 +25,7 @@ jobs: pcre2: runs-on: ubuntu-latest name: "PCRE2" - container: crystallang/crystal:1.9.2-alpine + container: crystallang/crystal:1.10.1-alpine steps: - name: Download Crystal source uses: actions/checkout@v4 diff --git a/.github/workflows/wasm32.yml b/.github/workflows/wasm32.yml index a11e353f6509..3da1ef4ca89a 100644 --- a/.github/workflows/wasm32.yml +++ b/.github/workflows/wasm32.yml @@ -12,7 +12,7 @@ env: jobs: wasm32-test: runs-on: ubuntu-latest - container: crystallang/crystal:1.9.2-build + container: crystallang/crystal:1.10.1-build steps: - name: Download Crystal source uses: actions/checkout@v4 diff --git a/.github/workflows/win_build_portable.yml b/.github/workflows/win_build_portable.yml index fd3bb1da3120..f5b0ad542335 100644 --- a/.github/workflows/win_build_portable.yml +++ b/.github/workflows/win_build_portable.yml @@ -24,7 +24,7 @@ jobs: - name: Install Crystal uses: crystal-lang/install-crystal@v1 with: - crystal: "1.9.2" + crystal: "1.10.1" - name: Download Crystal source uses: actions/checkout@v4 diff --git a/bin/ci b/bin/ci index 008965ccded0..8e9f7a5ddadf 100755 --- a/bin/ci +++ b/bin/ci @@ -135,8 +135,8 @@ format() { prepare_build() { on_linux verify_linux_environment - on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/1.9.2/crystal-1.9.2-1-darwin-universal.tar.gz -o ~/crystal.tar.gz - on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-1.9.2-1 crystal;popd' + on_osx curl -L https://github.com/crystal-lang/crystal/releases/download/1.10.1/crystal-1.10.1-1-darwin-universal.tar.gz -o ~/crystal.tar.gz + on_osx 'pushd ~;gunzip -c ~/crystal.tar.gz | tar xopf -;mv crystal-1.10.1-1 crystal;popd' # These commands may take a few minutes to run due to the large size of the repositories. # This restriction has been made on GitHub's request because updating shallow @@ -189,7 +189,7 @@ with_build_env() { on_linux verify_linux_environment - export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:1.9.2}" + export DOCKER_TEST_PREFIX="${DOCKER_TEST_PREFIX:=crystallang/crystal:1.10.1}" case $ARCH in x86_64) diff --git a/shell.nix b/shell.nix index 453894568061..ab90080aca13 100644 --- a/shell.nix +++ b/shell.nix @@ -52,18 +52,18 @@ let # Hashes obtained using `nix-prefetch-url --unpack ` latestCrystalBinary = genericBinary ({ x86_64-darwin = { - url = "https://github.com/crystal-lang/crystal/releases/download/1.9.2/crystal-1.9.2-1-darwin-universal.tar.gz"; - sha256 = "sha256:0ngiflk7yxb6ry5ax1zrbm3rh4psq7flv7xj6ph4g8qqx74qv79m"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.10.1/crystal-1.10.1-1-darwin-universal.tar.gz"; + sha256 = "sha256:08k8sixhnk9ld99nyrya11rkpp34zamsg3lk9h50ppbmzfixjyyc"; }; aarch64-darwin = { - url = "https://github.com/crystal-lang/crystal/releases/download/1.9.2/crystal-1.9.2-1-darwin-universal.tar.gz"; - sha256 = "sha256:0ngiflk7yxb6ry5ax1zrbm3rh4psq7flv7xj6ph4g8qqx74qv79m"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.10.1/crystal-1.10.1-1-darwin-universal.tar.gz"; + sha256 = "sha256:08k8sixhnk9ld99nyrya11rkpp34zamsg3lk9h50ppbmzfixjyyc"; }; x86_64-linux = { - url = "https://github.com/crystal-lang/crystal/releases/download/1.9.2/crystal-1.9.2-1-linux-x86_64.tar.gz"; - sha256 = "sha256:1d4wmr49m3ykylh4zwp184mm98vj0cqmflhgnmgry8nkwhkvs900"; + url = "https://github.com/crystal-lang/crystal/releases/download/1.10.1/crystal-1.10.1-1-linux-x86_64.tar.gz"; + sha256 = "sha256:02hzslzgv0xxsal3fkbcdrnrrnzf9lraamy36p36sjf8n14v45a2"; }; }.${pkgs.stdenv.system}); diff --git a/src/VERSION b/src/VERSION index 4dae2985b58c..1f724bf455d7 100644 --- a/src/VERSION +++ b/src/VERSION @@ -1 +1 @@ -1.10.1 +1.11.0-dev From 8ceea56467ceba46f4b7be0d94f354274f532b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 16 Oct 2023 19:17:51 +0200 Subject: [PATCH 33/37] Use `Char#to_i?` in lexer (#13841) --- src/compiler/crystal/syntax/lexer.cr | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/src/compiler/crystal/syntax/lexer.cr b/src/compiler/crystal/syntax/lexer.cr index 33f991c5fc0f..bb4fbe50f4a9 100644 --- a/src/compiler/crystal/syntax/lexer.cr +++ b/src/compiler/crystal/syntax/lexer.cr @@ -2194,8 +2194,8 @@ module Crystal def consume_non_braced_unicode_escape codepoint = 0 4.times do - hex_value = char_to_hex(next_char) { expected_hexadecimal_character_in_unicode_escape } - codepoint = 16 * codepoint + hex_value + hex_value = next_char.to_i?(16) || expected_hexadecimal_character_in_unicode_escape + codepoint = 16 &* codepoint &+ hex_value end if 0xD800 <= codepoint <= 0xDFFF raise "invalid unicode codepoint (surrogate half)" @@ -2224,8 +2224,8 @@ module Crystal expected_hexadecimal_character_in_unicode_escape end else - hex_value = char_to_hex(char) { expected_hexadecimal_character_in_unicode_escape } - codepoint = 16 * codepoint + hex_value + hex_value = char.to_i?(16) || expected_hexadecimal_character_in_unicode_escape + codepoint = 16 &* codepoint &+ hex_value found_digit = true end end @@ -2339,18 +2339,6 @@ module Crystal @token end - def char_to_hex(char, &) - if '0' <= char <= '9' - char - '0' - elsif 'a' <= char <= 'f' - 10 + (char - 'a') - elsif 'A' <= char <= 'F' - 10 + (char - 'A') - else - yield - end - end - def consume_loc_pragma case current_char when '"' From 51fb08fdcaedd27159c5210e8a317ea47a354707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Mon, 16 Oct 2023 19:18:08 +0200 Subject: [PATCH 34/37] Remove unnecessary file check for CLI arguments (#13853) --- src/compiler/crystal/command.cr | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/compiler/crystal/command.cr b/src/compiler/crystal/command.cr index 1540aafa9a46..a6b5c18455ea 100644 --- a/src/compiler/crystal/command.cr +++ b/src/compiler/crystal/command.cr @@ -614,9 +614,6 @@ class Crystal::Command private def gather_sources(filenames) filenames.map do |filename| - unless File.file?(filename) - error "file '#{filename}' does not exist" - end filename = File.expand_path(filename) Compiler::Source.new(filename, File.read(filename)) end From fc5a56a1d0a0bf76d0fedd8f3f69c4c2305dd079 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 17 Oct 2023 11:11:59 +0200 Subject: [PATCH 35/37] Fix tool init error message when target exists but not a dir (#13869) --- src/compiler/crystal/tools/init.cr | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/crystal/tools/init.cr b/src/compiler/crystal/tools/init.cr index 456386f5b14e..96b004eec2fd 100644 --- a/src/compiler/crystal/tools/init.cr +++ b/src/compiler/crystal/tools/init.cr @@ -238,8 +238,8 @@ module Crystal end def run - if File.file?(config.expanded_dir) - raise Error.new "#{config.dir.inspect} is a file" + if (info = File.info?(config.expanded_dir)) && !info.directory? + raise Error.new "#{config.dir.inspect} is a #{info.type.to_s.downcase}" end views = self.views From 44583d7d164baba3a31b65bc8f7c5a906dcce226 Mon Sep 17 00:00:00 2001 From: Quinton Miller Date: Tue, 17 Oct 2023 17:12:13 +0800 Subject: [PATCH 36/37] Implement `BigRational`'s rounding modes (#13871) --- spec/std/big/big_rational_spec.cr | 94 +++++++++++++++++++++++++++++++ src/big/big_rational.cr | 21 +++++-- 2 files changed, 111 insertions(+), 4 deletions(-) diff --git a/spec/std/big/big_rational_spec.cr b/spec/std/big/big_rational_spec.cr index ce9387d8489a..2c9b138ba357 100644 --- a/spec/std/big/big_rational_spec.cr +++ b/spec/std/big/big_rational_spec.cr @@ -299,6 +299,100 @@ describe BigRational do br(-291, 100).trunc.should eq(-2) end + describe "#round" do + describe "rounding modes" do + it "to_zero" do + br(-9, 6).round(:to_zero).should eq BigRational.new(-1) + br(-6, 6).round(:to_zero).should eq BigRational.new(-1) + br(-5, 6).round(:to_zero).should eq BigRational.new(0) + br(-3, 6).round(:to_zero).should eq BigRational.new(0) + br(-1, 6).round(:to_zero).should eq BigRational.new(0) + br(0, 6).round(:to_zero).should eq BigRational.new(0) + br(1, 6).round(:to_zero).should eq BigRational.new(0) + br(3, 6).round(:to_zero).should eq BigRational.new(0) + br(5, 6).round(:to_zero).should eq BigRational.new(0) + br(6, 6).round(:to_zero).should eq BigRational.new(1) + br(9, 6).round(:to_zero).should eq BigRational.new(1) + end + + it "to_positive" do + br(-9, 6).round(:to_positive).should eq BigRational.new(-1) + br(-6, 6).round(:to_positive).should eq BigRational.new(-1) + br(-5, 6).round(:to_positive).should eq BigRational.new(0) + br(-3, 6).round(:to_positive).should eq BigRational.new(0) + br(-1, 6).round(:to_positive).should eq BigRational.new(0) + br(0, 6).round(:to_positive).should eq BigRational.new(0) + br(1, 6).round(:to_positive).should eq BigRational.new(1) + br(3, 6).round(:to_positive).should eq BigRational.new(1) + br(5, 6).round(:to_positive).should eq BigRational.new(1) + br(6, 6).round(:to_positive).should eq BigRational.new(1) + br(9, 6).round(:to_positive).should eq BigRational.new(2) + end + + it "to_negative" do + br(-9, 6).round(:to_negative).should eq BigRational.new(-2) + br(-6, 6).round(:to_negative).should eq BigRational.new(-1) + br(-5, 6).round(:to_negative).should eq BigRational.new(-1) + br(-3, 6).round(:to_negative).should eq BigRational.new(-1) + br(-1, 6).round(:to_negative).should eq BigRational.new(-1) + br(0, 6).round(:to_negative).should eq BigRational.new(0) + br(1, 6).round(:to_negative).should eq BigRational.new(0) + br(3, 6).round(:to_negative).should eq BigRational.new(0) + br(5, 6).round(:to_negative).should eq BigRational.new(0) + br(6, 6).round(:to_negative).should eq BigRational.new(1) + br(9, 6).round(:to_negative).should eq BigRational.new(1) + end + + it "ties_even" do + br(-15, 6).round(:ties_even).should eq BigRational.new(-2) + br(-9, 6).round(:ties_even).should eq BigRational.new(-2) + br(-6, 6).round(:ties_even).should eq BigRational.new(-1) + br(-5, 6).round(:ties_even).should eq BigRational.new(-1) + br(-3, 6).round(:ties_even).should eq BigRational.new(0) + br(-1, 6).round(:ties_even).should eq BigRational.new(0) + br(0, 6).round(:ties_even).should eq BigRational.new(0) + br(1, 6).round(:ties_even).should eq BigRational.new(0) + br(3, 6).round(:ties_even).should eq BigRational.new(0) + br(5, 6).round(:ties_even).should eq BigRational.new(1) + br(6, 6).round(:ties_even).should eq BigRational.new(1) + br(9, 6).round(:ties_even).should eq BigRational.new(2) + br(15, 6).round(:ties_even).should eq BigRational.new(2) + end + + it "ties_away" do + br(-15, 6).round(:ties_away).should eq BigRational.new(-3) + br(-9, 6).round(:ties_away).should eq BigRational.new(-2) + br(-6, 6).round(:ties_away).should eq BigRational.new(-1) + br(-5, 6).round(:ties_away).should eq BigRational.new(-1) + br(-3, 6).round(:ties_away).should eq BigRational.new(-1) + br(-1, 6).round(:ties_away).should eq BigRational.new(0) + br(0, 6).round(:ties_away).should eq BigRational.new(0) + br(1, 6).round(:ties_away).should eq BigRational.new(0) + br(3, 6).round(:ties_away).should eq BigRational.new(1) + br(5, 6).round(:ties_away).should eq BigRational.new(1) + br(6, 6).round(:ties_away).should eq BigRational.new(1) + br(9, 6).round(:ties_away).should eq BigRational.new(2) + br(15, 6).round(:ties_away).should eq BigRational.new(3) + end + + it "default (=ties_even)" do + br(-15, 6).round.should eq BigRational.new(-2) + br(-9, 6).round.should eq BigRational.new(-2) + br(-6, 6).round.should eq BigRational.new(-1) + br(-5, 6).round.should eq BigRational.new(-1) + br(-3, 6).round.should eq BigRational.new(0) + br(-1, 6).round.should eq BigRational.new(0) + br(0, 6).round.should eq BigRational.new(0) + br(1, 6).round.should eq BigRational.new(0) + br(3, 6).round.should eq BigRational.new(0) + br(5, 6).round.should eq BigRational.new(1) + br(6, 6).round.should eq BigRational.new(1) + br(9, 6).round.should eq BigRational.new(2) + br(15, 6).round.should eq BigRational.new(2) + end + end + end + it "#hash" do b = br(10, 3) hash = b.hash diff --git a/src/big/big_rational.cr b/src/big/big_rational.cr index d8975e637341..6b6a6c0bb0b2 100644 --- a/src/big/big_rational.cr +++ b/src/big/big_rational.cr @@ -154,16 +154,29 @@ struct BigRational < Number Number.expand_div [BigInt, BigFloat, BigDecimal], BigRational def ceil : BigRational - diff = (denominator - numerator % denominator) % denominator - BigRational.new(numerator + diff, denominator) + BigRational.new(-(-numerator // denominator)) end def floor : BigRational - BigRational.new(numerator - numerator % denominator, denominator) + BigRational.new(numerator // denominator) end def trunc : BigRational - self < 0 ? ceil : floor + BigRational.new(numerator.tdiv(denominator)) + end + + def round_away : BigRational + rem2 = numerator.remainder(denominator).abs * 2 + x = BigRational.new(numerator.tdiv(denominator)) + x += sign if rem2 >= denominator + x + end + + def round_even : BigRational + rem2 = numerator.remainder(denominator).abs * 2 + x = BigRational.new(numerator.tdiv(denominator)) + x += sign if rem2 > denominator || (rem2 == denominator && x.numerator.odd?) + x end # Divides the rational by (2 ** *other*) From 67885d6ac3c0dde2acdadaed927221dafc08e817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= Date: Tue, 17 Oct 2023 11:12:31 +0200 Subject: [PATCH 37/37] Add `Enumerable#present?` (#13866) Co-authored-by: Quinton Miller --- spec/std/enumerable_spec.cr | 7 ++++++- src/enumerable.cr | 29 ++++++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/spec/std/enumerable_spec.cr b/spec/std/enumerable_spec.cr index 3b3cef2bd0c1..624666d35992 100644 --- a/spec/std/enumerable_spec.cr +++ b/spec/std/enumerable_spec.cr @@ -436,11 +436,16 @@ describe "Enumerable" do end end - describe "empty?" do + describe "#empty?" do it { SpecEnumerable.new.empty?.should be_false } it { SpecEmptyEnumerable.new.empty?.should be_true } end + describe "#present?" do + it { SpecEnumerable.new.present?.should be_true } + it { SpecEmptyEnumerable.new.present?.should be_false } + end + describe "find" do it "finds" do [1, 2, 3].find { |x| x > 2 }.should eq(3) diff --git a/src/enumerable.cr b/src/enumerable.cr index 7d95cb6f81b2..d3f9a1dbb02a 100644 --- a/src/enumerable.cr +++ b/src/enumerable.cr @@ -107,7 +107,16 @@ module Enumerable(T) # ``` # [nil, true, 99].any? # => true # [nil, false].any? # => false + # ([] of Int32).any? # => false # ``` + # + # * `#present?` does not consider truthiness of elements. + # * `#any?(&)` and `#any(pattern)` allow custom conditions. + # + # NOTE: `#any?` usually has the same semantics as `#present?`. They only + # differ if the element type can be falsey (i.e. `T <= Nil || T <= Pointer || T <= Bool`). + # It's typically advised to prefer `#present?` unless these specific truthiness + # semantics are required. def any? : Bool any? &.itself end @@ -1613,17 +1622,35 @@ module Enumerable(T) count { true } end - # Returns `true` if `self` is empty, `false` otherwise. + # Returns `true` if `self` does not contain any element. # # ``` # ([] of Int32).empty? # => true # ([1]).empty? # => false + # [nil, false].empty? # => false # ``` + # + # * `#present?` returns the inverse. def empty? : Bool each { return false } true end + # Returns `true` if `self` contains at least one element. + # + # ``` + # ([] of Int32).present? # => false + # ([1]).present? # => true + # [nil, false].present? # => true + # ``` + # + # * `#empty?` returns the inverse. + # * `#any?` considers only truthy elements. + # * `#any?(&)` and `#any(pattern)` allow custom conditions. + def present? : Bool + !empty? + end + # Returns an `Array` with the first *count* elements removed # from the original collection. #