Browse Source

Add most ATTL files.

Beoran 5 months ago
parent
commit
e702d5e29e
21 changed files with 3958 additions and 2 deletions
  1. 1 1
      LICENSE
  2. 50 1
      README.md
  3. BIN
      attl
  4. 5 0
      attl.go
  5. 869 0
      builtin.go
  6. BIN
      cmd/attl/attl
  7. 143 0
      cmd/attl/main.go
  8. 148 0
      cmd/attl/testdata/basic.attl
  9. 260 0
      cmd/attl/testdata/builtin.attl
  10. 27 0
      cmd/attl/testdata/overload.attl
  11. 247 0
      convert.go
  12. 31 0
      effect.go
  13. 338 0
      environment.go
  14. 5 0
      go.mod
  15. 6 0
      go.sum
  16. 459 0
      parse.go
  17. 385 0
      parse_test.go
  18. 140 0
      support.go
  19. 25 0
      support_test.go
  20. 285 0
      tutorial.attl
  21. 534 0
      value.go

+ 1 - 1
LICENSE

@@ -1,5 +1,5 @@
 MIT License
-Copyright (c) <year> <copyright holders>
+Copyright (c) 2020-2022 Beoran
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
 

+ 50 - 1
README.md

@@ -2,4 +2,53 @@
 
 A Tiny Tool Language. 
 
-Attl is a TCL-like embedded scripting language implemented in Go.
+Attl is a TCL-like embedded scripting language implemented in Go.# ATTL
+
+## Introduction
+
+Attl is A Tiny Tool Language, an embeddable, interpreted scripting language,
+implemented in Go, that somewhat resembles TCL and shell scripts, but with
+typed values, and LISP like semantics.
+
+One notable feature is that the language itself has no key words, nor
+flow control by itself, but that this is provided by the commands that
+ATTL executes. This makes it possible to disable certain commands,
+or example, in case where it is desirable for the script to be not Turing
+complete.
+
+The syntax is extremelty simple, and based on a rescursive descent LL1 parser,
+where the parser only considers the next character and the current state
+of parsing to deterine the meaning of the code. Code that is easy to parse
+by the computer is easy to understand by humans also, which is why the
+limitations of LL1 parsing are acceptable.
+
+## Grammar
+The formal grammar of ATTL is as follows:
+
+SCRIPT        -> STATEMENTS .
+STATEMENTS    -> STATEMENT OPTSTATEMENTS .
+OPTSTATEMENTS -> rs STATEMENT OPTSTATEMENTS | .
+STATEMENT     -> OPTWS EXPRESSION .
+EXPRESSION    -> COMMAND | BLOCK | comment | .
+OPTWS         -> ws | .
+COMMAND       -> ORDER PARAMETERS .
+ORDER         -> LITERAL | EVALUATION .
+BLOCK         -> ob STATEMENTS cb .
+PARAMETERS    -> ws PARAMETER OPTPARAMETERS | .
+PARAMETER     -> LITERAL | BLOCK | GETTER | EVALUATION | .
+EVALUATION    -> oe COMMAND ce .
+GETTER        -> get TARGET .
+TARGET 		  -> GETTER | LITERAL .
+LITERAL       -> word | string | integer .
+rs			-> /[\n\r]+/ .
+ws			-> /[\t ]+/  .
+word 		-> /[^ \t\n\$\(\)\{\}\]\[]+/
+string 		-> /"[^"]+"/ | /`[^`]+`/
+integer     -> [+-]?[0-9]+
+comment 	-> /#[^\n]+\n/ .
+get			-> '$' .
+oe 			-> '[' .
+ce          -> ']' .
+ob 			-> '{' .
+cb 			-> '}' .
+

BIN
attl


+ 5 - 0
attl.go

@@ -0,0 +1,5 @@
+// A Tiny Tool Language is a tiny TCL like language interpreter
+// but with real values, not with everything as a string.
+// A secondary requirement is for ATTL not to have any dependencies at all,
+// apart from the go language built-in functionality.
+package attl

+ 869 - 0
builtin.go

@@ -0,0 +1,869 @@
+package attl
+
+func p(env *Environment, args ...Value) (Value, Effect) {
+	for _, arg := range args {
+		print(arg, " ")
+	}
+	print("\n")
+	return nil, nil
+}
+
+func print_(env *Environment, args ...Value) (Value, Effect) {
+	var msg string
+	erra := Args(args, &msg)
+	if erra != nil {
+		return env.FailString("printf: ${1}", erra)
+	}
+	extra := []Value{}
+	if len(args) > 1 {
+		extra = args[1:len(args)]
+	}
+	n, err := env.Printi(msg, extra...)
+	if err == nil {
+		return Int(n), nil
+	}
+	return Int(n), ErrorFromError(err)
+}
+
+func write(env *Environment, args ...Value) (Value, Effect) {
+	var msg string
+	erra := Args(args, &msg)
+	if erra != nil {
+		return env.FailString("write: ${1}", erra)
+	}
+	n, err := env.Write(msg)
+	if err == nil {
+		return Int(n), nil
+	}
+	return Int(n), ErrorFromError(err)
+}
+
+func iadd(env *Environment, args ...Value) (Value, Effect) {
+	var i, j int
+	err := Args(args, &i, &j)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Int(i + j), nil
+}
+
+func isub(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Int(v1 - v2), nil
+}
+
+func imul(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Int(v1 * v2), nil
+}
+
+func idiv(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	if v2 == 0 {
+		return nil, ErrorFromString("division by 0")
+	}
+	return Int(v1 / v2), nil
+}
+
+func igt(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 > v2), nil
+}
+
+func ilt(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 < v2), nil
+}
+
+func ige(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 >= v2), nil
+}
+
+func ile(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 <= v2), nil
+}
+
+func ieq(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 int
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 == v2), nil
+}
+
+func seq(env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 string
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(v1 == v2), nil
+}
+
+func teq(env *Environment, args ...Value) (Value, Effect) {
+	var t1, t2 Type
+	err := Args(args, &t1, &t2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(t1 == t2), nil
+}
+
+func updateIntByName(update func(in Int) Int, env *Environment, args ...Value) (Int, Effect) {
+	var name Word
+	err := Args(args, &name)
+	if err != nil {
+		return Int(0), err
+	}
+	val := env.Lookup(name.String())
+	vi, ok := val.(Int)
+	if !ok {
+		return Int(0), ErrorFromString("Not an integer.")
+	}
+	newi := update(vi)
+	env.Set(name.String(), newi)
+	return newi, nil
+}
+
+func inc(env *Environment, args ...Value) (Value, Effect) {
+	return updateIntByName(func(in Int) Int {
+		return in + 1
+	}, env, args...)
+}
+
+func dec(env *Environment, args ...Value) (Value, Effect) {
+	return updateIntByName(func(in Int) Int {
+		return in - 1
+	}, env, args...)
+}
+
+func str(env *Environment, args ...Value) (Value, Effect) {
+	var v1 Value
+	err := Args(args, &v1)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return String(v1.String()), nil
+}
+
+func int_(env *Environment, args ...Value) (Value, Effect) {
+	var v1 Value
+	err := Args(args, &v1)
+	if err != nil {
+		return env.Fail(err)
+	}
+	rs := []rune(v1.String() + " ")
+	index := 0
+	return ParseInteger(rs, &index)
+}
+
+func boolBinop(op func(b1, b2 bool) bool, env *Environment, args ...Value) (Value, Effect) {
+	var v1, v2 bool
+	err := Args(args, &v1, &v2)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(op(v1, v2)), nil
+}
+
+func isnil(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 1 {
+		return env.FailString("isnil requires 1 argument")
+	}
+	return Bool(args[0] == nil), nil
+}
+
+func band(env *Environment, args ...Value) (Value, Effect) {
+	return boolBinop(func(b1, b2 bool) bool {
+		return b1 && b2
+	}, env, args...)
+}
+
+func bor(env *Environment, args ...Value) (Value, Effect) {
+	return boolBinop(func(b1, b2 bool) bool {
+		return b1 || b2
+	}, env, args...)
+}
+
+func bxor(env *Environment, args ...Value) (Value, Effect) {
+	return boolBinop(func(b1, b2 bool) bool {
+		return b1 != b2
+	}, env, args...)
+}
+
+func bnot(env *Environment, args ...Value) (Value, Effect) {
+	var v1 bool
+	err := Args(args, &v1)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Bool(!v1), nil
+}
+
+func val(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 1 {
+		return env.FailString("val requres at least one argument.")
+	}
+	return List(args), nil
+}
+
+func ret(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 1 {
+		return env.Return(nil)
+	} else if len(args) == 1 {
+		return env.Return(args[0])
+	} else {
+		return env.Return(List(args))
+	}
+}
+
+func fail(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 1 {
+		return env.Fail(ErrorFromString("fail"))
+	} else {
+		return env.FailString(args[0].String(), args[1:len(args)]...)
+	}
+}
+
+func break_(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 1 {
+		return env.Break(nil)
+	} else if len(args) == 1 {
+		return env.Break(args[0])
+	} else {
+		return env.Break(List(args))
+	}
+}
+
+func nop(env *Environment, args ...Value) (Value, Effect) {
+	return nil, nil
+}
+
+func typeof_(env *Environment, args ...Value) (Value, Effect) {
+	var val Value
+	err := Args(args, &val)
+	if err != nil {
+		return nil, err
+	}
+	return TypeOf(val), nil
+}
+
+func type_(env *Environment, args ...Value) (Value, Effect) {
+	var val Value
+	err := Args(args, &val)
+	if err != nil {
+		return nil, err
+	}
+	name := val.String()
+	return Type(name), nil
+}
+
+func to(env *Environment, args ...Value) (Value, Effect) {
+	var name string
+
+	if len(args) < 2 {
+		return env.FailString("to needs at least 2 arguments")
+	}
+	err := Convert(args[0], &name)
+	if err != nil {
+		return env.Fail(err)
+	}
+	block, ok := (args[len(args)-1]).(Block)
+	if !ok {
+		return env.FailString("to: last argument must be a block")
+	}
+
+	last := args[len(args)-1]
+	block, isBlock := last.(Block)
+	if !isBlock {
+		return env.FailString("Not a block")
+	}
+	params := args[1 : len(args)-1]
+	defined := Defined{name, params, block}
+	env.Define(name, defined, 1)
+	return defined, nil
+}
+
+func do(env *Environment, args ...Value) (Value, Effect) {
+	var name string
+	var doArgs List
+	err := Args(args, &name, &doArgs)
+	if err != nil {
+		return env.Fail(err)
+	}
+	fun := env.Lookup(name)
+	if fun == nil {
+		return env.FailString("Cannot evaluate unknown order: " + name)
+	}
+	eva, ok := fun.(Evaler)
+	if !ok {
+		return env.FailString("Cannot evaluate: " + name)
+	}
+	return eva.Eval(env, doArgs...)
+}
+
+func if_(env *Environment, args ...Value) (Value, Effect) {
+	var cond, ok, haveElse bool
+	var ifBlock, elseBlock Block
+
+	if len(args) < 2 {
+		return env.FailString("if needs at least 2 arguments")
+	}
+	if len(args) > 4 {
+		return env.FailString("if needs at most 4 arguments")
+	}
+	err := Convert(args[0], &cond)
+	if err != nil {
+		return env.Fail(err)
+	}
+	ifBlock, ok = (args[1]).(Block)
+	if !ok {
+		return env.FailString("if: second argument must be a block")
+	}
+	elseIndex := 2
+	if 2 < len(args) {
+		// look for an else keyword but don't mind if it really is else
+		_, ok = (args[2]).(Word)
+		if ok {
+			// block after else keyword
+			elseIndex = 3
+		}
+	}
+	if elseIndex < len(args) {
+		// There should be an else block...
+		elseBlock, ok = (args[elseIndex]).(Block)
+		if !ok {
+			return env.FailString("if: missing else block")
+		}
+		haveElse = true
+	}
+	if cond {
+		return ifBlock.Eval(env, args...)
+	} else {
+		if haveElse {
+			return elseBlock.Eval(env, args...)
+		} else {
+			return nil, nil
+		}
+	}
+}
+
+func switch_(env *Environment, args ...Value) (Value, Effect) {
+	var defaultBlock Block
+	var haveDefault bool = false
+	if len(args) < 3 {
+		return env.FailString("switch needs at least 3 arguments")
+	}
+	compareTo := args[0]
+	for i := 2; i < len(args); i += 2 {
+		case_ := args[i-1]
+		block, blockOk := args[i].(Block)
+		if !blockOk {
+			return env.FailString("switch: argument ${1} is not a block",
+				Int(i))
+		}
+		if kw, kwOk := case_.(Word); kwOk && kw.String() == "default" {
+			if haveDefault {
+				return env.FailString("switch: duplicate default block ${1}",
+					Int(i))
+			}
+			haveDefault = true
+			defaultBlock = block
+		} else {
+			if compareTo.String() == case_.String() {
+				return block.Eval(env, args...)
+			}
+		}
+	}
+	if haveDefault {
+		return defaultBlock.Eval(env, args...)
+	}
+	return nil, nil
+}
+
+func while(env *Environment, args ...Value) (Value, Effect) {
+	var blockRes Value
+	var blockEff Effect
+	if len(args) != 2 {
+		return env.FailString("while needs exactly 3 arguments")
+	}
+	cond, condOk := args[0].(Block)
+	block, blockOk := args[1].(Block)
+	if !condOk {
+		return env.FailString("while condition must be a block")
+	}
+	if !blockOk {
+		return env.FailString("while body must be a block")
+	}
+
+	for res, eff := cond.Eval(env, args...); ValToBool(res); res, eff = cond.Eval(env, args...) {
+		if eff != nil && eff.Flow() > NormalFlow {
+			return res, eff
+		}
+		blockRes, blockEff = block.Eval(env, args...)
+		if blockEff != nil && blockEff.Flow() > NormalFlow {
+			return blockRes, blockEff
+		}
+	}
+	return blockRes, blockEff
+}
+
+func rescue(env *Environment, args ...Value) (Value, Effect) {
+	var block Block
+	err := Args(args, &block)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return env.Prevent(block)
+}
+
+func set(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 2 {
+		return env.FailString("set needs at 2 arguments")
+	}
+	if args[0] == nil {
+		return env.FailString("set $1 is nil")
+	}
+	return env.Set(args[0].String(), args[1])
+}
+
+func let(env *Environment, args ...Value) (Value, Effect) {
+	if len(args) < 2 {
+		return env.FailString("def needs at 2 arguments")
+	}
+	if args[0] == nil {
+		return env.FailString("def $1 is nil")
+	}
+	return env.Define(args[0].String(), args[1], 1)
+}
+
+func get(env *Environment, val ...Value) (Value, Effect) {
+	if len(val) < 1 {
+		return env.FailString("get needs at least 1 argument")
+	}
+	target := val[0].String()
+	return env.Lookup(target), nil
+}
+
+func list(env *Environment, args ...Value) (Value, Effect) {
+	return List(args), nil
+}
+
+func sadd(env *Environment, args ...Value) (Value, Effect) {
+	var value Value
+	var str String
+	err := Args(args, &str, &value)
+	if err != nil {
+		return env.Fail(err)
+	}
+	str = str + String(value.String())
+	return str, nil
+}
+
+func sget(env *Environment, args ...Value) (Value, Effect) {
+	var index int
+	var str String
+	err := Args(args, &str, &index)
+	if err != nil {
+		return env.Fail(err)
+	}
+	runes := []rune(str)
+	if (index < 0) || (index >= len(runes)) {
+		return env.FailString("index out of range")
+	}
+	return Int(runes[index]), nil
+}
+
+func runes(env *Environment, args ...Value) (Value, Effect) {
+	var str String
+	err := Args(args, &str)
+	if err != nil {
+		return env.Fail(err)
+	}
+	res := List{}
+	runes := []rune(str)
+	for i := 0; i < len(runes); i++ {
+		res = append(res, Int(runes[i]))
+	}
+	return res, nil
+}
+
+func wire(env *Environment, args ...Value) (Value, Effect) {
+	var str String
+	for i := 0; i < len(args); i++ {
+		var ch Int
+		err := Convert(args[i], &ch)
+		if err != nil {
+			return str, err
+		}
+		str = str + String([]rune{rune(ch)})
+	}
+	return str, nil
+}
+
+func slen(env *Environment, args ...Value) (Value, Effect) {
+	var str String
+	err := Args(args, &str)
+	if err != nil {
+		return env.Fail(err)
+	}
+	runes := []rune(str)
+	return Int(len(runes)), nil
+}
+
+func ladd(env *Environment, args ...Value) (Value, Effect) {
+	var value Value
+	var list List
+	err := Args(args, &list, &value)
+	if err != nil {
+		return env.Fail(err)
+	}
+	list = append(list, value)
+	return list, nil
+}
+
+func lget(env *Environment, args ...Value) (Value, Effect) {
+	var index int
+	var list List
+	err := Args(args, &list, &index)
+	if err != nil {
+		return env.Fail(err)
+	}
+	if (index < 0) || (index >= len(list)) {
+		return env.FailString("index out of range")
+	}
+	return list[index], nil
+}
+
+func lset(env *Environment, args ...Value) (Value, Effect) {
+	var index int
+	var list List
+	var val Value
+	err := Args(args, &list, &index, &val)
+	if err != nil {
+		return env.Fail(err)
+	}
+	if (index < 0) || (index >= len(list)) {
+		return env.FailString("index out of range")
+	}
+	list[index] = val
+	return list[index], nil
+}
+
+func llen(env *Environment, args ...Value) (Value, Effect) {
+	var list List
+	err := Args(args, &list)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return Int(len(list)), nil
+}
+
+func lsort(env *Environment, args ...Value) (Value, Effect) {
+	var list List
+	err := Args(args, &list)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return list.SortStrings(), nil
+}
+
+func leach(env *Environment, args ...Value) (Value, Effect) {
+	var list List
+	var key Word
+	var name Word
+	var block Block
+	err := Args(args, &list, &key, &name, &block)
+	if err != nil {
+		return env.Fail(err)
+	}
+	for i, v := range list {
+		env.Define(key.String(), Int(i), 0)
+		env.Define(name.String(), v, 0)
+		bval, berr := block.Eval(env, args...)
+		if berr != nil {
+			return bval, berr
+		}
+	}
+	return list, nil
+}
+
+func lslice(env *Environment, args ...Value) (Value, Effect) {
+	var list List
+	var from Int
+	var to Int
+	err := Args(args, &list, &from, &to)
+	if err != nil {
+		return env.Fail(err)
+	}
+	length := Int(len(list))
+	if length == 0 {
+		return list, nil
+	}
+	if from < 0 {
+		from = length - from
+	}
+	if to < 0 {
+		from = length - from
+	}
+	if from >= length {
+		from = length - 1
+	}
+	if to >= length {
+		to = length - 1
+	}
+	if from > to {
+		from, to = to, from
+	}
+	return list[from:to], nil
+}
+
+func map_(env *Environment, args ...Value) (Value, Effect) {
+	res := make(Map)
+	for i := 1; i < len(args); i += 2 {
+		key := args[i-1]
+		val := args[i]
+		res[key.String()] = val
+	}
+	return res, nil
+}
+
+func mget(env *Environment, args ...Value) (Value, Effect) {
+	var index string
+	var hmap Map
+	err := Args(args, &hmap, &index)
+	if err != nil {
+		return env.Fail(err)
+	}
+	return hmap[index], nil
+}
+
+func mset(env *Environment, args ...Value) (Value, Effect) {
+	var index string
+	var hmap Map
+	var val Value
+	err := Args(args, &hmap, &index, &val)
+	if err != nil {
+		return env.Fail(err)
+	}
+	hmap[index] = val
+	return hmap[index], nil
+}
+
+func mkeys(env *Environment, args ...Value) (Value, Effect) {
+	var hmap Map
+	err := Args(args, &hmap)
+	if err != nil {
+		return env.Fail(err)
+	}
+	res := List{}
+	for k, _ := range hmap {
+		res = append(res, String(k))
+	}
+	return res, nil
+}
+
+func meach(env *Environment, args ...Value) (Value, Effect) {
+	var map_ Map
+	var key Word
+	var name Word
+	var block Block
+	err := Args(args, &map_, &key, &name, &block)
+	if err != nil {
+		return env.Fail(err)
+	}
+	miter := Map{}
+
+	for k, v := range map_ {
+		miter[k] = v
+	}
+	for k, v := range miter {
+		env.Define(key.String(), String(k), 0)
+		env.Define(name.String(), v, 0)
+		bval, beff := block.Eval(env, args...)
+		if beff != nil {
+			return bval, beff
+		}
+	}
+	return map_, nil
+}
+
+func expand(env *Environment, args ...Value) (Value, Effect) {
+	var msg string
+	err := Args(args, &msg)
+	if err != nil {
+		return env.Fail(err)
+	}
+	res := env.Interpolate(msg)
+	return String(res), nil
+}
+
+func help(env *Environment, args ...Value) (Value, Effect) {
+	var name string
+	err := Args(args, &name)
+	if err != nil {
+		return env.Fail(err)
+	}
+	helpMap := env.Lookup("HELP")
+	if helpMap == nil {
+		env.Printi("help: $1:No help available 1.\n", String(name))
+		return nil, err
+	}
+	if name == "all" {
+		keys := helpMap.(Map).SortedKeys()
+		for _, k := range keys {
+			v := helpMap.(Map)[k.String()]
+			env.Printi("$1: $2\n", k, v)
+		}
+		return nil, nil
+	}
+
+	msg, ok := helpMap.(Map)[name]
+	if ok {
+		env.Printi("help: $1:\n$2\n", String(name), msg)
+
+	} else {
+		env.Printi("help: $1:No help available 2.\n", String(name))
+	}
+	return msg, nil
+}
+
+func explain(env *Environment, args ...Value) (Value, Effect) {
+	var name string
+	var help String
+	err := Args(args, &name, &help)
+	if err != nil {
+		return env.Fail(err)
+	}
+	helpMap := env.Lookup("HELP")
+	if helpMap == nil {
+		helpMap = make(Map)
+	}
+	helpMap.(Map)[name] = help
+	env.Define("HELP", helpMap, -1)
+	return help, nil
+}
+
+func overload(env *Environment, args ...Value) (Value, Effect) {
+	var name string
+	var target Value
+	err := Args(args, &name, &target)
+	if err != nil {
+		return env.Fail(err)
+	}
+	if len(args) < 3 {
+		return env.FailString("overload needs at least 3 arguments")
+	}
+	return env.Overload(name, target, args[2:len(args)])
+}
+
+func (env *Environment) Register(name string,
+	f func(e *Environment, args ...Value) (Value, Effect), help string) {
+	env.Define(name, Proc(f), -1)
+	explain(env, String(name), String(help))
+}
+
+func (env *Environment) RegisterBuiltins() {
+	env.Define("true", Bool(true), -1)
+	env.Define("false", Bool(false), -1)
+	env.Register("sadd", sadd, "returns a string  with $2 appended to string $1")
+	env.Register("sget", sget, "gets a rune from a string by index")
+	env.Register("slen", slen, "returns the length of a string")
+	env.Register("iadd", iadd, "adds two integers together")
+	env.Register("band", band, `returns true if $1 and $2 arguments are true`)
+	env.Register("bor", bor, `returns true if $1 or $2 arguments are true`)
+	env.Register("bxor", bxor, `returns true if $1 and $2 are different booleans`)
+	env.Register("bnot", bnot, `returns true if $1 is false and false otherwise`)
+	env.Register("ladd", ladd, "returns a list with $2 appended to List $1")
+	env.Register("list", list, "creates a new array list")
+	env.Register("lget", lget, "gets a value from a list by index")
+	env.Register("lset", lset, "sets a value to a list by index and value")
+	env.Register("llen", llen, "returns the length of a list")
+	env.Register("lsort", lsort, "returns the List $1 sorted by string value")
+	env.Register("leach", leach, "calls the block $4 for each entry in the list")
+	env.Register("lslice", lslice, "slices the list $1 from $2 to $3")
+	env.Register("iadd", iadd, "adds and Ints to and Int")
+	env.Register("isub", isub, "subtracts an Int from an Int")
+	env.Register("imul", imul, "multiplies an Ints by an Int")
+	env.Register("idiv", idiv, "divides an Int by an Int")
+	env.Register("ilt", ilt, "checks if $1 < $2, where $1 and $2 must be Int")
+	env.Register("ile", ile, "checks if $1 <= $2, where $1 and $2 must be Int")
+	env.Register("igt", igt, "checks if $1 > $2, where $1 and $2 must be Int")
+	env.Register("ige", ige, "checks if $1 >= $2, where $1 and $2 must be Int")
+	env.Register("ieq", ieq, "checks if $1 == $2, where $1 and $2 must be Int")
+	env.Register("seq", seq, "checks if [str $1] == [str $2]")
+	env.Register("str", str, "converts $1 to String")
+	env.Register("wire", wire, "converts unicode character indexes or runes to String")
+	env.Register("runes", runes, "converts String to alist of character indexes or runes")
+	env.Register("int", int_, "converts $1 to Int")
+	env.Register("inc", inc, "increments the named integer $1")
+	env.Register("dec", dec, "decrements the named integer $1")
+	env.Register("map", map_, "creates a new hash map")
+	env.Register("mget", mget, "gets a value from a map by key")
+	env.Register("mset", mset, "sets a value to a map by key and value")
+	env.Register("mkeys", mkeys, "returns all keys of a map as an unsorted list")
+	env.Register("meach", meach, "calls the block $4 for each entry in the map")
+
+	env.Register("p", p, "print debug output")
+	env.Register("print", print_, "print to the environnment's current writer with interpolation")
+	env.Register("write", write, "write to the environnment's current writer")
+	env.Register("to", to, "define a procedure")
+	env.Register("do", do, "execute a command $1 with arguments in $2 as array")
+	env.Register("ret", ret, "return from a procedure")
+	env.Register("return", ret, "return from a procedure")
+	env.Register("break", break_, "return from a block")
+	env.Register("val", val, "gets the value of a value")
+	env.Register("let", let, "creates a new vavariable with given value")
+	env.Register("set", set, "sets an existing variable")
+	env.Register("get", get, "get the contents of a variable")
+	env.Register("help", help, "get help for a procedure")
+	env.Register("explain", explain, "set the help for a procedure")
+	env.Register("expand", expand, "interpolate strings from environment")
+	env.Register("fail", fail, "fail execution of a procedure")
+	env.Register("rescue", rescue, "call $1 as the error handler on failure")
+	env.Register("if", if_, "if runs $1 if $0 is true, otherwise runs $2")
+	env.Register("isnil", isnil, "returns true if $1 is nil, false if not")
+	env.Register("switch", switch_, "selects one of many cases")
+	env.Register("type", type_, "returns $1 converted to a type")
+	env.Register("teq", teq, "checks if $1 and $2 are exactly the same type")
+	env.Register("typeof", typeof_, "returns the type of $1 or Unknown if not known")
+	env.Register("nop", nop, "does nothing and returns nil")
+	env.Register("overload", overload, "creates a command overload named $1 targeting $2 for the types following $2")
+}
+
+// This function registers builtins that make Attl turing complete
+// Not to be used in situations where this is undesirable.
+func (env *Environment) RegisterTuringCompleteBuiltins() {
+	env.Register("while", while, "executes $2 while $1 returns true")
+}

BIN
cmd/attl/attl


+ 143 - 0
cmd/attl/main.go

@@ -0,0 +1,143 @@
+package main
+
+import (
+	"io"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	//	"sort"
+)
+
+import "src.eruta.nl/beoran/attl"
+import "github.com/peterh/liner"
+
+func runLine(env *attl.Environment, in string) *attl.Error {
+	parsed, err := attl.Parse(in)
+	if err != nil {
+		return err
+	}
+	if parsed == nil {
+		return attl.ErrorFromString("No parse results")
+	}
+	val, eff := parsed.Eval(env)
+	if val != nil {
+		env.Printi(">>${1}\n", val)
+	} else {
+		env.Printi(">>nil\n")
+	}
+	err, ok := eff.(*attl.Error)
+	if ok {
+		return err
+	}
+	return nil
+}
+
+func runLines(env *attl.Environment, line *liner.State) error {
+	buf := ""
+	for {
+		if in, err := line.Prompt("> "); err == nil {
+			first := ';'
+			if len(in) > 0 {
+				first = rune(in[0])
+			}
+			if first == '\\' {
+				buf = buf + "\n" + in[1:len(in)]
+			} else {
+				if len(buf) > 0 {
+					buf = buf + "\n" + in
+				} else {
+					buf = in + "\n"
+				}
+				rerr := runLine(env, buf)
+				buf = ""
+				if rerr != nil {
+					env.Printi("Error ${1}: \n", attl.String(rerr.Message))
+				}
+			}
+			line.AppendHistory(in)
+		} else if err == liner.ErrPromptAborted {
+			env.Printi("Aborted\n")
+			return nil
+		} else if err == io.EOF {
+			return nil
+		} else {
+			env.Printi("Error reading line: ${1}\n", attl.ErrorFromError(err))
+		}
+	}
+	return nil
+}
+
+func runFile(env *attl.Environment, name string) *attl.Error {
+	fin, err := os.Open(name)
+	if err != nil {
+		return attl.ErrorFromError(err)
+	}
+	defer fin.Close()
+	buf, err := ioutil.ReadAll(fin)
+	if err != nil {
+		return attl.ErrorFromError(err)
+	}
+	in := string(buf)
+
+	parsed, rerr := attl.Parse(in)
+	if rerr != nil {
+		return rerr
+	}
+	if parsed == nil {
+		return attl.ErrorFromString("Parse result is empty.")
+	}
+	args := attl.List{}
+	for _, a := range os.Args {
+		args = append(args, attl.String(a))
+	}
+	_, reff := parsed.Eval(env, args...)
+	rerr, ok := reff.(*attl.Error)
+	if ok {
+		return rerr
+	}
+	return nil
+}
+
+func main() {
+	// console := muesli.NewStdConsole()
+	env := &attl.Environment{}
+	env.Out = os.Stdout
+	env.Push()
+
+	env.RegisterBuiltins()
+	env.RegisterTuringCompleteBuiltins()
+	line := liner.NewLiner()
+	defer line.Close()
+
+	line.SetCtrlCAborts(true)
+	home, _ := os.UserHomeDir()
+	historyName := filepath.Join(home, ".attl_history")
+
+	if f, err := os.Open(historyName); err == nil {
+		line.ReadHistory(f)
+		f.Close()
+	}
+
+	if len(os.Args) > 1 {
+		for _, name := range os.Args {
+			rerr := runFile(env, name)
+			if rerr != nil {
+				sname := attl.String(name)
+				env.Printi("error in ${1}: ${2}\n", sname,
+					rerr)
+			}
+		}
+		return
+	}
+	line.SetWordCompleter(func(line string, pos int) (head string, c []string, tail string) {
+		return attl.WordCompleter(*env, line, pos)
+	})
+	runLines(env, line)
+
+	if f, err := os.Create(historyName); err != nil {
+		env.Printi("Error writing history file: ${1}\n", attl.ErrorFromError(err))
+	} else {
+		line.WriteHistory(f)
+		f.Close()
+	}
+}

+ 148 - 0
cmd/attl/testdata/basic.attl

@@ -0,0 +1,148 @@
+#!/usr/bin/env attl
+#
+# Basic test of attl. All prints should produce PASS. Comments should be ignored.
+let tc 1
+let tn "Test: basic printing and command calling"
+print "PASS $tc $tn $tn\n"
+inc tc
+set tn "Test: string interpolation"
+print "${1}${2} $tc $tn\n" "PA" "SS"
+inc tc
+set tn "Test: variable setting, evaluations, and if"
+let a 10
+if [ilt $a 20] {
+	print "PASS $tc $tn\n"
+} else {
+	print "FAIL $tc $tn\n"
+}
+inc tc
+set tn "Test: if with block condition"
+let b 12
+if {ilt $b 20} {
+	print "PASS $tc $tn\n"
+} else {
+	print "FAIL $tc $tn\n"
+}
+inc tc
+set tn "Test: else block"
+if [ige $a 20] {
+	print "FAIL  $tc $tn\n"
+} else {
+	print "PASS $tc $tn\n"
+}
+inc tc
+set tn "Test: command definition"
+to test_ok {
+	print "PASS $tc $tn\n"
+	return 0
+}
+test_ok
+
+inc tc
+set tn "Test: break in block"
+{
+	# break should only break the block
+	break
+}
+print "PASS $tc $tn\n"
+
+inc tc
+set tn "Test: break in block function"
+to break_block {
+	break [expand "PASS $tc $tn"]
+	print "FAIL $tc $tn went too far \n"
+	expand "FAIL $tc $tn"
+}
+print "$1\n" [break_block]
+
+inc tc
+set tn "Test: return values"
+to return_ok {
+	return [expand "PASS $tc $tn"]
+	print "FAIL $tc $tn went too far \n"
+}
+print "$1\n" [return_ok]
+
+inc tc
+set tn "Test: multiple return values"
+to return_multi_ok {
+	return "PA" "AS" [expand "$tc $tn"]
+	print "FAIL $tc $tn went too far \n"
+}
+print "$1\n" [return_multi_ok]
+
+set tn "Test: fail/rescue catching"
+to fail_ok {
+	fail "FAIL $tc $tn"
+}
+inc tc
+to test_rescue {
+	rescue {
+		let var [expand "PASS $tc $tn\n"]
+		return "PASS $tc $tn\n"
+	}
+	fail_ok
+	print  "FAIL $tc $tn went too far\n"
+	return 0
+}
+print [test_rescue]
+
+set tn "Test: top level block evaluation and stack"
+inc tc
+let var "FAIL $tc $tn"
+{
+	let var "FAIL $tc $tn 2"
+	{
+		let var [expand "PASS $tc $tn\n"]
+		print $var
+	}
+	print $var
+}
+print $var
+
+let rec 0
+set tn "Test: no recursion from fail in rescue blocks"
+inc tc
+to test_fail_in_rescue {
+	rescue {
+		inc rec
+		if [igt $rec 1] {
+			return "FAIL $tc $tn: recursion detected: $rec\n"
+		} else {
+			fail "PASS $tc $tn: $rec\n"
+		}
+	}
+	fail_ok
+	print  "FAIL $tc $tn went too far\n"
+	return 0
+}
+print [test_fail_in_rescue]
+
+
+# This test does stop the recursion but it fails
+# in the sense thet the error cannot be caught
+# because somehow the rescue block isn't set up
+# correctly.
+# set rec 0
+# set tn "Test: recursion limit"
+# inc tc
+# rescue {
+# 	print "PASS $tc $tn: limited\n"
+# }
+# to test_recursion_limit {
+# 	rescue {
+# 		return "PASS $tc $tn: limited\n"
+# 	}
+# 	inc rec
+# 	print "$rec "
+# 	if [igt $rec 38]  {
+# 		print  "FAIL $tc $tn: not limited\n"
+# 		return 0
+# 	}
+# 	test_recursion_limit
+# }
+# nop
+# print [test_recursion_limit]
+# return "PASS $tc $tn: returned\n"
+
+

+ 260 - 0
cmd/attl/testdata/builtin.attl

@@ -0,0 +1,260 @@
+#!/usr/bin/env attl
+#
+# Tests for the builtins except the following:
+# if: if runs $1 if $0 is true, otherwise runs $2
+# fail: fail execution of a procedure
+# set: sets a variable
+# get: get the contents of a variable
+# ret: return from a procedure
+# return: return from a procedure
+# to: define a procedure
+# see basic.attl for tests for these builtins
+
+## Test function
+# Performs a test case case and then passes the test if cond returns nonzero
+# Can also rescue failures.
+to test case cond {
+	rescue {
+		let res $1
+		if [cond $res "fail"] {
+			print "PASS rescue test $1: $2\n" $case $res
+			return 0
+		} else {
+			print "FAIL rescue test $1: $2\n" $case $res
+			return 0
+		}
+		return "$res"
+	}
+	let res [case]
+	if [cond $res "return"] {
+		print "PASS test $1: $2\n" $case $res
+	} else {
+		print "FAIL test $1: $2\n" $case $res
+	}
+}
+
+## Output
+
+# print: print to the environnment's current output writer
+print "PASS print"
+
+## Help
+# explain: set the help for a procedure
+explain "PASS" "PASS help"
+
+# help: get help for a procedure
+help "PASS"
+
+## Strings
+# sadd: returns a string  with $2 appended to string $1
+test {sadd "PA" "SS"} {seq $1 "PASS"}
+# sget: gets a rune from a string by index
+test {sget "abµc" 2} {seq $1 181}
+# slen: returns the length of a string in runes
+test {slen "abµc"} {seq $1 4}
+# expand: interpolate strings from environment
+let e "PASS expand"
+print "$1\n" [expand "$e"]
+
+# Equality
+# seq: checks if [str $1] == [str $2]
+if [seq "7" 7] {
+	print "PASS seq\n"
+}
+
+# ieq: checks if $1 == $2, where $1 and $2 must be Int
+if [ieq 7 7] {
+	print "PASS ieq\n"
+}
+
+## Integers
+# iadd: adds an Int to an Int
+test {iadd 3 4} {ieq $1 7}
+# isub: subtracts an Int from an Int
+test {isub 3 4} {ieq $1 -1}
+# imul: multiplies an Int by an Int
+test {imul 3 4} {ieq $1 12}
+# idiv: divides an Int by an Int
+test {idiv 12 4} {ieq $1 3}
+# idiv: divide an Int by 0 should give an error
+test {idiv 12 0} {seq $1 "division by 0"}
+# ilt: checks if $1 < $2, where $1 and $2 must be Int
+test {ilt 12 4} {ieq $1 0}
+# ilt: checks if $1 < $2, where $1 and $2 must be Int
+test {ilt 4 12} {ieq $1 -1}
+# igt: checks if $1 > $2, where $1 and $2 must be Int
+test {igt 12 4} {ieq $1 -1}
+# igt: checks if $1 > $2, where $1 and $2 must be Int
+test {igt 4 12} {ieq $1 0}
+# ile: checks if $1 <= $2, where $1 and $2 must be Int
+test {ile 12 4} {ieq $1 0}
+# ile: checks if $1 <= $2, where $1 and $2 must be Int
+test {ile 4 12} {ieq $1 -1}
+# ige: checks if $1 >= $2, where $1 and $2 must be Int
+test {ige 12 4} {ieq $1 -1}
+# ige: checks if $1 >= $2, where $1 and $2 must be Int
+test {ige 4 12} {ieq $1 0}
+
+let i 123
+# inc: increments the named integer $1 and returns it
+test {inc i} {ieq $i 124}
+
+set i 123
+# dec: decrements the named integer $1 and returns it
+test {dec i} {ieq $i 122}
+
+## Booleans
+# bor: returns true if one of its arguments are true
+test {bor 0 0} {ieq $1 0}
+# bor: returns true if one of its arguments are true
+test {bor 0 -1} {ieq $1 -1}
+# bor: returns true if one of its arguments are true
+test {bor -1 0} {ieq $1 -1}
+# bor: returns true if one of its arguments are true
+test {bor -1 -1} {ieq $1 -1}
+# band: returns true if all of its arguments are true
+test {band 0 0} {ieq $1 0}
+# band: returns true if all of its arguments are true
+test {band 0 -1} {ieq $1 0}
+# band: returns true if all of its arguments are true
+test {band -1 0} {ieq $1 0}
+# band: returns true if all of its arguments are true
+test {band -1 -1} {ieq $1 -1}
+# bxor: returns true if its arguments are different
+test {bxor 0 0} {ieq $1 0}
+# bxor: returns true if its arguments are different
+test {bxor 0 -1} {ieq $1 -1}
+# bxor: returns true if its arguments are different
+test {bxor -1 0} {ieq $1 -1}
+# bxor: returns true if its arguments are different
+test {bxor -1 -1} {ieq $1 0}
+# bnot
+test {bnot -1 } {ieq $1 0}
+# bnot
+test {bnot 0 } {ieq $1 -1}
+
+## Lists
+# list: creates a new array list
+let l [list 1 2 3]
+test {expand "$l"} {seq $1 "[list 1 2 3]"}
+# llen: returns the length of a list
+test {llen $l} {ieq $1 3}
+# lget: gets a value from a list by index
+test {lget $l 0} {ieq $1 1}
+# lget: gets a value from a list by index
+test {lget $l 1} {ieq $1 2}
+# lget: out of bounds
+test {lget $l 77} {seq $1 "index out of range"}
+# lget: out of bounds
+test {lget $l -1} {seq $1 "index out of range"}
+# lset: sets a value to a list by index and value
+lset $l 1 7
+test {lget $l 1} {ieq $1 7}
+# ladd: returns a list with $2 appended to List $1
+set l [ladd $l 99]
+test {lget $l 3} {seq $1 99}
+# leach: calls the block $4 for each entry in the list
+to leach_test l {
+	let ksum 0
+	let vsum 0
+	let reps 0
+	let l2 $l
+	leach $l k v {
+	    inc reps
+		set ksum [iadd $ksum $k] 2
+		set vsum [iadd $vsum $v] 2
+		# try to cause overflow, should not cause leach to change
+		ladd $l2 77
+	}
+	return [list $ksum $vsum $reps]
+}
+test {leach_test $l} {set l $1; seq [str $l] "[list 6 110 4]"}
+
+## Maps
+# map: creates a new hash map
+let m [map "key3" "value3"]
+test {expand "$m"} {seq $1 "[map  key3 value3]"}
+# mget: gets a value from a map by key
+test {mget $m "key3"} {seq $1 "value3"}
+
+# mset: sets a value to a map by key and value
+mset $m "key1" "value1"
+mset $m "key2" "2"
+test {mget $m key2} {seq $1 "2"}
+test {mget $m key1} {seq $1 "value1"}
+
+# mkeys: returns all keys of a map as an unsorted list
+test {mkeys $m} {set k $1; ieq [llen $k] 3}
+
+# meach: calls the block $4 for each entry in the list
+to meach_test m {
+	let ksum ""
+	let vsum ""
+	let reps 0
+	meach $m k v {
+	    inc reps
+		set ksum [sadd $ksum $k]
+		set vsum [sadd $vsum [str $v]]
+		# try to cause overflow, should not cause meach to change iterations
+		mset $m $ksum $vsum
+	}
+	return $reps
+}
+test {meach_test $m} {set m $1;  ieq $m 3}
+
+## Conversions
+# str: converts $1 to String
+test {str 123} {seq $1 "123"}
+
+# int: converts $1 to Int
+test {int "-123"} {ieq $1 -123}
+test {int "banana"} {seq $1 "Value cannot be converted"}
+test {int "-123banana"} {ieq $1 -123}
+
+# val: gets the value of a value
+let vi 23
+# FIXME: val has a bug
+# test {val $vi} {ieq $1 23}
+
+## Types
+# type: returns $1 converted to a type
+test {type Foo} {seq $1 "Foo"}
+# typeof: returns the type of $1 or Unknown if not known
+test {typeof 1} {seq $1 [type Int]}
+# typeof: String
+test {typeof "foo"} {seq $1 [type String]}
+# typeof: Word
+test {typeof FooWord} {seq $1 [type Word]}
+# teq: checks if $1 and $2 are exactly the same type"
+test {teq [type Foo] [type Foo]} {ieq $1 -1}
+
+## Control statemnts
+let i 10
+# while: executes $2 while $1 returns true
+to while_test {
+	while { igt $i 0 } {
+		dec i
+	}
+}
+test {while_test} {ieq $i 0}
+
+# switch: selects one of many cases
+to switch_test v {
+	switch $v 0 {
+		return 10
+	} 1 {
+		return 20
+	} 2 {
+		return 30
+	} "3" {
+		return 40
+	} default {
+		return 50
+	}
+}
+test {switch_test 0} {ieq $1 10}
+test {switch_test 1} {ieq $1 20}
+test {switch_test 2} {ieq $1 30}
+test {switch_test "3"} {ieq $1 40}
+test {switch_test "not in case"} {ieq $1 50}
+

+ 27 - 0
cmd/attl/testdata/overload.attl

@@ -0,0 +1,27 @@
+#!/usr/bin/env attl
+#
+#
+
+overload add $iadd Int Int
+overload add sadd String String
+overload add {
+	print "In _String_Int\n"
+	break [sadd $1 [str $2]]
+} String Int
+
+print "$1\n" [add "7" "9"]
+print "$1\n" [add 7 9]
+
+to bad_overload {
+	rescue {
+		print "PASS: Failure expected\n"
+		rescue nil
+	}
+	print "$1\n" [add 7 "9"]
+}
+
+bad_overload
+
+print "Finally $1\n" [add "9" 7]
+
+

+ 247 - 0
convert.go

@@ -0,0 +1,247 @@
+package attl
+
+//Converter is an interface that Values can optionally implement
+// to allow conversion to other arbitrary types at run time.
+type Converter interface {
+	Convert(to interface{}) *Error
+}
+
+func (from Int) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *int8:
+		(*toPtr) = int8(from)
+	case *int16:
+		(*toPtr) = int16(from)
+	case *int32:
+		(*toPtr) = int32(from)
+	case *int64:
+		(*toPtr) = int64(from)
+	case *int:
+		(*toPtr) = int(from)
+	case *bool:
+		(*toPtr) = (from != 0)
+	case *Bool:
+		(*toPtr) = (from != 0)
+	case *float32:
+		(*toPtr) = float32(from)
+	case *float64:
+		(*toPtr) = float64(from)
+	case *Int:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert Int value")
+	}
+	return nil
+}
+
+func (from Bool) Convert(to interface{}) *Error {
+	iVal := 0
+	if from {
+		iVal = -1
+	}
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *int8:
+		(*toPtr) = int8(iVal)
+	case *int16:
+		(*toPtr) = int16(iVal)
+	case *int32:
+		(*toPtr) = int32(iVal)
+	case *int64:
+		(*toPtr) = int64(iVal)
+	case *int:
+		(*toPtr) = int(iVal)
+	case *bool:
+		(*toPtr) = bool(from)
+	case *Bool:
+		(*toPtr) = from
+	case *float32:
+		(*toPtr) = float32(iVal)
+	case *float64:
+		(*toPtr) = float64(iVal)
+	case *Int:
+		(*toPtr) = Int(iVal)
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert Int value")
+	}
+	return nil
+}
+
+func (from Word) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *bool:
+		(*toPtr) = (from.String() != "")
+	case *Bool:
+		(*toPtr) = (from.String() != "")
+	case *Word:
+		(*toPtr) = from
+	case *Type:
+		(*toPtr) = Type(string(from))
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert Word value")
+	}
+	return nil
+}
+
+func (from Type) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *bool:
+		(*toPtr) = (from.String() != "")
+	case *Bool:
+		(*toPtr) = (from.String() != "")
+	case *Type:
+		(*toPtr) = from
+	case *Word:
+		(*toPtr) = Word(string(from))
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert Word value")
+	}
+	return nil
+}
+
+func (from String) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *bool:
+		(*toPtr) = (from.String() != "")
+	case *Bool:
+		(*toPtr) = (from.String() != "")
+	case **Error:
+		(*toPtr) = ErrorFromString(from.String())
+	case *String:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert String value")
+	}
+	return nil
+}
+
+func (from *Error) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *string:
+		(*toPtr) = from.String()
+	case *bool:
+		(*toPtr) = (from == nil)
+	case *Bool:
+		(*toPtr) = (from == nil)
+	case *error:
+		(*toPtr) = from
+	case **Error:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert Error value")
+	}
+	return nil
+}
+
+func (from Block) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *bool:
+		(*toPtr) = (len(from.Statements) > 0)
+	case *Bool:
+		(*toPtr) = (len(from.Statements) > 0)
+	case *Block:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert block value")
+	}
+	return nil
+}
+
+func (from Map) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *bool:
+		(*toPtr) = (len(from) > 0)
+	case *Bool:
+		(*toPtr) = (len(from) > 0)
+	case *Map:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert map value")
+	}
+	return nil
+}
+
+func (from List) Convert(to interface{}) *Error {
+	switch toPtr := to.(type) {
+	case *bool:
+		(*toPtr) = (len(from) > 0)
+	case *Bool:
+		(*toPtr) = (len(from) > 0)
+	case *List:
+		(*toPtr) = from
+	case *Value:
+		(*toPtr) = from
+	default:
+		return ErrorFromString("Cannot convert map value")
+	}
+	return nil
+}
+
+func ValToBool(val Value) bool {
+	if val == nil {
+		return false
+	}
+	switch check := val.(type) {
+	case *Error:
+		return check == nil
+	case Int:
+		return (int(check) != 0)
+	case Bool:
+		return bool(check)
+	default:
+		return val.String() != ""
+	}
+}
+
+func Convert(val Value, to interface{}) *Error {
+	if converter, ok := val.(Converter); ok {
+		return converter.Convert(to)
+	} else if pval, ok := to.(*Value); ok {
+		*pval = val
+		return nil
+	}
+	return ErrorFromString("Value cannot be converted")
+}
+
+// StringList makes a List from string arguments
+func StringList(sa ...string) List {
+	list := List{}
+	for _, s := range sa {
+		list = append(list, String(s))
+	}
+	return list
+}
+
+// Converts a list to raw strings
+func (l List) ToStrings() []string {
+	res := []string{}
+	for _, s := range l {
+		res = append(res, s.String())
+	}
+	return res
+}

+ 31 - 0
effect.go

@@ -0,0 +1,31 @@
+package attl
+
+// Flow deterimes how the flow of execution is
+// affected by a command
+type Flow int
+
+// No effect
+const NormalFlow Flow = 0
+
+// Breaks out of the current block
+const BreakFlow Flow = 1
+
+// Breaks out of the current command
+const ReturnFlow Flow = 2
+
+// Error, breaks until rescue block is ofound
+const FailFlow Flow = 4
+
+// Every attl command evaluates to a value, which is the result
+// of the command itself, but also an Effect that describes
+// it's special effect on the flow of evaluation itself.
+// A nil effect simply means "continue to the next command"
+// But other effects may cause the flow of execution to change
+// as per the Flow member
+// The unwrap member returns the Value that
+// the effect was carrying wrapped in it
+// and which is unwrapped when the effect has influenced the flow.
+type Effect interface {
+	Flow() Flow
+	Unwrap() Value
+}

+ 338 - 0
environment.go

@@ -0,0 +1,338 @@
+package attl
+
+// Maximum amount of frames,
+// to prevent unlimited recursion.
+const FRAMES_MAX = 80
+
+type Writer interface {
+	Write(p []byte) (n int, err error)
+}
+
+type Reader interface {
+	Read(p []byte) (n int, err error)
+}
+
+type Frame struct {
+	Variables Map
+	Effect
+	Out     Writer
+	In      Reader
+	Rescuer Value
+}
+
+type Environment struct {
+	Frames   []*Frame
+	Out      Writer
+	In       Reader
+	Rescuing bool
+}
+
+// Looks up the value of a variable and the frame it is in
+func (env Environment) LookupFrame(name string) (Value, *Frame) {
+	for i := len(env.Frames) - 1; i >= 0; i-- {
+		frame := env.Frames[i]
+		val, ok := frame.Variables[name]
+		if ok {
+			return val, frame
+		}
+	}
+	return nil, nil
+}
+
+func (env Environment) Lookup(name string) Value {
+	val, _ := env.LookupFrame(name)
+	return val
+}
+
+func (env *Environment) Push() *Error {
+	env.Frames = append(env.Frames, &Frame{make(Map), nil, env.Out, env.In, nil})
+	if len(env.Frames) >= FRAMES_MAX && !env.Rescuing {
+		return ErrorFromString("PROGRAM HAS DISAPPEARED INTO THE BLACK LAGOON - too much recursion or function calls")
+	}
+	return nil
+}
+
+func (env *Environment) Pop() {
+	l := len(env.Frames)
+	if l > 0 {
+		env.Frames = env.Frames[0 : l-1]
+	}
+}
+
+// Depth returns the amount of frames on the frame stack
+func (env *Environment) Depth() int {
+	return len(env.Frames)
+}
+
+// Frame returns a frame pointer based on the level.
+// 0 is the top-level index. A negative level wil refer to the
+// outermost frame.
+// Returns nil if the level is somehow out of range or
+// if no frames have been pushed yet
+func (env *Environment) Frame(level int) *Frame {
+	if len(env.Frames) < 1 {
+		return nil
+	}
+	if level > 0 {
+		l := len(env.Frames)
+		index := l - level - 1
+		if index < 0 || index >= l {
+			return nil
+		}
+		return env.Frames[index]
+	} else {
+		return env.Frames[0]
+	}
+}
+
+// Top returns a pointer to the top or
+// inner most Frame of the environment's frame stack
+func (env *Environment) Top() *Frame {
+	if len(env.Frames) > 0 {
+		index := len(env.Frames) - 1
+		return env.Frames[index]
+	}
+	return nil
+}
+
+// Botttom returns a pointer to the bottom or// outer most Frame of the environment
+func (env *Environment) Bottom() *Frame {
+	if len(env.Frames) > 0 {
+		return env.Frames[0]
+	}
+	return nil
+}
+
+// Defines the variable in the given scope level
+func (env *Environment) Define(name string, val Value, level int) (Value, Effect) {
+	frame := env.Frame(level)
+	if frame == nil {
+		return nil, ErrorFromString("no such frame available.")
+	}
+	frame.Variables[name] = val
+	return val, nil
+}
+
+// Looks up the variable and sets it in the scope where it is found.
+// Returns an error if no such variable could be found.
+func (env *Environment) Set(name string, val Value) (Value, Effect) {
+	_, frame := env.LookupFrame(name)
+	if frame == nil {
+		return nil, ErrorFromString("no such variable")
+	}
+	frame.Variables[name] = val
+	return val, nil
+}
+
+func (env *Environment) Rescuer() Value {
+	frame := env.Top()
+	if frame == nil {
+		return nil
+	}
+	return frame.Rescuer
+}
+
+// Prevent sets the rescue block to use for
+// the top frame of the environment.
+// It returns the previous rescuer.
+func (env *Environment) Prevent(block Block) (Value, Effect) {
+	frame := env.Frame(1)
+	if frame == nil {
+		return env.FailString("Could not set rescuer")
+	}
+	old := frame.Rescuer
+	frame.Rescuer = Rescue{block}
+	return old, nil
+}
+
+//
+func (env *Environment) Rescue(res Value, eff Effect) (Value, Effect) {
+	if eff == nil || eff.Flow() < FailFlow {
+		return res, eff
+	}
+	// if there is no rescue installed,
+	// just return as is.
+	if env.Rescuer() == nil {
+		return res, eff
+	}
+	// failures become normal returns
+	// if the rescue didn't fail.
+	val := eff.Unwrap()
+	rres, reff := env.Rescuer().Eval(env, val, res)
+	if reff == nil {
+		return env.Return(rres)
+	} else {
+		// Here, unpack the effect and replace it with a return
+		// to avoid recursion loops of fail in rescue
+		return env.Return(reff.Unwrap())
+	}
+}
+
+func (env *Environment) Flow() Flow {
+	if len(env.Frames) > 0 {
+		index := len(env.Frames) - 1
+		return env.Frames[index].Flow()
+	}
+	return FailFlow
+}
+
+func (env *Environment) SetEffect(e Effect) Effect {
+	frame := env.Frame(1)
+	if frame != nil {
+		frame.Effect = e
+	}
+	return e
+}
+
+func (env *Environment) Return(val Value) (Value, Effect) {
+	effect := env.SetEffect(Return{val})
+	return val, effect
+}
+
+func (env *Environment) Fail(err *Error) (Value, Effect) {
+	effect := env.SetEffect(err)
+	return nil, effect
+}
+
+func (env *Environment) Break(val Value) (Value, Effect) {
+	effect := env.SetEffect(Break{val})
+	return val, effect
+}
+
+func (env *Environment) FailString(msg string, args ...Value) (Value, Effect) {
+	return env.Fail(env.ErrorFromString(msg, args...))
+}
+
+func (env Environment) Interpolate(s string, args ...Value) string {
+	runes := []rune(s)
+	res := []rune{}
+	name := []rune{}
+	inName := 0
+	for i, a := range args {
+		env.Define(Itoa(i+1), a, 0)
+	}
+	apply := func() {
+		inName = 0
+		val := env.Lookup(string(name))
+		if val == nil {
+			res = append(res, '!', 'n', 'i', 'l')
+		} else {
+			add := []rune(val.String())
+			res = append(res, add...)
+		}
+		name = []rune{}
+	}
+	for i := 0; i < len(runes); i++ {
+		r := runes[i]
+		switch r {
+		case '$':
+			if inName == 0 {
+				inName = 1
+			} else if inName == 1 {
+				if len(name) < 1 {
+					// $$ escape
+					res = append(res, '$')
+					inName = 0
+				} else { // $ at end of name
+					apply()
+				}
+			}
+		case '{':
+			if inName > 0 {
+				inName++
+			} else {
+				res = append(res, '{')
+			}
+		case '}':
+			if inName > 0 {
+				inName--
+				if inName == 1 {
+					apply()
+				}
+			} else {
+				res = append(res, '}')
+			}
+		default:
+			if inName > 0 {
+				if IsNumber(r) || IsLetter(r) {
+					name = append(name, r)
+				} else {
+					apply()
+					res = append(res, r)
+				}
+			} else {
+				res = append(res, r)
+			}
+		}
+	}
+	if len(name) > 0 {
+		apply()
+	}
+	return string(res)
+}
+
+func (env Environment) Printi(msg string, args ...Value) (int, error) {
+	msg = env.Interpolate(msg, args...)
+	return env.Write(msg)
+}
+
+func (env Environment) Write(msg string) (int, error) {
+	buf := []byte(msg)
+	writer := env.Out
+	if len(env.Frames) > 0 {
+		writer = env.Frames[len(env.Frames)-1].Out
+	}
+	if writer == nil {
+		return -1, env.ErrorFromString("no writer set in environment.")
+	}
+	return writer.Write(buf)
+}
+
+func (env Environment) ErrorFromString(msg string, args ...Value) *Error {
+	msg = env.Interpolate(msg, args...)
+	return ErrorFromString(msg)
+}
+
+// Complete is for use with liner
+func (env Environment) Complete(prefix String) List {
+	res := List{}
+	for _, frame := range env.Frames {
+		for name, _ := range frame.Variables {
+			if len(name) >= len(prefix) {
+				if String(name[0:len(prefix)]) == prefix {
+					res = append(res, String(name))
+				}
+			}
+		}
+	}
+	if len(res) == 0 {
+		res = append(res, prefix)
+	}
+	return res.SortStrings()
+}
+
+func (env *Environment) Overload(name string, target Value, types []Value) (Value, Effect) {
+	val := env.Lookup(name)
+	cov, ok := val.(Overload)
+	if val == nil {
+		cov = make(Overload)
+	} else if !ok {
+		return env.FailString("Not a overload: " + name)
+	}
+	signature := ""
+	for _, arg := range types {
+		signature += "_" + arg.String()
+	}
+	if _, ok := target.(String); ok {
+		tarVal := env.Lookup(target.String())
+		cov[signature] = tarVal
+	} else if _, ok := target.(Word); ok {
+		tarVal := env.Lookup(target.String())
+		cov[signature] = tarVal
+	} else {
+		cov[signature] = target
+	}
+
+	env.Define(name, cov, -1)
+	return cov, nil
+}

+ 5 - 0
go.mod

@@ -0,0 +1,5 @@
+module src.eruta.nl/beoran/attl
+
+go 1.16
+
+require github.com/peterh/liner v1.2.2

+ 6 - 0
go.sum

@@ -0,0 +1,6 @@
+github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw=
+github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI=
+golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1 h1:kwrAHlwJ0DUBZwQ238v+Uod/3eZ8B2K5rYsUHBQvzmI=
+golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

+ 459 - 0
parse.go

@@ -0,0 +1,459 @@
+package attl
+
+// ParseFunc is a parser function.
+// It parses the input input starting from *index, which must be
+// guaranteed by the caller to be non-nil.
+// It should return as follows:
+// * If the parse function matched what it is intended to parse
+//   it should return the parsed value, nil, and index should be moved to
+//   point right after the parsed part of te string.
+// * If the parse function did not match what it is intended to parse
+//   it should retirn nil, nil, and index  should be unchanged.
+// * If the parse function did match what it is intended to parse
+//   but there is a parse error, it should return nil, *Error,
+//   and index should be set to the error location.
+type ParseFunc func(input []rune, index *int) (Value, *Error)
+
+var Debug = false
+
+func debug(msg string) {
+	if Debug {
+		print(msg)
+	}
+}
+
+func ParseAlternative(input []rune, index *int, funcs ...ParseFunc) (Value, *Error) {
+	for _, fun := range funcs {
+		val, err := fun(input, index)
+		if err != nil || val != nil {
+			return val, err
+		}
+	}
+	return nil, nil
+}
+
+func ParseWhileRuneOk(input []rune, index *int, ok func(r rune) bool) (Value, *Error) {
+	length := len(input)
+	start, now := *index, 0
+	for ; *index < length; *index++ {
+		r := input[*index]
+		if !ok(r) {
+			if now == 0 {
+				return nil, nil
+			}
+			return String(input[start:*index]), nil
+		}
+		now++
+	}
+	return nil, ErrorFromString("unexpected EOF: >" + string(input[start:*index]) + "<")
+}
+
+type LineInfo struct {
+	Line int
+	From int
+	To   int
+}
+
+type LineIndex []LineInfo
+
+func PhysicalLineIndex(input []rune) LineIndex {
+	res := LineIndex{}
+	line, last, index := 0, 0, 0
+	for ; index < len(input); index++ {
+		ch := input[index]
+		if ch == '\n' {
+			line++
+			li := LineInfo{line, last, index}
+			last = index
+			res = append(res, li)
+		}
+	}
+	li := LineInfo{line, last, index}
+	res = append(res, li)
+	return res
+}
+
+func (li LineIndex) Lookup(index int) (row, col int) {
+	for _, info := range li {
+		if index >= info.From && index < info.To {
+			return info.Line, index - info.From
+		}
+	}
+	return -1, -1
+}
+
+func Parse(input string) (value Value, rerr *Error) {
+	index := 0
+	return ParseScript([]rune(input), &index)
+}
+
+func ParseScript(input []rune, index *int) (value Value, rerr *Error) {
+	defer func() {
+		val := recover()
+		err, ok := val.(*Error)
+		if ok {
+			rerr.Children = append(rerr.Children, err)
+		}
+	}()
+	value, rerr = ParseStatements([]rune(input), index)
+	if value != nil {
+		value = Block{value.(List)}
+	}
+	return value, rerr
+}
+
+func IsEof(input []rune, index *int) bool {
+	return *index >= len(input)
+}
+
+func ParseStatements(input []rune, index *int) (Value, *Error) {
+	debug("ParseStatements")
+	statements := List{}
+	for {
+		val, err := ParseStatement(input, index)
+		if err != nil {
+			debug("error in statement")
+			return nil, err
+		}
+		if val != nil {
+			statements = append(statements, val)
+		}
+		sep, err := ParseRs(input, index)
+		if IsEof(input, index) {
+			return statements, nil
+		}
+		if err != nil {
+			debug("error in rs")
+			return nil, err
+		}
+		if sep == nil {
+			return statements, nil
+		}
+	}
+}
+
+func ParseRs(input []rune, index *int) (Value, *Error) {
+	debug("ParseRs")
+	SkipWs(input, index)
+	return ParseWhileRuneOk(input, index, func(r rune) bool {
+		return r == '\n' || r == '\r' || r == ';'
+	})
+}
+
+func ParseWs(input []rune, index *int) (Value, *Error) {
+	debug("ParseWs")
+	return ParseWhileRuneOk(input, index, func(r rune) bool {
+		return r == ' ' || r == '\t'
+	})
+}
+
+func ParseWsRs(input []rune, index *int) (Value, *Error) {
+	debug("ParseRs")
+	SkipWs(input, index)
+	return ParseWhileRuneOk(input, index, func(r rune) bool {
+		return r == '\n' || r == '\r' || r == ';' || r == ' ' || r == '\t'
+	})
+}
+
+func SkipWs(input []rune, index *int) {
+	ParseWs(input, index)
+}
+
+func SkipRs(input []rune, index *int) {
+	ParseRs(input, index)
+}
+
+func SkipWsRs(input []rune, index *int) {
+	ParseWsRs(input, index)
+}
+
+func ParseComment(input []rune, index *int) (Value, *Error) {
+	debug("ParseComment")
+	start := *index
+	if !RequireRune(input, index, '#') {
+		return nil, nil
+	}
+	for ; *index < len(input); *index++ {
+		r := input[*index]
+		if r == '\n' || r == '\r' {
+			end := *index
+			return Comment(string(input[start:end])), nil
+		}
+	}
+	return nil, ErrorFromString("unexpected EOF in comment")
+}
+
+func ParseStatement(input []rune, index *int) (Value, *Error) {
+	debug("ParseStatement")
+	SkipWs(input, index)
+	return ParseAlternative(input, index, ParseCommand, ParseBlock, ParseComment)
+}
+
+func ParseParameters(input []rune, index *int) (Value, *Error) {
+	debug("ParseParameters")
+	params := List{}
+	for {
+		sep, err := ParseWs(input, index)
+		if err != nil {
+			return nil, err
+		}
+		if sep == nil {
+			return params, nil
+		}
+		val, err := ParseParameter(input, index)
+		if err != nil {
+			return nil, err
+		}
+		if val == nil {
+			return params, nil
+		}
+		params = append(params, val)
+	}
+}
+
+func ParseParameter(input []rune, index *int) (Value, *Error) {
+	debug("ParseParameter")
+	funcs := []ParseFunc{ParseLiteral, ParseEvaluation, ParseBlock, ParseGetter}
+	return ParseAlternative(input, index, funcs...)
+}
+
+func ParseOrder(input []rune, index *int) (Value, *Error) {
+	debug("ParseOrder")
+	return ParseAlternative(input, index, ParseLiteral, ParseEvaluation)
+}
+
+func ParseCommand(input []rune, index *int) (Value, *Error) {
+	debug("ParseCommand")
+	order, err := ParseOrder(input, index)
+	if err != nil || order == nil {
+		return order, err
+	}
+	params, err := ParseParameters(input, index)
+	if err != nil {
+		return params, err
+	}
+	if params == nil {
+		params = List{}
+	}
+	return Command{order, params.(List)}, nil
+}
+
+// RequireRune requires a single rune to be present,
+// and skips it, however that rune is discared.
+// Returns true if the rune was found, false if not
+func RequireRune(input []rune, index *int, req rune) bool {
+	if input[*index] == req {
+		*index++
+		return true
+	}
+	return false
+}
+
+func ParseEvaluation(input []rune, index *int) (Value, *Error) {
+	debug("ParseEvaluation")
+	if !RequireRune(input, index, '[') {
+		return nil, nil
+	}
+	res, err := ParseCommand(input, index)
+	if err != nil {
+		return nil, err
+	}
+	if !RequireRune(input, index, ']') {
+		print(input[*index])
+		return nil, ErrorFromString("Expected end of evaluation ]")
+	}
+	if res != nil {
+		res = Evaluation{Command: res.(Command)}
+	}
+	return res, nil
+}
+
+func ParseBlock(input []rune, index *int) (Value, *Error) {
+	debug("ParseBlock")
+	if !RequireRune(input, index, '{') {
+		return nil, nil
+	}
+	res, err := ParseStatements(input, index)
+	if err != nil {
+		return nil, err
+	}
+	SkipWsRs(input, index)
+	if !RequireRune(input, index, '}') {
+		return nil, ErrorFromString("Expected end of block }")
+	}
+	return Block{Statements: res.(List)}, nil
+	return nil, nil
+}
+
+func ParseGetter(input []rune, index *int) (Value, *Error) {
+	debug("ParseGetter")
+	if RequireRune(input, index, '$') {
+		if input[*index] == '$' { // recusively parse double getters
+			val, err := ParseGetter(input, index)
+			if err == nil { // Getter with a getter inside.
+				return Getter{val}, err
+			} else {
+				return nil, err
+			}
+		} else { // integer, sring or getter name
+			key, err := ParseLiteral(input, index)
+			if key == nil {
+				return nil, ErrorFromString("Expected literal after getter $")
+			}
+			if err == nil {
+				return Getter{key}, nil
+			}
+			return nil, err
+		}
+	}
+	return nil, nil
+}
+
+func ParseLiteral(input []rune, index *int) (Value, *Error) {
+	debug("ParseLiteral")
+	return ParseAlternative(input, index, ParseWord, ParseString, ParseInteger,
+		ParseRawString)
+}
+
+func IsLetter(r rune) bool {
+	return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r > rune(128)) ||
+		r == '_' || r == '/'
+}
+
+func IsNumber(r rune) bool {
+	return (r >= '0' && r <= '9')
+}
+
+func ParseWord(input []rune, index *int) (Value, *Error) {
+	debug("ParseWord")
+	// a word consists of an ascii letter or non asci characters, or underscore
+	// followed by an ascii letter or number, or non ascii characters, or underscore
+	start := *index
+	r := input[*index]
+	if !IsLetter(r) {
+		return nil, nil
+	}
+	for *index++; *index < len(input); *index++ {
+		r := input[*index]
+		if !(IsLetter(r) || IsNumber(r)) {
+			return Word(string(input[start:*index])), nil
+		}
+	}
+	return nil, ErrorFromString("unexpected EOF in string")
+}
+
+func next(input []rune, index *int) {
+	*index++
+	if *index >= len(input) {
+		panic(ErrorFromString("Unexpected end of input."))
+	}
+}
+
+func ParseEscape(input []rune, index *int) (Value, *Error) {
+	res := ""
+	if input[*index] != '\\' {
+		return nil, nil
+	}
+	next(input, index)
+	switch input[*index] {
+	case 'a':
+		res += "\a"
+	case 'b':
+		res += "\b"
+	case 'e':
+		res += "\033"
+	case 'f':
+		res += "\f"
+	case 'n':
+		res += "\n"
+	case 'r':
+		res += "\r"
+	case 't':
+		res += "\t"
+	case '\\':
+		res += "\\"
+	case '"':
+		res += "\""
+	default:
+		return nil, ErrorFromString("Unknown escape sequence character")
+	}
+
+	return String(res), nil
+}
+
+func ParseString(input []rune, index *int) (Value, *Error) {
+	debug("ParseString")
+	res := ""
+	ch := input[*index]
+	if ch != '"' {
+		return nil, nil
+	}
+	*index++
+	for *index < len(input) {
+		ch = input[*index]
+		esc, err := ParseEscape(input, index)
+		if err != nil {
+			return nil, err
+		}
+		if esc != nil {
+			res += string(esc.(String))
+		} else if ch == '"' {
+			*index++
+			return String(res), nil
+		} else {
+			res += string(ch)
+		}
+		*index++
+	}
+	return nil, ErrorFromString("Unexpected end of input.")
+}
+
+func ParseRawString(input []rune, index *int) (Value, *Error) {
+	debug("ParseRawString")
+	res := ""
+	ch := input[*index]
+	if ch != '`' {
+		return nil, nil
+	}
+	*index++
+	for *index < len(input) {
+		ch = input[*index]
+		if ch == '`' {
+			*index++
+			return String(res), nil
+		} else {
+			res += string(ch)
+		}
+		*index++
+	}
+	return nil, ErrorFromString("Unexpected end of input.")
+}
+
+func ParseInteger(input []rune, index *int) (Value, *Error) {
+	debug("ParseInteger")
+	ch := input[*index]
+	neg := 1
+	res := 0
+	if ch == '-' {
+		neg = -1
+	} else if ch == '+' {
+		// do nothing, ignore + as an integer prefix
+	} else {
+		res = int(ch - '0')
+		if res < 0 || res > 9 { // Not a digit, no integer
+			return nil, nil
+		}
+	}
+	*index++
+	for *index < len(input) {
+		ch = input[*index]
+		ch -= '0'
+		if ch < 0 || ch > 9 { // Not a digit, finished
+			return Int(neg * res), nil
+		}
+		res = res * 10
+		res = res + int(ch)
+		*index++
+	}
+	return nil, ErrorFromString("unexpected EOF in number")
+}

+ 385 - 0
parse_test.go

@@ -0,0 +1,385 @@
+package attl
+
+import "testing"
+import "reflect"
+
+type testCase struct {
+	ParseFunc
+	input         string
+	index         int
+	expectedIndex int
+	expectError   bool
+	expectedValue Value
+}
+
+func (tc *testCase) Run(t *testing.T) {
+	t.Logf("Test case input: %s", tc.input)
+	res, err := tc.ParseFunc([]rune(tc.input), &tc.index)
+	if tc.expectError {
+		if err == nil {
+			t.Errorf("expected parse error")
+			return
+		}
+	} else {
+		if err != nil {
+			t.Errorf("error: unexpected parse error at %d: %v", tc.index, err)
+			return
+		}
+	}
+	if tc.index != tc.expectedIndex {
+		if err == nil {
+			t.Errorf("error: index not correct: %d <> %d", tc.index, tc.expectedIndex)
+		}
+	}
+	if tc.expectedValue == nil {
+		if res != nil {
+			t.Errorf("error: expected nil value, got %v", res)
+		} else {
+			t.Logf("Test case value: nil")
+		}
+	} else {
+		if res == nil {
+			t.Errorf("error: expected value %v was nil", tc.expectedValue)
+		} else {
+			t.Logf("Test case value: %v", res)
+			se := tc.expectedValue.String()
+			so := res.String()
+			if so != se {
+				t.Errorf("error: value is not as expected: %s <-> %s", so, se)
+			} else {
+				if !reflect.DeepEqual(tc.expectedValue, res) {
+					t.Errorf("error: values are not deeply equal: %s <-> %s", so, se)
+				}
+			}
+		}
+	}
+	rs := []rune(tc.input)
+	b := string(rs[0:tc.index])
+	a := string(rs[tc.index:len(rs)])
+	t.Logf("Test case index: %s|%s", b, a)
+}
+
+func TestParseComment(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseComment, "# comment\nnot comment", 0, 9, false, Comment("# comment")},
+		testCase{ParseComment, "not comment\n#comment\n", 0, 0, false, nil},
+		testCase{ParseComment, "# comment\nnot comment", 9, 9, false, nil},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseWs(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseWs, "# comment   ", 0, 0, false, nil},
+		testCase{ParseWs, "1234567890  ", 0, 0, false, nil},
+		testCase{ParseWs, "-1234567890 ", 0, 0, false, nil},
+		testCase{ParseWs, "+1234567890 ", 0, 0, false, nil},
+		testCase{ParseWs, "`string`    ", 0, 0, false, nil},
+		testCase{ParseWs, `"string"    `, 0, 0, false, nil},
+		testCase{ParseWs, "word        ", 0, 0, false, nil},
+		testCase{ParseWs, " \tword     ", 0, 2, false, String(" \t")},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseGetter(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseGetter, "not a getter ", 0, 0, false, nil},
+		testCase{ParseGetter, "$# comment   ", 0, 0, true, nil},
+		testCase{ParseGetter, "$ bad space  ", 0, 0, true, nil},
+		testCase{ParseGetter, "$1234567890  ", 0, 11, false, Getter{Int(+1234567890)}},
+		testCase{ParseGetter, "$-1234567890 ", 0, 12, false, Getter{Int(-1234567890)}},
+		testCase{ParseGetter, "$+1234567890 ", 0, 12, false, Getter{Int(+1234567890)}},
+		testCase{ParseGetter, "$`string`    ", 0, 9, false, Getter{String("string")}},
+		testCase{ParseGetter, `$"string"    `, 0, 9, false, Getter{String("string")}},
+		testCase{ParseGetter, "$word        ", 0, 5, false, Getter{Word("word")}},
+		testCase{ParseGetter, "$µ_rd        ", 0, 5, false, Getter{Word("µ_rd")}},
+		testCase{ParseGetter, "$wo09        ", 0, 5, false, Getter{Word("wo09")}},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseInteger(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseInteger, "01234567890 ", 0, 11, false, Int(1234567890)},
+		testCase{ParseInteger, "-1234567890 ", 0, 11, false, Int(-1234567890)},
+		testCase{ParseInteger, "+1234567890 ", 0, 11, false, Int(1234567890)},
+		testCase{ParseInteger, "+1234567890", 0, 11, true, nil},
+		testCase{ParseInteger, "not an integer", 0, 0, false, nil},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseString(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseString, `"string"`, 0, 8, false, String("string")},
+		testCase{ParseString, `"string\"\n"`, 0, 12, false, String("string\"\n")},
+		testCase{ParseString, `"bad        `, 0, 12, true, nil},
+		testCase{ParseString, `not a string`, 0, 0, false, nil},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseRawString(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseRawString, "`string` ", 0, 8, false, String("string")},
+		testCase{ParseRawString, "`string\"quote` ", 0, 14, false,
+			String("string\"quote")},
+		testCase{ParseRawString, "`bad        ", 0, 11, true, nil},
+		testCase{ParseRawString, "not a string` ", 0, 0, false, nil},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseWord(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseWord, "word ", 0, 4, false, Word("word")},
+		testCase{ParseWord, "word\n", 0, 4, false, Word("word")},
+		testCase{ParseWord, "word; ", 0, 4, false, Word("word")},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseLiteral(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseLiteral, "# comment   ", 0, 0, false, nil},
+		testCase{ParseLiteral, "   \t       ", 0, 0, false, nil},
+		testCase{ParseLiteral, "1234567890  ", 0, 10, false, Int(1234567890)},
+		testCase{ParseLiteral, "-1234567890 ", 0, 11, false, Int(-1234567890)},
+		testCase{ParseLiteral, "+1234567890 ", 0, 11, false, Int(1234567890)},
+		testCase{ParseLiteral, "`string`    ", 0, 8, false, String("string")},
+		testCase{ParseLiteral, `"string"    `, 0, 8, false, String("string")},
+		testCase{ParseLiteral, "word        ", 0, 4, false, Word("word")},
+		testCase{ParseLiteral, "µ_rd        ", 0, 4, false, Word("µ_rd")},
+		testCase{ParseLiteral, "wo09        ", 0, 4, false, Word("wo09")},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseParameters(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseParameters, " world\n", 0, 6, false, List{Word("world")}},
+		testCase{ParseParameters, ` world 7 "foo"` + "`\n", 0, 14, false,
+			List{Word("world"), Int(7), String("foo")},
+		},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseOrder(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseOrder, "hello ", 0, 5, false, Word("hello")},
+		testCase{ParseOrder, "1 ", 0, 1, false, Int(1)},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseCommand(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseCommand, "hello world\n", 0, 11, false,
+			Command{Word("hello"), List{Word("world")}},
+		},
+	}
+
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseEvaluation(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseEvaluation, "[hello world]", 0, 13, false,
+			Evaluation{Command{Word("hello"), List{Word("world")}}},
+		},
+		testCase{ParseEvaluation, "[hello 123 ]", 0, 12, false,
+			Evaluation{Command{Word("hello"), List{Int(123)}}},
+		},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseAStatement(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseStatement, " hello world\n", 0, 12, false,
+			Command{Word("hello"), List{Word("world")}},
+		},
+		testCase{ParseStatement, " hello;world;", 0, 6, false,
+			Command{Word("hello"), List{}},
+		},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseStatements(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseStatements, "\n\n\n", 0, 3, false,
+			List{},
+		},
+		testCase{ParseStatements, "\n\n \n", 0, 4, false,
+			List{},
+		},
+		testCase{ParseStatements, "hello world\n", 0, 12, false,
+			List{Command{Word("hello"), List{Word("world")}}},
+		},
+		testCase{ParseStatements, "hello;world;", 0, 12, false,
+			List{Command{Word("hello"), List{}}, Command{Word("world"), List{}}},
+		},
+		testCase{ParseStatements, "hello \n world \n\n", 0, 16, false,
+			List{Command{Word("hello"), List{}}, Command{Word("world"), List{}}},
+		},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseBlock(t *testing.T) {
+	tcs := []testCase{
+		testCase{ParseBlock, "{hello world}", 0, 13, false,
+			Block{List{Command{Word("hello"), List{Word("world")}}}},
+		},
+		testCase{ParseBlock, "{hello;world}", 0, 13, false,
+			Block{List{Command{Word("hello"), List{}}, Command{Word("world"), List{}}}},
+		},
+		testCase{ParseBlock, "{hello\nworld}", 0, 13, false,
+			Block{List{Command{Word("hello"), List{}}, Command{Word("world"), List{}}}},
+		},
+		testCase{ParseBlock, "{ hello\n world\n}", 0, 16, false,
+			Block{List{Command{Word("hello"), List{}}, Command{Word("world"), List{}}}},
+		},
+		testCase{ParseBlock, "{ #Comment\n hello\n world\n}", 0, 26, false,
+			Block{List{Comment("#Comment"), Command{Word("hello"), List{}}, Command{Word("world"), List{}}}}},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParse(t *testing.T) {
+	script1 := `
+	# Comment
+	print "Hello world!"
+`
+	res1 := Block{List{Comment("# Comment"), Command{Word("print"), List{String("Hello world!")}}}}
+
+	script2 := `
+	# Comment
+	print "Hello world!"
+`
+	res2 := Block{List{Comment("# Comment"), Command{Word("print"), List{String("Hello world!")}}}}
+
+	tcs := []testCase{
+		testCase{ParseScript, script1, 0, len(script1), false, res1},
+		testCase{ParseScript, script2, 0, len(script1), false, res2},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t)
+	}
+}
+
+func TestParseAndRun(t *testing.T) {
+	script1 := `
+		# Comment
+		print "Hello world!"
+`
+	parsed, err := Parse(script1)
+	if err != nil {
+		t.Errorf("Parse error: %v", err)
+		return
+	}
+	if parsed == nil {
+		t.Errorf("No parse results error: %v", parsed)
+		return
+	}
+	env := &Environment{}
+	env.Push()
+	env.Define("print", Proc(func(e *Environment, args ...Value) (Value, Effect) {
+		var msg string
+		Args(args, &msg)
+		print(msg, "\n")
+		return nil, nil
+	}), 0)
+	parsed.Eval(env)
+}
+
+type iTestCase struct {
+	in          string
+	ex          string
+	expectError bool
+	args        []Value
+}
+
+func (tc *iTestCase) Run(t *testing.T, e Environment) {
+	t.Logf("Test case input: %s", tc.in)
+	res := e.Interpolate(tc.in, tc.args...)
+	t.Logf("Test case result: %v", res)
+	if res != tc.ex {
+		t.Errorf("error: value not expected: %s <-> %s", res, tc.ex)
+	}
+}
+
+func TestInterpolate(t *testing.T) {
+	e := Environment{}
+	e.Push()
+	e.Define("foo", String("{world}"), 0)
+
+	tcs := []iTestCase{
+		iTestCase{`hello {world}`, `hello {world}`, false, []Value{}},
+		iTestCase{`hello ${foo}`, `hello {world}`, false, []Value{}},
+		iTestCase{`hello $${foo}`, `hello ${foo}`, false, []Value{}},
+		iTestCase{`hello ${1}`, `hello {world}`, false, []Value{String("{world}")}},
+	}
+	for i, tc := range tcs {
+		t.Logf("Case: %d", i+1)
+		tc.Run(t, e)
+	}
+}

+ 140 - 0
support.go

@@ -0,0 +1,140 @@
+package attl