Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions internal/analysis/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const FnVarOriginBalance = "balance"
const FnVarOriginOverdraft = "overdraft"
const FnVarOriginGetAsset = "get_asset"
const FnVarOriginGetAmount = "get_amount"
const FnVarOriginVirtual = "virtual"

var Builtins = map[string]FnCallResolution{
FnSetTxMeta: StatementFnCallResolution{
Expand Down Expand Up @@ -114,6 +115,17 @@ var Builtins = map[string]FnCallResolution{
},
},
},
FnVarOriginVirtual: VarOriginFnCallResolution{
Params: []string{},
Return: TypeAccount,
Docs: "create a virtual account",
VersionConstraints: []VersionClause{
{
Version: parser.NewVersionInterpreter(0, 0, 20),
FeatureFlag: flags.ExperimentalVirtualAccount,
},
},
},
}

type Diagnostic struct {
Expand Down
2 changes: 2 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
ExperimentalAccountInterpolationFlag FeatureFlag = "experimental-account-interpolation"
ExperimentalMidScriptFunctionCall FeatureFlag = "experimental-mid-script-function-call"
ExperimentalAssetColors FeatureFlag = "experimental-asset-colors"
ExperimentalVirtualAccount FeatureFlag = "experimental-virtual-account"
)
Comment on lines +13 to 14
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.


var AllFlags []string = []string{
Expand All @@ -20,4 +21,5 @@ var AllFlags []string = []string{
ExperimentalAccountInterpolationFlag,
ExperimentalMidScriptFunctionCall,
ExperimentalAssetColors,
ExperimentalVirtualAccount,
}
8 changes: 4 additions & 4 deletions internal/interpreter/args_parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func TestParseValid(t *testing.T) {

p := NewArgsParser([]Value{
NewMonetaryInt(42),
AccountAddress("user:001"),
Account{Repr: AccountAddress("user:001")},
})
a1 := parseArg(p, parser.Range{}, expectNumber)
a2 := parseArg(p, parser.Range{}, expectAccount)
Expand All @@ -47,16 +47,16 @@ func TestParseValid(t *testing.T) {
require.NotNil(t, a1, "a1 should not be nil")
require.NotNil(t, a2, "a2 should not be nil")

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

func TestParseBadType(t *testing.T) {
t.Parallel()

p := NewArgsParser([]Value{
NewMonetaryInt(42),
AccountAddress("user:001"),
Account{Repr: AccountAddress("user:001")},
})
parseArg(p, parser.Range{}, expectMonetary)
parseArg(p, parser.Range{}, expectAccount)
Expand Down
16 changes: 13 additions & 3 deletions internal/interpreter/batch_balances_query.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ func (st *programState) findBalancesQueriesInStatement(statement parser.Statemen
if err != nil {
return err
}
st.batchQuery(*account, *asset, nil)

if account, ok := account.Repr.(AccountAddress); ok {
st.batchQuery(string(account), *asset, nil)
}

return nil

case *parser.SendStatement:
Expand Down Expand Up @@ -95,7 +99,10 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr
return err
}

st.batchQuery(*account, st.CurrentAsset, color)
if account, ok := account.Repr.(AccountAddress); ok {
st.batchQuery(string(account), st.CurrentAsset, color)
}

return nil

case *parser.SourceOverdraft:
Expand All @@ -113,7 +120,10 @@ func (st *programState) findBalancesQueries(source parser.Source) InterpreterErr
return err
}

st.batchQuery(*account, st.CurrentAsset, color)
if account, ok := account.Repr.(AccountAddress); ok {
st.batchQuery(string(account), st.CurrentAsset, color)
}

return nil

case *parser.SourceInorder:
Expand Down
2 changes: 1 addition & 1 deletion internal/interpreter/evaluate_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ func (st *programState) divOp(rng parser.Range, left parser.ValueExpr, right par

func castToString(v Value, rng parser.Range) (string, InterpreterError) {
switch v := v.(type) {
case AccountAddress:
case Account:
return v.String(), nil
case String:
return v.String(), nil
Expand Down
16 changes: 13 additions & 3 deletions internal/interpreter/function_exprs.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ func overdraft(

// TODO more precise args range location
p := NewArgsParser(args)
account := parseArg(p, r, expectAccount)
account := parseArg(p, r, expectAccountAddress) // TODO also handle virtual account
asset := parseArg(p, r, expectAsset)
err = p.parse()
if err != nil {
return nil, err
}

balance_, err := getBalance(s, *account, *asset)
balance_, err := getBalance(s, Account{Repr: AccountAddress(*account)}, *asset)
if err != nil {
return nil, err
}
Expand All @@ -53,7 +53,7 @@ func meta(
) (string, InterpreterError) {
// TODO more precise location
p := NewArgsParser(args)
account := parseArg(p, rng, expectAccount)
account := parseArg(p, rng, expectAccountAddress)
key := parseArg(p, rng, expectString)
err := p.parse()
if err != nil {
Expand Down Expand Up @@ -155,3 +155,13 @@ func getAmount(

return mon.Amount, nil
}

func virtual(
s *programState,
) (Value, InterpreterError) {
err := s.checkFeatureFlag(flags.ExperimentalVirtualAccount)
if err != nil {
return nil, err
}
return Account{Repr: NewVirtualAccount()}, nil
}
2 changes: 1 addition & 1 deletion internal/interpreter/function_statements.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ func setTxMeta(st *programState, r parser.Range, args []Value) InterpreterError

func setAccountMeta(st *programState, r parser.Range, args []Value) InterpreterError {
p := NewArgsParser(args)
account := parseArg(p, r, expectAccount)
account := parseArg(p, r, expectAccountAddress)
key := parseArg(p, r, expectString)
meta := parseArg(p, r, expectAnything)
err := p.parse()
Expand Down
26 changes: 13 additions & 13 deletions internal/interpreter/funds_stack.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import (
)

type Sender struct {
Name string
Amount *big.Int
Color string
Account AccountValue
Amount *big.Int
Color string
}

type stack[T any] struct {
Expand Down Expand Up @@ -76,15 +76,15 @@ func (s *fundsStack) compactTop() {
continue
}

if first.Name != second.Name || first.Color != second.Color {
if first.Account != second.Account || first.Color != second.Color {
return
}
Comment on lines +79 to 81
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.


s.senders = &stack[Sender]{
Head: Sender{
Name: first.Name,
Color: first.Color,
Amount: new(big.Int).Add(first.Amount, second.Amount),
Account: first.Account,
Color: first.Color,
Amount: new(big.Int).Add(first.Amount, second.Amount),
},
Tail: s.senders.Tail.Tail,
}
Expand Down Expand Up @@ -152,19 +152,19 @@ func (s *fundsStack) Pull(requiredAmount *big.Int, color *string) []Sender {
case 1: // more than enough
s.senders = &stack[Sender]{
Head: Sender{
Name: available.Name,
Color: available.Color,
Amount: new(big.Int).Sub(available.Amount, requiredAmount),
Account: available.Account,
Color: available.Color,
Amount: new(big.Int).Sub(available.Amount, requiredAmount),
},
Tail: s.senders,
}
fallthrough

case 0: // exactly the same
out = append(out, Sender{
Name: available.Name,
Color: available.Color,
Amount: new(big.Int).Set(requiredAmount),
Account: available.Account,
Color: available.Color,
Amount: new(big.Int).Set(requiredAmount),
})
return out
}
Expand Down
Loading
Loading