Files
zeekayandClaude Opus 5 2c2f0e6bf3 Go 1.27.1, and the code it can say more simply
Modules declare go 1.27.1 and the build images follow. go fix applied the
modernizers, and the interpreter's parseArg, evaluateExprAs and
evaluateOptExprAs are now generic methods on the parser and program state
they take. omitzero is not applied, since it changes what the JSON says.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-16 19:52:45 -07:00

1011 lines
25 KiB
Go

package analysis
import (
"maps"
"math/big"
"slices"
"strings"
"github.com/formancehq/numscript/internal/flags"
"github.com/formancehq/numscript/internal/parser"
"github.com/formancehq/numscript/internal/utils"
)
const TypeMonetary = "monetary"
const TypeAccount = "account"
const TypePortion = "portion"
const TypeAsset = "asset"
const TypeNumber = "number"
const TypeString = "string"
const TypeAny = "any"
var AllowedTypes = []string{
TypeMonetary,
TypeAccount,
TypePortion,
TypeAsset,
TypeNumber,
TypeString,
}
type FnCallResolution interface {
ContextName() string
GetParams() []string
fnCallResolution()
}
type VarOriginFnCallResolution struct {
Params []string
Docs string
Return string
VersionConstraints []VersionClause
}
type StatementFnCallResolution struct {
Params []string
Docs string
}
func (VarOriginFnCallResolution) ContextName() string { return "variable origin" }
func (StatementFnCallResolution) ContextName() string { return "statement" }
func (VarOriginFnCallResolution) fnCallResolution() {}
func (StatementFnCallResolution) fnCallResolution() {}
func (r VarOriginFnCallResolution) GetParams() []string { return r.Params }
func (r StatementFnCallResolution) GetParams() []string { return r.Params }
const FnSetTxMeta = "set_tx_meta"
const FnSetAccountMeta = "set_account_meta"
const FnVarOriginMeta = "meta"
const FnVarOriginBalance = "balance"
const FnVarOriginOverdraft = "overdraft"
const FnVarOriginGetAsset = "get_asset"
const FnVarOriginGetAmount = "get_amount"
var Builtins = map[string]FnCallResolution{
FnSetTxMeta: StatementFnCallResolution{
Params: []string{TypeString, TypeAny},
Docs: "set transaction metadata",
},
FnSetAccountMeta: StatementFnCallResolution{
Params: []string{TypeAccount, TypeString, TypeAny},
Docs: "set account metadata",
},
FnVarOriginMeta: VarOriginFnCallResolution{
Params: []string{TypeAccount, TypeString},
Return: TypeAny,
Docs: "fetch account metadata",
},
FnVarOriginBalance: VarOriginFnCallResolution{
Params: []string{TypeAccount, TypeAsset},
Return: TypeMonetary,
Docs: "fetch account balance",
},
FnVarOriginOverdraft: VarOriginFnCallResolution{
Params: []string{TypeAccount, TypeAsset},
Return: TypeMonetary,
Docs: "get absolute amount of the overdraft of an account. Returns zero if balance is not negative",
VersionConstraints: []VersionClause{
{
Version: parser.NewVersionInterpreter(0, 0, 15),
FeatureFlag: flags.ExperimentalOverdraftFunctionFeatureFlag,
},
},
},
FnVarOriginGetAsset: VarOriginFnCallResolution{
Params: []string{TypeMonetary},
Return: TypeAsset,
Docs: "get the asset of the given monetary",
VersionConstraints: []VersionClause{
{
Version: parser.NewVersionInterpreter(0, 0, 16),
FeatureFlag: flags.ExperimentalGetAssetFunctionFeatureFlag,
},
},
},
FnVarOriginGetAmount: VarOriginFnCallResolution{
Params: []string{TypeMonetary},
Return: TypeNumber,
Docs: "get the amount of the given monetary",
VersionConstraints: []VersionClause{
{
Version: parser.NewVersionInterpreter(0, 0, 16),
FeatureFlag: flags.ExperimentalGetAmountFunctionFeatureFlag,
},
},
},
}
type Diagnostic struct {
Range parser.Range
Kind DiagnosticKind
Id int32
}
type CheckResult struct {
nextDiagnosticId int32
unboundedAccountInSend parser.ValueExpr
emptiedAccount map[string]struct{}
unboundedSend bool
DeclaredVars map[string]parser.VarDeclaration
unusedVars map[string]parser.Range
varResolution map[*parser.Variable]parser.VarDeclaration
fnCallResolution map[*parser.FnCallIdentifier]FnCallResolution
Diagnostics []Diagnostic
Program parser.Program
stmtType Type
exprTypes map[parser.ValueExpr]Type
varTypes map[parser.VarDeclaration]Type
}
func (r *CheckResult) GetExprType(expr parser.ValueExpr) Type {
exprType, ok := r.exprTypes[expr]
if !ok {
t := TVar{}
r.exprTypes[expr] = &t
return &t
}
return exprType.Resolve()
}
func (r *CheckResult) GetVarDeclType(decl parser.VarDeclaration) Type {
exprType, ok := r.varTypes[decl]
if !ok {
t := TVar{}
r.varTypes[decl] = &t
return &t
}
return exprType.Resolve()
}
func (r *CheckResult) unifyNodeWith(expr parser.ValueExpr, t Type) {
exprT := r.GetExprType(expr)
r.unify(expr.GetRange(), exprT, t)
}
func (r *CheckResult) unify(rng parser.Range, t1 Type, t2 Type) {
ok := Unify(t1, t2)
if ok {
return
}
r.Diagnostics = append(r.Diagnostics, Diagnostic{
Range: rng,
Kind: AssetMismatch{
Expected: TypeToString(t1),
Got: TypeToString(t2),
},
})
}
func (r CheckResult) GetErrorsCount() int {
c := 0
for _, d := range r.Diagnostics {
if d.Kind.Severity() == ErrorSeverity {
c++
}
}
return c
}
func (r CheckResult) GetWarningsCount() int {
c := 0
for _, d := range r.Diagnostics {
if d.Kind.Severity() == WarningSeverity {
c++
}
}
return c
}
func (r CheckResult) ResolveVar(v *parser.Variable) *parser.VarDeclaration {
k, ok := r.varResolution[v]
if !ok {
return nil
}
return &k
}
func (r CheckResult) ResolveBuiltinFn(v *parser.FnCallIdentifier) FnCallResolution {
k, ok := r.fnCallResolution[v]
if !ok {
return nil
}
return k
}
func newCheckResult(program parser.Program) CheckResult {
return CheckResult{
Program: program,
emptiedAccount: make(map[string]struct{}),
DeclaredVars: make(map[string]parser.VarDeclaration),
unusedVars: make(map[string]parser.Range),
varResolution: make(map[*parser.Variable]parser.VarDeclaration),
fnCallResolution: make(map[*parser.FnCallIdentifier]FnCallResolution),
exprTypes: make(map[parser.ValueExpr]Type),
varTypes: make(map[parser.VarDeclaration]Type),
}
}
func (res *CheckResult) check() {
for _, flag := range res.Program.Flags {
validFlag := slices.Contains(flags.AllFlags, flag.String)
if !validFlag {
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Range: flag.Range,
Kind: InvalidFeature{Feature: flag.String},
})
}
}
if res.Program.Vars != nil {
for _, varDecl := range res.Program.Vars.Declarations {
if varDecl.Type != nil {
res.checkVarType(*varDecl.Type)
}
if varDecl.Name != nil {
res.checkDuplicateVars(*varDecl.Name, varDecl)
}
if varDecl.Origin != nil {
res.checkExpression(*varDecl.Origin, varDecl.Type.Name)
res.unifyNodeWith(*varDecl.Origin, res.GetVarDeclType(varDecl))
}
}
}
for _, statement := range res.Program.Statements {
res.unboundedAccountInSend = nil
res.checkStatement(statement)
}
// after static AST traversal is complete, check for unused vars
for name, rng := range res.unusedVars {
res.pushDiagnostic(rng, UnusedVar{Name: name})
}
res.checkBoundVars()
}
func (res *CheckResult) checkBoundVars() {
if res.Program.Vars == nil {
return
}
// heuristic: we skipCheck if we already emit type mismatch or asset mismatch
// even though they could have been related to a different part of the program
// the rationale is that
// 1. it'd be hard to track whose variable the unification error belong from and it would require adding injustified complexity
// 2. we don't have to "pollute" the diagnostics with so many errors all at once
skipCheck := slices.ContainsFunc(res.Diagnostics, func(d Diagnostic) bool {
_, isAssetMismatch := d.Kind.(AssetMismatch)
if isAssetMismatch {
return true
}
_, isTypeMismatch := d.Kind.(TypeMismatch)
return isTypeMismatch
})
if skipCheck {
return
}
for _, varDecl := range res.Program.Vars.Declarations {
if varDecl.Origin != nil {
continue
}
t := res.GetVarDeclType(varDecl)
boundType, isBound := t.(*TAsset)
if !isBound {
continue
}
if varDecl.Type == nil {
continue
}
var boundAssetType BoundAssetType
switch varDecl.Type.Name {
case TypeAsset:
boundAssetType = BoundAssetTypeAsset
case TypeMonetary:
boundAssetType = BoundAssetTypeMonetary
}
res.pushDiagnostic(varDecl.Range, BoundAsset{
BoundAssetType: boundAssetType,
InferredAsset: string(*boundType),
})
}
}
func (res *CheckResult) checkStatement(statement parser.Statement) {
res.emptiedAccount = make(map[string]struct{})
res.stmtType = &TVar{}
switch statement := statement.(type) {
case *parser.SaveStatement:
res.checkSentValue(statement.SentValue)
res.checkExpression(statement.Account, TypeAccount)
case *parser.SendStatement:
_, isSendAll := statement.SentValue.(*parser.SentValueAll)
res.unboundedSend = isSendAll
res.checkSentValue(statement.SentValue)
res.checkSource(statement.Source)
res.checkDestination(statement.Destination)
case *parser.FnCall:
resolution, ok := Builtins[statement.Caller.Name]
if ok {
if varOrigin, ok := resolution.(StatementFnCallResolution); ok {
res.fnCallResolution[statement.Caller] = varOrigin
}
}
// This must come after resolution
res.checkFnCallArity(statement)
}
}
func CheckProgram(program parser.Program) CheckResult {
res := newCheckResult(program)
res.check()
return res
}
func CheckSource(source string) CheckResult {
result := parser.Parse(source)
res := newCheckResult(result.Value)
for _, parserError := range result.Errors {
res.pushDiagnostic(parserError.Range, Parsing{Description: parserError.Msg})
}
res.check()
return res
}
func (res *CheckResult) checkFnCallArity(fnCall *parser.FnCall) {
resolution, resolved := res.fnCallResolution[fnCall.Caller]
var validArgs []parser.ValueExpr
for _, lit := range fnCall.Args {
if lit != nil {
validArgs = append(validArgs, lit)
}
}
if resolved {
sig := resolution.GetParams()
actualArgs := len(validArgs)
expectedArgs := len(sig)
if actualArgs < expectedArgs {
// Too few args
res.pushDiagnostic(fnCall.Range, BadArity{
Expected: expectedArgs,
Actual: actualArgs,
})
} else if actualArgs > expectedArgs {
// Too many args
firstIllegalArg := validArgs[expectedArgs]
lastIllegalArg := validArgs[len(validArgs)-1]
if lastIllegalArg != nil {
rng := parser.Range{
Start: firstIllegalArg.GetRange().Start,
End: lastIllegalArg.GetRange().End,
}
res.pushDiagnostic(rng, BadArity{
Expected: expectedArgs,
Actual: actualArgs,
})
}
}
for index, arg := range validArgs {
lastElemIndex := len(sig) - 1
if index > lastElemIndex {
break
}
type_ := sig[index]
res.checkExpression(arg, type_)
}
switch fnCall.Caller.Name {
case FnVarOriginBalance, FnVarOriginOverdraft:
if len(validArgs) > 1 {
// we run unify(<expr>, <asset>) in:
// <expr> := balance(@acc, <asset>)
res.unifyNodeWith(fnCall, res.GetExprType(validArgs[1]))
}
case FnVarOriginGetAsset:
if len(validArgs) > 0 {
res.unifyNodeWith(fnCall, res.GetExprType(validArgs[0]))
}
}
} else {
for _, arg := range validArgs {
res.checkExpression(arg, TypeAny)
}
res.pushDiagnostic(fnCall.Caller.Range, UnknownFunction{
Name: fnCall.Caller.Name,
})
}
}
func isTypeAllowed(typeName string) bool {
return slices.Contains(AllowedTypes, typeName)
}
func (res *CheckResult) checkVarType(typeDecl parser.TypeDecl) {
if !isTypeAllowed(typeDecl.Name) {
res.pushDiagnostic(typeDecl.Range, InvalidType{Name: typeDecl.Name})
}
}
func (res *CheckResult) checkDuplicateVars(variableName parser.Variable, decl parser.VarDeclaration) {
// check there aren't duplicate variables
if _, ok := res.DeclaredVars[variableName.Name]; ok {
res.pushDiagnostic(variableName.Range, DuplicateVariable{Name: variableName.Name})
} else {
res.DeclaredVars[variableName.Name] = decl
res.unusedVars[variableName.Name] = variableName.Range
}
}
func (res *CheckResult) checkFnCall(fnCall *parser.FnCall) string {
returnType := TypeAny
if resolution, ok := Builtins[fnCall.Caller.Name]; ok {
if resolution, ok := resolution.(VarOriginFnCallResolution); ok {
res.fnCallResolution[fnCall.Caller] = resolution
returnType = resolution.Return
res.requireVersion(fnCall.Range, resolution.VersionConstraints...)
}
}
// this must come after resolution
res.checkFnCallArity(fnCall)
return returnType
}
func (res *CheckResult) checkExpression(lit parser.ValueExpr, requiredType string) {
actualType := res.checkTypeOf(lit, requiredType)
res.assertHasType(lit, requiredType, actualType)
}
func (res *CheckResult) checkTypeOf(lit parser.ValueExpr, typeHint string) string {
switch lit := lit.(type) {
case *parser.Variable:
if varDeclaration, ok := res.DeclaredVars[lit.Name]; ok {
res.varResolution[lit] = varDeclaration
res.unifyNodeWith(lit, res.GetVarDeclType(varDeclaration))
} else {
res.pushDiagnostic(lit.Range, UnboundVariable{Name: lit.Name, Type: typeHint})
}
delete(res.unusedVars, lit.Name)
resolved := res.ResolveVar(lit)
if resolved == nil || resolved.Type == nil || !isTypeAllowed(resolved.Type.Name) {
return TypeAny
}
return resolved.Type.Name
case *parser.MonetaryLiteral:
res.checkExpression(lit.Asset, TypeAsset)
res.checkExpression(lit.Amount, TypeNumber)
/*
we unify $mon and $asset in:
`let $mon := [$asset 42]`
*/
res.unifyNodeWith(lit, res.GetExprType(lit.Asset))
return TypeMonetary
case *parser.BinaryInfix:
res.unifyNodeWith(lit.Left, res.GetExprType(lit.Right))
switch lit.Operator {
case parser.InfixOperatorPlus:
return res.checkInfixOverload(lit, []string{TypeNumber, TypeMonetary})
case parser.InfixOperatorMinus:
return res.checkInfixOverload(lit, []string{TypeNumber, TypeMonetary})
case parser.InfixOperatorDiv:
res.checkInfixVersion(*lit)
res.checkExpression(lit.Left, TypeNumber)
res.checkExpression(lit.Right, TypeNumber)
return TypePortion
default:
// we should never get here
// but just to be sure
res.checkExpression(lit.Left, TypeAny)
res.checkExpression(lit.Right, TypeAny)
return TypeAny
}
case *parser.Prefix:
switch lit.Operator {
case parser.PrefixOperatorMinus:
res.unifyNodeWith(lit, res.GetExprType(lit.Expr))
return res.checkHasOneOfTypes(lit.Expr, []string{TypeNumber, TypeMonetary})
default:
return TypeAny
}
case *parser.AccountInterpLiteral:
res.checkAccountInterpolationVersion(*lit)
for _, part := range lit.Parts {
if v, ok := part.(*parser.Variable); ok {
res.checkExpression(v, TypeAny)
}
}
return TypeAccount
case *parser.PercentageLiteral:
return TypePortion
case *parser.AssetLiteral:
t := TAsset(lit.Asset)
res.unifyNodeWith(lit, &t)
return TypeAsset
case *parser.NumberLiteral:
return TypeNumber
case *parser.StringLiteral:
return TypeString
case *parser.FnCall:
return res.checkFnCall(lit)
default:
return TypeAny
}
}
func (res *CheckResult) checkInfixOverload(bin *parser.BinaryInfix, allowed []string) string {
leftType := res.checkTypeOf(bin.Left, allowed[0])
if leftType == TypeAny || slices.Contains(allowed, leftType) {
res.checkExpression(bin.Right, leftType)
return leftType
}
res.pushDiagnostic(bin.Left.GetRange(), TypeMismatch{
Expected: strings.Join(allowed, "|"),
Got: leftType,
})
return TypeAny
}
func (res *CheckResult) checkHasOneOfTypes(expr parser.ValueExpr, allowed []string) string {
exprType := res.checkTypeOf(expr, allowed[0])
if exprType == TypeAny || slices.Contains(allowed, exprType) {
return exprType
}
res.pushDiagnostic(expr.GetRange(), TypeMismatch{
Expected: strings.Join(allowed, "|"),
Got: exprType,
})
return TypeAny
}
func (res *CheckResult) assertHasType(lit parser.ValueExpr, requiredType string, actualType string) {
if requiredType == TypeAny || actualType == TypeAny || requiredType == actualType {
return
}
res.pushDiagnostic(lit.GetRange(), TypeMismatch{
Expected: requiredType,
Got: actualType,
})
}
func (res *CheckResult) checkSentValue(sentValue parser.SentValue) {
switch sentValue := sentValue.(type) {
case *parser.SentValueAll:
res.checkExpression(sentValue.Asset, TypeAsset)
res.unifyNodeWith(sentValue.Asset, res.stmtType)
case *parser.SentValueLiteral:
res.checkExpression(sentValue.Monetary, TypeMonetary)
res.unifyNodeWith(sentValue.Monetary, res.stmtType)
}
}
func (res *CheckResult) checkSource(source parser.Source) {
if source == nil {
return
}
if res.unboundedAccountInSend != nil {
res.pushDiagnostic(source.GetRange(), UnboundedAccountIsNotLast{})
}
switch source := source.(type) {
case *parser.SourceAccount:
res.checkExpression(source.ValueExpr, TypeAccount)
res.checkExpression(source.Color, TypeString)
if account, ok := source.ValueExpr.(*parser.AccountInterpLiteral); ok {
if account.IsWorld() && res.unboundedSend {
res.pushDiagnostic(source.GetRange(), InvalidUnboundedAccount{})
} else if account.IsWorld() {
res.unboundedAccountInSend = account
}
coloredAccountName := account.String()
switch col := source.Color.(type) {
case *parser.Variable:
coloredAccountName += "\\$" + col.Name
case *parser.StringLiteral:
if col.String != "" {
coloredAccountName += "\\\"" + col.String + "\""
}
}
if _, emptied := res.emptiedAccount[coloredAccountName]; emptied && !account.IsWorld() {
res.pushDiagnostic(account.Range, EmptiedAccount{Name: account.String()})
}
res.emptiedAccount[coloredAccountName] = struct{}{}
}
case *parser.SourceOverdraft:
if accountLiteral, ok := source.Address.(*parser.AccountInterpLiteral); ok && accountLiteral.IsWorld() {
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Range: accountLiteral.Range,
Kind: &InvalidWorldOverdraft{},
})
}
if source.Bounded == nil {
res.unboundedAccountInSend = source.Address
if res.unboundedSend {
res.pushDiagnostic(source.Address.GetRange(), InvalidUnboundedAccount{})
}
}
res.checkExpression(source.Address, TypeAccount)
res.checkExpression(source.Color, TypeString)
if source.Bounded != nil {
res.checkExpression(*source.Bounded, TypeMonetary)
res.unifyNodeWith(*source.Bounded, res.stmtType)
}
case *parser.SourceWithScaling:
res.checkExpression(source.Address, TypeAccount)
res.checkExpression(source.Through, TypeAccount)
res.checkExpression(source.Color, TypeString)
case *parser.SourceInorder:
for _, source := range source.Sources {
res.checkSource(source)
}
case *parser.SourceOneof:
res.checkOneofVersion(source.Range)
for _, source := range source.Sources {
res.checkSource(source)
}
case *parser.SourceCapped:
onExit := res.enterCappedSource()
res.unifyNodeWith(source.Cap, res.stmtType)
res.checkExpression(source.Cap, TypeMonetary)
res.checkSource(source.From)
onExit()
case *parser.SourceAllotment:
if res.unboundedSend {
res.pushDiagnostic(source.Range, NoAllotmentInSendAll{})
}
var remainingAllotment *parser.RemainingAllotment = nil
var variableLiterals []parser.ValueExpr
sum := big.NewRat(0, 1)
for i, allottedItem := range source.Items {
isLast := i == len(source.Items)-1
switch allotment := allottedItem.Allotment.(type) {
case *parser.ValueExprAllotment:
res.checkExpression(allotment.Value, TypePortion)
rat := res.tryEvaluatingPortionExpr(allotment.Value)
if rat == nil {
variableLiterals = append(variableLiterals, allotment.Value)
} else {
sum.Add(sum, rat)
}
case *parser.RemainingAllotment:
if isLast {
remainingAllotment = allotment
} else {
res.pushDiagnostic(source.Range, RemainingIsNotLast{})
}
}
onExit := res.enterCappedSource()
res.checkSource(allottedItem.From)
onExit()
}
res.checkHasBadAllotmentSum(*sum, source.Range, remainingAllotment, variableLiterals)
default:
utils.NonExhaustiveMatchPanic[any](source)
}
}
// Try evaluating an expression, if it can be done statically.
//
// Returns nil when the expression contains variables, fn calls, or anything
// that cannot be computed statically.
//
// For example:
//
// 1 + 2 => 3
// 1 + $x => nil
func (res CheckResult) tryEvaluatingNumberExpr(expr parser.ValueExpr) *big.Int {
switch expr := expr.(type) {
case *parser.NumberLiteral:
return expr.Number
case *parser.Prefix:
switch expr.Operator {
case parser.PrefixOperatorMinus:
evExpr := res.tryEvaluatingNumberExpr(expr.Expr)
if evExpr == nil {
return nil
}
return new(big.Int).Neg(evExpr)
default:
return nil
}
case *parser.BinaryInfix:
switch expr.Operator {
case parser.InfixOperatorPlus:
left := res.tryEvaluatingNumberExpr(expr.Left)
if left == nil {
return nil
}
right := res.tryEvaluatingNumberExpr(expr.Right)
if right == nil {
return nil
}
return new(big.Int).Add(left, right)
case parser.InfixOperatorMinus:
left := res.tryEvaluatingNumberExpr(expr.Left)
if left == nil {
return nil
}
right := res.tryEvaluatingNumberExpr(expr.Right)
if right == nil {
return nil
}
return new(big.Int).Sub(left, right)
default:
return nil
}
default:
return nil
}
}
// Same as analysis.tryEvaluatingNumberExpr, for portion
func (res *CheckResult) tryEvaluatingPortionExpr(expr parser.ValueExpr) *big.Rat {
switch expr := expr.(type) {
case *parser.PercentageLiteral:
return expr.ToRatio()
case *parser.BinaryInfix:
switch expr.Operator {
case parser.InfixOperatorDiv:
right := res.tryEvaluatingNumberExpr(expr.Right)
if right == nil {
return nil
}
if right.Cmp(big.NewInt(0)) == 0 {
res.pushDiagnostic(expr.Range, DivByZero{})
return nil
}
left := res.tryEvaluatingNumberExpr(expr.Left)
if left == nil {
return nil
}
return new(big.Rat).SetFrac(left, right)
default:
return nil
}
default:
return nil
}
}
func (res *CheckResult) checkDestination(destination parser.Destination) {
if destination == nil {
return
}
switch destination := destination.(type) {
case *parser.DestinationAccount:
res.checkExpression(destination.ValueExpr, TypeAccount)
case *parser.DestinationInorder:
for _, clause := range destination.Clauses {
res.checkExpression(clause.Cap, TypeMonetary)
res.unifyNodeWith(clause.Cap, res.stmtType)
res.checkKeptOrDestination(clause.To)
}
res.checkKeptOrDestination(destination.Remaining)
case *parser.DestinationOneof:
res.checkOneofVersion(destination.Range)
for _, clause := range destination.Clauses {
res.checkExpression(clause.Cap, TypeMonetary)
res.unifyNodeWith(clause.Cap, res.stmtType)
res.checkKeptOrDestination(clause.To)
}
res.checkKeptOrDestination(destination.Remaining)
case *parser.DestinationAllotment:
var remainingAllotment *parser.RemainingAllotment
var variableLiterals []parser.ValueExpr
sum := big.NewRat(0, 1)
for i, allottedItem := range destination.Items {
isLast := i == len(destination.Items)-1
switch allotment := allottedItem.Allotment.(type) {
case *parser.ValueExprAllotment:
res.checkExpression(allotment.Value, TypePortion)
rat := res.tryEvaluatingPortionExpr(allotment.Value)
if rat == nil {
variableLiterals = append(variableLiterals, allotment.Value)
} else {
sum.Add(sum, rat)
}
// res.checkExpression(allotment, TypePortion)
// case *parser.PortionLiteral:
// sum.Add(sum, allotment.ToRatio())
case *parser.RemainingAllotment:
if isLast {
remainingAllotment = allotment
} else {
res.pushDiagnostic(destination.Range, RemainingIsNotLast{})
}
}
res.checkKeptOrDestination(allottedItem.To)
}
res.checkHasBadAllotmentSum(*sum, destination.Range, remainingAllotment, variableLiterals)
}
}
func (res *CheckResult) checkKeptOrDestination(target parser.KeptOrDestination) {
switch target := target.(type) {
case *parser.DestinationTo:
res.checkDestination(target.Destination)
case *parser.DestinationKept:
// nothing to do
}
}
func (res *CheckResult) checkHasBadAllotmentSum(
sum big.Rat,
rng parser.Range,
remaining *parser.RemainingAllotment,
variableLiterals []parser.ValueExpr,
) {
cmp := sum.Cmp(big.NewRat(1, 1))
switch cmp {
case 1, -1:
if (cmp == -1 && remaining != nil) || (cmp == -1 && len(variableLiterals) > 1) {
return
}
if cmp == -1 && len(variableLiterals) == 1 {
var value big.Rat
res.pushDiagnostic(variableLiterals[0].GetRange(), FixedPortionVariable{
Value: *value.Sub(big.NewRat(1, 1), &sum),
})
} else {
res.pushDiagnostic(rng, BadAllotmentSum{Sum: sum})
}
// sum == 1
case 0:
for _, varLit := range variableLiterals {
res.pushDiagnostic(varLit.GetRange(), FixedPortionVariable{
Value: *big.NewRat(0, 1),
})
}
if remaining != nil {
res.pushDiagnostic(remaining.Range, RedundantRemaining{})
}
}
}
func (res *CheckResult) withCloneEmptyAccount() func() {
initial := res.emptiedAccount
res.emptiedAccount = make(map[string]struct{})
maps.Copy(res.emptiedAccount, initial)
return func() {
res.emptiedAccount = initial
}
}
func (res *CheckResult) withCloneUnboundedAccountInSend() func() {
initial := res.unboundedAccountInSend
res.unboundedAccountInSend = nil
return func() {
res.unboundedAccountInSend = initial
}
}
func (res *CheckResult) withCloneUnboundedSend() func() {
initial := res.unboundedSend
res.unboundedSend = false
return func() {
res.unboundedSend = initial
}
}
func (res *CheckResult) enterCappedSource() func() {
exitCloneEmptyAccount := res.withCloneEmptyAccount()
exitCloneUnboundeAccountInSend := res.withCloneUnboundedAccountInSend()
exitCloneUnboundedSend := res.withCloneUnboundedSend()
return func() {
exitCloneEmptyAccount()
exitCloneUnboundeAccountInSend()
exitCloneUnboundedSend()
}
}
func (res *CheckResult) pushDiagnostic(rng parser.Range, kind DiagnosticKind) {
id := res.nextDiagnosticId
res.nextDiagnosticId++
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Range: rng,
Kind: kind,
Id: id,
})
}