aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2018-10-09 10:37:56 +0200
committerPřemysl Janouch <p@janouch.name>2018-10-09 10:42:20 +0200
commitf751975cfd967c717473fea400d926a4c9f8beb1 (patch)
treea09b8095acdc5e535c345fbbc8a39248fc816168
parent55a107636733320f6ae84920a171799f102cefe1 (diff)
downloadell-f751975cfd967c717473fea400d926a4c9f8beb1.tar.gz
ell-f751975cfd967c717473fea400d926a4c9f8beb1.tar.xz
ell-f751975cfd967c717473fea400d926a4c9f8beb1.zip
Add a port to Go
-rw-r--r--README.adoc5
-rw-r--r--cmd/interpreter/main.go63
-rw-r--r--cmd/repl/main.go91
-rw-r--r--ell/ell.go1294
4 files changed, 1451 insertions, 2 deletions
diff --git a/README.adoc b/README.adoc
index ebc324f..a8ceec3 100644
--- a/README.adoc
+++ b/README.adoc
@@ -6,8 +6,9 @@ ell
a programming language implementable with as little code as possible while
still being reasonably comfortable to use.
-This package is an implementation of said language, meant to be self-contained,
-portable and reusable. Performance is specifically not an intent.
+This package contains two implementations of said language--one in C and
+another in Go--which are meant to be self-contained, portable and reusable.
+Performance is specifically not an intent.
The project is currently in a "proof of concept" stage with many useful data
operations missing but I believe it won't be a problem to implement them as
diff --git a/cmd/interpreter/main.go b/cmd/interpreter/main.go
new file mode 100644
index 0000000..822aec8
--- /dev/null
+++ b/cmd/interpreter/main.go
@@ -0,0 +1,63 @@
+//
+// Copyright (c) 2018, Přemysl Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// Program interpreter is a basic ell interpreter.
+package main
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+
+ "janouch.name/ell/ell"
+)
+
+func main() {
+ var script []byte
+ var err error
+
+ if len(os.Args) < 2 {
+ script, err = ioutil.ReadAll(os.Stdin)
+ } else {
+ script, err = ioutil.ReadFile(os.Args[1])
+ }
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+
+ L := ell.New()
+ if !ell.StdInitialize(L) {
+ fmt.Printf("runtime library initialization failed: %s\n", L.Error)
+ }
+
+ program, err := ell.NewParser(script).Run()
+ if err != nil {
+ fmt.Printf("%s: %s\n", "parse error", err)
+ os.Exit(1)
+ }
+
+ var args *ell.V
+ tail := &args
+ for i := 2; i < len(os.Args); i++ {
+ *tail = ell.NewString([]byte(os.Args[i]))
+ tail = &(*tail).Next
+ }
+
+ var result *ell.V
+ if !L.EvalBlock(program, args, &result) {
+ fmt.Printf("%s: %s\n", "runtime error", L.Error)
+ }
+}
diff --git a/cmd/repl/main.go b/cmd/repl/main.go
new file mode 100644
index 0000000..e7f35fb
--- /dev/null
+++ b/cmd/repl/main.go
@@ -0,0 +1,91 @@
+//
+// Copyright (c) 2018, Přemysl Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// Program repl is an interactive ell interpreter.
+package main
+
+import (
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ // This library is rather simplistic but it's going to serve us fine.
+ "github.com/peterh/liner"
+
+ "janouch.name/ell/ell"
+)
+
+func run(L *ell.Ell, program *ell.V) {
+ var result *ell.V
+ if !L.EvalBlock(program, nil, &result) {
+ fmt.Printf("\x1b[31m%s: %s\x1b[0m\n", "runtime error", L.Error)
+ L.Error = ""
+ } else {
+ ell.PrintSeq(os.Stdout, result)
+ os.Stdout.WriteString("\n")
+ }
+}
+
+func complete(L *ell.Ell, line string) (res []string) {
+ // This never actually completes anything, just shows the options,
+ // we'd have to figure out the longest common prefix.
+ res = append(res, line)
+
+ line = strings.ToLower(line)
+ for v := L.Globals; v != nil; v = v.Next {
+ name := string(v.Head.String)
+ if strings.HasPrefix(strings.ToLower(name), line) {
+ res = append(res, name)
+ }
+ }
+ for name := range L.Native {
+ if strings.HasPrefix(strings.ToLower(name), line) {
+ res = append(res, name)
+ }
+ }
+ return
+}
+
+func main() {
+ L := ell.New()
+ if !ell.StdInitialize(L) {
+ fmt.Printf("runtime library initialization failed: %s\n", L.Error)
+ }
+
+ line := liner.NewLiner()
+ line.SetCompleter(func(line string) []string { return complete(L, line) })
+ line.SetMultiLineMode(true)
+ line.SetTabCompletionStyle(liner.TabPrints)
+
+ for {
+ script, err := line.Prompt("> ")
+ if err == nil {
+ line.AppendHistory(script)
+
+ p := ell.NewParser([]byte(script))
+ if program, err := p.Run(); err != nil {
+ fmt.Printf("\x1b[31m%s: %s\x1b[0m\n", "parse error", err)
+ } else {
+ run(L, program)
+ }
+ } else if err == liner.ErrPromptAborted || err == io.EOF {
+ break
+ } else {
+ fmt.Printf("\x1b[31m%s: %s\x1b[0m\n", "error", err)
+ }
+ }
+ os.Stdout.WriteString("\n")
+}
diff --git a/ell/ell.go b/ell/ell.go
new file mode 100644
index 0000000..fec359f
--- /dev/null
+++ b/ell/ell.go
@@ -0,0 +1,1294 @@
+//
+// Copyright (c) 2018, Přemysl Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// Package ell implements a simple scripting language.
+package ell
+
+import (
+ "errors"
+ "fmt"
+ "io"
+
+ // standard library
+ "bytes"
+ "os"
+ "os/exec"
+)
+
+// --- Values ------------------------------------------------------------------
+
+// VType denotes the type of a value.
+type VType int
+
+const (
+ // VTypeString denotes a string value.
+ VTypeString VType = iota
+ // VTypeList denotes a list value.
+ VTypeList
+)
+
+// V is a value in the ell language.
+type V struct {
+ Type VType // the type of this value
+ Next *V // next value in sequence
+ Head *V // the head of a VTypeList
+ String []byte // the immutable contents of a VTypeString
+}
+
+// Clone clones a value without following the rest of its chain.
+func (v *V) Clone() *V {
+ if v == nil {
+ return nil
+ }
+ return &V{
+ Type: v.Type,
+ Next: nil,
+ Head: v.Head.CloneSeq(),
+ // TODO: Consider actually storing the string as a string,
+ // so that the compiler/runtime assure its immutability.
+ String: v.String,
+ }
+}
+
+// CloneSeq clones a value including the rest of its chain.
+func (v *V) CloneSeq() *V {
+ var head *V
+ for out := &head; v != nil; v = v.Next {
+ *out = v.Clone()
+ out = &(*out).Next
+ }
+ return head
+}
+
+// NewString creates a new value containing a string.
+func NewString(string []byte) *V {
+ return &V{
+ Type: VTypeString,
+ String: string,
+ }
+}
+
+// NewList creates a new list value containing the given sequence.
+func NewList(head *V) *V {
+ return &V{
+ Type: VTypeList,
+ Head: head,
+ }
+}
+
+// --- Lexer -------------------------------------------------------------------
+
+type token int
+
+const (
+ tAbort token = iota
+ tLParen
+ tRParen
+ tLBracket
+ tRBracket
+ tLBrace
+ tRBrace
+ tString
+ tNewline
+ tAt
+)
+
+func (t token) String() string {
+ switch t {
+ case tAbort:
+ return "end of input"
+ case tLParen:
+ return "left parenthesis"
+ case tRParen:
+ return "right parenthesis"
+ case tLBracket:
+ return "left bracket"
+ case tRBracket:
+ return "right bracket"
+ case tLBrace:
+ return "left brace"
+ case tRBrace:
+ return "right brace"
+ case tString:
+ return "string"
+ case tNewline:
+ return "newline"
+ case tAt:
+ return "at symbol"
+ }
+ panic("unknown token")
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+type lexer struct {
+ p []byte // unread input
+ line, column int // current line and column
+ buf []byte // parsed string value
+}
+
+func newLexer(p []byte) *lexer {
+ return &lexer{p: p}
+}
+
+func (lex *lexer) advance() byte {
+ ch := lex.p[0]
+ lex.p = lex.p[1:]
+
+ if ch == '\n' {
+ lex.column = 0
+ lex.line++
+ } else {
+ lex.column++
+ }
+ return ch
+}
+
+var lexerHexAlphabet = "0123456789abcdef"
+
+// fromHex converts a nibble from hexadecimal. Avoiding dependencies.
+func lexerFromHex(ch byte) int {
+ if ch >= 'A' && ch <= 'Z' {
+ ch += 32
+ }
+ for i := 0; i < len(lexerHexAlphabet); i++ {
+ if lexerHexAlphabet[i] == ch {
+ return i
+ }
+ }
+ return -1
+}
+
+func (lex *lexer) hexaEscape() bool {
+ if len(lex.p) < 2 {
+ return false
+ }
+ h := lexerFromHex(lex.advance())
+ if h < 0 {
+ return false
+ }
+ l := lexerFromHex(lex.advance())
+ if l < 0 {
+ return false
+ }
+ lex.buf = append(lex.buf, byte(h<<4|l))
+ return true
+
+}
+
+const (
+ lexerStringQuote = '\''
+ lexerEscape = '\\'
+ lexerComment = '#'
+)
+
+func lexerIsWhitespace(ch byte) bool {
+ return ch == 0 || ch == ' ' || ch == '\t' || ch == '\r'
+}
+
+var lexerEscapes = map[byte]byte{
+ lexerStringQuote: lexerStringQuote,
+ lexerEscape: lexerEscape,
+ 'a': '\a',
+ 'b': '\b',
+ 'n': '\n',
+ 'r': '\r',
+ 't': '\t',
+}
+
+func (lex *lexer) escapeSequence() error {
+ if len(lex.p) == 0 {
+ return errors.New("premature end of escape sequence")
+ }
+ ch := lex.advance()
+ if ch == 'x' {
+ if lex.hexaEscape() {
+ return nil
+ }
+ return errors.New("invalid hexadecimal escape")
+ }
+ ch, ok := lexerEscapes[ch]
+ if !ok {
+ return errors.New("unknown escape sequence")
+ }
+ lex.buf = append(lex.buf, ch)
+ return nil
+}
+
+func (lex *lexer) string() error {
+ for len(lex.p) > 0 {
+ ch := lex.advance()
+ if ch == lexerStringQuote {
+ return nil
+ }
+ if ch != lexerEscape {
+ lex.buf = append(lex.buf, ch)
+ } else if err := lex.escapeSequence(); err != nil {
+ return err
+ }
+ }
+ return errors.New("premature end of string")
+}
+
+var lexerTokens = map[byte]token{
+ '(': tLParen,
+ ')': tRParen,
+ '[': tLBracket,
+ ']': tRBracket,
+ '{': tLBrace,
+ '}': tRBrace,
+ ';': tNewline,
+ '\n': tNewline,
+ '@': tAt,
+ lexerStringQuote: tString,
+}
+
+func (lex *lexer) next() (token, error) {
+ for len(lex.p) > 0 && lexerIsWhitespace(lex.p[0]) {
+ lex.advance()
+ }
+ if len(lex.p) == 0 {
+ return tAbort, nil
+ }
+
+ lex.buf = nil
+
+ ch := lex.advance()
+ if ch == lexerComment {
+ for len(lex.p) > 0 {
+ if ch := lex.advance(); ch == '\n' {
+ return tNewline, nil
+ }
+ }
+ return tAbort, nil
+ }
+
+ token, ok := lexerTokens[ch]
+ if !ok {
+ lex.buf = append(lex.buf, ch)
+ for len(lex.p) > 0 && !lexerIsWhitespace(lex.p[0]) &&
+ lexerTokens[lex.p[0]] == 0 /* ugly but short */ {
+ lex.buf = append(lex.buf, lex.advance())
+ }
+ return tString, nil
+ }
+
+ if token == tString {
+ if err := lex.string(); err != nil {
+ return tAbort, err
+ }
+ }
+ return token, nil
+}
+
+func (lex *lexer) errorf(format string, a ...interface{}) error {
+ return fmt.Errorf("at or before line %d, column %d: %s",
+ lex.line+1, lex.column+1, fmt.Sprintf(format, a...))
+}
+
+// --- Printing ----------------------------------------------------------------
+
+func printStringNeedsQuoting(s *V) bool {
+ for i := 0; i < len(s.String); i++ {
+ ch := s.String[i]
+ if lexerIsWhitespace(ch) || lexerTokens[ch] != 0 ||
+ ch == lexerEscape || ch < 32 {
+ return true
+ }
+ }
+ return len(s.String) == 0
+}
+
+func printString(w io.Writer, s *V) bool {
+ if s.Type != VTypeString {
+ return false
+ }
+ if !printStringNeedsQuoting(s) {
+ _, _ = w.Write(s.String)
+ return true
+ }
+
+ _, _ = w.Write([]byte{lexerStringQuote})
+ for i := 0; i < len(s.String); i++ {
+ ch := s.String[i]
+ if ch < 32 {
+ _, _ = fmt.Fprintf(w, "\\x%02x", ch)
+ } else if ch == lexerEscape || ch == lexerStringQuote {
+ _, _ = fmt.Fprintf(w, "\\%c", ch)
+ } else {
+ _, _ = w.Write([]byte{ch})
+ }
+ }
+ _, _ = w.Write([]byte{lexerStringQuote})
+ return true
+}
+
+func printBlock(w io.Writer, list *V) bool {
+ if list.Head == nil || string(list.Head.String) != "block" {
+ return false
+ }
+
+ list = list.Head.Next
+ for line := list; line != nil; line = line.Next {
+ if line.Type != VTypeList {
+ return false
+ }
+ }
+
+ _, _ = w.Write([]byte{'{'})
+ for line := list; line != nil; line = line.Next {
+ _, _ = w.Write([]byte{' '})
+ PrintSeq(w, line.Head)
+
+ if line.Next != nil {
+ _, _ = w.Write([]byte{';'})
+ } else {
+ _, _ = w.Write([]byte{' '})
+ }
+ }
+ _, _ = w.Write([]byte{'}'})
+ return true
+}
+
+func printSet(w io.Writer, list *V) bool {
+ if list.Head == nil || string(list.Head.String) != "set" ||
+ list.Head.Next == nil || list.Head.Next.Next != nil {
+ return false
+ }
+
+ _, _ = w.Write([]byte{'@'})
+ PrintSeq(w, list.Head.Next)
+ return true
+}
+
+func printList(w io.Writer, list *V) bool {
+ if list.Head == nil || string(list.Head.String) != "list" {
+ return false
+ }
+ _, _ = w.Write([]byte{'['})
+ PrintSeq(w, list.Head.Next)
+ _, _ = w.Write([]byte{']'})
+ return true
+}
+
+// PrintV serializes a value to the given writer, ignoring I/O errors.
+func PrintV(w io.Writer, v *V) {
+ if printString(w, v) ||
+ printBlock(w, v) ||
+ printSet(w, v) ||
+ printList(w, v) {
+ return
+ }
+
+ _, _ = w.Write([]byte{'('})
+ PrintSeq(w, v.Head)
+ _, _ = w.Write([]byte{')'})
+}
+
+// PrintSeq serializes a sequence of values to the given writer.
+func PrintSeq(w io.Writer, v *V) {
+ for ; v != nil; v = v.Next {
+ PrintV(w, v)
+ if v.Next != nil {
+ _, _ = w.Write([]byte{' '})
+ }
+ }
+}
+
+// --- Parsing -----------------------------------------------------------------
+
+// Parser is a context for parsing.
+type Parser struct {
+ lexer *lexer // tokenizer
+ token token // current token in the lexer
+ replaceToken bool // replace the token
+}
+
+// NewParser returns a new parser for the give byte slice.
+func NewParser(script []byte) *Parser {
+ // As reading in tokens may cause exceptions, we wait for the first peek
+ // to replace the initial ELLT_ABORT.
+ return &Parser{
+ lexer: newLexer(script),
+ replaceToken: true,
+ }
+}
+
+func (p *Parser) peek() token {
+ if p.replaceToken {
+ token, err := p.lexer.next()
+ if err != nil {
+ panic(p.lexer.errorf("%s", err))
+ }
+ p.token = token
+ p.replaceToken = false
+ }
+ return p.token
+}
+
+func (p *Parser) accept(token token) bool {
+ p.replaceToken = p.peek() == token
+ return p.replaceToken
+}
+
+func (p *Parser) expect(token token) {
+ if !p.accept(token) {
+ panic(p.lexer.errorf("unexpected `%s', expected `%s'", p.token, token))
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func (p *Parser) skipNL() {
+ for p.accept(tNewline) {
+ }
+}
+
+func parsePrefixList(seq *V, name string) *V {
+ prefix := NewString([]byte(name))
+ prefix.Next = seq
+ return NewList(prefix)
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func (p *Parser) parseV() *V {
+ var result *V
+ tail := &result
+
+ p.skipNL()
+ switch {
+ case p.accept(tString):
+ return NewString(p.lexer.buf)
+ case p.accept(tAt):
+ result = p.parseV()
+ return parsePrefixList(result, "set")
+ case p.accept(tLParen):
+ for !p.accept(tRParen) {
+ *tail = p.parseV()
+ tail = &(*tail).Next
+ p.skipNL()
+ }
+ return NewList(result)
+ case p.accept(tLBracket):
+ for !p.accept(tRBracket) {
+ *tail = p.parseV()
+ tail = &(*tail).Next
+ p.skipNL()
+ }
+ return parsePrefixList(result, "list")
+ case p.accept(tLBrace):
+ for {
+ *tail = p.parseLine()
+ if *tail == nil {
+ break
+ }
+ tail = &(*tail).Next
+ }
+ p.expect(tRBrace)
+ return parsePrefixList(result, "block")
+ }
+ panic(p.lexer.errorf("unexpected `%s', expected a value", p.token))
+}
+
+func (p *Parser) parseLine() *V {
+ var result *V
+ tail := &result
+
+ for p.peek() != tRBrace && p.peek() != tAbort {
+ if !p.accept(tNewline) {
+ *tail = p.parseV()
+ tail = &(*tail).Next
+ } else if result != nil {
+ return NewList(result)
+ }
+ }
+ if result != nil {
+ return NewList(result)
+ }
+ return nil
+}
+
+// Run runs the parser and returns a value to be interpreted or an error.
+func (p *Parser) Run() (result *V, err error) {
+ // "The convention in the Go libraries is that even when a package
+ // uses panic internally, its external API still presents explicit
+ // error return values." We're good.
+ defer func() {
+ if r := recover(); r != nil {
+ result, err = nil, r.(error)
+ }
+ }()
+
+ tail := &result
+ for {
+ *tail = p.parseLine()
+ if *tail == nil {
+ break
+ }
+ tail = &(*tail).Next
+ }
+ p.expect(tAbort)
+ return result, nil
+}
+
+// --- Runtime -----------------------------------------------------------------
+
+// Handler is a Go handler for an Ell function.
+type Handler func(*Ell, *V, **V) bool
+
+// Ell is an interpreter context.
+type Ell struct {
+ Globals *V // list of global variables
+ scopes *V // dynamic scopes from the newest
+ Native map[string]Handler // maps strings to Go functions
+
+ Error string // error information
+}
+
+// New returns a new interpreter context ready for program execution.
+func New() *Ell {
+ return &Ell{
+ Native: make(map[string]Handler),
+ }
+}
+
+func scopeFind(scope **V, name string) **V {
+ for ; *scope != nil; scope = &(*scope).Next {
+ if string((*scope).Head.String) == name {
+ return scope
+ }
+ }
+ return nil
+}
+
+func scopePrepend(scope **V, name string, v *V) {
+ key := NewString([]byte(name))
+ pair := NewList(key)
+
+ key.Next = v
+ pair.Next = *scope
+ *scope = pair
+}
+
+// Get retrieves a value by name from the scope or from global variables.
+func (ell *Ell) Get(name string) *V {
+ var place **V
+ for scope := ell.scopes; scope != nil; scope = scope.Next {
+ if place = scopeFind(&scope.Head, name); place != nil {
+ return (*place).Head.Next
+ }
+ }
+ if place = scopeFind(&ell.Globals, name); place != nil {
+ return (*place).Head.Next
+ }
+ return nil
+}
+
+// Set sets a value by name in the scope or in global variables.
+func (ell *Ell) Set(name string, v *V) {
+ var place **V
+ for scope := ell.scopes; scope != nil; scope = scope.Next {
+ if place = scopeFind(&scope.Head, name); place != nil {
+ (*place).Head.Next = v
+ return
+ }
+ }
+
+ // Variables only get deleted by "arg" or from the global scope.
+ if place = scopeFind(&ell.Globals, name); place != nil {
+ *place = (*place).Next
+ }
+ scopePrepend(&ell.Globals, name, v)
+}
+
+// NativeFind returns the handler for a native function or nil.
+func (ell *Ell) NativeFind(name string) Handler {
+ return ell.Native[name]
+}
+
+// NativeRegister registers a native Go function handler.
+func (ell *Ell) NativeRegister(name string, handler Handler) {
+ ell.Native[name] = handler
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Errorf sets an error message in the interpreter context and returns false.
+func (ell *Ell) Errorf(format string, args ...interface{}) bool {
+ ell.Error = fmt.Sprintf(format, args...)
+ return false
+}
+
+func (ell *Ell) canModifyError() bool {
+ // Errors starting with an underscore are exceptions and would not work
+ // with stack traces generated this way.
+ return ell.Error == "" || ell.Error[0] != '_'
+}
+
+func (ell *Ell) evalArgs(args *V, result **V) bool {
+ var res *V
+ out := &res
+
+ i := 0
+ for ; args != nil; args = args.Next {
+ var evaluated *V
+ // Arguments should not evaporate, default to a nil value.
+ if !ell.evalStatement(args, &evaluated) {
+ goto error
+ }
+ if evaluated == nil {
+ evaluated = NewList(nil)
+ }
+ evaluated.Next = nil
+ *out = evaluated
+ out = &(*out).Next
+ i++
+ }
+ *result = res
+ return true
+
+error:
+ // Once the code flows like this, at least make some use of it.
+ if ell.canModifyError() {
+ ell.Errorf("(argument %d) -> %s", i, ell.Error)
+ }
+ return false
+}
+
+func (ell *Ell) evalNative(name string, args *V, result **V) bool {
+ fn := ell.NativeFind(name)
+ if fn == nil {
+ return ell.Errorf("unknown function")
+ }
+
+ var arguments *V
+ if !ell.evalArgs(args, &arguments) {
+ return false
+ }
+ return fn(ell, arguments, result)
+}
+
+func (ell *Ell) evalResolved(body *V, args *V, result **V) bool {
+ // Resolving names recursively could be pretty fatal, let's not do that.
+ if body.Type == VTypeString {
+ *result = body.Clone()
+ return true
+ }
+ var arguments *V
+ return ell.evalArgs(args, &arguments) &&
+ ell.EvalBlock(body.Head, arguments, result)
+}
+
+func (ell *Ell) evalValue(body *V, result **V) bool {
+ args := body.Next
+ if body.Type == VTypeString {
+ name := string(body.String)
+ if name == "block" {
+ if args != nil {
+ *result = NewList(args.CloneSeq())
+ }
+ return true
+ }
+ if body := ell.Get(name); body != nil {
+ return ell.evalResolved(body, args, result)
+ }
+ return ell.evalNative(name, args, result)
+ }
+
+ // When someone tries to call a block directly, we must evaluate it;
+ // e.g. something like `{ choose [@f1 @f2 @f3] } arg1 arg2 arg3`.
+ var evaluated *V
+ if !ell.evalStatement(body, &evaluated) {
+ return false
+ }
+
+ // It might a bit confusing that this doesn't evaluate arguments
+ // but neither does "block" and there's nothing to do here.
+ if evaluated == nil {
+ return true
+ }
+ return ell.evalResolved(evaluated, args, result)
+}
+
+func (ell *Ell) evalStatement(statement *V, result **V) bool {
+ if statement.Type == VTypeString {
+ *result = statement.Clone()
+ return true
+ }
+
+ // Executing a nil value results in no value. It's not very different from
+ // calling a block that returns no value--it's for our callers to resolve.
+ if statement.Head == nil || ell.evalValue(statement.Head, result) {
+ return true
+ }
+
+ *result = nil
+
+ name := "(block)"
+ if statement.Head.Type == VTypeString {
+ name = string(statement.Head.String)
+ }
+
+ if ell.canModifyError() {
+ ell.Errorf("%s -> %s", name, ell.Error)
+ }
+ return false
+}
+
+func argsToScope(args *V, scope **V) {
+ args = NewList(args)
+ scopePrepend(scope, "args", args)
+
+ i := 0
+ for args = args.Head; args != nil; args = args.Next {
+ i++
+ scopePrepend(scope, fmt.Sprintf("%d", i), args.Clone())
+ }
+ *scope = NewList(*scope)
+}
+
+// EvalBlock executes a block and returns whatever the last statement returned,
+// eats args.
+func (ell *Ell) EvalBlock(body *V, args *V, result **V) bool {
+ var scope *V
+ argsToScope(args, &scope)
+
+ scope.Next = ell.scopes
+ ell.scopes = scope
+
+ ok := true
+ for ; body != nil; body = body.Next {
+ *result = nil
+ if ok = ell.evalStatement(body, result); !ok {
+ break
+ }
+ }
+ ell.scopes = scope.Next
+ return ok
+}
+
+// --- Standard library --------------------------------------------------------
+
+// EvalAny evaluates any value.
+func EvalAny(ell *Ell, body *V, arg *V, result **V) bool {
+ if body.Type == VTypeString {
+ *result = body.Clone()
+ return true
+ }
+ return ell.EvalBlock(body.Head, arg.Clone(), result)
+}
+
+// NewNumber creates a new string value containing a number.
+func NewNumber(n float64) *V {
+ s := fmt.Sprintf("%f", n)
+ i := len(s)
+ for i > 0 && s[i-1] == '0' {
+ i--
+ }
+ if s[i-1] == '.' {
+ i--
+ }
+ return NewString([]byte(s[:i]))
+}
+
+// Truthy decides whether any value is logically true.
+func Truthy(v *V) bool {
+ return v != nil && (v.Head != nil || len(v.String) > 0)
+}
+
+// NewBoolean creates a new string value copying the boolean's truthiness.
+func NewBoolean(b bool) *V {
+ if b {
+ return NewString([]byte("1"))
+ }
+ return NewString(nil)
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func fnLocal(ell *Ell, args *V, result **V) bool {
+ names := args
+ if names == nil || names.Type != VTypeList {
+ return ell.Errorf("first argument must be a list")
+ }
+
+ // Duplicates or non-strings don't really matter to us, user's problem.
+ scope := &ell.scopes.Head
+
+ values := names.Next
+ for names = names.Head; names != nil; names = names.Next {
+ scopePrepend(scope, string(names.String), values.Clone())
+ if values != nil {
+ values = values.Next
+ }
+ }
+ return true
+}
+
+func fnSet(ell *Ell, args *V, result **V) bool {
+ name := args
+ if name == nil || name.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+
+ v := name.Next
+ if v != nil {
+ *result = v.Clone()
+ ell.Set(string(name.String), v)
+ return true
+ }
+
+ // We return an empty list for a nil value.
+ if v = ell.Get(string(name.String)); v != nil {
+ *result = v.Clone()
+ } else {
+ *result = NewList(nil)
+ }
+ return true
+}
+
+func fnList(ell *Ell, args *V, result **V) bool {
+ *result = NewList(args.CloneSeq())
+ return true
+}
+
+func fnValues(ell *Ell, args *V, result **V) bool {
+ *result = args.CloneSeq()
+ return true
+}
+
+func fnIf(ell *Ell, args *V, result **V) bool {
+ var cond, body, keyword *V
+ for cond = args; ; cond = keyword.Next {
+ if cond == nil {
+ return ell.Errorf("missing condition")
+ }
+ if body = cond.Next; body == nil {
+ return ell.Errorf("missing body")
+ }
+
+ var res *V
+ if !EvalAny(ell, cond, nil, &res) {
+ return false
+ }
+ if Truthy(res) {
+ return EvalAny(ell, body, nil, result)
+ }
+
+ if keyword = body.Next; keyword == nil {
+ break
+ }
+ if keyword.Type != VTypeString {
+ return ell.Errorf("expected keyword, got list")
+ }
+
+ switch kw := string(keyword.String); kw {
+ case "else":
+ if body = keyword.Next; body == nil {
+ return ell.Errorf("missing body")
+ }
+ return EvalAny(ell, body, nil, result)
+ case "elif":
+ default:
+ return ell.Errorf("invalid keyword: %s", kw)
+ }
+ }
+ return true
+}
+
+func fnMap(ell *Ell, args *V, result **V) bool {
+ var body, values *V
+ if body = args; body == nil {
+ return ell.Errorf("first argument must be a function")
+ }
+ if values = body.Next; values == nil || values.Type != VTypeList {
+ return ell.Errorf("second argument must be a list")
+ }
+
+ var res *V
+ out := &res
+
+ for v := values.Head; v != nil; v = v.Next {
+ if !EvalAny(ell, body, v, out) {
+ return false
+ }
+ for *out != nil {
+ out = &(*out).Next
+ }
+ }
+ *result = NewList(res)
+ return true
+}
+
+func fnPrint(ell *Ell, args *V, result **V) bool {
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ PrintV(os.Stdout, args)
+ } else if _, err := os.Stdout.Write(args.String); err != nil {
+ return ell.Errorf("write failed: %s", err)
+ }
+ }
+ return true
+}
+
+func fnCat(ell *Ell, args *V, result **V) bool {
+ buf := bytes.NewBuffer(nil)
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ PrintV(buf, args)
+ } else {
+ buf.Write(args.String)
+ }
+ }
+ *result = NewString(buf.Bytes())
+ return true
+}
+
+func fnSystem(ell *Ell, args *V, result **V) bool {
+ var argv []string
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ argv = append(argv, string(args.String))
+ }
+ if len(argv) == 0 {
+ return ell.Errorf("command name required")
+ }
+
+ cmd := exec.Command(argv[0], argv[1:]...)
+
+ // Approximation of system(3) return value to match C ell at least a bit.
+ if err := cmd.Run(); err == nil {
+ *result = NewNumber(0)
+ } else if _, ok := err.(*exec.Error); ok {
+ return ell.Errorf("%s", err)
+ } else {
+ *result = NewNumber(1)
+ }
+ return true
+}
+
+func fnParse(ell *Ell, args *V, result **V) bool {
+ body := args
+ if body == nil || body.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+
+ res, err := NewParser(body.String).Run()
+ if err != nil {
+ return ell.Errorf("%s", err)
+ }
+ *result = NewList(res)
+ return true
+}
+
+func fnTry(ell *Ell, args *V, result **V) bool {
+ var body, handler *V
+ if body = args; body == nil {
+ return ell.Errorf("first argument must be a function")
+ }
+ if handler = body.Next; handler == nil {
+ return ell.Errorf("second argument must be a function")
+ }
+ if EvalAny(ell, body, nil, result) {
+ return true
+ }
+
+ msg := NewString([]byte(ell.Error))
+ ell.Error = ""
+ *result = nil
+
+ return EvalAny(ell, handler, msg, result)
+}
+
+func fnThrow(ell *Ell, args *V, result **V) bool {
+ message := args
+ if message == nil || message.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+ return ell.Errorf("%s", message.String)
+}
+
+func fnPlus(ell *Ell, args *V, result **V) bool {
+ res := 0.
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ var arg float64
+ if n, _ := fmt.Sscan(string(args.String), &arg); n < 1 {
+ return ell.Errorf("invalid number: %s", args.String)
+ }
+ res += arg
+ }
+ *result = NewNumber(res)
+ return true
+}
+
+func fnMinus(ell *Ell, args *V, result **V) bool {
+ if args == nil || args.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+
+ var res float64
+ if n, _ := fmt.Sscan(string(args.String), &res); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ if args = args.Next; args == nil {
+ res = -res
+ }
+
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ var arg float64
+ if n, _ := fmt.Sscan(string(args.String), &arg); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ res -= arg
+ }
+ *result = NewNumber(res)
+ return true
+}
+
+func fnMultiply(ell *Ell, args *V, result **V) bool {
+ res := 1.
+ for ; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ var arg float64
+ if n, _ := fmt.Sscan(string(args.String), &arg); n < 1 {
+ return ell.Errorf("invalid number: %s", args.String)
+ }
+ res *= arg
+ }
+ *result = NewNumber(res)
+ return true
+}
+
+func fnDivide(ell *Ell, args *V, result **V) bool {
+ if args == nil || args.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+
+ var res float64
+ if n, _ := fmt.Sscan(string(args.String), &res); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ for args = args.Next; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ var arg float64
+ if n, _ := fmt.Sscan(string(args.String), &arg); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ res /= arg
+ }
+ *result = NewNumber(res)
+ return true
+}
+
+func fnNot(ell *Ell, args *V, result **V) bool {
+ if args == nil {
+ return ell.Errorf("missing argument")
+ }
+ *result = NewBoolean(!Truthy(args))
+ return true
+}
+
+func fnAnd(ell *Ell, args *V, result **V) bool {
+ if args == nil {
+ *result = NewBoolean(true)
+ return true
+ }
+ for ; args != nil; args = args.Next {
+ *result = nil
+ if !EvalAny(ell, args, nil, result) {
+ return false
+ }
+ if !Truthy(*result) {
+ *result = NewBoolean(false)
+ return true
+ }
+ }
+ return true
+}
+
+func fnOr(ell *Ell, args *V, result **V) bool {
+ for ; args != nil; args = args.Next {
+ if !EvalAny(ell, args, nil, result) {
+ return false
+ }
+ if Truthy(*result) {
+ return true
+ }
+ *result = nil
+ }
+ *result = NewBoolean(false)
+ return true
+}
+
+func fnEq(ell *Ell, args *V, result **V) bool {
+ etalon := args
+ if etalon == nil || etalon.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+ res := true
+ for args = etalon.Next; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ if res = string(etalon.String) == string(args.String); !res {
+ break
+ }
+ }
+ *result = NewBoolean(res)
+ return true
+}
+
+func fnLt(ell *Ell, args *V, result **V) bool {
+ etalon := args
+ if etalon == nil || etalon.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+ res := true
+ for args = etalon.Next; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ if res = string(etalon.String) < string(args.String); !res {
+ break
+ }
+ etalon = args
+ }
+ *result = NewBoolean(res)
+ return true
+}
+
+func fnEquals(ell *Ell, args *V, result **V) bool {
+ etalon := args
+ if etalon == nil || etalon.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+ var first, second float64
+ if n, _ := fmt.Sscan(string(etalon.String), &first); n < 1 {
+ return ell.Errorf("invalid number: %f", etalon.String)
+ }
+ res := true
+ for args = etalon.Next; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ if n, _ := fmt.Sscan(string(args.String), &second); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ if res = first == second; !res {
+ break
+ }
+ first = second
+ }
+ *result = NewBoolean(res)
+ return true
+}
+
+func fnLess(ell *Ell, args *V, result **V) bool {
+ etalon := args
+ if etalon == nil || etalon.Type != VTypeString {
+ return ell.Errorf("first argument must be string")
+ }
+ var first, second float64
+ if n, _ := fmt.Sscan(string(etalon.String), &first); n < 1 {
+ return ell.Errorf("invalid number: %f", etalon.String)
+ }
+ res := true
+ for args = etalon.Next; args != nil; args = args.Next {
+ if args.Type != VTypeString {
+ return ell.Errorf("arguments must be strings")
+ }
+ if n, _ := fmt.Sscan(string(args.String), &second); n < 1 {
+ return ell.Errorf("invalid number: %f", args.String)
+ }
+ if res = first < second; !res {
+ break
+ }
+ first = second
+ }
+ *result = NewBoolean(res)
+ return true
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+var stdNative = map[string]Handler{
+ "local": fnLocal,
+ "set": fnSet,
+ "list": fnList,
+ "values": fnValues,
+ "if": fnIf,
+ "map": fnMap,
+ "print": fnPrint,
+ "..": fnCat,
+ "system": fnSystem,
+ "parse": fnParse,
+ "try": fnTry,
+ "throw": fnThrow,
+ "+": fnPlus,
+ "-": fnMinus,
+ "*": fnMultiply,
+ "/": fnDivide,
+ "not": fnNot,
+ "and": fnAnd,
+ "or": fnOr,
+ "eq?": fnEq,
+ "lt?": fnLt,
+ "=": fnEquals,
+ "<": fnLess,
+}
+
+var stdComposed = `
+set unless { if (not (@1)) @2 }
+set filter { local [_body _list] @1 @2;
+ map { if (@_body @1) { @1 } } @_list }
+set for { local [_list _body] @1 @2;
+ try { map { @_body @1 } @_list } { if (ne? @1 _break) { throw @1 } } }
+
+set break { throw _break }
+
+# TODO: we should be able to apply them to all arguments
+set ne? { not (eq? @1 @2) }; set le? { ge? @2 @1 }
+set ge? { not (lt? @1 @2) }; set gt? { lt? @2 @1 }
+set <> { not (= @1 @2) }; set <= { >= @2 @1 }
+set >= { not (< @1 @2) }; set > { < @2 @1 }`
+
+// StdInitialize initializes the ell standard library.
+func StdInitialize(ell *Ell) bool {
+ for name, handler := range stdNative {
+ ell.NativeRegister(name, handler)
+ }
+
+ p := NewParser([]byte(stdComposed))
+ program, err := p.Run()
+ if err != nil {
+ return false
+ }
+
+ var result *V
+ return ell.EvalBlock(program, nil, &result)
+}