Browse Source

Fix error handling and bug where let bindings stay in scope

master
Dylan Baker 2 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,6 +5,7 @@ const path = require('path')
5 5
 const args = require('minimist')(process.argv.slice(2))
6 6
 
7 7
 const oslo = require('../src/index')
8
+const OsloError = require('../src/osloError')
8 9
 
9 10
 class OsloCLI {
10 11
   constructor(opts) {
@@ -39,8 +40,8 @@ class OsloCLI {
39 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 47
     if (opts.o) {

+ 14
- 6
src/env.js View File

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

+ 9
- 0
src/index.js View File

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

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

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

+ 4
- 2
src/parser.js View File

@@ -1,5 +1,5 @@
1 1
 const AST = require('./ast')
2
-const Error = require('./Error')
2
+const OsloError = require('./OsloError')
3 3
 const tokenTypes = require('./tokenTypes')
4 4
 
5 5
 module.exports = class Parser {
@@ -106,8 +106,10 @@ module.exports = class Parser {
106 106
   }
107 107
 
108 108
   identifier() {
109
+    let token = this.tokenStream.eat(tokenTypes.IDENTIFIER)
109 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,4 +1,4 @@
1
-const Error = require('./error')
1
+const OsloError = require('./osloError')
2 2
 
3 3
 module.exports = class TokenStream {
4 4
   constructor() {
@@ -18,7 +18,7 @@ module.exports = class TokenStream {
18 18
       return token
19 19
     }
20 20
 
21
-    this.error = new Error({
21
+    this.error = new OsloError({
22 22
       line: token.line,
23 23
       message: `Encountered an unexpected ${
24 24
         token.type

+ 8
- 0
test/evaluatorTest.js View File

@@ -3,6 +3,7 @@ const helpers = require('./helpers')
3 3
 
4 4
 const AST = require('../src/ast')
5 5
 const Evaluator = require('../src/evaluator')
6
+const OsloError = require('../src/osloError')
6 7
 
7 8
 test('applies functions from core library', t => {
8 9
   t.plan(1)
@@ -47,3 +48,10 @@ test('let bindings', t => {
47 48
   const tree = helpers.evaluate('(let ((x 1) (y 2)) (+ x y))')
48 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,5 +23,3 @@ module.exports = {
23 23
   parse: parse,
24 24
   scan: scan,
25 25
 }
26
-
27
-console.log(evaluate('(+ 1 2)'))

+ 14
- 17
test/parserTest.js View File

@@ -8,29 +8,26 @@ test('parses token stream into a tree', t => {
8 8
   t.plan(1)
9 9
   const tree = helpers.parse(`
10 10
     (div :class "foobar"
11
-      (p :class (cond #t "primary" "secondary")))
11
+      (p :class (if #t "primary" "secondary")))
12 12
   `)
13 13
 
14 14
   t.deepEqual(tree, [
15 15
     new AST.Application({
16
-      function: new AST.Identifier({ name: 'div' }),
16
+      function: new AST.Identifier({name: 'div', line: 2 }),
17 17
       args: [
18 18
         new AST.Attribute({
19 19
           name: 'class',
20 20
           value: new AST.String({ value: 'foobar' }),
21 21
         }),
22 22
         new AST.Application({
23
-          function: new AST.Identifier({ name: 'p' }),
23
+          function: new AST.Identifier({ name: 'p', line: 3 }),
24 24
           args: [
25 25
             new AST.Attribute({
26 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,7 +43,7 @@ test('allow empty strings', t => {
46 43
 
47 44
   t.deepEqual(tree, [
48 45
     new AST.Application({
49
-      function: new AST.Identifier({ name: 'p' }),
46
+      function: new AST.Identifier({ name: 'p', line: 1 }),
50 47
       args: [new AST.String({ value: '' })],
51 48
     }),
52 49
   ])
@@ -61,9 +58,9 @@ test('parse lambdas and expressions in function position', t => {
61 58
       function: new AST.Lambda({
62 59
         parameters: [new AST.Identifier({ name: 'n' })],
63 60
         body: new AST.Application({
64
-          function: new AST.Identifier({ name: '+' }),
61
+          function: new AST.Identifier({ name: '+', line: 1 }),
65 62
           args: [
66
-            new AST.Identifier({ name: 'n' }),
63
+            new AST.Identifier({ name: 'n', line: 1 }),
67 64
             new AST.Number({ value: 1 }),
68 65
           ],
69 66
         }),
@@ -81,12 +78,12 @@ test('parse conditionals', t => {
81 78
     new AST.Conditional({
82 79
       condition: new AST.Boolean({ value: true }),
83 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 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