diff --git a/cmd/eval/main.go b/cmd/eval/main.go index b696508..4c3d6e5 100644 --- a/cmd/eval/main.go +++ b/cmd/eval/main.go @@ -26,7 +26,7 @@ func main() { res, err := vm.EvaluateSource( bytes.NewBufferString(opSrc), - vm.Value{Value: inVal}, + vm.Value{Value: inVal.Value}, vm.GlobalScope, ) if err != nil { diff --git a/gg/decoder.go b/gg/decoder.go index 8d7e223..6d41d80 100644 --- a/gg/decoder.go +++ b/gg/decoder.go @@ -1,73 +1,310 @@ package gg import ( - "bufio" "fmt" "io" + "log" + "strconv" + "unicode" + + . "github.com/mediocregopher/ginger/gg/grammar" + "github.com/mediocregopher/ginger/graph" + "golang.org/x/exp/slices" ) -// Decoder reads Value's off of a byte stream. -type Decoder struct { - br *bufio.Reader - brNextLoc Location - - unread []locatableRune - lastRead locatableRune +func s(str string) fmt.Stringer { + return Stringer{S: str} } -// NewDecoder returns a Decoder which will decode the given stream as a gg -// formatted stream of a Values. -func NewDecoder(r io.Reader) *Decoder { - return &Decoder{ - br: bufio.NewReader(r), - brNextLoc: Location{Row: 1, Col: 1}, +var ( + notNewline = RuneFunc( + s("not-newline"), func(r rune) bool { return r != '\n' }, + ) + + comment = Prefixed( + Prefixed(Rune('*'), ZeroOrMore(notNewline)), Rune('\n'), + ) + + whitespace = ZeroOrMore(FirstOf( + Discard(RuneFunc(s("whitespace"), unicode.IsSpace)), + Discard(comment), + )) +) + +func trimmed[T any](sym Symbol[T]) Symbol[T] { + sym = PrefixDiscarded(whitespace, sym) + sym = Suffixed(sym, whitespace) + return sym +} + +func trimmedRune(r rune) Symbol[Located[rune]] { + return trimmed(Rune(r)) +} + +var ( + digit = RuneFunc( + s("digit"), func(r rune) bool { return '0' <= r && r <= '9' }, + ) + + positiveNumber = StringFromRunes(OneOrMore(digit)) + + negativeNumber = Reduction( + s("negative-number"), + Rune('-'), + positiveNumber, + func(neg Located[rune], posNum Located[string]) Located[string] { + return Located[string]{ + neg.Location, string(neg.Value) + posNum.Value, + } + }, + ) + + number = Mapping( + s("number"), + FirstOf(negativeNumber, positiveNumber), + func(str Located[string]) Located[Value] { + i, err := strconv.ParseInt(str.Value, 10, 64) + if err != nil { + panic(fmt.Errorf("parsing %q as int: %w", str, err)) + } + + return Located[Value]{str.Location, Number(i)} + }, + ) +) + +var ( + letter = RuneFunc( + s("letter"), + func(r rune) bool { + return unicode.In(r, unicode.Letter, unicode.Mark) + }, + ) + + nameTail = ZeroOrMore(FirstOf(letter, digit)) + + name = Reduction( + s("name"), + letter, + nameTail, + func(head Located[rune], tail []Located[rune]) Located[Value] { + name := make([]rune, 0, len(tail)+1) + name = append(name, head.Value) + for _, r := range tail { + name = append(name, r.Value) + } + return Located[Value]{head.Location, Name(string(name))} + }, + ) +) + +func openEdgeIntoValue(val Value, oe *OpenEdge) *OpenEdge { + switch { + case oe == nil: + return graph.ValueOut(None, val) + case !oe.EdgeValue().Valid: + return oe.WithEdgeValue(Some(val)) + default: + return graph.TupleOut(Some(val), oe) } } -func (d *Decoder) readRune() (locatableRune, error) { - if len(d.unread) > 0 { - d.lastRead = d.unread[len(d.unread)-1] - d.unread = d.unread[:len(d.unread)-1] - return d.lastRead, nil +var graphSym, value = func() ( + Symbol[Located[Value]], Symbol[Located[Value]], +) { + + type tupleState struct { + ins []*OpenEdge + oe *OpenEdge } - loc := d.brNextLoc - - r, _, err := d.br.ReadRune() - if err != nil { - return d.lastRead, err + type graphState struct { + g *Graph + oe *OpenEdge } - if r == '\n' { - d.brNextLoc.Row++ - d.brNextLoc.Col = 1 - } else { - d.brNextLoc.Col++ - } + var ( + rightParen = trimmedRune(')') + tupleEnd = Mapping( + rightParen, + rightParen, + func(Located[rune]) tupleState { + // if ')', then map that to an empty state. This acts as a + // sentinel value to indicate "end of tuple". + return tupleState{} + }, + ) - d.lastRead = locatableRune{loc, r} - return d.lastRead, nil + rightCurlyBrace = trimmedRune('}') + graphEnd = Mapping( + rightCurlyBrace, + rightCurlyBrace, + func(Located[rune]) graphState { + // if '}', then map that to an empty state. This acts as a + // sentinel value to indicate "end of graph". + return graphState{} + }, + ) + ) + + var ( + // pre-define these, and then fill in the pointers after, in order to + // deal with recursive dependencies between them. + value = new(SymbolPtr[Located[Value]]) + + tuple = new(SymbolPtr[*OpenEdge]) + tupleTail = new(SymbolPtr[tupleState]) + tupleOpenEdge = new(SymbolPtr[tupleState]) + tupleOpenEdgeTail = new(SymbolPtr[tupleState]) + tupleOpenEdgeValueTail = new(SymbolPtr[tupleState]) + + graphSym = new(SymbolPtr[Located[Value]]) + graphTail = new(SymbolPtr[graphState]) + graphOpenEdge = new(SymbolPtr[graphState]) + graphOpenEdgeTail = new(SymbolPtr[graphState]) + graphOpenEdgeValueTail = new(SymbolPtr[graphState]) + ) + + tuple.Symbol = Reduction[Located[rune], tupleState, *OpenEdge]( + s("tuple"), + trimmedRune('('), + tupleTail, + func(_ Located[rune], ts tupleState) *OpenEdge { + slices.Reverse(ts.ins) + return graph.TupleOut(None, ts.ins...) + }, + ) + + tupleTail.Symbol = FirstOf( + tupleEnd, + Mapping[tupleState, tupleState]( + tupleOpenEdge, + tupleOpenEdge, + func(ts tupleState) tupleState { + ts.ins = append(ts.ins, ts.oe) + ts.oe = nil + return ts + }, + ), + ) + + tupleOpenEdge.Symbol = FirstOf( + Reduction[Located[Value], tupleState, tupleState]( + value, + value, + tupleOpenEdgeValueTail, + func(val Located[Value], ts tupleState) tupleState { + ts.oe = openEdgeIntoValue(val.Value, ts.oe) + return ts + }, + ), + Reduction[*OpenEdge, tupleState, tupleState]( + tuple, + tuple, + tupleOpenEdgeTail, + func(oe *OpenEdge, ts tupleState) tupleState { + ts.oe = oe + return ts + }, + ), + ) + + tupleOpenEdgeTail.Symbol = FirstOf( + tupleEnd, + Prefixed[Located[rune], tupleState](trimmedRune(','), tupleTail), + ) + + tupleOpenEdgeValueTail.Symbol = FirstOf[tupleState]( + tupleOpenEdgeTail, + Prefixed[Located[rune], tupleState](trimmedRune('<'), tupleOpenEdge), + ) + + graphSym.Symbol = Reduction[Located[rune], graphState, Located[Value]]( + s("graph"), + trimmedRune('{'), + graphTail, + func(r Located[rune], gs graphState) Located[Value] { + if gs.g == nil { + gs.g = new(Graph) + } + + return Located[Value]{r.Location, Value{Graph: gs.g}} + }, + ) + + graphTail.Symbol = FirstOf( + graphEnd, + Reduction( + name, + name, + Prefixed[Located[rune], graphState]( + trimmedRune('='), graphOpenEdge, + ), + func(name Located[Value], gs graphState) graphState { + if gs.g == nil { + gs.g = new(Graph) + } + + gs.g = gs.g.AddValueIn(name.Value, gs.oe) + gs.oe = nil + return gs + }, + ), + ) + log.Printf("populated graphTail with %v", graphTail) + + graphOpenEdge.Symbol = FirstOf( + Reduction[Located[Value], graphState, graphState]( + value, + value, + graphOpenEdgeValueTail, + func(val Located[Value], gs graphState) graphState { + gs.oe = openEdgeIntoValue(val.Value, gs.oe) + return gs + }, + ), + Reduction[*OpenEdge, graphState, graphState]( + tuple, + tuple, + graphOpenEdgeTail, + func(oe *OpenEdge, gs graphState) graphState { + gs.oe = oe + return gs + }, + ), + ) + + graphOpenEdgeTail.Symbol = FirstOf( + graphEnd, + Prefixed[Located[rune], graphState](trimmedRune(';'), graphTail), + ) + log.Printf("populated graphOpenEdgeTail with %v", graphOpenEdgeTail) + + graphOpenEdgeValueTail.Symbol = FirstOf[graphState]( + graphOpenEdgeTail, + Prefixed[Located[rune], graphState](trimmedRune('<'), graphOpenEdge), + ) + + value.Symbol = trimmed(FirstOf[Located[Value]](name, number, graphSym)) + + return graphSym, value +}() + +// Decoder reads Values off of an io.Reader, or return io.EOF. +type Decoder interface { + Next() (Located[Value], error) } -func (d *Decoder) unreadRune(lr locatableRune) { - if d.lastRead != lr { - panic(fmt.Sprintf( - "unreading rune %#v, but last read rune was %#v", lr, d.lastRead, - )) - } - - d.unread = append(d.unread, lr) +type decoder struct { + r Reader } -func (d *Decoder) nextLoc() Location { - if len(d.unread) > 0 { - return d.unread[len(d.unread)-1].Location - } - - return d.brNextLoc +// NewDecoder returns a Decoder which reads off the given io.Reader. The +// io.Reader should not be read from after this call. +func NewDecoder(r io.Reader) Decoder { + return &decoder{r: NewReader(r)} } -// Next returns the next top-level value in the stream, or io.EOF. -func (d *Decoder) Next() (Value, error) { - return topLevelTerm.decodeFn(d) +func (d *decoder) Next() (Located[Value], error) { + return value.Decode(d.r) } diff --git a/gg/term_test.go b/gg/decoder_test.go similarity index 50% rename from gg/term_test.go rename to gg/decoder_test.go index 80a041e..8756046 100644 --- a/gg/term_test.go +++ b/gg/decoder_test.go @@ -2,86 +2,72 @@ package gg import ( "bytes" - "io" "strconv" "testing" + . "github.com/mediocregopher/ginger/gg/grammar" "github.com/mediocregopher/ginger/graph" "github.com/stretchr/testify/assert" ) -func decoderLeftover(d *Decoder) string { - unread := make([]rune, len(d.unread)) - for i := range unread { - unread[i] = d.unread[i].r - } - - rest, err := io.ReadAll(d.br) - if err != nil { - panic(err) - } - return string(unread) + string(rest) -} - -func TestTermDecoding(t *testing.T) { +func TestDecoder(t *testing.T) { type test struct { - in string - exp Value - expErr string - leftover string + in string + exp Located[Value] + expErr string } runTests := func( - t *testing.T, name string, term *term[Value], tests []test, + t *testing.T, name string, sym Symbol[Located[Value]], tests []test, ) { t.Run(name, func(t *testing.T) { for i, test := range tests { t.Run(strconv.Itoa(i), func(t *testing.T) { - dec := NewDecoder(bytes.NewBufferString(test.in)) - got, err := term.decodeFn(dec) + r := NewReader(bytes.NewBufferString(test.in)) + got, err := sym.Decode(r) if test.expErr != "" { assert.Error(t, err) assert.Equal(t, test.expErr, err.Error()) } else if assert.NoError(t, err) { assert.True(t, - test.exp.Equal(got), + test.exp.Value.Equal(got.Value), "\nexp:%v\ngot:%v", test.exp, got, ) - assert.Equal(t, test.leftover, decoderLeftover(dec)) + assert.Equal(t, test.exp.Location, got.Location) } }) } }) } - expNum := func(row, col int, n int64) Value { - return Value{Number: &n, Location: Location{row, col}} + expNum := func(row, col int, n int64) Located[Value] { + return Located[Value]{Location{row, col}, Number(n)} } - runTests(t, "number", numberTerm, []test{ + runTests(t, "number", number, []test{ {in: `0`, exp: expNum(1, 1, 0)}, {in: `100`, exp: expNum(1, 1, 100)}, {in: `-100`, exp: expNum(1, 1, -100)}, - {in: `0foo`, exp: expNum(1, 1, 0), leftover: "foo"}, - {in: `100foo`, exp: expNum(1, 1, 100), leftover: "foo"}, + {in: `0foo`, exp: expNum(1, 1, 0)}, + {in: `100foo`, exp: expNum(1, 1, 100)}, }) - expName := func(row, col int, name string) Value { - return Value{Name: &name, Location: Location{row, col}} + expName := func(row, col int, name string) Located[Value] { + return Located[Value]{Location{row, col}, Name(name)} } - expGraph := func(row, col int, g *Graph) Value { - return Value{Graph: g, Location: Location{row, col}} + expGraph := func(row, col int, g *Graph) Located[Value] { + return Located[Value]{Location{row, col}, Value{Graph: g}} } - runTests(t, "name", nameTerm, []test{ + runTests(t, "name", name, []test{ {in: `a`, exp: expName(1, 1, "a")}, {in: `ab`, exp: expName(1, 1, "ab")}, {in: `ab2c`, exp: expName(1, 1, "ab2c")}, - {in: `ab2c,`, exp: expName(1, 1, "ab2c"), leftover: ","}, + {in: `ab2c,`, exp: expName(1, 1, "ab2c")}, }) - runTests(t, "graph", graphTerm, []test{ + runTests(t, "graph", graphSym, []test{ {in: `{}`, exp: expGraph(1, 1, new(Graph))}, {in: `{`, expErr: `1:2: expected '}' or name`}, {in: `{a}`, expErr: `1:3: expected '='`}, @@ -90,20 +76,14 @@ func TestTermDecoding(t *testing.T) { in: `{foo=a}`, exp: expGraph( 1, 1, new(Graph). - AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut(None, expName(6, 1, "a")), - ), + AddValueIn(Name("foo"), graph.ValueOut(None, Name("a"))), ), }, { in: `{ foo = a }`, exp: expGraph( 1, 1, new(Graph). - AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut(None, expName(6, 1, "a")), - ), + AddValueIn(Name("foo"), graph.ValueOut(None, Name("a"))), ), }, {in: `{1=a}`, expErr: `1:2: expected '}' or name`}, @@ -114,11 +94,8 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut( - Some(expName(6, 1, "a")), - expName(8, 1, "b"), - ), + Name("foo"), + graph.ValueOut(Some(Name("a")), Name("b")), ), ), }, @@ -127,12 +104,12 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.ValueOut( - Some(expName(8, 1, "b")), - expName(10, 1, "c"), + Some(Name("b")), + Name("c"), ), ), ), @@ -143,14 +120,14 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.TupleOut( - Some(expName(8, 1, "b")), + Some(Name("b")), graph.ValueOut( - Some(expName(10, 1, "c")), - expNum(12, 1, 1), + Some(Name("c")), + Number(1), ), ), ), @@ -162,10 +139,10 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expName(6, 1, "a")), - expName(8, 1, "b"), + Some(Name("a")), + Name("b"), ), ), ), @@ -175,15 +152,15 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expName(6, 1, "a")), - expName(8, 1, "b"), + Some(Name("a")), + Name("b"), ), ). AddValueIn( - expName(10, 1, "bar"), - graph.ValueOut(None, expName(15, 1, "c")), + Name("bar"), + graph.ValueOut(None, Name("c")), ), ), }, @@ -192,18 +169,18 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expName(6, 1, "a")), - expGraph(8, 1, new(Graph).AddValueIn( - expName(9, 1, "baz"), - graph.ValueOut(None, expNum(13, 1, 1)), - )), + Some(Name("a")), + Value{Graph: new(Graph).AddValueIn( + Name("baz"), + graph.ValueOut(None, Number(1)), + )}, ), ). AddValueIn( - expName(16, 1, "bar"), - graph.ValueOut(None, expName(20, 1, "c")), + Name("bar"), + graph.ValueOut(None, Name("c")), ), ), }, @@ -212,31 +189,31 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expGraph(8, 1, new(Graph).AddValueIn( - expName(9, 1, "baz"), - graph.ValueOut(None, expNum(13, 1, 1)), - ))), - expName(6, 1, "a"), + Some(Value{Graph: new(Graph).AddValueIn( + Name("baz"), + graph.ValueOut(None, Number(1)), + )}), + Name("a"), ), ). AddValueIn( - expName(16, 1, "bar"), - graph.ValueOut(None, expName(20, 1, "c")), + Name("bar"), + graph.ValueOut(None, Name("c")), ), ), }, }) - runTests(t, "tuple", graphTerm, []test{ + runTests(t, "tuple", graphSym, []test{ { in: `{foo=(a)}`, exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut(None, expName(6, 1, "a")), + Name("foo"), + graph.ValueOut(None, Name("a")), ), ), }, @@ -245,10 +222,10 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expName(6, 1, "a")), - expName(8, 1, "b"), + Some(Name("a")), + Name("b"), ), ), ), @@ -258,10 +235,10 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.ValueOut( - Some(expName(6, 1, "a")), - expName(8, 1, "b"), + Some(Name("a")), + Name("b"), ), ), ), @@ -271,11 +248,11 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), - graph.ValueOut(None, expName(8, 1, "b")), - graph.ValueOut(None, expName(10, 1, "c")), + Some(Name("a")), + graph.ValueOut(None, Name("b")), + graph.ValueOut(None, Name("c")), ), ), ), @@ -285,12 +262,12 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.TupleOut( - Some(expName(6, 1, "b")), - graph.ValueOut(None, expName(8, 1, "c")), + Some(Name("b")), + graph.ValueOut(None, Name("c")), ), ), ), @@ -301,12 +278,12 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.TupleOut( - Some(expName(6, 1, "b")), - graph.ValueOut(None, expName(8, 1, "c")), + Some(Name("b")), + graph.ValueOut(None, Name("c")), ), ), ), @@ -317,15 +294,15 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.TupleOut( - Some(expName(6, 1, "b")), - graph.ValueOut(None, expName(8, 1, "c")), + Some(Name("b")), + graph.ValueOut(None, Name("c")), graph.ValueOut( - Some(expName(12, 1, "d")), - expNum(10, 1, 1), + Some(Name("d")), + Number(1), ), ), ), @@ -337,12 +314,12 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), + Name("foo"), graph.TupleOut( - Some(expName(6, 1, "a")), + Some(Name("a")), graph.TupleOut( - Some(expName(6, 1, "b")), - graph.ValueOut(None, expName(8, 1, "c")), + Some(Name("b")), + graph.ValueOut(None, Name("c")), ), ), ), @@ -350,14 +327,14 @@ func TestTermDecoding(t *testing.T) { }, }) - runTests(t, "comment", graphTerm, []test{ + runTests(t, "comment", graphSym, []test{ { in: "*\n{}", - exp: expGraph(1, 1, new(Graph)), + exp: expGraph(2, 1, new(Graph)), }, { in: "* ignore me!\n{}", - exp: expGraph(1, 1, new(Graph)), + exp: expGraph(2, 1, new(Graph)), }, { in: "{* ignore me!\n}", @@ -368,8 +345,8 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut(None, expName(6, 1, "a")), + Name("foo"), + graph.ValueOut(None, Name("a")), ), ), }, @@ -378,8 +355,8 @@ func TestTermDecoding(t *testing.T) { exp: expGraph( 1, 1, new(Graph). AddValueIn( - expName(2, 1, "foo"), - graph.ValueOut(None, expName(6, 1, "a")), + Name("foo"), + graph.ValueOut(None, Name("a")), ), ), }, diff --git a/gg/gg.bnf b/gg/gg.bnf index a625749..0cd4359 100644 --- a/gg/gg.bnf +++ b/gg/gg.bnf @@ -17,7 +17,7 @@ ::= | ::= "}" | ";" - ::= | "<" + ::= | "<" ::= | | ::= | diff --git a/gg/gg.go b/gg/gg.go index c57d9f9..e5ef3f2 100644 --- a/gg/gg.go +++ b/gg/gg.go @@ -15,8 +15,6 @@ type ( // Value represents a value which can be serialized by the gg text format. type Value struct { - Location - // Only one of these fields may be set Name *string Number *int64 diff --git a/gg/grammar/grammar.go b/gg/grammar/grammar.go new file mode 100644 index 0000000..d346483 --- /dev/null +++ b/gg/grammar/grammar.go @@ -0,0 +1,27 @@ +// Package grammar is used for parsing a stream of runes according to a set of +// grammatical rules. This package only supports context-free grammars. +package grammar + +import "fmt" + +// Stringer is a convenience tool for working with fmt.Stringer. Exactly one of +// the fields must be set, and will be used to implement the fmt.Stringer +// interface. +type Stringer struct { + I fmt.Stringer + F func() string + S string +} + +func (s Stringer) String() string { + switch { + case s.I != nil: + return s.I.String() + case s.F != nil: + return s.F() + case s.S != "": + return s.S + default: + panic("no fields set on Stringer") + } +} diff --git a/gg/grammar/location.go b/gg/grammar/location.go new file mode 100644 index 0000000..1c45171 --- /dev/null +++ b/gg/grammar/location.go @@ -0,0 +1,27 @@ +package grammar + +import "fmt" + +// Location indicates a position in a stream of runes identified by column +// within newline-separated rows. +type Location struct { + Row, Col int +} + +func (l Location) errf(str string, args ...any) LocatedError { + return LocatedError{l, fmt.Errorf(str, args...)} +} + +// Located wraps a value so that it has a Location attached to it. +type Located[T any] struct { + Location + Value T +} + +// LocatedError is an error related to a specific point within a stream of +// runes. +type LocatedError Located[error] + +func (e LocatedError) Error() string { + return fmt.Sprintf("%d:%d: %v", e.Row, e.Col, e.Value) +} diff --git a/gg/grammar/reader.go b/gg/grammar/reader.go new file mode 100644 index 0000000..b3e0d5b --- /dev/null +++ b/gg/grammar/reader.go @@ -0,0 +1,74 @@ +package grammar + +import ( + "bufio" + "io" +) + +// Reader is used for reading Runes from a stream. +type Reader interface { + + // ReadRune reads the next Rune off the stream, or returns io.EOF. + ReadRune() (Located[rune], error) + + // UnreadRune can be used to place a Rune onto an internal buffer, such that + // the Rune will be the next to be read using ReadRune. If called multiple + // times then ReadRune will produce the given Runes in LIFO order. + UnreadRune(Located[rune]) + + // NextLocation returns the Location of the next Rune which will be returned + // with ReadRune. + NextLocation() Location +} + +type reader struct { + br *bufio.Reader + brNextLoc Location + + unread []Located[rune] +} + +// NewReader wraps the io.Reader as a Reader. The given Reader should not be +// read from after this call. +func NewReader(r io.Reader) Reader { + return &reader{ + br: bufio.NewReader(r), + brNextLoc: Location{Row: 1, Col: 1}, + } +} + +func (rr *reader) ReadRune() (Located[rune], error) { + if len(rr.unread) > 0 { + r := rr.unread[len(rr.unread)-1] + rr.unread = rr.unread[:len(rr.unread)-1] + return r, nil + } + + loc := rr.brNextLoc + + r, _, err := rr.br.ReadRune() + if err != nil { + return Located[rune]{}, err + } + + if r == '\n' { + rr.brNextLoc.Row++ + rr.brNextLoc.Col = 1 + } else { + rr.brNextLoc.Col++ + } + + return Located[rune]{loc, r}, nil +} + +func (rr *reader) UnreadRune(r Located[rune]) { + rr.unread = append(rr.unread, r) +} + +func (rr *reader) NextLocation() Location { + if len(rr.unread) > 0 { + return rr.unread[len(rr.unread)-1].Location + } + + return rr.brNextLoc +} diff --git a/gg/grammar/symbol.go b/gg/grammar/symbol.go new file mode 100644 index 0000000..daace9c --- /dev/null +++ b/gg/grammar/symbol.go @@ -0,0 +1,294 @@ +package grammar + +import ( + "errors" + "fmt" + "io" + "strings" +) + +// ErrNoMatch is used by Symbol's Decode method, see that method's docs for more +// details. +var ErrNoMatch = errors.New("no match") + +// Symbol represents a symbol in the grammar. A Symbol is expected to be +// stateless, and is usually constructed from other Symbols using functions in +// this package. +type Symbol[T any] interface { + fmt.Stringer // Used when generating errors related to this Symbol, e.g. "number" + + // Decode reads and parses a value represented by this Symbol off the + // Reader. + // + // This may return ErrNoMatch to indicate that the upcoming data on the + // Reader is rejected by this Symbol. In this case the Symbol should leave + // the Reader in the same state it was passed. + Decode(Reader) (T, error) +} + +type symbol[T any] struct { + fmt.Stringer + decodeFn func(Reader) (T, error) +} + +func (s *symbol[T]) Decode(r Reader) (T, error) { return s.decodeFn(r) } + +// SymbolPtr wraps a Symbol in a such a way as to make lazily initializing a +// Symbol variable possible. This allows for recursion amongst different +// Symbols. +// +// Example: +// +// a := new(SymbolPtr) +// b := new(SymbolPtr) +// a.Symbol = FirstOf(Rune('a'), b) +// b.Symbol = FirstOf(Rune('b'), a) +type SymbolPtr[T any] struct { + Symbol[T] +} + +// RuneFunc matches and produces any rune for which the given function returns +// true. +func RuneFunc(stringer fmt.Stringer, fn func(rune) bool) Symbol[Located[rune]] { + return &symbol[Located[rune]]{ + stringer, + func(rr Reader) (Located[rune], error) { + var zero Located[rune] + + r, err := rr.ReadRune() + if errors.Is(err, io.EOF) { + return zero, ErrNoMatch + } else if err != nil { + return zero, err + } + + if !fn(r.Value) { + rr.UnreadRune(r) + return zero, ErrNoMatch + } + + return r, nil + }, + } +} + +// Rune matches and produces the given rune. +func Rune(r rune) Symbol[Located[rune]] { + return RuneFunc( + Stringer{S: fmt.Sprintf("'%c'", r)}, + func(r2 rune) bool { return r == r2 }, + ) +} + +// StringFromRunes produces a string from the slice of runes produced by the +// given Symbol. The slice must not be empty. StringFromRunes does not match if +// the given Symbol does not match. +func StringFromRunes(sym Symbol[[]Located[rune]]) Symbol[Located[string]] { + return Mapping(sym, sym, func(runes []Located[rune]) Located[string] { + if len(runes) == 0 { + panic("StringFromRunes used on empty set of runes") + } + + str := make([]rune, len(runes)) + for i := range runes { + str[i] = runes[i].Value + } + return Located[string]{runes[0].Location, string(str)} + }) +} + +// Mapping produces a value of type Tb by decoding a value from the given +// Symbol and passing it through the given mapping function. If the given Symbol +// doesn't match then neither does Map. +func Mapping[Ta, Tb any]( + stringer fmt.Stringer, sym Symbol[Ta], fn func(Ta) Tb, +) Symbol[Tb] { + return &symbol[Tb]{ + stringer, + func(rr Reader) (Tb, error) { + var zero Tb + va, err := sym.Decode(rr) + if err != nil { + return zero, err + } + return fn(va), nil + }, + } +} + +// OneOrMore will produce as many of the given Symbol's value as can be found +// sequentially, up until a non-matching value is encountered. If no matches are +// found then OneOrMore does not match. +func OneOrMore[T any](sym Symbol[T]) Symbol[[]T] { + return &symbol[[]T]{ + Stringer{F: func() string { + return fmt.Sprintf("one or more %v", sym) + }}, + func(rr Reader) ([]T, error) { + var vv []T + for { + v, err := sym.Decode(rr) + if errors.Is(err, ErrNoMatch) { + break + } else if err != nil { + return nil, err + } + + vv = append(vv, v) + } + + if len(vv) == 0 { + return nil, ErrNoMatch + } + + return vv, nil + }, + } +} + +// ZeroOrMore will produce as many of the given Symbol's value as can be found +// sequentially, up until a non-matching value is encountered. If no matches are +// found then an empty slice is produced. +func ZeroOrMore[T any](sym Symbol[T]) Symbol[[]T] { + return &symbol[[]T]{ + Stringer{F: func() string { + return fmt.Sprintf("zero or more %v", sym) + }}, + func(rr Reader) ([]T, error) { + var vv []T + for { + v, err := sym.Decode(rr) + if errors.Is(err, ErrNoMatch) { + break + } else if err != nil { + return nil, err + } + + vv = append(vv, v) + } + + return vv, nil + }, + } +} + +func firstOf[T any](stringer fmt.Stringer, syms ...Symbol[T]) Symbol[T] { + return &symbol[T]{ + stringer, + func(rr Reader) (T, error) { + var zero T + for _, sym := range syms { + v, err := sym.Decode(rr) + if errors.Is(err, ErrNoMatch) { + continue + } else if err != nil { + return zero, err + } + + return v, nil + } + + return zero, ErrNoMatch + }, + } +} + +// FirstOf matches and produces the value for the first Symbol in the list which +// matches. FirstOf does not match if none of the given Symbols match. +func FirstOf[T any](syms ...Symbol[T]) Symbol[T] { + return firstOf( + Stringer{F: func() string { + descrs := make([]string, len(syms)) + for i := range syms { + descrs[i] = syms[i].String() + } + return strings.Join(descrs, " or ") + }}, + syms..., + ) +} + +// Reduction produces a value of type Tc by first reading a value from symA, +// then symB, and then running those through the given function. +// +// If symA does not match then Reduction does not match. If symA matches but +// symB does not then also match then Reduction produces a LocatedError. +func Reduction[Ta, Tb, Tc any]( + stringer fmt.Stringer, + symA Symbol[Ta], + symB Symbol[Tb], + fn func(Ta, Tb) Tc, +) Symbol[Tc] { + return &symbol[Tc]{ + stringer, + func(rr Reader) (Tc, error) { + var zero Tc + + va, err := symA.Decode(rr) + if err != nil { + return zero, err + } + + vb, err := symB.Decode(rr) + if errors.Is(err, ErrNoMatch) { + return zero, rr.NextLocation().errf("expected %v", symB) + } else if err != nil { + return zero, err + } + + return fn(va, vb), nil + + }, + } +} + +// Prefixed matches on prefixSym, discards its value, then produces the value +// produced by sym. +// +// If prefixSym does not match then Prefixed does not match. If prefixSym +// matches but sym does not also match then Prefixed produces a LocatedError. +func Prefixed[Ta, Tb any](prefixSym Symbol[Ta], sym Symbol[Tb]) Symbol[Tb] { + return Reduction(prefixSym, prefixSym, sym, func(_ Ta, b Tb) Tb { + return b + }) +} + +// PrefixDiscarded is similar to Prefixed, except that if sym does not match +// then PrefixDiscarded does not match, whereas Prefixed produces a LocatedError +// in that case. +// +// NOTE PrefixDiscarded does not fully honor the contract of Symbol. If +// prefixSym matches, but sym does not, then only sym will restore Reader to its +// prior state; prefixSym cannot return whatever data it read back onto the +// Reader. Therefore ErrNoMatch can be returned without Reader being fully back +// in its original state. In practice this isn't a big deal, given the common +// use-cases of PrefixDiscarded, but it may prove tricky. +func PrefixDiscarded[Ta, Tb any](prefixSym Symbol[Ta], sym Symbol[Tb]) Symbol[Tb] { + return &symbol[Tb]{ + sym, + func(rr Reader) (Tb, error) { + var zero Tb + if _, err := prefixSym.Decode(rr); err != nil { + return zero, err + } + return sym.Decode(rr) + }, + } +} + +// Suffixed matchs on sym and then suffixSym, returning the value produced by +// sym and discarding the one produced by suffixSym. +// +// If sym does not match then Suffixed does not match. If sym matches but +// suffixSym does not also match then Suffixed produces a LocatedError. +func Suffixed[Ta, Tb any](sym Symbol[Ta], suffixSym Symbol[Tb]) Symbol[Ta] { + return Reduction(sym, sym, suffixSym, func(a Ta, _ Tb) Ta { + return a + }) +} + +// Discard matches if the given Symbol does, but discards the value it produces, +// producing an empty value instead. +func Discard[T any](sym Symbol[T]) Symbol[struct{}] { + return Mapping(sym, sym, func(T) struct{} { return struct{}{} }) +} diff --git a/gg/location.go b/gg/location.go deleted file mode 100644 index a1de1aa..0000000 --- a/gg/location.go +++ /dev/null @@ -1,36 +0,0 @@ -package gg - -import "fmt" - -// Location indicates a position in a stream of bytes identified by column -// within newline-separated rows. -type Location struct { - Row, Col int -} - -func (l Location) errf(str string, args ...any) LocatedError { - return LocatedError{l, fmt.Errorf(str, args...)} -} - -func (l Location) locate() Location { return l } - -// LocatedError is an error related to a specific point within a decode gg -// stream. -type LocatedError struct { - Location - Err error -} - -func (e LocatedError) Error() string { - return fmt.Sprintf("%d:%d: %v", e.Row, e.Col, e.Err) -} - -type locatableRune struct { - Location - r rune -} - -type locatableString struct { - Location - str string -} diff --git a/gg/term.go b/gg/term.go deleted file mode 100644 index fa43dbd..0000000 --- a/gg/term.go +++ /dev/null @@ -1,508 +0,0 @@ -package gg - -import ( - "errors" - "fmt" - "io" - "strconv" - "strings" - "unicode" - - "github.com/mediocregopher/ginger/graph" - "golang.org/x/exp/slices" -) - -var ( - errNoMatch = errors.New("not found") -) - -type stringerFn func() string - -func (fn stringerFn) String() string { - return fn() -} - -type stringerStr string - -func (str stringerStr) String() string { - return string(str) -} - -type term[T any] struct { - name fmt.Stringer - decodeFn func(d *Decoder) (T, error) -} - -func (t term[T]) String() string { - return t.name.String() -} - -func firstOf[T any](terms ...*term[T]) *term[T] { - if len(terms) < 2 { - panic("firstOfTerms requires at least 2 terms") - } - - return &term[T]{ - name: stringerFn(func() string { - descrs := make([]string, len(terms)) - for i := range terms { - descrs[i] = terms[i].String() - } - return strings.Join(descrs, " or ") - }), - decodeFn: func(d *Decoder) (T, error) { - var zero T - for _, t := range terms { - v, err := t.decodeFn(d) - if errors.Is(err, errNoMatch) { - continue - } else if err != nil { - return zero, err - } - - return v, nil - } - - return zero, errNoMatch - }, - } -} - -func seq[Ta, Tb, Tc any]( - name fmt.Stringer, - termA *term[Ta], - termB *term[Tb], - fn func(Ta, Tb) Tc, -) *term[Tc] { - return &term[Tc]{ - name: name, - decodeFn: func(d *Decoder) (Tc, error) { - var zero Tc - - va, err := termA.decodeFn(d) - if err != nil { - return zero, err - } - - vb, err := termB.decodeFn(d) - if errors.Is(err, errNoMatch) { - return zero, d.nextLoc().errf("expected %v", termB) - } else if err != nil { - return zero, err - } - - return fn(va, vb), nil - }, - } -} - -func prefixed[Ta, Tb any](termA *term[Ta], termB *term[Tb]) *term[Tb] { - return seq(termA, termA, termB, func(_ Ta, b Tb) Tb { - return b - }) -} - -func prefixIgnored[Ta, Tb any](termA *term[Ta], termB *term[Tb]) *term[Tb] { - return &term[Tb]{ - name: termB, - decodeFn: func(d *Decoder) (Tb, error) { - var zero Tb - if _, err := termA.decodeFn(d); err != nil { - return zero, err - } - return termB.decodeFn(d) - }, - } -} - -func suffixIgnored[Ta, Tb any]( - termA *term[Ta], termB *term[Tb], -) *term[Ta] { - return seq(termA, termA, termB, func(a Ta, _ Tb) Ta { - return a - }) -} - -func oneOrMore[T any](t *term[T]) *term[[]T] { - return &term[[]T]{ - name: stringerFn(func() string { - return fmt.Sprintf("one or more %v", t) - }), - decodeFn: func(d *Decoder) ([]T, error) { - var vv []T - for { - v, err := t.decodeFn(d) - if errors.Is(err, errNoMatch) { - break - } else if err != nil { - return nil, err - } - - vv = append(vv, v) - } - - if len(vv) == 0 { - return nil, errNoMatch - } - - return vv, nil - }, - } -} - -func zeroOrMore[T any](t *term[T]) *term[[]T] { - return &term[[]T]{ - name: stringerFn(func() string { - return fmt.Sprintf("zero or more %v", t) - }), - decodeFn: func(d *Decoder) ([]T, error) { - var vv []T - for { - v, err := t.decodeFn(d) - if errors.Is(err, errNoMatch) { - break - } else if err != nil { - return nil, err - } - - vv = append(vv, v) - } - - return vv, nil - }, - } -} - -func mapTerm[Ta, Tb any]( - name fmt.Stringer, t *term[Ta], fn func(Ta) Tb, -) *term[Tb] { - return &term[Tb]{ - name: name, - decodeFn: func(d *Decoder) (Tb, error) { - var zero Tb - va, err := t.decodeFn(d) - if err != nil { - return zero, err - } - return fn(va), nil - }, - } -} - -func runePredTerm( - name fmt.Stringer, pred func(rune) bool, -) *term[locatableRune] { - return &term[locatableRune]{ - name: name, - decodeFn: func(d *Decoder) (locatableRune, error) { - lr, err := d.readRune() - if errors.Is(err, io.EOF) { - return locatableRune{}, errNoMatch - } else if err != nil { - return locatableRune{}, err - } - - if !pred(lr.r) { - d.unreadRune(lr) - return locatableRune{}, errNoMatch - } - - return lr, nil - }, - } -} - -func runeTerm(r rune) *term[locatableRune] { - return runePredTerm( - stringerStr(fmt.Sprintf("'%c'", r)), - func(r2 rune) bool { return r2 == r }, - ) -} - -func locatableRunesToString(rr []locatableRune) string { - str := make([]rune, len(rr)) - for i := range rr { - str[i] = rr[i].r - } - return string(str) -} - -func runesToStringTerm( - t *term[[]locatableRune], -) *term[locatableString] { - return mapTerm( - t, t, func(rr []locatableRune) locatableString { - return locatableString{rr[0].locate(), locatableRunesToString(rr)} - }, - ) -} - -func discard[T any](t *term[T]) *term[struct{}] { - return mapTerm(t, t, func(_ T) struct{} { return struct{}{} }) -} - -var ( - notNewlineTerm = runePredTerm( - stringerStr("not-newline"), - func(r rune) bool { return r != '\n' }, - ) - - commentTerm = prefixed( - prefixed(runeTerm('*'), zeroOrMore(notNewlineTerm)), - runeTerm('\n'), - ) - - whitespaceTerm = zeroOrMore(firstOf( - discard(runePredTerm(stringerStr("whitespace"), unicode.IsSpace)), - discard(commentTerm), - )) -) - -func trimmedTerm[T any](t *term[T]) *term[T] { - t = prefixIgnored(whitespaceTerm, t) - t = suffixIgnored(t, whitespaceTerm) - return t -} - -func trimmedRuneTerm(r rune) *term[locatableRune] { - return trimmedTerm(runeTerm(r)) -} - -var ( - digitTerm = runePredTerm( - stringerStr("digit"), - func(r rune) bool { return '0' <= r && r <= '9' }, - ) - - positiveNumberTerm = runesToStringTerm(oneOrMore(digitTerm)) - - negativeNumberTerm = seq( - stringerStr("negative-number"), - runeTerm('-'), - positiveNumberTerm, - func(neg locatableRune, posNum locatableString) locatableString { - return locatableString{neg.locate(), string(neg.r) + posNum.str} - }, - ) - - numberTerm = mapTerm( - stringerStr("number"), - firstOf(negativeNumberTerm, positiveNumberTerm), - func(str locatableString) Value { - i, err := strconv.ParseInt(str.str, 10, 64) - if err != nil { - panic(fmt.Errorf("parsing %q as int: %w", str, err)) - } - - return Value{Number: &i, Location: str.locate()} - }, - ) -) - -var ( - letterTerm = runePredTerm( - stringerStr("letter"), - func(r rune) bool { - return unicode.In(r, unicode.Letter, unicode.Mark) - }, - ) - - letterTailTerm = zeroOrMore(firstOf(letterTerm, digitTerm)) - - nameTerm = seq( - stringerStr("name"), - letterTerm, - letterTailTerm, - func(head locatableRune, tail []locatableRune) Value { - name := string(head.r) + locatableRunesToString(tail) - return Value{Name: &name, Location: head.locate()} - }, - ) -) - -func openEdgeIntoValue(val Value, oe *OpenEdge) *OpenEdge { - switch { - case oe == nil: - return graph.ValueOut(None, val) - case !oe.EdgeValue().Valid: - return oe.WithEdgeValue(Some(val)) - default: - return graph.TupleOut(Some(val), oe) - } -} - -var graphTerm, valueTerm = func() (*term[Value], *term[Value]) { - type tupleState struct { - ins []*OpenEdge - oe *OpenEdge - } - - type graphState struct { - g *Graph - oe *OpenEdge - } - - var ( - rightParenthesis = trimmedRuneTerm(')') - tupleEndTerm = mapTerm( - rightParenthesis, - rightParenthesis, - func(lr locatableRune) tupleState { - // if ')', then map that to an empty state. This acts as a - // sentinel value to indicate "end of tuple". - return tupleState{} - }, - ) - - rightCurlyBrace = trimmedRuneTerm('}') - graphEndTerm = mapTerm( - rightCurlyBrace, - rightCurlyBrace, - func(lr locatableRune) graphState { - // if '}', then map that to an empty state. This acts as a - // sentinel value to indicate "end of graph". - return graphState{} - }, - ) - ) - - var ( - // pre-define these, and then fill in the pointers after, in order to - // deal with recursive dependencies between them. - valueTerm = new(term[Value]) - - tupleTerm = new(term[*OpenEdge]) - tupleTailTerm = new(term[tupleState]) - tupleOpenEdgeTerm = new(term[tupleState]) - tupleOpenEdgeTailTerm = new(term[tupleState]) - tupleOpenEdgeValueTailTerm = new(term[tupleState]) - - graphTerm = new(term[Value]) - graphTailTerm = new(term[graphState]) - graphOpenEdgeTerm = new(term[graphState]) - graphOpenEdgeTailTerm = new(term[graphState]) - graphOpenEdgeValueTailTerm = new(term[graphState]) - ) - - *tupleTerm = *seq( - stringerStr("tuple"), - trimmedRuneTerm('('), - tupleTailTerm, - func(lr locatableRune, ts tupleState) *OpenEdge { - slices.Reverse(ts.ins) - return graph.TupleOut(None, ts.ins...) - }, - ) - - *tupleTailTerm = *firstOf( - tupleEndTerm, - mapTerm( - tupleOpenEdgeTerm, - tupleOpenEdgeTerm, - func(ts tupleState) tupleState { - ts.ins = append(ts.ins, ts.oe) - ts.oe = nil - return ts - }, - ), - ) - - *tupleOpenEdgeTerm = *firstOf( - seq( - valueTerm, - valueTerm, - tupleOpenEdgeValueTailTerm, - func(val Value, ts tupleState) tupleState { - ts.oe = openEdgeIntoValue(val, ts.oe) - return ts - }, - ), - seq( - tupleTerm, - tupleTerm, - tupleOpenEdgeTailTerm, - func(oe *OpenEdge, ts tupleState) tupleState { - ts.oe = oe - return ts - }, - ), - ) - - *tupleOpenEdgeTailTerm = *firstOf( - tupleEndTerm, - prefixed(trimmedRuneTerm(','), tupleTailTerm), - ) - - *tupleOpenEdgeValueTailTerm = *firstOf( - tupleOpenEdgeTailTerm, - prefixed(trimmedRuneTerm('<'), tupleOpenEdgeTerm), - ) - - *graphTerm = *seq( - stringerStr("graph"), - trimmedRuneTerm('{'), - graphTailTerm, - func(lr locatableRune, gs graphState) Value { - if gs.g == nil { - gs.g = new(Graph) - } - - return Value{Graph: gs.g, Location: lr.locate()} - }, - ) - - *graphTailTerm = *firstOf( - graphEndTerm, - seq( - nameTerm, - nameTerm, - prefixed(trimmedRuneTerm('='), graphOpenEdgeTerm), - func(name Value, gs graphState) graphState { - if gs.g == nil { - gs.g = new(Graph) - } - - gs.g = gs.g.AddValueIn(name, gs.oe) - gs.oe = nil - return gs - }, - ), - ) - - *graphOpenEdgeTerm = *firstOf( - seq( - valueTerm, - valueTerm, - graphOpenEdgeValueTailTerm, - func(val Value, gs graphState) graphState { - gs.oe = openEdgeIntoValue(val, gs.oe) - return gs - }, - ), - seq( - tupleTerm, - tupleTerm, - graphOpenEdgeTailTerm, - func(oe *OpenEdge, gs graphState) graphState { - gs.oe = oe - return gs - }, - ), - ) - - *graphOpenEdgeTailTerm = *firstOf( - graphEndTerm, - prefixed(trimmedRuneTerm(';'), graphTailTerm), - ) - - *graphOpenEdgeValueTailTerm = *firstOf( - graphOpenEdgeTailTerm, - prefixed(trimmedRuneTerm('<'), graphOpenEdgeTerm), - ) - - *valueTerm = *firstOf(nameTerm, numberTerm, graphTerm) - - return graphTerm, valueTerm -}() - -var topLevelTerm = trimmedTerm(valueTerm) diff --git a/vm/vm.go b/vm/vm.go index 4655aab..09372eb 100644 --- a/vm/vm.go +++ b/vm/vm.go @@ -110,11 +110,11 @@ func EvaluateSource(opSrc io.Reader, input Value, scope Scope) (Value, error) { v, err := gg.NewDecoder(opSrc).Next() if err != nil { return Value{}, err - } else if v.Graph == nil { + } else if v.Value.Graph == nil { return Value{}, errors.New("value must be a graph") } - fn, err := FunctionFromGraph(v.Graph, scope.NewScope()) + fn, err := FunctionFromGraph(v.Value.Graph, scope.NewScope()) if err != nil { return Value{}, err }