A powerful and flexible parser for meta-annotated variables and function calls in Go. This project helps you create domain-specific languages (DSLs) by allowing you to expose Go variables and functions through a simple, custom syntax.
Note
Head over to the GoDSL Examples repository for example code that showcases how GoDSL can be used.
A Domain-Specific Language (DSL) is a programming language designed for a specific purpose or domain, rather than for general-purpose programming. Unlike general-purpose languages like Go or Python, DSLs are tailored to solve problems in a particular domain, making them more expressive and easier to use for that specific purpose. Common examples include:
- SQL for database queries
- HTML for web page structure
- Regular expressions for text pattern matching
- Configuration languages like YAML or TOML
DSLs are particularly useful when you want to:
- Create a simple scripting interface for your Go application
- Build a configuration language
- Create a mathematical or logical expression evaluator
- Expose Go functionality through a custom syntax
GoDSL is a tool that generates a parser for a domain-specific language (DSL) from Go source code with special annotations. The parser implementation (located in the parser
folder) is generic and will be created in your package directory. Note that files with the prefixes dsl_
and template_
will be automatically removed when running GoDSL, so you should avoid using these prefixes in your own files.
After copying the base files, GoDSL generates a dsl_init.go
file containing an init()
function that loads all annotated functions and variables into the parser. Once this setup is complete, you can begin using your custom language.
- Parse nested function calls and expressions
- Support named and positional arguments
- Validate and convert int, float, bool, string, and image types
- Handle optional parameters with defaults
- Manage global variables
- Visualize AST trees in debug mode
- Reference script arguments ($1, $2, etc.)
- Process inline comments (# comment #)
- Handle escaped characters in strings and comments
- Flexible expression whitespace
- Detailed error reporting
- Image processing capabilities:
- Support for RGBA and NRGBA image formats
- Proper transparency handling with alpha channel support
- Pre-multiplied alpha support for correct blending
- Comprehensive test suite for image operations including:
- Test image generators for validation:
- Gradient patterns with alpha transitions
- Checkerboard patterns with alternating transparency
- Color wheels with radial transparency
- Noise patterns with random transparency
- Color bands and edge case test patterns
- Test image generators for validation:
The parser supports a simple but powerful syntax for function calls and variable management. Here are the key features:
- Basic Function Calls: Call functions with arguments separated by spaces, like
functionName(arg1 arg2 "string arg")
- Named Arguments: Use named parameters for more readable function calls, such as
functionName(param1=value1 param2=value2)
- Nested Function Calls: Combine function calls by nesting them, for example
outerFunction(innerFunction(arg1 arg2) arg3)
- Variable Assignment: Create and set variables using the syntax
variableName: value
- Argument References: Reference script arguments using
$1
,$2
, etc., as infunctionName($1 $2)
- Comments: Add inline comments using the
#
symbol, likefunctionName(arg1 # This is a comment # arg2)
. You can escape the#
character using\#
if needed. - Strings: Enclose text in
"
characters, like"hello world"
. You can escape the"
character using\"
if needed.
Note
The same argument style (named or unnamed) must be used consistently throughout the entire expression, including any nested function calls.
These expressions are therefore invalid:
functionName(arg1 param2=value2 arg3)
(mixing styles in the same call)outerFunction(innerFunction(arg1 arg2) param=value)
(different styles in nested calls)
The parser supports a set of basic types to handle different kinds of data:
- Integer: Whole numbers like
42
for counting and discrete values - Float: Decimal numbers like
3.14
for precise calculations and measurements - Boolean: Logical values
true
andfalse
for conditional operations - String: Text values enclosed in
"
characters, like"hello \"world"
. You can escape the"
character using\"
if needed - Image: Image data in RGBA/RGBA64 or NRGBA/NRGBA64 format, supporting 8-bit and 16-bit color depths with full alpha channel transparency
The parser provides detailed error messages to help you identify and fix issues in your DSL code. Here are the types of errors that will be reported:
- Syntax Errors: Missing or mismatched parentheses, unterminated strings or comments
- Function Errors: Unknown function names, invalid function calls, or unexpected tokens
- Argument Errors: Invalid argument types, invalid named arguments, or type conversion failures
- Variable Errors: Invalid variable assignments, reference errors, or missing variables
- Runtime Errors: Any errors that occur during the execution of your DSL code
The parser processes your DSL code through several distinct stages:
- Tokenization: The input is broken down into individual tokens, carefully handling strings, variables, and special characters
- Lexical Analysis: The sequence of tokens is validated to ensure proper syntax and structure
- Parsing: An abstract syntax tree (AST) is constructed from the validated tokens
- Evaluation: The function calls and variable assignments are executed according to the AST structure
You can find detailed test coverage of these stages in the parser/pkg_test.go
file.
Here are some important requirements and considerations for developing with GoDSL:
- Package Initialization: Do not write your own
init()
function for the package, as GoDSL will generate one that loads everything the DSL needs - Namespace Organization: GoDSL uses a pseudo-namespace under
dsl
in the package root to minimize interference with existing files. All DSL-related types will be prefixed withdsl
- File Management: GoDSL copies source code and templates to your package directory, prefixing them with
dsl_
(source code) andtemplate_
(templates). Avoid using these prefixes for your own files as they will be removed during subsequent GoDSL runs - Language Exposure: By default, no language features are exposed. You'll need to write your own code to expose the desired functionality
- Generated Files: Do not edit the generated files if you plan to use GoDSL to update your language later
/src/my-project/example/definition.go
:
package main
var (
// @Name: last
// @Desc: last result
// @Range: -
// @Unit: -
res = 0.0
)
// @Name: add
// @Desc: Adds two numbers
// @Param: x - 0..10 0 First number
// @Param: y - 0..10 0 Second number
// @Returns: result - 0..20 0 Sum of the numbers
func add(x, y float64) (result float64, err error) {
return x + y, nil
}
This will define a DSL with one variable and one function, each is annotated so GoDSL can parse out the necessary information for validation and documentation.
When defining functions for your DSL, keep these requirements in mind:
- Function Location: Functions must be defined at the package level
- Parameter Count: Functions can have any number of parameters
- Return Values: Functions must return a pair of values, with the second value being an
error
- Supported Types: The following types are allowed for parameters and returns:
float*
(any float type)int*
(any integer type)uint*
(any unsigned integer type)bool
string
*image.RGBA
(8-bit RGBA image type)*image.NRGBA
(8-bit non-premultiplied RGBA image type)*image.RGBA64
(16-bit RGBA image type)*image.NRGBA64
(16-bit non-premultiplied RGBA image type)
Each function must be annotated with the following information:
- @Name: The function's name
- @Desc: A description of what the function does
- @Param: For each parameter, specify:
- Name
- Unit (
-
for unitless) - Range (
-
for no range) - Default value (
-
for no default) - Description
- @Returns: For the first return value, specify:
- Name
- Unit (
-
for unitless) - Range (
-
for no range) - Default value (
-
for no default) - Description
Note
While you can annotate the error
return value, it's recommended to omit it for functions that never return an error to keep the documentation clean. The error
return is used internally by the parser to determine if a function executed successfully.
- Variables MUST be defined in a single block
- GoDSL expects these annotations:
- @Name is the name of the variable.
- @Desc is the description of the variable.
- @Range is the range of the variable (omit or use
-
for no range). - @Unit is the unit of the variable (omit or use
-
for no unit).
To get started with your DSL, follow these steps:
-
Build and Install GoDSL: If you haven't built
go-dsl
yet, you'll need to build and install it first:go build -o go-dsl ./app/ sudo cp go-dsl /usr/local/bin/
-
Generate the DSL: Navigate to your project's root directory and run:
cd /src/my-project go-dsl "basic" "Basic Example" "A basic example implementation" "1.0.0" "basic" example/
The command takes these arguments:
id
: Language identifier (e.g.,cpp
for C++ orgo
for Golang)name
: The name of your languagedescription
: A description of your languageversion
: The version of your languageextension
: File extension for your language (e.g.,go
for*.go
files)packages
: The packages to scan for annotated functions and variables (each package will generate a separate DSL)
Once complete, your package directories will contain all necessary files for the DSL, including an init()
function in dsl_init.go
that prepares the pseudo-namespace and loads all variables and functions. You're now ready to use your DSL!
Here's a simple example of how to create a CLI tool that processes your DSL:
package main
import (
"fmt"
"strings"
"github.com/toxyl/flo"
)
func main() {
// Execute the DSL script using command line arguments
r, err := dsl.run(strings.Join(os.Args[1:], " "), true) // true enables debug mode
if err != nil {
fmt.Println("\x1b[31mError:\x1b[0m", err)
return
}
// Process the script's result
var res any
if r.err != nil {
fmt.Println("\x1b[31mError:\x1b[0m", r.err)
return
}
res = r.value
fmt.Printf("\x1b[32mResult:\x1b[0m %v\n\n", res)
// Example of variable manipulation
v := dsl.vars.get("gx")
gx := v.get().(float64)
fmt.Println("\x1b[32mgx\x1b[0m =", "gx", "*", 10)
dsl.vars.set("gx", gx*10)
fmt.Println("\x1b[32mgx\x1b[0m =", v.get())
// Export documentation
flo.File("doc.md").StoreString(dsl.docMarkdown())
flo.File("doc.html").StoreString(dsl.docHTML())
}
To run your application:
cd /src/my-project/
go run ./example/
Alternatively, you can use the DSL shell for a more interactive experience:
The DSL shell provides an interactive environment for testing and using your DSL. It can be used both for development and as a standalone application, as demonstrated in several examples. To launch it, simply call dsl.shell()
in your main function:
package main
func main() {
dsl.shell() // Launch the interactive shell
}
The shell will display a welcome message with basic usage examples and available commands.
The shell supports several types of operations:
-
Function Calls:
add(1 2) // Basic function call add(x=1 y=2) // Named arguments add(1 sub(2 3)) // Nested function calls
-
Variable Management:
x: 42 // Create and set variable y: add(x 10) // Use variables in expressions
-
Variable Inspection:
x // Display value of x y // Display value of y
The shell provides several built-in commands to help you work with your DSL:
?
- Display the help screenhelp
- Show the full documentationdebug
- Toggle debug mode (displays AST trees)store
- Save the current variable staterestore
- Restore the previous variable stateexport-md
- Export documentation as Markdownexport-html
- Export documentation as HTMLexport-vscode-extension
- Generate a VSCode extension for your DSLsearch [term]
- Search documentation for variables or functionsexit
orCTRL+D
- Exit the shellTAB
TAB
- Show autocomplete suggestions for variables and functions