Is this tutorial ever ending? Who knows. But while I am motivated for it, I will continue. I already have some ideas for future parts, so I can’t see this finishing before the ten or twelve issues.
To start with, the news since the last part. I added documentation to the repository as well as a few samples. I will add more with the merge for this part of the tutorial.
For this part, we will keep adding new features to the grammar. While most of this part is quite similar to the previous issues of this tutorial, give a special look to the testing part at the bottom, where I introduce the use of the Fake It Easy library.
This time, support for colour and turning on or off the drawing process (we’ll call it raising or lowering the pen). And regarding that, I need to do a mea culpa and correct myself. In Part IV we introduced the commands setx
, sety
, setxy
and setpos
, as moving commands. They placed the turtle in a new position. This is a wrong interpretation. They should draw a line from the current position to the new one. We will fix this during this issue of the tutorial.
Lexer Changes
We are including five new commands and therefore, we need rules to recognize their names. We will also change the regular expressions for the variables. Tokens starting with the quote can support almost any character. But we will add only a new character, the sharp, that will be used to allow users to specify colours in the CSS notation.
Variable : '"' [#a-zA-Z0-9_]+ { Text = Text.Substring(1); };
VariableRef : ':' [#a-zA-Z0-9_]+ { Text = Text.Substring(1); };
PenDown : P D | P E N D O W N ;
PenUp : P U | P E N U P ;
SetPenColor : S E T P E N C O L O R ;
SetPalette : S E T P A L E T T E ;
SetPenSize : S E T P E N S I Z E ;
PlaintextThe lines 1 and 2 are simple rewrites of the previous one we already had. The other four are the new commands and their possible abbreviations.
Parser Changes
For the grammar we need a couple more changes, to support the syntax of RGB colours and the new colour commands. We will also merge the two rules we already had, simpleCommand
and controlStmt
, as they are basic calls to the base class. Thus, we can just use the default ANTLR implementation.
command : (simpleCommand | colourCmd | controlStmt) #basicCommand
| SetPos squarePoint #setPosition
| SetXY simplePoint #setPosition
| cmd=(Home|Bye|PenDown|PenUp) #atomicCmd
| Arc expr expr #arc
| Make Variable expr #setVariable
| Name expr Variable #setVariable
| Show expr #show
;
colourList : '[' expr expr expr ']'
;
colourCmd : SetPenColor ( expr | Variable | colourList ) #setPenColor
| SetPalette expr ( Variable | colourList) #setPalette
| SetPenSize expr #setPenSize
;
PlaintextSo, in the command
production, we put the simpleCommand
and controlStmt
clauses together and add the new one, for the colour commands: colourCmd
. We also added two new commands in line 4: PenDown
and PenUp
.
Production colourList
defines how a colour can be recognized in the RGB notation. Note that we are being more permissive than the JS implementation we are using as inspiration, as we allow any expression, and will just cast the resulting value to an integer (more on that later).
For the colourCmd
, we have one to set a colour, one to save a colour in the LOGO’s palette, and another one to set the stroke width. For setting the pen colour, there are three options: a simple integer, that selects the colour with that index from the LOGO’s palette, a variable name (in fact, a string) that has a CSS colour name or a CSS colour code (starting with a sharp sign), and finally the RGB notation referred in the previous paragraph. The SetPalette
command has a first parameter which is the index of the palette being modified, and the second argument the new colour to be set. Finally, the last command just includes de width of the stroke.
Adapting Existing Classes
We have some classes that need adapting.
Line Class
The class that stores information about a line needs a new attribute to store the line’s width. Follows the relevant changes.
private readonly int _width;
public Line(Point pt1, Point pt2, Colour colour, int width)
{
_pt1 = pt1;
_pt2 = pt2;
_colour = colour;
_width = width;
}
public override string ToString()
{
var style = $"stroke:{_colour}; stroke-width:{_width}";
return $@"<line x1=""{_pt1.X}"" y1=""{_pt1.Y}"" x2=""{_pt2.X}"" y2=""{_pt2.Y}"" style=""{style}""/>";
}
C#Line 1 introduces a new field to store the line width. In the constructor, a new parameter is now in place, and in the stringification method, the stroke width is no longer hardcoded.
Note that there are further changes introduced in these files offline, to support the infinite canvas I promised in the last issue. So, please check in GitHub the full files for reference. I hope you can understand how it works. If not, if something is not clear, let me know, and I will try to explain in a future new issue.
Arc Class
The changes for the class Arc
are similar. Add the information about the stroke width as a field, support it in the constructor, and output it in the stringification method.
private readonly int _width;
public Arc(Point center, float facing, float radius, float angle, Colour colour, int width)
{
_center = center;
_facing = facing;
_radius = radius;
_angle = angle;
_colour = colour;
_width = width;
}
public override string ToString()
{
var points = ComputePoints();
var first = points.First();
var path = $"{first.X} {first.Y}";
path = points.Skip(1).Aggregate(path, (current, pt) => current + $" L {pt.X} {pt.Y}");
return @$"<path fill=""none"" style=""stroke:{_colour}; stroke-width:{_width}"" d=""M {path}""/>";
}
C#Turtle Class
The changes in the turtle are simple as well. We add the field to store the stroke width and add it as an extra parameter to all new lines and arcs. We also add a new field to store information about the pen status, if it is up (not drawing) or down (drawing).
public int Width = 1;
public bool IsDrawing = true;
public void AddLine(Point from, Point to)
=> _canvas.Add(new Line(from, to, Colour, Width));
public void AddArc(Point turtlePosition, float turtleAngle, float radius, float angle)
=> _canvas.Add(new Arc(turtlePosition, turtleAngle, radius, angle, Colour, Width));
C#Colour Class
Let’s prepare the Colour class for the palette concept as well as the named colours. We will also make the red/green/blue fields public to allow us to write tests later.
public static readonly Colour[] Palette =
{
new(0, 0, 0), // black
new(0, 0, 255), // blue
new(0, 255, 0), // green
new(0, 255, 255), // cyan
new(255, 0, 0), // red
new(255, 0, 255), // magenta
new(255, 255, 0), // yellow
new(255, 255, 255), // white
new(165, 42, 42), // brown
new(210, 180, 140), // tan
new(0, 128, 0), // green
new(127, 255, 212), // aqua
new(250, 128, 114), // salmon
new(128, 0, 128), // purple
new(255, 165, 0), // orange
new(128, 128, 128) // gray
};
public static readonly ReadOnlyDictionary<string, Colour> ColourNames = new(
new Dictionary<string, Colour> {
{"black", new Colour(0, 0, 0)},
{"silver", new Colour(192, 192, 192)},
{"gray", new Colour(128, 128, 128)},
{"white", new Colour(255, 255, 255)},
{"maroon", new Colour(128, 0, 0)},
{"red", new Colour(255, 0, 0)},
{"purple", new Colour(128, 0, 128)},
{"fuchsia", new Colour(255, 0, 255)},
{"green", new Colour(0, 128, 0)},
{"lime", new Colour(0, 255, 0)},
{"olive", new Colour(128, 128, 0)},
{"yellow", new Colour(255, 255, 0)},
{"navy", new Colour(0, 0, 128)},
{"blue", new Colour(0, 0, 255)},
{"teal", new Colour(0, 128, 128)},
{"aqua", new Colour(0, 255, 255)}
});
public readonly int Red, Green, Blue;
public Colour(int red, int green, int blue)
{
Red = red;
Green = green;
Blue = blue;
}
public override string ToString() => $"rgb({Red},{Green},{Blue})";
C#LOGO defines a palette of 16 colours. We define them in an array of 16 positions and initialize each position with the correct colour (I tried to get the right colours, but let me know if I have any mistakes). Note that the user can change each position at will. The ColourNames
dictionary is just a mapping of the colour names defined in the CSS standard and their respective values. It is read-only, and can not be changed. Finally, we renamed the three component names to uppercase and made them public.
New AST Node
We will need to store colors in the Abstract Syntax Tree. To make it more complicated, we will need to store different kinds of colours: it can be a LOGO colour value, a CSS name, a CSS RGB colour code, or even the index for the LOGO’s palette. We can’t resolve these different types, evaluating them to their real value, as they can change during runtime (for the LOGO colour format or the LOGO’s palette). This new class will be named ColourNode.cs
and will be placed in the AST
folder.
namespace Logo2Svg.AST;
public class ColourNode : INode
{
private Parameter _redExpr, _greenExpr, _blueExpr;
private Colour _cssColour;
private Parameter _id;
public ColourNode(string possibleName)
{
if (possibleName.StartsWith("#"))
{
if (!Regex.IsMatch(possibleName, "^#[0-9A-Fa-f]{6}$"))
throw new Exception("Invalid CSS colour");
var red = int.Parse(possibleName.Substring(1, 2), NumberStyles.HexNumber);
var green = int.Parse(possibleName.Substring(3, 2), NumberStyles.HexNumber);
var blue = int.Parse(possibleName.Substring(5, 2), NumberStyles.HexNumber);
_cssColour = new Colour(red, green, blue);
}
else
{
if (!SVG.Colour.ColourNames.TryGetValue(
possibleName.ToLowerInvariant(), out _cssColour))
throw new Exception("Invalid CSS colour name");
}
}
public ColourNode(Parameter id) => _id = id;
public ColourNode(Parameter redExpr, Parameter greenExpr, Parameter blueExpr)
{
_redExpr = redExpr;
_greenExpr = greenExpr;
_blueExpr = blueExpr;
}
public Colour Colour(Turtle turtle)
{
if (_id is not null)
{
var i = (int) _id.Value(turtle);
if (i is < 0 or > 15)
throw new IndexOutOfRangeException($"Invalid palette index: {i}");
return SVG.Colour.Palette[i];
}
if (_redExpr is not null)
{
var red = _redExpr.Value(turtle);
var green = _greenExpr.Value(turtle);
var blue = _blueExpr.Value(turtle);
if (red is < 0 or > 99 || green is < 0 or > 99 || blue is < 0 or > 99)
throw new IndexOutOfRangeException("Invalid LOGO colour intensity");
const float factor = 255f / 99f;
return new Colour((int) (factor * red),
(int) (factor * green),
(int) (factor * blue));
}
return _cssColour;
}
public void Execute(Turtle turtle) => throw new NotImplementedException();
}
C#We’ll visit the code step by step. We have three groups of parameters:
- The three component expressions are used when the programmer specifies the colour using the LOGO notation.
- The field
_cssColour
saves the colour information for the cases when it is hardcoded (either a CSS colour name or a CSS colour specification). - Finally,
_id
stores the expression that will compute the index of the colour palette where the colour information should be fetched from.
As already mentioned, note that we are being more permissive than our JS reference implementation. In the JS reference implementation, you can not use an expression in the palette position, for example.
We have three constructors. The first one receives a variable name and checks if it starts with a sharp sign. If it does, then it tries to parse the colour components. If it fails, or the string has an invalid amount of characters, an exception is thrown. If the variable does not start with a sharp, it should be a CSS colour name. Therefore, we lowercase the colour name and check if it is a valid colour name. If it is, we store a reference to that colour. The other two constructors are the expected ones. They receive the expressions and store them.
The Colour
method is, mostly, a replacement for the evaluation or execution methods. It will be called in run time to compute the colour stored in the AST node:
- If we have an expression stored in the
_id
field, then it must be the ID for the desired colour in the palette. Thus, we evaluate it, convert it to an integer value and check if it is inside the valid range of values: \([0, 15]\). If the value is out of bounds, an exception is thrown. - If we have an expression stored for the red component we infer that the other components are also valid. Thus, we evaluate the three expressions, convert them to integer values, and check if they are in the valid range of \([0, 100[\). If the value is out of bounds, an exception is thrown. If it is a valid value, it is then converted to the CSS range of \([0, 255]\).
- Finally, if neither of the previous cases were true, the colour is a CSS colour. This is a simple case, as we have that information cached already.
Finally, we specify that this node will not implement the Execute
method.
The Visitors
It’s now time to implement the visitors. First, for the LOGO colour format. We visit each one of the expressions and return a new ColourNode
using the appropriate constructor.
public override INode VisitColourList(LogoParser.ColourListContext context)
{
var rgb = context.expr().Select(Visit<Parameter>).ToList();
return new ColourNode(rgb[0], rgb[1], rgb[2]);
}
C#The visitor to set the stroke width is also quite similar to previous situations. We visit the expression and call the proper command constructor.
public override INode VisitSetPenSize(LogoParser.SetPenSizeContext context)
=> new Command(LogoLexer.SetPenSize, "setPenSize",
Visit<Parameter>(context.expr()));
C#Follows the visitor for setting a palette colour. This one is a little more complex. The first argument, the position to be used of the palette, is just a visit to the expression. But for the second argument, we need to check if it is a colour specification or a CSS name/code. In the first case, is just a matter of visiting that predicate, as it already returns a ColourNode
. But for the second case, we need to visit it as a variable name, and then use that name to create a new ColourNode
.
public override INode VisitSetPalette(LogoParser.SetPaletteContext context)
{
INode colour = context.colourList() is { } colourListContext
? Visit<ColourNode>(colourListContext)
: new ColourNode(new VarName(context.Variable().GetText()));
return new Command(LogoLexer.SetPalette, "setPalette",
Visit<Parameter>(context.expr()), colour);
}
C#Finally, the command SetPenCollor
is similar to the previous one, but with an extra case: when the colour is specified as the LOGO’s palette colour index.
public override INode VisitSetPenColor(LogoParser.SetPenColorContext context)
{
INode colour = context.expr() is { } exprContext ?
new ColourNode(Visit<ExprParam>(exprContext)) :
context.colourList() is { } colourListContext ?
Visit<ColourNode>(colourListContext) :
new ColourNode(new VarName(context.Variable().GetText()));
return new Command(LogoLexer.SetPenColor, "setPenColor", colour);
}
C#Code Execution
The next step is to update the file Command.cs
. We will start by fixing my mistake regarding drawing or not, according to the position of the pen. The recipe is always the same. Compute the target point and add a line only if the turtle has the pen down. These are the switch
options we need to edit:
case LogoLexer.Forward:
{
var value = Parameter(0).Value(turtle);
var pos = turtle.Position;
var target = new Point(pos.X + MathF.Cos(turtle.Rotation) * value,
pos.Y - MathF.Sin(turtle.Rotation) * value);
if (turtle.IsDrawing) turtle.AddLine(pos, target);
turtle.Position = target;
break;
}
case LogoLexer.Back:
{
var value = Parameter(0).Value(turtle);
var pos = turtle.Position;
var target = new Point(pos.X + MathF.Cos(turtle.Rotation) * value,
pos.Y + MathF.Sin(turtle.Rotation) * value);
if (turtle.IsDrawing) turtle.AddLine(pos, target);
turtle.Position = target;
break;
}
case LogoLexer.SetXY:
case LogoLexer.SetPos:
{
var target = Parameter<PointParam>(0).Point(turtle);
if (turtle.IsDrawing) turtle.AddLine(turtle.Position, target);
turtle.Position = target;
break;
}
case LogoLexer.SetX:
{
var target = turtle.Position.Clone();
target.X = Parameter(0).Value(turtle);
if (turtle.IsDrawing) turtle.AddLine(turtle.Position, target);
turtle.Position = target;
break;
}
case LogoLexer.SetY:
{
var target = turtle.Position.Clone();
target.Y = Parameter(0).Value(turtle);
if (turtle.IsDrawing) turtle.AddLine(turtle.Position, target);
turtle.Position = target;
break;
}
case LogoLexer.Arc:
if (turtle.IsDrawing)
{
var angle = Parameter(0).Value(turtle) * Turtle.ToRadians;
var radius = Parameter(1).Value(turtle);
turtle.AddArc(turtle.Position, turtle.Rotation, radius, angle);
}
break;
C#The most different case is the arc. As the turtle does not move during arc drawing, we just do not perform any operation in case the turtle is not in drawing mode.
After all the work on creating the new class, the evaluators for the new commands are quite simple:
case LogoLexer.PenDown:
turtle.IsDrawing = true;
break;
case LogoLexer.PenUp:
turtle.IsDrawing = false;
break;
case LogoLexer.SetPenSize:
turtle.Width = (int) Parameter(0).Value(turtle);
break;
case LogoLexer.SetPenColor:
turtle.Colour = Parameter<ColourNode>(0).Colour(turtle);
break;
case LogoLexer.SetPalette:
{
var pos = (int) Parameter(0).Value(turtle);
if (pos is < 0 or > 15) throw new IndexOutOfRangeException();
Colour.Palette[pos] = Parameter<ColourNode>(1).Colour(turtle);
break;
}
C#Note the last one, which evaluates the index of the palette, converts it to an integer value and checks its bounds. Only if it is inside of the valid bounds for the size of the palette, that the colour is stored.
Testing
We will add some tests, as usual. But, as usual, they will not be exhaustive. At all. They are just to give you an idea of how to write them. If you want to train and prepare a Pull Request with more tests, I will welcome them.
Test Utils
It is a little strange to start with this class, but I prefer to put it here and explain what is its goal, and you will see why we need it later when looking at some of the tests.
First, we will rewrite the ToAst
method. The goal is to have two signatures. One that works with the default LOGO tree visitor, and another one where the tree visitor can be supplied during the test phase.
public static INode ToAst(this string code) => code.ToAst(new TreeVisitor());
public static INode ToAst(this string code, LogoParserBaseVisitor<INode> visitor)
{
var input = new AntlrInputStream(code);
var lexer = new LogoLexer(input);
var parser = new LogoParser(new CommonTokenStream(lexer));
var tree = parser.program();
return visitor.Visit(tree);
}
C#So, the old method uses the new one, creating a new tree visitor on the fly. The second one just wraps the visitor and calls it.
We will add tests to the whole execution of the LOGO language. For that, it will be helpful to have a shortcut method that evaluates a LOGO program and returns both the AST and the turtle after executing the program:
public static (Turtle, Program?) Execute(this string code)
{
var tree = code.ToAst();
var turtle = new Turtle();
tree.Execute(turtle);
return (turtle, tree as Program);
}
C#Finally, we will want to validate colours. Validating each component at hand is tiring, so we can create an auxiliary method:
public static void AssertColour(this Colour colour, int red, int green, int blue)
{
Assert.AreEqual(colour.Red, red);
Assert.AreEqual(colour.Green, green);
Assert.AreEqual(colour.Blue, blue);
}
C#Testing the AST
The following two blocks define tests to validate the structure of the AST for the SetPalette
command. They are similar to the ones written in previous parts of the tutorial.
[TestMethod]
public void ColorPalette_CSSColor()
{
var tree = @"SetPalette 1 ""#ff00ff".ToAst() as Program;
Assert.IsNotNull(tree);
Assert.AreEqual(1, tree.Count);
var cmd = tree[0];
Assert.IsNotNull(cmd);
Assert.AreEqual(LogoLexer.SetPalette, cmd.Id);
Assert.AreEqual(2, cmd.Params.Count);
var position = cmd.Parameter<ValueParam>(0);
Assert.IsNotNull(position);
var colour = cmd.Parameter<ColourNode>(1);
Assert.IsNotNull(colour);
}
[TestMethod]
public void ColorPalette_LogoColor()
{
var tree = @"SetPalette 1 [1 2 3]".ToAst() as Program;
Assert.IsNotNull(tree);
Assert.AreEqual(1, tree.Count);
var cmd = tree[0];
Assert.IsNotNull(cmd);
Assert.AreEqual(LogoLexer.SetPalette, cmd.Id);
Assert.AreEqual(2, cmd.Params.Count);
var position = cmd.Parameter<ValueParam>(0);
Assert.IsNotNull(position);
var colour = cmd.Parameter<ColourNode>(1);
Assert.IsNotNull(colour);
}
C#Testing the Execution
The following block shows a new file with a test for the execution of the setPalette
command. We specify a position in the palette and validate if it is assigned with the desired colour. Note that we have some tests in the file LogoTests/ExpressionsParser.cs
that make sense to be moved to this new file. I might do that meanwhile.
namespace LogoTests;
[TestClass]
public class Commands
{
[TestMethod]
[DataRow("[ 99 0 0 ]")]
[DataRow(@"""red")]
[DataRow(@"""#ff0000")]
[DataRow(@"""#FF0000")]
public void Commands_SetPalette(string c)
{
$"SetPalette 0 {c}".Execute();
var colour = Colour.Palette[0];
Assert.IsNotNull(colour);
colour.AssertColour(255, 0, 0);
}
}
C#We test different formats for the red colour, setting the palette in position 0. We execute the code, using the shiny new command Execute
, and fetch the colour from the palette.
Testing the Parser: Faking it
This section is, probably, the one with the most novelty from this entire tutorial part. We will use a new library, named FakeItEasy, to mimic a tree visitor (that just visits all nodes, doing nothing), and use the fake visitor to verify if the correct visitors are called during the parsing process.
We will need the nuget for the new library:
dotnet add LogoTests package FakeItEasy
BashThe new file will have this content:
using FakeItEasy;
namespace LogoTests;
[TestClass]
public class Parser
{
[TestMethod]
public void Parser_BasicCommand()
{
var fakeVisitor = LogoFaker();
"FD 10 RT 90 FD 20".ToAst(fakeVisitor);
A.CallTo(() =>
fakeVisitor.VisitBasicCommand(A<LogoParser.BasicCommandContext>.Ignored))
.MustHaveHappened(3, Times.Exactly);
}
[TestMethod]
public void Parser_IfElse()
{
var fakeVisitor = LogoFaker();
"IfElse [ 10 > 20 ] [ FD 10 ] [ BK 20 ]".ToAst(fakeVisitor);
A.CallTo(() =>
fakeVisitor.VisitIfElseStmt(A<LogoParser.IfElseStmtContext>.Ignored))
.MustHaveHappenedOnceExactly();
A.CallTo(() =>
fakeVisitor.VisitCmdBlock(A<LogoParser.CmdBlockContext>.Ignored))
.MustHaveHappenedTwiceExactly();
}
private LogoParserBaseVisitor<INode> LogoFaker()
=> A.Fake<LogoParserBaseVisitor<INode>>(options => options.CallsBaseMethods());
}
C#Let’s start with line 32. We create a simple method that just creates a custom instant of a Fake It Easy object. In this code, A
is a shortcut class from the Fake It Easy library. We use it to ask the library to fake an implementation that inherits from the LogoParserBaseVisitor<INode>
class. Notice that this is our base class for own own tree visitor. The options we set in this invocation just indicate that each one of the methods from the fake implementation calls their base method.
Now, look into the first test. In line 10 we create the new fake tree visitor. In line 11 we convert a snippet of code into an AST using the fake visitor. Finally, line 13 tests that, during the parsing process, the method VisitBasicCommand
was called. As this method receives a parameter we need to supply that Ignored
instance, specifying that it is not important what is the value for this parameter (as far as the method is invoked we are happy). Then, we specify how many times this visitor should be called. As the code snippet has three basic commands, the visitor needs to be called exactly three times.
The second test is similar. The only difference is that we test two visitors instead of one.
Using this library is an interesting way of testing the parsing stage without using our visitor code, which might be introducing new bugs.