Browse Source

Rewrite AST and compiler to be more normal

master
Dylan Baker 5 years ago
parent
commit
581c99f794

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

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

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

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

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

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

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

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

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
+const util = require('util')
2
+
1
 module.exports = class Compiler {
3
 module.exports = class Compiler {
2
   constructor(tree, context) {
4
   constructor(tree, context) {
3
     this.tree = tree
5
     this.tree = tree
4
     this.context = context
6
     this.context = context
7
+    this.pos = 0
5
     this.result = ''
8
     this.result = ''
6
-  }
7
-
8
-  compile() {
9
-    const selfClosingTags = [
9
+    this.selfClosingTags = [
10
       'area',
10
       'area',
11
       'base',
11
       'base',
12
       'br',
12
       'br',
25
       'track',
25
       'track',
26
       'wbr',
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
     this.tree.forEach(node => {
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
 const Parser = require('./parser')
2
 const Parser = require('./parser')
3
 const Compiler = require('./compiler')
3
 const Compiler = require('./compiler')
4
 
4
 
5
+const util = require('util')
6
+
5
 module.exports = function oslo(source, context) {
7
 module.exports = function oslo(source, context) {
6
   const lexer = new Lexer()
8
   const lexer = new Lexer()
7
   const tokens = lexer.scan(source)
9
   const tokens = lexer.scan(source)

+ 3
- 3
src/lexer.js View File

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

+ 54
- 82
src/parser.js View File

1
 const Error = require('./Error')
1
 const Error = require('./Error')
2
-const Node = require('./node')
2
+
3
+const AST = require('./ast')
4
+
3
 const tokenTypes = require('./tokenTypes')
5
 const tokenTypes = require('./tokenTypes')
4
 
6
 
5
 module.exports = class Parser {
7
 module.exports = class Parser {
21
   }
23
   }
22
 
24
 
23
   expr() {
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
     this.tokenStream.eat(tokenTypes.OPAREN)
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
     this.tokenStream.eat(tokenTypes.CPAREN)
60
     this.tokenStream.eat(tokenTypes.CPAREN)
43
     return node
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
   identifier() {
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
   string() {
90
   string() {
127
     this.tokenStream.eat(tokenTypes.QUOTE)
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
     this.tokenStream.eat(tokenTypes.QUOTE)
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
   SYMBOL: 'symbol',
8
   SYMBOL: 'symbol',
9
   NUMBER: 'number',
9
   NUMBER: 'number',
10
   BOOLEAN: 'boolean',
10
   BOOLEAN: 'boolean',
11
+  IDENTIFIER: 'identifier',
11
   EOF: 'EOF',
12
   EOF: 'EOF',
12
 }
13
 }

+ 0
- 20
test/compiler.js View File

2
 
2
 
3
 const Compiler = require('../src/compiler')
3
 const Compiler = require('../src/compiler')
4
 const Lexer = require('../src/lexer')
4
 const Lexer = require('../src/lexer')
5
-const Node = require('../src/node')
6
 const Parser = require('../src/parser')
5
 const Parser = require('../src/parser')
7
 const tt = require('../src/tokenTypes')
6
 const tt = require('../src/tokenTypes')
8
 
7
 
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
 test('self closing tags are respected', function(t) {
46
 test('self closing tags are respected', function(t) {
67
   t.plan(1)
47
   t.plan(1)
68
   const lexer = new Lexer()
48
   const lexer = new Lexer()

+ 9
- 9
test/lexer.js View File

11
   ).tokens
11
   ).tokens
12
   t.deepEqual(tokens.map(token => token.type), [
12
   t.deepEqual(tokens.map(token => token.type), [
13
     tt.OPAREN,
13
     tt.OPAREN,
14
-    tt.LITERAL,
14
+    tt.IDENTIFIER,
15
     tt.ATTRIBUTE,
15
     tt.ATTRIBUTE,
16
     tt.QUOTE,
16
     tt.QUOTE,
17
     tt.LITERAL,
17
     tt.LITERAL,
18
     tt.QUOTE,
18
     tt.QUOTE,
19
     tt.OPAREN,
19
     tt.OPAREN,
20
-    tt.LITERAL,
20
+    tt.IDENTIFIER,
21
     tt.QUOTE,
21
     tt.QUOTE,
22
     tt.LITERAL,
22
     tt.LITERAL,
23
     tt.QUOTE,
23
     tt.QUOTE,
47
   let tokens = lexer.scan(`(test test test)`).tokens
47
   let tokens = lexer.scan(`(test test test)`).tokens
48
   t.deepEqual(tokens.map(token => token.type), [
48
   t.deepEqual(tokens.map(token => token.type), [
49
     tt.OPAREN,
49
     tt.OPAREN,
50
-    tt.LITERAL,
51
-    tt.LITERAL,
52
-    tt.LITERAL,
50
+    tt.IDENTIFIER,
51
+    tt.IDENTIFIER,
52
+    tt.IDENTIFIER,
53
     tt.CPAREN,
53
     tt.CPAREN,
54
     tt.EOF,
54
     tt.EOF,
55
   ])
55
   ])
56
   tokens = lexer.scan(`(test "test" test test)`).tokens
56
   tokens = lexer.scan(`(test "test" test test)`).tokens
57
   t.deepEqual(tokens.map(token => token.type), [
57
   t.deepEqual(tokens.map(token => token.type), [
58
     tt.OPAREN,
58
     tt.OPAREN,
59
-    tt.LITERAL,
59
+    tt.IDENTIFIER,
60
     tt.QUOTE,
60
     tt.QUOTE,
61
     tt.LITERAL,
61
     tt.LITERAL,
62
     tt.QUOTE,
62
     tt.QUOTE,
63
-    tt.LITERAL,
64
-    tt.LITERAL,
63
+    tt.IDENTIFIER,
64
+    tt.IDENTIFIER,
65
     tt.CPAREN,
65
     tt.CPAREN,
66
     tt.EOF,
66
     tt.EOF,
67
   ])
67
   ])
75
   `).tokens
75
   `).tokens
76
   t.deepEqual(tokens.map(token => token.type), [
76
   t.deepEqual(tokens.map(token => token.type), [
77
     tt.OPAREN,
77
     tt.OPAREN,
78
-    tt.LITERAL,
78
+    tt.IDENTIFIER,
79
     tt.QUOTE,
79
     tt.QUOTE,
80
     tt.LITERAL,
80
     tt.LITERAL,
81
     tt.QUOTE,
81
     tt.QUOTE,

+ 24
- 54
test/parser.js View File

1
 const test = require('tape')
1
 const test = require('tape')
2
 
2
 
3
 const Lexer = require('../src/lexer')
3
 const Lexer = require('../src/lexer')
4
-const Node = require('../src/node')
4
+const AST = require('../src/ast/index')
5
 const Parser = require('../src/parser')
5
 const Parser = require('../src/parser')
6
 const tt = require('../src/tokenTypes')
6
 const tt = require('../src/tokenTypes')
7
 
7
 
10
   const lexer = new Lexer()
10
   const lexer = new Lexer()
11
   let tokenStream = lexer.scan(`
11
   let tokenStream = lexer.scan(`
12
     (div :class "foobar"
12
     (div :class "foobar"
13
-      (p :class "bazquux"))
13
+      (p :class (cond #t "primary" "secondary")))
14
   `)
14
   `)
15
   let parser = new Parser(tokenStream)
15
   let parser = new Parser(tokenStream)
16
   let tree = parser.parse()
16
   let tree = parser.parse()
17
 
17
 
18
   t.deepEqual(tree, [
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
       args: [
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
           args: [
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