summaryrefslogtreecommitdiff
path: root/conflang.go
blob: 0b4a856a5ebae8581e3d1071493825c0dc65d828 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package main

import (
	"bufio"
	"fmt"
	"io"
)

type toktype int

const (
	tText toktype = iota
	tNextCmd
)

type token struct {
	Type toktype
	Data string
	Line int
}

func scan(r io.Reader, tokens chan<- token, errch chan<- error) {
	emit := func(t toktype, d []byte, line int) {
		if t == tText && len(d) == 0 {
			return
		}
		tokens <- token{t, string(d), line}
	}

	err := func() error {
		br := bufio.NewReader(r)

		escaped := false
		data := []byte{}
		line := 1

		for {
			b, err := br.ReadByte()

			switch err {
			case nil:
			case io.EOF:
				return nil
			default:
				return ErrorAtLine{line, err}
			}

			if b == '\n' {
				line++
			}

			if escaped {
				data = append(data, b)
				escaped = false
				continue
			}

			switch b {
			case '\\':
				escaped = true
			case ' ', '\t':
				emit(tText, data, line)
				data = data[:0]
			case '\n':
				emit(tText, data, line)
				data = data[:0]
				emit(tNextCmd, nil, line)
			default:
				data = append(data, b)
			}
		}

		emit(tText, data, line)
		emit(tNextCmd, nil, line)
		return nil
	}()

	close(tokens)
	errch <- err
}

type command struct {
	Name   string
	Params []string
	Line   int
}

func parse(tokens <-chan token, cmds chan<- command) {
	defer close(cmds)

	startcmd := true
	cmd := command{"", make([]string, 0), 0}

	for tok := range tokens {
		switch tok.Type {
		case tText:
			if startcmd {
				cmd.Name = tok.Data
				cmd.Line = tok.Line
				startcmd = false
			} else {
				cmd.Params = append(cmd.Params, tok.Data)
			}
		case tNextCmd:
			if !startcmd {
				cmds <- cmd
				cmd.Name = ""
				cmd.Params = make([]string, 0)
				startcmd = true
			}
		}
	}
}

type cmdfunc func(params []string) error

var commands = map[string]cmdfunc{
	"nop": func(_ []string) error { return nil },
}

func RegisterCommand(name string, f cmdfunc) {
	commands[name] = f
}

type ErrorAtLine struct {
	Line int
	Err  error
}

func (err ErrorAtLine) Error() string {
	return fmt.Sprintf("%s (at line %d)", err.Err, err.Line)
}

type CommandNotFound string

func (c CommandNotFound) Error() string {
	return fmt.Sprintf("Command \"%s\" not found", c)
}

func RunCommands(r io.Reader) error {
	errch := make(chan error)
	tokens := make(chan token)
	cmds := make(chan command)

	go scan(r, tokens, errch)
	go parse(tokens, cmds)

	for cmd := range cmds {
		f, ok := commands[cmd.Name]
		if !ok {
			return ErrorAtLine{cmd.Line, CommandNotFound(cmd.Name)}
		}

		if err := f(cmd.Params); err != nil {
			return ErrorAtLine{cmd.Line, err}
		}
	}

	return <-errch
}