tsort

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

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 2 Imported by: 0

README

go-ruby-tsort/tsort

tsort — go-ruby-tsort

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of Ruby's TSort standard library — topological sorting and strongly connected components over an arbitrary directed graph, using Tarjan's algorithm exactly as MRI 4.0.5's tsort.rb does. Component grouping, each component's internal ordering, and the reverse-topological order in which components are emitted all match MRI byte-for-byte — including the quirks (a self-loop is a single-node SCC and so does not raise; the TSort::Cyclic message is topological sort failed: [...]).

It is the topological-sort backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-regexp (the Onigmo engine), go-ruby-erb (the ERB compiler), and go-ruby-yaml (the Psych emitter/loader).

What it is. TSort interprets any object as a graph through two callbacks: one that yields every node, one that yields a node's children. This package models that functionally — you supply the two iterators and get back the sorted nodes (or the SCCs). Nodes are arbitrary any values; their graph identity (Ruby's eql? / hash) is host-supplied via Options.Identity, so a host such as rbgo can plug in boxed Ruby objects, while the library's own callers use plain comparable Go values.

Features

Faithful port of tsort.rb, validated against the ruby binary on every supported platform:

  • TSort — nodes sorted children-first (a depends-on bb precedes a); raises *Cyclic on a real cycle (an SCC of size ≥ 2) with MRI's exact message; a self-loop, being a size-1 SCC, returns normally.
  • StronglyConnectedComponents — Tarjan SCCs in reverse topological order; each component's internal order is its DFS-discovery order, matching MRI.
  • EachStronglyConnectedComponent / …From — the iterator forms, including the …From(start, …) variant that walks only the subgraph reachable from a start node and never enumerates the full node set.
  • Host identity & inspectOptions.Identity decides node equality; Options.Inspect renders nodes for the Cyclic message (a built-in Ruby-style #inspect covers the numeric tower, strings, Symbol, booleans, nil, and arrays for the library's own callers).

CGO-free, dependency-free, 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three operating systems (Linux, macOS, Windows).

Install

go get github.com/go-ruby-tsort/tsort

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-tsort/tsort"
)

func main() {
	// {1=>[2,3], 2=>[3], 3=>[], 4=>[]} — model it as two iterators.
	graph := map[int][]int{1: {2, 3}, 2: {3}, 3: {}, 4: {}}
	order := []int{1, 2, 3, 4} // enumeration order (a Ruby Hash is ordered)

	nodes := func(yield func(any)) {
		for _, n := range order {
			yield(n)
		}
	}
	children := func(node any, yield func(any)) {
		for _, c := range graph[node.(int)] {
			yield(c)
		}
	}

	sorted, err := tsort.TSort(nodes, children)
	fmt.Println(sorted, err) // [3 2 1 4] <nil>

	scc := tsort.StronglyConnectedComponents(nodes, children)
	fmt.Println(scc) // [[3] [2] [1] [4]]
}

A cycle returns a *tsort.Cyclic whose message matches MRI:

// {1=>[2], 2=>[3,4], 3=>[2], 4=>[]} → tsort raises TSort::Cyclic
_, err := tsort.TSort(nodes, children)
// err.Error() == "topological sort failed: [2, 3]"

API

// NodesFunc yields every node; ChildrenFunc yields a node's children.
type NodesFunc    = func(yield func(any))
type ChildrenFunc = func(node any, yield func(any))

// TSort returns nodes sorted children-first, or a *Cyclic on a real cycle.
func TSort(nodes NodesFunc, children ChildrenFunc) ([]any, error)

// StronglyConnectedComponents returns SCCs in reverse topological order.
func StronglyConnectedComponents(nodes NodesFunc, children ChildrenFunc) [][]any

// Iterator forms.
func EachStronglyConnectedComponent(nodes NodesFunc, children ChildrenFunc, yield func(component []any))
func EachStronglyConnectedComponentFrom(start any, children ChildrenFunc, yield func(component []any))

// *With variants take Options for host-supplied node identity / inspect.
func TSortWith(nodes NodesFunc, children ChildrenFunc, opts *Options) ([]any, error)
func StronglyConnectedComponentsWith(nodes NodesFunc, children ChildrenFunc, opts *Options) [][]any
func EachStronglyConnectedComponentWith(nodes NodesFunc, children ChildrenFunc, opts *Options, yield func([]any))
func EachStronglyConnectedComponentFromWith(start any, children ChildrenFunc, opts *Options, yield func([]any))

// Options mirrors Ruby's eql?/hash (Identity) and #inspect (Inspect).
type Options struct {
	Identity func(node any) any    // graph-equality key; nil ⇒ node must be comparable
	Inspect  func(node any) string // for the Cyclic message; nil ⇒ built-in Ruby-style
}

// Cyclic is the equivalent of Ruby's TSort::Cyclic.
type Cyclic struct{ Component []any /* … */ }
func (c *Cyclic) Error() string // "topological sort failed: [...]"

// Symbol is a node value that inspects as a Ruby symbol (":name").
type Symbol string

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential MRI oracle: a corpus of fixed and deterministically-random digraphs is sorted both here and by the system ruby (TSort.tsort, TSort.strongly_connected_components, each_strongly_connected_component_from), and the results — including TSort::Cyclic messages — must match byte-for-byte. The oracle scripts $stdout.binmode so Windows text-mode never pollutes the bytes, gate on RUBY_VERSION >= "4.0", and skip themselves where ruby is absent.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-tsort/tsort authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package tsort is a pure-Go (CGO=0), MRI-faithful port of Ruby's TSort stdlib: topological sorting and strongly connected components over an arbitrary directed graph, using Tarjan's algorithm exactly as MRI's tsort.rb does.

A graph is described functionally by two callbacks, mirroring Ruby's tsort_each_node / tsort_each_child:

  • nodes(yield) yields every node in the graph.
  • children(node, yield) yields every child (out-edge target) of node.

Both use the Go range-function shape func(yield func(any)). Nodes are arbitrary any values; equality between two nodes is decided by an Identity function (mirroring Ruby's eql?/hash). When no Identity is supplied the nodes are used as Go map keys directly, which requires them to be comparable.

The traversal order, component grouping and component ordering all match MRI byte-for-byte: each SCC is emitted in reverse topological order (children before parents), and a component's internal order is its DFS discovery order.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func EachStronglyConnectedComponent

func EachStronglyConnectedComponent(nodes NodesFunc, children ChildrenFunc, yield func(component []any))

EachStronglyConnectedComponent invokes yield for each strongly connected component, in reverse topological order. Mirrors Ruby's TSort.each_strongly_connected_component.

func EachStronglyConnectedComponentFrom

func EachStronglyConnectedComponentFrom(start any, children ChildrenFunc, yield func(component []any))

EachStronglyConnectedComponentFrom iterates over the strongly connected components in the subgraph reachable from start. It does NOT enumerate the node set (no NodesFunc), exactly like Ruby's TSort.each_strongly_connected_component_from. Mirrors that method.

func EachStronglyConnectedComponentFromWith

func EachStronglyConnectedComponentFromWith(start any, children ChildrenFunc, opts *Options, yield func(component []any))

EachStronglyConnectedComponentFromWith is EachStronglyConnectedComponentFrom with explicit Options.

func EachStronglyConnectedComponentWith

func EachStronglyConnectedComponentWith(nodes NodesFunc, children ChildrenFunc, opts *Options, yield func(component []any))

EachStronglyConnectedComponentWith is EachStronglyConnectedComponent with explicit Options.

func StronglyConnectedComponents

func StronglyConnectedComponents(nodes NodesFunc, children ChildrenFunc) [][]any

StronglyConnectedComponents returns the strongly connected components as a slice of slices, sorted from children to parents (reverse topological order). Mirrors Ruby's TSort.strongly_connected_components.

func StronglyConnectedComponentsWith

func StronglyConnectedComponentsWith(nodes NodesFunc, children ChildrenFunc, opts *Options) [][]any

StronglyConnectedComponentsWith is StronglyConnectedComponents with explicit Options.

func TSort

func TSort(nodes NodesFunc, children ChildrenFunc) ([]any, error)

TSort returns a topologically sorted slice of nodes, sorted from children to parents: the first element has no child and the last has no parent. It mirrors Ruby's TSort.tsort.

If the graph contains a cycle (an SCC with more than one node) a *Cyclic error is returned. A self-loop forms a single-node SCC and therefore — exactly like MRI — does NOT raise.

func TSortWith

func TSortWith(nodes NodesFunc, children ChildrenFunc, opts *Options) ([]any, error)

TSortWith is TSort with explicit Options.

Types

type ChildrenFunc

type ChildrenFunc = func(node any, yield func(any))

ChildrenFunc yields every child of node, mirroring Ruby's tsort_each_child.

type Cyclic

type Cyclic struct {
	// Component is the strongly connected component (length >= 2) that broke the
	// sort, in the same order MRI would have yielded it.
	Component []any
	// contains filtered or unexported fields
}

Cyclic is raised (returned) when a topological sort encounters a strongly connected component of more than one node, i.e. a true cycle. Its Error string reproduces MRI's message exactly: "topological sort failed: [...]" where the bracketed part is the Ruby #inspect of the offending component.

func (*Cyclic) Error

func (c *Cyclic) Error() string

type NodesFunc

type NodesFunc = func(yield func(any))

NodesFunc yields every node of the graph, mirroring Ruby's tsort_each_node.

type Options

type Options struct {
	// Identity maps a node to a comparable key used for graph-equality (Ruby's
	// eql?/hash). If nil, the node itself is used as the key, which means nodes
	// must be comparable Go values.
	Identity func(node any) any

	// Inspect renders a node the way Ruby's #inspect would, used only to build the
	// Cyclic error message. If nil, a built-in approximation is used.
	Inspect func(node any) string
}

Options tunes how nodes are identified and inspected so that the MRI-faithful behaviour can be reproduced for any host node representation (e.g. rbgo's boxed Ruby objects). The zero Options work for comparable Go nodes inspected with a Go-side approximation of Ruby #inspect.

type Symbol

type Symbol string

Symbol is a node value that inspects as a Ruby symbol (":name"). rbgo binds its own Ruby Symbol; this type lets the library's own tests reproduce MRI's symbol rendering in Cyclic messages.

Jump to

Keyboard shortcuts

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