├── LICENSE ├── README.md ├── ast ├── ast.go ├── ast_test.go ├── doc.go ├── internal │ └── pgen │ │ ├── pgen.go │ │ └── table.go ├── node.go └── tokenstack.go ├── cmd ├── pgen │ ├── grammar.go │ ├── pgen.go │ └── tmpl.go └── wdte │ ├── .gitignore │ ├── README.md │ ├── file.go │ ├── import.go │ ├── import_linux.go │ ├── import_other.go │ ├── stdin.go │ └── wdte.go ├── doc.go ├── go.mod ├── go.sum ├── readme_test.go ├── repl ├── doc.go ├── example_repl_test.go ├── examples_test.go ├── repl.go └── repl_test.go ├── res └── grammar.ebnf ├── scanner ├── doc.go ├── scanner.go ├── scanner_test.go ├── token.go └── util.go ├── std ├── all │ ├── all.go │ ├── gen.bash │ └── gen.go ├── arrays │ └── arrays.go ├── debug │ ├── debug.go │ ├── norace.go │ └── race.go ├── doc.go ├── import.go ├── io │ ├── file │ │ └── file.go │ └── io.go ├── math │ └── math.go ├── mods.go ├── rand │ └── rand.go ├── std.go ├── stream │ ├── end.go │ ├── middle.go │ ├── start.go │ └── stream.go └── strings │ ├── format.go │ └── strings.go ├── translate.go ├── types.go ├── wdte.go ├── wdte_test.go └── wdteutil ├── doc.go ├── func.go ├── func_test.go ├── save.go └── wdteutil.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | wdte 2 | ==== 3 | 4 | [![GoDoc](https://godoc.org/github.com/DeedleFake/wdte?status.svg)](https://godoc.org/github.com/DeedleFake/wdte) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/DeedleFake/wdte)](https://goreportcard.com/report/github.com/DeedleFake/wdte) 6 | [![cover.run](https://cover.run/go/github.com/DeedleFake/wdte.svg?style=flat&tag=golang-1.10)](https://cover.run/go?tag=golang-1.10&repo=github.com/DeedleFake/wdte) 7 | 8 | WDTE is a simple, functional-ish, embedded scripting language. 9 | 10 | Why does this exist? 11 | -------------------- 12 | 13 | Good question. In fact, I found myself asking the same thing, hence the name. 14 | 15 | I had a number of design goals in mind when I started working on this project: 16 | 17 | * Extremely simple. Entire grammar is less than 20-30 lines of specification. 18 | * Grammar is LL(1) parseable. 19 | * Functional-ish, but not particularly strict about it. 20 | * Designed primarily for embedding. 21 | * Extremely easy to use from the binding side. In this case, that's primarily Go. 22 | 23 | If you want to try the language yourself, feel free to take a look at [the playground][playground]. It shows not only some of the features of the language in terms of actually writing code in it, but also how embeddable it is. The playground runs entirely in the browser *on the client's end* thanks to WebAssembly. 24 | 25 | Example 26 | ------- 27 | 28 | ```go 29 | package main 30 | 31 | import ( 32 | "fmt" 33 | "os" 34 | "strings" 35 | 36 | "github.com/DeedleFake/wdte" 37 | "github.com/DeedleFake/wdte/wdteutil" 38 | ) 39 | 40 | const src = ` 41 | let i => import 'some/import/path/or/another'; 42 | 43 | i.print 3; 44 | + 5 2 -> i.print; 45 | 7 -> + 5 -> i.print; 46 | ` 47 | 48 | func im(from string) (*wdte.Scope, error) { 49 | return wdte.S().Map(map[wdte.ID]wdte.Func{ 50 | "print": wdteutil.Func("print", func(v interface{}) interface{} { 51 | fmt.Println(v) 52 | return v 53 | }), 54 | }), nil 55 | } 56 | 57 | func Sum(frame wdte.Frame, args ...wdte.Func) wdte.Func { 58 | frame = frame.Sub("+") 59 | 60 | if len(args) < 2 { 61 | return wdteutil.SaveArgs(wdte.GoFunc(Sum), args...) 62 | } 63 | 64 | var sum wdte.Number 65 | for _, arg := range args { 66 | sum += arg.(wdte.Number) 67 | } 68 | return sum 69 | } 70 | 71 | func main() { 72 | m, err := wdte.Parse(strings.NewReader(src), wdte.ImportFunc(im), nil) 73 | if err != nil { 74 | fmt.Fprintf(os.Stderr, "Error parsing script: %v\n", err) 75 | os.Exit(1) 76 | } 77 | 78 | scope := wdte.S().Add("+", wdte.GoFunc(Sum)) 79 | 80 | r := m.Call(wdte.F().WithScope(scope)) 81 | if err, ok := r.(error); ok { 82 | fmt.Fprintf(os.Stderr, "Error running script: %v\n", err) 83 | os.Exit(1) 84 | } 85 | } 86 | ``` 87 | 88 | ##### Output 89 | 90 | ``` 91 | 3 92 | 7 93 | 12 94 | ``` 95 | 96 | Documentation 97 | ------------- 98 | 99 | For an overview of the language's design and features, see [the GitHub wiki][wiki]. 100 | 101 | Status 102 | ------ 103 | 104 | WDTE is in a pre-alpha state. It is filled with bugs and large amounts of stuff are subject to change without warning. That being said, if you're interested in anything, feel free to submit a pull request and get things fixed and/or implemented faster. 105 | 106 | [playground]: https://deedlefake.github.io/wdte 107 | [wiki]: https://github.com/DeedleFake/wdte/wiki 108 | -------------------------------------------------------------------------------- /ast/ast.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/DeedleFake/wdte/ast/internal/pgen" 8 | "github.com/DeedleFake/wdte/scanner" 9 | ) 10 | 11 | // Parse parses a full script, returning the root node of the AST. 12 | func Parse(r io.Reader, macros scanner.MacroMap) (Node, error) { 13 | return parse(r, tokenStack{pgen.NTerm("script")}, pgen.Table, macros) 14 | } 15 | 16 | func parse(r io.Reader, g tokenStack, table map[pgen.Lookup]pgen.Rule, macros scanner.MacroMap) (ast Node, err error) { 17 | s := scanner.New(r, macros) 18 | 19 | more := s.Scan() 20 | var cur *NTerm 21 | for { 22 | gtok := g.Pop() 23 | if gtok == nil { 24 | cur = cur.Parent().(*NTerm) 25 | continue 26 | } 27 | if !more { 28 | if err = s.Err(); err != nil { 29 | return nil, parseError(s, err) 30 | } 31 | 32 | return nil, parseError(s, fmt.Errorf("Expected %v, but found EOF", gtok)) 33 | } 34 | 35 | switch gtok := gtok.(type) { 36 | case pgen.Term: 37 | if !tokensEqual(s.Tok(), gtok) { 38 | return nil, parseError(s, fmt.Errorf("Expected %v (<%v>), but found %v", gtok, cur.nt, s.Tok().Val)) 39 | } 40 | 41 | cur.AddChild(&Term{ 42 | tok: s.Tok(), 43 | 44 | t: gtok, 45 | p: cur, 46 | }) 47 | more = s.Scan() 48 | 49 | case pgen.NTerm: 50 | rule := table[pgen.Lookup{Term: toPGenTerm(s.Tok()), NTerm: gtok}] 51 | if rule == nil { 52 | return nil, parseError(s, fmt.Errorf("No rule for (%v, <%v>)", toPGenTerm(s.Tok()), gtok)) 53 | } 54 | 55 | g.PushRule(rule) 56 | 57 | child := &NTerm{ 58 | nt: gtok, 59 | p: cur, 60 | } 61 | cur.AddChild(child) 62 | cur = child 63 | 64 | case pgen.Epsilon: 65 | cur.AddChild(&Epsilon{ 66 | p: cur, 67 | }) 68 | 69 | case pgen.EOF: 70 | if s.Tok().Type != scanner.EOF { 71 | return nil, parseError(s, fmt.Errorf("EOF expected, but found %v", s.Tok().Type)) 72 | } 73 | return cur, nil 74 | } 75 | } 76 | } 77 | 78 | func tokensEqual(stok scanner.Token, gtok pgen.Token) bool { 79 | switch gtok := gtok.(type) { 80 | case pgen.Term: 81 | return (gtok.Type == stok.Type) && ((stok.Type != scanner.Keyword) || (gtok.Keyword == stok.Val)) 82 | } 83 | 84 | panic(fmt.Errorf("Tried to compare non-terminal: %#v", gtok)) 85 | } 86 | 87 | func toPGenTerm(tok scanner.Token) pgen.Token { 88 | var keyword string 89 | switch tok.Type { 90 | case scanner.Keyword: 91 | keyword = fmt.Sprintf("%v", tok.Val) 92 | case scanner.EOF: 93 | return pgen.EOF{} 94 | } 95 | 96 | return pgen.Term{ 97 | Type: tok.Type, 98 | Keyword: keyword, 99 | } 100 | } 101 | 102 | // A ParseError is returned if an error happens during parsing. 103 | type ParseError struct { 104 | Line, Col int 105 | Err error 106 | } 107 | 108 | func parseError(s *scanner.Scanner, err error) ParseError { 109 | line, col := s.Pos() 110 | return ParseError{ 111 | Line: line, Col: col, 112 | Err: err, 113 | } 114 | } 115 | 116 | func (err ParseError) Error() string { 117 | return fmt.Sprintf("%v:%v: %v", err.Line, err.Col, err.Err) 118 | } 119 | -------------------------------------------------------------------------------- /ast/ast_test.go: -------------------------------------------------------------------------------- 1 | package ast_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/DeedleFake/wdte/ast" 8 | ) 9 | 10 | func printTree(t *testing.T, cur ast.Node, depth int) { 11 | indent := strings.Repeat(" ", depth) 12 | switch cur := cur.(type) { 13 | case *ast.Term: 14 | t.Logf("%v%v", indent, cur) 15 | 16 | case *ast.NTerm: 17 | t.Logf("%v(%v", indent, cur) 18 | for _, c := range cur.Children() { 19 | printTree(t, c, depth+1) 20 | } 21 | t.Logf("%v)", indent) 22 | 23 | case *ast.Epsilon: 24 | t.Logf("%vε", indent) 25 | 26 | default: 27 | t.Fatalf("Unexpected node: %#v", cur) 28 | } 29 | } 30 | 31 | func TestParse(t *testing.T) { 32 | //const test = `"test" => t; + x y => nil;` 33 | 34 | const test = ` 35 | let test => import 'test'; 36 | 37 | let fib n => n { 38 | 0 => 0; 39 | default => + (fib (- n 1)) (fib (- n 2)); 40 | }; 41 | 42 | let memo main => ( 43 | print (fib 5); 44 | print 3; 45 | 3 : a -> + 2 -- * 3 a; 46 | ); 47 | ` 48 | 49 | root, err := ast.Parse(strings.NewReader(test), nil) 50 | if err != nil { 51 | t.Fatal(err) 52 | } 53 | printTree(t, root, 0) 54 | } 55 | -------------------------------------------------------------------------------- /ast/doc.go: -------------------------------------------------------------------------------- 1 | // Package ast provides the parser for WDTE. 2 | package ast 3 | -------------------------------------------------------------------------------- /ast/internal/pgen/pgen.go: -------------------------------------------------------------------------------- 1 | package pgen 2 | 3 | import "github.com/DeedleFake/wdte/scanner" 4 | 5 | //go:generate pgen -out table.go ../../../res/grammar.ebnf 6 | 7 | func newTerm(t string) Term { 8 | switch t { 9 | case "id": 10 | return Term{ 11 | Type: scanner.ID, 12 | } 13 | 14 | case "string": 15 | return Term{ 16 | Type: scanner.String, 17 | } 18 | 19 | case "number": 20 | return Term{ 21 | Type: scanner.Number, 22 | } 23 | } 24 | 25 | return Term{ 26 | Type: scanner.Keyword, 27 | Keyword: t, 28 | } 29 | } 30 | 31 | func newNTerm(nt string) NTerm { 32 | return NTerm(nt) 33 | } 34 | 35 | func newEpsilon() Epsilon { 36 | return Epsilon{} 37 | } 38 | 39 | func newEOF() EOF { 40 | return EOF{} 41 | } 42 | 43 | func newRule(tokens ...Token) (r Rule) { 44 | return Rule(tokens) 45 | } 46 | 47 | type Token interface{} 48 | 49 | type Term struct { 50 | Type scanner.TokenType 51 | Keyword string 52 | } 53 | 54 | func (t Term) String() string { 55 | if t.Type == scanner.Keyword { 56 | return t.Keyword 57 | } 58 | 59 | return t.Type.String() 60 | } 61 | 62 | type NTerm string 63 | 64 | type Epsilon struct{} 65 | 66 | func (e Epsilon) String() string { 67 | return "ε" 68 | } 69 | 70 | type EOF struct{} 71 | 72 | func (e EOF) String() string { 73 | return "Ω" 74 | } 75 | 76 | type Rule []Token 77 | 78 | type Lookup struct { 79 | Term Token 80 | NTerm NTerm 81 | } 82 | -------------------------------------------------------------------------------- /ast/internal/pgen/table.go: -------------------------------------------------------------------------------- 1 | // Code generated by pgen. DO NOT EDIT. 2 | 3 | package pgen 4 | 5 | var Table = map[Lookup]Rule{ 6 | {Term: newTerm("("), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 7 | {Term: newTerm("(@"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 8 | {Term: newTerm("(|"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 9 | {Term: newTerm(";"), NTerm: newNTerm("aexprs")}: newRule(newTerm(";")), 10 | {Term: newTerm("["), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 11 | {Term: newTerm("id"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 12 | {Term: newTerm("import"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 13 | {Term: newTerm("number"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 14 | {Term: newTerm("string"), NTerm: newNTerm("aexprs")}: newRule(newNTerm("exprs")), 15 | {Term: newTerm("]"), NTerm: newNTerm("aexprs")}: newRule(newEpsilon()), 16 | {Term: newTerm("["), NTerm: newNTerm("argdecl")}: newRule(newTerm("["), newNTerm("argdecls"), newTerm(";"), newTerm("]")), 17 | {Term: newTerm("id"), NTerm: newNTerm("argdecl")}: newRule(newTerm("id")), 18 | {Term: newTerm("["), NTerm: newNTerm("argdecls")}: newRule(newNTerm("argdecl"), newNTerm("argdecls")), 19 | {Term: newTerm("id"), NTerm: newNTerm("argdecls")}: newRule(newNTerm("argdecl"), newNTerm("argdecls")), 20 | {Term: newTerm(";"), NTerm: newNTerm("argdecls")}: newRule(newEpsilon()), 21 | {Term: newTerm("=>"), NTerm: newNTerm("argdecls")}: newRule(newEpsilon()), 22 | {Term: newTerm("("), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 23 | {Term: newTerm("(@"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 24 | {Term: newTerm("(|"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 25 | {Term: newTerm("["), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 26 | {Term: newTerm("id"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 27 | {Term: newTerm("import"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 28 | {Term: newTerm("number"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 29 | {Term: newTerm("string"), NTerm: newNTerm("args")}: newRule(newNTerm("single"), newNTerm("args")), 30 | {Term: newTerm("--"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 31 | {Term: newTerm("->"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 32 | {Term: newTerm("-|"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 33 | {Term: newTerm(":"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 34 | {Term: newTerm(";"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 35 | {Term: newTerm("=>"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 36 | {Term: newTerm("{"), NTerm: newNTerm("args")}: newRule(newEpsilon()), 37 | {Term: newTerm("["), NTerm: newNTerm("array")}: newRule(newTerm("["), newNTerm("aexprs"), newTerm("]")), 38 | {Term: newTerm("("), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 39 | {Term: newTerm("(@"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 40 | {Term: newTerm("(|"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 41 | {Term: newTerm("["), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 42 | {Term: newTerm("id"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 43 | {Term: newTerm("import"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 44 | {Term: newTerm("let"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("letexpr"), newTerm(";"), newNTerm("cexprs")), 45 | {Term: newTerm("number"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 46 | {Term: newTerm("string"), NTerm: newNTerm("cexprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("cexprs")), 47 | {Term: newTerm(")"), NTerm: newNTerm("cexprs")}: newRule(newEpsilon()), 48 | {Term: newTerm("|)"), NTerm: newNTerm("cexprs")}: newRule(newEpsilon()), 49 | {Term: newEOF(), NTerm: newNTerm("cexprs")}: newRule(newEpsilon()), 50 | {Term: newTerm("--"), NTerm: newNTerm("chain")}: newRule(newTerm("--"), newNTerm("expr")), 51 | {Term: newTerm("->"), NTerm: newNTerm("chain")}: newRule(newTerm("->"), newNTerm("expr")), 52 | {Term: newTerm("-|"), NTerm: newNTerm("chain")}: newRule(newTerm("-|"), newNTerm("expr")), 53 | {Term: newTerm(";"), NTerm: newNTerm("chain")}: newRule(newEpsilon()), 54 | {Term: newTerm("=>"), NTerm: newNTerm("chain")}: newRule(newEpsilon()), 55 | {Term: newTerm("("), NTerm: newNTerm("compound")}: newRule(newTerm("("), newNTerm("cexprs"), newTerm(")")), 56 | {Term: newTerm("(|"), NTerm: newNTerm("compound")}: newRule(newTerm("(|"), newNTerm("cexprs"), newTerm("|)")), 57 | {Term: newTerm("("), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 58 | {Term: newTerm("(@"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 59 | {Term: newTerm("(|"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 60 | {Term: newTerm("["), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 61 | {Term: newTerm("id"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 62 | {Term: newTerm("import"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 63 | {Term: newTerm("number"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 64 | {Term: newTerm("string"), NTerm: newNTerm("expr")}: newRule(newNTerm("single"), newNTerm("args"), newNTerm("switch"), newNTerm("slot"), newNTerm("chain")), 65 | {Term: newTerm("("), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 66 | {Term: newTerm("(@"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 67 | {Term: newTerm("(|"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 68 | {Term: newTerm("["), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 69 | {Term: newTerm("id"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 70 | {Term: newTerm("import"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 71 | {Term: newTerm("number"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 72 | {Term: newTerm("string"), NTerm: newNTerm("exprs")}: newRule(newNTerm("expr"), newTerm(";"), newNTerm("exprs")), 73 | {Term: newTerm("]"), NTerm: newNTerm("exprs")}: newRule(newEpsilon()), 74 | {Term: newTerm("("), NTerm: newNTerm("funcmods")}: newRule(newTerm("("), newNTerm("expr"), newTerm(";"), newTerm(")"), newNTerm("funcmods")), 75 | {Term: newTerm("id"), NTerm: newNTerm("funcmods")}: newRule(newEpsilon()), 76 | {Term: newTerm("import"), NTerm: newNTerm("import")}: newRule(newTerm("import"), newTerm("string")), 77 | {Term: newTerm("(@"), NTerm: newNTerm("lambda")}: newRule(newTerm("(@"), newNTerm("funcmods"), newTerm("id"), newNTerm("argdecls"), newTerm("=>"), newNTerm("cexprs"), newTerm(")")), 78 | {Term: newTerm("("), NTerm: newNTerm("letassign")}: newRule(newNTerm("funcmods"), newTerm("id"), newNTerm("argdecls"), newTerm("=>"), newNTerm("expr")), 79 | {Term: newTerm("["), NTerm: newNTerm("letassign")}: newRule(newNTerm("argdecl"), newTerm("=>"), newNTerm("expr")), 80 | {Term: newTerm("id"), NTerm: newNTerm("letassign")}: newRule(newNTerm("funcmods"), newTerm("id"), newNTerm("argdecls"), newTerm("=>"), newNTerm("expr")), 81 | {Term: newTerm("let"), NTerm: newNTerm("letexpr")}: newRule(newTerm("let"), newNTerm("letassign")), 82 | {Term: newTerm("("), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 83 | {Term: newTerm("(@"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 84 | {Term: newTerm("(|"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 85 | {Term: newTerm("["), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 86 | {Term: newTerm("id"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 87 | {Term: newTerm("import"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 88 | {Term: newTerm("let"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 89 | {Term: newTerm("number"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 90 | {Term: newTerm("string"), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 91 | {Term: newEOF(), NTerm: newNTerm("script")}: newRule(newNTerm("cexprs"), newEOF()), 92 | {Term: newTerm("("), NTerm: newNTerm("single")}: newRule(newNTerm("subbable")), 93 | {Term: newTerm("(@"), NTerm: newNTerm("single")}: newRule(newNTerm("lambda")), 94 | {Term: newTerm("(|"), NTerm: newNTerm("single")}: newRule(newNTerm("subbable")), 95 | {Term: newTerm("["), NTerm: newNTerm("single")}: newRule(newNTerm("array")), 96 | {Term: newTerm("id"), NTerm: newNTerm("single")}: newRule(newNTerm("subbable")), 97 | {Term: newTerm("import"), NTerm: newNTerm("single")}: newRule(newNTerm("import")), 98 | {Term: newTerm("number"), NTerm: newNTerm("single")}: newRule(newTerm("number")), 99 | {Term: newTerm("string"), NTerm: newNTerm("single")}: newRule(newTerm("string")), 100 | {Term: newTerm(":"), NTerm: newNTerm("slot")}: newRule(newTerm(":"), newNTerm("argdecl")), 101 | {Term: newTerm("--"), NTerm: newNTerm("slot")}: newRule(newEpsilon()), 102 | {Term: newTerm("->"), NTerm: newNTerm("slot")}: newRule(newEpsilon()), 103 | {Term: newTerm("-|"), NTerm: newNTerm("slot")}: newRule(newEpsilon()), 104 | {Term: newTerm(";"), NTerm: newNTerm("slot")}: newRule(newEpsilon()), 105 | {Term: newTerm("=>"), NTerm: newNTerm("slot")}: newRule(newEpsilon()), 106 | {Term: newTerm("."), NTerm: newNTerm("sub")}: newRule(newTerm("."), newNTerm("subbable")), 107 | {Term: newTerm("("), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 108 | {Term: newTerm("(@"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 109 | {Term: newTerm("(|"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 110 | {Term: newTerm("--"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 111 | {Term: newTerm("->"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 112 | {Term: newTerm("-|"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 113 | {Term: newTerm(":"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 114 | {Term: newTerm(";"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 115 | {Term: newTerm("=>"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 116 | {Term: newTerm("["), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 117 | {Term: newTerm("id"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 118 | {Term: newTerm("import"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 119 | {Term: newTerm("number"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 120 | {Term: newTerm("string"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 121 | {Term: newTerm("{"), NTerm: newNTerm("sub")}: newRule(newEpsilon()), 122 | {Term: newTerm("("), NTerm: newNTerm("subbable")}: newRule(newNTerm("compound"), newNTerm("sub")), 123 | {Term: newTerm("(|"), NTerm: newNTerm("subbable")}: newRule(newNTerm("compound"), newNTerm("sub")), 124 | {Term: newTerm("id"), NTerm: newNTerm("subbable")}: newRule(newTerm("id"), newNTerm("sub")), 125 | {Term: newTerm("{"), NTerm: newNTerm("switch")}: newRule(newTerm("{"), newNTerm("switches"), newTerm("}")), 126 | {Term: newTerm("--"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 127 | {Term: newTerm("->"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 128 | {Term: newTerm("-|"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 129 | {Term: newTerm(":"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 130 | {Term: newTerm(";"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 131 | {Term: newTerm("=>"), NTerm: newNTerm("switch")}: newRule(newEpsilon()), 132 | {Term: newTerm("("), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 133 | {Term: newTerm("(@"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 134 | {Term: newTerm("(|"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 135 | {Term: newTerm("["), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 136 | {Term: newTerm("id"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 137 | {Term: newTerm("import"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 138 | {Term: newTerm("number"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 139 | {Term: newTerm("string"), NTerm: newNTerm("switches")}: newRule(newNTerm("expr"), newTerm("=>"), newNTerm("expr"), newTerm(";"), newNTerm("switches")), 140 | {Term: newTerm("}"), NTerm: newNTerm("switches")}: newRule(newEpsilon()), 141 | } 142 | -------------------------------------------------------------------------------- /ast/node.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import ( 4 | "github.com/DeedleFake/wdte/ast/internal/pgen" 5 | "github.com/DeedleFake/wdte/scanner" 6 | ) 7 | 8 | // A Node represents a node of the AST. 9 | type Node interface { 10 | // Parent returns the Node's parent, or nil if it is the root node. 11 | Parent() Node 12 | 13 | // Children returns the node's children in left-to-right order. 14 | Children() []Node 15 | } 16 | 17 | // A Term is a Node that represents a terminal, such as a string or a 18 | // keyword. Terms are always leaf nodes. 19 | type Term struct { 20 | tok scanner.Token 21 | 22 | t pgen.Term 23 | p Node 24 | } 25 | 26 | func (t Term) Parent() Node { 27 | return t.p 28 | } 29 | 30 | // Tok returns the scanner token that the node was generated from. 31 | func (t Term) Tok() scanner.Token { 32 | return t.tok 33 | } 34 | 35 | func (t Term) Children() []Node { 36 | return nil 37 | } 38 | 39 | // An NTerm is a Node that represents a non-terminal. NTerms are 40 | // always parent nodes. 41 | type NTerm struct { 42 | nt pgen.NTerm 43 | p Node 44 | c []Node 45 | } 46 | 47 | // Name returns the name of the non-terminal, minus the surrounding `<` and `>`. 48 | func (nt NTerm) Name() string { 49 | return string(nt.nt) 50 | } 51 | 52 | func (nt NTerm) Parent() Node { 53 | return nt.p 54 | } 55 | 56 | // AddChild adds a child to the right-hand side of the NTerm's list of 57 | // children. 58 | func (nt *NTerm) AddChild(n Node) { 59 | if nt == nil { 60 | return 61 | } 62 | 63 | nt.c = append(nt.c, n) 64 | } 65 | 66 | func (nt NTerm) Children() []Node { 67 | return nt.c 68 | } 69 | 70 | // An Epsilon is a special terminal which represnts a non-action. 71 | type Epsilon struct { 72 | p Node 73 | } 74 | 75 | func (e Epsilon) Parent() Node { 76 | return e.p 77 | } 78 | 79 | func (e Epsilon) Children() []Node { 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /ast/tokenstack.go: -------------------------------------------------------------------------------- 1 | package ast 2 | 3 | import "github.com/DeedleFake/wdte/ast/internal/pgen" 4 | 5 | type tokenStack []pgen.Token 6 | 7 | func (ts *tokenStack) Pop() pgen.Token { 8 | t := (*ts)[len(*ts)-1] 9 | *ts = (*ts)[:len(*ts)-1] 10 | return t 11 | } 12 | 13 | func (ts *tokenStack) Push(t pgen.Token) { 14 | *ts = append(*ts, t) 15 | } 16 | 17 | func (ts *tokenStack) PushRule(r pgen.Rule) { 18 | ts.Push(nil) 19 | for i := len(r) - 1; i >= 0; i-- { 20 | ts.Push(r[i]) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /cmd/pgen/grammar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | // A Grammar is a map from non-terminals to lists of rules. 12 | type Grammar map[NTerm][]Rule 13 | 14 | // LoadGrammar loads a grammar from an io.Reader. 15 | func LoadGrammar(r io.Reader, detectAmbiguity bool) (g Grammar, err error) { 16 | var cur NTerm 17 | g = make(map[NTerm][]Rule) 18 | 19 | s := bufio.NewScanner(r) 20 | for s.Scan() { 21 | line := strings.TrimSpace(s.Text()) 22 | if (len(line) == 0) || (line[0] == '#') { 23 | continue 24 | } 25 | 26 | parts := strings.SplitN(line, "->", 2) 27 | if line[0] == '|' { 28 | parts = strings.SplitN(line, "|", 2) 29 | parts[0] = string(cur) 30 | } 31 | cur = NTerm(strings.TrimSpace(parts[0])) 32 | 33 | parts = strings.Fields(parts[1]) 34 | 35 | rule := make(Rule, 0, 1+len(parts)) 36 | for _, p := range parts { 37 | rule = append(rule, NewToken(p)) 38 | } 39 | g[cur] = append(g[cur], rule) 40 | } 41 | 42 | if detectAmbiguity { 43 | g.detectAmbiguities() 44 | } 45 | 46 | return g, s.Err() 47 | } 48 | 49 | func (g Grammar) detectAmbiguities() { 50 | type lookup struct { 51 | NTerm Token 52 | Term Token 53 | } 54 | known := make(map[lookup]struct{}, len(g)) 55 | 56 | for nterm := range g { 57 | for term, rule := range g.First(nterm) { 58 | if isEpsilon(term) { 59 | continue 60 | } 61 | 62 | _, ambiguous := known[lookup{nterm, term}] 63 | if ambiguous { 64 | fmt.Fprintf(os.Stderr, "Ambiguity for (%v, %v) from %v\n", nterm, term, rule) 65 | } 66 | 67 | known[lookup{nterm, term}] = struct{}{} 68 | } 69 | 70 | if g.Nullable(nterm) { 71 | for term, rule := range g.Follow(nterm) { 72 | if isEpsilon(term) { 73 | continue 74 | } 75 | 76 | _, ambiguous := known[lookup{nterm, term}] 77 | if ambiguous { 78 | fmt.Fprintf(os.Stderr, "Ambiguity for (%v, %v) from %v\n", nterm, term, rule) 79 | } 80 | 81 | known[lookup{nterm, term}] = struct{}{} 82 | } 83 | } 84 | } 85 | } 86 | 87 | // Nullable returns true if the given token is nullable according to 88 | // the grammar. 89 | func (g Grammar) Nullable(tok Token) bool { 90 | switch tok := tok.(type) { 91 | case Term: 92 | return false 93 | 94 | case NTerm: 95 | rules: 96 | for _, rule := range g[tok] { 97 | if rule.Epsilon() { 98 | return true 99 | } 100 | 101 | for _, p := range rule { 102 | if !g.Nullable(p) { 103 | continue rules 104 | } 105 | } 106 | 107 | return true 108 | } 109 | 110 | return false 111 | 112 | case Epsilon: 113 | return true 114 | 115 | case EOF: 116 | return false 117 | } 118 | 119 | panic(fmt.Errorf("Unexpected token type: %T", tok)) 120 | } 121 | 122 | // First returns the first set of the given token. For terminals, the 123 | // first set only contains the terminal in question. For 124 | // non-terminals, the first set is the set of all terminals which can 125 | // appear first in rules that the non-terminal maps to, including ε. 126 | func (g Grammar) First(tok Token) TokenSet { 127 | ts := make(TokenSet) 128 | 129 | switch tok := tok.(type) { 130 | case Term, Epsilon, EOF: 131 | ts.Add(tok, nil) 132 | 133 | case NTerm: 134 | for _, rule := range g[tok] { 135 | for _, p := range rule { 136 | ts.AddAll(g.First(p), rule) 137 | if !g.Nullable(p) { 138 | break 139 | } 140 | } 141 | } 142 | } 143 | 144 | return ts 145 | } 146 | 147 | // Follow returns the follow set of the given non-terminal. The follow 148 | // set consists of every terminal which can appear immediately after 149 | // the non-terminal in the grammar, excluding ε. 150 | func (g Grammar) Follow(nt NTerm) TokenSet { 151 | return g.followWithout(nt, nil) 152 | } 153 | 154 | func (g Grammar) followWithout(nt NTerm, ignore []NTerm) TokenSet { 155 | isIgnored := func(nt NTerm) bool { 156 | for _, i := range ignore { 157 | if i == nt { 158 | return true 159 | } 160 | } 161 | 162 | return false 163 | } 164 | 165 | ts := make(TokenSet) 166 | 167 | for name, rules := range g { 168 | for _, rule := range rules { 169 | for i, tok := range rule { 170 | if tok == nt { 171 | if i == len(rule)-1 { 172 | if !isIgnored(name) { 173 | ts.AddAll(g.followWithout(name, append(ignore, nt)), rule) 174 | } 175 | continue 176 | } 177 | 178 | for i := i + 1; i < len(rule); i++ { 179 | ts.AddAll(g.First(rule[i]), rule) 180 | if !g.Nullable(rule[i]) { 181 | break 182 | } 183 | 184 | if i == len(rule)-1 { 185 | if !isIgnored(name) { 186 | ts.AddAll(g.followWithout(name, append(ignore, nt)), rule) 187 | } 188 | continue 189 | } 190 | } 191 | } 192 | } 193 | } 194 | } 195 | 196 | ts.Remove(Epsilon{}) 197 | return ts 198 | } 199 | 200 | // A Rule is an ordered list of tokens. 201 | type Rule []Token 202 | 203 | // Epsilon returns true if the rule contains only a single ε. 204 | func (r Rule) Epsilon() bool { 205 | return (len(r) == 1) && isEpsilon(r[0]) 206 | } 207 | 208 | // A Token represnts one of four things: 209 | // * A terminal, which is anything that the scanner sends to the parser. 210 | // * A non-terminal, which is structure in the grammar's definition. 211 | // * Epsilon, written as ε, which represents a non-action. 212 | // * EOF, written as Ω. 213 | type Token interface{} 214 | 215 | // NewToken determines the type of a token from the given string and 216 | // returns the appropriate token. 217 | func NewToken(str string) Token { 218 | if (str[0] == '<') && (str[len(str)-1] == '>') { 219 | return NTerm(str) 220 | } 221 | 222 | switch str { 223 | case "ε": 224 | return Epsilon{} 225 | case "Ω": 226 | return EOF{} 227 | } 228 | 229 | return Term(str) 230 | } 231 | 232 | // Term is a terminal token. 233 | type Term string 234 | 235 | // NTerm is a non-terminal token. 236 | type NTerm string 237 | 238 | // Epsilon is an ε token. 239 | type Epsilon struct{} 240 | 241 | func isEpsilon(t Token) bool { 242 | _, ok := t.(Epsilon) 243 | return ok 244 | } 245 | 246 | // EOF is an Ω token. 247 | type EOF struct{} 248 | 249 | // A TokenSet is an unordered set of tokens mapped to the rules that 250 | // put them there. 251 | type TokenSet map[Token]Rule 252 | 253 | // Add adds a token to the token set, mapping it to the given rule. If 254 | // the token is already in the token set, this is a no-op. 255 | func (s TokenSet) Add(t Token, r Rule) { 256 | if _, ok := s[t]; ok { 257 | return 258 | } 259 | 260 | s[t] = r 261 | } 262 | 263 | // AddAll adds all of the tokens from another token set to this one, 264 | // mapping all of them to the given rule. 265 | func (s TokenSet) AddAll(o TokenSet, r Rule) { 266 | for t := range o { 267 | s.Add(t, r) 268 | } 269 | } 270 | 271 | // Contains returns true if t is in s. 272 | func (s TokenSet) Contains(t Token) bool { 273 | _, ok := s[t] 274 | return ok 275 | } 276 | 277 | // Remove removes t from s. 278 | func (s TokenSet) Remove(t Token) { 279 | delete(s, t) 280 | } 281 | -------------------------------------------------------------------------------- /cmd/pgen/pgen.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | ) 8 | 9 | func main() { 10 | flag.Usage = func() { 11 | fmt.Fprintf(os.Stderr, "Usage: %v [options] \n", os.Args[0]) 12 | fmt.Fprintln(os.Stderr) 13 | fmt.Fprintln(os.Stderr, "Options:") 14 | flag.PrintDefaults() 15 | } 16 | output := flag.String("out", "", "File to output to, or stdout if blank.") 17 | detectAmbiguity := flag.Bool("detectAmbiguity", true, "Give warnings for grammar ambiguities.") 18 | flag.Parse() 19 | 20 | if flag.NArg() != 1 { 21 | flag.Usage() 22 | os.Exit(2) 23 | } 24 | 25 | file, err := os.Open(flag.Arg(0)) 26 | if err != nil { 27 | fmt.Fprintf(os.Stderr, "Error opening %q: %v", flag.Arg(0), err) 28 | os.Exit(1) 29 | } 30 | defer file.Close() 31 | 32 | g, err := LoadGrammar(file, *detectAmbiguity) 33 | if err != nil { 34 | fmt.Fprintf(os.Stderr, "Error loading grammar: %v", err) 35 | os.Exit(1) 36 | } 37 | 38 | out := &formatter{w: os.Stdout} 39 | if *output != "" { 40 | file, err := os.Create(*output) 41 | if err != nil { 42 | fmt.Fprintf(os.Stderr, "Error creating %q: %v", *output, err) 43 | os.Exit(1) 44 | } 45 | defer file.Close() 46 | 47 | out.w = file 48 | } 49 | 50 | err = tmpl.Execute(out, g) 51 | if err != nil { 52 | fmt.Fprintf(os.Stderr, "Error printing code: %v", err) 53 | os.Exit(1) 54 | } 55 | 56 | err = out.Close() 57 | if err != nil { 58 | fmt.Fprintf(os.Stderr, "Error formatting code: %v", err) 59 | os.Exit(1) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /cmd/pgen/tmpl.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "io" 8 | "text/template" 9 | ) 10 | 11 | var tmpl = template.New("code") 12 | 13 | func init() { 14 | token := func(t Token) string { 15 | switch t := t.(type) { 16 | case Term: 17 | return fmt.Sprintf("newTerm(%q)", t) 18 | 19 | case NTerm: 20 | return fmt.Sprintf("newNTerm(%q)", t[1:len(t)-1]) 21 | 22 | case Epsilon: 23 | return "newEpsilon()" 24 | 25 | case EOF: 26 | return "newEOF()" 27 | } 28 | 29 | panic(fmt.Errorf("Unexpected token type: %T", t)) 30 | } 31 | 32 | tmpl.Funcs(template.FuncMap{ 33 | "isEpsilon": isEpsilon, 34 | "token": token, 35 | "rule": func(r Rule) string { 36 | var buf bytes.Buffer 37 | buf.WriteString("newRule(") 38 | 39 | var s string 40 | for _, t := range r { 41 | buf.WriteString(s) 42 | buf.WriteString(token(t)) 43 | s = ", " 44 | } 45 | 46 | buf.WriteRune(')') 47 | return buf.String() 48 | }, 49 | }) 50 | 51 | tmpl = template.Must(tmpl.Parse(`// Code generated by pgen. DO NOT EDIT. 52 | 53 | package pgen 54 | 55 | var Table = map[Lookup]Rule{ 56 | {{ range $nterm, $_ := . }} 57 | {{- range $term, $from := $.First $nterm -}} 58 | {{- if isEpsilon $term | not -}} 59 | {Term: {{ $term | token }}, NTerm: {{ $nterm | token -}} }: {{ $from | rule }}, 60 | {{ end -}} 61 | {{ end -}} 62 | 63 | {{- if $.Nullable $nterm -}} 64 | {{- range $term, $from := $.Follow $nterm -}} 65 | {{- if isEpsilon $term | not -}} 66 | {Term: {{ $term | token }}, NTerm: {{ $nterm | token -}} }: newRule(newEpsilon()), 67 | {{ end -}} 68 | {{ end -}} 69 | {{ end -}} 70 | {{ end }} 71 | }`)) 72 | } 73 | 74 | type formatter struct { 75 | w io.Writer 76 | buf bytes.Buffer 77 | } 78 | 79 | func (f *formatter) Write(data []byte) (int, error) { 80 | return f.buf.Write(data) 81 | } 82 | 83 | func (f formatter) Close() error { 84 | src, err := format.Source(f.buf.Bytes()) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | _, err = f.w.Write(src) 90 | return err 91 | } 92 | -------------------------------------------------------------------------------- /cmd/wdte/.gitignore: -------------------------------------------------------------------------------- 1 | wdte 2 | -------------------------------------------------------------------------------- /cmd/wdte/README.md: -------------------------------------------------------------------------------- 1 | wdte 2 | ==== 3 | 4 | wdte is a command-line interpreter for the WDTE scripting language. It provides a basic WDTE environment to run scripts in. Execution is starts in std.Scope with a custom importer. The importer provides full access to the standard library, as well as a few custom features. 5 | 6 | Importer 7 | -------- 8 | 9 | The custom importer provides two main features over std.Import: 10 | 11 | * It provides a `cli` module which gives access to features that make sense from the command-line, such as arguments. 12 | * It provides a means of importing scripts from the file system in much the same way that languages like Python or Ruby do. 13 | 14 | ### cli 15 | 16 | The `cli` module provides the following functions: 17 | 18 | #### args 19 | 20 | ```wdte 21 | args 22 | ``` 23 | 24 | args returns an array containing the arguments passed to the interpreter on the command-line, starting with the path to the current script. 25 | 26 | ### File System Imports 27 | 28 | An import from the file system is attempted if the import string starts with either a `.` or a `/`. If this is true, an import is attempted of the Go plugin at `.so`. If that doesn't exist, the script at `.wdte` is tried instead. 29 | 30 | If a plugin is found, a function with the signature `func S() *wdte.Scope` is called in it. This is expected to return the module's scope. 31 | 32 | If a script is found, the script is parsed with the same importer as the interpreter itself uses, except that any file system imports attempted by that script will be relative to that script's path. 33 | 34 | Note that, due to limitations in the Go `plugin` package, plugin imports currently only work on Linux. 35 | -------------------------------------------------------------------------------- /cmd/wdte/file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | 8 | "github.com/DeedleFake/wdte" 9 | "github.com/DeedleFake/wdte/std" 10 | ) 11 | 12 | func file(im wdte.Importer, file io.Reader) { 13 | m, err := wdte.Parse(file, im, nil) 14 | if err != nil { 15 | fmt.Fprintf(os.Stderr, "Failed to parse script: %v", err) 16 | os.Exit(1) 17 | } 18 | 19 | ret := m.Call(std.F()) 20 | if err, ok := ret.(error); ok { 21 | fmt.Fprintf(os.Stderr, "Script returned an error: %v", err) 22 | os.Exit(3) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/wdte/import.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | 9 | "github.com/DeedleFake/wdte" 10 | "github.com/DeedleFake/wdte/scanner" 11 | "github.com/DeedleFake/wdte/std" 12 | _ "github.com/DeedleFake/wdte/std/all" 13 | ) 14 | 15 | func importScript(from string, im wdte.Importer, macros scanner.MacroMap) (*wdte.Scope, error) { 16 | file, err := os.Open(from + ".wdte") 17 | if err != nil { 18 | return nil, err 19 | } 20 | defer file.Close() 21 | 22 | c, err := wdte.Parse(file, im, macros) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | s, _ := c.Collect(wdte.F().WithScope(std.Scope)) 28 | return s, nil 29 | } 30 | 31 | func importer(wd string, blacklist []string, args []string, macros scanner.MacroMap) wdte.Importer { 32 | wargs := make(wdte.Array, 0, len(args)) 33 | for _, arg := range args { 34 | wargs = append(wargs, wdte.String(arg)) 35 | } 36 | 37 | cliScope := wdte.S().Map(map[wdte.ID]wdte.Func{ 38 | "args": wargs, 39 | }) 40 | 41 | std.Register("cli", cliScope) 42 | 43 | return wdte.ImportFunc(func(from string) (*wdte.Scope, error) { 44 | for _, m := range blacklist { 45 | if from == m { 46 | return nil, fmt.Errorf("%q is blacklisted", from) 47 | } 48 | } 49 | 50 | if strings.HasPrefix(from, ".") || strings.HasPrefix(from, "/") { 51 | path := filepath.FromSlash(from) 52 | im := importer(filepath.Dir(path), blacklist, args, macros) 53 | 54 | s, err := importPlugin(filepath.Join(wd, path), im) 55 | if !os.IsNotExist(err) { 56 | return s, err 57 | } 58 | 59 | s, err = importScript(filepath.Join(wd, path), im, macros) 60 | if !os.IsNotExist(err) { 61 | return s, err 62 | } 63 | } 64 | 65 | return std.Import(from) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /cmd/wdte/import_linux.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "plugin" 7 | 8 | "github.com/DeedleFake/wdte" 9 | ) 10 | 11 | func importPlugin(from string, im wdte.Importer) (*wdte.Scope, error) { 12 | _, err := os.Stat(from + ".so") 13 | if err != nil { 14 | return nil, err 15 | } 16 | 17 | p, err := plugin.Open(from + ".so") 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | init, err := p.Lookup("S") 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | s, ok := init.(func() *wdte.Scope) 28 | if !ok { 29 | return nil, errors.New("S symbol has wrong type") 30 | } 31 | 32 | return s(), nil 33 | } 34 | -------------------------------------------------------------------------------- /cmd/wdte/import_other.go: -------------------------------------------------------------------------------- 1 | // +build !linux 2 | 3 | package main 4 | 5 | import ( 6 | "os" 7 | 8 | "github.com/DeedleFake/wdte" 9 | ) 10 | 11 | func importPlugin(from string, im wdte.Importer) (*wdte.Scope, error) { 12 | return nil, os.ErrNotExist 13 | } 14 | -------------------------------------------------------------------------------- /cmd/wdte/stdin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | 8 | "github.com/DeedleFake/wdte" 9 | "github.com/DeedleFake/wdte/repl" 10 | "github.com/DeedleFake/wdte/scanner" 11 | "github.com/DeedleFake/wdte/std" 12 | "github.com/peterh/liner" 13 | "golang.org/x/crypto/ssh/terminal" 14 | ) 15 | 16 | func printRet(ret wdte.Func) { 17 | switch ret := ret.(type) { 18 | case error, fmt.Stringer: 19 | fmt.Printf(": %v\n", ret) 20 | return 21 | } 22 | 23 | switch k := reflect.Indirect(reflect.ValueOf(ret)).Kind(); k { 24 | case reflect.Chan, reflect.Func, reflect.Map, reflect.Ptr, reflect.Struct, reflect.UnsafePointer: 25 | fmt.Printf(": complex value (%v)\n", k) 26 | 27 | default: 28 | fmt.Printf(": %v\n", ret) 29 | } 30 | } 31 | 32 | func stdin(im wdte.Importer, macros scanner.MacroMap) { 33 | if !terminal.IsTerminal(int(os.Stdin.Fd())) { 34 | file(im, os.Stdin) 35 | return 36 | } 37 | 38 | lr := liner.NewLiner() 39 | lr.SetCtrlCAborts(true) 40 | defer lr.Close() 41 | 42 | const ( 43 | modeTop = ">>> " 44 | modeSub = "... " 45 | ) 46 | 47 | mode := modeTop 48 | next := func() ([]byte, error) { 49 | line, err := lr.Prompt(mode) 50 | if err == nil { 51 | lr.AppendHistory(line) 52 | } 53 | return []byte(line + "\n"), err 54 | } 55 | 56 | r := repl.New(next, im, macros, std.Scope) 57 | 58 | for { 59 | ret, err := r.Next() 60 | if err != nil { 61 | if err == repl.ErrIncomplete { 62 | mode = modeSub 63 | continue 64 | } 65 | 66 | if err == liner.ErrPromptAborted { 67 | r.Cancel() 68 | mode = modeTop 69 | continue 70 | } 71 | 72 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 73 | continue 74 | } 75 | if ret == nil { 76 | break 77 | } 78 | 79 | printRet(ret) 80 | 81 | mode = modeTop 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /cmd/wdte/wdte.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "os" 7 | "runtime" 8 | "strings" 9 | 10 | "github.com/DeedleFake/wdte" 11 | "github.com/DeedleFake/wdte/std/debug" 12 | ) 13 | 14 | func wdteVersion() (string, error) { 15 | v := debug.Version(wdte.F()) 16 | if err, ok := v.(error); ok { 17 | return "", err 18 | } 19 | return string(v.(wdte.String)), nil 20 | } 21 | 22 | func main() { 23 | blacklist := flag.String("blacklist", "", "Comma-separated list of modules that can't be imported.") 24 | eval := flag.String("e", "", "An expression to evaluate instead of reading from a file.") 25 | version := flag.Bool("version", false, "Print the Go and WDTE versions and then exit.") 26 | flag.Usage = func() { 27 | fmt.Fprintf(os.Stderr, "Usage: %v [options] [ | -] [arguments...]\n\n", os.Args[0]) 28 | 29 | fmt.Fprintf(os.Stderr, "Options:\n") 30 | flag.PrintDefaults() 31 | } 32 | flag.Parse() 33 | 34 | if *version { 35 | g := runtime.Version() 36 | w, err := wdteVersion() 37 | if err != nil { 38 | fmt.Fprintf(os.Stderr, "Failed to get WDTE version: %v\n", err) 39 | os.Exit(1) 40 | } 41 | 42 | fmt.Printf("Go: %v\n", g) 43 | fmt.Printf("WDTE: %v\n", w) 44 | return 45 | } 46 | 47 | im := importer("", strings.Split(*blacklist, ","), flag.Args(), nil) 48 | 49 | if *eval != "" { 50 | file(im, strings.NewReader(*eval)) 51 | return 52 | } 53 | 54 | inpath := flag.Arg(0) 55 | switch inpath { 56 | case "", "-": 57 | stdin(im, nil) 58 | 59 | default: 60 | f, err := os.Open(inpath) 61 | if err != nil { 62 | fmt.Fprintf(os.Stderr, "Failed to open %q: %v", inpath, err) 63 | os.Exit(1) 64 | } 65 | defer f.Close() 66 | 67 | file(im, f) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package wdte implements the WDTE scripting language. 2 | // 3 | // WDTE is an embeddable, functionalish scripting language with a 4 | // primary goal of simplicity of use from the embedding side, which is 5 | // what this package provides. 6 | // 7 | // Quick Language Overview 8 | // 9 | // In order to understand how this package works, an overview of the 10 | // language itself is first necessary. WDTE is functional-ish, with 11 | // some emphasis on the "-ish". Although it generally follows a 12 | // functional design, it is not purely functional. In WDTE, everything 13 | // is a function in that everything can be "called", optionally with 14 | // arguments. Value types return themselves, however, allowing them to 15 | // be passed around. 16 | // 17 | // WDTE contains a construct called a "compound" which is similar to a 18 | // function body in most languages. It is surrounded by parentheses 19 | // and contains a semicolon separated list of expressions, with the 20 | // last semicolon being optional. The top-level of a WDTE script is a 21 | // compound without the parentheses. When a compound is executed, each 22 | // expression is evaluated in turn. If any yield an error, that error 23 | // is immediately returned from the entire compound. If not, the 24 | // result of the last expression is returned. 25 | // 26 | // Example 1 27 | // # Declare a function called print3 that takes no arguments. 28 | // let print3 => ( 29 | // let x => 3; 30 | // io.writeln io.stdout x; 31 | // ); 32 | // 33 | // There are very few functions built-in in WDTE, but the standard 34 | // library, found in the std directory and its subdirectories, 35 | // contains a number of useful functions and definitions. For example, 36 | // the stream module contains iterator functionality, which provides a 37 | // means of looping over expressions, something which is otherwise not 38 | // possible. 39 | // 40 | // Example 2 41 | // # Import 'stream' and 'array' and assign them to s and a, 42 | // # respectively. Note that an import is a compile-time operation, 43 | // # unlike normal functions. As such, it must be passed a string 44 | // # literal, not a variable. 45 | // let s => import 'stream'; 46 | // let a => import 'arrays'; 47 | // 48 | // # Create a function called flatten that takes one argument, 49 | // # array. 50 | // let flatten array => 51 | // # Create a new stream that iterates over array. 52 | // a.stream array 53 | // 54 | // # Create a stream from the previous one that performs a flat 55 | // # map operation. The (@ name arg => ...) syntax is a lambda 56 | // # declaration. 57 | // -> s.flatMap (@ f v => v { 58 | // # If the current element of the stream, v, is an array, 59 | // # recursively flatten it into the stream. 60 | // reflect 'Array' => a.stream v -> s.flatMap f; 61 | // }) 62 | // 63 | // # Collect the previous stream into an array. 64 | // -> s.collect 65 | // ; 66 | // 67 | // This example also demonstrates "chains" and "switches", some 68 | // features that seem complicated at first but quickly become second 69 | // nature so with some practice. 70 | // 71 | // A chain is a series of expressions separated by either the chain 72 | // operator, "->", the ignored chain operator, "--", or the error 73 | // chain operator, "-|". Each piece of the chain is executed in turn, 74 | // and the output of the previous section is passed as an argument to 75 | // the output of the current section. In other words, in the previous 76 | // example, the chain's execution matches the following pseudocode 77 | // r1 = a.stream(array) 78 | // r2 = s.flatMap() 79 | // r1 = r2(r1) 80 | // r2 = s.collect 81 | // return r2(r1) 82 | // 83 | // A chain with a use of "--" operates in much the same way, but the 84 | // output of the piece of the chain immediately following the operator 85 | // is ignored, meaning that it doesn't affect the remainder of the 86 | // chain. 87 | // 88 | // The "-|" chain operator is used for error handling. During the 89 | // evaluation of a chain, if no errors have occurred, chain segements 90 | // using "-|" are ignored completely. Unlike with "--", they are 91 | // completely not executed. If, however, an error occurs, all chain 92 | // segments that don't use "-|" are ignored instead. If a "-|" segment 93 | // exists in the chain after the location that the error occurred, 94 | // then that segment is executed next, following which normal 95 | // execution continues, unless that segment itself returned an error. 96 | // If no "-|" segment exists, the error is returned from the entire 97 | // chain. 98 | // 99 | // Chains can also have "slots" assigned to each piece. This is an 100 | // identifier immediately following the expression of a piece of 101 | // chain. This identifier is inserted into the scope for the remainder 102 | // of the chain, allowing manual access to earlier sections of the 103 | // chain. For example 104 | // let io => import 'io'; 105 | // let file => import 'io/file'; 106 | // 107 | // let readFile path => 108 | // file.open path : f 109 | // -> io.string 110 | // -- io.close f 111 | // ; 112 | // 113 | // The aforementioned switch expression is the only conditional 114 | // provided by WDTE. It looks like an expression followed by a 115 | // semicolon separated series of cases in squiggly braces. A case is 116 | // two expressions separated by the assignment operator, "=>". The 117 | // original expression is first evaluated, following which each case's 118 | // left-hand side is evaluated and the result of the original 119 | // expression's evaluation is passed to it. If and only if this call 120 | // results in the boolean value true, the right-hand side of that case 121 | // is returned. If no cases match, the original expression is 122 | // returned. For example, 123 | // func arg1 { 124 | // lhs1 arg2 => rhs1 arg3; 125 | // lhs2 arg4 => rhs2 arg6; 126 | // } 127 | // 128 | // This is analogous to the following pseudocode 129 | // check = func(arg1) 130 | // if lhs := lhs1(arg2); lhs(check) { 131 | // return rhs1(arg3) 132 | // } 133 | // if lhs := lhs2(arg4); lhs(check) { 134 | // return rhs2(arg6) 135 | // } 136 | // return check 137 | // 138 | // A few more minor points exist as well: 139 | // Array literals are a semicolon list of expression surrounded by 140 | // square brackets. Like in compounds and switches, the last 141 | // semicolon is optional. 142 | // 143 | // Identifier parsing rules are very loose; essentially, anything 144 | // that isn't ambiguous with an existing keyword, operator, or 145 | // other syntactic construct is allowed. 146 | // 147 | // All strings are essentially heredocs, allowing newlines like 148 | // they're any other character. There's no difference between 149 | // single-quoted and double-quoted strings. 150 | // 151 | // There are no boolean literals, but the standard library provides 152 | // true and false functions that are essentially the same thing. 153 | // 154 | // Embedding 155 | // 156 | // As previously mentioned, everything in WDTE is a function. In Go 157 | // terms, everything in WDTE implements the Func type defined in this 158 | // package. This includes syntactic constructs as well, such as 159 | // compounds, switches, and chains. 160 | // 161 | // When a script is parsed by one of the parsing functions in this 162 | // package, it is translated into a recursive series of Func 163 | // implementations. The specific types that it is translated to are 164 | // all defined in and exported by this package. For example, the 165 | // top-level of a script, being itself a compound, results in the 166 | // instantiation of a Compound. 167 | // 168 | // What this means in terms of embedding is that the only thing 169 | // required for interaction between Go and WDTE is an interoperative 170 | // layer of Func implementations. As a functional language, WDTE is 171 | // stateless; there is no global interpreter state to keep track of at 172 | // all. Systems for tracking interpreter state, should they be 173 | // required, are provided by the repl package. 174 | // 175 | // When a Func is called, it is passed a Frame. A Frame keeps track of 176 | // anything the function needs that isn't directly an argument to the 177 | // function. This includes the scope in which the Func call should be 178 | // evaluated. For example, the expression 179 | // 180 | // func arg1 arg2 181 | // 182 | // translates to an instance of the FuncCall implementation of Func. 183 | // When the FuncCall is "called", it must be given a scope which 184 | // contains, at a minimum, "func", "arg1", and "arg2", or the call 185 | // will fail with an error. It is through this mechanism that new 186 | // functions can be provided to WDTE. A custom scope can be created 187 | // with new implementations of Func inserted into it. If this scope is 188 | // inserted into a Frame which is then passed to a call of, for 189 | // example, the top-level compound created by parsing a script, they 190 | // will be available during the evaluation. 191 | // 192 | // Example: 193 | // const src = ` 194 | // let io => import 'io'; 195 | // io.writeln io.stdout example; 196 | // ` 197 | // 198 | // c, _ := wdte.Parse(strings.NewReader(src), std.Import) 199 | // 200 | // scope := std.Scope.Add("example", wdte.String("This is an example.")) 201 | // r := c.Call(std.F().WithScope(scope)) 202 | // if err, ok := r.(error); ok { 203 | // log.Fatalln(err) 204 | // } 205 | // 206 | // This will print "This is an example." to stdout. 207 | // 208 | // For convenience, a simple function wrapper around the single method 209 | // required by Func is provided in the form of GoFunc. GoFunc provides 210 | // a number of extra features, such as automatically converting panics 211 | // into errors, but for the most part is just a simple wrapper around 212 | // manual implementations of Func. If more automatic behavior is 213 | // required, possibly at the cost of some runtime performance, 214 | // functions for automatically wrapping Go functions are provided in 215 | // the wdteutil package. 216 | package wdte 217 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DeedleFake/wdte 2 | 3 | require ( 4 | github.com/mattn/go-runewidth v0.0.14 // indirect 5 | github.com/peterh/liner v1.2.2 6 | github.com/rivo/uniseg v0.4.3 // indirect 7 | golang.org/x/crypto v0.6.0 8 | ) 9 | 10 | go 1.13 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 2 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 3 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 4 | github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= 5 | github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= 6 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 7 | github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= 8 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 9 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 10 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 11 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 12 | golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= 13 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 14 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 15 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 16 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 17 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 18 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 19 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 21 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 22 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 23 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 28 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 29 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 30 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 31 | golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= 32 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 35 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 36 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 37 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 38 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 39 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | -------------------------------------------------------------------------------- /readme_test.go: -------------------------------------------------------------------------------- 1 | package wdte_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/DeedleFake/wdte" 9 | "github.com/DeedleFake/wdte/wdteutil" 10 | ) 11 | 12 | const src = ` 13 | let i => import 'some/import/path/or/another'; 14 | 15 | i.print 3; 16 | + 5 2 -> i.print; 17 | 7 -> + 5 -> i.print; 18 | ` 19 | 20 | func im(from string) (*wdte.Scope, error) { 21 | return wdte.S().Map(map[wdte.ID]wdte.Func{ 22 | "print": wdteutil.Func("print", func(v interface{}) interface{} { 23 | fmt.Println(v) 24 | return v 25 | }), 26 | }), nil 27 | } 28 | 29 | func Sum(frame wdte.Frame, args ...wdte.Func) wdte.Func { 30 | frame = frame.Sub("+") 31 | 32 | if len(args) < 2 { 33 | return wdteutil.SaveArgs(wdte.GoFunc(Sum), args...) 34 | } 35 | 36 | var sum wdte.Number 37 | for _, arg := range args { 38 | sum += arg.(wdte.Number) 39 | } 40 | return sum 41 | } 42 | 43 | func Example() { 44 | m, err := wdte.Parse(strings.NewReader(src), wdte.ImportFunc(im), nil) 45 | if err != nil { 46 | fmt.Fprintf(os.Stderr, "Error parsing script: %v\n", err) 47 | os.Exit(1) 48 | } 49 | 50 | scope := wdte.S().Add("+", wdte.GoFunc(Sum)) 51 | 52 | r := m.Call(wdte.F().WithScope(scope)) 53 | if err, ok := r.(error); ok { 54 | fmt.Fprintf(os.Stderr, "Error running script: %v\n", err) 55 | os.Exit(1) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /repl/doc.go: -------------------------------------------------------------------------------- 1 | // Package repl provides a layer intended to help with the development 2 | // of a read-eval-print loop. 3 | package repl 4 | -------------------------------------------------------------------------------- /repl/example_repl_test.go: -------------------------------------------------------------------------------- 1 | package repl_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | 8 | "github.com/DeedleFake/wdte/repl" 9 | "github.com/DeedleFake/wdte/std" 10 | _ "github.com/DeedleFake/wdte/std/all" 11 | "github.com/peterh/liner" 12 | ) 13 | 14 | var ( 15 | mode = ">>> " 16 | ) 17 | 18 | func next(lr *liner.State) repl.NextFunc { 19 | return func() ([]byte, error) { 20 | line, err := lr.Prompt(mode) 21 | return []byte(line + "\n"), err 22 | } 23 | } 24 | 25 | func Example() { 26 | lr := liner.NewLiner() 27 | lr.SetCtrlCAborts(true) 28 | defer lr.Close() 29 | 30 | r := repl.New(next(lr), std.Import, nil, std.Scope) 31 | 32 | for { 33 | ret, err := r.Next() 34 | if err != nil { 35 | if err == repl.ErrIncomplete { 36 | mode = "... " 37 | continue 38 | } 39 | 40 | if err == liner.ErrPromptAborted { 41 | r.Cancel() 42 | mode = ">>> " 43 | continue 44 | } 45 | 46 | fmt.Fprintf(os.Stderr, "Error: %v\n", err) 47 | continue 48 | } 49 | if ret == nil { 50 | break 51 | } 52 | 53 | switch reflect.Indirect(reflect.ValueOf(ret)).Kind() { 54 | case reflect.Struct: 55 | fmt.Printf(": complex value\n") 56 | 57 | default: 58 | fmt.Printf(": %v\n", ret) 59 | } 60 | 61 | mode = ">>> " 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /repl/examples_test.go: -------------------------------------------------------------------------------- 1 | package repl_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/DeedleFake/wdte/repl" 8 | ) 9 | 10 | func ExamplePartial() { 11 | stack, partial := repl.Partial(strings.NewReader("let io =>"), nil, nil) 12 | fmt.Println(partial) 13 | 14 | stack, partial = repl.Partial(strings.NewReader("import 'io'"), stack, nil) 15 | fmt.Println(partial) 16 | 17 | _, partial = repl.Partial(strings.NewReader(";"), stack, nil) 18 | fmt.Println(partial) 19 | // Output: true 20 | // true 21 | // false 22 | } 23 | -------------------------------------------------------------------------------- /repl/repl.go: -------------------------------------------------------------------------------- 1 | package repl 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "io" 8 | 9 | "github.com/DeedleFake/wdte" 10 | "github.com/DeedleFake/wdte/scanner" 11 | ) 12 | 13 | var ( 14 | // ErrIncomplete is returned by REPL.Next() if it expects more input 15 | // before it will begin evaluation. 16 | ErrIncomplete = errors.New("input incomplete") 17 | ) 18 | 19 | // A NextFunc returns the next piece of code to be interpreted. When 20 | // reading from stdin, this is likely the next line entered. 21 | // 22 | // If NextFunc should not be called again, it should return 23 | // nil, io.EOF. 24 | type NextFunc func() ([]byte, error) 25 | 26 | // SimpleNext returns a next func that scans lines from r. 27 | func SimpleNext(r io.Reader) NextFunc { 28 | s := bufio.NewScanner(r) 29 | return func() ([]byte, error) { 30 | more := s.Scan() 31 | if !more { 32 | err := s.Err() 33 | if err != nil { 34 | err = io.EOF 35 | } 36 | return nil, err 37 | } 38 | 39 | return s.Bytes(), nil 40 | } 41 | } 42 | 43 | // REPL provides a means to track a global state and interpret 44 | // separate lines of a WDTE script, allowing for the implementation of 45 | // a read-eval-print loop. 46 | type REPL struct { 47 | // Scope is the scope that the next line will be executed in. It is 48 | // automatically updated every time an executed line changes the 49 | // scope. 50 | Scope *wdte.Scope 51 | 52 | next NextFunc 53 | im wdte.Importer 54 | macros scanner.MacroMap 55 | 56 | stack []string 57 | buf []byte 58 | } 59 | 60 | // New creates a new REPL which reads with next, imports using im, and 61 | // executes the first line with the scope start. 62 | func New(next NextFunc, im wdte.Importer, macros scanner.MacroMap, start *wdte.Scope) *REPL { 63 | return &REPL{ 64 | next: next, 65 | im: im, 66 | macros: macros, 67 | Scope: start, 68 | } 69 | } 70 | 71 | // Next reads and evaluates the next line of input. It returns the 72 | // value returned from that line, or an error if one is encountered. 73 | // If the end of the input has been reached, it will return nil, nil. 74 | // 75 | // If an input ends in a partial expression, such as a single line of 76 | // a mult-line expression, nil, ErrIncomplete is returned. 77 | func (r *REPL) Next() (ret wdte.Func, err error) { 78 | defer func() { 79 | switch e := recover().(type) { 80 | case error: 81 | err = e 82 | 83 | case nil: 84 | 85 | default: 86 | panic(e) 87 | } 88 | }() 89 | 90 | src, err := r.next() 91 | if err != nil { 92 | if err == io.EOF { 93 | err = nil 94 | } 95 | return nil, err 96 | } 97 | 98 | stack, partial := Partial(bytes.NewReader(src), r.stack, r.macros) 99 | r.stack = stack 100 | if partial { 101 | r.buf = append(r.buf, src...) 102 | return nil, ErrIncomplete 103 | } 104 | 105 | src = append(r.buf, src...) 106 | r.buf = r.buf[:0] 107 | 108 | m, err := wdte.Parse(bytes.NewReader(src), r.im, r.macros) 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | frame := wdte.F().WithScope(r.Scope) 114 | next, ret := m.Collect(frame) 115 | if err, ok := ret.(error); ok { 116 | return nil, err 117 | } 118 | 119 | r.Scope = r.Scope.Sub(next) 120 | return ret.Call(frame), nil 121 | } 122 | 123 | // Cancel cancels a partial expression. This is useful if, for 124 | // example, a user sends an interrupt to a command-line REPL while 125 | // entering a subsequent line of a multi-line expression. 126 | // 127 | // If a partial expression is not in progress, this has no effect. 128 | func (r *REPL) Cancel() { 129 | r.stack = r.stack[:0] 130 | r.buf = r.buf[:0] 131 | } 132 | 133 | func peek(stack []string) string { 134 | if len(stack) == 0 { 135 | return "" 136 | } 137 | 138 | return stack[len(stack)-1] 139 | } 140 | 141 | func pop(stack []string) (string, []string) { 142 | if len(stack) == 0 { 143 | return "", stack 144 | } 145 | 146 | p := stack[len(stack)-1] 147 | stack = stack[:len(stack)-1] 148 | return p, stack 149 | } 150 | 151 | // Partial checks if an expression, read from r, is incomplete. The 152 | // initial value of stack should be nil, and subsequent values should 153 | // be the value of the first return. The second return is true if the 154 | // expression was incomplete. 155 | func Partial(r io.Reader, stack []string, macros scanner.MacroMap) ([]string, bool) { 156 | s := scanner.New(r, macros) 157 | var prev scanner.Token 158 | for s.Scan() { 159 | switch tok := s.Tok(); tok.Type { 160 | case scanner.Keyword: 161 | switch v := tok.Val.(string); v { 162 | case "(", "(@": 163 | stack = append(stack, ")") 164 | case "[": 165 | stack = append(stack, "]") 166 | case "{": 167 | stack = append(stack, "}") 168 | 169 | case ")", "]", "}": 170 | if v == peek(stack) { 171 | _, stack = pop(stack) 172 | } 173 | } 174 | 175 | case scanner.EOF: 176 | if (len(stack) > 0) || (prev.Val != ";") { 177 | return stack, true 178 | } 179 | } 180 | 181 | prev = s.Tok() 182 | } 183 | 184 | return stack, false 185 | } 186 | -------------------------------------------------------------------------------- /repl/repl_test.go: -------------------------------------------------------------------------------- 1 | package repl_test 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/DeedleFake/wdte/repl" 10 | "github.com/DeedleFake/wdte/std" 11 | ) 12 | 13 | func TestREPL(t *testing.T) { 14 | src := bufio.NewReader(strings.NewReader(`let test [a b] => + a b;`)) 15 | r := repl.New(func() ([]byte, error) { 16 | return src.ReadBytes('\n') 17 | }, std.Import, nil, nil) 18 | 19 | ret, err := r.Next() 20 | if err != nil { 21 | t.Error(err) 22 | } 23 | 24 | fmt.Printf("%#v\n", ret) 25 | } 26 | -------------------------------------------------------------------------------- /res/grammar.ebnf: -------------------------------------------------------------------------------- 1 |