Skip to content
Merged
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
52 changes: 52 additions & 0 deletions Lexer.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
lexer grammar Lexer;
WS: [ \t\r\n]+ -> skip;
NEWLINE: [\r\n]+;
MULTILINE_COMMENT: '/*' (MULTILINE_COMMENT | .)*? '*/' -> skip;
LINE_COMMENT: '//' .*? NEWLINE -> skip;

VARS: 'vars';
MAX: 'max';
SOURCE: 'source';
DESTINATION: 'destination';
SEND: 'send';
FROM: 'from';
UP: 'up';
TO: 'to';
REMAINING: 'remaining';
ALLOWING: 'allowing';
UNBOUNDED: 'unbounded';
OVERDRAFT: 'overdraft';
ONEOF: 'oneof';
KEPT: 'kept';
SAVE: 'save';
LPARENS: '(';
RPARENS: ')';
LBRACKET: '[';
RBRACKET: ']';
LBRACE: '{';
RBRACE: '}';
COMMA: ',';
EQ: '=';
STAR: '*';
PLUS: '+';
MINUS: '-';
DIV: '/';

PERCENTAGE_PORTION_LITERAL: [0-9]+ ('.' [0-9]+)? '%';

STRING: '"' ('\\"' | ~[\r\n"])* '"';

IDENTIFIER: [a-z]+ [a-z_]*;
NUMBER: MINUS? [0-9]+ ('_' [0-9]+)*;
ASSET: [A-Z][A-Z0-9]* ('/' [0-9]+)?;

ACCOUNT_START: '@' -> pushMode(ACCOUNT_MODE);
COLON: ':' -> pushMode(ACCOUNT_MODE);
fragment VARIABLE_NAME_FRAGMENT: '$' [a-z_]+ [a-z0-9_]*;

mode ACCOUNT_MODE;
ACCOUNT_TEXT: [a-zA-Z0-9_-]+ -> popMode;
VARIABLE_NAME_ACC: VARIABLE_NAME_FRAGMENT -> popMode;

mode DEFAULT_MODE;
VARIABLE_NAME: VARIABLE_NAME_FRAGMENT;
77 changes: 22 additions & 55 deletions Numscript.g4
Original file line number Diff line number Diff line change
@@ -1,60 +1,27 @@
grammar Numscript;

// Tokens
WS: [ \t\r\n]+ -> skip;
NEWLINE: [\r\n]+;
MULTILINE_COMMENT: '/*' (MULTILINE_COMMENT | .)*? '*/' -> skip;
LINE_COMMENT: '//' .*? NEWLINE -> skip;

VARS: 'vars';
MAX: 'max';
SOURCE: 'source';
DESTINATION: 'destination';
SEND: 'send';
FROM: 'from';
UP: 'up';
TO: 'to';
REMAINING: 'remaining';
ALLOWING: 'allowing';
UNBOUNDED: 'unbounded';
OVERDRAFT: 'overdraft';
KEPT: 'kept';
SAVE: 'save';
LPARENS: '(';
RPARENS: ')';
LBRACKET: '[';
RBRACKET: ']';
LBRACE: '{';
RBRACE: '}';
COMMA: ',';
EQ: '=';
STAR: '*';
MINUS: '-';

PERCENTAGE_PORTION_LITERAL: [0-9]+ ('.' [0-9]+)? '%';

STRING: '"' ('\\"' | ~[\r\n"])* '"';

IDENTIFIER: [a-z]+ [a-z_]*;
NUMBER: MINUS? [0-9]+ ('_' [0-9]+)*;
VARIABLE_NAME: '$' [a-z_]+ [a-z0-9_]*;
ACCOUNT: '@' [a-zA-Z0-9_-]+ (':' [a-zA-Z0-9_-]+)*;
ASSET: [A-Z][A-Z0-9]* ('/' [0-9]+)?;
options {
tokenVocab = 'Lexer';
}

monetaryLit:
LBRACKET (asset = valueExpr) (amt = valueExpr) RBRACKET;

accountLiteralPart:
ACCOUNT_TEXT # accountTextPart
| VARIABLE_NAME_ACC # accountVarPart;

valueExpr:
VARIABLE_NAME # variableExpr
| ASSET # assetLiteral
| STRING # stringLiteral
| ACCOUNT # accountLiteral
| NUMBER # numberLiteral
| PERCENTAGE_PORTION_LITERAL # percentagePortionLiteral
| monetaryLit # monetaryLiteral
| left = valueExpr op = '/' right = valueExpr # infixExpr
| left = valueExpr op = ('+' | '-') right = valueExpr # infixExpr
| '(' valueExpr ')' # parenthesizedExpr;
VARIABLE_NAME # variableExpr
| ASSET # assetLiteral
| STRING # stringLiteral
| ACCOUNT_START accountLiteralPart (COLON accountLiteralPart)* # accountLiteral
| NUMBER # numberLiteral
| PERCENTAGE_PORTION_LITERAL # percentagePortionLiteral
| monetaryLit # monetaryLiteral
| left = valueExpr op = DIV right = valueExpr # infixExpr
| left = valueExpr op = (PLUS | MINUS) right = valueExpr # infixExpr
| LPARENS valueExpr RPARENS # parenthesizedExpr;

functionCallArgs: valueExpr ( COMMA valueExpr)*;
functionCall:
Expand All @@ -80,7 +47,7 @@ source:
| valueExpr # srcAccount
| LBRACE allotmentClauseSrc+ RBRACE # srcAllotment
| LBRACE source* RBRACE # srcInorder
| 'oneof' LBRACE source+ RBRACE # srcOneof
| ONEOF LBRACE source+ RBRACE # srcOneof
| MAX cap = valueExpr FROM source # srcCapped;
allotmentClauseSrc: allotment FROM source;

Expand All @@ -90,10 +57,10 @@ keptOrDestination:
destinationInOrderClause: MAX valueExpr keptOrDestination;

destination:
valueExpr # destAccount
| LBRACE allotmentClauseDest+ RBRACE # destAllotment
| LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destInorder
| 'oneof' LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destOneof;
valueExpr # destAccount
| LBRACE allotmentClauseDest+ RBRACE # destAllotment
| LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destInorder
| ONEOF LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destOneof;
allotmentClauseDest: allotment keptOrDestination;

sentValue: valueExpr # sentLiteral | sentAllLit # sentAll;
Expand Down
3 changes: 2 additions & 1 deletion generate-parser.sh
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
antlr4 -Dlanguage=Go Numscript.g4 -o internal/parser/antlr
antlr4 -Dlanguage=Go Lexer.g4 Numscript.g4 -o internal/parser/antlrParser -package antlrParser
mv internal/parser/antlrParser/_lexer.go internal/parser/antlrParser/lexer.go
17 changes: 11 additions & 6 deletions internal/analysis/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,12 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr) string {
return TypeAny
}

case *parser.AccountLiteral:
case *parser.AccountInterpLiteral:
for _, part := range lit.Parts {
if v, ok := part.(*parser.Variable); ok {
res.checkExpression(v, TypeAny)
}
}
return TypeAccount
case *parser.PercentageLiteral:
return TypePortion
Expand Down Expand Up @@ -459,7 +464,7 @@ func (res *CheckResult) checkSource(source parser.Source) {
switch source := source.(type) {
case *parser.SourceAccount:
res.checkExpression(source.ValueExpr, TypeAccount)
if account, ok := source.ValueExpr.(*parser.AccountLiteral); ok {
if account, ok := source.ValueExpr.(*parser.AccountInterpLiteral); ok {
if account.IsWorld() && res.unboundedSend {
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Range: source.GetRange(),
Expand All @@ -469,18 +474,18 @@ func (res *CheckResult) checkSource(source parser.Source) {
res.unboundedAccountInSend = account
}

if _, emptied := res.emptiedAccount[account.Name]; emptied && !account.IsWorld() {
if _, emptied := res.emptiedAccount[account.String()]; emptied && !account.IsWorld() {
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Kind: &EmptiedAccount{Name: account.Name},
Kind: &EmptiedAccount{Name: account.String()},
Range: account.Range,
})
}

res.emptiedAccount[account.Name] = struct{}{}
res.emptiedAccount[account.String()] = struct{}{}
}

case *parser.SourceOverdraft:
if accountLiteral, ok := source.Address.(*parser.AccountLiteral); ok && accountLiteral.IsWorld() {
if accountLiteral, ok := source.Address.(*parser.AccountInterpLiteral); ok && accountLiteral.IsWorld() {
res.Diagnostics = append(res.Diagnostics, Diagnostic{
Range: accountLiteral.Range,
Kind: &InvalidWorldOverdraft{},
Expand Down
48 changes: 48 additions & 0 deletions internal/analysis/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1790,3 +1790,51 @@ func TestCheckMinus(t *testing.T) {
}, diagnostics)
})
}

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

input := `vars { number $id }
send [EUR/2 *] (
source = @user:$id:pending
destination = @dest
)`

program := parser.Parse(input).Value

diagnostics := analysis.CheckProgram(program).Diagnostics
require.Empty(t, diagnostics)

}

func TestWrongTypeInsideAccountInterp(t *testing.T) {
t.Skip("TODO formalize a better type system to model this easy")

t.Parallel()

input := `vars { monetary $m }
send [EUR/2 *] (
source = @user:$m
destination = @dest
)`

program := parser.Parse(input).Value

diagnostics := analysis.CheckProgram(program).Diagnostics

require.Len(t, diagnostics, 1, "diagnostics=%#v\n", diagnostics)

d1 := diagnostics[0]
assert.Equal(t,
&analysis.TypeMismatch{
Expected: "number|account|string",
Got: "monetary",
},
d1.Kind,
)

assert.Equal(t,
parser.RangeOfIndexed(input, "$m", 1),
d1.Range,
)
}
10 changes: 10 additions & 0 deletions internal/analysis/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ func hoverOnExpression(lit parser.ValueExpr, position parser.Position) Hover {
Range: lit.Range,
Node: lit,
}
case *parser.AccountInterpLiteral:
for _, part := range lit.Parts {
if v, ok := part.(*parser.Variable); ok {

hover := hoverOnExpression(v, position)
if hover != nil {
return hover
}
}
}
case *parser.MonetaryLiteral:
hover := hoverOnExpression(lit.Amount, position)
if hover != nil {
Expand Down
27 changes: 27 additions & 0 deletions internal/analysis/hover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,30 @@ func TestHoverFaultTolerance(t *testing.T) {
require.Nil(t, hover)
})
}

func TestHoverOnStringInterp(t *testing.T) {

input := `vars { number $id }
send [ASSET *] (
source = @world
destination = @user:$id
)
`

rng := parser.RangeOfIndexed(input, "$id", 1)

program := parser.Parse(input).Value
hover := analysis.HoverOn(program, rng.Start)
require.NotNil(t, hover)

variableHover, ok := hover.(*analysis.VariableHover)
require.True(t, ok, "Expected VariableHover")

require.Equal(t, rng, variableHover.Range)

checkResult := analysis.CheckProgram(program)
require.NotNil(t, variableHover.Node)

resolved := checkResult.ResolveVar(variableHover.Node)
require.NotNil(t, resolved)
}
44 changes: 42 additions & 2 deletions internal/interpreter/evaluate_expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import (
"math/big"
"strings"

"github.com/formancehq/numscript/internal/parser"
"github.com/formancehq/numscript/internal/utils"
Expand All @@ -11,8 +12,32 @@
switch expr := expr.(type) {
case *parser.AssetLiteral:
return Asset(expr.Asset), nil
case *parser.AccountLiteral:
return AccountAddress(expr.Name), nil
case *parser.AccountInterpLiteral:
var parts []string
for _, part := range expr.Parts {
switch part := part.(type) {
case parser.AccountTextPart:
parts = append(parts, part.Name)
case *parser.Variable:
err := st.checkFeatureFlag(ExperimentalAccountInterpolationFlag)
if err != nil {
return nil, err
}

value, err := st.evaluateExpr(part)
if err != nil {
return nil, err
}

Check warning on line 30 in internal/interpreter/evaluate_expr.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L29-L30

Added lines #L29 - L30 were not covered by tests
strValue, err := castToString(value, expr.Range)
if err != nil {
return nil, err
}
parts = append(parts, strValue)
}
}
name := strings.Join(parts, ":")
return NewAccountAddress(name)

case *parser.StringLiteral:
return String(expr.String), nil
case *parser.PercentageLiteral:
Expand Down Expand Up @@ -149,3 +174,18 @@

return Portion(*rat), nil
}

func castToString(v Value, rng parser.Range) (string, InterpreterError) {
switch v := v.(type) {
case AccountAddress:
return v.String(), nil
case String:
return v.String(), nil
case MonetaryInt:
return v.String(), nil

default:
// No asset nor ratio can be implicitly cast to string
return "", CannotCastToString{Value: v, Range: rng}
}
}
3 changes: 2 additions & 1 deletion internal/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ func parseVar(type_ string, rawValue string, r parser.Range) (Value, Interpreter
case analysis.TypeMonetary:
return parseMonetary(rawValue)
case analysis.TypeAccount:
return AccountAddress(rawValue), nil
return NewAccountAddress(rawValue)
case analysis.TypePortion:
bi, err := ParsePortionSpecific(rawValue)
if err != nil {
Expand Down Expand Up @@ -182,6 +182,7 @@ type FeatureFlag = string
const (
ExperimentalOverdraftFunctionFeatureFlag FeatureFlag = "experimental-overdraft-function"
ExperimentalOneofFeatureFlag FeatureFlag = "experimental-oneof"
ExperimentalAccountInterpolationFlag FeatureFlag = "experimental-account-interpolation"
)

func RunProgram(
Expand Down
Loading