Skip to content

feat: get involved accounts#131

Merged
ascandone merged 19 commits into
mainfrom
feat/get-involved-accounts
May 15, 2026
Merged

feat: get involved accounts#131
ascandone merged 19 commits into
mainfrom
feat/get-involved-accounts

Conversation

@ascandone
Copy link
Copy Markdown
Contributor

No description provided.

@ascandone ascandone marked this pull request as draft April 14, 2026 14:46
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 14, 2026

Review Change Stack

Warning

Rate limit exceeded

@ascandone has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 13 minutes and 52 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 04ad5e9e-e12a-44fa-a3ea-751400f494b2

📥 Commits

Reviewing files that changed from the base of the PR and between a37898e and e9dcdf7.

📒 Files selected for processing (2)
  • internal/interpreter/get_involved_accounts.go
  • internal/interpreter/get_involved_accounts_test.go

Walkthrough

Renames SaveStatement.Amount → SaveStatement.Account across parser, analysis, interpreter, and batch-prefetch. Adds GetInvolvedAccounts analysis (expression model, validators, and traversal) with comprehensive tests, exposes involved-account types via an accounts package, and adds InvalidOperatorErr.

Changes

Save Statement Fix & Involved Accounts Analysis

Layer / File(s) Summary
SaveStatement field rename and propagation
internal/parser/ast.go, internal/parser/parser.go, internal/analysis/check.go, internal/analysis/hover.go, internal/interpreter/interpreter.go, internal/interpreter/batch_balances_query.go
AST field SaveStatement.Amount renamed to SaveStatement.Account. Parser, type checker, hover resolver, interpreter, and batch balance query updated to reference Account. SourceAccount now embeds Range.
GetInvolvedAccounts analysis core
internal/interpreter/get_involved_accounts.go
New InvolvedAccountExpr model and evaluator: parses variables, converts values to expression nodes, traverses SendStatement/SaveStatement sources and destinations, records involved account/asset pairs and InvolvedMeta, and provides IsValidCall validation enforcing top-level constraints.
GetInvolvedAccounts tests
internal/interpreter/get_involved_accounts_test.go
Extensive test coverage exercising account interpolation, meta handling (including nesting and invalid keys), balance-driven vars, bounded sends, asset-from-meta scenarios, allotments, multi-asset aggregation, and error cases for missing/invalid/unbound variables.
Interpreter error type
internal/interpreter/interpreter_error.go
Adds exported InvalidOperatorErr (embeds parser.Range, includes Operator string) with Error() implementation.
Public accounts export & ParseResult API
accounts/accounts.go, numscript.go
New accounts package re-exports involved-account types and IsValidCall; ParseResult.GetInvolvedAccounts delegates to the interpreter API.
sequenceDiagram
  participant Client as Caller
  participant Parse as GetInvolvedAccounts
  participant Vars as parseVars
  participant Eval as evalExpr
  participant Stmts as evalSaveStmt/evalSendStmt

  Client->>Parse: GetInvolvedAccounts(vars, program)
  Parse->>Vars: parse/evaluate declared variables
  Vars->>Eval: fold/convert var origins to InvolvedAccountExpr
  Parse->>Stmts: iterate statements
  Stmts->>Eval: evalSaveStmt / evalSendStmt (account, asset)
  Eval-->>Parse: emit InvolvedAccount[], InvolvedMeta[]
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • Azorlogh

Poem

A rabbit hops through ASTs, tidy and spry,
Renames Amount to Account with a blink of an eye,
Then sniffs program paths, finds accounts and meta,
Collects every asset, each origin and letter,
Hops off with a list and a hop full of joy. 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The pull request has no description provided by the author, making it impossible to assess whether descriptive content is related to the changeset. Add a pull request description explaining the feature, its purpose, and how the GetInvolvedAccounts function should be used.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: get involved accounts' directly and clearly summarizes the main feature introduced in this PR, which adds a GetInvolvedAccounts function and related infrastructure for collecting involved account metadata from Numscript programs.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/get-involved-accounts

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@ascandone ascandone requested review from Azorlogh and gfyrag April 14, 2026 14:51
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 14, 2026

Codecov Report

❌ Patch coverage is 50.23697% with 210 lines in your changes missing coverage. Please review.
✅ Project coverage is 67.00%. Comparing base (2175505) to head (e9dcdf7).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/interpreter/get_involved_accounts.go 50.00% 183 Missing and 23 partials ⚠️
internal/interpreter/interpreter_error.go 0.00% 2 Missing ⚠️
numscript.go 0.00% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #131      +/-   ##
==========================================
- Coverage   68.52%   67.00%   -1.52%     
==========================================
  Files          46       47       +1     
  Lines        4651     5068     +417     
==========================================
+ Hits         3187     3396     +209     
- Misses       1290     1476     +186     
- Partials      174      196      +22     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ascandone ascandone force-pushed the feat/get-involved-accounts branch from 49b84b6 to c069a0b Compare May 12, 2026 11:20
@ascandone ascandone marked this pull request as ready for review May 12, 2026 11:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/interpreter/get_involved_accounts.go`:
- Around line 582-602: The code in evalDest (cases for parser.DestinationInorder
and parser.DestinationOneof) only visits clause targets (clause.To / acc.To) and
therefore never collects accounts reachable via the clause.Remaining
destination; update those cases so after calling st.evalKeptOrDest(clause.To)
(or acc.To) you also check for and traverse clause.Remaining (or acc.Remaining)
by calling st.evalKeptOrDest on it (or iterating if Remaining can contain
multiple destinations) so Remaining destinations are visited the same way as To;
use the existing evalKeptOrDest helper and the symbols
parser.DestinationInorder, parser.DestinationOneof, clause/acc and
st.evalKeptOrDest to locate where to add this traversal.
- Around line 162-174: The case handling analysis.FnSetAccountMeta directly
indexes stmt.Args[0] and [1] and can panic on malformed input; add an arity
guard before calling st.evalExpr (e.g., if len(stmt.Args) < 2) and return a
typed interpreter error (or fmt.Errorf with clear message) if insufficient args,
otherwise proceed to eval both args and append the InvolvedMeta entry; update
the switch branch around FnSetAccountMeta and use the same error type/format the
interpreter uses elsewhere for consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3887a13f-f3b8-4b28-aa53-6abde49fdbab

📥 Commits

Reviewing files that changed from the base of the PR and between 5c66ad9 and c069a0b.

⛔ Files ignored due to path filters (2)
  • internal/parser/__snapshots__/parser_fault_tolerance_test.snap is excluded by !**/*.snap, !**/*.snap
  • internal/parser/__snapshots__/parser_test.snap is excluded by !**/*.snap, !**/*.snap
📒 Files selected for processing (9)
  • internal/analysis/check.go
  • internal/analysis/hover.go
  • internal/interpreter/batch_balances_query.go
  • internal/interpreter/get_involved_accounts.go
  • internal/interpreter/get_involved_accounts_test.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/interpreter_error.go
  • internal/parser/ast.go
  • internal/parser/parser.go

Comment thread internal/interpreter/get_involved_accounts.go
Comment thread internal/interpreter/get_involved_accounts.go
Copy link
Copy Markdown
Contributor

@gfyrag gfyrag left a comment

Choose a reason for hiding this comment

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

Review from the ledger side

I analyzed this PR against the ledger integration (replacing the discoveryStore emulation + full meta() support). The static analysis approach is fundamentally cleaner and more performant than execution with infinite balances. Here's what needs adjusting to go to production.


Bug: lost : separator in interpolated accounts

get_involved_accounts.go:346-360 — For @user:$id:pending, the real interpreter does strings.Join(parts, ":") (evaluate_expr.go:38), but the analysis uses foldedAdd which produces Add{Add{AccountLiteral{user}, NumberLiteral{42}}, AccountLiteral{pending}}. When resolving this tree, you get "user42pending" instead of "user:42:pending".

Fix: replace Add with a dedicated AccountSegments type for account interpolations:

type AccountSegments struct {
    Parts []InvolvedAccountExpr
}

And in evalExpr for AccountInterpLiteral:

case *parser.AccountInterpLiteral:
    parts := make([]InvolvedAccountExpr, 0, len(expr.Parts))
    for _, part := range expr.Parts {
        partExpr, err := st.evalAccountNamePart(part)
        if err != nil { return nil, err }
        parts = append(parts, partExpr)
    }
    if len(parts) == 1 {
        return parts[0], nil
    }
    return AccountSegments{Parts: parts}, nil

Add stays for arithmetic only.


Missing: color constraints and asset scaling

batch_balances_query.go transforms the asset before querying:

  • SourceAccount with color → coloredAsset(asset, color) (line 98)
  • SourceWithScalingassetToScaledAsset(asset) for the main address (line 113), and no query for the through account (line 106)

The static analysis completely ignores Color and doesn't distinguish SourceWithScaling cases. The consumer would preload the wrong asset.

Fix: enrich InvolvedAccount:

type InvolvedAccount struct {
    AccountExpr  InvolvedAccountExpr
    AssetExpr    InvolvedAccountExpr
    Color        InvolvedAccountExpr // nil if no color constraint
    Scaled       bool                // true → consumer applies assetToScaledAsset()
    NeedsBalance bool                // false for unbounded overdraft, destinations, through accounts
}

And adjust evalSrc to propagate source.Color, source.Bounded == nil (unbounded overdraft), and the SourceWithScaling case (address = scaled+needs balance, through = not scaled+no balance).


Missing: separate meta reads from meta writes

involvedMeta mixes meta() in var declarations (reads) and set_account_meta (writes). On the consumer side, reads block resolution (you need to read the metadata to know which account to preload), writes are just preloading for previous value capture.

Fix: either add an IsWrite bool field on InvolvedMeta, or separate into two slices in the return:

func GetInvolvedAccounts(vars VariablesMap, program parser.Program) (
    []InvolvedAccount,
    []InvolvedMeta,   // reads: meta() in var declarations
    []InvolvedMeta,   // writes: set_account_meta
    InterpreterError,
)

Minor points

  • set_tx_meta (line 167): the // can we ignore this ? is correct — transaction metadata doesn't affect preloading. Replace the comment with // set_tx_meta does not affect involved accounts or preloading to be explicit.
  • Public API: GetInvolvedAccounts is in internal/interpreter/. The consumer imports the root package — it will need to be exposed in numscript.go along with the expression types.
  • Coverage at 47%: GetBalance, GetOverdraft, Sub, Div, SubPrefix, most evalSrc/evalDest branches are uncovered. Add tests for SourceCapped, SourceInorder, SourceOneof, DestinationInorder, DestinationOneof, save statement.

@ascandone ascandone force-pushed the feat/get-involved-accounts branch from c069a0b to fef44f2 Compare May 13, 2026 13:07
@ascandone ascandone requested a review from gfyrag May 14, 2026 14:49
Copy link
Copy Markdown
Contributor

@gfyrag gfyrag left a comment

Choose a reason for hiding this comment

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

Second pass — deux points bloquants pour le ledger v3

L'approche est bonne et le gros du travail est fait, mais il reste deux problèmes d'API qu'il vaut mieux régler maintenant — une fois intégrés côté ledger, les changer sera beaucoup plus coûteux. Le ledger v3 ne sera pas lancé sans ces deux points.


1. Add réutilisé pour la concaténation de segments de comptes

Pour @user:$id:pending, le code produit :

Add(Add(Add(Add("user", ":"), 42), ":"), "pending")

Les séparateurs : sont bien préservés, donc les données sont correctes. Mais Add est utilisé à la fois pour l'arithmétique (100 + 42) et la concaténation de segments de comptes. Côté consommateur, il n'y a aucun moyen de distinguer les deux sans heuristiques fragiles.

Fix : un type dédié AccountSegments pour les interpolations de comptes :

type AccountSegments struct {
    Parts []InvolvedAccountExpr
}

Et dans evalExpr pour AccountInterpLiteral :

case *parser.AccountInterpLiteral:
    parts := make([]InvolvedAccountExpr, 0, len(expr.Parts))
    for _, part := range expr.Parts {
        partExpr, err := st.evalAccountNamePart(part)
        if err != nil { return nil, err }
        parts = append(parts, partExpr)
    }
    if len(parts) == 1 {
        return parts[0], nil
    }
    return AccountSegments{Parts: parts}, nil

Le consommateur peut alors faire strings.Join(resolvedParts, ":") naturellement. Add reste réservé à l'arithmétique.


2. Meta reads et writes mélangés dans le même slice

GetInvolvedAccounts retourne un seul []InvolvedMeta qui mélange :

  • Les lectures (meta(@acc, "k") dans les vars) — bloquantes, il faut lire la metadata pour résoudre les comptes impliqués
  • Les écritures (set_account_meta) — juste du preloading pour capturer la valeur précédente

Le consommateur doit pouvoir les distinguer pour ordonner correctement ses opérations (les reads bloquent la résolution, les writes non).

Fix : soit un champ IsWrite bool sur InvolvedMeta, soit deux slices séparés dans le retour :

func GetInvolvedAccounts(vars VariablesMap, program parser.Program) (
    []InvolvedAccount,
    []InvolvedMeta,   // reads: meta() dans les var declarations
    []InvolvedMeta,   // writes: set_account_meta
    InterpreterError,
)

Je préfère la deuxième option (deux slices), c'est plus explicite et ça évite que le consommateur ait à filtrer.

Copy link
Copy Markdown
Contributor

@gfyrag gfyrag left a comment

Choose a reason for hiding this comment

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

Second pass — two blocking points for ledger v3

The approach is solid and the bulk of the work is done, but two API issues need to be fixed now — once integrated on the ledger side, changing them will be much more costly. Ledger v3 won't ship without these.


1. Add reused for account segment concatenation

For @user:$id:pending, the code produces:

Add(Add(Add(Add("user", ":"), 42), ":"), "pending")

The : separators are preserved, so the data is correct. But Add is used for both arithmetic (100 + 42) and account segment concatenation. On the consumer side, there's no way to distinguish the two without fragile heuristics.

Fix: a dedicated AccountSegments type for account interpolations:

type AccountSegments struct {
    Parts []InvolvedAccountExpr
}

And in evalExpr for AccountInterpLiteral:

case *parser.AccountInterpLiteral:
    parts := make([]InvolvedAccountExpr, 0, len(expr.Parts))
    for _, part := range expr.Parts {
        partExpr, err := st.evalAccountNamePart(part)
        if err != nil { return nil, err }
        parts = append(parts, partExpr)
    }
    if len(parts) == 1 {
        return parts[0], nil
    }
    return AccountSegments{Parts: parts}, nil

The consumer can then naturally do strings.Join(resolvedParts, ":"). Add stays reserved for arithmetic.


2. Meta reads and writes mixed in the same slice

GetInvolvedAccounts returns a single []InvolvedMeta that mixes:

  • Reads (meta(@acc, "k") in var declarations) — blocking, metadata must be fetched to resolve the involved accounts
  • Writes (set_account_meta) — just preloading to capture the previous value

The consumer needs to distinguish them to order operations correctly (reads block resolution, writes don't).

Fix: either an IsWrite bool field on InvolvedMeta, or two separate slices in the return:

func GetInvolvedAccounts(vars VariablesMap, program parser.Program) (
    []InvolvedAccount,
    []InvolvedMeta,   // reads: meta() in var declarations
    []InvolvedMeta,   // writes: set_account_meta
    InterpreterError,
)

I prefer the second option (two slices) — it's more explicit and the consumer doesn't have to filter.

@gfyrag
Copy link
Copy Markdown
Contributor

gfyrag commented May 15, 2026

Regarding point 2 — set_account_meta does append to st.involvedMeta:

// GetInvolvedAccounts, case FnSetAccountMeta:
st.involvedMeta = append(st.involvedMeta, InvolvedMeta{
    Account: acc,
    Key:     key,
})

And meta reads from var declarations go to the same slice:

// evalVar, case FnVarOriginMeta:
st.involvedMeta = append(st.involvedMeta, InvolvedMeta{
    Account: acc,
    Key:     key,
})

Both end up in the single st.involvedMeta returned by GetInvolvedAccounts. There's no way for the consumer to tell reads from writes.

@ascandone ascandone force-pushed the feat/get-involved-accounts branch from a37898e to 688c0e8 Compare May 15, 2026 09:01
@ascandone ascandone force-pushed the feat/get-involved-accounts branch from 688c0e8 to 1832b94 Compare May 15, 2026 09:02
@gfyrag
Copy link
Copy Markdown
Contributor

gfyrag commented May 15, 2026

Two things on the latest changes:

1. My mistake on meta writes — we do need them

I was wrong to suggest dropping set_account_meta writes entirely. We actually need the previous metadata value to populate the log entry on the ledger side. Sorry about the confusion.

The original approach (reporting writes in involvedMeta) was correct. The separation between reads and writes is still needed though — reads block resolution (the consumer must fetch metadata before it can resolve involved accounts), writes don't (they're just preloading for previous value capture). Either two separate slices or an IsWrite bool field on InvolvedMeta would work.

2. Bug: ConcatAccount missing from IsValidCall

The new ConcatAccount type isn't handled in isValidCall — it falls through to the default return false. This means any meta expression involving an interpolated account (e.g. meta(@user:$id, "k")) will incorrectly fail validation even when all components are resolvable literals.

Fix:

case ConcatAccount:
    return st.isValidCall(expr.Left) && st.isValidCall(expr.Right)

Copy link
Copy Markdown
Contributor

@gfyrag gfyrag left a comment

Choose a reason for hiding this comment

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

Both blocking points are addressed:

  1. ConcatAccount cleanly separates account segment concatenation from arithmetic Add, with constant folding as a bonus.
  2. Meta reads vs writes distinguished via nullable Write field — simple and effective.

LGTM, good to merge.

@ascandone ascandone merged commit 7856fd0 into main May 15, 2026
5 of 7 checks passed
@ascandone ascandone deleted the feat/get-involved-accounts branch May 15, 2026 09:47
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.

2 participants