diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_CallStack.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_CallStack.res deleted file mode 100644 index 520ce6eb..00000000 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_CallStack.res +++ /dev/null @@ -1,36 +0,0 @@ -@genType.opaque -type rec lambdaFrame = {location: Reducer_Peggy_Parse.location, name: string} -@genType.opaque and ffiFrame = {name: string} -@genType -and frame = - | InLambda(lambdaFrame) - | InFFI(ffiFrame) - -let toStringFrame = (t: frame) => - switch t { - | InLambda({location}) => - `Line ${location.start.line->Js.Int.toString}, column ${location.start.column->Js.Int.toString}, source ${location.source}` - | InFFI({name}) => `Builtin ${name}` - } - -@genType.opaque -type rec t = list - -let make = (): t => list{} - -let extend = (t: t, frame: frame) => t->Belt.List.add(frame) - -let toString = (t: t) => - t->Belt.List.map(s => " " ++ s->toStringFrame ++ "\n")->Belt.List.toArray->Js.Array2.joinWith("") - -@genType -let toFrameArray = (t: t): array => t->Belt.List.toArray - -@genType -let getTopFrame = (t: t): option => t->Belt.List.head - -let isEmpty = (t: t): bool => - switch t->Belt.List.head { - | Some(_) => true - | None => false - } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res index 030ca71d..f334335a 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Expression/Reducer_Expression.res @@ -6,8 +6,11 @@ let toLocation = (expression: T.expression): SqError.location => { expression.ast.location } -let throwFrom = (error: SqError.Message.t, context: T.context) => - error->SqError.throwMessage(context) +let throwFrom = (error: SqError.Message.t, expression: T.expression, context: T.context) => + error->SqError.throwMessage( + context.callStack, + location: Some(expression->toLocation) + ) /* Recursively evaluate the expression @@ -53,7 +56,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => { let (key, _) = eKey->evaluate(context) let keyString = switch key { | IEvString(s) => s - | _ => REOther("Record keys must be strings")->throwFrom(context) + | _ => REOther("Record keys must be strings")->throwFrom(expression, context) } let (value, _) = eValue->evaluate(context) (keyString, value) @@ -77,7 +80,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => { | T.ESymbol(name) => switch context.bindings->Bindings.get(name) { | Some(v) => (v, context) - | None => RESymbolNotFound(name)->throwFrom(context) + | None => RESymbolNotFound(name)->throwFrom(expression, context) } | T.EValue(value) => (value, context) @@ -86,7 +89,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => { let (predicateResult, _) = predicate->evaluate(context) switch predicateResult { | T.IEvBool(value) => (value ? trueCase : falseCase)->evaluate(context) - | _ => REExpectedType("Boolean", "")->throwFrom(context) + | _ => REExpectedType("Boolean", "")->throwFrom(expression, context) } } @@ -112,7 +115,7 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => { let result = Reducer_Lambda.doLambdaCall(lambda, argValues, context, evaluate) (result, context) } - | _ => RENotAFunction(lambda->Reducer_Value.toString)->throwFrom(context) + | _ => RENotAFunction(lambda->Reducer_Value.toString)->throwFrom(expression, context) } } } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_FrameStack.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_FrameStack.res new file mode 100644 index 00000000..217993f0 --- /dev/null +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_FrameStack.res @@ -0,0 +1,40 @@ +type t = Reducer_T.frameStack + +module Frame = { + let toString = ({lambda, location}: Reducer_T.frame) => + `${fromFrame} at ${location.start.line->Js.Int.toString}, column ${location.start.column->Js.Int.toString}` +} + +let make = (): t => list{} + +let topFrameName = (t: t) => + switch t->getTopFrame { + | Some({lambda}) => + switch lambda { + | FnLambda({name}) => name + | FnBuiltin({name}) => name + } + | None => "
" + } + +let extend = (t: t, lambda: Reducer_T.lambdaValue, location: option) => + t->Belt.List.add({ + lambda: lambda, + fromLocation: location, + fromFrame: t->topFrameName, + }) + +let toString = (t: t) => + t->Belt.List.map(s => " " ++ s->toStringFrame ++ "\n")->Belt.List.toArray->Js.Array2.joinWith("") + +@genType +let toFrameArray = (t: t): array => t->Belt.List.toArray + +@genType +let getTopFrame = (t: t): option => t->Belt.List.head + +let isEmpty = (t: t): bool => + switch t->Belt.List.head { + | Some(_) => true + | None => false + } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Lambda.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Lambda.res index 81e30cfb..04adeea0 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_Lambda.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_Lambda.res @@ -57,14 +57,6 @@ let makeFFILambda = (name: string, body: Reducer_T.lambdaBody): t => FnBuiltin({ name: name, }) -let extendCallStack = (t: t, callStack: Reducer_CallStack.t): Reducer_CallStack.t => { - switch t { - | FnLambda({location}) => - callStack->Reducer_CallStack.extend(InLambda({location: location, name: "TODO"})) // FIXME - | FnBuiltin({name}) => callStack->Reducer_CallStack.extend(InFFI({name: name})) - } -} - // this function doesn't scale to FunctionRegistry's polymorphic functions let parameters = (t: t): array => { switch t { @@ -76,13 +68,13 @@ let parameters = (t: t): array => { let doLambdaCall = (t: t, args, context: Reducer_Context.t, reducer) => { let newContext = { ...context, - callStack: t->extendCallStack(context.callStack), + callStack: t->Reducer_CallStack.extend(t), } - SqError.contextualizeAndRethrow(() => { + SqError.rethrowWithStacktrace(() => { switch t { | FnLambda({body}) => body(args, newContext, reducer) | FnBuiltin({body}) => body(args, newContext, reducer) } - }, newContext) + }, newContext.callStack) } diff --git a/packages/squiggle-lang/src/rescript/Reducer/Reducer_T.res b/packages/squiggle-lang/src/rescript/Reducer/Reducer_T.res index cb0edfd2..eab7bd55 100644 --- a/packages/squiggle-lang/src/rescript/Reducer/Reducer_T.res +++ b/packages/squiggle-lang/src/rescript/Reducer/Reducer_T.res @@ -50,10 +50,18 @@ and bindings = { parent: option, } +and frame = { + name: string, + location: option, // can be empty for calls from builtin functions +} + +and frameStack = list + and context = { bindings: bindings, environment: environment, - callStack: Reducer_CallStack.t, + inFunction: option, // used to build the next frame in frameStack + callStack: frameStack, } and reducerFn = (expression, context) => (value, context) diff --git a/packages/squiggle-lang/src/rescript/SqError.res b/packages/squiggle-lang/src/rescript/SqError.res index 3cf3dfe2..461e1feb 100644 --- a/packages/squiggle-lang/src/rescript/SqError.res +++ b/packages/squiggle-lang/src/rescript/SqError.res @@ -90,91 +90,68 @@ module Message = { @genType.opaque type t = { message: Message.t, - /* - Errors raised from internal functions can have empty location. - - Also, location is not the same as the top of the stackTrace. - Consider this: - ``` - f() = { - x = 5 - y = z // no such var - x + y - } - ``` - This code should report the location of assignment issue, but there's no function call there. - */ - location: option, - stackTrace: Reducer_CallStack.t, + frameStack: Reducer_FrameStack.t, } exception SqException(t) // `context` should be specified for runtime errors, but can be left empty for errors from Reducer_Project and so on. // `location` can be empty for errors raised from FunctionRegistry. -let fromMessage = ( - message: Message.t, - context: option, - location: option, -): t => { +let fromMessage = (message: Message.t, frameStack: Reducer_FrameStack.t): t => { message: message, - location: location, - stackTrace: switch context { - | Some(context) => context.callStack - | None => Reducer_CallStack.make() - }, + stackTrace: stackTrace, } +let fromMessageWithoutFrameStack = (message: Message.t) => + fromMessage(message, Reducer_FrameStack.make()) + @genType let getTopFrame = (t: t): option => - t.stackTrace->Reducer_CallStack.getTopFrame + t.stackTrace->Reducer_FrameStack.getTopFrame @genType -let getStackTrace = (t: t): Reducer_CallStack.t => t.stackTrace +let getFrameStack = (t: t): Reducer_FrameStack.t => t.frameStack @genType let toString = (t: t): string => t.message->Message.toString @genType -let createOtherError = (v: string): t => Message.REOther(v)->fromMessage() +let createOtherError = (v: string): t => Message.REOther(v)->fromMessageWithoutFrameStack @genType let getFrameArray = (t: t): array => t.stackTrace->Reducer_CallStack.toFrameArray @genType -let toStringWithStackTrace = (t: t) => - if t.stackTrace->Reducer_CallStack.isEmpty { - "Traceback:\n" ++ t.stackTrace->Reducer_CallStack.toString - } else { - "" - } ++ - t->toString - +let toStringWithStacktrace = (t: t) => + t->toString ++ if t.frameStack->Reducer_FrameStack.isEmpty { + "Traceback:\n" ++ t.frameStack->Reducer_FrameStack.toString + } let throw = (t: t) => t->SqException->raise -let throwMessage = (message: Message.t, context: Reducer_T.context, location: location) => - fromMessage(message, context, location)->throw +let throwMessage = (message: Message.t, frameStack: Reducer_FrameStack.t) => + fromMessage(message, frameStack)->throw // this shouldn't be used for most runtime errors - the resulting error would have an empty stacktrace let fromException = exn => switch exn { | SqException(e) => e - | Message.MessageException(e) => e->fromMessage - | Js.Exn.Error(obj) => REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessage - | _ => REOther("Unknown exception")->fromMessage + | Message.MessageException(e) => e->fromMessage(Reducer_CallStack.make()) + | Js.Exn.Error(obj) => + REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessageWithoutStacktrace + | _ => REOther("Unknown exception")->fromMessageWithoutStacktrace } // converts raw exceptions into exceptions with stacktrace attached // already converted exceptions won't be affected -let contextualizeAndRethrow = (fn: unit => 'a, context: Reducer_T.context) => { +let rethrowWithStacktrace = (fn: unit => 'a, stackTrace: Reducer_CallStack.t) => { try { fn() } catch { - | SqException(e) => e->throw - | Message.MessageException(e) => e->throwMessage(context) + | SqException(e) => e->throw // exception already has a stacktrace + | Message.MessageException(e) => e->throwMessage(stackTrace) // probably comes from FunctionRegistry, adding stacktrace | Js.Exn.Error(obj) => - REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessage(context) - | _ => REOther("Unknown exception")->throwMessage(context) + REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessage(stackTrace) + | _ => REOther("Unknown exception")->throwMessage(stackTrace) } }