gsl

package module
v0.4.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 20, 2026 License: GPL-3.0 Imports: 7 Imported by: 0

README

GSL (GSL-Lang)

GSL is a small, declarative language for describing directed graphs with attributes and set-based grouping.

It is designed to be:

  • Human-readable
  • Deterministic
  • Canonicalisable
  • Easy to parse
  • Easy to diff

GSL is not a visual graph language. It is a textual graph representation designed for tooling, transformation, and programmatic analysis.


Table of Contents


What GSL Describes

A GSL document defines:

  • Nodes
  • Directed edges
  • Sets (groupings)
  • Arbitrary attributes on all of the above

Quick Example

# Declare sets
set flow [color="blue"]

# Declare nodes
node A: "Start" @flow
node B [flag]

# Declare edges
A->B [weight=1.2] @flow

This defines:

  • 2 nodes
  • 1 directed edge
  • 1 set
  • Attributes on nodes, edges, and sets

Core Features

Nodes
node A
node B [flag, weight=2]
node C: "Hello"
Edges
A->B
A,B->C
C->D,E

Grouped edges expand automatically.

Duplicate edges are allowed.


Sets and Membership
set cluster [visible]

node A @cluster
A->B @cluster

Sets are named groupings. Membership accumulates across declarations.


Parent Relationships
node C {
    node D
}

This is syntactic sugar for:

node D [parent=C]

parent is treated as a normal attribute.


Design Goals

GSL is designed to:

  • Round-trip cleanly
  • Produce a canonical internal representation
  • Merge repeated declarations
  • Preserve duplicate edges
  • Keep semantics simple and explicit

It intentionally avoids:

  • Edge identity
  • Schema enforcement
  • Graph correctness constraints (acyclicity, tree validity, etc.)

Canonical Behaviour

A compliant parser must ensure:

parse(serialize(parse(input))) == parse(input)

Grouped edges expand. Blocks become explicit parent attributes. Implicit sets are materialised.


Tools

gsl-diagram

Convert GSL graphs to visual diagram formats (Mermaid, PlantUML).

Installation
go build -o gsl-diagram ./cmd/gsl-diagram
Usage
gsl-diagram -i graph.gsl -f mermaid -t component
gsl-diagram -i graph.gsl -f plantuml
cat graph.gsl | gsl-diagram -f mermaid > diagram.mmd

Supported formats:

  • Mermaid: Component diagrams and flowcharts
  • PlantUML: Component diagrams

See cmd/gsl-diagram/README.md for full documentation and examples.


GSL Query Language

GSL includes a query language for selecting, filtering, and transforming graphs using a pipeline-based syntax.

Queries enable you to:

  • Extract subgraphs by filtering nodes and edges
  • Traverse graph neighbourhoods (incoming, outgoing, bidirectional)
  • Assign and remove attributes
  • Merge (collapse) nodes
  • Combine multiple graphs using set operations (union, intersection, difference)
Query Concepts

The core idea is a pipeline of expressions where each expression receives a graph and produces a new graph:

input graph → expr₁ → expr₂ → … → result graph

Expressions are separated by | and evaluated left-to-right, similar to a Unix shell pipeline but for graphs.

Query Quick Start
import (
	gsl "github.com/dnnrly/gsl-lang"
	"github.com/dnnrly/gsl-lang/query"
)

// Parse a query
q, errs := query.ParseQuery(`subgraph node.team == "payments" | remove orphans`)
if len(errs) > 0 {
    log.Fatal(errs)
}

// Serialize back to string
queryStr := query.SerializeQuery(q)
fmt.Println(queryStr)
Pipeline Expressions
Expression Syntax Purpose
Source from * or from NAME Switch the working graph
Subgraph subgraph <predicate> [traverse <dir> <depth>] Filter nodes or edges, optionally traverse
Make make <path> = <value> where <predicate> Assign attributes to matching elements
Remove remove edge where <predicate> Delete matching edges
Remove remove node.<attr> where <predicate> Delete attributes from matching nodes
Remove remove orphans Delete nodes with no incident edges
Collapse collapse into <id> where <predicate> Merge matching nodes into one
Binding (<pipeline>) as NAME Save pipeline result as a named graph
Algebra NAME + NAME2, NAME & NAME2, etc. Combine named graphs (union, intersection, etc.)
Subgraph Filtering (Most Common)

Extract subgraphs by matching nodes or edges:

Node Matching
subgraph node.team == "payments"

Selects all nodes where team equals "payments" and includes edges between matched nodes only.

Edge Matching
subgraph edge.protocol == "grpc"

Selects all edges where protocol is "grpc" and includes their source and target nodes.

Traversal

After matching, optionally explore the graph neighbourhood:

subgraph node.team == "payments" traverse out 1
subgraph node.team == "payments" traverse in all

Directions: in, out, both
Depths: 1, 2, N (hops), or all (unlimited)

Predicates

Predicates filter by attributes, set membership, or existence:

Form Example Meaning
Equality node.team == "payments" Attribute equals value
Inequality node.zone != "C" Attribute does not equal value
Exists node.team exists Attribute is present
Not exists edge.debug not exists Attribute is absent
Set membership node in @critical Node belongs to set
Set non-membership edge not in @deprecated Node does not belong to set
Compound node.team == "payments" AND node.zone == "B" Both conditions true

Important: Cannot mix node. and edge. in one predicate. Only AND is supported (no OR).

Transformation Examples
Example 1: Basic Filtering
subgraph node.team == "payments"

Result: All nodes from the payments team and edges between them.

Example 2: Filtering + Cleanup
subgraph node.team == "payments" | remove orphans

Result: Payments team nodes with any orphaned nodes removed.

Example 3: Traversal + Removal
subgraph node.team == "payments" traverse out 1 | remove edge where edge.protocol == "tcp"

Result: Payments team and their direct outbound neighbours, excluding TCP edges.

Example 4: Node Collapse
subgraph node.zone == "A" | collapse into zone_a_cluster where node.team == "platform"

Result: All nodes in zone A, with platform team nodes merged into a single zone_a_cluster node.

Example 5: Named Graphs + Set Operations
(subgraph node.team == "payments") as PAY
| from *
| (subgraph node.team == "identity") as ID
| PAY + ID

Result: Union of payments team and identity team nodes.

Query Combinators (Graph Algebra)

After binding named graphs, combine them:

GRAPH1 + GRAPH2    # Union: all nodes and edges from both
GRAPH1 & GRAPH2    # Intersection: only shared elements
GRAPH1 - GRAPH2    # Difference: in GRAPH1 but not GRAPH2
GRAPH1 ^ GRAPH2    # Symmetric difference: in exactly one

When the same node appears in both graphs, attributes from the right-hand side overwrite conflicts.

More Examples

For additional examples and detailed explanations, see:

Query Examples
# Select a team and show their dependencies
subgraph node.team == "payments" traverse out all

# Recursive traversal with edge filter
start "Service" | flow out where edge.color = "Blue" recursive

# Filter by node attributes
start A | where status = "active"

# Multiple filters in a pipeline
start A | flow out | where critical = true | where type != "archived"

# Combine pipelines with union
(start A | flow out) union (start B | flow in)

# Complex combinators
(start X | flow out recursive) union (start Y) minus (start Z | flow both)
Query API

Import the query package:

import "github.com/dnnrly/gsl-lang/query"
query.ParseQuery(input string) (*Query, []error)

Parses a query string and returns:

  • *Query: The parsed query AST
  • []error: Parse errors, if any
query.SerializeQuery(q *Query) string

Serializes a query AST back to a query string.

Query AST Structure
type Query struct {
    Root Step  // Entry point of the query
}

type Pipeline struct {
    Steps []Step  // Sequence of pipeline steps
}

type StartStep struct {
    NodeIDs []string  // Starting node IDs
}

type FlowStep struct {
    Direction   string       // "in", "out", "both"
    Recursive   bool         // true if * or "recursive"
    EdgeFilter  *FilterSpec  // Optional edge filter
}

type FilterStep struct {
    Filter *FilterSpec  // Node attribute filter
}

type MinusStep struct {
    Pipeline *Pipeline  // Sub-pipeline to subtract
}

type CombinatorExpr struct {
    Type  string      // "union", "intersect", "minus"
    Left  *Pipeline   // Left operand
    Right *Pipeline   // Right operand
}

type FilterSpec struct {
    IsEdge bool        // true for edge filters, false for node filters
    Attr   string      // Attribute name
    Op     string      // Operator: "=", "!=", "contains", "matches"
    Value  interface{} // Comparison value
}

Using the Library

The GSL library provides a Go API for parsing and manipulating GSL documents programmatically.

For LLMs and AI Agents

If you are an LLM or AI agent that needs to work with GSL, see LLM_GUIDE.md.

The LLM Guide is a self-contained reference that covers:

  • Complete GSL syntax with examples
  • Go API reference with code patterns
  • Algorithm implementations (topological sort, cycle detection, path finding)
  • Best practices and common gotchas

You can copy the entire guide and use it as context for your tasks.

Installation
go get github.com/dnnrly/gsl-lang
Basic Usage
package main

import (
	"bytes"
	"fmt"
	"log"
	"os"

	gsl "github.com/dnnrly/gsl-lang"
)

func main() {
	// Read a GSL file
	content, err := os.ReadFile("graph.gsl")
	if err != nil {
		log.Fatal(err)
	}

	// Parse the GSL
	graph, warnings, err := gsl.Parse(bytes.NewReader(content))
	if err != nil {
		log.Fatal(err)
	}

	// Check for non-fatal warnings
	for _, w := range warnings {
		fmt.Printf("Warning: %v\n", w)
	}

	// Access the graph
	fmt.Printf("Nodes: %d, Edges: %d, Sets: %d\n", 
		len(graph.Nodes), len(graph.Edges), len(graph.Sets))
}
Common Patterns
1. Accessing Nodes
// Get all nodes
for nodeID, node := range graph.Nodes {
	fmt.Printf("Node: %s\n", nodeID)
	
	// Access attributes
	if text, ok := node.Attributes["text"]; ok {
		fmt.Printf("  Text: %v\n", text)
	}
	
	// Check parent relationship
	if parent, ok := node.Attributes["parent"]; ok {
		fmt.Printf("  Parent: %v\n", parent)
	}
}
2. Traversing Edges
// Find all outbound edges from a node
for _, edge := range graph.Edges {
	if edge.From == "NodeA" {
		fmt.Printf("NodeA -> %s\n", edge.To)
		
		// Access edge attributes
		if method, ok := edge.Attributes["method"]; ok {
			fmt.Printf("  Method: %v\n", method)
		}
	}
}
3. Working with Sets
// Find all nodes in a specific set
for nodeID, node := range graph.Nodes {
	if _, isMember := node.Sets["critical"]; isMember {
		fmt.Printf("Critical node: %s\n", nodeID)
	}
}

// Find all edges in a specific set
for _, edge := range graph.Edges {
	if _, isMember := edge.Sets["production"]; isMember {
		fmt.Printf("Production edge: %s -> %s\n", edge.From, edge.To)
	}
}
4. Computing Graph Statistics
// Count nodes by set membership
setCounts := make(map[string]int)
for _, node := range graph.Nodes {
	for setName := range node.Sets {
		setCounts[setName]++
	}
}

for setName, count := range setCounts {
	fmt.Printf("%s: %d nodes\n", setName, count)
}
5. Round-Tripping (Parse → Modify → Serialize)
// Parse
graph, _, err := gsl.Parse(bytes.NewReader(content))
if err != nil {
	log.Fatal(err)
}

// Serialize back to canonical form
canonical := gsl.Serialize(graph)
fmt.Println(canonical)

// The serialized form can be re-parsed to produce an identical graph
graph2, _, _ := gsl.Parse(bytes.NewReader([]byte(canonical)))
// graph and graph2 are semantically equivalent
API Reference
Parse(io.Reader) (*Graph, []error, error)

Parses GSL input and returns:

  • *Graph: The parsed graph structure
  • []error: Non-fatal warnings (implicit set creation, name collisions, etc.)
  • error: Fatal parse error, if any
Serialize(*Graph) string

Serializes a graph to canonical GSL form. The output can be re-parsed to produce an identical graph.

Graph Structure
type Graph struct {
	Nodes map[string]*Node  // Nodes indexed by ID
	Edges []*Edge           // All edges (allows duplicates)
	Sets  map[string]*Set   // Named sets indexed by name
}

type Node struct {
	ID         string                 // Node identifier
	Attributes map[string]interface{} // Key-value attributes
	Sets       map[string]struct{}    // Set membership
	Parent     *string                // Cached parent reference
}

type Edge struct {
	From       string                 // Source node ID
	To         string                 // Target node ID
	Attributes map[string]interface{} // Key-value attributes
	Sets       map[string]struct{}    // Set membership
}

type Set struct {
	ID         string                 // Set identifier
	Attributes map[string]interface{} // Key-value attributes
}
Examples and Tests

The examples/ directory contains:

  • 7 example GSL files demonstrating different graph patterns
  • 10 runnable Example tests showing common usage patterns
  • Documentation of warning types

Run examples:

go test ./examples -v

View example code:

Important Notes
  • Parsing is lenient: Warnings are non-fatal. Parse will succeed even if implicit sets are created or name collisions occur.
  • Canonical form: Serialized output may have different ordering than input but represents the same graph.
  • Graph structure: No validation of graph properties (acyclicity, tree validity, etc.) is performed.
  • Attributes are untyped: All attributes are stored as interface{}. Type assertion is needed for safety.
  • Duplicate edges preserved: The graph preserves multiple edges between the same nodes (multiset).

Reference

The formal language specification is defined in:

Documentation

Index

Constants

This section is empty.

Variables

View Source
var Guides embed.FS

Guides embeds the AI/LLM guide files

Functions

func Parse

func Parse(r io.Reader) (*Graph, *ParseError)

Parse reads a GSL document and produces a Graph. Returns the graph and a ParseError (nil on success). The ParseError distinguishes between fatal errors and non-fatal warnings.

func Serialize

func Serialize(g *Graph) string

Serialize converts a Graph to canonical GSL text. Output is deterministic: sets first (sorted by ID), nodes second (sorted by ID), edges last (in slice order). Attribute keys are sorted alphabetically.

Types

type Edge

type Edge struct {
	From       string
	To         string
	Attributes map[string]interface{}
	Sets       map[string]struct{}
}

Edge represents a directed edge in the graph.

func (*Edge) GetBool

func (e *Edge) GetBool(key string) (bool, bool)

GetBool returns the bool value of an edge attribute. Returns (value, true) if the attribute exists and is a bool. Returns (false, false) if the attribute is missing or not a bool.

func (*Edge) GetInt

func (e *Edge) GetInt(key string) (int64, bool)

GetInt returns the int64 value of an edge attribute. Returns (value, true) if the attribute exists and is a number. Returns (0, false) if the attribute is missing or not a number.

func (*Edge) GetString

func (e *Edge) GetString(key string) (string, bool)

GetString returns the string value of an edge attribute. Returns (value, true) if the attribute exists and is a string. Returns ("", false) if the attribute is missing or not a string.

func (*Edge) SetAttribute

func (e *Edge) SetAttribute(key string, val interface{}) error

SetAttribute sets an attribute on an edge. Returns an error if validation fails.

type Graph

type Graph struct {
	// contains filtered or unexported fields
}

Graph is the top-level semantic model produced by parsing a GSL document.

func NewGraph

func NewGraph() *Graph

NewGraph creates a new empty graph.

func (*Graph) AddEdge

func (g *Graph) AddEdge(from, to string, attrs map[string]interface{}) (*Edge, error)

AddEdge adds an edge to the graph. Returns the created edge and an error if validation fails. Validates that both from and to nodes exist.

func (*Graph) AddExistingEdge

func (g *Graph) AddExistingEdge(edge *Edge) error

AddExistingEdge adds a pre-created edge to the graph, preserving all edge state. Used by graph operations that need to preserve edge attributes and set memberships.

func (*Graph) AddExistingNode

func (g *Graph) AddExistingNode(node *Node) error

AddExistingNode adds a pre-created node to the graph, preserving all node state. Used by graph operations that need to preserve node attributes and set memberships.

func (*Graph) AddExistingSet

func (g *Graph) AddExistingSet(set *Set) error

AddExistingSet adds a pre-created set to the graph, preserving all set state. Used by graph operations that need to preserve set attributes.

func (*Graph) AddNode

func (g *Graph) AddNode(id string, attrs map[string]interface{}) (*Node, error)

AddNode adds or updates a node in the graph. Returns the created/updated node and an error if validation fails. If a node with the same ID already exists, it is returned and no error occurs.

func (*Graph) AddSet

func (g *Graph) AddSet(id string, attrs map[string]interface{}) (*Set, error)

AddSet adds or updates a set in the graph. Returns the created/updated set and an error if validation fails. If a set with the same ID already exists, it is returned and no error occurs.

func (*Graph) Clone

func (g *Graph) Clone() *Graph

Clone creates a deep copy of the graph. All nodes, edges, and sets are copied with their attributes and set memberships. The cloned graph is independent: mutations to the clone do not affect the original.

func (*Graph) GetEdges

func (g *Graph) GetEdges() []*Edge

GetEdges returns a read-only copy of the graph's edge slice. Changes to the returned slice do not affect the graph.

func (*Graph) GetNode

func (g *Graph) GetNode(id string) *Node

GetNode returns the node with the given ID, or nil if not found.

func (*Graph) GetNodes

func (g *Graph) GetNodes() map[string]*Node

GetNodes returns a read-only copy of the graph's node map. Changes to the returned map do not affect the graph.

func (*Graph) GetSets

func (g *Graph) GetSets() map[string]*Set

GetSets returns a read-only copy of the graph's set map. Changes to the returned map do not affect the graph.

func (*Graph) RemoveEdge

func (g *Graph) RemoveEdge(from, to string) error

RemoveEdge removes an edge from the graph. Returns an error if the edge does not exist.

func (*Graph) RemoveNode

func (g *Graph) RemoveNode(id string) error

RemoveNode removes a node from the graph. Returns an error if the node does not exist or has dangling edges.

func (*Graph) SetInternalState

func (g *Graph) SetInternalState(nodes map[string]*Node, edges []*Edge, sets map[string]*Set)

SetInternalState sets the internal state of a graph. This is a testing-only method and should not be used in production code. It directly sets nodes, edges, and sets without validation.

type Lexer

type Lexer struct {
	// contains filtered or unexported fields
}

func NewLexer

func NewLexer(r io.Reader) (*Lexer, error)

func (*Lexer) NextToken

func (l *Lexer) NextToken() Token

func (*Lexer) Tokenize

func (l *Lexer) Tokenize() []Token

type Node

type Node struct {
	ID         string
	Attributes map[string]interface{}
	Sets       map[string]struct{}
	Parent     *string // cached from Attributes["parent"] if it's a NodeRef
}

Node represents a node in the graph.

func (*Node) GetBool

func (n *Node) GetBool(key string) (bool, bool)

GetBool returns the bool value of a node attribute. Returns (value, true) if the attribute exists and is a bool. Returns (false, false) if the attribute is missing or not a bool.

func (*Node) GetInt

func (n *Node) GetInt(key string) (int64, bool)

GetInt returns the int64 value of a node attribute. Returns (value, true) if the attribute exists and is a number. Returns (0, false) if the attribute is missing or not a number.

func (*Node) GetRef

func (n *Node) GetRef(key string) (*NodeRef, bool)

GetRef returns the NodeRef value of a node attribute. Returns (value, true) if the attribute exists and is a NodeRef. Returns (nil, false) if the attribute is missing or not a NodeRef.

func (*Node) GetString

func (n *Node) GetString(key string) (string, bool)

GetString returns the string value of a node attribute. Returns (value, true) if the attribute exists and is a string. Returns ("", false) if the attribute is missing or not a string.

func (*Node) SetAttribute

func (n *Node) SetAttribute(key string, val interface{}) error

SetAttribute sets an attribute on a node. Returns an error if validation fails.

type NodeRef

type NodeRef string

NodeRef is a distinct type for node references in attribute values.

type ParseError

type ParseError struct {
	Message  string  // Main error message (empty if no fatal error)
	Warnings []error // Non-fatal parse issues
	Err      error   // Fatal error, if any
}

ParseError represents a structured error from parsing GSL code. It contains a fatal error (if any) and non-fatal warnings discovered during parsing.

func (*ParseError) Error

func (pe *ParseError) Error() string

Error implements the error interface for ParseError.

func (*ParseError) HasError

func (pe *ParseError) HasError() bool

HasError returns true if there was a fatal error.

func (*ParseError) HasWarnings

func (pe *ParseError) HasWarnings() bool

HasWarnings returns true if there were any non-fatal warnings.

type Set

type Set struct {
	ID         string
	Attributes map[string]interface{}
	// contains filtered or unexported fields
}

Set represents a named set/grouping.

func (*Set) GetBool

func (s *Set) GetBool(key string) (bool, bool)

GetBool returns the bool value of a set attribute. Returns (value, true) if the attribute exists and is a bool. Returns (false, false) if the attribute is missing or not a bool.

func (*Set) GetInt

func (s *Set) GetInt(key string) (int64, bool)

GetInt returns the int64 value of a set attribute. Returns (value, true) if the attribute exists and is a number. Returns (0, false) if the attribute is missing or not a number.

func (*Set) GetString

func (s *Set) GetString(key string) (string, bool)

GetString returns the string value of a set attribute. Returns (value, true) if the attribute exists and is a string. Returns ("", false) if the attribute is missing or not a string.

func (*Set) SetAttribute

func (s *Set) SetAttribute(key string, val interface{}) error

SetAttribute sets an attribute on a set. Returns an error if validation fails.

type Token

type Token struct {
	Type    TokenType
	Literal string
	Line    int
	Column  int
}

type TokenType

type TokenType int
const (
	// Special
	TOKEN_ILLEGAL TokenType = iota
	TOKEN_EOF

	// Literals
	TOKEN_IDENT
	TOKEN_STRING
	TOKEN_NUMBER

	// Keywords
	TOKEN_NODE
	TOKEN_SET
	TOKEN_TRUE
	TOKEN_FALSE

	// Symbols
	TOKEN_LBRACKET // [
	TOKEN_RBRACKET // ]
	TOKEN_LBRACE   // {
	TOKEN_RBRACE   // }
	TOKEN_COMMA    // ,
	TOKEN_EQUALS   // =
	TOKEN_ARROW    // ->
	TOKEN_AT       // @
	TOKEN_COLON    // :
)

func (TokenType) String

func (t TokenType) String() string

Directories

Path Synopsis
cmd
gsl-diagram command
gsl-query command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL