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.
As Escher is a so called Conceptual Programming Language, it uses concepts that are very different then what you may know from Object Oriented or Functional Programming, for example. It therefore also uses very different concepts, parts, and names thereof. This section tries to clarify those names.
Declarative unit | Java | Escher | C++ |
---|---|---|---|
Basic declaration unit | class | circuit | class |
Logical group of basic declaration units | package | faculty | namespace |
Variable name + type/interface | variable name + type | gate | variable name + type |
enclosing declataion unit instance | this | super gate | this |
The runtime structure containing all the code | class-path | index | LD_LIBRARY_PATH |
Unique string identifier of a declaration unit | fully-qualified (class-)name | address | fully-qualified (class-)name |
The underlying runtime | JVM | Escher runtime | the OS |
The underlying technology | JVM/C/Assembler | Golang | none/the OS |
A circuit has the strucutre of a graph, consists of nodes, called gates, and edges, called links. Gates have a name and a value:
interface{}
.
A circuits links go 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 (within the circuit) 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/hoijui/escher/pkg/a"
and "github.com/hoijui/escher/pkg/see"
The following Go example illustrates this:
package main import ( "fmt" "github.com/hoijui/escher/pkg/a" "github.com/hoijui/escher/pkg/see" ) func main() { src := "alpha { a 123; b 3.14; a: = b:}\n beta { 1, 2, 3, \"abc\" }" p := a.NewSrcString(src) // create a parsing object for { n, v := see.SeePeer(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.
We use a trick: We use syntactic sugared (empty string named), string valued gates, and — purely to visually indicate a comment — we use "//" in the beginning, or "/*" plus "*/" at the end.
alpha { `// circuit definition` float 1.23 ; `// gate named float with a floating-point value` beta {} ; `// gate named beta with an empty circuit value` `/* * We can also do this: * A multi-line comment within a circuit definition. * Outside the circuit though, no comments are possible. */` }
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:
Type | Represents |
---|---|
Integer | native Go type int |
Floating-point number | native Go type float64 |
Complex number | native Go type complex128 |
String | native Go type string |
Directive | Escher internal Go type Address , representing a sequence of names, written as dot-separated, fully-qualified names |
Circuit | Escher internal Go type Circuit |
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 consecutive 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" } }which is equivalent to:
alpha { 0 *fully.qualified.Name 1 @fully.qualified.Name 2 123 3 3.14 4 (1-3i) 5 "abcd\n\tefgh" 6 ` <html> <div>abc</div> </html> ` 7 { 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. Valid examples:
and:XAndY = not:X and:XAndY= not:X and:XAndY =not:X 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 this example, both sides of the equation are sugared:
*os.Scanln = *os.PrintlnThis will expand to:
0 *os.Scanln 1 *os.Println 0: = 1: