The Dither programming language features a powerful macro system that allows compiler plugins to operate on the abstract syntax tree (AST) and generate code (in the form of another AST) on the programmer's behalf.
One of the most useful application of this technology is that now you can write shaders right in the Dither language — your CPU and GPU codes can share the same functions and variables. Given an entry function, Dither's macro system will extract the necessary tidbits and send it to a transpiler plugin, which generates and returns the shader code (such as GLSL) as a string constant. This would work not only for shaders, but also for any other domain-specific language — as long as a transpiler plugin is provided.
Another handy application is that a plugin can scan your code for any variable marked as a parameter, and automatically generate a GUI with sliders etc., for each of these variables. The possibilities are endless.
But let's first look at how the syntax works.
Macros are invoked with the embed statement, which takes the form of:
embed <expr> as "plugin-name"
Where <expr> can be a function, a variable, a namespace, or any valid name of something that already exisits in your code and is in-scope where the embed is invoked (but beware of operator precedence, of which the embed operator has high).
"plugin-name" is simply the name of the compiler plugin as a string constant. Plugins needs to be registered with the complier to work; Dither comes with a few plugins; You can also write your own plugins, which we'll cover later.
Let's look at the simplest example. We'll write a trivial function, and use the embed keyword to ask the fragment shader plugin to transpile it.
Now that was a valid program, but we don't see any output. As it happens, the fragment shader plugin returns a string constant as its output. So let's print that directly.
Depending on your current platform running the code, you might see a GLSL or GLES program printed (more backends are on their way!). We can also combine the function definition, embed statement, and output usage together so we have fewer loose parts:
Notice that we need to put a pair of parentheses around the lambda function — this is because the embed operator has high precedence. In the entry function, you're free to refer to any functions or variables from your program that is in-scope — the plugin will pull together all the pieces needed for the shader program.
Of course, to take that generated shader program and draw graphics with it, we need to import Dither's standard library "std/frag", and pass to frag.program (instead of io.println) the code. Here's an example that draws a gradient:
For more sophisticated sample codes, feel free to look at Dither's many examples that involve fragment shaders.
As you might have noticed, in the previous example, we prepended a function argument with @varying. This is called a hint in Dither, and is, well, a hint exclusively for macro plugins. The complier itself compeletely ignores these, and they do not interfere with the rest of Dither's syntax. Yet, a plugin might need that extra bit of information for something, and in this case, to tell the role of the parameter: @uniform/@varying/@builtin, etc.
You might think of these hints as “fancy” comments. But they do get checked at compile time to make sure they conform to certain syntax, and are then passed on to plugins as-is for further interpretation. The most basic form for hints can be written as follows:
@ <hint-name> <expr>
The rule for naming the <hint-name> is the same as those for an identifier. The space after the @ operator can be, and is often omitted for aesthetical reasons. The <expr> can also be anything, but the @ operator has high operator precedence, so insert parentheses as needed.
For more advanced use, the name of the hint, like a function name or a type name, can also take parameters using the subscript ([]) operator:
@ <hint-name>[<hint-param>, <hint-param>, ...] <expr>
Let's look at an example where we use hint parameters to build a slider panel with the builtin std/gui library and its associated plugin.
Here we use a hint called param that can take two parameters (min and max of a range) to decorate the variables x and y. Later we call embed on global (which is the name of the global, default namespace) to generate code for laying out and syncing the GUI automatically.
In this case, the plugin generates full AST's instead of just string constants (technically still an AST, just with a single node).
For more sophisticated sample codes, feel free to look at Dither's many examples that involve generated GUI's.
You can write your own plugins for the Dither complier, but that's a bit more involved. Currently part of Dither's complier is written in JavaScript, and that is the language you need to write in for the plugins. Eventually, you'll be able to write Dither plugins in Dither (when Dither complier will also be written in Dither), but for now, we have to bear with JavaScript for a while.
The rest of this section might change as Dither is being developed.
Your plugin needs to be a JavaScript object with an exec field that is a function. The first argument to the function is the AST passed to the embed operator. The second argument is all the identifiers defined in every scope (namespaces, blocks, etc.) of the program, which you can use to find all needed tidbits to inform your transformation of the AST.
var my_plugin = {
exec: function(ast,scopes){
...
return new_ast;
}
}
To see what the AST for some code you want to generate look like, you can write some code and make the Dither complier output AST for it.
Next, you need to register your new plugin with the compiler. You do so when initializing the parser.
let parser = new PARSER(sys,{
"plugin_name_1": my_plugin
"plugin_name_2": ...
})
That "plugin_name_1" will be the string you use as the second operand of the embed/as operator.
These interfaces will also be exposed for the dither commandline utility in the future.