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