Skip to content

Core Concepts

Grammar School follows a consistent conceptual model across all language implementations.

The Pipeline

Every DSL program flows through this pipeline:

DSL Code (string)
Parse → CallChain (AST)
Interpret → Execute methods directly

Note: Internally, Grammar School maintains a two-layer architecture (Grammar/Runtime), but this is hidden from users. Methods execute directly when called.

DSL Program

A DSL program is a plain string, typically generated by an LLM:

track(name="Drums").add_clip(start=0, length=8)
Cmaj7(1,1), Fmaj7(2,1)
tracks().filter(name~="FX").mute()

Abstract Syntax Tree (AST)

Grammar School uses a simple AST structure:

  • Value: Represents a value (number, string, identifier, bool)
  • Arg: A named argument with a value
  • Call: A function call with named arguments
  • CallChain: A chain of calls connected by dots (method chaining)

Example AST

For the code greet(name="Alice", count=2):

CallChain
  └─ Call(name="greet")
      ├─ Arg(name="name", value=Value(kind="string", value="Alice"))
      └─ Arg(name="count", value=Value(kind="number", value=2))

Methods

Methods are DSL handlers that contain the actual implementation:

class MusicDSL(Grammar):
    def __init__(self):
        super().__init__()
        self.tracks = []

    @method
    def track(self, name, color=None):
        track = {"name": name, "color": color}
        self.tracks.append(track)
        print(f"Created track: {name}")
func (d *MyDSL) Track(args gs.Args, ctx *gs.Context) ([]gs.Action, *gs.Context, error) {
    name := args["name"].Str
    color := ""
    if c, ok := args["color"]; ok {
        color = c.Str
    }
    action := gs.Action{
        Kind: "create_track",
        Payload: map[string]interface{}{
            "name":  name,
            "color": color,
        },
    }
    return []gs.Action{action}, ctx, nil
}

Execution

Methods execute directly when called - no Runtime needed:

class MusicDSL(Grammar):
    @method
    def track(self, name):
        # Implementation runs directly
        print(f"Created track: {name}")

dsl = MusicDSL()
dsl.execute('track(name="Drums")')  # Prints: Created track: Drums
type MyRuntime struct{}

func (r *MyRuntime) ExecuteAction(ctx context.Context, a gs.Action) error {
    switch a.Kind {
    case "create_track":
        // Create a track...
    }
    return nil
}

State Management

State is managed using self attributes in your Grammar class:

class MusicDSL(Grammar):
    def __init__(self):
        super().__init__()
        self.tracks = []
        self.current_track = None

    @method
    def track(self, name):
        self.current_track = {"name": name}
        self.tracks.append(self.current_track)

    @method
    def add_clip(self, start, length):
        if self.current_track:
            # Access state via self
            self.current_track["clips"].append({"start": start, "length": length})
func (d *MyDSL) Track(args gs.Args, ctx *gs.Context) ([]gs.Action, *gs.Context, error) {
    // ctx contains previous context
    newCtx := gs.NewContext()
    newCtx.Set("last_track", name)
    return []gs.Action{action}, newCtx, nil
}

Grammar

Grammars define the syntax of your DSL. Grammar School provides a default grammar, but you can customize it:

custom_grammar = """
start: call_chain
call_chain: call ('.' call)*
# ... more rules
"""
class MyDSL(Grammar):
    @method
    def greet(self, name):
        print(f"Hello, {name}!")

dsl = MyDSL(grammar=custom_grammar)
customGrammar := `
start: call_chain
call_chain: call ('.' call)*
// ... more rules
`
engine, _ := gs.NewEngine(customGrammar, dsl, parser)