Skip to content

feat: resolve dependencies#169

Open
ascandone wants to merge 12 commits into
mainfrom
feat/resolve-dependencies-simplified
Open

feat: resolve dependencies#169
ascandone wants to merge 12 commits into
mainfrom
feat/resolve-dependencies-simplified

Conversation

@ascandone

@ascandone ascandone commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

This PR:

  1. (refactor) extracts an utility that evaluates expressions and handles vars on its own (internal/interpreter/evaluate_expr.go), so that the interpreter can eval exprs using that one instead of the whole interpreter state
  2. (feat) implements a static analysis that (after vars substitutions, given some balances) finds what accounts balances we need to read/write, what account meta we need to read/write, and what tx meta we need to write

The exact algo for balances tracking is:

  1. we register account destinations as writes. We mark each dest with each color that appears anywhere in the source
  2. we register account sources as writes+reads if they have bounded overdraft, or as writes if they are unbounded overdraft.
    1. note: if allowing unbounded overdraft or allowing overdraft up to .. isn't specified, it counts as bounded overdraft, unless it's @world. Otherwise, that's the source of truth. For example: the @world allowing overdraft up to [ASSET 0] counts as bounded overdraft (recorded as read+write)
  3. we register balance() and overdraft() calls as (uncolored) reads

In order to get 3. properly, we make sure we eval every single expr of the ast (even though it's not used as source or dest). For example, a "max" clause could be using a balance() expr, which we need to track

Note that we explicitly avoid scaling to be implemented, and output an error. That's ok, as scaling isn't documented yet, nor properly specified

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f21ea7a2-2e19-4ac8-8382-894696a01ae5

📥 Commits

Reviewing files that changed from the base of the PR and between 5d119e3 and 92efd3e.

📒 Files selected for processing (3)
  • internal/interpreter/evaluate_expr.go
  • internal/interpreter/internal_accounts_metadata.go
  • internal/interpreter/resolve_dependencies_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/interpreter/evaluate_expr.go
  • internal/interpreter/resolve_dependencies_test.go

Walkthrough

This PR moves interpreter expression evaluation to a standalone evalEnv, updates execution and balance-query paths, and adds static dependency resolution for account and metadata reads/writes through internal and public APIs with comprehensive tests.

Changes

evalEnv refactor and dependency resolution

Layer / File(s) Summary
evalEnv type and core expression evaluation
internal/interpreter/evaluate_expr.go, internal/interpreter/interpreter_error.go
Adds env-based expression evaluation, variable binding, arithmetic helpers, and explicit unhandled-AST errors.
Function expressions and infix operators on evalEnv
internal/interpreter/function_exprs.go, internal/interpreter/infix_overloads.go
Updates function helpers, balance and metadata lookups, feature flags, and monetary operators to use evalEnv.
Interpreter statement execution wired to evalEnv
internal/interpreter/interpreter.go
Initializes evalEnv, narrows programState, and updates statement, source, destination, and sent-amount evaluation.
Batch balance-query discovery updated to evalEnv
internal/interpreter/batch_balances_query.go
Uses evalEnv for account and asset evaluation and removes destination prefetching from send handling.
ResolveDependencies implementation
internal/interpreter/resolve_dependencies.go
Adds dependency models and records account, metadata, and transaction metadata reads and writes while traversing programs.
ResolveDependencies tests and public API
internal/interpreter/resolve_dependencies_test.go, internal/interpreter/internal_accounts_metadata.go, numscript.go, numscript_test.go
Adds dependency-resolution coverage, metadata cache merging, public aliases, and ParseResult API validation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant ResolveDependencies
  participant recordingStore
  participant evalEnv
  participant Store
  Caller->>ResolveDependencies: program, vars, store
  ResolveDependencies->>recordingStore: wrap Store
  ResolveDependencies->>evalEnv: initialize evaluation context
  ResolveDependencies->>recordingStore: evaluate statements
  recordingStore->>Store: query balances or account metadata
  Store-->>recordingStore: return queried values
  ResolveDependencies-->>Caller: return ResolvedDependencies
Loading

Possibly related PRs

Suggested reviewers: Azorlogh, gfyrag, paul-nicolas

Poem

I hopped through evalEnv bright,
Tracking balances left and right.
Metadata joined the cheerful flow,
Reads and writes now clearly show. 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.70% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: dependency resolution was added.
Description check ✅ Passed The description accurately summarizes the refactor and new static dependency analysis in the changeset.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/resolve-dependencies-simplified

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


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.

@codecov

codecov Bot commented Jul 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.30303% with 78 lines in your changes missing coverage. Please review.
✅ Project coverage is 70.66%. Comparing base (fadd1f8) to head (92efd3e).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
internal/interpreter/resolve_dependencies.go 71.13% 33 Missing and 23 partials ⚠️
internal/interpreter/evaluate_expr.go 90.10% 7 Missing and 2 partials ⚠️
internal/interpreter/function_exprs.go 85.71% 3 Missing and 3 partials ⚠️
internal/interpreter/interpreter_error.go 0.00% 4 Missing ⚠️
numscript.go 50.00% 1 Missing and 1 partial ⚠️
internal/interpreter/interpreter.go 97.50% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #169      +/-   ##
==========================================
- Coverage   70.71%   70.66%   -0.05%     
==========================================
  Files          57       58       +1     
  Lines        5262     5482     +220     
==========================================
+ Hits         3721     3874     +153     
- Misses       1328     1370      +42     
- Partials      213      238      +25     

☔ View full report in Codecov by Harness.
📢 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/resolve-dependencies-simplified branch 3 times, most recently from 63d72f4 to 860fa76 Compare July 9, 2026 17:00
@ascandone ascandone marked this pull request as ready for review July 9, 2026 17:01
Base automatically changed from feat/scoped-accounts to main July 9, 2026 17:01
@NumaryBot

NumaryBot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

✅ Approve — automated review

All previously identified issues have been addressed or dismissed. The major finding about unrecognized top-level function calls silently succeeding in ResolveDependencies was raised in multiple prior threads and has been marked as resolved. The suggestion to validate callees before evaluating arguments in function_exprs.go was explicitly dismissed by a project maintainer. The single remaining reviewer found no new actionable correctness issues. There are no outstanding blockers or major findings.

No findings.

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #169 (comment)

Comment thread internal/interpreter/resolve_dependencies.go
@ascandone ascandone force-pushed the feat/resolve-dependencies-simplified branch from 860fa76 to b928a5a Compare July 9, 2026 17:10

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot posted 1 new inline finding.

Summary: #169 (comment)

Comment thread internal/interpreter/resolve_dependencies.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

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

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

320-331: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return UnboundFunctionErr before evaluating top-level call args.

For unsupported statement calls, arguments are evaluated before the callee is rejected. That can mask the unbound-function error and trigger store reads for a statement that will never run.

🐛 Proposed dispatch-first structure
 	case *parser.FnCall:
-		args, err := evaluateExpressions(&st.evalEnv, statement.Args)
-		if err != nil {
-			return err
-		}
-
 		switch statement.Caller.Name {
 		case analysis.FnSetTxMeta:
+			args, err := evaluateExpressions(&st.evalEnv, statement.Args)
+			if err != nil {
+				return err
+			}
 			return setTxMeta(st, statement.Caller.Range, args)
 		case analysis.FnSetAccountMeta:
+			args, err := evaluateExpressions(&st.evalEnv, statement.Args)
+			if err != nil {
+				return err
+			}
 			return setAccountMeta(st, statement.Caller.Range, args)
 		default:
 			return UnboundFunctionErr{Name: statement.Caller.Name}
 		}
🤖 Prompt for 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.

In `@internal/interpreter/interpreter.go` around lines 320 - 331, The dispatch in
the statement handler is evaluating arguments before confirming the callee is
supported, which can hide the unbound-function error and cause unnecessary store
reads. In the statement execution path around the switch on
statement.Caller.Name, check for supported functions first and return
UnboundFunctionErr immediately for anything else, then only call
evaluateExpressions for the recognized cases before invoking setTxMeta or
setAccountMeta.
internal/interpreter/batch_balances_query.go (1)

93-103: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Mirror the asset-color feature guard during preloading.

findBalancesQueries evaluates color expressions and batches colored balance reads before runtime checks ExperimentalAssetColors. If the flag is disabled, preloading can return a color-expression error or hit storage before the feature error.

🐛 Proposed guard
 	case *parser.SourceAccount:
 		account, err := evaluateExprAs(&st.evalEnv, source.ValueExpr, expectAccount)
 		if err != nil {
 			return err
 		}
 
+		if source.Color != nil {
+			if err := st.checkFeatureFlag(flags.ExperimentalAssetColors); err != nil {
+				return err
+			}
+		}
 		color, err := evaluateOptExprAs(&st.evalEnv, source.Color, expectString)
 		if err != nil {
 			return err
 		}
 
 		st.batchQuery(account, st.CurrentAsset, color)
 		return nil
@@
 		account, err := evaluateExprAs(&st.evalEnv, source.Address, expectAccount)
 		if err != nil {
 			return err
 		}
+		if source.Color != nil {
+			if err := st.checkFeatureFlag(flags.ExperimentalAssetColors); err != nil {
+				return err
+			}
+		}
 		color, err := evaluateOptExprAs(&st.evalEnv, source.Color, expectString)

Also applies to: 122-131

🤖 Prompt for 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.

In `@internal/interpreter/batch_balances_query.go` around lines 93 - 103, Mirror
the ExperimentalAssetColors guard in findBalancesQueries before evaluating
source.Color or calling st.batchQuery, so preload behavior matches runtime
checks. Update the findBalancesQueries flow (including the batch path at both
affected sites) to skip colored balance preloading or return the same
feature-disabled error when the flag is off, rather than evaluating the color
expression or touching storage first.
🧹 Nitpick comments (2)
internal/interpreter/resolve_dependencies_test.go (1)

43-177: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a save case to TestResolveDependenciesCoversRuntime.

The soundness test validates that resolved writes cover runtime postings, but has no save test case. This gap means the SaveStatement handler's read-only treatment of the source account (lines 136-146 in resolve_dependencies.go) is never validated against actual runtime postings. Adding a save case would confirm whether the current read-only classification is correct or if a write is missing.

🤖 Prompt for 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.

In `@internal/interpreter/resolve_dependencies_test.go` around lines 43 - 177, Add
a new `save` scenario to `TestResolveDependenciesCoversRuntime` so the soundness
check exercises `SaveStatement` alongside the existing `send` cases. Use
`parser.Parse`, `interpreter.RunProgram`, and `interpreter.ResolveDependencies`
in the same pattern as the other table-driven cases, with a source account and
destination/recipient setup that produces runtime postings from `save`. Then
verify the resolved `AccountsReads` and `AccountsWrites` against the
`result.Postings` the same way the test already does, so the `SaveStatement`
handling in `resolve_dependencies.go` is covered.
internal/interpreter/evaluate_expr.go (1)

63-72: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Use the metadata cache instead of replacing it per lookup.

getMetadata always calls Store and overwrites CachedAccountsMeta, so repeated or multi-key meta() calls evict prior cached values and can re-hit storage unnecessarily. Mirror getBalance by checking cached values first and merging query results.

♻️ Proposed cache merge
 func (env *evalEnv) getMetadata(account AccountAddress, key string) (string, bool, InterpreterError) {
+	if value, ok := env.CachedAccountsMeta.Get(account.Name, account.Scope, key); ok {
+		return value, true, nil
+	}
+
 	rows, err := env.Store.GetAccountsMetadata(env.ctx, MetadataQuery{
 		{Account: account.Name, Scope: account.Scope, Keys: []string{key}},
 	})
 	if err != nil {
 		return "", false, QueryMetadataError{WrappedError: err}
 	}
-	env.CachedAccountsMeta = FromAccountsMetadataRows(rows)
+	for k, value := range FromAccountsMetadataRows(rows) {
+		env.CachedAccountsMeta[k] = value
+	}
 
 	value, ok := env.CachedAccountsMeta.Get(account.Name, account.Scope, key)
 	return value, ok, nil
 }
🤖 Prompt for 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.

In `@internal/interpreter/evaluate_expr.go` around lines 63 - 72, getMetadata
currently overwrites CachedAccountsMeta on every lookup, so repeated meta()
calls can evict earlier entries and always re-query Store. Update
evalEnv.getMetadata to first check env.CachedAccountsMeta for the requested
account/key, and only call Store.GetAccountsMetadata when the value is missing.
When new rows are fetched, merge them into the existing cache instead of
replacing it, mirroring the getBalance caching pattern.
🤖 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/function_exprs.go`:
- Around line 18-27: The callee in function expression handling is being
validated too late, which allows argument evaluation to run before rejecting
unknown functions or invalid nested meta calls. In function_exprs.go, update the
function expression path around evaluateExpressions and the fnCall.Caller.Name
switch so the callee/default branch is checked first, returning
UnboundFunctionErr or InvalidNestedMeta before any argument evaluation or store
reads. Keep the guard logic near the existing fnCall.Caller.Name handling so
FunctionExpr evaluation short-circuits correctly.

In `@internal/interpreter/resolve_dependencies.go`:
- Around line 185-186: The default branch in the dependency resolver is
swallowing unknown top-level function calls by returning nil, which lets invalid
scripts pass; update the logic in resolve_dependencies.go so the function-call
handling for set_account_meta and set_tx_meta explicitly returns an error for
any other call, matching the interpreter’s UnboundFunctionErr behavior. Make the
change in the switch’s default path so dependency resolution fails consistently
when an unrecognized top-level function is encountered.

---

Outside diff comments:
In `@internal/interpreter/batch_balances_query.go`:
- Around line 93-103: Mirror the ExperimentalAssetColors guard in
findBalancesQueries before evaluating source.Color or calling st.batchQuery, so
preload behavior matches runtime checks. Update the findBalancesQueries flow
(including the batch path at both affected sites) to skip colored balance
preloading or return the same feature-disabled error when the flag is off,
rather than evaluating the color expression or touching storage first.

In `@internal/interpreter/interpreter.go`:
- Around line 320-331: The dispatch in the statement handler is evaluating
arguments before confirming the callee is supported, which can hide the
unbound-function error and cause unnecessary store reads. In the statement
execution path around the switch on statement.Caller.Name, check for supported
functions first and return UnboundFunctionErr immediately for anything else,
then only call evaluateExpressions for the recognized cases before invoking
setTxMeta or setAccountMeta.

---

Nitpick comments:
In `@internal/interpreter/evaluate_expr.go`:
- Around line 63-72: getMetadata currently overwrites CachedAccountsMeta on
every lookup, so repeated meta() calls can evict earlier entries and always
re-query Store. Update evalEnv.getMetadata to first check env.CachedAccountsMeta
for the requested account/key, and only call Store.GetAccountsMetadata when the
value is missing. When new rows are fetched, merge them into the existing cache
instead of replacing it, mirroring the getBalance caching pattern.

In `@internal/interpreter/resolve_dependencies_test.go`:
- Around line 43-177: Add a new `save` scenario to
`TestResolveDependenciesCoversRuntime` so the soundness check exercises
`SaveStatement` alongside the existing `send` cases. Use `parser.Parse`,
`interpreter.RunProgram`, and `interpreter.ResolveDependencies` in the same
pattern as the other table-driven cases, with a source account and
destination/recipient setup that produces runtime postings from `save`. Then
verify the resolved `AccountsReads` and `AccountsWrites` against the
`result.Postings` the same way the test already does, so the `SaveStatement`
handling in `resolve_dependencies.go` is covered.
🪄 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: 750cbe94-e3e4-476d-a7ac-5299ed11cc7b

📥 Commits

Reviewing files that changed from the base of the PR and between fadd1f8 and b928a5a.

📒 Files selected for processing (9)
  • internal/interpreter/batch_balances_query.go
  • internal/interpreter/evaluate_expr.go
  • internal/interpreter/function_exprs.go
  • internal/interpreter/infix_overloads.go
  • internal/interpreter/interpreter.go
  • internal/interpreter/resolve_dependencies.go
  • internal/interpreter/resolve_dependencies_test.go
  • numscript.go
  • numscript_test.go

Comment thread internal/interpreter/function_exprs.go
Comment thread internal/interpreter/resolve_dependencies.go Outdated

@NumaryBot NumaryBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

NumaryBot review complete: no remaining inline findings.

Resolved 1 stale NumaryBot review thread (1 fixed, 0 outdated).

Summary: #169 (comment)

@ascandone ascandone requested review from Azorlogh and gfyrag July 10, 2026 08:52
@Azorlogh

Copy link
Copy Markdown
Contributor

This seems to miss a metadata read

func TestResolveMultipleMetaKeysAreAllReads(t *testing.T) {
      deps := resolve(t, `
              vars {
                      account $x = meta(@config, "recipient")
                      account $y = meta(@config, "fallback")
              }
              send [USD 10] (
                      source = $x allowing unbounded overdraft
                      destination = $y
              )
      `, nil, interpreter.StaticStore{
              Meta: interpreter.AccountsMetadata{
                      {Account: "config", Key: "recipient", Value: "alice"},
                      {Account: "config", Key: "fallback", Value: "bob"},
              },
      })

      require.Equal(t, map[interpreter.MetaDependency]struct{}{
              {Account: "config", Key: "recipient"}: {},
              {Account: "config", Key: "fallback"}:  {},
      }, deps.MetaReads)
}

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants