Browse Source

Rewrite AST and compiler to be more normal

master
Dylan Baker 5 years ago
parent
commit
581c99f794
17 changed files with 226 additions and 212 deletions
  1. 7
    0
      src/ast/application.js
  2. 7
    0
      src/ast/attribute.js
  3. 7
    0
      src/ast/boolean.js
  4. 7
    0
      src/ast/identifier.js
  5. 17
    0
      src/ast/index.js
  6. 0
    0
      src/ast/node.js
  7. 7
    0
      src/ast/number.js
  8. 7
    0
      src/ast/string.js
  9. 7
    0
      src/ast/symbol.js
  10. 67
    44
      src/compiler.js
  11. 2
    0
      src/index.js
  12. 3
    3
      src/lexer.js
  13. 54
    82
      src/parser.js
  14. 1
    0
      src/tokenTypes.js
  15. 0
    20
      test/compiler.js
  16. 9
    9
      test/lexer.js
  17. 24
    54
      test/parser.js

+ 7
- 0
src/ast/application.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Application extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 7
- 0
src/ast/attribute.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Attribute extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 7
- 0
src/ast/boolean.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Boolean extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 7
- 0
src/ast/identifier.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Identifier extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 17
- 0
src/ast/index.js View File

@@ -0,0 +1,17 @@
1
+const Application = require('./application')
2
+const Attribute = require('./attribute')
3
+const Boolean = require('./boolean')
4
+const Identifier = require('./identifier')
5
+const Number = require('./number')
6
+const String = require('./string')
7
+const Symbol = require('./symbol')
8
+
9
+module.exports = {
10
+  Application: Application,
11
+  Attribute: Attribute,
12
+  Boolean: Boolean,
13
+  Identifier: Identifier,
14
+  Number: Number,
15
+  String: String,
16
+  Symbol, Symbol,
17
+}

src/node.js → src/ast/node.js View File


+ 7
- 0
src/ast/number.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Number extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 7
- 0
src/ast/string.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class String extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 7
- 0
src/ast/symbol.js View File

@@ -0,0 +1,7 @@
1
+const Node = require('./node')
2
+
3
+module.exports = class Symbol extends Node {
4
+  constructor(opts) {
5
+    super(opts)
6
+  }
7
+}

+ 67
- 44
src/compiler.js View File

@@ -1,12 +1,12 @@
1
+const util = require('util')
2
+
1 3
 module.exports = class Compiler {
2 4
   constructor(tree, context) {
3 5
     this.tree = tree
4 6
     this.context = context
7
+    this.pos = 0
5 8
     this.result = ''
6
-  }
7
-
8
-  compile() {
9
-    const selfClosingTags = [
9
+    this.selfClosingTags = [
10 10
       'area',
11 11
       'base',
12 12
       'br',
@@ -25,56 +25,79 @@ module.exports = class Compiler {
25 25
       'track',
26 26
       'wbr',
27 27
     ]
28
+    this.standardLibrary = {
29
+      cond: function(predicate, left, right) {
30
+        if (predicate) {
31
+          let compiler = new Compiler([left], this.context)
32
+          return compiler.compile()
33
+        } else {
34
+          let compiler = new Compiler([right], this.context)
35
+          return compiler.compile()
36
+        }
37
+      },
38
+    }
39
+  }
28 40
 
41
+  compile() {
29 42
     this.tree.forEach(node => {
30
-      if (node.type === 'functionCall') {
31
-        const attributes = node.args.map(
32
-          arg =>
33
-            `${arg.attributeName}="${this.compileAttribute(
34
-              arg.attributeValue,
35
-            )}"`,
36
-        )
37
-        const compiler = new Compiler(node.subtree, this.context)
38
-        const content = compiler.compile()
39
-        this.result += `<${node.functionName}${
40
-          attributes.length ? ' ' : ''
41
-        }${attributes.join(' ')}>`
43
+      switch (node.constructor.name) {
44
+        case 'Application':
45
+          this.result += this.application(node)
46
+          break;
47
+        case 'String':
48
+          this.result += node.value;
49
+          break;
50
+        case 'Identifier':
51
+          this.result += this.lookup(node)
52
+          break;
53
+      }
54
+    })
42 55
 
43
-        if (content) {
44
-          this.result += content
45
-        }
56
+    return this.result
57
+  }
46 58
 
47
-        if (!selfClosingTags.includes(node.functionName)) {
48
-          this.result += `</${node.functionName}>`
49
-        }
50
-      } else if (node.type === 'string') {
51
-        this.result += node.content
52
-      } else if (node.type === 'identifier') {
53
-        this.result += this.lookup(node.name)
54
-      } else if (node.type === 'each') {
55
-        const symbol = node.symbol.value
56
-        const subject = this.lookup(node.subject.name)
57
-        subject.forEach(item => {
58
-          let context = {}
59
-          context[symbol] = item
60
-          const compiler = new Compiler([node.body], context)
61
-          this.result += compiler.compile()
62
-        })
63
-      }
59
+  application(node) {
60
+    if (this.standardLibrary[node.functionName.name]) {
61
+      return this.standardLibrary[node.functionName.name](...node.args)
62
+    }
63
+
64
+    return this.htmlElement(node)
65
+  }
66
+
67
+  htmlElement(node) {
68
+    let result = `<${node.functionName.name}`
69
+
70
+    node.args.filter(arg => arg.constructor.name === 'Attribute').forEach(arg => {
71
+      result += ` ${arg.name}=`
72
+      let compiler = new Compiler([arg.value], this.context)
73
+      result += `"${compiler.compile()}"`
64 74
     })
65 75
 
66
-    return this.result.trim()
76
+    result += '>'
77
+
78
+    node.args.filter(arg => arg.constructor.name !== 'Attribute').forEach(arg => {
79
+      let compiler = new Compiler([arg], this.context)
80
+      result += compiler.compile()
81
+    })
82
+
83
+    if (!this.selfClosingTags.includes(node.functionName.name)) {
84
+      result += `</${node.functionName.name}>`
85
+    }
86
+
87
+    return result
67 88
   }
68 89
 
69
-  compileAttribute(attribute) {
70
-    if (attribute.type === 'string') {
71
-      return attribute.content
72
-    } else if (attribute.type === 'identifier') {
73
-      return this.lookup(attribute.name)
90
+  lookup(identifier) {
91
+    let result = this.context[identifier.name]
92
+
93
+    if (!result) {
94
+      throw `Undefined variable '${identifier.name}'`
74 95
     }
96
+
97
+    return result
75 98
   }
76 99
 
77
-  lookup(name) {
78
-    return this.context[name]
100
+  currentNode() {
101
+    return this.tree[this.pos]
79 102
   }
80 103
 }

+ 2
- 0
src/index.js View File

@@ -2,6 +2,8 @@ const Lexer = require('./lexer')
2 2
 const Parser = require('./parser')
3 3
 const Compiler = require('./compiler')
4 4
 
5
+const util = require('util')
6
+
5 7
 module.exports = function oslo(source, context) {
6 8
   const lexer = new Lexer()
7 9
   const tokens = lexer.scan(source)

+ 3
- 3
src/lexer.js View File

@@ -77,15 +77,15 @@ module.exports = class Lexer {
77 77
 
78 78
         let value = endPattern.exec(source.slice(pos))[0].trim()
79 79
 
80
-        if (['if', 'each'].includes(value)) {
80
+        if (allowSpecialCharactersInLiterals) {
81 81
           tokenStream.tokens.push({
82
-            type: tokenTypes.KEYWORD,
82
+            type: tokenTypes.LITERAL,
83 83
             line: line,
84 84
             value: value,
85 85
           })
86 86
         } else {
87 87
           tokenStream.tokens.push({
88
-            type: tokenTypes.LITERAL,
88
+            type: tokenTypes.IDENTIFIER,
89 89
             line: line,
90 90
             value: value.trim(),
91 91
           })

+ 54
- 82
src/parser.js View File

@@ -1,5 +1,7 @@
1 1
 const Error = require('./Error')
2
-const Node = require('./node')
2
+
3
+const AST = require('./ast')
4
+
3 5
 const tokenTypes = require('./tokenTypes')
4 6
 
5 7
 module.exports = class Parser {
@@ -21,21 +23,38 @@ module.exports = class Parser {
21 23
   }
22 24
 
23 25
   expr() {
24
-    let node
26
+    let token = this.tokenStream.peek()
27
+    switch (token.type) {
28
+      case tokenTypes.ATTRIBUTE:
29
+        return this.attribute()
30
+      case tokenTypes.BOOLEAN:
31
+        return this.bool()
32
+      case tokenTypes.IDENTIFIER:
33
+        return this.identifier()
34
+      case tokenTypes.NUMBER:
35
+        return this.number()
36
+      case tokenTypes.QUOTE:
37
+        return this.string()
38
+      case tokenTypes.SYMBOL:
39
+        return this.symbol()
40
+      case tokenTypes.OPAREN:
41
+        return this.form()
42
+      default:
43
+        this.tokenStream.error = `Unexpected ${token.type} on line ${token.line}`
44
+        break;
45
+    }
46
+  }
25 47
 
48
+  form() {
26 49
     this.tokenStream.eat(tokenTypes.OPAREN)
27 50
 
28
-    while (
29
-      this.tokenStream.peek().type !== tokenTypes.CPAREN &&
30
-      this.tokenStream.peek().type !== tokenTypes.EOF
31
-    ) {
32
-      if (this.tokenStream.error) return
51
+    let node = new AST.Application()
33 52
 
34
-      if (this.tokenStream.peek().type === tokenTypes.LITERAL) {
35
-        node = this.element()
36
-      } else if (this.tokenStream.peek().type === tokenTypes.KEYWORD) {
37
-        node = this.keyword()
38
-      }
53
+    node.functionName = this.identifier()
54
+    node.args = []
55
+
56
+    while (this.tokenStream.peek().type !== tokenTypes.CPAREN) {
57
+      node.args.push(this.expr())
39 58
     }
40 59
 
41 60
     this.tokenStream.eat(tokenTypes.CPAREN)
@@ -43,90 +62,43 @@ module.exports = class Parser {
43 62
     return node
44 63
   }
45 64
 
46
-  element() {
47
-    let elementNode = new Node({
48
-      type: 'functionCall',
49
-      args: [],
50
-      subtree: [],
51
-      functionName: this.tokenStream.eat(tokenTypes.LITERAL).value,
65
+  attribute() {
66
+    return new AST.Attribute({
67
+      name: this.tokenStream.eat(tokenTypes.ATTRIBUTE).value,
68
+      value: this.expr()
52 69
     })
53
-
54
-    while (
55
-      ![tokenTypes.CPAREN, tokenTypes.EOF].includes(
56
-        this.tokenStream.peek().type,
57
-      )
58
-    ) {
59
-      if (this.tokenStream.error) return
60
-
61
-      if (this.tokenStream.peek().type === tokenTypes.ATTRIBUTE) {
62
-        elementNode.args.push(this.attribute())
63
-      } else if (this.tokenStream.peek().type === tokenTypes.QUOTE) {
64
-        elementNode.subtree.push(this.quotedString())
65
-      } else if (this.tokenStream.peek().type === tokenTypes.OPAREN) {
66
-        elementNode.subtree.push(this.expr())
67
-      } else if (this.tokenStream.peek().type === tokenTypes.LITERAL) {
68
-        elementNode.subtree.push(this.identifier())
69
-      }
70
-    }
71
-
72
-    return elementNode
73 70
   }
74 71
 
75
-  attribute() {
76
-    let attributeNode = new Node()
77
-    attributeNode.attributeName = this.tokenStream.eat(
78
-      tokenTypes.ATTRIBUTE,
79
-    ).value
80
-    if (this.tokenStream.peek().type === tokenTypes.QUOTE) {
81
-      attributeNode.attributeValue = this.quotedString()
82
-    } else if (this.tokenStream.peek().type === tokenTypes.LITERAL) {
83
-      attributeNode.attributeValue = this.identifier()
84
-    } else {
85
-      let token = this.tokenStream.peek()
86
-      this.tokenStream.error = new Error({
87
-        line: token.line,
88
-        message: `Encountered an unexpected ${token.type} while looking for a string or identifier.`
89
-      })
90
-    }
91
-
92
-    return attributeNode
72
+  bool() {
73
+    return new AST.Boolean({
74
+      value: this.tokenStream.eat(tokenTypes.BOOLEAN).value
75
+    })
93 76
   }
94 77
 
95 78
   identifier() {
96
-    const identifier = this.tokenStream.eat(tokenTypes.LITERAL)
97
-    return new Node({
98
-      type: 'identifier',
99
-      name: identifier.value,
79
+    return new AST.Identifier({
80
+      name: this.tokenStream.eat(tokenTypes.IDENTIFIER).value
100 81
     })
101 82
   }
102 83
 
103
-  quotedString() {
104
-    return new Node({
105
-      type: 'string',
106
-      content: this.string(),
84
+  number() {
85
+    return new AST.Number({
86
+      value: this.tokenStream.eat(tokenTypes.NUMBER).value
107 87
     })
108 88
   }
109 89
 
110
-  keyword() {
111
-    const keyword = this.tokenStream.eat(tokenTypes.KEYWORD)
112
-
113
-    if (keyword.value === 'each') {
114
-      const subject = this.identifier()
115
-      const symbol = this.tokenStream.eat(tokenTypes.SYMBOL)
116
-      const body = this.expr()
117
-      return new Node({
118
-        type: 'each',
119
-        symbol: symbol,
120
-        body: body,
121
-        subject: subject,
122
-      })
123
-    }
124
-  }
125
-
126 90
   string() {
127 91
     this.tokenStream.eat(tokenTypes.QUOTE)
128
-    let stringValue = this.tokenStream.eat(tokenTypes.LITERAL).value
92
+    let node = new AST.String({
93
+      value: this.tokenStream.eat(tokenTypes.LITERAL).value
94
+    })
129 95
     this.tokenStream.eat(tokenTypes.QUOTE)
130
-    return stringValue
96
+    return node
97
+  }
98
+
99
+  symbol() {
100
+    return new AST.Symbol({
101
+      value: this.tokenStream.eat(tokenTypes.SYMBOL).value
102
+    })
131 103
   }
132 104
 }

+ 1
- 0
src/tokenTypes.js View File

@@ -8,5 +8,6 @@ module.exports = {
8 8
   SYMBOL: 'symbol',
9 9
   NUMBER: 'number',
10 10
   BOOLEAN: 'boolean',
11
+  IDENTIFIER: 'identifier',
11 12
   EOF: 'EOF',
12 13
 }

+ 0
- 20
test/compiler.js View File

@@ -2,7 +2,6 @@ const test = require('tape')
2 2
 
3 3
 const Compiler = require('../src/compiler')
4 4
 const Lexer = require('../src/lexer')
5
-const Node = require('../src/node')
6 5
 const Parser = require('../src/parser')
7 6
 const tt = require('../src/tokenTypes')
8 7
 
@@ -44,25 +43,6 @@ test('renders variables according to passed-in context', t => {
44 43
   )
45 44
 })
46 45
 
47
-test('compiles map operations', function(t) {
48
-  t.plan(1)
49
-  const lexer = new Lexer()
50
-  const tokenStream = lexer.scan(`
51
-    (ul
52
-      (each items 'item (li item)))
53
-  `)
54
-  const parser = new Parser(tokenStream)
55
-  const tree = parser.parse()
56
-  const compiler = new Compiler(tree, {
57
-    items: ['one', 'two', 'three'],
58
-  })
59
-  const result = compiler.compile()
60
-  t.deepEqual(
61
-    result.replace(/\n/g, '').replace(/  +/g, ''),
62
-    '<ul><li>one</li><li>two</li><li>three</li></ul>',
63
-  )
64
-})
65
-
66 46
 test('self closing tags are respected', function(t) {
67 47
   t.plan(1)
68 48
   const lexer = new Lexer()

+ 9
- 9
test/lexer.js View File

@@ -11,13 +11,13 @@ test('lexes simple template correctly', t => {
11 11
   ).tokens
12 12
   t.deepEqual(tokens.map(token => token.type), [
13 13
     tt.OPAREN,
14
-    tt.LITERAL,
14
+    tt.IDENTIFIER,
15 15
     tt.ATTRIBUTE,
16 16
     tt.QUOTE,
17 17
     tt.LITERAL,
18 18
     tt.QUOTE,
19 19
     tt.OPAREN,
20
-    tt.LITERAL,
20
+    tt.IDENTIFIER,
21 21
     tt.QUOTE,
22 22
     tt.LITERAL,
23 23
     tt.QUOTE,
@@ -47,21 +47,21 @@ test('multiple identifiers in a row are kept separate', t => {
47 47
   let tokens = lexer.scan(`(test test test)`).tokens
48 48
   t.deepEqual(tokens.map(token => token.type), [
49 49
     tt.OPAREN,
50
-    tt.LITERAL,
51
-    tt.LITERAL,
52
-    tt.LITERAL,
50
+    tt.IDENTIFIER,
51
+    tt.IDENTIFIER,
52
+    tt.IDENTIFIER,
53 53
     tt.CPAREN,
54 54
     tt.EOF,
55 55
   ])
56 56
   tokens = lexer.scan(`(test "test" test test)`).tokens
57 57
   t.deepEqual(tokens.map(token => token.type), [
58 58
     tt.OPAREN,
59
-    tt.LITERAL,
59
+    tt.IDENTIFIER,
60 60
     tt.QUOTE,
61 61
     tt.LITERAL,
62 62
     tt.QUOTE,
63
-    tt.LITERAL,
64
-    tt.LITERAL,
63
+    tt.IDENTIFIER,
64
+    tt.IDENTIFIER,
65 65
     tt.CPAREN,
66 66
     tt.EOF,
67 67
   ])
@@ -75,7 +75,7 @@ test('allow special characters inside quotes', t => {
75 75
   `).tokens
76 76
   t.deepEqual(tokens.map(token => token.type), [
77 77
     tt.OPAREN,
78
-    tt.LITERAL,
78
+    tt.IDENTIFIER,
79 79
     tt.QUOTE,
80 80
     tt.LITERAL,
81 81
     tt.QUOTE,

+ 24
- 54
test/parser.js View File

@@ -1,7 +1,7 @@
1 1
 const test = require('tape')
2 2
 
3 3
 const Lexer = require('../src/lexer')
4
-const Node = require('../src/node')
4
+const AST = require('../src/ast/index')
5 5
 const Parser = require('../src/parser')
6 6
 const tt = require('../src/tokenTypes')
7 7
 
@@ -10,66 +10,36 @@ test('parses token stream into a tree', t => {
10 10
   const lexer = new Lexer()
11 11
   let tokenStream = lexer.scan(`
12 12
     (div :class "foobar"
13
-      (p :class "bazquux"))
13
+      (p :class (cond #t "primary" "secondary")))
14 14
   `)
15 15
   let parser = new Parser(tokenStream)
16 16
   let tree = parser.parse()
17 17
 
18 18
   t.deepEqual(tree, [
19
-    new Node({
20
-      type: 'functionCall',
21
-      functionName: 'div',
19
+    new AST.Application({
20
+      functionName: new AST.Identifier({ name: 'div' }),
22 21
       args: [
23
-        new Node({
24
-          attributeName: 'class',
25
-          attributeValue: new Node({ type: 'string', content: 'foobar' }),
22
+        new AST.Attribute({
23
+          name: 'class',
24
+          value: new AST.String({ value: 'foobar' })
26 25
         }),
27
-      ],
28
-      subtree: [
29
-        new Node({
30
-          type: 'functionCall',
31
-          functionName: 'p',
26
+        new AST.Application({
27
+          functionName: new AST.Identifier({ name: 'p' }),
32 28
           args: [
33
-            new Node({
34
-              attributeName: 'class',
35
-              attributeValue: new Node({ type: 'string', content: 'bazquux' }),
36
-            }),
37
-          ],
38
-          subtree: [],
39
-        }),
40
-      ],
41
-    }),
29
+            new AST.Attribute({
30
+              name: 'class',
31
+              value: new AST.Application({
32
+                functionName: new AST.Identifier({ name: 'cond' }),
33
+                args: [
34
+                  new AST.Boolean({ value: true }),
35
+                  new AST.String({ value: 'primary' }),
36
+                  new AST.String({ value: 'secondary' }),
37
+                ]
38
+              })
39
+            })
40
+          ]
41
+        })
42
+      ]
43
+    })
42 44
   ])
43 45
 })
44
-
45
-test('missing close paren returns an error', function(t) {
46
-  t.plan(2)
47
-  const lexer = new Lexer()
48
-  let tokenStream = lexer.scan(`
49
-    (div :class "foobar"
50
-  `)
51
-  let parser = new Parser(tokenStream)
52
-  let tree = parser.parse()
53
-  t.equal(tree.error.constructor.name, 'Error')
54
-  t.deepEqual(tree.error, {
55
-    file: undefined,
56
-    line: 3,
57
-    message: 'Encountered an unexpected EOF while looking for a ).',
58
-  })
59
-})
60
-
61
-test('unexpected attribute return an error', function(t) {
62
-  t.plan(2)
63
-  const lexer = new Lexer()
64
-  let tokenStream = lexer.scan(`
65
-    (div :class :id)
66
-  `)
67
-  let parser = new Parser(tokenStream)
68
-  let tree = parser.parse()
69
-  t.equal(tree.error.constructor.name, 'Error')
70
-  t.deepEqual(tree.error, {
71
-    file: undefined,
72
-    line: 2,
73
-    message: 'Encountered an unexpected attribute while looking for a string or identifier.',
74
-  })
75
-})

Loading…
Cancel
Save