Fix silent shape errors#272
Conversation
|
This sort of matches what we do to report pipeline compilation errors (although that's in an imperative loop and not a When), and it should work, but isn't really the solution I was hoping for -- if possible, I think it would be better to fix this problem in general by having any error in a When block bubble up to the statement producer, although I'm not sure how hard that is (we would probably Say an error statement per level of the 'statement trace', walking up the match/statement graph, instead of just Say for the leaf level? again, not sure if that works in practice, but something like that) (and then we wouldn't need to insert these repetitive try-catch blocks at every point in the standard library where we want to report errors to caller) |
|
Gotcha, yeah, that makes sense. I went with this approach because it was the most conservative but I think it’s pretty tractable to add something in prelude.tcl that can walk the graph and issue that |
This commit resolves an issue where errors from evaluated When
blocks were either lost or incorrectly attributed when multiple
parent statements contributed to a match. It propagates errors
up through the bipartite graph to all responsible ancestor
statements, while properly handling stack traces and Claim
statements for error rendering.
- db.c / db.h:
- Update Statement and Match creation to accept and store an
array of all parent statements rather than a single parent.
- Add dbGetCausalTrace to walk the causal graph (BFS) and
return a list of all ancestor program files.
- folk.c:
- Move error propagation out of Tcl and into C's runWhenBlock.
- Traverse dbGetCausalTrace on JIM_ERR and dispatch
SayWithSource to report errors to all responsible ancestors.
- Properly format -errorinfo tuples and safely strdup the
error message to prevent JimTcl memory corruption.
- prelude.tcl:
- Remove single-parent error bubbling logic from evaluateBlock.
- Let -code error pass the error to runWhenBlock natively.
- builtin-programs/errors.folk:
- Update error and warning matchers to also match Claim
statement outputs so bubbled errors render on the table.
|
Alright, with
This involved making a few deeper (but relatively small!) changes:
|
| Wish $p is titled $err | ||
| } | ||
|
|
||
| When /someone/ claims /p/ has error /err/ with info /info/ { |
There was a problem hiding this comment.
Do we need these new Whens in this file? Do they fix anything? These cases should already be handled by the non-claims When
| } | ||
|
|
||
| When /someone/ wishes /p/ draws a /shape/ with /...options/ &\ | ||
| When /wisher/ wishes /p/ draws a /shape/ with /...options/ &\ |
There was a problem hiding this comment.
Can you revert all the changes to this file? (In general, can you read the diff and remove cruft like this before asking for review?)
There was a problem hiding this comment.
@cwervo mentioned that in the PR request, "We change someone to wisher so we don't lose the context of the wisher program," but I'm not entirely sure why /someone/ would be losing context?
There was a problem hiding this comment.
@cwervo mentioned that in the PR request, "We change
someonetowisherso we don't lose the context of the wisher program," but I'm not entirely sure why /someone/ would be losing context?
I think these changes are a relic from the original design of this PR where it just did manual try-catch in all the draw Whens and then would manually re-Say the error on $wisher.
|
Will test and read the new code soon, thanks! |
| // The program that caused the chain of matches leading to this statement. | ||
| char causalityFileName[100]; |
There was a problem hiding this comment.
I'm really struggling to understand what "causality" means in this context. This is the comment that should go more in detail as to what causality means, or it should at least refer to a part of the code where causality is detailed. This would be the place to put "why" comment, not a "what" comment.
There was a problem hiding this comment.
I guess my main question is why this is different than sourceFileName, or getting the filename by following to the parent?
| int workerThreadIndex; | ||
|
|
||
| int nParentStatements; | ||
| StatementRef* parentStatements; |
There was a problem hiding this comment.
I'm not a massive fan of bidirectional pointers, but I think in this case it makes sense, because traversal upwards would be tricky otherwise.
There was a problem hiding this comment.
I was worried about the thread-safety implications of these new properties, but I think it's fine because 1. they are immutable (set at object construction when access is exclusive) and 2. they are weak refs anyway
| trace[0] = '\0'; | ||
|
|
||
| // Simple BFS queue | ||
| MatchRef queue[1024]; |
There was a problem hiding this comment.
Not a massive fan of hardcoding 1024 here, since it's used in a couple different places below where it could desynchronize pretty quickly. Also could you expand the BFS acronym? I'm assuming you mean breadth first search, but it took me a bit to figure that out.
| } | ||
| } | ||
|
|
||
| // Returns a comma-separated list of unique files in the causal trace. |
There was a problem hiding this comment.
Is this topologically sorted? Might be worth mentioning in the comment, since otherwise someone may be unsure if it could be used for a stack trace (nice idea btw of using unique file names).
| // add to trace if unique | ||
| if (fname && strlen(fname) > 0 && strcmp(fname, "(null)") != 0) { | ||
| // Check if already in trace | ||
| if (!strstr(trace, fname)) { |
There was a problem hiding this comment.
This becomes O(n^2) which is probably fine for now, but might be worth adding a TODO. Same with the strcat, since it uses a linear scan on each append.
| MatchRef parent, | ||
| StatementRef* outReusedStatementRef); | ||
|
|
||
| // Returns a comma-separated list of unique files in the causal trace. |
There was a problem hiding this comment.
Again, there's the term "causal trace", but I never really feel like it's been defined anywhere.
| Jim_DictAddElement(interp, errorInfoObj, | ||
| Jim_NewStringObj(interp, "-errorinfo", -1), | ||
| errorInfoStr); | ||
| Jim_Obj *cmd[] = { |
There was a problem hiding this comment.
Can't you call the function here directly? iirc you don't need to go through the interpreter for this.
| sourceFileName, sourceLineNumber); | ||
| StatementRef ref = statementNew(db, clause, keepMs, atomicallyVersion, | ||
| sourceFileName, sourceLineNumber, | ||
| parentMatch ? parentMatch->causalityFileName : sourceFileName, |
There was a problem hiding this comment.
So is "causality" essentially the first Wish or Claim? Won't that mean that "causality" will always just be whatever the very first claim is, i.e. the claim from the interpreter at the very beginning?
| pthread_mutex_unlock(&parentStatements[i]->destructorSetMutex); | ||
|
|
||
| // Inherit causality from the last parent (usually the triggering statement). | ||
| if (i == nParents - 1) { |
There was a problem hiding this comment.
Why is this in the for loop at all? Plus why are the multiple parent statements being tracked if you flatten out causality? I think this would work with a single parentStatementRef, instead of all the overhead for moving around the statements list. Remember, currently there's only two possible length of parentStatements: a root When, which has only one parent (the When), and when the When unifies with a statement, so the parent statements are the When and the statement it unified with.
| Jim_MakeErrorMessage(interp); | ||
| const char *errorMessage = Jim_GetString(Jim_GetResult(interp), NULL); | ||
| const char *errorMessageRaw = Jim_GetString(Jim_GetResult(interp), NULL); | ||
| char *errorMessage = strdup(errorMessageRaw ? errorMessageRaw : "Unknown error"); |
There was a problem hiding this comment.
Jim_GetString guarantees to return a string, so this is unnecessary defensive programming.
|
|
||
| const char* fileName = statementSourceFileName(stmt); | ||
| const char* fileName = statementCausalityFileName(stmt); | ||
| int lineNumber = statementSourceLineNumber(stmt); |
There was a problem hiding this comment.
Don't you need to track the causality line number?
| Jim_Obj *cmd[] = { | ||
| Jim_NewStringObj(interp, "SayWithSource", -1), | ||
| Jim_NewStringObj(interp, tok, -1), /* sourceFileName */ | ||
| Jim_NewIntObj(interp, 1), /* sourceLineNumber */ |
There was a problem hiding this comment.
Line number is erased here too.
|
Overall I think this is a really cool idea, and the implementation has some unique solutions to the problem. However, reading through this, I felt like causality is not well defined. I think I've pieced together what the code does? Essentially, causality tracks the line information of the root In general, I feel like causality as conceived by this PR still has some architectural issues:
I'm really excited to see where this goes! |
| /* exit(EXIT_FAILURE); */ | ||
|
|
||
| char* trace = dbGetCausalTrace(db, matchRef(db, self->currentMatch)); | ||
| char* tok = strtok(trace, ","); |
There was a problem hiding this comment.
Just skimming, but this use of strtok is probably not thread-safe? I also wonder if we can call SayWithSource directly instead of bouncing through the Tcl interpreter call
There was a problem hiding this comment.
Or we do this whole thing in Tcl. This feels like an awkward medium
| stmt->sourceLineNumber = sourceLineNumber; | ||
|
|
||
| if (causalityFileName != NULL) { | ||
| snprintf(stmt->causalityFileName, sizeof(stmt->causalityFileName), |
There was a problem hiding this comment.
It seems like we should just excise causalityFileName from the PR, but it's also worth pointing out that this use of snprintf doesn't make sense -- the size being passed in is always going to be the size of a char pointer.
| char causalityFileName[100]; | ||
|
|
||
| // The match that created this statement, if any. | ||
| MatchRef parentMatch; |
There was a problem hiding this comment.
It should probably be noted here that this is purely for ancestor walk for error reporting and that it isn't used for the reactive semantics.
I also wonder how this interacts with statement reuse? A match has an immutable, canonical set of parent statements that are set at birth, but a statement can have many different (and/or successive) match parents because of deduplication. Is this the first match parent that spawned this statement? (probably fine -- that's what we do with sourceFileName and sourceLineNumber -- but should be explicitly noted)
There was a problem hiding this comment.
I'd be super curious to see if we could list current ancestors using an atomic linked list or similar, so whenever you support a statement you also register where you come from. This is rough on the design side, but it would be incredible if you got a full stack trace from both C and Tcl with every statement insert.
There was a problem hiding this comment.
I think I was originally doing that, but obviously the current atomic counter of supports was easier to figure out and has been sufficient for the reactive behavior. I bet we could get it to work though
There was a problem hiding this comment.
(+ I was attracted to making the database graph unidirectional, which has been pretty nice until now…)

After merging #261 we introduced an issue with
draw/shapes.folkerrors, for example:this should produce an error, but it gets swallowed by the
shapes.folkfile.The fix
someonetowisherso we don't lose the context of the wisher programdraw/shapes.folkthat could throw an error intry/on errorand bubble the error to the wisher.