Browse Source

Fix error handling and bug where let bindings stay in scope

master
Dylan Baker 5 years ago
parent
commit
bd4aa9d124
10 changed files with 72 additions and 39 deletions
  1. 3
    2
      bin/oslo.js
  2. 14
    6
      src/env.js
  3. 18
    7
      src/evaluator.js
  4. 9
    0
      src/index.js
  5. 0
    1
      src/osloError.js
  6. 4
    2
      src/parser.js
  7. 2
    2
      src/tokenStream.js
  8. 8
    0
      test/evaluatorTest.js
  9. 0
    2
      test/helpers.js
  10. 14
    17
      test/parserTest.js

+ 3
- 2
bin/oslo.js View File

5
 const args = require('minimist')(process.argv.slice(2))
5
 const args = require('minimist')(process.argv.slice(2))
6
 
6
 
7
 const oslo = require('../src/index')
7
 const oslo = require('../src/index')
8
+const OsloError = require('../src/osloError')
8
 
9
 
9
 class OsloCLI {
10
 class OsloCLI {
10
   constructor(opts) {
11
   constructor(opts) {
39
       process.exit()
40
       process.exit()
40
     }
41
     }
41
 
42
 
42
-    if (output.error) {
43
-      this.error(output.error)
43
+    if (output.constructor == OsloError) {
44
+      this.error(output)
44
     }
45
     }
45
 
46
 
46
     if (opts.o) {
47
     if (opts.o) {

+ 14
- 6
src/env.js View File

1
-const AST = require('../src/ast')
1
+const AST = require('./ast')
2
+const OsloError = require('./osloError')
2
 
3
 
3
 module.exports = class Env {
4
 module.exports = class Env {
4
   constructor(parent = null) {
5
   constructor(parent = null) {
6
+    this.data = {}
7
+
5
     if (parent) {
8
     if (parent) {
6
-      this.data = parent.data
9
+      Object.keys(parent.data).forEach(key => {
10
+        this.data[key] = parent.data[key]
11
+      })
7
     } else {
12
     } else {
8
       this.data = {
13
       this.data = {
9
         '+': (a, b) => new AST.Number({ value: a.value + b.value }),
14
         '+': (a, b) => new AST.Number({ value: a.value + b.value }),
20
   }
25
   }
21
 
26
 
22
   get(symbol) {
27
   get(symbol) {
23
-    if (this.data[symbol]) {
24
-      return this.data[symbol]
28
+    if (this.data[symbol.name]) {
29
+      return this.data[symbol.name]
25
     }
30
     }
26
 
31
 
27
-    throw `Symbol ${symbol} is not bound`
32
+    return new OsloError({
33
+      line: symbol.line,
34
+      message: `Symbol '${symbol.name}' is not bound`,
35
+    })
28
   }
36
   }
29
 
37
 
30
   set(symbol, value) {
38
   set(symbol, value) {
31
-    this.data[symbol] = value
39
+    this.data[symbol.name] = value
32
   }
40
   }
33
 }
41
 }

+ 18
- 7
src/evaluator.js View File

1
 const AST = require('./ast')
1
 const AST = require('./ast')
2
 const Env = require('./env')
2
 const Env = require('./env')
3
+const OsloError = require('./osloError')
3
 
4
 
4
 module.exports = class Evaluator {
5
 module.exports = class Evaluator {
5
   eval(tree, env) {
6
   eval(tree, env) {
6
     this.env = env
7
     this.env = env
8
+    this.error = false
7
 
9
 
8
     let evaluatedTree = []
10
     let evaluatedTree = []
9
 
11
 
11
       let evaluatedNode = this.evalNode(node)
13
       let evaluatedNode = this.evalNode(node)
12
 
14
 
13
       if (evaluatedNode) {
15
       if (evaluatedNode) {
14
-        evaluatedTree.push(evaluatedNode)
16
+        if (evaluatedNode.constructor === OsloError) {
17
+          this.error = evaluatedNode
18
+        } else {
19
+          evaluatedTree.push(evaluatedNode)
20
+        }
15
       }
21
       }
16
     })
22
     })
17
 
23
 
24
+    if (this.error) {
25
+      return this.error
26
+    }
27
+
18
     return evaluatedTree
28
     return evaluatedTree
19
   }
29
   }
20
 
30
 
27
       case AST.String:
37
       case AST.String:
28
         return node
38
         return node
29
       case AST.Identifier:
39
       case AST.Identifier:
30
-        return env.get(node.name)
40
+        return env.get(node)
31
       case AST.Definition:
41
       case AST.Definition:
32
-        this.env.set(node.symbol.name, node.value)
42
+        this.env.set(node.symbol, node.value)
33
         return false
43
         return false
34
       case AST.LetBinding:
44
       case AST.LetBinding:
35
         let innerEnv = new Env(env)
45
         let innerEnv = new Env(env)
36
 
46
 
37
         node.bindings.forEach(binding => {
47
         node.bindings.forEach(binding => {
38
-          innerEnv.set(binding.key.name, binding.value)
48
+          innerEnv.set(binding.key, binding.value)
39
         })
49
         })
40
 
50
 
41
         return this.evalNode(node.body, innerEnv)
51
         return this.evalNode(node.body, innerEnv)
52
           case AST.Identifier:
62
           case AST.Identifier:
53
             return this.evalNode(
63
             return this.evalNode(
54
               new AST.Application({
64
               new AST.Application({
55
-                function: env.get(node.function.name),
65
+                function: env.get(node.function),
56
                 args: node.args,
66
                 args: node.args,
57
               }),
67
               }),
68
+              env,
58
             )
69
             )
59
           case AST.Lambda:
70
           case AST.Lambda:
60
             let innerEnv = new Env(env)
71
             let innerEnv = new Env(env)
61
 
72
 
62
             node.function.parameters.forEach((param, index) => {
73
             node.function.parameters.forEach((param, index) => {
63
-              innerEnv.set(param.name, node.args[index])
74
+              innerEnv.set(param, node.args[index])
64
             })
75
             })
65
 
76
 
66
             return this.evalNode(node.function.body, innerEnv)
77
             return this.evalNode(node.function.body, innerEnv)
67
           case Function:
78
           case Function:
68
             let args = node.args.map(arg => {
79
             let args = node.args.map(arg => {
69
-              return this.evalNode(arg)
80
+              return this.evalNode(arg, env)
70
             })
81
             })
71
             args.unshift(this)
82
             args.unshift(this)
72
             return node.function.call(...args)
83
             return node.function.call(...args)

+ 9
- 0
src/index.js View File

1
 const Env = require('./env')
1
 const Env = require('./env')
2
 const Evaluator = require('./evaluator')
2
 const Evaluator = require('./evaluator')
3
 const Lexer = require('./lexer')
3
 const Lexer = require('./lexer')
4
+const OsloError = require('./osloError')
4
 const Parser = require('./parser')
5
 const Parser = require('./parser')
5
 
6
 
6
 module.exports = function oslo(source, context) {
7
 module.exports = function oslo(source, context) {
10
   const parser = new Parser()
11
   const parser = new Parser()
11
   const tree = parser.parse(tokens)
12
   const tree = parser.parse(tokens)
12
 
13
 
14
+  if (tree.error) {
15
+    return tree.error
16
+  }
17
+
13
   const evaluator = new Evaluator()
18
   const evaluator = new Evaluator()
14
   const evaluatedTree = evaluator.eval(tree, new Env())
19
   const evaluatedTree = evaluator.eval(tree, new Env())
15
 
20
 
21
+  if (evaluatedTree.constructor == OsloError) {
22
+    return evaluatedTree
23
+  }
24
+
16
   return ''
25
   return ''
17
 }
26
 }

src/error.js → src/osloError.js View File

1
 module.exports = class Error {
1
 module.exports = class Error {
2
   constructor(opts) {
2
   constructor(opts) {
3
-    this.file = opts.file
4
     this.line = opts.line
3
     this.line = opts.line
5
     this.message = opts.message
4
     this.message = opts.message
6
   }
5
   }

+ 4
- 2
src/parser.js View File

1
 const AST = require('./ast')
1
 const AST = require('./ast')
2
-const Error = require('./Error')
2
+const OsloError = require('./OsloError')
3
 const tokenTypes = require('./tokenTypes')
3
 const tokenTypes = require('./tokenTypes')
4
 
4
 
5
 module.exports = class Parser {
5
 module.exports = class Parser {
106
   }
106
   }
107
 
107
 
108
   identifier() {
108
   identifier() {
109
+    let token = this.tokenStream.eat(tokenTypes.IDENTIFIER)
109
     return new AST.Identifier({
110
     return new AST.Identifier({
110
-      name: this.tokenStream.eat(tokenTypes.IDENTIFIER).value,
111
+      name: token.value,
112
+      line: token.line,
111
     })
113
     })
112
   }
114
   }
113
 
115
 

+ 2
- 2
src/tokenStream.js View File

1
-const Error = require('./error')
1
+const OsloError = require('./osloError')
2
 
2
 
3
 module.exports = class TokenStream {
3
 module.exports = class TokenStream {
4
   constructor() {
4
   constructor() {
18
       return token
18
       return token
19
     }
19
     }
20
 
20
 
21
-    this.error = new Error({
21
+    this.error = new OsloError({
22
       line: token.line,
22
       line: token.line,
23
       message: `Encountered an unexpected ${
23
       message: `Encountered an unexpected ${
24
         token.type
24
         token.type

+ 8
- 0
test/evaluatorTest.js View File

3
 
3
 
4
 const AST = require('../src/ast')
4
 const AST = require('../src/ast')
5
 const Evaluator = require('../src/evaluator')
5
 const Evaluator = require('../src/evaluator')
6
+const OsloError = require('../src/osloError')
6
 
7
 
7
 test('applies functions from core library', t => {
8
 test('applies functions from core library', t => {
8
   t.plan(1)
9
   t.plan(1)
47
   const tree = helpers.evaluate('(let ((x 1) (y 2)) (+ x y))')
48
   const tree = helpers.evaluate('(let ((x 1) (y 2)) (+ x y))')
48
   t.deepEqual(tree[0], new AST.Number({ value: 3 }))
49
   t.deepEqual(tree[0], new AST.Number({ value: 3 }))
49
 })
50
 })
51
+
52
+test('let bindings go out of scope after the let block', t => {
53
+  t.plan(1)
54
+
55
+  const tree = helpers.evaluate('(let ((x 1)) x) x')
56
+  t.deepEqual(tree, new OsloError({ line: 1, message: 'Symbol \'x\' is not bound' }))
57
+})

+ 0
- 2
test/helpers.js View File

23
   parse: parse,
23
   parse: parse,
24
   scan: scan,
24
   scan: scan,
25
 }
25
 }
26
-
27
-console.log(evaluate('(+ 1 2)'))

+ 14
- 17
test/parserTest.js View File

8
   t.plan(1)
8
   t.plan(1)
9
   const tree = helpers.parse(`
9
   const tree = helpers.parse(`
10
     (div :class "foobar"
10
     (div :class "foobar"
11
-      (p :class (cond #t "primary" "secondary")))
11
+      (p :class (if #t "primary" "secondary")))
12
   `)
12
   `)
13
 
13
 
14
   t.deepEqual(tree, [
14
   t.deepEqual(tree, [
15
     new AST.Application({
15
     new AST.Application({
16
-      function: new AST.Identifier({ name: 'div' }),
16
+      function: new AST.Identifier({name: 'div', line: 2 }),
17
       args: [
17
       args: [
18
         new AST.Attribute({
18
         new AST.Attribute({
19
           name: 'class',
19
           name: 'class',
20
           value: new AST.String({ value: 'foobar' }),
20
           value: new AST.String({ value: 'foobar' }),
21
         }),
21
         }),
22
         new AST.Application({
22
         new AST.Application({
23
-          function: new AST.Identifier({ name: 'p' }),
23
+          function: new AST.Identifier({ name: 'p', line: 3 }),
24
           args: [
24
           args: [
25
             new AST.Attribute({
25
             new AST.Attribute({
26
               name: 'class',
26
               name: 'class',
27
-              value: new AST.Application({
28
-                function: new AST.Identifier({ name: 'cond' }),
29
-                args: [
30
-                  new AST.Boolean({ value: true }),
31
-                  new AST.String({ value: 'primary' }),
32
-                  new AST.String({ value: 'secondary' }),
33
-                ],
27
+              value: new AST.Conditional({
28
+                condition: new AST.Boolean({ value: true }),
29
+                ifCase: new AST.String({ value: 'primary' }),
30
+                elseCase: new AST.String({ value: 'secondary' }),
34
               }),
31
               }),
35
             }),
32
             }),
36
           ],
33
           ],
46
 
43
 
47
   t.deepEqual(tree, [
44
   t.deepEqual(tree, [
48
     new AST.Application({
45
     new AST.Application({
49
-      function: new AST.Identifier({ name: 'p' }),
46
+      function: new AST.Identifier({ name: 'p', line: 1 }),
50
       args: [new AST.String({ value: '' })],
47
       args: [new AST.String({ value: '' })],
51
     }),
48
     }),
52
   ])
49
   ])
61
       function: new AST.Lambda({
58
       function: new AST.Lambda({
62
         parameters: [new AST.Identifier({ name: 'n' })],
59
         parameters: [new AST.Identifier({ name: 'n' })],
63
         body: new AST.Application({
60
         body: new AST.Application({
64
-          function: new AST.Identifier({ name: '+' }),
61
+          function: new AST.Identifier({ name: '+', line: 1 }),
65
           args: [
62
           args: [
66
-            new AST.Identifier({ name: 'n' }),
63
+            new AST.Identifier({ name: 'n', line: 1 }),
67
             new AST.Number({ value: 1 }),
64
             new AST.Number({ value: 1 }),
68
           ],
65
           ],
69
         }),
66
         }),
81
     new AST.Conditional({
78
     new AST.Conditional({
82
       condition: new AST.Boolean({ value: true }),
79
       condition: new AST.Boolean({ value: true }),
83
       ifCase: new AST.Application({
80
       ifCase: new AST.Application({
84
-        function: new AST.Identifier({ name: 'do' }),
85
-        args: [new AST.Identifier({ name: 'this' })],
81
+        function: new AST.Identifier({ line: 1,  name: 'do' }),
82
+        args: [new AST.Identifier({ line: 1,  name: 'this' })],
86
       }),
83
       }),
87
       elseCase: new AST.Application({
84
       elseCase: new AST.Application({
88
-        function: new AST.Identifier({ name: 'do' }),
89
-        args: [new AST.Identifier({ name: 'that' })],
85
+        function: new AST.Identifier({ line: 1,  name: 'do' }),
86
+        args: [new AST.Identifier({ line: 1,  name: 'that' })],
90
       }),
87
       }),
91
     }),
88
     }),
92
   ])
89
   ])

Loading…
Cancel
Save