goon
goon
https://git.tonybtw.com/goon.git
git://git.tonybtw.com/goon.git
Added documentation, and examples.
Diff
diff --git a/README.md b/README.md
index 15fd85b..ee0bd63 100644
--- a/README.md
+++ b/README.md
@@ -1,34 +1,54 @@
# Goon
-A 1k line embeddable configuration language with Nix-like syntax.
+A tiny embeddable configuration language with Nix-like syntax.
-## Example
+- **~1800 lines of C** - zero dependencies
+- **39KB binary** - smaller than most config parsers
+- **JSON output** - integrates with anything
+- **Arrow functions** - define your own config templates
-```nix
-let name = "goon";
-let version = 1;
+## Quick Example
-let colors = import("./colors");
+```goon
+let key = (mods, key, cmd) => { mods = mods; key = key; cmd = cmd; };
-{
- name = name;
- version = version;
+let keys = map([1..9], (n) => key("super", n, "workspace ${n}"));
- border = {
- width = 2;
- color = colors.blue;
- };
+{
+ border_width = 2;
+ gap = 10;
+ keys = keys;
+}
+```
- tags = ["1", "2", "3", "4", "5"];
+Output:
+```json
+{
+ "border_width": 2,
+ "gap": 10,
+ "keys": [
+ { "mods": "super", "key": 1, "cmd": "workspace 1" },
+ { "mods": "super", "key": 2, "cmd": "workspace 2" },
+ ...
+ ]
}
```
-## CLI Options
+## Features
-```bash
-goon eval config.goon
-goon check config.goon
-```
+- **Let bindings**: `let x = 1;`
+- **Records**: `{ name = "foo"; value = 42; }`
+- **Lists**: `[1, 2, 3]`
+- **Ranges**: `[1..9]` expands to `[1, 2, 3, 4, 5, 6, 7, 8, 9]`
+- **Arrow functions**: `(x) => { doubled = x; }`
+- **String interpolation**: `"value is ${x}"`
+- **Spread operator**: `{ ...defaults; custom = 1; }`
+- **Imports**: `import("./other.goon")`
+- **Conditionals**: `if cond then a else b` or `cond ? a : b`
+
+Why C? Because C has the best C interop.
+
+Requires only a C99 compiler.
## Embedding
@@ -40,17 +60,53 @@ Copy `src/goon.c` and `src/goon.h` into your project.
int main() {
Goon_Ctx *ctx = goon_create();
- if (goon_load_file(ctx, "config.goon")) {
- Goon_Value *result = goon_eval_result(ctx);
- Goon_Value *name = goon_record_get(result, "name");
- if (goon_is_string(name)) {
- printf("name: %s\n", goon_to_string(name));
- }
- } else {
- printf("error: %s\n", goon_get_error(ctx));
+ if (!goon_load_file(ctx, "config.goon")) {
+ goon_error_print(goon_get_error_info(ctx));
+ goon_destroy(ctx);
+ return 1;
+ }
+
+ Goon_Value *result = goon_eval_result(ctx);
+
+ // Access fields
+ Goon_Value *name = goon_record_get(result, "name");
+ if (goon_is_string(name)) {
+ printf("name: %s\n", goon_to_string(name));
}
+// Or convert to JSON
+ char *json = goon_to_json_pretty(result, 2);
+ printf("%s\n", json);
+ free(json);
goon_destroy(ctx);
return 0;
}
```
+
+### Registering Custom Functions
+
+```c
+Goon_Value *my_func(Goon_Ctx *ctx, Goon_Value **args, size_t argc) {
+ if (argc < 1 || !goon_is_int(args[0])) return goon_nil(ctx);
+ return goon_int(ctx, goon_to_int(args[0]) * 2);
+}
+
+Goon_Ctx *ctx = goon_create();
+goon_register(ctx, "double", my_func);
+```
+
+## Why Goon?
+
+| Language | Deps | Size | Turing Complete |
+|----------|------|------|-----------------|
+| **Goon** | 0 | 39KB | No |
+| Lua | 0 | ~250KB | Yes |
+| Nickel | Many | ~20MB | Yes |
+| Dhall | Many | ~50MB | No |
+
+Use goon for configs.
+
+## Documentation
+
+- [SPEC.md](SPEC.md) - Language specification
+- [examples/](examples/) - Example configs
diff --git a/SPEC.md b/SPEC.md
new file mode 100644
index 0000000..a83ebb4
--- /dev/null
+++ b/SPEC.md
@@ -0,0 +1,377 @@
+# Goon Language Specification
+
+Version: 0.1.0
+
+Goon is an embeddable configuration language with Nix-like syntax. It evaluates to JSON and is designed for window manager configs and similar applications.
+
+## Lexical Structure
+
+### Comments
+
+```goon
+// Single line comment
+
+/* Multi-line
+ comment */
+```
+
+### Identifiers
+
+```
+IDENT = [a-zA-Z_][a-zA-Z0-9_]*
+```
+
+Reserved keywords: `let`, `if`, `then`, `else`, `true`, `false`, `import`
+
+### Integers
+
+```
+INT = -?[0-9]+
+```
+
+Examples: `0`, `42`, `-10`
+
+### Strings
+
+Double-quoted with escape sequences and interpolation:
+
+```goon
+"hello world"
+"line1\nline2"
+"value is ${x}"
+```
+
+Escape sequences:
+- `\n` - newline
+- `\t` - tab
+- `\r` - carriage return
+- `\\` - backslash
+- `\"` - double quote
+- `\$` - literal dollar sign (escape interpolation)
+
+Interpolation:
+- `${identifier}` is replaced with the string value of the variable
+- Integers and booleans are converted to strings automatically
+
+### Booleans
+
+```goon
+true
+false
+```
+
+### Operators and Punctuation
+
+| Token | Description |
+|-------|-------------|
+| `=` | Assignment |
+| `=>` | Arrow (lambda) |
+| `..` | Range |
+| `...` | Spread |
+| `?` | Ternary condition |
+| `:` | Ternary separator |
+| `;` | Statement terminator |
+| `,` | List/argument separator |
+| `.` | Field access |
+| `{}` | Record delimiters |
+| `[]` | List delimiters |
+| `()` | Grouping / parameters |
+
+## Grammar
+
+```ebnf
+program = statement* expression? ;
+
+statement = let_binding ;
+let_binding = "let" IDENT "=" expression ";" ;
+
+expression = if_expr
+ | ternary ;
+
+if_expr = "if" expression "then" expression "else" expression ;
+
+ternary = primary ("?" expression ":" expression)? ;
+
+primary = INT
+ | STRING
+ | "true" | "false"
+ | IDENT ("(" args? ")")? (* variable or function call *)
+ | IDENT ("." IDENT)* (* field access *)
+ | record
+ | list
+ | lambda
+ | import_expr
+ | "(" expression ")" ;
+
+lambda = "(" params? ")" "=>" expression ;
+params = IDENT ("," IDENT)* ;
+args = expression ("," expression)* ;
+
+record = "{" (record_item (";" record_item)* ";"?)? "}" ;
+record_item = IDENT "=" expression
+ | "..." expression ;
+
+list = "[" (list_item ("," list_item)* ","?)? "]" ;
+list_item = range
+ | "..." expression
+ | expression ;
+
+range = INT ".." INT ;
+
+import_expr = "import" "(" STRING ")" ;
+```
+
+## Types
+
+Goon has the following value types:
+
+| Type | Description | JSON Output |
+|------|-------------|-------------|
+| `nil` | Null value | `null` |
+| `bool` | Boolean | `true` / `false` |
+| `int` | 64-bit integer | Number |
+| `string` | UTF-8 string | String |
+| `list` | Ordered collection | Array |
+| `record` | Key-value map | Object |
+| `lambda` | Function | (not serializable) |
+
+## Semantics
+
+### Let Bindings
+
+Bind a name to a value. Semicolon is required.
+
+```goon
+let x = 42;
+let name = "goon";
+```
+
+Bindings are visible after their definition in the same scope.
+
+### Records
+
+Records are key-value maps with string keys.
+
+```goon
+{
+ name = "goon";
+ version = 1;
+ nested = {
+ foo = "bar";
+ };
+}
+```
+
+Field access with dot notation:
+
+```goon
+let config = { name = "test"; };
+let n = config.name; // "test"
+```
+
+### Lists
+
+Ordered collections of values.
+
+```goon
+[1, 2, 3]
+["a", "b", "c"]
+[{ x = 1; }, { x = 2; }]
+```
+
+### Ranges
+
+Ranges expand to inclusive integer sequences inside lists.
+
+```goon
+[1..5] // [1, 2, 3, 4, 5]
+[1..1] // [1]
+```
+
+### Spread Operator
+
+Spread (`...`) merges values into lists or records.
+
+In lists:
+```goon
+let a = [1, 2];
+let b = [...a, 3, 4]; // [1, 2, 3, 4]
+```
+
+In records:
+```goon
+let defaults = { gap = 10; border = 2; };
+let config = { ...defaults; gap = 20; }; // { gap = 20; border = 2; }
+```
+
+Later values override earlier ones.
+
+### Arrow Functions (Lambdas)
+
+Anonymous functions for creating templates.
+
+```goon
+let double = (x) => { value = x; doubled = x; };
+let make_key = (mod, key, cmd) => { mod = mod; key = key; cmd = cmd; };
+```
+
+Functions are called with parentheses:
+
+```goon
+let result = double(5); // { value = 5; doubled = 5; }
+let k = make_key("super", "a", "app");
+```
+
+Functions capture their lexical environment (closures).
+
+### String Interpolation
+
+Variables can be embedded in strings:
+
+```goon
+let name = "world";
+let greeting = "hello ${name}"; // "hello world"
+
+let n = 42;
+let msg = "value is ${n}"; // "value is 42"
+```
+
+### Conditionals
+
+If-then-else:
+```goon
+let x = if true then 1 else 2; // 1
+```
+
+Ternary operator:
+```goon
+let x = true ? 1 : 2; // 1
+```
+
+Both require an else branch.
+
+### Imports
+
+Import other goon files:
+
+```goon
+let colors = import("./colors.goon");
+let theme = colors.dark;
+```
+
+- Paths are relative to the importing file
+- `.goon` extension is optional
+- Imported files are evaluated and their final expression is returned
+
+## Built-in Functions
+
+### map(list, function)
+
+Apply a function to each element of a list.
+
+```goon
+let nums = [1..3];
+let doubled = map(nums, (n) => n); // [1, 2, 3] (identity)
+
+let keys = map([1..9], (n) => {
+ mod = "super";
+ key = n;
+ cmd = "workspace ${n}";
+});
+```
+
+## Constraints
+
+1. **No arithmetic**: Goon does not have `+`, `-`, `*`, `/` operators
+2. **No comparison**: No `==`, `<`, `>` operators
+3. **No recursion**: Functions cannot call themselves
+4. **Immutable**: Values cannot be reassigned after binding
+
+## Output
+
+Goon evaluates to a single value, typically a record, which is serialized to JSON.
+
+```goon
+let name = "myapp";
+let version = 1;
+
+{
+ name = name;
+ version = version;
+ enabled = true;
+}
+```
+
+Output:
+```json
+{
+ "name": "myapp",
+ "version": 1,
+ "enabled": true
+}
+```
+
+## CLI Usage
+
+```bash
+# Evaluate and output JSON
+goon eval config.goon
+
+# Pretty-print output
+goon eval config.goon --pretty
+
+# Check syntax without evaluating
+goon check config.goon
+
+# Show version
+goon --version
+```
+
+## C API
+
+```c
+#include "goon.h"
+
+// Create context
+Goon_Ctx *ctx = goon_create();
+
+// Load and evaluate
+if (!goon_load_file(ctx, "config.goon")) {
+ const Goon_Error *err = goon_get_error_info(ctx);
+ goon_error_print(err);
+ return 1;
+}
+
+// Get result
+Goon_Value *result = goon_eval_result(ctx);
+
+// Convert to JSON
+char *json = goon_to_json_pretty(result, 2);
+printf("%s\n", json);
+free(json);
+
+// Cleanup
+goon_destroy(ctx);
+```
+
+### Registering Built-in Functions
+
+```c
+Goon_Value *my_builtin(Goon_Ctx *ctx, Goon_Value **args, size_t argc) {
+ // Implementation
+ return goon_int(ctx, 42);
+}
+
+goon_register(ctx, "my_func", my_builtin);
+```
+
+## Future Considerations
+
+The following features may be added in future versions:
+
+- Arithmetic operators (`+`, `-`, `*`, `/`, `%`)
+- Comparison operators (`==`, `!=`, `<`, `>`, `<=`, `>=`)
+- Logical operators (`&&`, `||`, `!`)
+- Optional type annotations
+- Hex integer literals (`0xFF`)
+- Filter in map: `map(list, fn, predicate)`
diff --git a/examples/conditionals.goon b/examples/conditionals.goon
new file mode 100644
index 0000000..0c7f2a7
--- /dev/null
+++ b/examples/conditionals.goon
@@ -0,0 +1,15 @@
+// Conditional expressions
+
+let debug = true;
+let theme = "dark";
+
+{
+ // If-then-else
+ log_level = if debug then "verbose" else "error";
+
+ // Ternary operator
+ background = theme ? "#1a1a1a" : "#ffffff";
+
+ // Nested conditional
+ border_width = if debug then 4 else if theme then 2 else 1;
+}
diff --git a/examples/functions.goon b/examples/functions.goon
new file mode 100644
index 0000000..c50782a
--- /dev/null
+++ b/examples/functions.goon
@@ -0,0 +1,31 @@
+// Arrow functions as config templates
+
+// Simple template
+let color = (hex) => { hex = hex; };
+
+// Multi-parameter template
+let keybind = (mods, key, cmd) => {
+ mods = mods;
+ key = key;
+ cmd = cmd;
+};
+
+// Nested template
+let workspace_key = (n) => keybind("super", n, "workspace ${n}");
+
+{
+ colors = {
+ red = color("#ff0000");
+ green = color("#00ff00");
+ blue = color("#0000ff");
+ };
+
+ keys = [
+ keybind("super", "return", "terminal"),
+ keybind("super", "d", "launcher"),
+ keybind(["super", "shift"], "q", "kill"),
+ workspace_key(1),
+ workspace_key(2),
+ workspace_key(3),
+ ];
+}
diff --git a/examples/interpolation.goon b/examples/interpolation.goon
new file mode 100644
index 0000000..2aa9dd7
--- /dev/null
+++ b/examples/interpolation.goon
@@ -0,0 +1,14 @@
+// String interpolation
+
+let app_name = "goon";
+let version = 1;
+let author = "tony";
+
+{
+ title = "${app_name} v${version}";
+ credit = "Created by ${author}";
+ path = "/home/${author}/.config/${app_name}";
+
+ // Escaping $ with \$
+ template = "Use \${var} for interpolation";
+}
diff --git a/examples/ranges.goon b/examples/ranges.goon
new file mode 100644
index 0000000..ad8ef3f
--- /dev/null
+++ b/examples/ranges.goon
@@ -0,0 +1,19 @@
+// Ranges and map function
+
+let workspaces = [1..9];
+
+let make_ws = (n) => {
+ id = n;
+ name = "workspace ${n}";
+};
+
+{
+ // Simple range
+ numbers = [1..5];
+
+ // Range with map
+ workspace_configs = map(workspaces, make_ws);
+
+ // Inline map with range
+ doubled = map([1..10], (n) => { original = n; value = n; });
+}
diff --git a/examples/spread.goon b/examples/spread.goon
new file mode 100644
index 0000000..68a18c0
--- /dev/null
+++ b/examples/spread.goon
@@ -0,0 +1,17 @@
+// Spread operator for merging records and lists
+
+let defaults = {
+ gap = 10;
+ border = 2;
+ focus_color = "#ff0000";
+};
+
+let base_tags = ["web", "dev", "music"];
+
+{
+ // Spread record - override gap
+ config = { ...defaults; gap = 20; };
+
+ // Spread list - add more tags
+ tags = [...base_tags, "chat", "games"];
+}
diff --git a/examples/wm_config.goon b/examples/wm_config.goon
new file mode 100644
index 0000000..ca1f716
--- /dev/null
+++ b/examples/wm_config.goon
@@ -0,0 +1,64 @@
+// Full window manager config showcasing all goon features
+
+// Templates
+let key = (mods, key, cmd) => { mods = mods; key = key; cmd = cmd; };
+let ws_key = (n) => key("super", n, "workspace ${n}");
+let ws_move = (n) => key(["super", "shift"], n, "move-to-workspace ${n}");
+
+// Theme
+let colors = {
+ bg = "#1a1a2e";
+ fg = "#eaeaea";
+ accent = "#e94560";
+ border_focus = "#e94560";
+ border_normal = "#333333";
+};
+
+// Layout defaults
+let defaults = {
+ gap = 10;
+ border_width = 2;
+ smart_gaps = true;
+};
+
+// Apps
+let terminal = "alacritty";
+let launcher = "rofi -show drun";
+let browser = "firefox";
+
+{
+ // Spread defaults and override
+ ...defaults;
+ gap = 8;
+
+ colors = colors;
+
+ keybinds = [
+ // App launchers
+ key("super", "return", terminal),
+ key("super", "d", launcher),
+ key("super", "b", browser),
+
+ // Window management
+ key("super", "q", "kill"),
+ key("super", "f", "fullscreen"),
+ key("super", "space", "float"),
+
+ // Workspace switching (1-9)
+ ...map([1..9], ws_key),
+
+ // Move to workspace (1-9)
+ ...map([1..9], ws_move),
+ ];
+
+ workspaces = map([1..9], (n) => {
+ id = n;
+ name = if n then "workspace ${n}" else "default";
+ });
+
+ rules = [
+ { class = "Firefox"; workspace = 2; },
+ { class = "Spotify"; workspace = 9; floating = true; },
+ { class = "Steam"; workspace = 8; },
+ ];
+}