framestack reimplemented

This commit is contained in:
Vyacheslav Matyukhin 2022-10-05 04:51:23 +04:00
parent a764f3075c
commit 26dbd29ec8
No known key found for this signature in database
GPG Key ID: 3D2A774C5489F96C
18 changed files with 153 additions and 92 deletions

View File

@ -1,4 +1,4 @@
import { SqError, SqLocation } from "@quri/squiggle-lang";
import { SqError, SqFrame } from "@quri/squiggle-lang";
import React from "react";
import { ErrorAlert } from "./Alert";
@ -6,24 +6,26 @@ type Props = {
error: SqError;
};
const StackTraceLocation: React.FC<{ location: SqLocation }> = ({
location,
}) => {
const StackTraceFrame: React.FC<{ frame: SqFrame }> = ({ frame }) => {
const location = frame.location();
return (
<div>
Line {location.start.line}, column {location.start.column}
{frame.name()}
{location
? ` at line ${location.start.line}, column ${location.start.column}`
: ""}
</div>
);
};
const StackTrace: React.FC<Props> = ({ error }) => {
const locations = error.toLocationArray();
return locations.length ? (
const frames = error.getFrameArray();
return frames.length ? (
<div>
<div>Traceback:</div>
<div className="ml-4">
{locations.map((location, i) => (
<StackTraceLocation location={location} key={i} />
{frames.map((frame, i) => (
<StackTraceFrame frame={frame} key={i} />
))}
</div>
</div>

View File

@ -45,7 +45,7 @@ export function getValueToRender({ result, bindings }: ResultAndBindings) {
export function getErrorLocations(result: ResultAndBindings["result"]) {
if (result.tag === "Error") {
const location = result.value.toLocation();
const location = result.value.location();
return location ? [location] : [];
} else {
return [];

View File

@ -1,8 +1,9 @@
import * as RSError from "../rescript/SqError.gen";
import * as RSReducerT from "../rescript/Reducer/Reducer_T.gen";
import * as RSCallStack from "../rescript/Reducer/Reducer_CallStack.gen";
import * as RSFrameStack from "../rescript/Reducer/Reducer_FrameStack.gen";
export type SqFrame = RSCallStack.frame;
export { location as SqLocation } from "../rescript/Reducer/Reducer_Peggy/Reducer_Peggy_Parse.gen";
export class SqError {
constructor(private _value: RSError.t) {}
@ -19,17 +20,31 @@ export class SqError {
return new SqError(RSError.createOtherError(v));
}
getTopFrame(): SqCallFrame | undefined {
const frame = RSCallStack.getTopFrame(RSError.getStackTrace(this._value));
return frame ? new SqCallFrame(frame) : undefined;
getTopFrame(): SqFrame | undefined {
const frame = RSFrameStack.getTopFrame(RSError.getFrameStack(this._value));
return frame ? new SqFrame(frame) : undefined;
}
getFrameArray(): SqCallFrame[] {
getFrameArray(): SqFrame[] {
const frames = RSError.getFrameArray(this._value);
return frames.map((frame) => new SqCallFrame(frame));
return frames.map((frame) => new SqFrame(frame));
}
location() {
return this.getTopFrame()?.location();
}
}
export class SqCallFrame {
constructor(private _value: SqFrame) {}
export class SqFrame {
constructor(private _value: RSReducerT.frame) {}
name(): string {
return RSFrameStack.Frame.toName(this._value);
}
location() {
console.log(RSFrameStack);
console.log(RSFrameStack.Frame);
return RSFrameStack.Frame.toLocation(this._value);
}
}

View File

@ -1,4 +1,3 @@
import { isParenthesisNode } from "mathjs";
import { SqProject } from "./SqProject";
type PathItem = string | number;

View File

@ -13,7 +13,7 @@ export {
environment,
defaultEnvironment,
} from "../rescript/ForTS/ForTS_Distribution/ForTS_Distribution.gen";
export { SqError } from "./SqError";
export { SqError, SqFrame, SqLocation } from "./SqError";
export { SqShape } from "./SqPointSetDist";
export { resultMap } from "./types";

View File

@ -4,8 +4,13 @@ let defaultEnvironment: Reducer_T.environment = DistributionOperation.defaultEnv
let createContext = (stdLib: Reducer_Namespace.t, environment: Reducer_T.environment): t => {
{
callStack: list{},
frameStack: list{},
bindings: stdLib->Reducer_Bindings.fromNamespace->Reducer_Bindings.extend,
environment: environment,
inFunction: None,
}
}
let currentFunctionName = (t: t): string => {
t.inFunction->E.O2.fmap(Reducer_Lambda_T.name)->E.O2.default("<top>")
}

View File

@ -2,14 +2,16 @@ module Bindings = Reducer_Bindings
module Result = Belt.Result
module T = Reducer_T
let toLocation = (expression: T.expression): SqError.location => {
let toLocation = (expression: T.expression): Reducer_Peggy_Parse.location => {
expression.ast.location
}
let throwFrom = (error: SqError.Message.t, expression: T.expression, context: T.context) =>
error->SqError.throwMessage(
context.callStack,
location: Some(expression->toLocation)
error->SqError.throwMessageWithFrameStack(
context.frameStack->Reducer_FrameStack.extend(
context->Reducer_Context.currentFunctionName,
Some(expression->toLocation),
),
)
/*
@ -93,9 +95,9 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => {
}
}
| T.ELambda(parameters, body) => (
| T.ELambda(parameters, body, name) => (
Reducer_Lambda.makeLambda(
None, // TODO - pass function name from parser
name,
parameters,
context.bindings,
body,
@ -112,7 +114,13 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => {
})
switch lambda {
| T.IEvLambda(lambda) => {
let result = Reducer_Lambda.doLambdaCall(lambda, argValues, context, evaluate)
let result = Reducer_Lambda.doLambdaCallFrom(
lambda,
argValues,
context,
evaluate,
Some(expression->toLocation), // we have to pass the location of a current expression here, to put it on frameStack
)
(result, context)
}
| _ => RENotAFunction(lambda->Reducer_Value.toString)->throwFrom(expression, context)

View File

@ -9,10 +9,11 @@ let eBool = aBool => aBool->T.IEvBool->T.EValue
let eCall = (fn: expression, args: array<expression>): expressionContent => T.ECall(fn, args)
let eLambda = (parameters: array<string>, expr: expression): expressionContent => T.ELambda(
parameters,
expr,
)
let eLambda = (
parameters: array<string>,
expr: expression,
name: option<string>,
): expressionContent => T.ELambda(parameters, expr, name)
let eNumber = aNumber => aNumber->T.IEvNumber->T.EValue

View File

@ -24,7 +24,7 @@ let rec toString = (expression: t) =>
`${predicate->toString} ? (${trueCase->toString}) : (${falseCase->toString})`
| EAssign(name, value) => `${name} = ${value->toString}`
| ECall(fn, args) => `(${fn->toString})(${args->Js.Array2.map(toString)->commaJoin})`
| ELambda(parameters, body) => `{|${parameters->commaJoin}| ${body->toString}}`
| ELambda(parameters, body, _) => `{|${parameters->commaJoin}| ${body->toString}}`
| EValue(aValue) => Reducer_Value.toString(aValue)
}

View File

@ -1,37 +1,43 @@
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 toString = ({name, location}: Reducer_T.frame) =>
name ++
switch location {
| Some(location) =>
` at line ${location.start.line->Js.Int.toString}, column ${location.start.column->Js.Int.toString}`
| None => ""
}
@genType
let toLocation = (t: Reducer_T.frame): option<Reducer_Peggy_Parse.location> => t.location
@genType
let toName = (t: Reducer_T.frame): string => t.name
}
let make = (): t => list{}
let topFrameName = (t: t) =>
switch t->getTopFrame {
| Some({lambda}) =>
switch lambda {
| FnLambda({name}) => name
| FnBuiltin({name}) => name
}
| None => "<main>"
}
@genType
let getTopFrame = (t: t): option<Reducer_T.frame> => Belt.List.head(t)
let extend = (t: t, lambda: Reducer_T.lambdaValue, location: option<location>) =>
let extend = (t: t, name: string, location: option<Reducer_Peggy_Parse.location>) =>
t->Belt.List.add({
lambda: lambda,
fromLocation: location,
fromFrame: t->topFrameName,
name: name,
location: location,
})
let toString = (t: t) =>
t->Belt.List.map(s => " " ++ s->toStringFrame ++ "\n")->Belt.List.toArray->Js.Array2.joinWith("")
t
->Belt.List.map(s => " " ++ s->Frame.toString ++ "\n")
->Belt.List.toArray
->Js.Array2.joinWith("")
@genType
let toFrameArray = (t: t): array<frame> => t->Belt.List.toArray
let toFrameArray = (t: t): array<Reducer_T.frame> => t->Belt.List.toArray
@genType
let getTopFrame = (t: t): option<frame> => t->Belt.List.head
let getTopFrame = (t: t): option<Reducer_T.frame> => t->Belt.List.head
let isEmpty = (t: t): bool =>
switch t->Belt.List.head {

View File

@ -33,7 +33,8 @@ let makeLambda = (
let lambdaContext: Reducer_T.context = {
bindings: localBindingsWithParameters, // based on bindings at the moment of lambda creation
environment: context.environment, // environment at the moment when lambda is called
callStack: context.callStack, // extended by main `evaluate` function
frameStack: context.frameStack, // already extended in `doLambdaCall`
inFunction: context.inFunction, // already updated in `doLambdaCall`
}
let (value, _) = reducer(body, lambdaContext)
@ -49,7 +50,7 @@ let makeLambda = (
})
}
// stdlib lambdas (everything in FunctionRegistry) is built by this method. Body is generated in SquiggleLibrary_StdLib.res
// stdlib functions (everything in FunctionRegistry) are built by this method. Body is generated in SquiggleLibrary_StdLib.res
let makeFFILambda = (name: string, body: Reducer_T.lambdaBody): t => FnBuiltin({
// Note: current bindings could be accidentally exposed here through context (compare with native lambda implementation above, where we override them with local bindings).
// But FunctionRegistry API is too limited for that to matter. Please take care not to violate that in the future by accident.
@ -65,10 +66,20 @@ let parameters = (t: t): array<string> => {
}
}
let doLambdaCall = (t: t, args, context: Reducer_Context.t, reducer) => {
let doLambdaCallFrom = (
t: t,
args: array<Reducer_T.value>,
context: Reducer_T.context,
reducer,
location: option<Reducer_Peggy_Parse.location>,
) => {
let newContext = {
...context,
callStack: t->Reducer_CallStack.extend(t),
frameStack: context.frameStack->Reducer_FrameStack.extend(
context->Reducer_Context.currentFunctionName,
location,
),
inFunction: Some(t),
}
SqError.rethrowWithStacktrace(() => {
@ -76,5 +87,9 @@ let doLambdaCall = (t: t, args, context: Reducer_Context.t, reducer) => {
| FnLambda({body}) => body(args, newContext, reducer)
| FnBuiltin({body}) => body(args, newContext, reducer)
}
}, newContext.callStack)
}, newContext.frameStack)
}
let doLambdaCall = (t: t, args, context, reducer) => {
doLambdaCallFrom(t, args, context, reducer, None)
}

View File

@ -0,0 +1,8 @@
type t = Reducer_T.lambdaValue
let name = (t: t): string => {
switch t {
| FnLambda({name}) => name->E.O2.default("<anonymous>")
| FnBuiltin({name}) => name
}
}

View File

@ -50,7 +50,7 @@ letStatement
defunStatement
= variable:variable '(' _nl args:array_parameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression
{ var value = h.nodeLambda(args, body, location())
{ var value = h.nodeLambda(args, body, location(), variable)
return h.nodeLetStatement(variable, value, location()) }
assignmentOp "assignment" = '='
@ -261,9 +261,9 @@ valueConstructor
lambda
= '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
{ statements.push(finalExpression)
return h.nodeLambda(args, h.nodeBlock(statements, location()), location()) }
return h.nodeLambda(args, h.nodeBlock(statements, location()), location(), undefined) }
/ '{' _nl '|' _nl args:array_parameters _nl '|' _nl finalExpression: expression _nl '}'
{ return h.nodeLambda(args, finalExpression, location()) }
{ return h.nodeLambda(args, finalExpression, location(), undefined) }
arrayConstructor 'array'
= '[' _nl ']'

View File

@ -44,7 +44,7 @@ type nodeIdentifier = {...node, "value": string}
type nodeInteger = {...node, "value": int}
type nodeKeyValue = {...node, "key": node, "value": node}
type nodeRecord = {...node, "elements": array<nodeKeyValue>}
type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": node}
type nodeLambda = {...node, "args": array<nodeIdentifier>, "body": node, "name": option<string>}
type nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node}
type nodeModuleIdentifier = {...node, "value": string}
type nodeString = {...node, "value": string}

View File

@ -20,7 +20,7 @@ let rec fromNode = (node: Parse.node): expression => {
nodeLambda["args"]->Js.Array2.map((argNode: Parse.nodeIdentifier) => argNode["value"])
let body = nodeLambda["body"]->fromNode
ExpressionBuilder.eLambda(args, body)
ExpressionBuilder.eLambda(args, body, nodeLambda["name"])
}
let caseRecord = (nodeRecord): expressionContent => {

View File

@ -89,6 +89,7 @@ type NodeLambda = Node & {
type: "Lambda";
args: AnyPeggyNode[];
body: AnyPeggyNode;
name?: string;
};
type NodeTernary = Node & {
@ -217,9 +218,10 @@ export function nodeKeyValue(
export function nodeLambda(
args: AnyPeggyNode[],
body: AnyPeggyNode,
location: LocationRange
location: LocationRange,
name?: NodeIdentifier
): NodeLambda {
return { type: "Lambda", args, body, location };
return { type: "Lambda", args, body, location, name: name?.value };
}
export function nodeLetStatement(
variable: NodeIdentifier,

View File

@ -36,7 +36,7 @@ and expressionContent =
| ETernary(expression, expression, expression)
| EAssign(string, expression)
| ECall(expression, array<expression>)
| ELambda(array<string>, expression)
| ELambda(array<string>, expression, option<string>)
| EValue(value)
and expression = {
@ -50,18 +50,18 @@ and bindings = {
parent: option<bindings>,
}
@genType.opaque
and frame = {
name: string,
location: option<Reducer_Peggy_Parse.location>, // can be empty for calls from builtin functions
}
and frameStack = list<frame>
@genType.opaque and frameStack = list<frame>
and context = {
bindings: bindings,
environment: environment,
inFunction: option<string>, // used to build the next frame in frameStack
callStack: frameStack,
frameStack: frameStack,
inFunction: option<lambdaValue>,
}
and reducerFn = (expression, context) => (value, context)

View File

@ -95,19 +95,19 @@ type 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, frameStack: Reducer_FrameStack.t): t => {
let fromMessageWithFrameStack = (message: Message.t, frameStack: Reducer_FrameStack.t): t => {
message: message,
stackTrace: stackTrace,
frameStack: frameStack,
}
let fromMessageWithoutFrameStack = (message: Message.t) =>
fromMessage(message, Reducer_FrameStack.make())
// this shouldn't be used much, since frame stack will be empty
// but it's useful for global errors, e.g. in ReducerProject or somethere in the frontend
@genType
let fromMessage = (message: Message.t) =>
fromMessageWithFrameStack(message, Reducer_FrameStack.make())
@genType
let getTopFrame = (t: t): option<Reducer_CallStack.frame> =>
t.stackTrace->Reducer_FrameStack.getTopFrame
let getTopFrame = (t: t): option<Reducer_T.frame> => t.frameStack->Reducer_FrameStack.getTopFrame
@genType
let getFrameStack = (t: t): Reducer_FrameStack.t => t.frameStack
@ -116,42 +116,42 @@ let getFrameStack = (t: t): Reducer_FrameStack.t => t.frameStack
let toString = (t: t): string => t.message->Message.toString
@genType
let createOtherError = (v: string): t => Message.REOther(v)->fromMessageWithoutFrameStack
let createOtherError = (v: string): t => Message.REOther(v)->fromMessage
@genType
let getFrameArray = (t: t): array<Reducer_CallStack.frame> =>
t.stackTrace->Reducer_CallStack.toFrameArray
let getFrameArray = (t: t): array<Reducer_T.frame> => t.frameStack->Reducer_FrameStack.toFrameArray
@genType
let toStringWithStacktrace = (t: t) =>
let toStringWithStackTrace = (t: t) =>
t->toString ++ if t.frameStack->Reducer_FrameStack.isEmpty {
"Traceback:\n" ++ t.frameStack->Reducer_FrameStack.toString
"\nTraceback:\n" ++ t.frameStack->Reducer_FrameStack.toString
} else {
""
}
let throw = (t: t) => t->SqException->raise
let throwMessage = (message: Message.t, frameStack: Reducer_FrameStack.t) =>
fromMessage(message, frameStack)->throw
let throwMessageWithFrameStack = (message: Message.t, frameStack: Reducer_FrameStack.t) =>
fromMessageWithFrameStack(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(Reducer_CallStack.make())
| Js.Exn.Error(obj) =>
REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessageWithoutStacktrace
| _ => REOther("Unknown exception")->fromMessageWithoutStacktrace
| Message.MessageException(e) => e->fromMessage
| Js.Exn.Error(obj) => REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessage
| _ => REOther("Unknown exception")->fromMessage
}
// converts raw exceptions into exceptions with stacktrace attached
// already converted exceptions won't be affected
let rethrowWithStacktrace = (fn: unit => 'a, stackTrace: Reducer_CallStack.t) => {
let rethrowWithStacktrace = (fn: unit => 'a, frameStack: Reducer_FrameStack.t) => {
try {
fn()
} catch {
| SqException(e) => e->throw // exception already has a stacktrace
| Message.MessageException(e) => e->throwMessage(stackTrace) // probably comes from FunctionRegistry, adding stacktrace
| Message.MessageException(e) => e->throwMessageWithFrameStack(frameStack) // probably comes from FunctionRegistry, adding stacktrace
| Js.Exn.Error(obj) =>
REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessage(stackTrace)
| _ => REOther("Unknown exception")->throwMessage(stackTrace)
REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessageWithFrameStack(frameStack)
| _ => REOther("Unknown exception")->throwMessageWithFrameStack(frameStack)
}
}