Skip to content

Design Q: const-value retention + bounded wrapper-following — prerequisite for resolving real-world Temporal/indirect dispatch #80

@avfirsov

Description

@avfirsov

Context

This is a design question, not a bug report — I'd like your steer before building, because the answer decides whether a fairly large body of follow-up work is even feasible in gortex's model.

I'm using gortex to build a cross-repo call graph over a polyglot Temporal estate (Go workflows/activities + Java services). The Go→Go Temporal layer already works well (internal/resolver/temporal_calls.go, internal/parser/languages/golang_temporal.go), and I've sent some incremental Go-side additions in #78 (query/signal/update handler edges) and #79 (env-var-with-default dispatch names). The Java→Go bridge is discussed in #77.

But when I point gortex at real-world workflow code, almost none of the activity dispatch resolves — and after digging in, the cause isn't Temporal-specific. It comes down to two generic capabilities the engine deliberately doesn't have today. Before I (or anyone) writes Temporal-pattern-specific code, I think these two need a decision, because every higher-level pattern bottoms out on them.

The two gaps

1. Constant value is not retained

emitConst (internal/parser/languages/golang.go) creates a KindConstant node but stores only visibility in Meta — the literal value is dropped. So for the extremely common shape:

const ChargeCardActivity = "ChargeCard"
...
workflow.ExecuteActivity(ctx, ChargeCardActivity, …)

goTemporalNameFromExpr extracts the identifier ("ChargeCardActivity"), not the value ("ChargeCard"), so the registry/resolver can never match it to the activity. Real codebases name activities almost exclusively through constants.* references, so this single gap silently drops the majority of edges.

2. Dispatch through a user wrapper is invisible

Detection requires the call site to be literally workflow.ExecuteActivity(...) (the receiver != "workflow" gate in goTemporalDispatchKind). In practice teams wrap it:

func executeActivity[T any](ctx workflow.Context, ao workflow.ActivityOptions, name string, args ...any) (T, error) {
    var out T
    err := workflow.ExecuteActivity(ctx, name, args...).Get(ctx, &out)
    return out, err
}
// caller:
executeActivity[Resp](ctx, ao, ChargeCardActivity, req)   // ← invisible to gortex

There's no interprocedural argument-flow, so the name passed into the wrapper is never connected to the workflow.ExecuteActivity(ctx, name, …) inside it. (The same shape covers prepareActivity() wrappers and a shared workflowutils module.)

The only existing data-flow shortcut in the tree is the one I added in #79 (goTemporalEnvDefaultName), and it explicitly calls itself a "deliberately narrow data-flow shortcut, not general constant propagation" — so I don't want to grow it into something architectural without your blessing.

The questions

I know gortex is intentionally AST-local and precision-first, so I'm not assuming either of these is welcome. Concretely:

Q1 — Const value retention. Would you accept storing a KindConstant's literal value (when the RHS is a string/numeric literal) in Meta["value"], and a resolver step that resolves a same-package/imported const reference to that value for the temporal name argument? Or is dropping the value a deliberate choice (graph size, ambiguity with iota/computed consts) you'd rather keep?

Q2 — Bounded wrapper-following. Would you accept a bounded interprocedural step — one level deep — that, for a user function which itself calls workflow.ExecuteActivity(ctx, <param>, …), propagates a caller-supplied literal/const argument into the dispatch name? Strictly: single level, parameter must flow directly to the dispatch's name position, literal/const only, no general taint analysis.

If yes to either, I'd propose to:

If you'd rather not take gortex into any interprocedural/const-propagation territory, that's completely fair — I'd then document these as known blind spots and lean on a hints mechanism (e.g. // gortex:provides temporal-activity:ChargeCard or .gortex.yaml) instead. Either way, knowing your preference shapes everything downstream.

Why it matters for the rest

A larger plan of mine (resolving Temporal dispatch across ~50 repos through 7 dispatch patterns) depends almost entirely on these two: patterns built on constants and wrappers (the dominant ones) can't be resolved without Q1/Q2 regardless of how much Temporal-specific code is added. The genuinely cheap, in-grain pieces (e.g. SetSignalHandler, SignalExternalWorkflow/QueryWorkflow detection, Java @…Method(name=) parsing) I'm happy to send as standalone PRs independent of this decision — they don't need data-flow.

Thanks for the engine, and for any steer here.

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions