06 Jan 2022

Crystal 1.3.0 is released!

We are delivering a new release with several bugfixes and improvements. Below we list the most important or interesting changes, without mentioning several bugfixes and smaller enhancements. For more details, visit the changelog. Breaking changes are marked with ⚠️.

Pre-built packages are available on GitHub Releases and our official distribution channels. See crystal-lang.org/install for installation instructions.

Stats

In this release we included 176 changes since the 1.2.2 release by 31 contributors. We thank all the effort put into improving the language! ❤️

Interpreter

The awaited Crystal interpreter has been merged. But it’s still a preview and misses substantial features for real use cases.

Our distribution packages are not being compiled with it, and we recommend the same for external packages. If you want to try it, you need to build the compiler with interpreter support explicitly (make crystal interpreter=1).

You can invoke the interpreter using two modes: crystal i or crystal i file.cr. In the first case, a REPL will start right away, and in the second case the file.cr will be interpreted. Interestingly, if the interpreter finds the debugger instruction, it will start an interactive process to debug it. For instance, if we have a file example.cr with:

a = 1
debugger
p a

Then calling crystal i example.cr stops after the call to debugger:

From: example.cr:3:3 <Program>#example.cr:

    1: a = 1
    2: debugger
 => 3: p a

 pry>

At this point we can inspect and modify variables, step into function calls, etc. If we enter a = 2 in the REPL and then next, we’ll see the program printing 2.

Read more about the interpreter in our blog post: Crystal’s interpreter – A very special holiday present

Windows Support

With over 30 individual changes there has been great progress for improving Windows support.

It’s still not yet a fully supported platform, but we’ve started building self-contained and portable Windows packages.

A self-contained and portable snapshot package for Windows x86-64 is available on the GitHub release. Please remember that this is still experimental and unsupported.

We’ve also started building packages for nightlies, and you can grab the current build at nightly.link/crystal-lang/crystal/workflows/win/master/crystal.zip. There is also a repository for the scoop package manager at neatorobito/scoop-crystal. Together with many smaller improvements in the compiler, user experience on Windows has greatly improved.

One of the most important missing features has been added: Stack traces. Now you can properly follow where your exceptions come from on Windows (#11461).

Additionally, several stdlib APIs have been ported to Windows, including Big numbers (#11412), OpenSSL (#11477), and text encoding (#11480). As a result, 96% of stdlib specs run successfully on Windows now.

Unicode

There are a couple of changes related to Unicode support.

Most significant is that Char#inspect and #dump as well as String#inspect and #dump escape all non-printable characters (#11452). Note that this may break up grapheme clusters if they depend on non-printable characters. There might be further refinements to this, but we believe it’s better to be explicit over potentially missing characters (#11630).

Further, we added an API for Unicode extended grapheme clusters at String::Grapheme (#11472). It allows splitting a string into grapheme clusters which represent a user-perceived character and may consist of multiple code points.

"a👍🏼à".graphemes # => [String::Grapheme('a'), String::Grapheme("👍🏼"), String::Grapheme("à")]
"a👍🏼à".chars     # => ['a', '👍', '🏼', 'a', '̀']

"a👍🏼à".grapheme_size # => 3
"a👍🏼à".size          # => 5

This API is experimental for now, and we expect to enhance and refine it in the following releases.

Further notable changes:

  • New: Char#unicode_escape returns the Unicode escape sequence representing the character (#11421).
  • New: Char#printable? returns true for printable characters, i.e. those with a visible glyph as well as the ASCII whitespace (U+0020) (#11429).
  • ⚠️ Fix: Char#ascii_control? no longer applies to C1 control codes. They are not part of the ASCII character set (#11510).
  • Fix: Char#letter? correctly identifies code points in the Unicode general categories Lo (Other Letter) and Lm (Modifier Letter) as letters (#11474).
  • Standardized Unicode escape formats for Char#inspect and #dump (#11421).

Syscalls

We added an experimental API to create native system calls (#10777). Only Linux is supported for now. This is a first step towards supporting Linux’s io_uring interface to improve IO performance.

To define system calls open a module and use the Syscall.def_syscall macro. As demonstrated in the following example, you need to pass in the system call name, the return type and its arguments.

require "syscall"

module MySyscalls
  Syscall.def_syscall write, Int32, fd : Int32, buf : UInt8*, count : LibC::SizeT
end

data = "Hello!\n"
MySyscalls.write(1, data.to_unsafe, LibC::SizeT.new(data.size))

Multi-Assign

Multiple assignments got improved in various ways. First, it’s possible to use a splat target in a multi-assignment (#10410):

# Splat in multi-assign with array
first, *rest, last = [1, 2, 3, 4, 5]
first # => 1
rest # => [2, 3, 4]
last # => 5
# Splat in multi-assign with tuple
*rest, last = {"This", 15, 4, "tuple", true}
rest # => {"This", 15, 4, "tuple"}
last # => true

To take the first and last element, it is possible to use the underscore splat notation:

# Ignoring the elements in the middle
first, *_, last = {"This", 15, 4, "tuple", true}
first # => "This"
last # => true

Second, there’s an optional preview feature to detect unbalanced multi-assignments (#11145). It can be enabled with the compiler flag -Dstrict_multi_assign.

a, b, c = {1, 2} # Error: index out of bounds for Tuple(Int32, Int32) (2 not in -2..1)
a, b = {1, 2, 3} # Error: cannot assign Tuple(Int32, Int32, Int32) to 2 targets

Note that the first example fails with a similar error in 1.2.2 (this error is expected to change in the near future).

To ignore the extra items in strict mode you can use the underscore splat notation. This can be used to port existing code to work with the strict_multi_assign flag.

a, b, *_ = {1, 2, 3} # Same as a, b = {1, 2, 3} without the flag

⚠️ This second improvement is a breaking change; therefore it’s not enabled by default. We encourage using it to detect possible errors in your code. This setting will likely be the default in 2.0. An additional restriction guarded by that flag is that if the right-hand side has a unique element, it must be of type Indexable (#11545).

Number Autocast

Primitive numeric values are now autocasted to fit into larger types (#11431, #11529). For instance, it is possible to call a function expecting an Int64 with an Int32 (note that before 1.3.0 only number literals were autocasted):

def foo(x : Int64)
  x
end

foo 1_i32 # Works in 1.2.2 and 1.3.0

bar = 1_i32
foo bar  # Fails in 1.2.2, works in 1.3.0

Unsigned integer types can be autocasted into larger signed ones. And autocasting also works for floating point types (Float32 to Float64).

If there is ambiguity, for instance, because there is more than one option, the compiler throws an error:

def foo(x : Int64)
  x
end

def foo(x : Int128)
  x
end

bar = 1_i32
foo bar # Error: ambiguous call, implicit cast of Int32 matches all of Int64, Int128

128-bit Literals

The parser has been improved to understand number literals in the full range or 128-bit integers (#11571). Until now, 128-bit literals had been supported only within the limits of 64-bit values.

1_i128                                       # Works in 1.2.2 and 1.3.0
170141183460469231731687303715884105727_i128 # Fails in 1.2.2, works in 1.3.0

In order to get there, we needed to implement some arithmetic primitives for all platforms, and refactor the parsing of number literals (#11211). The latter also cleaned up a couple of edge cases. Some examples are highlighted here:

1_.1   # Error: unexpected '_' in number
-0u64  # Error: Invalid negative value -0 for UInt64
-0_u64 # Error: Invalid negative value -0 for UInt64
1__2   # Error: consecutive underscores in numbers aren't allowed
0x_2   # Error: unexpected '_' in number
0_12   # Error: octal constants should be prefixed with 0o
0e40   # => 0.0
0x     # Error: numeric literal without digits

Other notable changes

  • ⚠️ Methods that enumerate on a sub-range now always use start as parameter name for the begin of the sub-range (#11350). This was standardized from several different forms, which keep working as deprecated overloads until the next major release.
  • ⚠️ More refactoring happened to Indexable::Mutable#fill’s overloads (#11368). Again, existing code continues to work with deprecated overloads.
  • ⚠️ Regex#name_table returns Hash(Int32, String) instead of Hash(Int16, String) (#11539). Noticeable effects are pretty limited due to number autocasting.

We have been able to do all of this thanks to the continued support of 84codes, Nikola Motor Company and every other sponsor. To maintain and increase the development pace, donations and sponsorships are essential. OpenCollective is available for that. Reach out to crystal@manas.tech if you’d like to become a direct sponsor or find other ways to support Crystal. We thank you in advance!