mirror of
https://github.com/hanzoai/numscript.git
synced 2026-10-30 18:26:57 +00:00
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]>
1011 lines
25 KiB
Go
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,
|
|
})
|
|
}
|