Wipple Home Blog

Wipple’s new compiler and language changes

January 10, 2024

I’m excited to announce that I’ve been working on a new compiler for Wipple! The new compiler is being written from the ground up for performance and stability. In addition, I’ve made some changes to Wipple’s syntax and features to make the language easier to learn and use that will debut alongside the new compiler.

Syntax updates

The most visible change in Wipple is the new syntax for comments and blocks. Comments are now written using brackets, and blocks are written with parentheses:

[Represents a sport.]
Sport : type (
name :: Text
emoji :: Text
players :: Number
)

instance (Show Sport) : ...

basketball : Sport (
name : "Basketball"
emoji : `🏀`
players : 5
)

show basketball [Basketball 🏀 has 5 players]

In writing, parentheses are used much more often than braces, and students I’ve worked with who are still learning to type have trouble finding the braces on the keyboard. It’s also hard for beginners to remember when to use braces versus parentheses, so having a single grouping symbol reduces the learning curve.

Comments’ bracket syntax takes the place of attributes, which have been removed from the language. Wipple’s new compiler parses comments rather than ignoring them, and can use them to generate documentation just like the previous [help] attributes.

New compilation model

Although the final program is the same, Wipple’s new compiler works much differently internally. Rather than parsing and compiling every file every time, the new compiler generates intermediate artifacts that can be reused. When compiling a library, you specify the list of files to be treated as a single unit:

$ wipple compile base/*.wipple \
--interface base.wippleinterface \
--library base.wipplelibrary

(base is the new name for the standard library, previously called std).

The .wippleinterface file contains the definitions of types, traits, constants, and instances contained within the provided files. The .wipplelibrary file contains the compiled bytecode for these constants and instances. The artifacts correspond to C’s .h and .o files, respectively.

Now, we can use base.wippleinterface to refer to standard library definitions from other files:

$ wipple compile turtle.wipple base.wippleinterface \
--interface turtle.wippleinterface \
--library turtle.wipplelibrary

Now only turtle.wipple will be parsed and compiled — the results of base are reused!

Finally, we can compile our main program against turtle and base to get an executable:

$ wipple compile main.wipple base.wippleinterface turtle.wippleinterface \
--library main.wipplelibrary
$ wipple link *.wipplelibrary -o main
$ ./main

main contains all the bytecode for our program — behind the scenes, it contains #!/usr/bin/env wipple run to actually run the code. You can also call wipple run main directly.

This new compilation model is more complicated, but the Wipple Playground will do it all automatically for you. And it means that instead of shipping the standard library’s source code with the playground, I can just provide the .wippleinterface and .wipplelibrary files to be linked against! The result should be a dramatic speedup.

In order for all of this to work, the new compiler no longer monomorphizes constant and instance definitions, instead opting for dynamic dispatch for traits at runtime. Essentially, all of the instances of a trait are stored in a list that’s iterated over whenever a trait expression is encountered. This decision is a tradeoff between compilation speed and runtime speed — the program will run more slowly if a lot of traits are used, but the compiler doesn’t have to recursively resolve generic definitions anymore. In the Wipple Playground, code is compiled on every keystroke and usually spends most of the time waiting for user interaction while it’s running, so I think this is a good tradeoff.

Structure expressions and lazy values

In the new compiler, support for custom syntax has been removed. Instead, there are two new constructs that cover almost all of the use cases for custom syntax: structure expressions and lazy values!

Structure expressions are blocks containing only variable assignments. Previously, to initialize a structure, you provided the structure’s name followed by this block. Now, the typechecker will infer the structure! This means you can write your own functions with "named parameters":

[Options for `fraction`.]
Fraction-Options : type (
round :: Number
)

[Display a number as a fraction.]
fraction :: Fraction-Options -> Number -> Text
fraction : ...

pi : 3.14 . fraction (round : 1)

show pi [31 / 10]

Structures also now support default values:

Box : type (
width :: Pixels
width : 100 pixels

height :: Pixels
height : 100 pixels

color :: Color
color : `red`
)

[Draw a box on the screen.]
box :: Box -> ()
box : ...

box (color : `blue`)

If you still want to specify the name of the structure, you can — the new compiler generates a constructor function that just returns the provided structure expression:

Box :: Box -> Box

The second new feature is lazy values: values that aren’t computed right away. Non-lazy values are implicitly converted into lazy values, and you can use evaluate to compute the value. For example, repeat is now a regular function that accepts a lazy body:

[Determines whether to evaluate a `repeat` body again.]
Repeat-Predicate : Predicate Body Result => trait (Predicate -> Body -> Result)

[Repeat using the `Control-Flow` produced by the `repeat` body.]
with-control-flow :: With-Control-Flow _
with-control-flow : With-Control-Flow

With-Control-Flow : Result => type

Result => instance (Repeat-Predicate With-Control-Flow (Control-Flow Result) Result) :
_ body -> body

[Repeat so long as the provided condition is `True`.]
while :: lazy Boolean -> While
while : condition -> (condition : condition)

While : type (condition :: lazy Boolean)

instance (Repeat-Predicate While () ()) : (condition : condition) body ->
if (evaluate condition) Continue (Break ())

[Repeat forever.]
forever :: Forever
forever : Forever

Forever : type

instance (Repeat-Predicate Forever _ _) : _ _ -> Continue

[Execute a block of code repeatedly.]
repeat :: Predicate Body Result where (Repeat-Predicate Predicate Body Result) =>
Predicate -> lazy Body -> Result
repeat : predicate body -> (
predicate : Repeat-Predicate predicate

[The predicate and body are grouped together in a tuple to allow for
tail-call optimization.]
repeat' :: Body Result => ((Body -> Result) ; lazy Body) -> Result
repeat' : (predicate ; body) -> when (predicate (evaluate body)) (
Continue -> repeat' (predicate ; body)
Break result -> result
)

repeat' (predicate ; body)
)

The resulting API is identical to the one that uses custom syntax!

[Displays "1 2 3 4 5 6 7 8 9 10" on the screen.]
counter : mutable 1
repeat (while (get counter < 10)) (
show (get counter)
increment! counter
)

Conclusion

In the playground and throughout the courses, Wipple will look and work mostly the same. But under the hood, almost everything has changed. The new compiler’s implementation is available on GitHub under the new-compiler branch. I look forward to releasing it once it’s ready!

Made by Wilson Gramer