At heart Escher is a Go package that parses a simple written syntax into a labeled graph data structure, called a circuit. If you view XML as a syntax that represents labeled trees, then Escher would be a syntax that represents labeled graphs.
A circuit consists of nodes, called gates, which have
a name and a value. Names are strings or integers. Gates have unique names
within a circuit. Values are anything representable by the underlying
technology, which for our implementation means any Go value, equivalently, interface{}
.
Additionally, a circuit has a set of links across pairs of gates. A link has two endpoints, called vectors. Each vector consists of a gate name and a valve name. Vectors do not overlap in the sense that all vectors with the same gate name have unique valve names.
Circuits have a standard visual representation that fully captures the internal structure of the circuit, which consists of the gate names and links and excludes the gate values—the external structure.
To draw a circuit we start with a solid black oval, denoting the circuit's internal name space. White ovals—contained inside the black one and mutually non-overlapping—denote gates.
Links are depicted as white lines that connect the outlines of gate ovals. Link endpoints connecting to the super gate are attached to the outline of the surrounding black oval.
Valve names are written in white within the black oval, next to their respective visual connection point. Connection points where valve names are visually missing correspond to empty-string valves.
The visual space inside the white gate ovals is reserved for the visual symbolic representation of that value, whatever it might be. If that value is primitive (integer, float, complex, string, directive), we just write it out in black text in the center of the oval. If that value is a circuit, we draw the symbolism for that circuit within the white oval recursively, but this time we switch white and black colors everywhere.
Within the Go runtime, circuits are represented by a dedicated type Circuit
,
whose definition is
type Circuit struct { Gate map[Name]Value Flow map[Name]map[Name]Vector } type Vector struct { Gate Name Valve Name } type Name interface{} type Value interface{}
Type Name
designates string
or int
.
Type Value
designates any Go value.
Using the Escher parser is very simple, in three steps:
"github.com/gocircuit/escher/see"
The following example illustrates this:
package main import ( "fmt" "github.com/gocircuit/escher/see" ) func main() { src = "alpha { a 123; b 3.14; a: = b:}\n beta { 1, 2, 3, \"abc\" }" p := see.NewSrcString(src) // create a parsing object for { n, v := see.See(p) // parse one circuit at a time if v == nil { break } fmt.Printf("%v %v\n", n, v) } }Note that parsing errors result in panics.
A definition starts with a circuit name followed by a circuit description inside brackets. The name is an alpha-numeric identifier. For instance,
alpha { … }
Between the brackets, one can have any number of statements which are of two kinds: gates and links. Statements are separated by new lines, commas or semi-colons.
Go-style end-of-line comments are allowed everywhere.
alpha { // circuit definition float 1.23 // gate named float with a floating-point value beta {} // gate named beta with an empty circuit value }
Gate statements begin on a new line with a gate name identifier, space, and a gate value expression. There are six value types that can be expressed:
The first four correspond to the Go types int
, float64
, complex128
and string
and are expressed using the same syntax.
Addresses have a dedicated Go type Address
. They represent a sequence of names and are
written as dot-separated fully-qualified names. Finally, circuits—whose dedicated Go type is Circuit
—
can be values of gates as well.
For instance,
alpha { directive1 *fully.qualified.Name directive2 @fully.qualified.Name integral 123 floating 3.14 complex (1-3i) quoted "abcd\n\tefgh" backquoted ` <html> <div>abc</div> </html> ` }
Gate values can be circuits themselves,
alpha { beta { Hello World Foo "Bar" } }
Gate names can be omitted in circuit definitions, in which case gates are assigned consequtive integral names, starting from zero. We call the resulting circuits series.
alpha { *fully.qualified.Name @fully.qualified.Name 123 3.14 (1-3i) "abcd\n\tefgh" ` <html> <div>abc</div> </html> ` { A 1 B "C" } }
Circuit links are semantically symmetric. A link is a pair of two vectors, and a vector consists of a gate name and a valve name.
Vectors are written as the gate name, followed by :
(the colon sign),
followed by the valve name. Links are written as a vector, followed by optional whitespace,
followed by =
(the equals sign), followed by another optional whitespace and
the second vector. For instance,
and:XAndY = not:X
A few idioms are commonly useful:
The super gate has a distinguished role in some contexts. For instance, when materializing circuits, the links connected to the super gate are exposed to the higher-level “super” circuit.
For instance, it is a common pattern to name the output valve of materializable circuits after the empty string. The default valve of the super gate, on the other hand, is a way of taking advantage of Escher's syntactic sugar rule.
Here is a comprehensive example of link definitions:
Nand { and *binary.And not *binary.Not and:X = :X and:Y = :Y and:XAndY = not:Z not:NotZ = : }
When circuits are used to represent programs—in other words, executable code—it is common to include a gate and then link to its default valve. To reduce verbosity in this case, link definitions support a piece of syntactic sugar.
Either (or both) vectors in a link definition can be substituted for a gate value. This will be expanded into a gate definition with an automatically-generated name and a link to its default gate in sugar-free syntax. For example,
sum:X = 123Will be expanded into
0 123 sum:Summand = 0:
In another example both sides of the equation are sugared:
*os.Scanln = *os.PrintlnThis will expand to:
0 *os.Scanln 1 *os.Println 0: = 1: