goon

goon

https://git.tonybtw.com/goon.git git://git.tonybtw.com/goon.git

Added documentation, and examples.

Commit
e8a410477f1d4436791467c4261f51337b5da5f7
Parent
a5a8bce
Author
tonybanters <tonyoutoften@gmail.com>
Date
2026-01-06 08:18:34

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; },
+    ];
+}