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

View File

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

View File

@ -1,8 +1,9 @@
import * as RSError from "../rescript/SqError.gen"; 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 { export class SqError {
constructor(private _value: RSError.t) {} constructor(private _value: RSError.t) {}
@ -19,17 +20,31 @@ export class SqError {
return new SqError(RSError.createOtherError(v)); return new SqError(RSError.createOtherError(v));
} }
getTopFrame(): SqCallFrame | undefined { getTopFrame(): SqFrame | undefined {
const frame = RSCallStack.getTopFrame(RSError.getStackTrace(this._value)); const frame = RSFrameStack.getTopFrame(RSError.getFrameStack(this._value));
return frame ? new SqCallFrame(frame) : undefined; return frame ? new SqFrame(frame) : undefined;
} }
getFrameArray(): SqCallFrame[] { getFrameArray(): SqFrame[] {
const frames = RSError.getFrameArray(this._value); 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 { export class SqFrame {
constructor(private _value: 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"; import { SqProject } from "./SqProject";
type PathItem = string | number; type PathItem = string | number;

View File

@ -13,7 +13,7 @@ export {
environment, environment,
defaultEnvironment, defaultEnvironment,
} from "../rescript/ForTS/ForTS_Distribution/ForTS_Distribution.gen"; } from "../rescript/ForTS/ForTS_Distribution/ForTS_Distribution.gen";
export { SqError } from "./SqError"; export { SqError, SqFrame, SqLocation } from "./SqError";
export { SqShape } from "./SqPointSetDist"; export { SqShape } from "./SqPointSetDist";
export { resultMap } from "./types"; 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 => { let createContext = (stdLib: Reducer_Namespace.t, environment: Reducer_T.environment): t => {
{ {
callStack: list{}, frameStack: list{},
bindings: stdLib->Reducer_Bindings.fromNamespace->Reducer_Bindings.extend, bindings: stdLib->Reducer_Bindings.fromNamespace->Reducer_Bindings.extend,
environment: environment, 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 Result = Belt.Result
module T = Reducer_T module T = Reducer_T
let toLocation = (expression: T.expression): SqError.location => { let toLocation = (expression: T.expression): Reducer_Peggy_Parse.location => {
expression.ast.location expression.ast.location
} }
let throwFrom = (error: SqError.Message.t, expression: T.expression, context: T.context) => let throwFrom = (error: SqError.Message.t, expression: T.expression, context: T.context) =>
error->SqError.throwMessage( error->SqError.throwMessageWithFrameStack(
context.callStack, context.frameStack->Reducer_FrameStack.extend(
location: Some(expression->toLocation) 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( Reducer_Lambda.makeLambda(
None, // TODO - pass function name from parser name,
parameters, parameters,
context.bindings, context.bindings,
body, body,
@ -112,7 +114,13 @@ let rec evaluate: T.reducerFn = (expression, context): (T.value, T.context) => {
}) })
switch lambda { switch lambda {
| T.IEvLambda(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) (result, context)
} }
| _ => RENotAFunction(lambda->Reducer_Value.toString)->throwFrom(expression, 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 eCall = (fn: expression, args: array<expression>): expressionContent => T.ECall(fn, args)
let eLambda = (parameters: array<string>, expr: expression): expressionContent => T.ELambda( let eLambda = (
parameters, parameters: array<string>,
expr, expr: expression,
) name: option<string>,
): expressionContent => T.ELambda(parameters, expr, name)
let eNumber = aNumber => aNumber->T.IEvNumber->T.EValue 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})` `${predicate->toString} ? (${trueCase->toString}) : (${falseCase->toString})`
| EAssign(name, value) => `${name} = ${value->toString}` | EAssign(name, value) => `${name} = ${value->toString}`
| ECall(fn, args) => `(${fn->toString})(${args->Js.Array2.map(toString)->commaJoin})` | 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) | EValue(aValue) => Reducer_Value.toString(aValue)
} }

View File

@ -1,37 +1,43 @@
type t = Reducer_T.frameStack type t = Reducer_T.frameStack
module Frame = { module Frame = {
let toString = ({lambda, location}: Reducer_T.frame) => let toString = ({name, location}: Reducer_T.frame) =>
`${fromFrame} at ${location.start.line->Js.Int.toString}, column ${location.start.column->Js.Int.toString}` 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 make = (): t => list{}
let topFrameName = (t: t) => @genType
switch t->getTopFrame { let getTopFrame = (t: t): option<Reducer_T.frame> => Belt.List.head(t)
| Some({lambda}) =>
switch lambda {
| FnLambda({name}) => name
| FnBuiltin({name}) => name
}
| None => "<main>"
}
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({ t->Belt.List.add({
lambda: lambda, name: name,
fromLocation: location, location: location,
fromFrame: t->topFrameName,
}) })
let toString = (t: t) => 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 @genType
let toFrameArray = (t: t): array<frame> => t->Belt.List.toArray let toFrameArray = (t: t): array<Reducer_T.frame> => t->Belt.List.toArray
@genType @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 => let isEmpty = (t: t): bool =>
switch t->Belt.List.head { switch t->Belt.List.head {

View File

@ -33,7 +33,8 @@ let makeLambda = (
let lambdaContext: Reducer_T.context = { let lambdaContext: Reducer_T.context = {
bindings: localBindingsWithParameters, // based on bindings at the moment of lambda creation bindings: localBindingsWithParameters, // based on bindings at the moment of lambda creation
environment: context.environment, // environment at the moment when lambda is called 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) 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({ 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). // 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. // 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 = { let newContext = {
...context, ...context,
callStack: t->Reducer_CallStack.extend(t), frameStack: context.frameStack->Reducer_FrameStack.extend(
context->Reducer_Context.currentFunctionName,
location,
),
inFunction: Some(t),
} }
SqError.rethrowWithStacktrace(() => { SqError.rethrowWithStacktrace(() => {
@ -76,5 +87,9 @@ let doLambdaCall = (t: t, args, context: Reducer_Context.t, reducer) => {
| FnLambda({body}) => body(args, newContext, reducer) | FnLambda({body}) => body(args, newContext, reducer)
| FnBuiltin({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 defunStatement
= variable:variable '(' _nl args:array_parameters _nl ')' _ assignmentOp _nl body:innerBlockOrExpression = 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()) } return h.nodeLetStatement(variable, value, location()) }
assignmentOp "assignment" = '=' assignmentOp "assignment" = '='
@ -261,9 +261,9 @@ valueConstructor
lambda lambda
= '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}' = '{' _nl '|' _nl args:array_parameters _nl '|' _nl statements:array_statements finalExpression: (statementSeparator @expression) _nl '}'
{ statements.push(finalExpression) { 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 '}' / '{' _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' arrayConstructor 'array'
= '[' _nl ']' = '[' _nl ']'

View File

@ -44,7 +44,7 @@ type nodeIdentifier = {...node, "value": string}
type nodeInteger = {...node, "value": int} type nodeInteger = {...node, "value": int}
type nodeKeyValue = {...node, "key": node, "value": node} type nodeKeyValue = {...node, "key": node, "value": node}
type nodeRecord = {...node, "elements": array<nodeKeyValue>} 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 nodeLetStatement = {...node, "variable": nodeIdentifier, "value": node}
type nodeModuleIdentifier = {...node, "value": string} type nodeModuleIdentifier = {...node, "value": string}
type nodeString = {...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"]) nodeLambda["args"]->Js.Array2.map((argNode: Parse.nodeIdentifier) => argNode["value"])
let body = nodeLambda["body"]->fromNode let body = nodeLambda["body"]->fromNode
ExpressionBuilder.eLambda(args, body) ExpressionBuilder.eLambda(args, body, nodeLambda["name"])
} }
let caseRecord = (nodeRecord): expressionContent => { let caseRecord = (nodeRecord): expressionContent => {

View File

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

View File

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

View File

@ -95,19 +95,19 @@ type t = {
exception SqException(t) exception SqException(t)
// `context` should be specified for runtime errors, but can be left empty for errors from Reducer_Project and so on. let fromMessageWithFrameStack = (message: Message.t, frameStack: Reducer_FrameStack.t): t => {
// `location` can be empty for errors raised from FunctionRegistry.
let fromMessage = (message: Message.t, frameStack: Reducer_FrameStack.t): t => {
message: message, message: message,
stackTrace: stackTrace, frameStack: frameStack,
} }
let fromMessageWithoutFrameStack = (message: Message.t) => // this shouldn't be used much, since frame stack will be empty
fromMessage(message, Reducer_FrameStack.make()) // 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 @genType
let getTopFrame = (t: t): option<Reducer_CallStack.frame> => let getTopFrame = (t: t): option<Reducer_T.frame> => t.frameStack->Reducer_FrameStack.getTopFrame
t.stackTrace->Reducer_FrameStack.getTopFrame
@genType @genType
let getFrameStack = (t: t): Reducer_FrameStack.t => t.frameStack 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 let toString = (t: t): string => t.message->Message.toString
@genType @genType
let createOtherError = (v: string): t => Message.REOther(v)->fromMessageWithoutFrameStack let createOtherError = (v: string): t => Message.REOther(v)->fromMessage
@genType @genType
let getFrameArray = (t: t): array<Reducer_CallStack.frame> => let getFrameArray = (t: t): array<Reducer_T.frame> => t.frameStack->Reducer_FrameStack.toFrameArray
t.stackTrace->Reducer_CallStack.toFrameArray
@genType @genType
let toStringWithStacktrace = (t: t) => let toStringWithStackTrace = (t: t) =>
t->toString ++ if t.frameStack->Reducer_FrameStack.isEmpty { 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 throw = (t: t) => t->SqException->raise
let throwMessage = (message: Message.t, frameStack: Reducer_FrameStack.t) => let throwMessageWithFrameStack = (message: Message.t, frameStack: Reducer_FrameStack.t) =>
fromMessage(message, frameStack)->throw fromMessageWithFrameStack(message, frameStack)->throw
// this shouldn't be used for most runtime errors - the resulting error would have an empty stacktrace // this shouldn't be used for most runtime errors - the resulting error would have an empty stacktrace
let fromException = exn => let fromException = exn =>
switch exn { switch exn {
| SqException(e) => e | SqException(e) => e
| Message.MessageException(e) => e->fromMessage(Reducer_CallStack.make()) | Message.MessageException(e) => e->fromMessage
| Js.Exn.Error(obj) => | Js.Exn.Error(obj) => REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessage
REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->fromMessageWithoutStacktrace | _ => REOther("Unknown exception")->fromMessage
| _ => REOther("Unknown exception")->fromMessageWithoutStacktrace
} }
// converts raw exceptions into exceptions with stacktrace attached // converts raw exceptions into exceptions with stacktrace attached
// already converted exceptions won't be affected // 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 { try {
fn() fn()
} catch { } catch {
| SqException(e) => e->throw // exception already has a stacktrace | 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) => | Js.Exn.Error(obj) =>
REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessage(stackTrace) REJavaScriptExn(obj->Js.Exn.message, obj->Js.Exn.name)->throwMessageWithFrameStack(frameStack)
| _ => REOther("Unknown exception")->throwMessage(stackTrace) | _ => REOther("Unknown exception")->throwMessageWithFrameStack(frameStack)
} }
} }