Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle PanicSentinel in Interpreter #1436

Merged
merged 21 commits into from
Feb 2, 2021
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions distribution/std-lib/Base/src/Data/Number/Extensions.enso
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
from Base import all
polyglot java import java.lang.Math
polyglot java import java.lang.Double
polyglot java import java.lang.Integer as Java_Integer
polyglot java import java.lang.String

## An error that occurs when the text does not represent a valid number.
type Number_Parse_Error
4e6 marked this conversation as resolved.
Show resolved Hide resolved

## Computes the inverse of the sine function

Selects a value in the -pi/2 through pi/2 range.
Expand Down Expand Up @@ -98,7 +102,17 @@ Number.to_json = Json.Number this

## Parses a textual representation of a decimal into a decimal number.
Returns `Nothing` if the text does not represent a valid decimal.
Decimal.parse : Text -> Decimal | Nothing
Decimal.parse : Text -> Decimal ! Number_Parse_Error
Decimal.parse text =
Panic.recover (Double.parseDouble [text]) . catch (_ -> Nothing)

Panic.recover (Double.parseDouble [text]) . catch (_ -> Error.throw Number_Parse_Error)

## Parses a textual representation of an integer into an integer number.
Returns `Nothing` if the text does not represent a valid integer.
Integer.parse : Text -> Integer ! Number_Parse_Error
Integer.parse text =
Panic.recover (Java_Integer.parseInt [text]) . catch (_ -> Error.throw Number_Parse_Error)

## Parses a textual representation of a number into an integer or decimal number.
Returns `Nothing` if the text does not represent a valid number.
Number.parse : Text -> Number ! Number_Parse_Error
Number.parse text = Integer.parse text . catch (_ -> Decimal.parse text)
4e6 marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 2 additions & 2 deletions docs/language-server/protocol-language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ interface ExpressionUpdate {
An information about the computed value.

```typescript
type ExpressionUpdatePayload = Value | DatafalowError | RuntimeError;
type ExpressionUpdatePayload = Value | DatafalowError | Panic;

/**
* An empty payload. Indicates that the expression was computed to a value.
Expand All @@ -339,7 +339,7 @@ interface DataflowError {
/**
* Indicates that the expression failed with the runtime exception.
*/
interface RuntimeError {
interface Panic {
/**
* The error message.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,9 @@ final class ContextEventsListener(
case Api.ExpressionUpdate.Payload.DataflowError(trace) =>
ContextRegistryProtocol.ExpressionUpdate.Payload.DataflowError(trace)

case Api.ExpressionUpdate.Payload.RuntimeError(message, trace) =>
case Api.ExpressionUpdate.Payload.Panic(message, trace) =>
ContextRegistryProtocol.ExpressionUpdate.Payload
.RuntimeError(message, trace)
.Panic(message, trace)
}

/** Convert the runtime profiling info to the context registry protocol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ object ContextRegistryProtocol {
* @param message the error message
* @param trace the stack trace
*/
case class RuntimeError(
case class Panic(
message: String,
trace: Seq[UUID]
) extends Payload
Expand All @@ -159,7 +159,7 @@ object ContextRegistryProtocol {

val DataflowError = "DataflowError"

val RuntimeError = "RuntimeError"
val Panic = "Panic"

}

Expand All @@ -175,11 +175,11 @@ object ContextRegistryProtocol {
Json.obj(CodecField.Type -> PayloadType.DataflowError.asJson)
)

case m: Payload.RuntimeError =>
Encoder[Payload.RuntimeError]
case m: Payload.Panic =>
Encoder[Payload.Panic]
.apply(m)
.deepMerge(
Json.obj(CodecField.Type -> PayloadType.RuntimeError.asJson)
Json.obj(CodecField.Type -> PayloadType.Panic.asJson)
)
}

Expand All @@ -192,8 +192,8 @@ object ContextRegistryProtocol {
case PayloadType.DataflowError =>
Decoder[Payload.DataflowError].tryDecode(cursor)

case PayloadType.RuntimeError =>
Decoder[Payload.RuntimeError].tryDecode(cursor)
case PayloadType.Panic =>
Decoder[Payload.Panic].tryDecode(cursor)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ class ContextEventsListenerSpec
None,
Vector(),
false,
Api.ExpressionUpdate.Payload.RuntimeError("Method failure", Seq())
Api.ExpressionUpdate.Payload.Panic("Method failure", Seq())
)
)
)
Expand All @@ -199,7 +199,7 @@ class ContextEventsListenerSpec
Vector(),
false,
ContextRegistryProtocol.ExpressionUpdate.Payload
.RuntimeError("Method failure", Seq())
.Panic("Method failure", Seq())
)
),
None
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,8 +281,8 @@ object Runtime {
name = "expressionUpdatePayloadDataflowError"
),
new JsonSubTypes.Type(
value = classOf[Payload.RuntimeError],
name = "expressionUpdatePayloadRuntimeError"
value = classOf[Payload.Panic],
name = "expressionUpdatePayloadPanic"
)
)
)
Expand All @@ -305,7 +305,7 @@ object Runtime {
* @param message the error message
* @param trace the stack trace
*/
case class RuntimeError(
case class Panic(
message: String,
trace: Seq[ExpressionId]
) extends Payload
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ trait ProgramExecutionSupport {
val payload = value.getValue match {
case sentinel: PanicSentinel =>
Api.ExpressionUpdate.Payload
.RuntimeError(
.Panic(
sentinel.getMessage,
ErrorResolver.getStackTrace(sentinel).flatMap(_.expressionId)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,31 +238,31 @@ class ExpressionErrorsTest
Update.panic(
contextId,
xId,
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `undefined` is not defined.",
Seq(xId)
)
),
Update.panic(
contextId,
fooBodyId,
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `undefined` is not defined.",
Seq(xId)
)
),
Update.panic(
contextId,
yId,
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `undefined` is not defined.",
Seq(xId)
)
),
Update.panic(
contextId,
mainResId,
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `undefined` is not defined.",
Seq(xId)
)
Expand Down Expand Up @@ -331,7 +331,7 @@ class ExpressionErrorsTest
contextId,
mainBodyId,
Api.MethodPointer("Test.Main", "Test.Main", "foo"),
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `x` is not defined.",
Seq(mainBodyId)
)
Expand All @@ -340,6 +340,66 @@ class ExpressionErrorsTest
)
}

it should "return dataflow errors in method body" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val fooBodyId = metadata.addItem(61, 5)
val xId = metadata.addItem(75, 19)
val yId = metadata.addItem(103, 8)
val mainResId = metadata.addItem(116, 7)

val code =
"""from Builtins import all
|
|type MyError
|
|main =
| foo a b = a + b
| x = Error.throw MyError
| y = foo x 42
| foo y 1
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)

// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)

// Open the new file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, true))
)
context.receiveNone shouldEqual None

// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Test.Main", "main"),
None,
Vector()
)
)
)
)
context.receive(6) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
TestMessages.update(contextId, xId, Constants.ERROR),
TestMessages.update(contextId, fooBodyId, Constants.ERROR),
TestMessages.update(contextId, yId, Constants.ERROR),
TestMessages.update(contextId, mainResId, Constants.ERROR),
context.executionComplete(contextId)
)
}

it should "return panic sentinels continuing execution" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
Expand Down Expand Up @@ -409,7 +469,7 @@ class ExpressionErrorsTest
Update.panic(
contextId,
xId,
Api.ExpressionUpdate.Payload.RuntimeError(
Api.ExpressionUpdate.Payload.Panic(
"Compile_Error Variable `undefined` is not defined.",
Seq(xId)
)
Expand All @@ -421,4 +481,74 @@ class ExpressionErrorsTest
context.consumeOut shouldEqual Seq("42")
}

it should "return dataflow errors continuing execution" in {
val contextId = UUID.randomUUID()
val requestId = UUID.randomUUID()
val moduleName = "Test.Main"
val metadata = new Metadata
val xId = metadata.addItem(55, 19)
val yId = metadata.addItem(83, 2)
val mainResId = metadata.addItem(90, 12)

val code =
"""from Builtins import all
|
|type MyError
|
|main =
| x = Error.throw MyError
| y = 42
| IO.println y
|""".stripMargin.linesIterator.mkString("\n")
val contents = metadata.appendToCode(code)
val mainFile = context.writeMain(contents)

// create context
context.send(Api.Request(requestId, Api.CreateContextRequest(contextId)))
context.receive shouldEqual Some(
Api.Response(requestId, Api.CreateContextResponse(contextId))
)

// Open the new file
context.send(
Api.Request(Api.OpenFileNotification(mainFile, contents, true))
)
context.receiveNone shouldEqual None

// push main
context.send(
Api.Request(
requestId,
Api.PushContextRequest(
contextId,
Api.StackItem.ExplicitCall(
Api.MethodPointer(moduleName, "Test.Main", "main"),
None,
Vector()
)
)
)
)
context.receive(6) should contain theSameElementsAs Seq(
Api.Response(requestId, Api.PushContextResponse(contextId)),
Api.Response(
Api.ExecutionUpdate(
contextId,
Seq(
Api.ExecutionResult.Diagnostic.warning(
"Unused variable x.",
Some(mainFile),
Some(model.Range(model.Position(5, 4), model.Position(5, 5)))
)
)
)
),
TestMessages.update(contextId, xId, Constants.ERROR),
TestMessages.update(contextId, yId, Constants.INTEGER),
TestMessages.update(contextId, mainResId, Constants.NOTHING),
context.executionComplete(contextId)
)
context.consumeOut shouldEqual Seq("42")
}

}
26 changes: 25 additions & 1 deletion test/Tests/src/Data/Numbers_Spec.enso
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ spec =
negative_big_bits.bit_shift_r -100 . should_equal (negative_big_bits.bit_shift_l 100)
Test.expect_error_with (negative_big_bits.bit_shift_r negative_big_bits) Arithmetic_Error
negative_big_bits.bit_shift_r positive_big_bits . should_equal -1
Test.specify "should parse text as integers" <|
Integer.parse "-1" . to_text . should_equal "-1"
Integer.parse "-0" . to_text . should_equal "0"
Integer.parse "0" . to_text . should_equal "0"
Integer.parse "1" . to_text . should_equal "1"
Test.expect_error_with (Integer.parse "2.0") Number_Parse_Error
Test.expect_error_with (Integer.parse "3.4") Number_Parse_Error
Test.expect_error_with (Integer.parse "five") Number_Parse_Error
Test.group "Decimals" <|
Test.specify "should exist and expose basic arithmetic operations" <|
((1.5 + 1.5)*1.3 / 2 - 3) . should_equal -1.05 epsilon=eps
Expand Down Expand Up @@ -190,4 +198,20 @@ spec =
1.ceil . should_equal 1
almost_max_long_times_three_decimal.ceil.to_decimal . should_equal almost_max_long_times_three_plus_1.to_decimal
almost_max_long_times_three_plus_1.ceil . should_equal almost_max_long_times_three_plus_1

Test.specify "should parse text as decimals" <|
Decimal.parse "-1" . to_text . should_equal "-1.0"
Decimal.parse "-0" . to_text . should_equal "-0.0"
Decimal.parse "0" . to_text . should_equal "0.0"
Decimal.parse "1" . to_text . should_equal "1.0"
Decimal.parse "2.0" . to_text . should_equal "2.0"
Decimal.parse "3.4" . to_text . should_equal "3.4"
Test.expect_error_with (Decimal.parse "five") Number_Parse_Error
Test.group "Numbers" <|
Test.specify "should parse text as numbers" <|
Number.parse "-1" . to_text . should_equal "-1"
Number.parse "-0" . to_text . should_equal "0"
Number.parse "0" . to_text . should_equal "0"
Number.parse "1" . to_text . should_equal "1"
Number.parse "2.0" . to_text . should_equal "2.0"
4e6 marked this conversation as resolved.
Show resolved Hide resolved
Number.parse "3.4" . to_text . should_equal "3.4"
Test.expect_error_with (Number.parse "five") Number_Parse_Error