authors/bcardiff.jpg Brian J. Cardiff 17 Apr 2019

Crystal 0.28.0 released!

Crystal 0.28.0 has been released!

This is a big release that includes some new language constructs, tidying up some existing features, many interesting additions to the std-lib and important changes for the much awaited multi-threading and windows features.

There were 221 commits since 0.27.2 by 31 contributors.

Let’s review some of the most relevant changes in this release. But don’t miss the rest of the release changelog which has a lot of valuable information.

Language changes


Enums are usually declared with one line per each member.

enum State

Sometimes you might want to declare it in one line, and since ; is basically a new line for the parser you can do:

enum State
  On; Off

In previous versions using spaces or commas were allowed. From now on ; is the required one. The formatter will migrate commas to ; in this version since that construct was used somewhat frequently. Read more at #7607 and #7618.


Sometimes you don’t know where to start or finish. The same happens to a Range. Ranges can now be begin-less and end-less ranges.

Let’s see some basic constructs for iterating ranges:

(3..).each do |x|
  puts x
  break if some_condition

(..3).reverse_each do |x|
  # ... eventually yields 0, -1, -2

Make sure that they stop at some point, please.

The good stuff comes from its integration in std-lib. Check #7179 to learn all about them. I will leave here some of my favorites:

numbers = [1, 10, 3, 4, 5, 8] # => [10, 8]
numbers[..2] # => [1, 10, 3]
numbers[...2] # => [1, 10]

[1, 2, 3].zip(6..) # => [[1, 6], [2, 7], [3, 8]]

5.clamp(10..) # => 10


The expression offsetof(Type, @ivar) was introduced to the language in order to return the offset of the @ivar within the memory representation of Type. One use case for that is working with vertex.

Another awesome aspect of this feature is that it went from initial idea to final implementation in only 1 week by the same person. Going from the need to the full implementation of all the aspects in the lexer, parser, compiler, internals, highlighter, formatter is pretty impressive. Read the full story at #7589.


Up until now Type#annotation was available to get the last annotation of a given kind. In #7326 Type#annotations is added to list all annotations.

Another addition is that ArrayLiteral#sort_by was added in #3947.

With these two features, we can do something like:

annotation ReviewedBy

class Foo

{% for reviewer in Foo.annotations(ReviewedBy).sort_by(&.[0].downcase) %}
  puts {{reviewer[0]}}
{% end %}
# output
# gru
# Kevin
# Stuart

Note that macros and annotations should not be overused. There are many ways to make a program simple and declarative.


Deprecated definitions

We are introducing some first-class citizen in the language to have a common idiom to mark methods and other constructs as deprecated. The code is able to compile as usual, but the compiler is able to warn the programmer about usages of deprecated definitions.

In this release, these warnings are off by default in order to gather feedback about what would be the best output and workflow for the regular usage.

Given the following program:

@[Deprecated("Use `#bar`")]
def foo

puts foo

You can build the program with the new options --warnings all|none --error-on-warnings.

$ ./bin/crystal --warnings all --error-on-warnings
Warning in Deprecated top-level foo. Use `#bar`

puts foo
A total of 1 warnings were found.

The warning options are also available in crystal spec and its exit status will be a combination of the spec itself and the deprecation check.

When building the docs using crystal docs the deprecated definitions will have a badge. Read more at #7653.

Library lookup

There was some development to simplify how some libraries and static libraries are looked up and therefore can be overridden in case it is needed. An env var CRYSTAL_LIBRARY_PATH is now used in the process of determining the location of libraries to link to.

When using @[Link("awesome", static: true)], the first step will be to lookup libawesome.a in the paths listed at CRYSTAL_LIBRARY_PATH. If nothing is found awesome will be looked up using pkg-config --static. If still nothing is found, libawesome.a will be looked up in /usr/lib, /usr/local/lib.

Essentially CRYSTAL_LIBRARY_PATH has the highest priority in the lookup. But it is also added to the linker for looking up shared libraries.

The compiler package will set a default value of CRYSTAL_LIBRARY_PATH to the embedded libraries where libgc.a is located. If the default libraries don’t suit you, just prepend the location where the alternative is located to CRYSTAL_LIBRARY_PATH.

Read about this change in #7562 and in the new entry on the docs

Semantic fixes

The compiler includes a bunch of fixes and improvements related to how some code constructs were handled.

A protected initialize will now define a protected new thanks to #7510.

Some corner cases related to method lookup were fixed in #7537, #7536, and #7529. There should be a couple of fewer surprises now.

The type inference was also improved a bit when dealing with procs in #7527 and #7568. If you store procs in structures it might be worth checking out, you might be able to remove a hack or two.

Some error messages were improved to clarify: the scope the user should have in mind thanks to #7384, and some common pitfalls when capturing blocks thanks to #7406.


We are really happy to include some refactors that move us forward in the way to parallelism and to include an extension to bdwgc that will enable its multi-threading support to work with the Crystal runtime.

Although you can’t use MT right now, the work done at #7546 will enable to compare alternatives in the GC and changes in the scheduler when -D preview_mt is used. While the MT is being developed piece by piece we are trying to avoid slow down in the single-threaded apps.

Two notable refactors introduced were cleanups in Fiber to extract a Fiber::StackPool in #7417, and refactor IO::Syscall as IO::Evented in #7505.

Standard library

Given an Enum with @[Flags] calling .from_value(0) or .from_value?(0) will now return None. Previously it was raising or returning nil. This is a breaking-change introduced in #6516 in order to fix the semantics of those methods.

The module PartialComparable is deprecated in #7664 since its behaviour has been fully integrated into Comparable.


Some time ago we decided to make Int#/ return Float. That will happen in the 0.29.0. In 0.28.0, you will need to start changing the code to use Int#//. Luckily there compiler can now assist you in that quest. Build your program and specs with --warnings all, the usages of / will pop-up. We’ve done it in #7639.

Another notable change is that we settle to remove the type suffixes of numbers in #7525. The output will be cleaner now. If, given an expression, you want to know the value, type, and static type you can use pp!(e, e.class, typeof(e)).


Numbers might be used to represent quantities (Seriously!). These can now be printed in a human-readable form using Number#humanize, Int#humanize_bytes and Number#format. This feature was added in #6314.


The counterpart of deprecating PartialComparable was that a Comparable#<=> is now able to return nil. This was done in order to allow better support of Array#sort over types that don’t have a total ordering. Read more at #6611.

Iterator#rewind is removed and #cycle got some performance improvements because now the elements are stored in an array and can be cycled despite the nature of the Iterator. Read more at #7440.

Indexable#zip and Indexable#zip? were moved to Enumerable and now there are far more flexible. They can work with any number of Indexable, Iterable or Iterator.

a = [1, 2, 3]
b = "a".."c"
c = 9.downto(1), c) # => [{1, "a", 9}, {2, "b", 8}, {3, "c", 7}]

Read more at #7453.


Time got some polishing again but given the impact of the changes we wanted to include, and the new deprecation support we are using that to offer a migration path.

Don’t forget to build with --warnings all your code at least once!

Although you should leave in the present, is now deprecated. But only because you need to know where you are. The code will need to decide between Time.local or Time.utc, much more explicit. This also affects constructors. is also deprecated and Time.local or Time.utc can be used to build specific values. Read more at #5346 and #7586.

Time#add_span is renamed and improved in Time#shift. It now allows changing a time instance by calendar units and handle other units thanks to #6598.

The last breaking-change in is in Time#date that will now return a Tuple of {year, month, day}. You can keep using Time#at_beginning_of_day if a Time instance is wanted. Read more at #5822.


There were a couple of efforts to improve HTTP and URI. Although they are breaking-changes there should be easy to migrate.

There are a couple of fixes in URI implementation. URI#opaque is dropped. URI#path no longer returns Nil. The #parse/#to_s normalization and default port handling have changed. Read the full story at #6323.

HTTP::Status is introduced to avoid, if wanted, the need to status code numbers. The only breaking change regarding statuses is that HTTP.default_status_message_for(Int) was replaced by Read more at #7247.

HTTP::Multipart was moved to MIME::Multipart in #7085.

In OAuth2 errors we no longer expect a proper JSON to exist. Discover why in #7467.

RequestProcessor connection reuse logic was fixed to deal properly with unconsumed payloads in #7055.


An OpenSSL::Algorithm instead of symbol is now used in digest/hexdigest to choose the algorithm to use. As a bonus track, LibCrypt’s PKCS5_PBKDF2_HMAC. Read more at #7264.

Next steps

Please update your Crystal and report any issues. We will keep moving forward and start the development focusing on 0.29.

Once again, check your code with --warnings all. This will help in the migration of your codebase and will probably push some feedback about that tool.

Don’t miss the rest of the release changelog which has a lot of valuable information.

The development is possible thanks to the community’s effort, 84codes’ support, and every supporter.