还记得我们之前返回的所有 NULL 并且我说你不应该担心,我们会回来的吗? 我们到了。 是时候在 Monkey 中实现一些真正的错误处理了,以免为时已晚,我们不得不退缩太多。 诚然,我们必须稍微退一步并纠正以前的代码,但不多。 我们没有在解释器中首先实现错误处理,因为老实说,我认为首先实现表达式比错误处理有趣得多。 但是我们现在需要添加它,否则在不久的将来调试和使用我们的解释器会变得太麻烦。
首先,让我们定义“真正的错误处理”是什么意思。 它不是用户定义的异常。 这是内部错误处理。 错误的操作符、不受支持的操作以及在执行过程中可能出现的其他用户或内部错误的错误。
至于此类错误的实现:这听起来可能很奇怪,但错误处理的实现方式几乎与处理 return 语句的方式相同。 这种相似性的原因很容易找到:errors 和 return 语句都停止了对一系列语句的评估。
我们需要的第一件事是一个错误对象:
// object/object.go
const (
// [...]
ERROR_OBJ = "ERROR"
)
type Error struct {
Message string
}
func (e *Error) Type() ObjectType { return ERROR_OBJ }
func (e *Error) Inspect() string { return "ERROR: " + e.Message }
如您所见, object.Error 非常非常简单。 它只包装一个用作错误消息的字符串。 在生产就绪的解释器中,我们希望将堆栈跟踪附加到此类错误对象,添加其来源的行号和列号,并提供的不仅仅是一条消息。 这并不难,只要行号和列号由词法分析器附加到标记上。 由于我们的词法分析器没有这样做,为了简单起见,我们只使用了一个错误消息,它通过给我们一些反馈和停止执行仍然对我们有很大帮助。
我们现在将在几个地方添加对错误的支持。 稍后,随着我们解释器能力的增强,我们将在适当的地方添加更多。 现在,这个测试函数显示了我们期望错误处理做什么:
// evaluator/evaluator_test.go
func TestErrorHandling(t *testing.T) {
tests := []struct {
input string
expectedMessage string
}{
{
"5 + true;",
"type mismatch: INTEGER + BOOLEAN",
},
{
"5 + true; 5;",
"type mismatch: INTEGER + BOOLEAN",
},
{
"-true",
"unknown operator: -BOOLEAN",
},
{
"true + false;",
"unknown operator: BOOLEAN + BOOLEAN",
},
{
"5; true + false; 5",
"unknown operator: BOOLEAN + BOOLEAN",
},
{
"if (10 > 1) { true + false; }",
"unknown operator: BOOLEAN + BOOLEAN",
},
{
`
if (10 > 1) {
if (10 > 1) {
return true + false;
}
return 1;
}
`,
"unknown operator: BOOLEAN + BOOLEAN",
},
}
for _, tt := range tests {
evaluated := testEval(tt.input)
errObj, ok := evaluated.(*object.Error)
if !ok {
t.Errorf("no error object returned. got=%T(%+v)",
evaluated, evaluated)
continue
}
if errObj.Message != tt.expectedMessage {
t.Errorf("wrong error message. expected=%q, got=%q",
tt.expectedMessage, errObj.Message)
}
}
}
当我们运行测试时,我们又遇到了我们的老朋友 NULL:
$ go test ./evaluator
--- FAIL: TestErrorHandling (0.00s)
evaluator_test.go:193: no error object returned. got=*object.Null(&{})
evaluator_test.go:193: no error object returned.\
got=*object.Integer(&{Value:5})
evaluator_test.go:193: no error object returned. got=*object.Null(&{})
evaluator_test.go:193: no error object returned. got=*object.Null(&{})
evaluator_test.go:193: no error object returned.\
got=*object.Integer(&{Value:5})
evaluator_test.go:193: no error object returned. got=*object.Null(&{})
evaluator_test.go:193: no error object returned.\
got=*object.Integer(&{Value:10})
FAIL
FAIL monkey/evaluator 0.007s
但也有意想不到的 *object.Integers。 这是因为这些测试用例实际上断言了两件事:错误是为不受支持的操作创建的,错误会阻止任何进一步的评估。 当测试因返回 *object.Integer 而失败时,评估没有正确停止。
创建错误并在 Eval 中传递它们很容易。 我们只需要一个辅助函数来帮助我们创建新的 *object.Errors 并在我们认为应该时返回它们:
// evaluator/evaluator.go
func newError(format string, a ...interface{}) *object.Error {
return &object.Error{Message: fmt.Sprintf(format, a...)}
}
这个 newError 函数在我们之前不知道该做什么的每个地方都有用处,而是返回 NULL:
// evaluator/evaluator.go
func evalPrefixExpression(operator string, right object.Object) object.Object {
switch operator {
// [...]
default:
return newError("unknown operator: %s%s", operator, right.Type())
}
}
func evalInfixExpression(
operator string,
left, right object.Object,
) object.Object {
switch {
// [...]
case left.Type() != right.Type():
return newError("type mismatch: %s %s %s",
left.Type(), operator, right.Type())
default:
return newError("unknown operator: %s %s %s",
left.Type(), operator, right.Type())
}
}
func evalMinusPrefixOperatorExpression(right object.Object) object.Object {
if right.Type() != object.INTEGER_OBJ {
return newError("unknown operator: -%s", right.Type())
}
// [...]
}
func evalIntegerInfixExpression(
operator string,
left, right object.Object,
) object.Object {
// [...]
switch operator {
// [...]
default:
return newError("unknown operator: %s %s %s",
left.Type(), operator, right.Type())
}
}
通过这些更改,失败的测试用例数量减少到只有两个:
$ go test ./evaluator
--- FAIL: TestErrorHandling (0.00s)
evaluator_test.go:193: no error object returned.\
got=*object.Integer(&{Value:5})
evaluator_test.go:193: no error object returned.\
got=*object.Integer(&{Value:5})
FAIL
FAIL monkey/evaluator 0.007s
该输出告诉我们创建错误不会造成问题,但停止评估仍然存在。 我们已经知道去哪里看,不是吗? 是的,没错:evalProgram 和 evalBlockStatement。 这是两个函数的全部内容,新增了对错误处理的支持:
// evaluator/evaluator.go
func evalProgram(program *ast.Program) object.Object {
var result object.Object
for _, statement := range program.Statements {
result = Eval(statement)
switch result := result.(type) {
case *object.ReturnValue:
return result.Value
case *object.Error:
return result
}
}
return result
}
func evalBlockStatement(block *ast.BlockStatement) object.Object {
var result object.Object
for _, statement := range block.Statements {
result = Eval(statement)
if result != nil {
rt := result.Type()
if rt == object.RETURN_VALUE_OBJ || rt == object.ERROR_OBJ {
return result
}
}
}
return result
}
做到了。 评估在正确的地方停止,测试现在通过:
$ go test ./evaluator
ok monkey/evaluator 0.010s
我们还需要做最后一件事。 每当我们在 Eval 内部调用 Eval 时,我们都需要检查错误,以防止错误被传递,然后在远离它们的源头的地方冒泡:
// evaluator/evaluator.go
func isError(obj object.Object) bool {
if obj != nil {
return obj.Type() == object.ERROR_OBJ
}
return false
}
func Eval(node ast.Node) object.Object {
switch node := node.(type) {
// [...]
case *ast.ReturnStatement:
val := Eval(node.ReturnValue)
if isError(val) {
return val
}
return &object.ReturnValue{Value: val}
// [...]
case *ast.PrefixExpression:
right := Eval(node.Right)
if isError(right) {
return right
}
return evalPrefixExpression(node.Operator, right)
case *ast.InfixExpression:
left := Eval(node.Left)
if isError(left) {
return left
}
right := Eval(node.Right)
if isError(right) {
return right
}
return evalInfixExpression(node.Operator, left, right)
// [...]
}
func evalIfExpression(ie *ast.IfExpression) object.Object {
condition := Eval(ie.Condition)
if isError(condition) {
return condition
}
// [...]
}
就是这样。 错误处理到位。
< 3.7return语句 | > 3.9绑定和环境 |
---|