kati: initial commit (from hamaji)
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..23de2e6
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,11 @@
+GOSRC = $(wildcard *.go)
+
+all: kati
+
+kati: $(GOSRC)
+	env $(shell go env) go build -o $@ .
+
+test:
+	ruby runtest.rb
+
+.PHONY: test
diff --git a/ast.go b/ast.go
new file mode 100644
index 0000000..07b01da
--- /dev/null
+++ b/ast.go
@@ -0,0 +1,47 @@
+package main
+
+const (
+	AST_ASSIGN = iota
+	AST_RULE
+)
+
+type AST interface {
+	typ() int
+	show()
+}
+
+type ASTBase struct {
+	lineno int
+}
+
+type AssignAST struct {
+	ASTBase
+	lhs string
+	rhs string
+}
+
+func (ast *AssignAST) typ() int {
+	return AST_ASSIGN
+}
+
+func (ast *AssignAST) show() {
+	Log("%s=%s", ast.lhs, ast.rhs)
+}
+
+type RuleAST struct {
+	ASTBase
+	lhs  string
+	rhs  string
+	cmds []string
+}
+
+func (ast *RuleAST) typ() int {
+	return AST_RULE
+}
+
+func (ast *RuleAST) show() {
+	Log("%s: %s", ast.lhs, ast.rhs)
+	for _, cmd := range ast.cmds {
+		Log("\t%s", cmd)
+	}
+}
diff --git a/eval.go b/eval.go
new file mode 100644
index 0000000..a676171
--- /dev/null
+++ b/eval.go
@@ -0,0 +1,167 @@
+package main
+
+import (
+	"bytes"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+	"strings"
+)
+
+type Rule struct {
+	output string
+	inputs []string
+	cmds   []string
+}
+
+type EvalResult struct {
+	vars  map[string]string
+	rules []*Rule
+	refs  map[string]bool
+}
+
+type Evaluator struct {
+	out_vars  map[string]string
+	out_rules []*Rule
+	refs      map[string]bool
+	vars      map[string]string
+	cur_rule  *Rule
+}
+
+func newEvaluator() *Evaluator {
+	return &Evaluator{
+		out_vars: make(map[string]string),
+		refs:     make(map[string]bool),
+		vars:     make(map[string]string),
+	}
+}
+
+func (ev *Evaluator) evalFunction(ex string) (string, bool) {
+	if strings.HasPrefix(ex, "wildcard ") {
+		arg := ex[len("wildcard "):]
+
+		files, err := filepath.Glob(arg)
+		if err != nil {
+			panic(err)
+		}
+		return strings.Join(files, " "), true
+	} else if strings.HasPrefix(ex, "shell ") {
+		arg := ex[len("shell "):]
+
+		args := []string{"/bin/sh", "-c", arg}
+		cmd := exec.Cmd{
+			Path: args[0],
+			Args: args,
+		}
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			panic(err)
+		}
+		re, err := regexp.Compile("\\s")
+		if err != nil {
+			panic(err)
+		}
+		return string(re.ReplaceAllString(string(out), " ")), true
+	}
+	return "", false
+}
+
+func (ev *Evaluator) evalExprSlice(ex string, term byte) (string, int) {
+	var buf bytes.Buffer
+	i := 0
+	for i < len(ex) && ex[i] != term {
+		ch := ex[i]
+		i++
+		switch ch {
+		case '$':
+			if i >= len(ex) || ex[i] == term {
+				continue
+			}
+
+			var varname string
+			switch ex[i] {
+			case '@':
+				buf.WriteString(ev.cur_rule.output)
+				i++
+				continue
+			case '(':
+				v, j := ev.evalExprSlice(ex[i+1:], ')')
+				i += j + 2
+				if r, done := ev.evalFunction(v); done {
+					buf.WriteString(r)
+					continue
+				}
+
+				varname = v
+			default:
+				varname = string(ex[i])
+				i++
+			}
+
+			value, present := ev.vars[varname]
+			if !present {
+				ev.refs[varname] = true
+				value = ev.out_vars[varname]
+			}
+			buf.WriteString(value)
+
+		default:
+			buf.WriteByte(ch)
+		}
+	}
+	return buf.String(), i
+}
+
+func (ev *Evaluator) evalExpr(ex string) string {
+	r, i := ev.evalExprSlice(ex, 0)
+	if len(ex) != i {
+		panic("Had a null character?")
+	}
+	return r
+}
+
+func (ev *Evaluator) evalAssign(ast *AssignAST) {
+	lhs := ev.evalExpr(ast.lhs)
+	rhs := ev.evalExpr(ast.rhs)
+	Log("ASSIGN: %s=%s", lhs, rhs)
+	ev.out_vars[lhs] = rhs
+}
+
+func (ev *Evaluator) evalRule(ast *RuleAST) {
+	ev.cur_rule = &Rule{}
+	lhs := ev.evalExpr(ast.lhs)
+	ev.cur_rule.output = lhs
+	rhs := ev.evalExpr(ast.rhs)
+	if rhs != "" {
+		ev.cur_rule.inputs = strings.Split(rhs, " ")
+	}
+	var cmds []string
+	for _, cmd := range ast.cmds {
+		cmds = append(cmds, ev.evalExpr(cmd))
+	}
+	Log("RULE: %s=%s", lhs, rhs)
+	ev.cur_rule.cmds = cmds
+	ev.out_rules = append(ev.out_rules, ev.cur_rule)
+	ev.cur_rule = nil
+}
+
+func (ev *Evaluator) eval(ast AST) {
+	switch ast.typ() {
+	case AST_ASSIGN:
+		ev.evalAssign(ast.(*AssignAST))
+	case AST_RULE:
+		ev.evalRule(ast.(*RuleAST))
+	}
+}
+
+func Eval(mk Makefile) *EvalResult {
+	ev := newEvaluator()
+	for _, stmt := range mk.stmts {
+		ev.eval(stmt)
+	}
+	return &EvalResult{
+		vars:  ev.out_vars,
+		rules: ev.out_rules,
+		refs:  ev.refs,
+	}
+}
diff --git a/exec.go b/exec.go
new file mode 100644
index 0000000..11b3653
--- /dev/null
+++ b/exec.go
@@ -0,0 +1,104 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+	"time"
+)
+
+type Executor struct {
+	rules map[string]*Rule
+}
+
+func newExecutor() *Executor {
+	return &Executor{
+		rules: make(map[string]*Rule),
+	}
+}
+
+func getTimestamp(filename string) int64 {
+	st, err := os.Stat(filename)
+	if err != nil {
+		return -2
+	}
+	return st.ModTime().Unix()
+}
+
+func (ex *Executor) runCommands(cmds []string) {
+	for _, cmd := range cmds {
+		fmt.Printf("%s\n", cmd)
+
+		args := []string{"/bin/sh", "-c", cmd}
+		cmd := exec.Cmd{
+			Path: args[0],
+			Args: args,
+		}
+		out, err := cmd.CombinedOutput()
+		if err != nil {
+			panic(err)
+		}
+		success := false
+		if cmd.ProcessState != nil {
+			success = cmd.ProcessState.Success()
+		}
+
+		fmt.Printf("%s", out)
+		if !success {
+			panic("Command failed")
+		}
+	}
+}
+
+func (ex *Executor) build(output string) int64 {
+	Log("Building: %s", output)
+	output_ts := getTimestamp(output)
+
+	rule, present := ex.rules[output]
+	if !present {
+		if output_ts >= 0 {
+			return output_ts
+		}
+		Error("No rule to make target '%s'", output)
+	}
+
+	latest := int64(-1)
+	for _, input := range rule.inputs {
+		ts := ex.build(input)
+		if latest < ts {
+			latest = ts
+		}
+	}
+
+	if output_ts >= latest {
+		return output_ts
+	}
+
+	ex.runCommands(rule.cmds)
+
+	output_ts = getTimestamp(output)
+	if output_ts < 0 {
+		output_ts = time.Now().Unix()
+	}
+	return output_ts
+}
+
+func (ex *Executor) exec(er *EvalResult) {
+	if len(er.rules) == 0 {
+		panic("No targets.")
+	}
+
+	for _, rule := range er.rules {
+		if _, present := ex.rules[rule.output]; present {
+			Warn("overiding recipie for target '%s'", rule.output)
+		}
+		ex.rules[rule.output] = rule
+	}
+
+	ex.build(er.rules[0].output)
+}
+
+func Exec(er *EvalResult) {
+	ex := newExecutor()
+	ex.exec(er)
+}
diff --git a/log.go b/log.go
new file mode 100644
index 0000000..be5b681
--- /dev/null
+++ b/log.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"bytes"
+	"fmt"
+)
+
+func Log(f string, a ...interface{}) {
+	var buf bytes.Buffer
+	buf.WriteString("*kati*: ")
+	buf.WriteString(f)
+	buf.WriteByte('\n')
+	fmt.Printf(buf.String(), a...)
+}
+
+func Warn(f string, a ...interface{}) {
+	var buf bytes.Buffer
+	buf.WriteString("warning: ")
+	buf.WriteString(f)
+	buf.WriteByte('\n')
+	fmt.Printf(buf.String(), a...)
+}
+
+func Error(f string, a ...interface{}) {
+	var buf bytes.Buffer
+	buf.WriteString("error: ")
+	buf.WriteString(f)
+	buf.WriteByte('\n')
+	fmt.Printf(buf.String(), a...)
+	panic("")
+}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9f82548
--- /dev/null
+++ b/main.go
@@ -0,0 +1,15 @@
+package main
+
+func main() {
+	mk, err := ParseDefaultMakefile()
+	if err != nil {
+		panic(err)
+	}
+
+	for _, stmt := range mk.stmts {
+		stmt.show()
+	}
+
+	er := Eval(mk)
+	Exec(er)
+}
diff --git a/parser.go b/parser.go
new file mode 100644
index 0000000..12dffe3
--- /dev/null
+++ b/parser.go
@@ -0,0 +1,210 @@
+package main
+
+import (
+	"bufio"
+	"errors"
+	"io"
+	"os"
+	"strings"
+)
+
+type Makefile struct {
+	stmts []AST
+}
+
+type parser struct {
+	rd     *bufio.Reader
+	mk     Makefile
+	lineno int
+	done   bool
+}
+
+func exists(filename string) bool {
+	f, err := os.Open(filename)
+	if err != nil {
+		return false
+	}
+	f.Close()
+	return true
+}
+
+func isdigit(ch byte) bool {
+	return ch >= '0' && ch <= '9'
+}
+
+func isident(ch byte) bool {
+	return (ch >= 'A' && ch <= 'Z' || ch >= 'a' && ch <= 'z' || ch == '_' || ch == '.')
+}
+
+func newParser(rd io.Reader) *parser {
+	return &parser{
+		rd: bufio.NewReader(rd),
+	}
+}
+
+func (p *parser) readByte() (byte, error) {
+	ch, err := p.rd.ReadByte()
+	if err != nil {
+		p.done = true
+	}
+	return ch, err
+}
+
+func (p *parser) unreadByte() {
+	p.rd.UnreadByte()
+}
+
+func (p *parser) skipWhiteSpaces() error {
+	for {
+		ch, err := p.readByte()
+		if err != nil {
+			return err
+		}
+		switch ch {
+		case '\n':
+			p.lineno++
+			fallthrough
+		case '\r', ' ':
+			continue
+		default:
+			p.unreadByte()
+			return nil
+		}
+	}
+}
+
+func (p *parser) getNextToken() (string, error) {
+	if err := p.skipWhiteSpaces(); err != nil {
+		return "", err
+	}
+	ch, err := p.readByte()
+	if err != nil {
+		return "", errors.New("TODO")
+	}
+	switch ch {
+	case '$', '=':
+		return string(ch), nil
+	case ':':
+		var s []byte
+		s = append(s, ch)
+		ch, err := p.readByte()
+		if ch == ':' {
+			ch, err = p.readByte()
+		}
+		if err != nil {
+			return string(s), err
+		}
+		if ch == '=' {
+			s = append(s, ch)
+		} else {
+			p.unreadByte()
+		}
+		return string(s), nil
+	default:
+		if isident(ch) {
+			var s []byte
+			s = append(s, ch)
+			for {
+				ch, err := p.readByte()
+				if err != nil {
+					return string(s), err
+				}
+				if isident(ch) || isdigit(ch) {
+					s = append(s, ch)
+				} else {
+					p.unreadByte()
+					return string(s), nil
+				}
+			}
+		}
+	}
+
+	return "", errors.New("foobar")
+}
+
+func (p *parser) readUntilEol() string {
+	var r []byte
+	for {
+		ch, err := p.readByte()
+		if err != nil || ch == '\n' {
+			return string(r)
+		}
+		r = append(r, ch)
+	}
+}
+
+func (p *parser) parseAssign(lhs string) AST {
+	ast := &AssignAST{lhs: lhs}
+	ast.lineno = p.lineno
+	ast.rhs = strings.TrimSpace(p.readUntilEol())
+	return ast
+}
+
+func (p *parser) parseRule(lhs string) AST {
+	ast := &RuleAST{lhs: lhs}
+	ast.lineno = p.lineno
+	ast.rhs = strings.TrimSpace(p.readUntilEol())
+	for {
+		ch, err := p.readByte()
+		if err != nil {
+			return ast
+		}
+		switch ch {
+		case '\n':
+			continue
+		case '\t':
+			ast.cmds = append(ast.cmds, strings.TrimSpace(p.readUntilEol()))
+			continue
+		default:
+			p.unreadByte()
+			return ast
+		}
+	}
+}
+
+func (p *parser) parse() (Makefile, error) {
+	for {
+		tok, err := p.getNextToken()
+		Log("tok=%s", tok)
+		if err == io.EOF {
+			return p.mk, nil
+		} else if err != nil {
+			return p.mk, err
+		}
+		switch tok {
+		default:
+			ntok, err := p.getNextToken()
+			if err != nil {
+				return p.mk, err
+			}
+			switch ntok {
+			case "=":
+				ast := p.parseAssign(tok)
+				p.mk.stmts = append(p.mk.stmts, ast)
+			case ":":
+				ast := p.parseRule(tok)
+				p.mk.stmts = append(p.mk.stmts, ast)
+			}
+		}
+	}
+	return p.mk, nil
+}
+
+func ParseMakefile(filename string) (Makefile, error) {
+	f, err := os.Open(filename)
+	if err != nil {
+		return Makefile{}, err
+	}
+	parser := newParser(f)
+	return parser.parse()
+}
+
+func ParseDefaultMakefile() (Makefile, error) {
+	candidates := []string{"GNUmakefile", "makefile", "Makefile"}
+	for _, filename := range candidates {
+		if exists(filename) {
+			return ParseMakefile(filename)
+		}
+	}
+	return Makefile{}, errors.New("No targets specified and no makefile found.")
+}
diff --git a/runtest.rb b/runtest.rb
new file mode 100755
index 0000000..72a9f84
--- /dev/null
+++ b/runtest.rb
@@ -0,0 +1,58 @@
+#!/usr/bin/env ruby
+
+require 'fileutils'
+
+def get_output_filenames
+  files = Dir.glob('*')
+  files.delete('Makefile')
+  files
+end
+
+def cleanup
+  get_output_filenames.each do |fname|
+    FileUtils.rm fname
+  end
+end
+
+Dir.glob('test/*.mk').sort.each do |mk|
+  c = File.read(mk)
+
+  name = mk[/([^\/]+)\.mk$/, 1]
+  dir = "out/#{name}"
+  FileUtils.rm_rf(dir)
+  FileUtils.mkdir_p(dir)
+
+  Dir.chdir(dir) do
+    File.open("Makefile", 'w') do |ofile|
+      ofile.print(c)
+    end
+
+    expected = ''
+    output = ''
+
+    c.scan(/^test\d*/).sort.each do |tc|
+      cleanup
+      expected += "=== #{tc} ===\n" + `make 2>&1`
+      expected_files = get_output_filenames
+      cleanup
+      output += "=== #{tc} ===\n" + `../../kati 2>&1`
+      output_files = get_output_filenames
+
+      expected.gsub!(/^make\[.*\n/, '')
+      output.gsub!(/^\*kati\*.*\n/, '')
+
+      expected += "\n=== FILES ===\n#{expected_files * "\n"}\n"
+      output += "\n=== FILES ===\n#{output_files * "\n"}\n"
+    end
+
+    File.open('out.make', 'w'){|ofile|ofile.print(expected)}
+    File.open('out.kati', 'w'){|ofile|ofile.print(output)}
+
+    if expected != output
+      puts "#{name}: FAIL"
+      puts `diff -u out.make out.kati`
+    else
+      puts "#{name}: OK"
+    end
+  end
+end
diff --git a/test/basic_dep.mk b/test/basic_dep.mk
new file mode 100644
index 0000000..de51a7f
--- /dev/null
+++ b/test/basic_dep.mk
@@ -0,0 +1,6 @@
+test1: foo
+
+test2: foo
+
+foo:
+	echo foo > $@
diff --git a/test/basic_rule.mk b/test/basic_rule.mk
new file mode 100644
index 0000000..eb26971
--- /dev/null
+++ b/test/basic_rule.mk
@@ -0,0 +1,4 @@
+test: foo
+
+foo:
+	echo foo
diff --git a/test/basic_var.mk b/test/basic_var.mk
new file mode 100644
index 0000000..f684c09
--- /dev/null
+++ b/test/basic_var.mk
@@ -0,0 +1,4 @@
+VAR=var
+
+test:
+	echo $(VAR)
diff --git a/test/var_target.mk b/test/var_target.mk
new file mode 100644
index 0000000..bd9e4e0
--- /dev/null
+++ b/test/var_target.mk
@@ -0,0 +1,5 @@
+FOO=BAR
+$(FOO)=BAZ
+
+test:
+	echo $(BAR)
diff --git a/test/warning.mk b/test/warning.mk
new file mode 100644
index 0000000..ac61f15
--- /dev/null
+++ b/test/warning.mk
@@ -0,0 +1,3 @@
+$(warning foo)
+
+test: