setlang

package module
v0.0.0-...-c40c2f6 Latest Latest
Warning

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

Go to latest
Published: Jan 18, 2026 License: Unlicense Imports: 4 Imported by: 0

README

setlang - A Generic Set Expression Language

setlang is a standalone Go library for parsing and evaluating set expressions. It provides a flexible, type-safe way to query and manipulate sets using an intuitive expression language inspired by Jujutsu's revset and fileset languages.

Features

  • Generic: Works with any comparable type (strings, ints, custom types, etc.)
  • Extensible: Consumer-defined functions for domain-specific operations
  • Type-safe: Uses Go generics for compile-time type safety
  • Well-tested: Comprehensive test coverage
  • Fast parsing: Built on participle for efficient parsing
  • Clean API: Simple Context interface for customization

Language Syntax

Operators
  • | - Union (set A or set B)
  • & - Intersection (set A and set B)
  • ~ - Difference (set A but not set B)

Precedence: ~ (highest) > & > | (lowest)

Expressions
a | b           # Union of sets a and b
a & b           # Intersection of sets a and b
a ~ b           # Difference: items in a but not in b
(a | b) & c     # Parentheses for grouping
all()           # Function call with no arguments
status(open)    # Function call with identifier
title("bug")    # Function call with string literal
filter(a | b, "predicate")  # Function call with expression and string
Function Arguments

Function arguments can be:

  1. Bare identifiers: status(done) - passed as identifier name to function
  2. String literals: title("hello") - passed as string value
  3. Set expressions: filter(a | b) - evaluated and passed as a set

Note: Bare identifiers in function arguments are NOT evaluated as expressions. To pass an evaluated set, use a set operation like filter((a)) or filter(a | empty()).

Installation

go get github.com/neongreen/mono/lib/setlang

Quick Start

package main

import (
	"fmt"
	"github.com/neongreen/mono/lib/setlang"
)

func main() {
	// Create a context with some predefined sets
	ctx := setlang.NewMapContext[string]()
	ctx.SetIdent("bugs", setlang.NewSetFrom("bug-1", "bug-2", "bug-3"))
	ctx.SetIdent("features", setlang.NewSetFrom("feat-1", "feat-2"))
	ctx.SetIdent("done", setlang.NewSetFrom("bug-1", "feat-1"))

	// Define a function
	ctx.SetFunc("all", func(args []setlang.FuncArg[string]) (*setlang.Set[string], error) {
		return setlang.NewSetFrom("bug-1", "bug-2", "bug-3", "feat-1", "feat-2"), nil
	})

	// Parse and evaluate an expression
	result, err := setlang.Eval(ctx, "(bugs | features) - done")
	if err != nil {
		panic(err)
	}

	// Use the result
	for _, item := range result.Items() {
		fmt.Println(item)
	}
	// Output:
	// bug-2
	// bug-3
	// feat-2
}

Usage

1. Define Your Context

Implement the Context interface to provide identifier lookup and function implementations:

type Context[T comparable] interface {
	LookupIdent(name string) (*Set[T], error)
	CallFunc(name string, args []FuncArg[T]) (*Set[T], error)
}

Or use the provided MapContext for simple cases:

ctx := setlang.NewMapContext[MyType]()
ctx.SetIdent("mySet", setlang.NewSetFrom(item1, item2, item3))
ctx.SetFunc("myFunc", func(args []setlang.FuncArg[MyType]) (*setlang.Set[MyType], error) {
	// Your function implementation
	return result, nil
})
2. Parse Expressions
// Parse an expression into an AST
expr, err := setlang.Parse("a | b & c")
if err != nil {
	// Handle parse error
}
3. Evaluate Expressions
// Evaluate using a context
eval := setlang.NewEvaluator(ctx)
result, err := eval.Eval(expr)
if err != nil {
	// Handle evaluation error
}

// Or use the convenience function
result, err := setlang.Eval(ctx, "a | b & c")
4. Work with Sets
// Create sets
set1 := setlang.NewSet[int]()
set2 := setlang.NewSetFrom(1, 2, 3)

// Add/remove items
set1.Add(42)
set1.Remove(42)

// Check membership
if set2.Has(2) {
	// ...
}

// Set operations
union := set1.Union(set2)
intersection := set1.Intersect(set2)
difference := set1.Diff(set2)

// Get items
items := set2.Items() // []int

Implementing Functions

Functions receive arguments as FuncArg[T] which can be:

  • A string literal (StrVal)
  • An identifier (Ident)
  • An evaluated set (Set)

Example function that filters by a predicate:

ctx.SetFunc("filter", func(args []setlang.FuncArg[string]) (*setlang.Set[string], error) {
	if len(args) != 2 {
		return nil, fmt.Errorf("filter() takes 2 arguments")
	}

	// First argument: a set expression
	set, err := args[0].GetSet()
	if err != nil {
		return nil, fmt.Errorf("first argument must be a set: %w", err)
	}

	// Second argument: a string predicate
	predicate, err := args[1].GetString()
	if err != nil {
		return nil, fmt.Errorf("second argument must be a string: %w", err)
	}

	// Apply filter
	result := setlang.NewSet[string]()
	for _, item := range set.Items() {
		if matchesPredicate(item, predicate) {
			result.Add(item)
		}
	}
	return result, nil
})

Example function that takes an identifier:

ctx.SetFunc("status", func(args []setlang.FuncArg[Task]) (*setlang.Set[Task], error) {
	if len(args) != 1 {
		return nil, fmt.Errorf("status() takes 1 argument")
	}

	// Get the status name (not evaluated as an expression)
	statusName, err := args[0].GetIdent()
	if err != nil {
		// Maybe it's a string literal instead
		statusName, err = args[0].GetString()
		if err != nil {
			return nil, fmt.Errorf("argument must be an identifier or string")
		}
	}

	// Return all tasks with that status
	return getTasksByStatus(statusName), nil
})

Advanced Example

package main

import (
	"fmt"
	"strings"
	"github.com/neongreen/mono/lib/setlang"
)

type Task struct {
	ID     string
	Status string
	Labels []string
}

type TaskContext struct {
	tasks map[string]*Task
}

func (tc *TaskContext) LookupIdent(name string) (*setlang.Set[string], error) {
	// Special identifiers
	switch name {
	case "all":
		result := setlang.NewSet[string]()
		for id := range tc.tasks {
			result.Add(id)
		}
		return result, nil
	default:
		return nil, fmt.Errorf("unknown identifier: %s", name)
	}
}

func (tc *TaskContext) CallFunc(name string, args []setlang.FuncArg[string]) (*setlang.Set[string], error) {
	switch name {
	case "status":
		if len(args) != 1 {
			return nil, fmt.Errorf("status() takes 1 argument")
		}
		statusName, _ := args[0].GetIdent()
		if statusName == "" {
			statusName, _ = args[0].GetString()
		}

		result := setlang.NewSet[string]()
		for id, task := range tc.tasks {
			if task.Status == statusName {
				result.Add(id)
			}
		}
		return result, nil

	case "label":
		if len(args) != 1 {
			return nil, fmt.Errorf("label() takes 1 argument")
		}
		labelName, _ := args[0].GetIdent()
		if labelName == "" {
			labelName, _ = args[0].GetString()
		}

		result := setlang.NewSet[string]()
		for id, task := range tc.tasks {
			for _, label := range task.Labels {
				if label == labelName {
					result.Add(id)
					break
				}
			}
		}
		return result, nil

	case "title":
		if len(args) != 1 {
			return nil, fmt.Errorf("title() takes 1 argument")
		}
		pattern, err := args[0].GetString()
		if err != nil {
			return nil, err
		}

		result := setlang.NewSet[string]()
		for id, task := range tc.tasks {
			if strings.Contains(task.ID, pattern) {
				result.Add(id)
			}
		}
		return result, nil

	default:
		return nil, fmt.Errorf("unknown function: %s", name)
	}
}

func main() {
	ctx := &TaskContext{
		tasks: map[string]*Task{
			"task-1": {ID: "task-1", Status: "open", Labels: []string{"bug", "urgent"}},
			"task-2": {ID: "task-2", Status: "open", Labels: []string{"feature"}},
			"task-3": {ID: "task-3", Status: "done", Labels: []string{"bug"}},
		},
	}

	// Find all open bugs
	result, err := setlang.Eval(ctx, "status(open) & label(bug)")
	if err != nil {
		panic(err)
	}

	fmt.Println("Open bugs:", result.Items())
	// Output: Open bugs: [task-1]
}

Testing

The library includes comprehensive tests:

  • Unit tests: Test individual components and features
  • Property tests: Verify algebraic properties using rapid
    • Set operation properties (commutativity, associativity, distributivity)
    • De Morgan's laws
    • Identity and absorption laws
    • Parser/evaluator properties (determinism, precedence, etc.)

Run all tests:

cd lib/setlang
go test -v

Run only property tests:

go test -v -run TestProperty

Property tests run 100 randomized test cases by default, providing high confidence in correctness.

License

This library is part of the mono repository and follows its license.

Comparison with Jujutsu

See JJ_COMPARISON.md for a detailed analysis of how this library compares to Jujutsu's revset/fileset languages, including:

  • Feature comparison table
  • Missing features and workarounds
  • Recommendations for building a JJ clone in Go

Acknowledgments

Inspired by Jujutsu's revset and fileset languages, which provide an elegant way to query version control history.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Arg

type Arg struct {
	Expr   *Expr   `  @@`
	StrLit *string `| @String`
	Ident  *string `| @Ident`
}

Arg represents a function argument, which can be either: - A nested expression (for functions that take set expressions as arguments) - A string literal (for functions that take string parameters) - An identifier (for simple arguments)

func (*Arg) String

func (a *Arg) String() string

type Context

type Context[T comparable] interface {
	// LookupIdent resolves an identifier to a set.
	// Returns an error if the identifier is unknown.
	LookupIdent(name string) (*Set[T], error)

	// CallFunc calls a function with the given arguments.
	// Args can be either string literals or evaluated sets.
	// Returns an error if the function is unknown or arguments are invalid.
	CallFunc(name string, args []FuncArg[T]) (*Set[T], error)
}

Context provides the evaluation context for set expressions. Consumers of the library implement this interface to provide application-specific behavior for identifier lookup and function calls.

type DiffExpr

type DiffExpr struct {
	Left  *Primary    `@@`
	Right []*DiffTail `@@*`
}

DiffExpr represents difference operations (~). Grammar: Primary ("~" Primary)*

func (*DiffExpr) String

func (d *DiffExpr) String() string

type DiffTail

type DiffTail struct {
	Op    string   `@"~"`
	Right *Primary `@@`
}

DiffTail represents the right-hand side of a difference operation.

type Evaluator

type Evaluator[T comparable] struct {
	// contains filtered or unexported fields
}

Evaluator evaluates set expressions using a provided context.

func NewEvaluator

func NewEvaluator[T comparable](ctx Context[T]) *Evaluator[T]

NewEvaluator creates a new evaluator with the given context.

func (*Evaluator[T]) Eval

func (e *Evaluator[T]) Eval(expr *Expr) (*Set[T], error)

Eval evaluates an expression and returns the resulting set.

type Expr

type Expr struct {
	Union *UnionExpr `@@`
}

Expr is the top-level expression node. Grammar: UnionExpr

func MustParse

func MustParse(input string) *Expr

MustParse is a convenience function that creates a new parser and parses the input, panicking on error.

func Parse

func Parse(input string) (*Expr, error)

Parse is a convenience function that creates a new parser and parses the input.

func (*Expr) String

func (e *Expr) String() string

String returns a string representation of an expression. This is useful for debugging.

type FuncArg

type FuncArg[T comparable] struct {
	// StrVal is non-nil if the argument is a string literal
	StrVal *string

	// Ident is non-nil if the argument is an identifier
	Ident *string

	// Set is non-nil if the argument is an evaluated set expression
	Set *Set[T]
}

FuncArg represents an argument to a function call. It can be either a string literal or an evaluated set expression.

func (FuncArg[T]) GetIdent

func (a FuncArg[T]) GetIdent() (string, error)

GetIdent returns the identifier value, or an error if not an identifier.

func (FuncArg[T]) GetSet

func (a FuncArg[T]) GetSet() (*Set[T], error)

GetSet returns the set value, or an error if not a set.

func (FuncArg[T]) GetString

func (a FuncArg[T]) GetString() (string, error)

GetString returns the string value, or an error if not a string.

func (FuncArg[T]) IsIdent

func (a FuncArg[T]) IsIdent() bool

IsIdent returns true if this argument is an identifier.

func (FuncArg[T]) IsSet

func (a FuncArg[T]) IsSet() bool

IsSet returns true if this argument is an evaluated set expression.

func (FuncArg[T]) IsString

func (a FuncArg[T]) IsString() bool

IsString returns true if this argument is a string literal.

type FuncCall

type FuncCall struct {
	Name string `@Ident`
	Args []*Arg `"(" ( @@ ( "," @@ )* )? ")"`
}

FuncCall represents a function call with arguments. Grammar: Ident "(" (Arg ("," Arg)*)? ")"

func (*FuncCall) String

func (f *FuncCall) String() string

type IntersectExpr

type IntersectExpr struct {
	Left  *DiffExpr        `@@`
	Right []*IntersectTail `@@*`
}

IntersectExpr represents intersection operations (&). Grammar: DiffExpr ("&" DiffExpr)*

func (*IntersectExpr) String

func (i *IntersectExpr) String() string

type IntersectTail

type IntersectTail struct {
	Op    string    `@"&"`
	Right *DiffExpr `@@`
}

IntersectTail represents the right-hand side of an intersection operation.

type MapContext

type MapContext[T comparable] struct {
	Idents map[string]*Set[T]
	Funcs  map[string]func([]FuncArg[T]) (*Set[T], error)
}

MapContext is a simple implementation of Context that uses maps for lookups. This is useful for testing and simple use cases.

func NewMapContext

func NewMapContext[T comparable]() *MapContext[T]

NewMapContext creates a new MapContext.

func (*MapContext[T]) CallFunc

func (m *MapContext[T]) CallFunc(name string, args []FuncArg[T]) (*Set[T], error)

CallFunc implements Context.CallFunc.

func (*MapContext[T]) LookupIdent

func (m *MapContext[T]) LookupIdent(name string) (*Set[T], error)

LookupIdent implements Context.LookupIdent.

func (*MapContext[T]) SetFunc

func (m *MapContext[T]) SetFunc(name string, fn func([]FuncArg[T]) (*Set[T], error))

SetFunc registers a function.

func (*MapContext[T]) SetIdent

func (m *MapContext[T]) SetIdent(name string, set *Set[T])

SetIdent registers an identifier with a set.

type Node

type Node interface {
	// contains filtered or unexported methods
}

Node is an interface that all AST nodes implement. This allows for uniform handling of different node types during evaluation.

type Parser

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

Parser is a parser for the set language.

func NewParser

func NewParser() (*Parser, error)

NewParser creates a new parser for the set language.

func (*Parser) MustParse

func (p *Parser) MustParse(input string) *Expr

MustParse parses an input string and panics on error. This is useful for testing.

func (*Parser) Parse

func (p *Parser) Parse(input string) (*Expr, error)

Parse parses an input string into an AST.

type Primary

type Primary struct {
	FuncCall *FuncCall `  @@`
	Ident    *string   `| @Ident`
	SubExpr  *Expr     `| "(" @@ ")"`
}

Primary represents atomic expressions: function calls, identifiers, or parenthesized expressions.

func (*Primary) String

func (p *Primary) String() string

type Set

type Set[T comparable] struct {
	// contains filtered or unexported fields
}

Set represents a set of items of type T. It uses a map for O(1) membership testing and efficient set operations.

func Eval

func Eval[T comparable](ctx Context[T], input string) (*Set[T], error)

Eval is a convenience function that parses and evaluates an expression.

func NewSet

func NewSet[T comparable]() *Set[T]

NewSet creates a new empty set.

func NewSetFrom

func NewSetFrom[T comparable](items ...T) *Set[T]

NewSetFrom creates a new set containing the given items.

func (*Set[T]) Add

func (s *Set[T]) Add(item T)

Add adds an item to the set.

func (*Set[T]) Clone

func (s *Set[T]) Clone() *Set[T]

Clone returns a shallow copy of the set.

func (*Set[T]) Diff

func (s *Set[T]) Diff(other *Set[T]) *Set[T]

Diff returns a new set containing items in s but not in other.

func (*Set[T]) Has

func (s *Set[T]) Has(item T) bool

Has returns true if the item is in the set.

func (*Set[T]) Intersect

func (s *Set[T]) Intersect(other *Set[T]) *Set[T]

Intersect returns a new set containing only items present in both sets.

func (*Set[T]) IsEmpty

func (s *Set[T]) IsEmpty() bool

IsEmpty returns true if the set is empty.

func (*Set[T]) Items

func (s *Set[T]) Items() []T

Items returns a slice of all items in the set. The order is not guaranteed.

func (*Set[T]) Remove

func (s *Set[T]) Remove(item T)

Remove removes an item from the set.

func (*Set[T]) Size

func (s *Set[T]) Size() int

Size returns the number of items in the set.

func (*Set[T]) Union

func (s *Set[T]) Union(other *Set[T]) *Set[T]

Union returns a new set containing all items from both sets.

type UnionExpr

type UnionExpr struct {
	Left  *IntersectExpr `@@`
	Right []*UnionTail   `@@*`
}

UnionExpr represents union operations (|). Grammar: IntersectExpr ("|" IntersectExpr)*

func (*UnionExpr) String

func (u *UnionExpr) String() string

type UnionTail

type UnionTail struct {
	Op    string         `@"|"`
	Right *IntersectExpr `@@`
}

UnionTail represents the right-hand side of a union operation.

Jump to

Keyboard shortcuts

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