Skip to content

Commit

Permalink
The beginning of something
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchellh committed Jul 31, 2014
1 parent 83c3298 commit b6a1162
Show file tree
Hide file tree
Showing 9 changed files with 403 additions and 0 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
y.go
y.output
9 changes: 9 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
default: test

test: y.go
go test

y.go: parse.y
go tool yacc -p "hcl" parse.y

.PHONY: default test
4 changes: 4 additions & 0 deletions hcl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package hcl

// This is the directory where our test fixtures are.
const fixtureDir = "./test-fixtures"
246 changes: 246 additions & 0 deletions lex.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package hcl

import (
"bytes"
"fmt"
"log"
"strconv"
"unicode"
"unicode/utf8"
)

// The parser expects the lexer to return 0 on EOF.
const lexEOF = 0

// The parser uses the type <prefix>Lex as a lexer. It must provide
// the methods Lex(*<prefix>SymType) int and Error(string).
type hclLex struct {
Input string

pos int
width int
col, line int
err error
}

// The parser calls this method to get each new token.
func (x *hclLex) Lex(yylval *hclSymType) int {
for {
c := x.next()
if c == lexEOF {
return lexEOF
}

// Ignore all whitespace
if unicode.IsSpace(c) {
continue
}

// If it is a number, lex the number
if c >= '0' && c <= '9' {
x.backup()
return x.lexNumber(yylval)
}

switch c {
case '=':
return EQUAL
case '{':
return LEFTBRACE
case '}':
return RIGHTBRACE
case ';':
return SEMICOLON
case '#':
fallthrough
case '/':
// Starting comment
if !x.consumeComment(c) {
return lexEOF
}
case '"':
return x.lexString(yylval)
default:
x.backup()
return x.lexId(yylval)
}
}
}

func (x *hclLex) consumeComment(c rune) bool {
single := c == '#'
if !single {
c = x.next()
if c != '/' && c != '*' {
x.backup()
x.createErr(fmt.Sprintf("comment expected, got '%c'", c))
return false
}

single = c == '/'
}

nested := 1
for {
c = x.next()
if c == lexEOF {
x.backup()
return true
}

// Single line comments continue until a '\n'
if single {
if c == '\n' {
return true
}

continue
}

// Multi-line comments continue until a '*/'
switch c {
case '/':
c = x.next()
if c == '*' {
nested++
} else {
x.backup()
}
case '*':
c = x.next()
if c == '/' {
nested--
} else {
x.backup()
}
default:
// Continue
}

// If we're done with the comment, return!
if nested == 0 {
return true
}
}
}

// lexId lexes an identifier
func (x *hclLex) lexId(yylval *hclSymType) int {
var b bytes.Buffer
for {
c := x.next()
if c == lexEOF {
break
}

// If this isn't a character we want in an ID, return out.
// One day we should make this a regexp.
if c != '_' &&
c != '-' &&
c != '.' &&
c != '*' &&
!unicode.IsLetter(c) &&
!unicode.IsNumber(c) {
x.backup()
break
}

if _, err := b.WriteRune(c); err != nil {
log.Printf("ERR: %s", err)
return lexEOF
}
}

yylval.str = b.String()
return IDENTIFIER
}

// lexNumber lexes out a number
func (x *hclLex) lexNumber(yylval *hclSymType) int {
var b bytes.Buffer
for {
c := x.next()
if c == lexEOF {
break
}

// No more numeric characters
if c < '0' || c > '9' {
x.backup()
break
}

if _, err := b.WriteRune(c); err != nil {
x.createErr(fmt.Sprintf("Internal error: %s", err))
return lexEOF
}
}

v, err := strconv.ParseInt(b.String(), 0, 0)
if err != nil {
x.createErr(fmt.Sprintf("Expected number: %s", err))
return lexEOF
}

yylval.num = int(v)
return NUMBER
}

// lexString extracts a string from the input
func (x *hclLex) lexString(yylval *hclSymType) int {
var b bytes.Buffer
for {
c := x.next()
if c == lexEOF {
break
}

// String end
if c == '"' {
break
}

if _, err := b.WriteRune(c); err != nil {
log.Printf("ERR: %s", err)
return lexEOF
}
}

yylval.str = b.String()
return STRING
}

// Return the next rune for the lexer.
func (x *hclLex) next() rune {
if int(x.pos) >= len(x.Input) {
x.width = 0
return lexEOF
}

r, w := utf8.DecodeRuneInString(x.Input[x.pos:])
x.width = w
x.pos += x.width
return r
}

// peek returns but does not consume the next rune in the input
func (x *hclLex) peek() rune {
r := x.next()
x.backup()
return r
}

// backup steps back one rune. Can only be called once per next.
func (x *hclLex) backup() {
x.pos -= x.width
}

// createErr records the given error
func (x *hclLex) createErr(msg string) {
x.err = fmt.Errorf("Line %d, column %d: %s", x.col, x.line, msg)
}

// The parser calls this method on a parse error.
func (x *hclLex) Error(s string) {
log.Printf("parse error: %s", s)
}
54 changes: 54 additions & 0 deletions lex_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package hcl

import (
"io/ioutil"
"path/filepath"
"reflect"
"testing"
)

func TestLex(t *testing.T) {
cases := []struct {
Input string
Output []int
}{
{
"comment.hcl",
[]int{IDENTIFIER, EQUAL, STRING, lexEOF},
},
{
"structure.hcl",
[]int{
IDENTIFIER, IDENTIFIER, STRING, LEFTBRACE,
IDENTIFIER, EQUAL, NUMBER, SEMICOLON,
RIGHTBRACE, lexEOF,
},
},
}

for _, tc := range cases {
d, err := ioutil.ReadFile(filepath.Join(fixtureDir, tc.Input))
if err != nil {
t.Fatalf("err: %s", err)
}

l := &hclLex{Input: string(d)}
var actual []int
for {
token := l.Lex(new(hclSymType))
actual = append(actual, token)

if token == lexEOF {
break
}

if len(actual) > 500 {
t.Fatalf("Input:%s\n\nExausted.", tc.Input)
}
}

if !reflect.DeepEqual(actual, tc.Output) {
t.Fatalf("Input: %s\n\nBad: %#v", tc.Input, actual)
}
}
}
34 changes: 34 additions & 0 deletions parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package hcl

import (
"sync"
)

// exprErrors are the errors built up from parsing. These should not
// be accessed directly.
var exprErrors []error
var exprLock sync.Mutex
var exprResult []map[string]interface{}

/*
// ExprParse parses the given expression and returns an executable
// Interpolation.
func ExprParse(v string) (Interpolation, error) {
exprLock.Lock()
defer exprLock.Unlock()
exprErrors = nil
exprResult = nil
// Parse
exprParse(&exprLex{input: v})
// Build up the errors
var err error
if len(exprErrors) > 0 {
err = &multierror.Error{Errors: exprErrors}
exprResult = nil
}
return exprResult, err
}
*/
34 changes: 34 additions & 0 deletions parse.y
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// This is the yacc input for creating the parser for HCL.

%{
package hcl

%}

%union {
num int
obj map[string]interface{}
str string
}

%type <obj> object

%token <num> NUMBER
%token <str> IDENTIFIER EQUAL SEMICOLON STRING
%token <str> LEFTBRACE RIGHTBRACE

%%

top:
object
{
exprResult = []map[string]interface{}{$1}
}

object:
IDENTIFIER EQUAL STRING
{
$$ = map[string]interface{}{$1: $3}
}

%%
Loading

0 comments on commit b6a1162

Please sign in to comment.