Renoir is a tool for generating code to be used by a programmer for coding tools that generate formated text. It works by specifying the format of the file in a specialised language, and then compiles this code to target language code (currently only Java), which will allow a programmer to easily output text in that format.
It is currently in a rough state, and I'm just dumping it out there in case someone has a use for it. It is currently used in other Scratchy Labs projects, so will receive ongoing development. If a particular direction is desired by any potential users, feedback will gladly be accepted.
Renoir is provided as a jar (or source code for compiling if you are brave), and as such, needs to be run with a command similar to:
java -jar Renoir.jar [ options ] files
If no files are specified, then the simple graphical user interface will appear. Otherwise, if "-help" is specified as an option then the following is outputted, and explains the available options:
Renoir help (Renoir --help) Usage: Renoir [OPTION]... [FILES]... Renoir is an tool for generating code for generating text in a particular format. Options --help - Show this message -user [user path] - Sets alternative path for user files -output [output path] - Sets alternative path for the output -library [library path] - Sets alternative path for library files -clean - clear target directories before outputing build results
The user must specify every file that is to produce output. Other Renoir files may be accessed (via the "import" option - see below), but unless specified, those files will not result in any generated java code).
The "user path" is the default path for searching for files specified by "import". The "output path" is the path where all generated code is stored. The "library path" is where files that are shared across projects are stored.
If no files or options are specified (or the jar is started simply by double-clicking), then a simple GUI interface will appear. This looks similar to the following:
Operation should be reasonably straightforward. The main task is that three folders ought to be specified (two at the minimum).
The first is the "library" folder, which contains Renoir files that are shared amongst projects. The second, the "source" folder, is the folder where all the Renoir files for a project sit. The vital difference between the two folders is that in a build, only files from the "source" folder will acutally result in the creation of any generated code. The library code must be generated seperately, by using it as the source in another project.
The third and final folder is the "target" folder, which is the base folder for the generated code. It ideally should be a folder that is set aside entirely for code generated by Renoir.
Once the "source" folder has been selected, the files in that folder should appear in the "Files" list. Renoir files should end in ".renoir", ".renoirx", or ".ren" (any is fine).
The buttons on the right allow the compiling and building. Compiling is just for testing. The Renoir files will be read and interpreted, and any areas can be noted, but no code will be produced, even if there are no errors.
The two build options do all that "Compile" does, but also produce code, if there are no errors. The difference between the two is that "Build" overwrites files in the target folder, and leaves any generated files that were there previously, but are not part of the new set of files. This can happen as a project changes, as Renoir produces a number of non-user files that are named in a sometimes inconsistent way. Using "Clean Build" will result in the target folder being cleared of all files first (use wisely!).
The Log informs the user of the compilation/build progress (sorry, no fancy progress bars yet). The output will be self-explanatory, though sometimes the errors given may be a bit strange (the error logging system needs a bit of work), they should direct the user to problems reasonably well. Use the "Clear" button to clear previous output.
There is support for a simple form of project. Once folders have been set up, they can be saved as a project by using the "Save"/"Save As" option in the "File" menu. As expected, use "Open" to open a previously saved project, and the "Recent" menu to quick load previously saved projects.
Renoir works by taking files in the "Renoir" language, and through interpretation of that file producing programmer usable code for personal projects. This section explains the components of such input files.
Files follow a simple format. Basically it is a list of "block" definitions, with "output" declrartions at suitable locations. The "output" declarations work the same as with AustenX output declarations.
An example file will look something like the follow:
output example.moo; Block1(int x) { subs* = SubBlock(); output { "Block:" $x "{" nl ++ $subs / ", " -- "}" } } SubBlock(int y, string z) { ouptut { "Sub:" $y " and " $z nl } }
The above Renoir file has two blocks and one output declaration.
The output declaration is simple. It directs the compiler to where the output code should end up. In Java in basically reflects the package where the code will be generated. More than one output declaration can occur in one file. An output declaration will affect all the blocks defined after it, until another output declaration is found. The format is "output package ;"
Block declarations (classes) are the guts of a renoir file. They are written in a "c-like" language style, with curly brackets delimiting everything. A block takes a number of primitive arguments, as well as having Block based children. They also include at least one "output" block (very different to the root level output declaration), which describes how the attributes and children of a Block node is outputed. In general there is just the default output block, but there may also be named output blocks, which will be discussed later.
A block is defined by a name, followed by a list of attributes, which are of "primitive" types (currently, "int", "double", "boolean", and "String/string"). They are provided just as in Java/C, as a comma seperated list with type then name, in between two round brackets. Eg, in the above example, block "SubBlock" has two attributes: the first is "y" of type int, and the second is "z" of type string.
Blocks have children, which are blocks. The block declaration lists the possible children, and their cardinality. In the above example "Block" has one type of child, which is called "subs", and is of the block type "SubBlock". The "*" means zero or more, so "Block" may have zero or more children, lumped together as "subs", and of type "SubBlock".
There are three types of cardinality. The first is zero or more, marked by an "*". The second is zero or one (maybe), marked by "?". The last is singleton, which is unmarked, and means exactly one. For example:
Example() { zeroOrMore* = Foo(); zeroOrOne? = Goo(); one = Zoo(); output { } }
In the above declaration of child types you may notice that the block type is followed by two round brackets, looking suspiciously like a function call. You can in fact pass attribute values, which are used when the actual block instances are added at run time (as will be obvious in the section on generated code). To do this, follow the following example:
Example() { one = Child(x = 4); output { } } Child(int x) { output {} }
More explanation will be given later, when generated code is discussed, but for now two more points. The first is that for the singleton/fixed cardinality, all attributes for the child must be specified, unless that child is a group (this is explained later). For other cardinalities (where the children are created by the user code), zero or more of the attributes can be given values, being all or just some of the total attributes of the child (*actually, of the current version, you must also supply all values for zero or one children as well*). Note also that supplied values can be attributes of the parent block, so the following is valid:
Example(int y) { one = Child(x = $y); output { } } Child(int x) { output {} }
The grunt work of Renoir is done in the output blocks. There are two types of output block. The first is the default output, which is the unnamed output block. The second type is the named one, and is used for greater flexibility. For now we will just discuss the default output block.
The general form of an output block is the output keyword followed by an opening and closing curly bracket. In between the brackets are a series of output elements. Elements in general are seperated by white space.
An output block is just a collection of output elements. Some elements are very basic, such as direct text, while others are a little more involved (such as conditional output).
The most basic output elements are the primitive ones. This includes text strings, numbers, and booleans. Text must be enclosed within double brackets. So for example, the following is valid:
output { "One plus one is " 2 "This statement is " true }
In the above example, when this is used to output text later, the result would actually be: "One plus one is 2This statement is true". That is, all text just runs on. The line break in the Renoir source file has no meaning. To insert a line break into the output, one needs to use the nl, or new line element. So, to rewrite the above example: output { "One plus one is " 2 nl "This statement is " true }
In addition to nl, there are also a few other basic elements that relate to specific text output. These include: sq or single, which outputs a single quote, and dq or double, which outputs a double quote.
As Renoir is meant for producing pretty printed output, support for indentation is provided. This occurs through the use of the ++, and -- elements. The first, ++, increased the indentation by one level, and the second, -- decreases it by. The effect is only apparent when a line break is encountered (so "++ --" does nothing effective). For example:
output { "function moo() { " ++ nl "print(" dq "statement" dq ");" nl -- "}" nl }
In the above example the final output would be:
function moo() { print("statement"); }
With the basic elements there is nothing dynamic. The end result of Renoir is to create code that can be used by user code to generate output in certain format. That output can be dynamically altered by the user code (if it couldn't Renoir would be useless, and you could just output a static text file). The interaction is provided by the attributes of an output block, and the blocks children, all of which, as will be seen later, are dictated by the user code.
Attributes and children are treated in a similar way, semantically, referenced as variables. To signify a variable use the attribute or child name is prefixed with "$". So to refer to the attribute "x", the reference is stated by "$x". For example, consider the following block with one attribute:
WithAttribute(int x) { output { "The value of x is:" $x nl } }
This works with children too. So for example:
Child() { output { "MOO " } } Parent() { myKids* = Child(); output { "The children say:" $myKids } }
The user code will add zero or more "Child" blocks to a Parent block (under the name "myKids" — more on this later). Assuming three "myKids" Child nodes have been added, the output from the above would be "The children say:MOO MOO MOO".
If a variable is a zero-or-one (optional) child (eg "child?=Child()"), then nothing is outputed if that child has not been set. (It does not cause an error, in other words).
When outputing a set of elements (specified by a variable) it may be useful to be able to output divider elements between each element of the main set. This can be done in Renoir by using the "/" (divider) operator. For example, reconsider the previous example as follows:
Child() { output { "MOO " } } Parent() { myKids* = Child(); output { "The children say:" $myKids / ", " } }
Again, if the user code addes three children then the output would be "The children say:MOO, MOO, MOO".
Alternatively, one can use the divider option over a set of given output elements. The set of elements must be surrounded by "{}". For example:
Simple() { output { { "A" "B" "C" } / ", " } }
The above will always output "A, B, C".
Sometimes output will be conditional on the user provided values. In such cases, the "if" statement can be used, which follows a similar form to c-like languages. So for example:
CatOrDog(boolean isCat) { output { if($isCat) { "A cat!" } else "A Dog!" } }
Note the use of "{}" to create a blank, or the single statement form (which I personally dislike but it's there if you need it).
As of the current version the conditionals must be very simple expressions, with only variable references being allowed. In general you should only reference two types of variables. The first is boolean attributes, which act as expected. The second is optional or zero-or-more children variables. In such cases, zero children (or no child), is like false, and one or more children is like true.
IfThereThenBoom() { child = Bomb(); output { if($child) { "BOOOOM! " $child } else { "No child :(" } } }
As mentioned earlier, there are two types of output block. The default block previously described, and the named output block. They work essentially the same, except for the addition of a name in the later case (later versions of Renoir will also support variable passing). For example, to declare a named output block, use the following example as a guide:
WithNamed() { output { "This is the default output" } output special { "This is the special named output" } }
WithNamed() { output { "This is the default output" } output special { "This is the special named output" } } UsingNamed() { $named* = UsingNamed(); output { "The default is " $named nl "The special is " $named.special nl } }
A useful feature of Renoir is the grouping of types. This allows you to define a set of objects that can be used for a pariticular variable. For example, you may have a "statements" variable to represent the statements of a language that is being outputed, and then use the grouping feature to group blocks together as "statements".
Groups are in ways comparable to classes or interfaces in other languages.
The first step is to define a group. This is done as follows:
group Statement { }
That is the basics of it. The "{}" are required, and as will be seen later, things can go between them.
There are two ways to put a block in a group. The first is use the "group" keyword again, but this time in a similar way to which "extends" or "implements" might be used in Java. So for example:
PrintStatement(String text) group Statement { output { "print(" dq $text dq ");" nl } IntDeclStatement(String variableName, int value) group Statement { output { "int " $variableName " = " $value ";" nl }
A block can belong to more than one grouping. This is done by listing them together separated by commas, as multiple interface implementation might be done with "implements" in Java (eg, "group A, B, C").
The second way is just to include a block definition within the group definition. So, for example:
group Statement { PrintStatement(String text) { output { "print(" dq $text dq ");" nl } IntDeclStatement(String variableName, int value) { output { "int " $variableName " = " $value ";" nl } }
(Blocks within a group statement can, probably, also be part of other groups with the "group" declaration).
A group is used just like a block, with regards to variables and output. So for example, where as you might refer to the "PrintStatement" block type, you could refer instead to the "Statement" type. So for example, the following is valid:
group Statement { PrintStatement(String text) { output { "print(" dq $text dq ");" nl } IntDeclStatement(String variableName, int value) { output { "int " $variableName " = " $value ";" nl } } Function(String name) { statements* = Statement(); output { "function " $name "() {" ++ nl $statements -- "}" nl } }
Note the use of "()" after the "Statement" reference (just like with block variables). Do not quote me on this, but if you do supply argument values, it will be passed on to the related block children. To make that make sense, consider the following:
group Thing { A(int x) { output "A:" $x } B(int y) { output "B:" $y } } ThingUser { things* = Thing(x=5, y=3); output { "Things:" $things } }
This should work to supply "x=5" to "A" children, and "y=3" to "B" children (for more explanation, see the code generated section).
Now we come to the important part of Renoir — what code is produced for a given input file. At the moment only Java code is produced.
The first thing to understand is that in general Renoir takes advantage of the "PrettyPrinter" framework, which exists within the Solar library. It is a simple framework for pretty printing text output.
At the heart of the PrettyPrinter framework is the org.ffd2.solar.pretty.PrettyPrinter interface. It looks something like the following:
package org.ffd2.solar.pretty; public interface PrettyPrinter { public PrettyPrinter breakPoint(); public PrettyPrinter increaseIndent(); public PrettyPrinter decreaseIndent(); public PrettyPrinter print(String text); public PrettyPrinter println(String text); public PrettyPrinter pjava(String label, boolean capitalise); public PrettyPrinter pdouble(double v); public PrettyPrinter pdoubleln(double v); public PrettyPrinter pint(int v); public PrettyPrinter pintln(int v); public PrettyPrinter pchar(char v); public PrettyPrinter pcharln(char v); public PrettyPrinter pboolean(boolean v); public PrettyPrinter pbooleanln(boolean v); public PrettyPrinter jbug(String text); public PrettyPrinter space(); public PrettyPrinter tab(); public PrettyPrinter newLine(); }
The exact details are not important as I'm sure it is self explanatory. All PrettyPrinter implementations should return themselves from function calls (to chain calls). In general, one should not have to implement a PrettyPrinter object, as some implementations are supplied with Solar.
The supplied implementations are as follows:
This implementation wraps around a java.io.Writer object.
Available constructors are:
This implementations wraps around the org.ffd2.solar.SimpleStringBuffer class, which is a Solar internal StringBuffer implementation.
In general, the normal user will only perhaps be interested in the following constructors:
Once created, the "toString()" method of the "SimpleStringBufferBasedPrettyPrinter" object can produce the result as a String.
Unlike say AustenX, which generates vast numbers of classes for its input files, Renoir in general produces relative few. Currently there is a one-to-one relationship between Renoir block declarations and created Java classes. Groups have a one-to-one relationship with generated Java interfaces.
Assume the following input:
output example; Example(int x) { output { "Example:" $x } }
The above is an example of a very simple Renoir file. Running the Renoir tool will produce one class. The important details, of this class, for the user are as follows:
package example; public final class ExampleRenoir { public ExampleRenoir(int x) { ... } public void reset() { ... } public final boolean outputTo(org.ffd2.solar.pretty.PrettyPrinter target) { ... } public final void setX(int value) { ... } public final int getX() { ... } }
The other parts of the class are for internal Renoir use. Implementation details have been left out.
The important parts of the class are:
This is the basic structure for all blocks. When there are more attributes, then there are more related setter methods, and the constructor changes. If there are no attributes, the constructor has no parameters, and there are no setter methods.
Now, we look at the more advanced case when a block has other blocks as children. Consider the following:
output example; Child(int y) { output { "Child:" $y nl } } Example(int x) { children*=Child(); possible?=Child(y=3); fixed=Child(y=4); output { "Example:" $x nl "Children:" $children nl if($possible) { "Possible:" $possible nl } "Fixed:" $fixed nl } }
In this case two Java classes are created, "ExampleRenoir", and "ChildRenoir". The class "ChildRenoir" will look very similar to "ExampleRenoir" in the previous example, but the new "ExampleRenoir" will be somewhat different. In particular, the important parts look as follows:
package example; public final class ExampleRenoir { public ExampleRenoir(int x) { ... } public final void reset() { ... } public final void markForDeletion() { ... } public final boolean outputTo(org.ffd2.solar.pretty.PrettyPrinter target) { ... } public final ChildRenoir addChildrenSubBlock(int y) { ... } public final void addChildrenSubBlockDirect(ChildRenoir created) { ... } public final void addChildren(ChildRenoir value) { ... } public final void addChildrenArray(ChildRenoir[] source) { ... } public final ChildRenoir createPossibleSubBlock() { ... } public final void setPossibleSubBlock(ChildRenoir value) { ... } public final int getX() { ... } public final void setX(int value) { ... } public final ChildRenoir getFixedSubBlock() { ... } }
Some of the methods are familiar from the previous example, but there additional methods to deal with the children. These methodsa are dependent on the cardinality of the child, and the supplied attribute values. In general, for non-fixed children there is the option of creating a child, or setting an already created one.
NOTE: IT IS BEST NOT TO ADD A CHILD TO MORE THAN ONE PARENT BLOCK. (Though you can probably get away with it if the blocks are never marked for deletion — though marking for deletion will work, but from both parents — so actually, it probably is fine).
Anyhow, a summary of the above methods should help with your expectations:
(There is no way to clear all of a particular child type. You can just use "reset()" only at the moment — which clears all children).
For a zero or more child of name "X", for block type Y, there will be at least the following methods:
(There is no way to clear all of a particular child type. You can just use "reset()" only at the moment — which clears all children).
For a zero or one child of name "X", for block type Y, there will be at least the following methods:
For a fixedchild of name "X", for block type Y, there will be at least the following methods:
Block nodes can be deleted from parents by marking them for deletion with "markForDeletion()" (call method to mark for deletion). This will do nothing until the next time the parent does its output (and refers to the children set that the block belongs to). When this happens, the block marked for deletion will not output anything, and will be subsequently removed from the parent's internal storage. Calling the "output()" method of a block that has been marked for deletion will produce no effect.
Groups complicate things slighty. The first thing about groups is that they create a matching interface, ending in "Group". So for example, if you have a group "Expression", there will be a "ExpressionGroup" interface created. As in:
output example; group Expression { }
Such a definition will in turn create a "ExpressionGroup.java" as follows:
package example; public interface ExpressionGroup { public boolean outputTo(org.ffd2.solar.pretty.PrettyPrinter target) ; }
So far so good. Now, if a block is part of a group, it will implement the related group interface. So:
output example; group Expression { } Variable(String name) group Expression { output { $name } }
This will result in an "ExpressionGroup.java", as above, as well as a "VariableRenoir.java" as follows:
package example; public final class VariableRenoir implements ExpressionGroup { ... }
Where things get most interesting is when a group is used for a child of a block. In this case, the methods are similar to the block case but there will be more for each type of block. Consider the following example:
group Expression { } Variable(String name) group Expression { output { $name } } Binary (String symbol) group Expression { left=Expression(); right=Expression(); output { $left $symbol $right } } FunctionCall(String functionName) { expressions*=Expression(); output { $functionName "(" $expressions / ", " ")" } }
The above example has references to group based children that use two different cardinalities. The first is in "FunctionCall", where zero-or-more children of type Expression (group) are assigned to "expressions". The second is a fixed use of a group type in Binary. Fixed types of group objects is allowed (even though attribute values are not supplied), as shall be seen.
First we shall examine the created "FunctionCallRenoir.java" class, which looks roughly as follows:
package example; public final class FunctionCallRenoir { public FunctionCallRenoir(java.lang.String functionName) { ... } public final BinaryRenoir addBinaryForExpressions(java.lang.String symbol, ExpressionGroup left, ExpressionGroup right) { ... } public final VariableRenoir addVariableForExpressions(java.lang.String name) { ... } public final void addExpressions(ExpressionGroup value) { ... } public final void addExpressionsArray(ExpressionGroup[] source) { ... } }
As can be seen the created methods are roughly similar to the case when a simple block type is used. What is different is that there are multiple "create and add" methods for each type of block that is part of a group. So, in this case, "addBinaryForExpressions()", and "addVariableForExpressions()" methods. Thre are still general, already created, add methods, but these take objects implementing the ExpressionGroup interface, instead of objects of a specific class.
Of particular note is that the fixed "left" and "right" children of the "Binary" block are passed as parameters. More on this becomes apparent when we see the "BinaryRenoir.java" class:
ackage example; public final class BinaryRenoir implements ExpressionGroup { public BinaryRenoir(java.lang.String symbol, ExpressionGroup left, ExpressionGroup right) { } public final ExpressionGroup getLeftValue() { } public final ExpressionGroup getRightValue() { } }
In the case of fixed children of group type, they cannot be created in the constructor of the parent (as is the case with fixed children of simple block types), so implementing instances must be passed to the constructor (future versions of Renoir will allow this for simple block types as well — it's a odd omission ).
I will leave "optional" group type children as an exercise for the user (it should be pretty obvious).
As has been mentioned, to use the outputed code, classes from Solar library must be included (version 1.1 of Solar for verision 0.9 of Renoir)
Renoir is an ongoing project guided by personal needs. Undoubtedly with greater use in other projects its abilities will greatly expand in future versions. Again, if there is a direction you would like it to take, please feel free to contact me. If it is of no use to anyone else, that's all good with me.