Browse Source

Add most ATTL files.

Beoran 2 years 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
 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:
 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. 
 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
+
+type Comparer func(v1, v2 Value) int
+
+func (data List) Sort(compare Comparer) List {
+	if len(data) < 2 {
+		return data
+	}
+	pivot := data[0]
+	smaller := make(List, 0, len(data))
+	equal := make(List, 1, len(data))
+	larger := make(List, 0, len(data))
+	equal[0] = pivot
+	for i := 1; i < len(data); i++ {
+		cmp := compare(data[i], pivot)
+		if cmp > 0 {
+			larger = append(larger, data[i])
+		} else if cmp < 0 {
+			smaller = append(smaller, data[i])
+		} else {
+			equal = append(equal, data[i])
+		}
+	}
+	res := smaller.Sort(compare)
+	res = append(res, equal...)
+	res = append(res, larger.Sort(compare)...)
+	return res
+}
+
+func (data List) SortStrings() List {
+	return data.Sort(func(v1, v2 Value) int {
+		s1 := v1.String()
+		s2 := v2.String()
+		if s1 > s2 {
+			return 1
+		} else if s1 < s2 {
+			return -1
+		}
+		return 0
+	})
+}
+
+// Converts an integer to a string
+func Itoa(i int) string {
+	if i == 0 {
+		return "0"
+	}
+	digits := "0123456789"
+	res := ""
+	neg := false
+	if i < 0 {
+		neg = true
+		i = -i
+	}
+	for d := i % 10; i > 0; { // dumb digit by digit algorithm
+		res = string(digits[d]) + res
+		i = i / 10
+		d = i % 10
+	}
+	if neg {
+		res = "-" + res
+	}
+	return res
+}
+
+func Args(froms []Value, tos ...interface{}) *Error {
+	for i, to := range tos {
+		if i >= len(froms) {
+			return ErrorFromString("Too few arguments: " +
+				Itoa(len(froms)) + " in stead of " + Itoa(len(tos)))
+		}
+		from := froms[i]
+		err := Convert(from, to)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+const (
+	maxRune      = '\U0010FFFF'
+	surrogateMin = 0xD800
+	surrogateMax = 0xDFFF
+	rune1Max     = 1<<7 - 1
+	rune2Max     = 1<<11 - 1
+	rune3Max     = 1<<16 - 1
+)
+
+func RuneLen(r rune) int {
+	switch {
+	case r < 0:
+		return -1
+	case r <= rune1Max:
+		return 1
+	case r <= rune2Max:
+		return 2
+	case surrogateMin <= r && r <= surrogateMax:
+		return -1
+	case r <= rune3Max:
+		return 3
+	case r <= maxRune:
+		return 4
+	}
+	return -1
+}
+
+// WordCompleter is for use with liner
+func WordCompleter(env Environment, line string, pos int) (head string, c []string, tail string) {
+	end := pos
+	begin := pos - 1
+	if begin < 0 {
+		begin = 0
+	}
+	for ; begin > 0 && len(line[begin:]) > 0; begin-- {
+		r := []rune(line[begin:])[0]
+		if RuneLen(r) < 0 {
+			continue
+		}
+		if !(IsLetter(r) || IsNumber(r)) {
+			rl := RuneLen(r) // skip to next rune
+			begin += rl
+			break
+		}
+	}
+	for end = pos; end < len(line); end++ {
+		r := []rune(line[end:])[0]
+		if RuneLen(r) < 0 {
+			continue
+		}
+		if !(IsLetter(r) || IsNumber(r)) {
+			break
+		}
+	}
+	head = line[0:begin]
+	tail = line[end:]
+	word := line[begin:end]
+	clist := env.Complete(String(word))
+	return head, clist.ToStrings(), tail
+}

+ 25 - 0
support_test.go

@@ -0,0 +1,25 @@
+package attl
+
+import "testing"
+import "reflect"
+
+func TestSort(t *testing.T) {
+	arr := StringList("banana", "pear", "apple")
+	expect := StringList("apple", "banana", "pear")
+	sorted := arr.SortStrings()
+	if !reflect.DeepEqual(sorted, expect) {
+		t.Errorf("Not equal: %v<->%v", sorted, expect)
+	}
+	arr = StringList("banana")
+	expect = StringList("banana")
+	sorted = arr.SortStrings()
+	if !reflect.DeepEqual(sorted, expect) {
+		t.Errorf("Not equal: %v<->%v", sorted, expect)
+	}
+	arr = StringList()
+	expect = StringList()
+	sorted = arr.SortStrings()
+	if !reflect.DeepEqual(sorted, expect) {
+		t.Errorf("Not equal: %v<->%v", sorted, expect)
+	}
+}

+ 285 - 0
tutorial.attl

@@ -0,0 +1,285 @@
+#!/usr/bin/env attl
+#
+# Welcome to ATTL! This is an executable tutorial for the ATTL language.
+# You will need to get the ATTL interpreter and install it for your platform
+# to run this script.
+# What is the line on top of the script? It is there to allow the ATTL script
+# to be executed directly on Unix like OS.
+#
+# Attl works in two phases: parse and evaluate.
+#
+# In the parse phase ATTL reads in the script and parses it,
+# which means it breaks it down into a format it can interpret. If an error
+# occurs in this phase, ATTL will report that error and the script will
+# do nothing at all.
+#
+# In the evaluate phase, ATTL then interprets or evaluates the parsed script.
+# If an error occurs the script will still partially be executed,
+# and such errors may be caught and handled.
+#
+# Now let's start exploring the ATTL language itself.
+# In front of this line and those above, you can see there is a # mark.
+# This is to indicate that the line is a comment.
+# ATTL parses comments but they do nothing during evaluation.
+# This is useful to explain what an ATTL script does like in this tutorial.
+#
+# Comments run from the # mark until the end of the line.
+# Comments may also be indented with spaces.
+  # For example: this is also comment
+#
+# To make ATTL actually do something, we need to write a command in the script.
+# Let's start with the traditional "Hello World!" below:
+print "Hello World!\n"
+# The command above consists of an order print, and it's parameter
+# "Hello world\n" which is a literal text, with a new line character in it at
+# the end.
+# This commands orders the ATTL interpreter to print the text "Hello World"
+# followed by a new line to the interpreter's standard output.
+#
+# Commands in ATTL consist of an order followed optionally by parameters
+# Every parameter has to have whitespace in front of it, so ATTL can find it.
+# That's why it's not allowed to have whitespace at the end of the line.
+# Attl will give a parse error since it expects a parameter to follow the space.
+# An Attl command must be terminated by a newline or a semicolon.
+print "Hello " ; print "World!\n"
+# Some arguments take multiple parameters.
+# For example, print can also take multiple parameters.
+print "Hello $1\n" "World!"
+#
+# The $1 in the string above is an example of string interpolation.
+# This is explained later, below in this tutorial. For now, note that
+# print will replace $1, $2, ... with the text representation of it's 1st,
+# 2nd, and so forth parameter.
+#
+# All commands in ATTL have this same form. Unlike many other programming
+# languages, there are no special constructs. The evaluation of an Attl script
+# is fully determined by the available commands.
+# In this tutorial we look at how ATTL works with the builtin commands it
+# provides, but when ATTL is used as an embedded language, it might work
+# differently if the commands are different or not available.
+#
+# The effect of ATTL commands also depend on the Environment in which they
+# are executed. ATTL command can write to the ATTL environment's output,
+# read from the ATTL's environment input, and store data in the environment
+# using variables. Variabes are described later.
+#
+# Let's continue with some examples of commands.
+# For example this is how do do 1 + 2 in ATTL:
+iadd 1 2
+# As you can see attl has no operators, just commands, so we use the command
+# iadd, short for integer add to add 1 and 2 together.
+# You will notice that when running this script, this iadd seems to do nothing
+# This is because we don't print the value anywhere.
+# Let's see how to do this.
+print "1+2=$1\n" [iadd 1 2]
+# The command above prints 1+2=3. But, what are the square brackets [] for?
+# Let's see what happens when we remove them:
+print "1+2=$1\n" iadd 1 2
+# Then the command prints 1+3=iadd
+# The brackets [] are to indicate to ATTL that it should not take what is
+# between them as parameters of the first command in the line, but that it
+# starts a new command of which the value should be taken and used as
+# a parameter. Such a command between [] is called an Evaluation.
+#
+# Evaluations are eagerly evaluated when they are in an argument list.
+# This means they are evaluated before the command receives them as arguments.
+#
+# Evaluations may be nested as much as you like. For instance:
+print "((5+4)*3)-1)/2=$1\n" [idiv [isub [imul [iadd 5 4] 3] 1] 2]
+# .. prints ((5+4)*3)-1)/2=13
+#
+# While this seems a bit cumbersome compared to operators, it keeps ATTL simple.
+# I do not recommend using ATTL for intensive maths.
+#
+# From this we can see that attl commands not only have effects such as writing
+# to the output, they also produce a result. If the ATTL command is successful
+# it returns a Value. If it not successful, it fails, which causes execution to
+# end, unless a rescue command is used (see later).
+# All commands either return a value or fail. Even print returns the amount of
+# bytes written, as follows:
+#
+print "Written $1 bytes\n" [print "Hello World\n"]
+#
+# The basic data which ATTL operates on, and which commands return
+# is called a Value. Unlike languages like TCL, ATTL Values are typed.
+# To see the type of a value use the typeof command
+print "type of 1: $1\n" [typeof 1]
+print "type of \"hi\": $1\n" [typeof "hi"]
+#
+# ATTL supports the following types by default:
+# * String: for literal text, in UTF-8 format.
+write "String µ出来る\n"
+# The above should write String µ出来るif your terminal supports it.
+# Normal strings support the following escape sequences:
+# \a: bell character
+# \b: backspace character
+# \e: escape character
+# \f: form feed character
+# \n: new line character
+# \r: carriage return character
+# \t: tab character
+# \\: a \ character
+# \": a " character
+# For instance, on a terminal which suports ANSI color the following prints
+# foo	bar\ in red:
+print "escapes: \nX\b\e[31mfoo\tbar\\\e[0m\n\r"
+# Strings can run over multiple lines
+# A string can also run over multiple lines if it is a raw string between ``
+# Raw strings accept no escape characters and always terminate on a `.
+# There is no way to put a `in a raw string, but a raw string ans a normal
+# string can be appended  to each other to allow this if the sadd
+# command is available.
+print `A
+"raw"
+string
+\\ no escapes \"
+`
+# * Int: which is for integer numbers.
+print "Int: $1\n" 123
+# * Word: which is for names.
+# Words consist have any non-symbol, non whitespace character, _ and /
+print "Word: $1\n" iAmA_Word/7
+# * Bool: for boolean values. $true and $false are default boolean variables.
+# Words consist have any non-symbol, non whitespace character, _ and /
+print "Bool: $1 $2\n" $true $false
+# * List: for lists of ATTL values.
+# lists are created wth the list command
+print "List: $1\n" [list 1 2 3]
+# * Map: for a lookup map by string to value.
+# maps are created wth the map command. The key of maps must be strings.
+print "Map: $1\n" [map "a" 1  "b" 2]
+# * Block: for blocks of ATTL commands, between {}.
+# More about blocks later.
+print "Block: $1\n" {print "block ";print "world"}
+# * Type: for types.
+# Types can be defined with the command type. they are useful for defining
+# overloads. More on overloads later.
+print "Type: $1\n" [typeof [type "Type"]]
+#
+# When embedding ATTL, the embedding program can define other types.
+#
+# We can store values the ATTL environment as variables.
+# In ATTL, a variable must first be declared and initialized with let
+# before using it.
+#
+print "Hello $1 variable\n" $variable
+# This prints Hello !nil because the variable has not been defined yet.
+# To define a variabe with a value, use let
+let variable "World"
+# To get the value of the variable use get in an evaluation:
+print "Hello Variable: $1\n" [get variable]
+# [get variablename] is a common operation so we can abbreviate it using
+# a dollar sign: $variablename
+print "Hello Variable: $1\n" $variable
+# It's possible to repeat the dollar sign for indirect variable expansion
+let name variable
+print "Hello indirect variable: $1\n" $$name
+
+# String interpolation.
+# print as a command, performs string interpolation. That means it looks for
+# any dolar signs in the text and replaces them with the value of the
+# following variable in the ATTL environment.
+print "Hello Variable: $variable\n"
+# The variable for interpolation can also be indicated by a ${variable}
+# This is useful to if the variable must befollowed by a non-space:
+print "Hello Variable: ${variable}Record\n"
+# To obtain a $ in an interpolated value, just double it.
+print "Hello dollar: $$${1}\n" 35
+# ATTL does not do string interpolation automatically, it is up
+# to the commands that take strings to do this if needed.
+# To interpolate strings ourselves, we can use the expand command
+set variable "expanded"
+let expanded [expand "Hello $variable\n"]
+print $expanded
+# Blocks are lists of commands. Block are not evaluated when passed as
+# a parameter to a command, but passed as such, and can be used to
+# implement flow control using commands, or to define new commands.
+# Blocks can run over multiple lines, the newlines in a block do NOT
+# terminate the top level command in front of the block.
+# The value of executing a block is set by the last command in the block,
+# or the return command if present.
+# Blocks also have anonymous parameters that are evaluated upon execution
+# of the block.
+let block {
+	# Blocks receive their anyonymous parameters in $1..$argc, and
+	# as a list in $argv
+	let p1 $1
+	print "hello $p1 $argc $argv"
+	print " block\n"
+	return 77
+}
+# Blocks stored in variables can be executed as if they were commands,
+# and can take parameters
+block "param1"
+# And blocks evaluate to their last command or return statement.
+print "block value: $1\n" [block]
+# Variables are dynamically and block-scoped.
+# When using set, and get, the innermost variable defined with let
+# will be used to set or get the value.
+# XXX bug here it seems
+let foo 1
+#{
+#	let foo 2
+#	{
+#		let foo 3
+#		print "$foo\n"
+#	}
+#	print "$foo\n"
+#}
+print "$foo\n"
+# compare the above with the below:
+let bar 1;{set bar 2;{set bar 3;print "$bar\n"};print "$bar\n"};print "$bar\n"
+# You can define commands yourself with the builtin "to" command.
+# This allows you to use named parameters in the block.
+# Parameters are passed by value, but using word parameters, you can implement
+# pass by name parameters as well.
+to set_print name value {
+	print "Setting $name from $1 to $value\n" $$name
+	set $name $value
+	return 0
+}
+let quux 123
+set_print quux 321
+print "quux: $quux\n"
+# Flow control can be done with the built in if command.
+if [igt $quux 200] {
+	print "OK\n"
+} else {
+	print "HUH?\n"
+}
+# ...or with the switch comand. The default word is used to mark the default
+# block. Other values are compared by string equality.
+switch $quux default {
+		print "NOK?\n"
+} 123 {
+		print "ALSO NOT OK\n"
+} 321 {
+		print "OK\n"
+}
+
+# The overload command allows you to use the same command for different types
+# by redirecting them to a type-specific command
+overload add iadd Int Int
+overload add sadd String String
+print [add 10 20]
+print [add "10" "20"]
+
+# When an error occurs, it can be caught by setting up a rescue block with
+# the rescue command. The rescue block itself has block scope, that is
+# it applies to the block in which the rescue command is called.
+# To cause an error, use the fail command.
+to rescue_fail {
+	rescue {
+		let e $1
+		print "Error rescued: $e\n"
+	}
+	fail "Synthetic error"
+}
+rescue_fail
+
+# There are many other builtin commands available. See their description
+# by using the help or help all command.
+# help all
+help help
+
+

+ 534 - 0
value.go

@@ -0,0 +1,534 @@
+package attl
+
+type Attl struct {
+	index int
+	input string
+}
+
+type Proc func(*Environment, ...Value) (Value, Effect)
+
+func (pv Proc) String() string {
+	return "proc"
+}
+
+// Evaler is an interface to a Value that can evaluate itself
+// based on and possibly modifying a given environment.
+type Evaler interface {
+	Eval(*Environment, ...Value) (Value, Effect)
+}
+
+// Lazyer is an interface to a Value that does not automatically
+// evaluate itself in List and similar contexts.
+type Lazyer interface {
+	// Lazy is a marker method only.
+	Lazy()
+}
+
+// Value is an interface for the basic unit of data that
+// the ATTL interpreter works with.
+// It is used both for run time data and parsing data.
+type Value interface {
+	// Since ATTL is TCL-like, all values must be
+	// convertable to string
+	String() string
+	// Furthermore any ATTL value must be able to evaluate itself.
+	Evaler
+}
+
+// Typer is an interface that Values can optionally implement to
+// allow them to report their type.
+type Typer interface {
+	Type() Type
+}
+
+type Int int
+
+type Bool bool
+
+type String string
+
+type Word string
+
+type Type string
+
+type Comment string
+
+type Error struct {
+	Message  string
+	Index    int
+	Children List
+}
+
+type List []Value
+
+type Map map[string]Value
+
+func (m Map) Keys() List {
+	res := List{}
+	for k, _ := range m {
+		res = append(res, String(k))
+	}
+	return res
+}
+
+func (m Map) SortedKeys() List {
+	return m.Keys().SortStrings()
+}
+
+type Getter struct {
+	Key Value
+}
+
+type Evaluation struct {
+	Command
+}
+
+type Command struct {
+	Order      Value
+	Parameters List
+}
+
+type Block struct {
+	Statements List
+}
+
+type Defined struct {
+	Name   string
+	Params List
+	Block
+}
+
+type Wrapper struct {
+	Kind    Type
+	Handle  interface{}
+	Methods Map
+}
+
+type Object struct {
+	Wrapper
+	Fields   Map
+	Embedded Map
+}
+
+type Overload Map
+
+func (bv Bool) String() string {
+	if bv {
+		return "true"
+	}
+	return "false"
+}
+
+func (sv String) String() string {
+	return string(sv)
+}
+
+func (cv Comment) String() string {
+	return string(cv)
+}
+
+func (sv Word) String() string {
+	return string(sv)
+}
+
+func (tv Type) String() string {
+	return string(tv)
+}
+
+func (gv Getter) String() string {
+	return gv.Key.String()
+}
+
+func (cv Command) String() string {
+	return cv.Order.String() + " " + cv.Parameters.String()
+}
+
+func (gv Evaluation) String() string {
+	return gv.Command.String()
+}
+
+func (bv Block) String() string {
+	return "{" + bv.Statements.String() + "}"
+}
+
+func (dv Defined) String() string {
+	return "to " + dv.Name + " (" +
+		dv.Params.String() + ") " + dv.Block.String()
+}
+
+func (cv Overload) String() string {
+	return Map(cv).String()
+}
+
+// Lazy marks that Blocks should be lazily evaluated.
+func (Block) Lazy() {}
+
+func (iv Int) String() string {
+	return Itoa(int(iv))
+}
+
+func (lv Map) String() string {
+	aid := "[map "
+	for k, v := range lv {
+		aid += " "
+		aid += k
+		aid += " "
+		aid += v.String()
+	}
+	aid += "]"
+	return aid
+}
+
+func (lv List) String() string {
+	aid := "[list"
+	for _, v := range lv {
+		aid += " "
+		if v == nil {
+			aid += "nil"
+		} else {
+			aid += v.String()
+		}
+	}
+	aid += "]"
+	return aid
+}
+
+func (ev Error) String() string {
+	return string(ev.Message)
+}
+
+func (ev Error) Error() string {
+	return ev.Message
+}
+
+// Implement the effect interface
+func (ev *Error) Flow() Flow {
+	if ev == nil {
+		return NormalFlow
+	}
+	return FailFlow
+}
+
+func (ev *Error) Unwrap() Value {
+	if ev == nil {
+		return nil
+	}
+	return ev
+}
+
+func NewError(message string, index int, children ...Value) *Error {
+	return &Error{message, index, children}
+}
+
+func ErrorFromString(message string) *Error {
+	return NewError(message, -1)
+}
+
+func ErrorFromError(err error, children ...Value) *Error {
+	if err == nil {
+		return nil
+	}
+	return NewError(err.Error(), -1, children...)
+}
+
+// Break is used for break flows
+type Break struct {
+	Value // value returned by break
+}
+
+func (bv Break) Flow() Flow {
+	return BreakFlow
+}
+
+func (bv Break) Unwrap() Value {
+	return bv.Value
+}
+
+// Return is used for return flows
+type Return struct {
+	Value // value returned
+}
+
+func (rv Return) Flow() Flow {
+	return ReturnFlow
+}
+
+func (bv Return) Unwrap() Value {
+	return bv.Value
+}
+
+// Rescue is used to evaluate rescue commands
+type Rescue struct {
+	Block // A rescue is a special block
+}
+
+// Rescue essentially protects the block from
+// rescue recursion by pushing the stack once.
+func (r Rescue) Eval(env *Environment, args ...Value) (Value, Effect) {
+	// ignore the stack depth
+	// protection here to be sure the
+	// rescue block is executed
+	_ = env.Push()
+	env.Rescuing = true
+	env.Printi("Rescuing.\n")
+	defer env.Pop()
+	defer func() {
+		env.Rescuing = false
+	}()
+	return r.Block.Eval(env, args...)
+}
+
+func (iv Wrapper) String() string {
+	aid := "[interface"
+	aid += " " + iv.Kind.String()
+	aid += " " + iv.Methods.String()
+	aid += "]"
+	return aid
+}
+
+func (sv Object) String() string {
+	aid := "[struct"
+	aid += " " + sv.Kind.String()
+	aid += " " + sv.Methods.String()
+	aid += " " + sv.Fields.String()
+	aid += " " + sv.Embedded.String()
+	aid += "]"
+	return aid
+}
+
+func NewAttl(input string) Attl {
+	return Attl{0, input}
+}
+
+func (wv Word) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return wv, nil
+}
+
+func (sv String) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return sv, nil
+}
+
+func (tv Type) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return tv, nil
+}
+
+func (iv Int) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return iv, nil
+}
+
+func (bv Bool) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return bv, nil
+}
+
+func (ev Error) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return ev, nil
+}
+
+// Eval of a List expands arguments, except Lazyer elements.
+func (lv List) Eval(env *Environment, args ...Value) (Value, Effect) {
+	res := List{}
+	for _, s := range lv {
+		_, isLazy := s.(Lazyer)
+		if isLazy {
+			res = append(res, s)
+		} else {
+			val, err := s.Eval(env, args...)
+			if err != nil {
+				return val, err
+			}
+			res = append(res, val)
+		}
+	}
+	return res, nil
+}
+
+func (mv Map) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return mv, nil
+}
+
+func (cv Comment) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return nil, nil
+}
+
+func (bv Block) Eval(env *Environment, args ...Value) (Value, Effect) {
+	var res Value
+	var eff Effect
+	// set parameters to $1 ... $(len(args))
+	for i, a := range args {
+		name := Itoa(i + 1)
+		env.Define(name, a, 0)
+	}
+	// Set $argc to amount of arguments
+	// and $argv to arguments as well
+	env.Define("argc", Int(len(args)), 0)
+	env.Define("argv", List(args), 0)
+	for _, s := range bv.Statements {
+		// Call the statement.
+		res, eff = s.Eval(env, args...)
+		// if the flow is not normal anymore,
+		// end the block execution at this point
+		if eff != nil && eff.Flow() > NormalFlow {
+			// If it was a break, unwrap and done,
+			if eff.Flow() <= BreakFlow {
+				return eff.Unwrap(), nil
+			} else if eff.Flow() == FailFlow {
+				// If it is a fail try to rescue it
+				return env.Rescue(res, eff)
+			}
+			return res, eff
+		}
+		env.Define("RESULT", res, 0)
+	}
+	return res, eff
+}
+
+func (pv Proc) Eval(env *Environment, args ...Value) (Value, Effect) {
+	return pv(env, args...)
+}
+
+func (cv Command) Eval(env *Environment, args ...Value) (Value, Effect) {
+	val, eff := cv.Order.Eval(env)
+	if eff != nil || val == nil {
+		return val, eff
+	}
+	name := val.String()
+	fun := env.Lookup(name)
+	if fun == nil {
+		return nil, ErrorFromString("Cannot evaluate nil order: " + name)
+	}
+	eva, ok := fun.(Evaler)
+	if !ok {
+		return nil, ErrorFromString("Cannot evaluate: " + name)
+	}
+	err := env.Push()
+	// stack depth protection
+	if err != nil {
+		return env.Rescue(env.Fail(err))
+	}
+	defer env.Pop()
+	fargs := cv.Parameters
+	// Expand Evaluation arguments, but not block elements.
+	eargs, eff := fargs.Eval(env, args...)
+	if eff != nil {
+		return nil, eff
+	}
+	return eva.Eval(env, eargs.(List)...)
+}
+
+func (gv Getter) Eval(env *Environment, args ...Value) (Value, Effect) {
+	val, err := gv.Key.Eval(env)
+	if err != nil || val == nil {
+		return val, err
+	}
+	return env.Lookup(val.String()), nil
+}
+
+func (ev Evaluation) Eval(env *Environment, args ...Value) (Value, Effect) {
+	err := env.Push()
+	// stack depth protection
+	if err != nil {
+		return env.Fail(err)
+	}
+	defer env.Pop()
+	val, eff := ev.Command.Eval(env, args...)
+	return val, eff
+}
+
+func (dv Defined) Eval(env *Environment, args ...Value) (Value, Effect) {
+	err := env.Push()
+	// stack depth protection
+	if err != nil {
+		return env.Rescue(env.Fail(err))
+	}
+	if len(dv.Params) > len(args) {
+		return env.FailString("Not enough arguments")
+	}
+	for i := 0; i < len(dv.Params); i++ {
+		env.Define(dv.Params[i].String(), args[i], 0)
+	}
+	// $0 contains the name of the defined procedure
+	env.Define("0", String(dv.Name), 0)
+	defer env.Pop()
+	val, eff := dv.Block.Eval(env, args...)
+	if eff == nil || eff.Flow() < ReturnFlow {
+		return val, nil
+	} else if eff.Flow() == ReturnFlow {
+		return eff.Unwrap(), nil
+	} else { // failures pass through
+		return val, eff
+	}
+}
+
+func (iv Wrapper) Eval(env *Environment, args ...Value) (Value, Effect) {
+	// Object like values such as interfaces or structs
+	// have methods that are called with the method
+	// name as the first word argument, which is used
+	// to dispatch the function.
+	// The dispathed function receives the object
+	// as it's first argument
+	var name Word
+	err := Args(args, &name)
+	if err != nil {
+		return env.Fail(err)
+	}
+	method, ok := iv.Methods[name.String()]
+	if !ok {
+		return env.FailString("No such method ${1}", name)
+	}
+	args[0] = iv
+	return method.Eval(env, args...)
+}
+
+func (sv Object) Eval(env *Environment, args ...Value) (Value, Effect) {
+	// See interface for this dispatch
+	var name Word
+	err := Args(args, &name)
+	if err != nil {
+		return env.Fail(err)
+	}
+	method, ok := sv.Methods[name.String()]
+	if !ok {
+		return env.FailString("No such method ${1}", name)
+	}
+	args[0] = sv
+	return method.Eval(env, args...)
+}
+
+func TypeOf(val Value) Type {
+	if typer, ok := val.(Typer); ok {
+		return typer.Type()
+	} else {
+		return Type("Unknown")
+	}
+}
+
+func (cv Overload) Eval(env *Environment, args ...Value) (Value, Effect) {
+	signature := ""
+	for _, arg := range args {
+		signature += "_" + TypeOf(arg).String()
+	}
+	target, ok := cv[signature]
+	if ok {
+		return target.Eval(env, args...)
+	}
+	return env.FailString("No overload defined for signature: " + signature)
+}
+
+// Implement Typer interface for commonly used Values
+func (String) Type() Type     { return Type("String") }
+func (Bool) Type() Type       { return Type("Bool") }
+func (Int) Type() Type        { return Type("Int") }
+func (Error) Type() Type      { return Type("Error") }
+func (List) Type() Type       { return Type("List") }
+func (Map) Type() Type        { return Type("Map") }
+func (Proc) Type() Type       { return Type("Proc") }
+func (Word) Type() Type       { return Type("Word") }
+func (Defined) Type() Type    { return Type("Defined") }
+func (Block) Type() Type      { return Type("Block") }
+func (Command) Type() Type    { return Type("Command") }
+func (Getter) Type() Type     { return Type("Getter") }
+func (Evaluation) Type() Type { return Type("Evaluation") }
+func (t Type) Type() Type     { return Type("Type") }
+func (s Object) Type() Type   { return s.Kind }
+func (i Wrapper) Type() Type  { return i.Kind }
+func (t Type) Overload() Type { return Type("Overload") }