During the last few days I wrote a few tutorials on installing Cedalion and getting started with it. All tutorials are accompanied by videos. In this post I will guide the reader (you…) as you will implement your first DSL in Cedalion. To keep things simple, I’ll base this DSL on the one we implemented in Prolog in my previous post.
A Short Description of the DSL
Our DSL is a simple expression language, in which values are restricted to numbers. Because all values are numbers, this language does not have lambda abstractions, and thus it cannot be considered a functional programming language.
In our language, users are able to define expressions (which evaluate to numbers). These expressions may use constants, arithmetic operations, conditionals or user-defined expressions. User-defined expressions may depend on variables, in which case the expression can be considered a function. However, these functions are not first-class. For example, defining the expression f(X) does not define f as a function.
In Part I we will define our first DSL concept, const(C), which represents a constant. We will go through this simple definition in great detail.
To start, create a new project (in the Project Explorer choose New/Project from the context menu, and then select General/Project) and name it simpleExpr. Then connect it to Git as explained in this video. Next, create a source file by selecting New/File from the project’s context menu and call it expr.ced. In this file we will define the core of our expression language. Your screen will look like this:
The new file is displayed containing an empty list (“”). This is an empty list of statements. To implement our DSL we will add statements to this list. To insert our very first one, we will right-click the “” and select “Insert” from the context menu. Alternatively, we can left-click it (to select it), and press Alt+Shift+Insert, as you can see in the context menu.
Once you did that, the “” is replaced by a bullet followed by an underscore (“_”):
This underscore is a placeholder for the statement. Hovering the mouse over it will show a tooltip reading “statement”. Click the underscore to enter the statement. Later, as you get more into practice with Cedalion’s navigation, you will probably prefer using Alt+Shift+Home to go from a list into its first element.
BDD is a great thing. For those who don’t know what it is, it is a form of TDD, in which tests are associated with behaviors, short descriptions of what is expected of a certain piece of software. This helps us provide “live” documentation to the code. The behaviors (requirements, if you will) document both the test and the code it tests.
Cedalion has support for BDD, through the behavior concept. To add a behavior, click the underscore and type “beh” and then hit Ctrl+Space. From the menu that opens select the only option that opens, and press Enter.
Note: There is a chance that due to a bug in the Cedalion editor the menu will not open for the first time. In such a case, use keyboard navigation to navigate out and back into the underscore (e.g., hold Alt+Shift and press Page-Up and then Home) and try again.
Now, select the leftmost underscore inside the behavior, and write:
and press Enter. Select the second underscore (you can navigate using Alt+Shift+Left) and enter:
Then, on the rightmost underscore write:
"should evaluate to C
This means that the concept “const(C)” of type “expr” should evaluate to C. The error markers around the “const” and the “expr” mean they have not been declared. Let’s declare them. Double-click the error sign at the left of the “const”. This will show a suggestion in the view below the editor:
Double-click the suggestion. It will insert a declaration before the behavior in our program.
This declaration says const(C) is an expression, but it does not specify a concrete type for C. To fix that, click the underscore representing the type of C and press “num” and Ctrl+Space. Select “number” from the menu (the only option).
Next, in the declaration of “const(C)”, double-click the error-sign to the left of “expr”. This will display a suggestion to insert a declaration for “expr”. Accept it by double-clicking it. You should see something like this:
The errors are still there because the changes we made did not yet take effect. Press Ctrl+S to save this file. Saving a Cedalion file gives effect to its content. The errors should go away:
Now, let us write the unit test. Click the small “+” sign to the left of the cyan rectangle. It will open a box with a place to write the test.
Hovering over the underscore in the white box will tell you what needs to go inside: “pred” — a predicate (actually, a goal).
Select this underscore, type the following and press Enter:
This test states that const(C) must evaluate to something. However, there are two errors. You can learn about their nature by hovering over the error signs. One error comes from the fact we did not declare the eval predicate, and the other is an exception that was thrown because we did not define it.
Let’s fix them one by one. First, let’s declare eval. You should already know how to do this (hint: it involves two double-clicks). This should give you something like this:
OK, this left us with more errors than we had before… The declaration we created is not good. The left-hand side of a declaration should have nothing but variables, and ours has “const(C)”. Fix this by clicking the “const” and entering:
Move to the underscore to its right and enter:
This just added more errors. Now the arguments on the left-hand side do not agree with the ones on the right-hand side.
Navigate to the “const(C)” to the right of the “where” and replace it with “Expr”, and the underscore to its right with “Value”.
Finally, go to the last underscore on that line and type “num” followed by Ctrl+Space. From the menu choose “number” by pressing Enter. After pressing Ctrl+S (to save and allow the changes take effect) things should look like this (press the “+” again to re-open the behavior block to see the error disappear):
From Red to Green and Back Again
We’re left with the second problem: a failing test. From a TDD perspective, we are at the red phase. We have a failing test and we need to write our code so it will pass.
Let’s do this! Right-click the bullet next to the behavior and select “Append“. Select the newly-created underscore and type:
and hit Ctrl+Space. This will open a menu with a few options. Select our eval predicate (the one from the /simpleExpr namespace) by using the arrow keys and Enter to make the selection. Press Ctrl+S to save. The error should go away.
What we did was state that anything evaluates to anything. In particular, const(C) evaluates to something. This is not very helpful. Let’s make this test more effective.
Select the underscore that is the second argument of eval inside the white text box and enter:
Then, select the whole eval(…) (by pressing Alt+Shift+Page-Up) and press “,” and Ctrl+Space. Select the only option that will appear in the menu. This added a comma (“,”) to the right of the eval(…) goal, and added an underscore below it, allowing us to write another goal. The test will pass only if both goals succeed.
Navigate to this underscore (Alt+Ctrl+End) and press “should” and Ctrl+Space. From the menu select the first entry (“should equal”).
This is an assertion, similar to assertions in unit-testing frameworks that exist in other languages. In the first underscore enter:
and in the second one enter:
Now there are two errors. The first is our test failing (which is good, red phase, remember?), but the second one comes from the type system. Without going too deep into Cedalion’s type system, the error says Cedalion can infer types we did not write explicitly. This refers to the third underscore on that line. Cedalion know the type there (the type of both X and C) should be “number”, but we can have Cedalion do this work for us. Double-click this error marker and then double-click the suggestion.
We’re back with one error: our failing test.
So now we need to really implement const(C), or actually, to implement eval(…) to accept const(C). To do this, select the first underscore in the last statement and type “con” and hit Ctrl+Space. The only option will be “const”. Select it. As its parameter enter:
As the second argument of the same eval() enter:
as well. Save and watch the error go away.
One of the biggest advantages of Cedalion is that it allows us to make things look the way we want them to look. For example, eval(..) should not look the way it does. It makes much more sense for eval to look either like a mathematical equation or as an English phrase. Let’s choose the latter. Instead of writing “eval(X, Y)” let’s write “X evaluates to Y”. To tell Cedalion to do that, we need to add a projection definition for eval.
Click the bullet next to the declaration of eval (the third statement). Then press F9 (you can learn such shortcuts from the context menu). It will insert a projection definition.
A projection definition has the form “display X as Y”, where X is a concept::type, and Y is a visualization. Visualization is a DSL by itself, allowing users to define how concepts are displayed. Cedalion provides a default, in which all arguments are displayed horizontally by their order. We can change this visualization as we see fit.
Important: The Cedalion editor is not very stable when it comes to incomplete projection definitions. If you are in the habit of saving your work after every other keystroke (like myself…) than you’re in for some trouble when it comes to editing projection definitions. When editing projection definitions make sure to save the file only when the definition is complete: no underscores are lying around and all fields are being displayed.
Click the little “h” at the left side of the visualization. This is a horizontal layout. It contains a list of elements laid out horizontally. Click Alt+Shift+Home to select the list. Cedalion lists can be seen as linked lists. A list consists of its first element and the rest of the list. Press Alt+Shift+End to move to the tail of the list (a list starting at the second element), and there press Alt+Shift+Insert to insert a new element in that spot. Press Alt+Shift+Home to select the element itself. Here, enter:
and press Ctrl+S to save.
Every place where eval(…) was mentioned is replaced by its new form.
A more pressing problem is that const(C) is too verbose for what it does. const(C) is merely an adapter, allowing us to place a constant in a places where an expression is expected, kind of like a socket adapter allows us to connect an American plug where a European plug is expected, or vice versa. As such, it must be as unobtrusive as possible, for both readers and writers.
For readers, we shall make its visualization subtle, merely a lightly coloured rectangle wrapping the constant. To do that, select the bullet next to the declaration of const(C) (second statement) and press F9. Go to its visualization (now, the third statement) and select the placeholder for C (the double angle brackets). Type “line” and press Ctrl+Space. Choose “lineBorder” from the menu.
The three zeros on the left represent values for red, gree and blue for the color of the border. Fill the three of them with 192, to get a light-grey color. The underscore to their right is intended for the width of the border. Enter 2, and save.
Reader unobtrusiveness accomplished! Now let’s make it just as unobtrusive to writers. We do this by declaring it as an adapter. Right-click the bullet next to the projection definition of const(C) (third statement) and select “Append” to create a new empty statement. Select the underscore and type: “adap” and press Ctrl+Space. From the menu select “adapter” (the only option).
We want to use const(C) as an adapter for C. Click the “::” on the left-hand side of the projection definition and press Ctrl+C to copy const(C)::expr. Now click the “::” on the left-hand side of the adapter statement and press Ctrl+V to paste it into the text area. Now press Enter to enter the code. Repeat this with the “::” on the right-hand side of the projection definition and the right-hand side of the adapter statement. The result should be:
That’s it. Mission accomplished. We defined our first DSL concept.