Skip to content

Conversation

ascandone
Copy link
Contributor

No description provided.

Copy link
Contributor

coderabbitai bot commented Jun 13, 2025

Walkthrough

Adds a typed Account value with AccountAddress and VirtualAccount implementations, introduces a virtual() builtin gated by a feature flag/version, refactors interpreter send/receive to route through Sender with Account, and implements VirtualAccount credit/debit settlement. Updates funds stack, errors, tests, and adds extensive virtual-account script tests.

Changes

Cohort / File(s) Summary
Builtins and Flags
internal/analysis/check.go, internal/flags/flags.go
Adds FnVarOriginVirtual builtin returning an account; gated by version 0.0.20 and ExperimentalVirtualAccount flag. Introduces ExperimentalVirtualAccount flag and registers it in AllFlags.
Value and Typing Refactor
internal/interpreter/value.go, internal/interpreter/evaluate_expr.go, internal/interpreter/args_parser_test.go
Introduces AccountValue interface, AccountAddress, Account wrapper; rewires constructors and expectors (expectAccount, expectAccountAddress); updates string casting to handle Account; fixes tests to wrap accounts.
Funds Stack API Update
internal/interpreter/funds_stack.go, internal/interpreter/funds_stack_test.go
Sender field Name string → Account AccountValue; updates comparisons/merges and all tests to use AccountAddress in Account field.
Interpreter Core Flow
internal/interpreter/interpreter.go, internal/interpreter/interpreter_error.go, internal/interpreter/batch_balances_query.go, internal/interpreter/function_exprs.go, internal/interpreter/function_statements.go
Wires virtual() resolution; tracks virtual credits/debts; refactors pushSender/pushReceiver; adds handling for Account vs VirtualAccount in send/receive; NegativeBalanceError.Account type → Account; adjusts batch queries and arg parsing for account types.
Virtual Account Feature
internal/interpreter/virtual_account.go, internal/interpreter/virtual_account_test.go
Adds VirtualAccount with credit/debit stacks and settlement logic (Receive, Pull, PullCredits); comprehensive tests validate basic, partial, overdraft, transitive, and commutativity behaviors.
Interpreter Tests (general)
internal/interpreter/interpreter_test.go
Updates error expectations to use Account instead of raw strings/AccountAddress.
Script Tests: Experimental Virtual Accounts
internal/interpreter/testdata/script-tests/experimental/virtual-account/*
Adds multiple .num scripts covering creation, overdraft, credits routing, min constraints, kept destination, transitive flows, double-spend prevention, self-send noop, wrong-currency scenarios.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Script
  participant Analyzer as Analyzer (builtins)
  participant Interpreter
  participant Feature as Flags/Version
  participant V as VirtualAccount
  note over Script,Interpreter: virtual() builtin resolution
  Script->>Analyzer: Resolve var-origin: virtual()
  Analyzer->>Feature: Check version>=0.0.20 + ExperimentalVirtualAccount
  Feature-->>Analyzer: Allowed/Denied
  alt Allowed
    Analyzer-->>Interpreter: Var resolves to fn-call returning Account(VirtualAccount)
    Interpreter->>V: NewVirtualAccount()
    V-->>Interpreter: VirtualAccount instance
    Interpreter-->>Script: Account{VirtualAccount}
  else Denied
    Analyzer-->>Script: Feature flag/version error
  end
Loading
sequenceDiagram
  autonumber
  actor Script
  participant I as Interpreter
  participant FS as fundsStack
  participant VA as VirtualAccount
  participant Bal as Balances

  note over Script,I: Send [ASSET X] from Source to Destination

  Script->>I: Execute send(amount, asset, source, destination)
  I->>I: Build Sender(s) (Account or VirtualAccount)
  I->>FS: pushSender(Sender)
  loop Pull senders for destination
    I->>FS: Pull colored
    FS-->>I: Sender
    alt Destination is VirtualAccount
      I->>VA: Receive(asset, sender)
      VA-->>I: Postings (repay debits, credit remainder)
    else Destination is AccountAddress
      I->>I: pushReceiverAddress(name, sender)
      I->>Bal: Update balances/postings
    end
  end

  alt Source is VirtualAccount
    I->>VA: Pull(asset, receiver)
    VA-->>I: Postings (consume credits, record debits if overdraft)
  else Source is AccountAddress
    I->>Bal: Safe withdraw (by color)
    Bal-->>I: Postings / errors
  end

  I-->>Script: Final postings or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • gfyrag
  • laouji
  • Quentin-David-24

Poem

Hop hop! I mint a ghostly purse,
Credits drift, debits reimburse.
Virtual trails through script we chart,
Strings to Accounts—a type-safe heart.
Overdraft clouds, then balances mend—
Postings align, and rabbits ascend.
(_/)> 💸

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/virtual-account

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

codecov bot commented Jun 13, 2025

Codecov Report

❌ Patch coverage is 82.29814% with 57 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.75%. Comparing base (0bad42a) to head (e69fbe3).

Files with missing lines Patch % Lines
internal/interpreter/interpreter.go 82.31% 25 Missing and 1 partial ⚠️
internal/interpreter/virtual_account.go 87.28% 15 Missing ⚠️
internal/interpreter/value.go 51.85% 12 Missing and 1 partial ⚠️
internal/interpreter/function_exprs.go 66.66% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #69      +/-   ##
==========================================
+ Coverage   71.32%   71.75%   +0.43%     
==========================================
  Files          41       42       +1     
  Lines        4631     4872     +241     
==========================================
+ Hits         3303     3496     +193     
- Misses       1176     1222      +46     
- Partials      152      154       +2     

☔ 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/virtual-account branch 3 times, most recently from dca1aab to 72e72f1 Compare June 19, 2025 11:59
@ascandone ascandone force-pushed the feat/virtual-account branch 4 times, most recently from afaa35a to 55e8fda Compare August 27, 2025 10:47
@ascandone ascandone changed the title Feat/virtual account feat: virtual account Aug 27, 2025
finally some tests passing

other passing tests

fix edge cases

fix edge cases

cleaned up

remove comment

feat: better dbg

I guess we made it?

refactor

remove unused code

adde test and removed comment

fix test

renamed var

moved function

refactor

simplify code

simplify code

added tests

WIP test

test: migrated tests

test: migrated test

test: migrated test

test: migrate tests

test: migrated tests

test: migrated tests

test: removed leftover test

fix after rebase
@ascandone ascandone force-pushed the feat/virtual-account branch from 55e8fda to 7f832fa Compare August 27, 2025 10:56
@ascandone ascandone force-pushed the feat/virtual-account branch from 8287590 to 44f703c Compare September 1, 2025 07:05
@ascandone ascandone marked this pull request as ready for review September 1, 2025 07:06
Copy link
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: 13

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/interpreter/interpreter_error.go (1)

115-123: Drop the hardcoded '@' and rely on Account’s own representation in the error message

Now that Account is a wrapper over multiple representations (e.g., AccountAddress, VirtualAccount), hardcoding '@' is incorrect for non-address accounts. Also, formatting with %s assumes a string; prefer using %v (or explicitly .String()).

Apply:

- return fmt.Sprintf("Cannot fetch negative balance from account @%s", e.Account)
+ return fmt.Sprintf("Cannot fetch negative balance from account %v", e.Account)

If Account doesn’t implement fmt.Stringer, add a String() string method that renders addresses with a leading '@' and virtual accounts with their own canonical form.

🧹 Nitpick comments (32)
internal/interpreter/function_statements.go (1)

35-35: Type erasure of meta to string.

If downstream consumers rely on typed metadata (numbers, booleans), String() conversion loses type info. Consider storing the raw Value or a typed/JSON form instead.

internal/interpreter/testdata/script-tests/experimental/virtual-account/test-send-self-is-noop.num (1)

1-5: Covers self-send noop; consider adding guard cases.

Add a counterpart that tries self-send across different assets (should fail) and one with an overdraft bound of zero to ensure no unintended postings occur.

internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-left-negative.num (1)

1-11: Edge-case companion tests would strengthen coverage.

  • Add a variant where the second transfer only partially offsets the first (to keep a negative residual).
  • Add assertions in the harness (if supported) to validate final balances of $v and @DesT.
internal/interpreter/testdata/script-tests/experimental/virtual-account/bounded-overdraft-when-does-not-fail.num (1)

1-11: Nice: positive flow with large bound. Add failing counterpart.

Add a “bounded-overdraft-when-fails” case (e.g., bound below 90 when sending 100 after receiving 10) to verify strict enforcement.

internal/interpreter/interpreter_error.go (1)

39-47: Optional: align other account-bearing error types to the Account wrapper for consistency

MetadataNotFound.Account still uses string, while NegativeBalanceError.Account is Account. Consider migrating others for a uniform API and future-proof messaging with virtual accounts.

I can batch-prepare a follow-up diff if you want.

Also applies to: 115-123

internal/interpreter/testdata/script-tests/experimental/virtual-account/repay-self.num (1)

1-14: Verify portions syntax and intent around USD/2 * vs USD/2 10

Both constructs look intentional; just confirm the grammar here matches current parser rules and semantics (half of all vs half with floor/min). If the distinction isn’t required, consider standardizing to one form in tests.

internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending-send-all.num (1)

8-14: Clarify intent: duplicate $v entries are intentional to test dedup.

Consider adding a brief comment so future maintainers don’t “fix” the duplication.

internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-merchant-pays-fees-if-needed.num (1)

5-11: Exercise the “merchant pays if needed” path with a concrete low-$amt case.

As written, this relies on external input for $amt. Add a companion script (or fixture) with $amt = 30 so 10% -> 3 EUR and the second send must draw 2 EUR from @merchant, plus one with $amt = 50 (exact 5) to cover rounding edges.

internal/interpreter/args_parser_test.go (2)

50-51: Make the expected Account literal explicit and keep expected-first order.

Use the field name to avoid relying on struct field order and keep Equal(expected, actual) consistent.

-	require.Equal(t, *big.NewInt(42), *a1)
-	require.Equal(t, Account{AccountAddress("user:001")}, *a2)
+	require.Equal(t, *big.NewInt(42), *a1)
+	require.Equal(t, Account{Repr: AccountAddress("user:001")}, *a2)

Outside this hunk: prefer require.NoError(t, err) over require.Nil(t, err) at Lines 16 and 45 for clarity.


59-59: Style: keep construction consistent across the file.

You construct the same value as Account{Repr: AccountAddress("user:001")} above; keep that form everywhere in this test for uniformity.

internal/interpreter/testdata/script-tests/experimental/virtual-account/account-kept.num (2)

8-14: Clarify “remaining kept” expectation with an assertion/golden.

Intended outcome appears: 5 USD to @DesT, 5 USD left in $v. Please add/verify a golden so the next send moves exactly 5 USD to @other, confirming kept semantics.


16-19: Final drain check.

Consider another variant where the second step is max [USD 15] to ensure the third send is zero, exercising the empty-balance path.

internal/interpreter/testdata/script-tests/experimental/virtual-account/creditors-stack.num (1)

4-15: Covers unbounded overdraft well; add a bounded-failure sibling.

Add a test using “allowing overdraft up to [USD 150]” (or equivalent) to validate failure behavior and messages when limits are exceeded.

internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-no-commissions-with-low-amount.num (1)

13-19: Oneof branch semantics: verify low-amount path is exercised.

For “no commissions with low amount,” make sure $amt is low enough that the “max [EUR 5] to @DesT” branch is actually taken in CI to cover this scenario.

If helpful, add a brief comment stating the intended ranges for $amt to clarify the test’s purpose.

internal/interpreter/testdata/script-tests/experimental/virtual-account/send-credit-around.num (1)

11-16: Fix minor grammar in comment.

-  // here's we're sending the credit we have from $v1
+  // here we're sending the credit we have from $v1
internal/interpreter/function_exprs.go (2)

22-27: Runtime expects a real account address; static typing still allows any account.

Switching to expectAccountAddress is correct at runtime, but analysis still types balance/meta/overdraft params as generic account. Users could pass virtual() and get a runtime error instead of a compile-time diagnostic.

Consider adding an analysis-time check that rejects virtual() where a real address is required, or introduce a distinct AccountAddress type in the analyzer to catch this earlier.


56-61: Meta on virtual accounts.

Same concern as above: meta() now requires a real address at runtime. Ensure the error clearly states “virtual accounts not allowed in meta()”.

If not already, specialize the expectAccountAddress error for virtual inputs.

internal/interpreter/batch_balances_query.go (2)

35-37: Skip batching for virtual accounts—good.

Type-asserting to AccountAddress avoids querying balances for virtual accounts. Consider a small helper (e.g., asAddress(acc Account) (string, bool)) to reduce repetition across sites.


102-105: Same assertion pattern—consider helper to DRY.

internal/analysis/check.go (1)

118-129: Builtin registration and gating look correct; mind analyzer vs runtime mismatch.

Zero-arity, returns account, gated at >= 0.0.20 + ExperimentalVirtualAccount—good. However, analyzer still allows virtual() anywhere TypeAccount is accepted, while runtime now rejects it for balance/meta/overdraft.

Add an analyzer diagnostic that forbids passing virtual() into those builtins, or model “account-address” as a distinct type for those parameters to fail fast at analysis time.

Also confirm tests set version >= 0.0.20 and enable the flag.

internal/interpreter/interpreter_test.go (3)

401-405: Use keyed struct literal for stability when constructing Account

Positional struct literals are brittle across refactors. Prefer a keyed literal to avoid breakage if fields are reordered.

-        Error: machine.NegativeBalanceError{
-            Account: machine.Account{machine.AccountAddress("a")},
-            Amount:  *big.NewInt(-100),
-        },
+        Error: machine.NegativeBalanceError{
+            Account: machine.Account{Repr: machine.AccountAddress("a")},
+            Amount:  *big.NewInt(-100),
+        },

470-475: Same: prefer keyed Account literal in TypeError.Value

Keeps tests resilient to internal struct changes.

-            Error: machine.TypeError{
-                Expected: "monetary",
-                Value:    machine.Account{machine.AccountAddress("bad:type")},
-            },
+            Error: machine.TypeError{
+                Expected: "monetary",
+                Value:    machine.Account{Repr: machine.AccountAddress("bad:type")},
+            },

611-615: Same: prefer keyed Account literal in TypeError.Value

-            Error: machine.TypeError{
-                Expected: "string",
-                Value:    machine.Account{machine.AccountAddress("key_wrong_type")},
-            },
+            Error: machine.TypeError{
+                Expected: "string",
+                Value:    machine.Account{Repr: machine.AccountAddress("key_wrong_type")},
+            },
internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-fail-if-not-enough.num (1)

1-16: Comment/code mismatch: script currently sends full 10% to @fees, not “up to 5”

  • The comment says “10%$amt (up to 5) to @fees”, but step 3 empties $fees_hold to @fees, resulting in total fees = 10%$amt when $amt > 50.
  • If the intent is “min(10%*$amt, 5) goes to @fees and the rest to @DesT”, send the remainder of $fees_hold to $dest_hold (or @DesT), not @fees.
-// say that we need to send 10%*$amt (up to 5) to @fees; the rest to @dest
+// send min(10% * $amt, 5) to @fees; send the rest to @dest
@@
 // now we empty the rest
 send [EUR *] (
   source = $fees_hold
-  destination = @fees
+  destination = $dest_hold
 )

If the goal was only “fail if $amt < 5”, updating the leading comment to reflect that would also resolve the mismatch.

Also applies to: 27-35

internal/interpreter/virtual_account_test.go (2)

26-33: Consider using a test helper type instead of anonymous struct.

The Posting type is defined as an anonymous struct. Consider creating a type alias or importing the actual Posting type from the interpreter package to improve maintainability.

Add a type alias at the top of the file:

type Posting = interpreter.Posting

381-409: Consider extracting the permutation helper to a test utilities package.

The permute function is a generic utility that could be useful for other test files. While the GPT-generated comment is honest, consider moving this to a shared test utilities package for reuse. The implementation itself is correct.

Would you like me to create a test utilities package with this and other potentially reusable test helpers?

internal/interpreter/virtual_account.go (1)

173-174: TODO comment needs clarification or removal.

The TODO comment on line 173 questions the overdraft implementation, but the code on line 174 appears to handle overdrafts correctly by checking if remainingAmt is positive. Either clarify the concern or remove the TODO if it's resolved.

Would you like me to help investigate the overdraft implementation to determine if this TODO can be resolved?

internal/interpreter/interpreter.go (3)

332-356: The function modifies totalSent in place which could be confusing.

The function takes totalSent *big.Int and modifies it in place, then returns it. This dual approach (both modifying the parameter and returning it) could lead to confusion. The function name pushSender also doesn't indicate that it calculates and returns a total.

Consider either:

  1. Only modifying the parameter in place (void function)
  2. Only returning the new total without modifying the parameter
  3. Renaming the function to better indicate its dual purpose (e.g., pushSenderAndAccumulate)

671-678: Inefficient retrieval and processing of credits.

The code first gets credits, then pulls from them. This two-step process could be optimized.

Consider combining the operations if the API allows:

-		fs := account.getCredits(s.CurrentAsset)
-		pulledSenders := fs.PullColored(amount, *color)
+		// Pull colored credits directly from the virtual account
+		pulledSenders := account.PullColoredCredits(s.CurrentAsset, amount, *color)

689-694: Complex logic for calculating additional send amount needs clarification.

The calculation of addionalSent (note the typo: should be additionalSent) involves multiple conditions and could benefit from clearer variable naming and comments.

-			var addionalSent *big.Int
+			var additionalSent *big.Int
 			if overdraft == nil {
-				addionalSent = new(big.Int).Set(missingAmt)
+				// Unlimited overdraft: send the full missing amount
+				additionalSent = new(big.Int).Set(missingAmt)
 			} else {
-				// TODO check this is the correct number to eventually send
-				// TODO test overdraft
-				addionalSent = utils.MinBigInt(overdraft, missingAmt)
+				// Limited overdraft: send up to the overdraft limit
+				additionalSent = utils.MinBigInt(overdraft, missingAmt)
 			}
 
-			s.pushSender(Sender{account, addionalSent, *color}, totalSent)
+			s.pushSender(Sender{account, additionalSent, *color}, totalSent)
internal/interpreter/value.go (2)

12-20: Interface method names could be more descriptive.

The account() method name doesn't clearly indicate its purpose as a marker method. Consider using a more descriptive name like isAccountValue() or following Go's convention with a private method like accountValue().

 type AccountValue interface {
-	account()
+	isAccountValue() // Marker method for AccountValue implementations
 	String() string
 }
 
 type AccountAddress string
 
-func (AccountAddress) account() {}
-func (VirtualAccount) account() {}
+func (AccountAddress) isAccountValue() {}
+func (VirtualAccount) isAccountValue() {}

166-178: Function naming inconsistency and unclear separation of concerns.

The function expectAccountAddress suggests it expects an AccountAddress value directly, but it actually expects an Account and then extracts the address. This creates confusion about the function's purpose.

Consider renaming for clarity:

-func expectAccountAddress(v Value, r parser.Range) (*string, InterpreterError) {
+func expectAccountWithAddress(v Value, r parser.Range) (*string, InterpreterError) {
 	acc, err := expectAccount(v, r)
 	if err != nil {
 		return nil, err
 	}
 	switch acc := acc.Repr.(type) {
 	case AccountAddress:
 		s := string(acc)
 		return &s, nil
 	default:
-		return nil, TypeError{Expected: analysis.TypeAccount, Value: v, Range: r}
+		// More specific error message
+		return nil, TypeError{Expected: "account with address", Value: v, Range: r}
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 0bad42a and 5103a0b.

⛔ Files ignored due to path filters (18)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/account-create.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/account-kept.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/bounded-overdraft-when-does-not-fail.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/creditors-stack.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/half-using-virtual.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-fail-if-not-enough.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-merchant-pays-fees-if-needed.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-no-commissions-with-low-amount.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-left-negative.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-when-fails.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending-send-all.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/repay-self.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/send-credit-around.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/test-send-self-is-noop.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/transitive.num.specs.json is excluded by !**/*.json
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/wrong-currency.num.specs.json is excluded by !**/*.json
📒 Files selected for processing (33)
  • internal/analysis/check.go (2 hunks)
  • internal/flags/flags.go (2 hunks)
  • internal/interpreter/args_parser_test.go (2 hunks)
  • internal/interpreter/batch_balances_query.go (3 hunks)
  • internal/interpreter/evaluate_expr.go (1 hunks)
  • internal/interpreter/function_exprs.go (5 hunks)
  • internal/interpreter/function_statements.go (1 hunks)
  • internal/interpreter/funds_stack.go (3 hunks)
  • internal/interpreter/funds_stack_test.go (3 hunks)
  • internal/interpreter/interpreter.go (11 hunks)
  • internal/interpreter/interpreter_error.go (1 hunks)
  • internal/interpreter/interpreter_test.go (3 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/account-create.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/account-kept.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/bounded-overdraft-when-does-not-fail.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/creditors-stack.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/half-using-virtual.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-fail-if-not-enough.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-merchant-pays-fees-if-needed.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-no-commissions-with-low-amount.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-left-negative.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-when-fails.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending-send-all.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/repay-self.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/send-credit-around.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/test-send-self-is-noop.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/transitive.num (1 hunks)
  • internal/interpreter/testdata/script-tests/experimental/virtual-account/wrong-currency.num (1 hunks)
  • internal/interpreter/value.go (4 hunks)
  • internal/interpreter/virtual_account.go (1 hunks)
  • internal/interpreter/virtual_account_test.go (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-16T14:51:51.052Z
Learnt from: ascandone
PR: formancehq/numscript#70
File: internal/interpreter/accounts_metadata.go:13-24
Timestamp: 2025-07-16T14:51:51.052Z
Learning: The DeepClone method in internal/interpreter/accounts_metadata.go works correctly despite using MapGetOrPutDefault in what initially appears to be a non-assignment context. The fetchAccountMetadata method returns a reference to the map stored in the cloned structure, and MapGetOrPutDefault modifies it in-place, so the cloned values persist correctly.

Applied to files:

  • internal/interpreter/function_statements.go
📚 Learning: 2024-12-05T11:42:58.472Z
Learnt from: ascandone
PR: formancehq/numscript#27
File: internal/interpreter/interpreter.go:667-668
Timestamp: 2024-12-05T11:42:58.472Z
Learning: In Go test files within the `internal/interpreter` package (e.g., `reconciler_test.go` and `interpreter_test.go`), it's acceptable to use hardcoded `"<kept>"` strings in test data and comments, and they do not need to be replaced with the `KEPT_ADDR` constant.

Applied to files:

  • internal/interpreter/args_parser_test.go
  • internal/interpreter/interpreter_test.go
🧬 Code graph analysis (13)
internal/interpreter/args_parser_test.go (3)
internal/interpreter/value.go (6)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Value (22-25)
  • NewMonetaryInt (253-256)
internal/interpreter/args_parser.go (1)
  • NewArgsParser (11-15)
numscript.go (1)
  • Value (72-72)
internal/interpreter/funds_stack.go (1)
internal/interpreter/value.go (3)
  • Account (30-30)
  • Account (38-38)
  • AccountValue (12-15)
internal/analysis/check.go (3)
internal/analysis/version_check.go (1)
  • VersionClause (54-57)
internal/parser/version.go (2)
  • Version (9-12)
  • NewVersionInterpreter (28-34)
internal/flags/flags.go (2)
  • FeatureFlag (3-3)
  • ExperimentalVirtualAccount (13-13)
internal/interpreter/interpreter_error.go (1)
internal/interpreter/value.go (2)
  • Account (30-30)
  • Account (38-38)
internal/interpreter/virtual_account_test.go (4)
internal/interpreter/virtual_account.go (2)
  • NewVirtualAccount (31-36)
  • VirtualAccount (10-14)
internal/interpreter/funds_stack.go (1)
  • Sender (7-11)
internal/interpreter/value.go (7)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Asset (28-28)
  • Asset (42-42)
  • VirtualAccount (20-20)
internal/interpreter/interpreter.go (1)
  • Posting (80-85)
internal/interpreter/virtual_account.go (4)
internal/interpreter/value.go (10)
  • VirtualAccount (20-20)
  • String (27-27)
  • String (37-37)
  • AccountValue (12-15)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Asset (28-28)
  • Asset (42-42)
  • Account (30-30)
  • Account (38-38)
internal/utils/utils.go (1)
  • MapGetOrPutDefault (50-58)
internal/interpreter/funds_stack.go (1)
  • Sender (7-11)
internal/interpreter/interpreter.go (1)
  • Posting (80-85)
internal/interpreter/funds_stack_test.go (2)
internal/interpreter/value.go (4)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
internal/interpreter/funds_stack.go (1)
  • Sender (7-11)
internal/interpreter/batch_balances_query.go (1)
internal/interpreter/value.go (2)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
internal/interpreter/interpreter_test.go (2)
internal/interpreter/value.go (5)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Value (22-25)
numscript.go (1)
  • Value (72-72)
internal/interpreter/function_exprs.go (4)
internal/interpreter/value.go (5)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Value (22-25)
internal/interpreter/interpreter.go (1)
  • InterpreterError (73-76)
internal/flags/flags.go (1)
  • ExperimentalVirtualAccount (13-13)
internal/interpreter/virtual_account.go (1)
  • NewVirtualAccount (31-36)
internal/interpreter/interpreter.go (7)
internal/analysis/check.go (1)
  • FnVarOriginVirtual (64-64)
internal/interpreter/value.go (9)
  • Account (30-30)
  • Account (38-38)
  • VirtualAccount (20-20)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Asset (28-28)
  • Asset (42-42)
  • String (27-27)
  • String (37-37)
internal/interpreter/virtual_account.go (1)
  • VirtualAccount (10-14)
internal/interpreter/funds_stack.go (1)
  • Sender (7-11)
internal/parser/ast.go (4)
  • Destination (189-192)
  • DestinationAccount (194-194)
  • DestinationAccount (200-202)
  • ValueExpr (8-11)
internal/interpreter/interpreter_error.go (1)
  • InvalidUnboundedInSendAll (151-154)
internal/utils/utils.go (2)
  • NonExhaustiveMatchPanic (8-10)
  • MinBigInt (12-22)
internal/interpreter/value.go (5)
internal/interpreter/virtual_account.go (1)
  • VirtualAccount (10-14)
internal/interpreter/interpreter.go (1)
  • InterpreterError (73-76)
internal/interpreter/interpreter_error.go (2)
  • InvalidAccountName (215-218)
  • TypeError (49-53)
internal/parser/range.go (1)
  • Range (13-16)
internal/analysis/check.go (1)
  • TypeAccount (14-14)
internal/interpreter/evaluate_expr.go (1)
internal/interpreter/value.go (2)
  • Account (30-30)
  • Account (38-38)
🔇 Additional comments (36)
internal/interpreter/function_statements.go (2)

23-23: Good call: require a concrete account address for metadata.

Using expectAccountAddress ensures metadata can’t be set on virtual accounts or non-concrete values. This aligns with the virtual-account model.


31-36: illation

internal/interpreter/evaluate_expr.go (2)

39-41: Ensure NewAccountAddress returns the Account wrapper.

Given castToString now matches Account, returning Account from NewAccountAddress avoids subtle mismatches in downstream type switches.


218-221: Verify castToString handles raw AccountAddress
Confirm that in evaluate_expr.go the AST’s account‐literal branch always wraps addresses via NewAccountAddress (returning Account) and that castToString never sees a bare AccountAddress value. If direct AccountAddress values can still flow into castToString, add:

  case Account:
    return v.String(), nil
+ case AccountAddress:
+   return v.String(), nil
internal/interpreter/testdata/script-tests/experimental/virtual-account/transitive.num (1)

1-16: LGTM: clear transitive flow across two virtual accounts

Sequential sends world→v1→v2→dest exercise the transitive path well. Good coverage for balance propagation.

internal/interpreter/testdata/script-tests/experimental/virtual-account/prevent-double-spending.num (1)

1-14: LGTM: good negative test for double-spending from the same virtual source

Listing $v twice in the source set should be rejected. Ensure the harness asserts the specific error surfaced by the interpreter for clarity.

internal/interpreter/testdata/script-tests/experimental/virtual-account/account-create.num (1)

1-11: LGTM: basic fund-and-spend with a virtual account

Covers the happy path world→$v then $v→dest. Nice baseline.

internal/interpreter/testdata/script-tests/experimental/virtual-account/half-using-virtual.num (1)

6-6: No changes needed: grammar supports allowing unbounded overdraft inside composite sources. Tests across internal/interpreter/testdata/script-tests already use this modifier within braces, so the existing line is valid.

internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft.num (2)

5-8: LGTM: unbounded overdraft path covered.

This exercises negative balance then later top-up; matches the described intent.


4-4: Comment syntax validated. // comments are consistently used in .num test files and supported by Numscript.

internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-when-fails.num (2)

8-11: Good negative case for bounded overdraft.

Covers “credit 10 then attempt 100 with limit 1”; this should fail.


8-11: Annotate expected overdraft failure in the test script.
In internal/interpreter/testdata/script-tests/experimental/virtual-account/overdraft-when-fails.num, add a test-harness directive (e.g. a comment like # expect-error: NegativeBalance: overdraft exceeded) so the framework records and asserts the expected failure.

internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-merchant-pays-fees-if-needed.num (1)

12-22: Confirm source stacking order ($fees_hold drained before @merchant).

The intent seems to be “use $fees_hold, then fallback to @merchant.” Please verify interpreter behavior preserves this order and add a golden asserting which postings are produced when $fees_hold < 5.

internal/interpreter/args_parser_test.go (1)

39-40: Good: tests now pass Account wrapper, not bare AccountAddress.

internal/interpreter/testdata/script-tests/experimental/virtual-account/creditors-stack.num (1)

16-19: Verify creditor ordering on later inflow.

Please add/confirm goldens showing how the incoming 150 is applied against prior debits (ordering rules), so regressions are caught.

internal/interpreter/testdata/script-tests/experimental/virtual-account/min-constraint-no-commissions-with-low-amount.num (2)

1-5: Virtual accounts declared correctly; ensure flag/version are enabled in tests.

This script depends on ExperimentalVirtualAccount and version >= 0.0.20. Please confirm the test harness sets the interpreter version and enables the flag, otherwise the run will fail at parse/exec time.


20-27: LGTM: final drains from holds mirror the intended flow.

Transfers from $fees_hold to @fees and from $dest_hold to @DesT look consistent with the multi-hop design.

internal/interpreter/testdata/script-tests/experimental/virtual-account/send-credit-around.num (1)

18-22: Verify “no postings” assertion is covered.

Please confirm the expected-results fixture asserts that this third send yields zero postings.

internal/interpreter/function_exprs.go (3)

89-96: Balance on virtual accounts.

Identical static/runtime mismatch note as for overdraft/meta. Otherwise, change is correct.

Please verify existing tests cover passing virtual() to balance/meta/overdraft and produce the intended error.


103-107: NegativeBalanceError.Account wrapping.

Using Account{AccountAddress(*account)} matches the new Account value model. Confirm all consumers of NegativeBalanceError now expect Account (not raw address).


159-167: virtual() builtin gated correctly.

Feature-flag check and return shape look good. No further issues.

internal/interpreter/batch_balances_query.go (1)

123-126: Overdraft source batching respects virtual accounts.

LGTM. Skips when overdraft is unbounded and when source is virtual.

internal/analysis/check.go (1)

64-65: New builtin symbol name is clear and consistent.

FnVarOriginVirtual constant naming aligns with existing conventions.

internal/interpreter/funds_stack.go (3)

8-10: Move to AccountValue looks good; confirm equality semantics across implementations

Storing Account as AccountValue is aligned with the new abstraction. Please confirm all current AccountValue implementations are intended to be compared by value (addresses) or identity (virtual accounts) consistently across the stack logic.


83-91: Merge result construction is correct; amount addition is safe

The aggregated Sender keeps Account and Color and sums Amounts as expected.


155-168: Split/return path is correct; leftover is pushed back with preserved Account/Color

Logic for “more than enough” and exact split looks good and preserves invariants.

internal/interpreter/funds_stack_test.go (1)

12-13: LGTM! Clean migration to AccountAddress type.

The refactoring from Name to Account: AccountAddress(...) improves type safety and aligns with the new account abstraction model.

Also applies to: 17-18

internal/interpreter/virtual_account_test.go (2)

1-10: Test file structure looks good.

Using a separate test package (interpreter_test) promotes black-box testing and ensures the API is tested from a consumer perspective.


314-379: Excellent test coverage for commutativity.

The commutative order test exhaustively validates that the order of operations doesn't affect the final outcome, which is crucial for a financial system. The test properly verifies all 24 permutations of the 4 operations.

internal/interpreter/virtual_account.go (3)

38-50: Good use of helper functions for lazy initialization.

The getCredits and getDebits helper methods properly encapsulate the lazy initialization pattern using the utility function. This ensures consistent behavior across the codebase.


52-69: Clear and well-documented public API.

The Receive method has excellent documentation explaining the FIFO policy and debt repayment mechanism. The implementation correctly handles the debt-first repayment logic.


114-140: Good separation of concerns in repayWithSender.

The helper function properly encapsulates the debt repayment logic and correctly manages the remaining amount calculation.

internal/interpreter/interpreter.go (4)

197-198: Remove redundant bounds‐check suggestionvirtual(s) already validates the ExperimentalVirtualAccount feature flag and returns an error on failure, so no additional validation is needed.

Likely an incorrect or invalid review comment.


224-230: Setting Dbg field directly mutates the struct - consider immutability.

The code creates a VirtualAccount and then mutates its Dbg field. This breaks immutability and could lead to unexpected behavior if the same VirtualAccount is used elsewhere.

Consider creating a new VirtualAccount with the debug name instead of mutating:

-			if acc, ok := value.(Account); ok {
-				if vacc, ok := acc.Repr.(VirtualAccount); ok {
-					vacc.Dbg = varsDecl.Name.Name
-					value = Account{vacc}
-				}
-			}
+			if acc, ok := value.(Account); ok {
+				if vacc, ok := acc.Repr.(VirtualAccount); ok {
+					// Create a new VirtualAccount with the debug name
+					newVacc := VirtualAccount{
+						Dbg:     varsDecl.Name.Name,
+						credits: vacc.credits,
+						debits:  vacc.debits,
+					}
+					value = Account{newVacc}
+				}
+			}

Likely an incorrect or invalid review comment.


543-557: Missing nil check for account.PullCredits return value.

The code calls account.PullCredits(s.CurrentAsset) and iterates over the result, but doesn't check if it returns nil or handle potential errors.

Add nil check for safety:

 	case VirtualAccount:
 		totalSent := big.NewInt(0)
 
 		senders := account.PullCredits(s.CurrentAsset)
+		if senders == nil {
+			return totalSent, nil
+		}
 		for _, sender := range senders {
 			s.pushSender(sender, totalSent)
 		}

Likely an incorrect or invalid review comment.


440-444: No extra error handling needed for account address conversion. evaluateExprAs(..., expectAccountAddress) already enforces and returns an error on invalid types.

Likely an incorrect or invalid review comment.

Comment on lines +13 to 14
ExperimentalVirtualAccount FeatureFlag = "experimental-virtual-account"
)
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Flag added and wired into AllFlags — LGTM. Also verify usage coverage.

  • OK to add ExperimentalVirtualAccount and include it in AllFlags.
  • Optional: align naming with peers (e.g., ExperimentalVirtualAccountFeatureFlag) or type AllFlags as []FeatureFlag for readability (alias to string anyway).

Run to confirm the flag is actually gating features and tests:

Also applies to: 24-25


🏁 Script executed:

#!/bin/bash
rg -nC2 '\bExperimentalVirtualAccount\b'

Length of output: 1295


Add unit tests for ExperimentalVirtualAccount gating

  • Usage confirmed in internal/interpreter/function_exprs.go (checkFeatureFlag at line 162) and internal/analysis/check.go (FeatureFlag at line 125).
  • No _test.go files reference ExperimentalVirtualAccount; add tests covering both enabled and disabled scenarios to ensure the flag actually gates behavior.
🤖 Prompt for AI Agents
In internal/flags/flags.go around lines 13-14, there are no unit tests for the
ExperimentalVirtualAccount flag; add tests that verify behavior when the flag is
enabled and disabled. Create a new _test.go in the same package that (1) sets
ExperimentalVirtualAccount on, exercises the code paths that depend on it (e.g.,
call the feature-checking helper used by internal/interpreter/function_exprs.go
and any API/analysis functions referenced in internal/analysis/check.go) and
asserts the gated behavior is allowed, and (2) sets the flag off and asserts the
gated behavior is denied; ensure tests clean up/reset global flag state (use
defer to restore original value) and include clear assertions for both
scenarios.

Comment on lines +126 to 127
{AccountAddress("src"), big.NewInt(10), "X"},
})
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix inconsistent Sender struct initialization.

Line 126 uses the shorthand struct literal syntax while the rest of the file uses explicit field names. This inconsistency makes the code harder to maintain.

Apply this diff to fix the inconsistency:

-		{AccountAddress("src"), big.NewInt(10), "X"},
+		{Account: AccountAddress("src"), Amount: big.NewInt(10), Color: "X"},

Also applies to: 131-131, 136-136

🤖 Prompt for AI Agents
In internal/interpreter/funds_stack_test.go around lines 126-127 (and also at
131 and 136), the Sender struct is initialized using a shorthand positional
literal ({AccountAddress("src"), big.NewInt(10), "X"}) while the rest of the
file uses explicit field names; update these initializations to use explicit
field names (e.g., Sender{Address: ..., Amount: ..., Currency: ...}) to match
the file's style and improve maintainability.

Comment on lines +166 to +167
{AccountAddress("s1"), big.NewInt(1), "c1"},
{AccountAddress("s2"), big.NewInt(1), "c2"},
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix inconsistent Sender struct initialization in multiple locations.

Lines 166-167, 179, and 187 use shorthand struct literal syntax inconsistently with the rest of the file.

Apply this diff to maintain consistency:

-		{AccountAddress("s1"), big.NewInt(1), "c1"},
-		{AccountAddress("s2"), big.NewInt(1), "c2"},
+		{Account: AccountAddress("s1"), Amount: big.NewInt(1), Color: "c1"},
+		{Account: AccountAddress("s2"), Amount: big.NewInt(1), Color: "c2"},
-		{AccountAddress("s1"), big.NewInt(10), ""},
+		{Account: AccountAddress("s1"), Amount: big.NewInt(10), Color: ""},
-		{AccountAddress("s1"), big.NewInt(10), ""},
+		{Account: AccountAddress("s1"), Amount: big.NewInt(10), Color: ""},

Also applies to: 179-180, 187-187

🤖 Prompt for AI Agents
internal/interpreter/funds_stack_test.go around lines 166-167, 179-180 and 187:
the tests use shorthand composite literals like {AccountAddress("s1"),
big.NewInt(1), "c1"} which is inconsistent with the rest of the file; update
each occurrence to use explicit field names matching the Sender struct (e.g.
Sender{Address: AccountAddress("s1"), Amount: big.NewInt(1), Currency: "c1"}) so
the initializers are consistent and resilient to field reordering.

Comment on lines +79 to 81
if first.Account != second.Account || first.Color != second.Color {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Avoid interface equality that can panic for non-comparable AccountValue types; make equality explicit

Comparing interface values with “!=” panics if the dynamic type is not comparable. Also, equality semantics may differ by implementation (string address vs virtual account identity). Use an explicit comparer.

-        if first.Account != second.Account || first.Color != second.Color {
+        if !accountsEqual(first.Account, second.Account) || first.Color != second.Color {
             return
         }

Add helper (outside the shown range) and import reflect:

import "reflect"

// accountsEqual defines equality semantics for AccountValue.
// - AccountAddress: value equality
// - pointer-backed implementations (e.g., virtual accounts): pointer identity
// - fallback: DeepEqual (safe for non-comparable structs)
func accountsEqual(a, b AccountValue) bool {
    if a == nil || b == nil {
        return a == b
    }
    ta, tb := reflect.TypeOf(a), reflect.TypeOf(b)
    if ta != tb {
        return false
    }
    va, vb := reflect.ValueOf(a), reflect.ValueOf(b)
    switch ta.Kind() {
    case reflect.String, reflect.Int, reflect.Int64, reflect.Uint, reflect.Uint64, reflect.Bool:
        return a == b
    case reflect.Ptr, reflect.UnsafePointer:
        return va.Pointer() == vb.Pointer()
    default:
        return reflect.DeepEqual(a, b)
    }
}

🏁 Script executed:

#!/bin/bash
set -eu

# Locate the definition of AccountValue
rg -nP 'type\s+AccountValue' -C3 -g '*.go'

# Preview imports in internal/interpreter/funds_stack.go
sed -n '1,50p' internal/interpreter/funds_stack.go

# Show context around lines 79-81
sed -n '60,100p' internal/interpreter/funds_stack.go

# Check whether reflect is already imported
rg -n 'reflect' internal/interpreter/funds_stack.go

Length of output: 2974


🏁 Script executed:

#!/bin/bash
set -eu

# Show AccountValue definition and its implementers in value.go
sed -n '1,200p' internal/interpreter/value.go

# Locate all types implementing the account() method
rg -nP '\.account\(' -C3 -g '*.go'

Length of output: 4506


🏁 Script executed:

#!/bin/bash
set -eu

# Locate the definition of VirtualAccount
rg -nP 'type\s+VirtualAccount\b' -C3 -g '*.go'

# If found, show its full definition (context +/-10 lines)
FILE_AND_LINE=$(rg -nP 'type\s+VirtualAccount\b' -n -g '*.go' | head -n1)
if [ -n "$FILE_AND_LINE" ]; then
  FILE=$(echo "$FILE_AND_LINE" | cut -d: -f1)
  LINE=$(echo "$FILE_AND_LINE" | cut -d: -f2)
  start=$(( LINE > 10 ? LINE - 10 : 1 ))
  sed -n "${start},$((LINE+10))p" "$FILE"
fi

Length of output: 1415


Replace interface equality on AccountValue with explicit helper
Dynamic types like VirtualAccount contain non-comparable maps, so first.Account != second.Account will panic at runtime. Change the check to:

if !accountsEqual(first.Account, second.Account) || first.Color != second.Color {
    return
}

Import "reflect" and add an accountsEqual(a, b AccountValue) bool helper that handles nil, enforces same type, compares pointers for pointer-backed types, and falls back to reflect.DeepEqual.

🤖 Prompt for AI Agents
internal/interpreter/funds_stack.go around lines 79 to 81: the current equality
check uses interface equality (first.Account != second.Account) which can panic
for dynamic types containing non-comparable fields; replace that condition with
a call to a new helper accountsEqual(first.Account, second.Account) and keep the
Color check; add import "reflect" and implement accountsEqual(a, b AccountValue)
bool that returns true for both nil, ensures types match, for pointer-backed
concrete types compares pointer addresses when possible, and otherwise falls
back to reflect.DeepEqual to determine equality.

Comment on lines +395 to +402
case VirtualAccount:
// Here we have a debt from a virtual acc.
// we don't want to emit that as a posting (but TODO check how does it interact with kept)
senderAccountAddress.Pull(st.CurrentAsset, Sender{
AccountAddress(name),
sender.Amount,
sender.Color,
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Virtual account debt handling lacks validation.

When handling virtual account debts, the code calls Pull on the sender but doesn't verify if the operation succeeds or if there are sufficient funds.

Add validation to ensure the Pull operation is valid:

 	case VirtualAccount:
 		// Here we have a debt from a virtual acc.
 		// we don't want to emit that as a posting (but TODO check how does it interact with kept)
+		// Validate that the virtual account can handle this debt
 		senderAccountAddress.Pull(st.CurrentAsset, Sender{
 			AccountAddress(name),
 			sender.Amount,
 			sender.Color,
 		})

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In internal/interpreter/interpreter.go around lines 395 to 402, the
VirtualAccount branch calls senderAccountAddress.Pull(...) without validating
the result or checking funds; update this block to check Pull's return (or
pre-check balance) and handle failures: verify Pull succeeded (or that sender
has sufficient amount for st.CurrentAsset), and on failure return or propagate
an appropriate error (or log and mark the transaction invalid) instead of
proceeding silently; ensure any error path cleans up state consistently and
preserves existing flow for successful pulls.

Comment on lines +44 to 49
func NewAccountAddress(src string) (Account, InterpreterError) {
if !validateAddress(src) {
return AccountAddress(""), InvalidAccountName{Name: src}
return Account{AccountAddress("")}, InvalidAccountName{Name: src}
}
return AccountAddress(src), nil
return Account{AccountAddress(src)}, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Error handling returns invalid state.

When validation fails, the function returns Account{AccountAddress("")} with an error. This creates an invalid Account object that could cause issues if the error is ignored.

Return a zero value or nil-like representation instead:

 func NewAccountAddress(src string) (Account, InterpreterError) {
 	if !validateAddress(src) {
-		return Account{AccountAddress("")}, InvalidAccountName{Name: src}
+		return Account{}, InvalidAccountName{Name: src}
 	}
 	return Account{AccountAddress(src)}, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func NewAccountAddress(src string) (Account, InterpreterError) {
if !validateAddress(src) {
return AccountAddress(""), InvalidAccountName{Name: src}
return Account{AccountAddress("")}, InvalidAccountName{Name: src}
}
return AccountAddress(src), nil
return Account{AccountAddress(src)}, nil
}
func NewAccountAddress(src string) (Account, InterpreterError) {
if !validateAddress(src) {
return Account{}, InvalidAccountName{Name: src}
}
return Account{AccountAddress(src)}, nil
}
🤖 Prompt for AI Agents
internal/interpreter/value.go around lines 44-49: the function currently returns
a partially populated Account{AccountAddress("")} when validation fails which
creates an invalid object; change the failure return to the zero-value Account{}
(i.e., return Account{}, InvalidAccountName{Name: src}) so callers can't
accidentally use an invalid Account when they ignore the error.

v0.Dbg = "v0"

v1 := interpreter.NewVirtualAccount()
v0.Dbg = "v1"
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix incorrect debug field assignment.

Line 139 incorrectly assigns to v0.Dbg instead of v1.Dbg, which defeats the purpose of labeling the virtual accounts for debugging.

Apply this diff to fix the assignment:

-	v0.Dbg = "v1"
+	v1.Dbg = "v1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
v0.Dbg = "v1"
v1.Dbg = "v1"
🤖 Prompt for AI Agents
In internal/interpreter/virtual_account_test.go around line 139, the test
incorrectly sets the debug label on v0 (v0.Dbg = "v1") instead of v1; change the
assignment to set v1.Dbg = "v1" so each virtual account has its intended debug
label (verify v0.Dbg remains "v0" and update only the single line to assign the
correct variable).

Comment on lines +16 to +19
func (v VirtualAccount) WithDbg(dbg string) VirtualAccount {
v.Dbg = dbg
return v
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Consider using pointer receiver for WithDbg method.

The WithDbg method uses a value receiver and returns a copy of the VirtualAccount. Since VirtualAccount contains maps (reference types), this creates a shallow copy where the maps are shared between instances. This could lead to unexpected behavior.

Apply this diff to use a pointer receiver:

-func (v VirtualAccount) WithDbg(dbg string) VirtualAccount {
-	v.Dbg = dbg
-	return v
+func (v *VirtualAccount) WithDbg(dbg string) *VirtualAccount {
+	v.Dbg = dbg
+	return v
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (v VirtualAccount) WithDbg(dbg string) VirtualAccount {
v.Dbg = dbg
return v
}
func (v *VirtualAccount) WithDbg(dbg string) *VirtualAccount {
v.Dbg = dbg
return v
}
🤖 Prompt for AI Agents
internal/interpreter/virtual_account.go around lines 16 to 19: change WithDbg to
use a pointer receiver so it mutates the existing VirtualAccount instead of
returning a shallow-copied value; update the signature to func (v
*VirtualAccount) WithDbg(dbg string) *VirtualAccount { v.Dbg = dbg; return v }
and then update any call sites that expect a value return to accept the pointer
(or explicitly dereference) to preserve chaining while avoiding shared-map copy
issues.

}}
case VirtualAccount:
// should be unreachable
panic("Unhandled: send from addr to virtual account")
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider returning errors instead of panicking.

The send function panics in two unreachable cases. While these cases should theoretically never occur, panicking in production code can crash the entire application. Consider returning an error instead.

Apply this diff to handle errors gracefully:

func send(
	source AccountValue,
	destination AccountValue,
	amount *big.Int,
	asset string,
	color string,
-) []Posting {
+) ([]Posting, error) {
	switch source := source.(type) {
	case AccountAddress:
		switch destination := destination.(type) {
		case AccountAddress:
-			return []Posting{{
+			return []Posting{{
				Source:      string(source),
				Destination: string(destination),
				Amount:      amount,
				Asset:       coloredAsset(asset, &color),
-			}}
+			}}, nil
		case VirtualAccount:
-			panic("Unhandled: send from addr to virtual account")
+			return nil, fmt.Errorf("unhandled: send from addr to virtual account")
		}
	case VirtualAccount:
		switch dest := destination.(type) {
		case AccountAddress:
-			return source.Pull(asset, Sender{
+			postings := source.Pull(asset, Sender{
				Account: dest,
				Amount:  amount,
				Color:   color,
			})
+			return postings, nil
		case VirtualAccount:
-			panic("Unhandled: send from virtual account to virtual account")
+			return nil, fmt.Errorf("unhandled: send from virtual account to virtual account")
		}
	}
-	panic("non exhaustive match")
+	return nil, fmt.Errorf("non exhaustive match in send function")
}

Note: This change would require updating callers to handle the error.

Also applies to: 106-106

🤖 Prompt for AI Agents
In internal/interpreter/virtual_account.go at lines 91 and 106, replace the
panics ("Unhandled: send from addr to virtual account") with returned errors:
change the send function signature to return an error (or add an error return if
it already returns values), create and return a descriptive error (e.g.,
fmt.Errorf or a package-level sentinel error) at those two branches, and update
all callers to handle and propagate or log the error appropriately; ensure tests
and call sites are adjusted to handle the new error return.

pulled := credits.PullColored(receiver.Amount, receiver.Color)

remainingAmt := new(big.Int).Set(receiver.Amount)

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Potential bug: remainingAmt is never decremented in Pull method.

The remainingAmt variable is initialized to receiver.Amount but is never decremented when postings are created. This means the overdraft logic will always trigger if any amount is requested, even if sufficient credits are available.

Apply this diff to fix the bug:

	for _, pulledSender := range pulled {
		newPostings := send(
			pulledSender.Account,
			receiver.Account,
			pulledSender.Amount,
			asset,
			receiver.Color,
		)
		postings = append(postings, newPostings...)
+		remainingAmt.Sub(remainingAmt, pulledSender.Amount)
	}

Also applies to: 173-183

🤖 Prompt for AI Agents
In internal/interpreter/virtual_account.go around line 159 (and similarly for
the block at 173-183), remainingAmt is initialized to receiver.Amount but never
decremented while creating postings, so overdraft logic always triggers; fix by
decrementing remainingAmt each time you allocate funds to a posting: inside the
loop over available credits compute allocated = min(credit.Amount,
remainingAmt), create a posting for allocated, subtract allocated from
remainingAmt, and stop iterating when remainingAmt == 0; after the loop, if
remainingAmt > 0 then trigger the overdraft behavior (or return an error) as
originally intended.

Copy link
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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/interpreter/interpreter.go (1)

524-553: Honor requested asset color in VirtualAccount send-all

In the VirtualAccount branch of sendAll (internal/interpreter/interpreter.go around line 547), we currently call

senders := account.PullCredits(s.CurrentAsset)

which unconditionally drains all colors. Instead, when color is non-empty we must filter by that color—e.g.:

senders := s.getCredits(s.CurrentAsset).PullColored(*color)

and only fall back to PullAll() if color == "".

♻️ Duplicate comments (1)
internal/interpreter/interpreter.go (1)

395-403: Validate Pull or pre-check availability (repeat from prior review).

Pull on a VirtualAccount is unchecked. Validate its result or pre-check available credits/debt policy; propagate an error on failure instead of proceeding silently.

🧹 Nitpick comments (3)
internal/interpreter/function_exprs.go (1)

22-24: Overdraft should not accept virtual accounts (clarify TODO or implement).

You switched to expectAccountAddress, which effectively excludes virtual accounts. If that’s the intended semantics, drop the TODO. If overdraft must support virtual accounts later, ensure getBalance(Account) handles VirtualAccount correctly (see my getBalance fix in interpreter.go).

internal/interpreter/interpreter.go (2)

225-231: Prefer named field init for clarity; copy semantics are fine.

Setting vacc.Dbg then re-wrapping avoids pointer aliasing. Minor clarity nit: initialize Account with a named field.

-					value = Account{vacc}
+					value = Account{Repr: vacc}

259-261: Remove unused virtualAccountsCredits and virtualAccountsDebts
The virtualAccountsCredits and virtualAccountsDebts fields (declared at lines 327–328) and their initializations (lines 259–260) aren’t referenced anywhere else. Drop them to eliminate dead code.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 5103a0b and e69fbe3.

📒 Files selected for processing (2)
  • internal/interpreter/function_exprs.go (3 hunks)
  • internal/interpreter/interpreter.go (12 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
internal/interpreter/interpreter.go (6)
internal/analysis/check.go (1)
  • FnVarOriginVirtual (64-64)
internal/interpreter/value.go (5)
  • Account (30-30)
  • Account (38-38)
  • VirtualAccount (20-20)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
internal/interpreter/virtual_account.go (1)
  • VirtualAccount (10-14)
internal/interpreter/funds_stack.go (1)
  • Sender (7-11)
internal/interpreter/interpreter_error.go (2)
  • InvalidUnboundedInSendAll (151-154)
  • QueryBalanceError (179-182)
internal/utils/utils.go (2)
  • NonExhaustiveMatchPanic (8-10)
  • MinBigInt (12-22)
internal/interpreter/function_exprs.go (4)
internal/interpreter/value.go (5)
  • Account (30-30)
  • Account (38-38)
  • AccountAddress (17-17)
  • AccountAddress (19-19)
  • Value (22-25)
internal/interpreter/interpreter.go (1)
  • InterpreterError (73-76)
internal/flags/flags.go (1)
  • ExperimentalVirtualAccount (13-13)
internal/interpreter/virtual_account.go (1)
  • NewVirtualAccount (31-36)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Tests
  • GitHub Check: Dirty
🔇 Additional comments (10)
internal/interpreter/function_exprs.go (3)

29-31: OK: use Account wrapper for balance lookup.

Wrapping AccountAddress into Account for getBalance aligns with the new API.


56-61: OK: meta() restricted to concrete account addresses.

Switching to expectAccountAddress avoids ambiguous virtual/meta lookups.


159-167: virtual() builtin correctly gated and returns Account.

Feature-flag check is correct; returning Account{Repr: NewVirtualAccount()} matches the new value model.

internal/interpreter/interpreter.go (7)

197-199: Wiring virtual() into handleFnCall looks good.

The new case analysis.FnVarOriginVirtual delegates to virtual(s) as expected.


331-357: pushSender: zero-amount fast-path and balance mutation LGTM.

Early return on zero and per-color balance subtraction for real senders are appropriate.


359-369: pushReceiver: correct dispatcher for AccountAddress vs VirtualAccount.

Delegation to pushVirtualReceiver / pushReceiverAddress is clear and cohesive.


371-374: Virtual receive doesn’t sync CachedBalances; confirm invariants.

vacc.Receive appends postings but doesn’t update CachedBalances. Ensure invariants don’t rely on CachedBalances reflecting virtual settlements mid-statement; if they do, update balances accordingly.


441-447: runSaveStatement: restricting to AccountAddress is correct.

expectAccountAddress matches the storage model for saves.


811-813: Early return on zero receive is good.

Avoids unnecessary stack ops and postings for no-ops.


919-921: Kept destination wrapping is correct.

Wrapping KEPT_ADDR as Account{AccountAddress(…)} unifies downstream handling.

Comment on lines +395 to 403
case VirtualAccount:
// Here we have a debt from a virtual acc.
// we don't want to emit that as a posting (but TODO check how does it interact with kept)
senderAccountAddress.Pull(st.CurrentAsset, Sender{
AccountAddress(name),
sender.Amount,
sender.Color,
})
}
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Kept-destination bug for virtual senders.

When destination is KEPT_ADDR and sender is VirtualAccount, we still call Pull, effectively removing value from the virtual account instead of “keeping” it. Mirror the AccountAddress branch: do not create postings, and ensure the amount remains available to the virtual account.

Would you like me to draft a fix once we confirm the intended kept semantics for virtual accounts?


🏁 Script executed:

#!/usr/bin/env bash
set -eo pipefail
# Show VirtualAccount branch context
rg -n 'case VirtualAccount' -C5 internal/interpreter/interpreter.go

# Show AccountAddress branch context
rg -n 'case AccountAddress' -C5 internal/interpreter/interpreter.go

# Find KEPT_ADDR references in interpreter
rg -n 'KEPT_ADDR' -C3 internal/interpreter/interpreter.go

Length of output: 5463


Handle KEPT_ADDR in VirtualAccount branch
In pushReceiverAddress (in internal/interpreter/interpreter.go around lines 394–402), add a check for name == KEPT_ADDR before calling Pull; if the destination is KEPT_ADDR, skip the Pull so the virtual account’s balance isn’t decremented.

🤖 Prompt for AI Agents
In internal/interpreter/interpreter.go around lines 395 to 403, the
VirtualAccount case always calls senderAccountAddress.Pull which wrongly
decrements a virtual account when the destination is KEPT_ADDR; add a guard that
if the destination name equals KEPT_ADDR then skip calling Pull (i.e., do
nothing for Pull in that branch) so virtual account balance is not decremented;
implement the check using the existing KEPT_ADDR constant and leave the rest of
the VirtualAccount logic unchanged.

Comment on lines +656 to 704
switch account := account.Repr.(type) {
case AccountAddress:
var actuallySentAmt *big.Int
if overdraft == nil {
// unbounded overdraft: we send the required amount
actuallySentAmt = new(big.Int).Set(amount)
} else {
balance := s.CachedBalances.fetchBalance(string(account), s.CurrentAsset, *color)

// that's the amount we are allowed to send (balance + overdraft)
actuallySentAmt = CalculateSafeWithdraw(balance, overdraft, amount)
// that's the amount we are allowed to send (balance + overdraft)
actuallySentAmt = CalculateSafeWithdraw(balance, overdraft, amount)
}
return s.pushSender(Sender{account, actuallySentAmt, *color}, nil), nil

case VirtualAccount:
totalSent := big.NewInt(0)

fs := account.getCredits(s.CurrentAsset)
pulledSenders := fs.PullColored(amount, *color)

for _, sender := range pulledSenders {
s.pushSender(sender, totalSent)
}

// if we didn't pull enough
if totalSent.Cmp(amount) == -1 {

// invariant: missingAmt > 0
// (we never pull more than required)
missingAmt := new(big.Int).Sub(amount, totalSent)

var addionalSent *big.Int
if overdraft == nil {
addionalSent = new(big.Int).Set(missingAmt)
} else {
// TODO check this is the correct number to eventually send
// TODO test overdraft
addionalSent = utils.MinBigInt(overdraft, missingAmt)
}

s.pushSender(Sender{account, addionalSent, *color}, totalSent)
}

return totalSent, nil

default:
utils.NonExhaustiveMatchPanic[any](account)
return nil, nil
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fallback to PullAnything when no color is specified.

If evaluateColor returns "", PullColored likely narrows to uncolored only. Use PullAnything in that case to honor “any color”.

Apply this localized change:

-		fs := account.getCredits(s.CurrentAsset)
-		pulledSenders := fs.PullColored(amount, *color)
+		fs := account.getCredits(s.CurrentAsset)
+		var pulledSenders []Sender
+		if *color == "" {
+			pulledSenders = fs.PullAnything(amount)
+		} else {
+			pulledSenders = fs.PullColored(amount, *color)
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch account := account.Repr.(type) {
case AccountAddress:
var actuallySentAmt *big.Int
if overdraft == nil {
// unbounded overdraft: we send the required amount
actuallySentAmt = new(big.Int).Set(amount)
} else {
balance := s.CachedBalances.fetchBalance(string(account), s.CurrentAsset, *color)
// that's the amount we are allowed to send (balance + overdraft)
actuallySentAmt = CalculateSafeWithdraw(balance, overdraft, amount)
// that's the amount we are allowed to send (balance + overdraft)
actuallySentAmt = CalculateSafeWithdraw(balance, overdraft, amount)
}
return s.pushSender(Sender{account, actuallySentAmt, *color}, nil), nil
case VirtualAccount:
totalSent := big.NewInt(0)
fs := account.getCredits(s.CurrentAsset)
pulledSenders := fs.PullColored(amount, *color)
for _, sender := range pulledSenders {
s.pushSender(sender, totalSent)
}
// if we didn't pull enough
if totalSent.Cmp(amount) == -1 {
// invariant: missingAmt > 0
// (we never pull more than required)
missingAmt := new(big.Int).Sub(amount, totalSent)
var addionalSent *big.Int
if overdraft == nil {
addionalSent = new(big.Int).Set(missingAmt)
} else {
// TODO check this is the correct number to eventually send
// TODO test overdraft
addionalSent = utils.MinBigInt(overdraft, missingAmt)
}
s.pushSender(Sender{account, addionalSent, *color}, totalSent)
}
return totalSent, nil
default:
utils.NonExhaustiveMatchPanic[any](account)
return nil, nil
}
case VirtualAccount:
totalSent := big.NewInt(0)
fs := account.getCredits(s.CurrentAsset)
var pulledSenders []Sender
if *color == "" {
pulledSenders = fs.PullAnything(amount)
} else {
pulledSenders = fs.PullColored(amount, *color)
}
for _, sender := range pulledSenders {
s.pushSender(sender, totalSent)
}
// if we didn't pull enough
if totalSent.Cmp(amount) == -1 {
// invariant: missingAmt > 0
// (we never pull more than required)
missingAmt := new(big.Int).Sub(amount, totalSent)
var addionalSent *big.Int
if overdraft == nil {
addionalSent = new(big.Int).Set(missingAmt)
} else {
// TODO check this is the correct number to eventually send
// TODO test overdraft
addionalSent = utils.MinBigInt(overdraft, missingAmt)
}
s.pushSender(Sender{account, addionalSent, *color}, totalSent)
}
return totalSent, nil
🤖 Prompt for AI Agents
internal/interpreter/interpreter.go around lines 656 to 704: in the
VirtualAccount branch we currently call fs.PullColored(amount, *color) which
narrows pulls to a specific color and will ignore the “any color” semantics when
evaluateColor returned an empty string; change the logic to check if *color ==
"" and call fs.PullAnything(amount) in that case, otherwise call
fs.PullColored(amount, *color); wire the resulting pulledSenders through the
same loop and keep the existing missingAmt/overdraft fallback behavior so
semantics are preserved.

Comment on lines +996 to +1027
switch accountRepr := account.Repr.(type) {
case AccountAddress:
account := string(accountRepr)
s.batchQuery(account, asset, nil)
fetchBalanceErr := s.runBalancesQuery()
if fetchBalanceErr != nil {
return nil, QueryBalanceError{WrappedError: fetchBalanceErr}
}
balance := s.CachedBalances.fetchBalance(account, asset, "")
return balance, nil

case VirtualAccount:
fs := accountRepr.credits[s.CurrentAsset]
if fs == nil {
return big.NewInt(0), nil
}

lst := fs.senders
sum := big.NewInt(0)

for lst != nil {
if lst.Head.Color == "" {
sum.Add(sum, lst.Head.Amount)
}
lst = lst.Tail
}
return sum, nil

default:
utils.NonExhaustiveMatchPanic[any](account.Repr)
return nil, nil

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

getBalance uses CurrentAsset for VirtualAccount — wrong asset.

VirtualAccount balance computation ignores the asset param and reads s.CurrentAsset, which can be unset or different. Use the function’s asset argument. Also, keep summing only uncolored credits to match address-path behavior.

Apply this fix:

 func getBalance(
 	s *programState,
-	account Account,
-	asset string,
+	account Account,
+	asset string,
 ) (*big.Int, InterpreterError) {
 	switch accountRepr := account.Repr.(type) {
 	case AccountAddress:
 		account := string(accountRepr)
 		s.batchQuery(account, asset, nil)
 		fetchBalanceErr := s.runBalancesQuery()
 		if fetchBalanceErr != nil {
 			return nil, QueryBalanceError{WrappedError: fetchBalanceErr}
 		}
 		balance := s.CachedBalances.fetchBalance(account, asset, "")
 		return balance, nil
 
 	case VirtualAccount:
-		fs := accountRepr.credits[s.CurrentAsset]
+		fs := accountRepr.credits[asset]
 		if fs == nil {
 			return big.NewInt(0), nil
 		}
 
 		lst := fs.senders
 		sum := big.NewInt(0)
 
 		for lst != nil {
 			if lst.Head.Color == "" {
 				sum.Add(sum, lst.Head.Amount)
 			}
 			lst = lst.Tail
 		}
 		return sum, nil
 
 	default:
 		utils.NonExhaustiveMatchPanic[any](account.Repr)
 		return nil, nil
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
switch accountRepr := account.Repr.(type) {
case AccountAddress:
account := string(accountRepr)
s.batchQuery(account, asset, nil)
fetchBalanceErr := s.runBalancesQuery()
if fetchBalanceErr != nil {
return nil, QueryBalanceError{WrappedError: fetchBalanceErr}
}
balance := s.CachedBalances.fetchBalance(account, asset, "")
return balance, nil
case VirtualAccount:
fs := accountRepr.credits[s.CurrentAsset]
if fs == nil {
return big.NewInt(0), nil
}
lst := fs.senders
sum := big.NewInt(0)
for lst != nil {
if lst.Head.Color == "" {
sum.Add(sum, lst.Head.Amount)
}
lst = lst.Tail
}
return sum, nil
default:
utils.NonExhaustiveMatchPanic[any](account.Repr)
return nil, nil
func getBalance(
s *programState,
account Account,
asset string,
) (*big.Int, InterpreterError) {
switch accountRepr := account.Repr.(type) {
case AccountAddress:
account := string(accountRepr)
s.batchQuery(account, asset, nil)
fetchBalanceErr := s.runBalancesQuery()
if fetchBalanceErr != nil {
return nil, QueryBalanceError{WrappedError: fetchBalanceErr}
}
balance := s.CachedBalances.fetchBalance(account, asset, "")
return balance, nil
case VirtualAccount:
// Use the function’s asset argument instead of s.CurrentAsset
fs := accountRepr.credits[asset]
if fs == nil {
return big.NewInt(0), nil
}
lst := fs.senders
sum := big.NewInt(0)
for lst != nil {
if lst.Head.Color == "" {
sum.Add(sum, lst.Head.Amount)
}
lst = lst.Tail
}
return sum, nil
default:
utils.NonExhaustiveMatchPanic[any](account.Repr)
return nil, nil
}
}
🤖 Prompt for AI Agents
In internal/interpreter/interpreter.go around lines 996 to 1027, the
VirtualAccount branch is using s.CurrentAsset instead of the function's asset
parameter and thus may compute balances for the wrong asset; change fs :=
accountRepr.credits[s.CurrentAsset] to fs := accountRepr.credits[asset] so the
lookup uses the passed-in asset, and keep the existing Color == "" check so only
uncolored credits are summed (preserving address-path behavior); ensure nil
checks remain and return the computed sum as before.

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.

1 participant