Skip to content

Fix silent shape errors#272

Open
cwervo wants to merge 5 commits into
mainfrom
ac/shapes-error-bubbling
Open

Fix silent shape errors#272
cwervo wants to merge 5 commits into
mainfrom
ac/shapes-error-bubbling

Conversation

@cwervo

@cwervo cwervo commented Jun 18, 2026

Copy link
Copy Markdown
Collaborator

After merging #261 we introduced an issue with draw/shapes.folk errors, for example:

Wish $this draws a circle with radius 10 filled true
#                                      ^ missing units, e.g. 10mm, 10cm, 10m

this should produce an error, but it gets swallowed by the shapes.folk file.

Before After
No error Now we correctly error
Screenshot 2026-06-18 at 19 09 19 Screenshot 2026-06-18 at 19 07 38

The fix

  1. We change someone to wisher so we don't lose the context of the wisher program
  2. We wrap anything in draw/shapes.folk that could throw an error in try/on error and bubble the error to the wisher.

@osnr

osnr commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

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)

@cwervo

cwervo commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator Author

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 Say instead! Will take another swing at it.

cwervo added 4 commits June 18, 2026 23:38
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.
@cwervo

cwervo commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator Author

Alright, with 23be48 we now have errors that correctly propagate back to their original Folk program:

IMG_9625

This involved making a few deeper (but relatively small!) changes:

  • Graph-Walking Causal Trace (db.c / db.h): Match and Statement structs now preserve an array of all parent statements instead of just a single parent. We added dbGetCausalTrace to do a BFS walk of this graph to get all the ancestors of the match.
    Native Error Bubbling (folk.c): Error propagation logic is now handled in C's runWhenBlock rather than in Tcl. When JIM_ERR is thrown, we traverse the causal graph using dbGetCausalTrace and automatically dispatch SayWithSource to report the error back to every responsible ancestor program.
  • Fixed Memory Corruption (folk.c): Jim_EvalObjVector overwrites the interpreter's result object on evaluation. Because we're now dispatching multiple SayWithSource calls in a loop using Jim_EvalObjVector, we added a strdup to safely copy the error message and avoid rendering corrupted memory (which was showing up as "p?").
  • Cleaner Tcl Bubbling (prelude.tcl): We removed the older, single-parent Tcl error bubbling logic inside evaluateBlock, allowing it to cleanly throw -code error up to the C handler.
  • Rendering errors on the table (errors.folk): We now match against both standard error / warning statements AND their claims ... equivalents, ensuring bubbled errors always correctly draw their outlines on the table.

@cwervo cwervo requested review from Copilot, osnr and smj-edison and removed request for Copilot June 20, 2026 00:06
Wish $p is titled $err
}

When /someone/ claims /p/ has error /err/ with info /info/ {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/ &\

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@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?

@osnr

osnr commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Will test and read the new code soon, thanks!

Comment thread db.c
Comment on lines +208 to +209
// The program that caused the chain of matches leading to this statement.
char causalityFileName[100];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess my main question is why this is different than sourceFileName, or getting the filename by following to the parent?

Comment thread db.c
int workerThreadIndex;

int nParentStatements;
StatementRef* parentStatements;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread db.c
trace[0] = '\0';

// Simple BFS queue
MatchRef queue[1024];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread db.c
}
}

// Returns a comma-separated list of unique files in the causal trace.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Comment thread db.c
// add to trace if unique
if (fname && strlen(fname) > 0 && strcmp(fname, "(null)") != 0) {
// Check if already in trace
if (!strstr(trace, fname)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread db.h
MatchRef parent,
StatementRef* outReusedStatementRef);

// Returns a comma-separated list of unique files in the causal trace.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, there's the term "causal trace", but I never really feel like it's been defined anywhere.

Comment thread folk.c
Jim_DictAddElement(interp, errorInfoObj,
Jim_NewStringObj(interp, "-errorinfo", -1),
errorInfoStr);
Jim_Obj *cmd[] = {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't you call the function here directly? iirc you don't need to go through the interpreter for this.

Comment thread db.c
sourceFileName, sourceLineNumber);
StatementRef ref = statementNew(db, clause, keepMs, atomicallyVersion,
sourceFileName, sourceLineNumber,
parentMatch ? parentMatch->causalityFileName : sourceFileName,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment thread db.c
pthread_mutex_unlock(&parentStatements[i]->destructorSetMutex);

// Inherit causality from the last parent (usually the triggering statement).
if (i == nParents - 1) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread folk.c
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");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jim_GetString guarantees to return a string, so this is unnecessary defensive programming.

Comment thread folk.c

const char* fileName = statementSourceFileName(stmt);
const char* fileName = statementCausalityFileName(stmt);
int lineNumber = statementSourceLineNumber(stmt);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't you need to track the causality line number?

Comment thread folk.c
Jim_Obj *cmd[] = {
Jim_NewStringObj(interp, "SayWithSource", -1),
Jim_NewStringObj(interp, tok, -1), /* sourceFileName */
Jim_NewIntObj(interp, 1), /* sourceLineNumber */

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Line number is erased here too.

@smj-edison

Copy link
Copy Markdown
Collaborator

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 Say in a sequence of Says. This isn't really useful though, because the root Say starts in the interpreter. I also never saw where the new causalityFileName is used. Maybe I'm not understanding the code though.

In general, I feel like causality as conceived by this PR still has some architectural issues:

  1. Is causality inherited via causalityFileName, or is it a breadth-first search starting from the statement? I don't see why there's both.
  2. Why isn't the stack trace of statements reported via the SayWithSource (line 963)? The stack trace of a distant When's eval error will make no sense without the stack trace of the claims and wishes that led to it. I think it makes the most sense for a blended stack trace to exist, where you see the procedures called at the top, but also all the statements that led to it below it. Note that Tcl captures the stack trace as a list, so you should be able to blend the two. In fact, I think [errorInfo] is also implemented in Tcl in Jim's prelude, so modifying it shouldn't be too hard.
  3. How do Hold! and Assert! play into this? They both can support statements, yet they wouldn't show up in the breadth-first walk. I'd be curious if it would be possible to capture the stack trace of where those were called from, something analogous to Zig's ConfigurableTrace (ConfigurableTrace walks the stack and saves the result into a statically sized array, it's surprisingly fast).
  4. Maybe a bit off-topic, but could we look into using -errorcode in Tcl? It's one of Tcl's little-known features that's surprisingly powerful. Essentially, whenever you return an error, you can specify an additional error code with it. The error code is meant to be a machine-readable list of codes (such as {TCL OOM} or {AIO READFAIL}). You can match to a certain depth using try -trap {AIO} or try -trap {AIO READFAIL} depending on how granular you want the error catch to be.

I'm really excited to see where this goes!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants