Jasper: a Crystal-like programming language, but mostly a playground for theories

I would say it started with a compatibility desire, but started to address things differently and drift apart from it.

1. Types are required everywhere, except for local variables and initialize signature (assumes Void)

def foo(bar)
end

foo(10)

$ bin/jasper run 21.jasper
21.jasper:1:12: error: expected type annotation
  def foo(bar)
             ^
21.jasper:1:13: error: expected type annotation
  def foo(bar)
              ^

Even constants require types (which Crystal 1.22 recently introduced)

2. All code is type checked, independently if is used (walked) or not:

class Foo
end

class Bar < Foo
end

def foo(x : Foo) : Int32
  1
end

def foo(x : Bat) : Int32
  2
end

foo(Bar.new)

The above compiles fine in Crystal, but fails to compile in Jasper:

$ bin/jasper run 20.jasper
20.jasper:11:9: error: unknown type `Bat`
  def foo(x : Bat) : Int32
          ^^^^^^^

(even if you don’t do foo(Bar.new))

Note that while everything is type-checked, it does not mean all the code is generated. A walker checks what paths of the code can be invoked and then only generates IR for those.

3. There is no argless, magical super like Ruby/Crystal

Right now if you have a Parent/Child hierarchy and their initialize has multiple arguments, doing super with no arguments will automatically forward the ones of that child initialize.

That does not compile in Jasper: it requires explicit calling instead of assumptions:

class Parent
  def initialize(@value : Int32)
  end

  def value : Int32
    @value
  end
end

class Child < Parent
  def initialize(value : Int32, extra : String)
    super
  end
end

$ bin/jasper run 23.jasper
23.jasper:12:5: error: bare `super` is not supported; write `super()` to pass no arguments, or `super(...)` to pass them explicitly
      super
      ^^^^^
23.jasper:12:5: error: wrong number of arguments for `initialize` (given 0, expected 1)
      super
      ^^^^^

3. A method that returns nothing is not the same as one that returns nil

return with no argument is a Void, return nil is not the same. Jasper treats nil as a value. This allow me to identify at compile time things that can be chained (returning self)

4. Generics work by monomorphization and methods can be dropped from the generated code

class Foo(T) will create a version of all methods of Foo for that specific T, but sometimes, depending on the type of T, you might not want to deal with things differently.

Since there is no macros (and there will not be, at least not like Crystal) sometimes I need methods that can be used with certain types, so where T : Reference will work with Reference-type T, or : Value.

class Cell(T)
  def initialize(@value : T)
  end

  def label : String where T : Reference
    "ref"
  end

  def label : String where T : Value
    "value"
  end

  def size : Int32 where T : Reference
    @value.size
  end
end

puts(Cell(String).new("hi").label)   # ref
puts(Cell(Int32).new(3).label)       # value
puts(Cell(String).new("hi").size)    # 2

Cell#size is dropped from the list of methods when T is not a reference (Eg. Int32):

puts(Cell(Int32).new(3).size)
$ bin/jasper run 24.jasper
24.jasper:21:6: error: undefined method `size` for Cell(Int32)
  puts(Cell(Int32).new(3).size)       # value
       ^^^^^^^^^^^^^^^^^^^^^^^
  note: 24.jasper:13:26: dropped by this constraint
      def size : Int32 where T : Reference
                             ^^^^^^^^^^^^^

The idea is that where can be used with interfaces and you define methods for the different types without having to get into spaguetti macros ({% if T < Reference %}).

4. Catching for some early Crystal errors

The following segfaults in Crystal at runtime:

class Point
  @name : String

  def initialize(c : Bool)
    if c
      return
    end
    @name = "set"       # skipped when c is true
  end

  def name : String
    @name
  end
end

puts(Point.new(true).name)

$ crystal  run 25.jasper
Invalid memory access (signal 11) at address 0x4
[0x1020a76c4] *Exception::CallStack::print_backtrace:Nil +112 in /Users/luis/.cache/crystal/crystal-run-25.tmp

But is properly catch at compilation time by Jasper:

$ bin/jasper run 25.jasper
25.jasper:6:7: error: instance variable `@name` must be assigned on every path through `initialize`
        return
        ^^^^^^

This also catch another problem with calling methods in initialize:

class Base
  def initialize
    describe
  end

  def describe : Void
    puts("base")
  end
end

class Child < Base
  @name : String

  def initialize(name : String)
    super()
    @name = name
  end

  def describe : Void
    puts(@name.size)
  end
end

Child.new("jasper")

Above segfaults in Crystal but fails to compile in Jasper:

$ bin/jasper run 26.jasper
26.jasper:3:5: error: `describe` reaches `Child#describe`, which reads instance variable `@name` before it is assigned
      describe
      ^^^^^^^^

5. Explicitness everywhere: nothing is created or converted for you

Crystal will allow class Shapes::Circle when there is no Shapes defined and it will assume is a module. Jasper will not allow that:

$ bin/jasper run 27.jasper
namespace.jasper:1:1: error: `Shapes` is not defined, so `Shapes::Circle` has no parent

6. All options for Unions must be valid

If you used a Union in a type restriction, all the possible candidates of that union must be able to answer it, or you need to narrow it down:

def show(v : Int32 | String) : Void
  puts(v.size)
end

show("abc")
$ bin/jasper run union.jasper
union.jasper:2:8: error: undefined method `size` for `Int32`, a member of `Int32 | String` (narrow first)
    puts(v.size)
         ^^^^^^

6. There are no exceptions or plans to have them

Exceptions are expensive and complicated when dealing with multiple threads and fibers (none currently implemented, btw)

Instead, I’m going on the path of a more lightweight, explicit treatment of errors: if a method returns an error, it explicitly says in the return type that it does and is up to you to handle it. Planning for some helpers in case you really don’t care about the error, or, like Crystal, methods where nil is an acceptable response for you to handle (like Array(T)#[]? or Hash(K, V)#[]?)

7. ARC means more memory management

Jasper frees memory when the last reference to it goes away, no GC. Right now there is no cycles intelligence, so the following program runs, but a and b are never freed:

Jasper frees memory when the last reference to it goes away, so there is no GC and no runtime to ship. The trade-off is cycles. This program runs, but a and b are never freed (you could link a cycle collector here, if one is planned).

class Node
  property other : Node? = nil
end

a = Node.new
b = Node.new
a.other = b
b.other = a   # cycle, leaks today