Skip to content
Closed
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
57 changes: 57 additions & 0 deletions Lexer.g4
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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';
KEPT: 'kept';
SAVE: 'save';
LPARENS: '(';
RPARENS: ')';
LBRACKET: '[';
RBRACKET: ']';
LBRACE: '{';
RBRACE: '}';
COMMA: ',';
EQ: '=';
STAR: '*';
PLUS: '+';
MINUS: '-';

RATIO_PORTION_LITERAL: [0-9]+ [ ]? '/' [ ]? [0-9]+;
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_]*;

ASSET: [A-Z/0-9]+;

ACCOUNT_START: '@' -> pushMode(ACCOUNT_MODE);
COLON: ':' -> pushMode(ACCOUNT_MODE);
// fragment ACCOUNT_FRAGMENT_PART: [a-zA-Z0-9_-]+ | VARIABLE_NAME; ACCOUNT: '@' [a-zA-Z0-9_-]+ (':'
// ACCOUNT_FRAGMENT_PART)*;

fragment VARIABLE_NAME_FRAMGMENT: '$' [a-z_]+ [a-z0-9_]*;

mode ACCOUNT_MODE;

ACCOUNT_TEXT: [a-zA-Z0-9_-]+ -> popMode;
VARIABLE_NAME_ACC: VARIABLE_NAME_FRAMGMENT -> popMode;

mode DEFAULT_MODE;
VARIABLE_NAME_DEFAULT: VARIABLE_NAME_FRAMGMENT;
71 changes: 19 additions & 52 deletions Numscript.g4
Original file line number Diff line number Diff line change
@@ -1,46 +1,9 @@
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: '-';

RATIO_PORTION_LITERAL: [0-9]+ [ ]? '/' [ ]? [0-9]+;
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/0-9]+;
options {
tokenVocab = 'Lexer';
}

monetaryLit:
LBRACKET (asset = valueExpr) (amt = valueExpr) RBRACKET;
Expand All @@ -49,33 +12,37 @@ portion:
RATIO_PORTION_LITERAL # ratio
| PERCENTAGE_PORTION_LITERAL # percentage;

accountLiteralPart:
ACCOUNT_TEXT # accountTextPart
| VARIABLE_NAME_ACC # accountVarPart;

valueExpr:
VARIABLE_NAME # variableExpr
| ASSET # assetLiteral
| STRING # stringLiteral
| ACCOUNT # accountLiteral
| NUMBER # numberLiteral
| monetaryLit # monetaryLiteral
| portion # portionLiteral
| left = valueExpr op = ('+' | '-') right = valueExpr # infixExpr;
VARIABLE_NAME_DEFAULT # variableExpr
| ASSET # assetLiteral
| STRING # stringLiteral
| ACCOUNT_START accountLiteralPart (COLON accountLiteralPart)* # accountLiteral
| NUMBER # numberLiteral
| monetaryLit # monetaryLiteral
| portion # portionLiteral
| left = valueExpr op = (PLUS | MINUS) right = valueExpr # infixExpr;

functionCallArgs: valueExpr ( COMMA valueExpr)*;
functionCall:
fnName = (OVERDRAFT | IDENTIFIER) LPARENS functionCallArgs? RPARENS;

varOrigin: EQ functionCall;
varDeclaration:
type_ = IDENTIFIER name = VARIABLE_NAME varOrigin?;
type_ = IDENTIFIER name = VARIABLE_NAME_DEFAULT varOrigin?;
varsDeclaration: VARS LBRACE varDeclaration* RBRACE;

program: varsDeclaration? statement* EOF;

sentAllLit: LBRACKET (asset = valueExpr) STAR RBRACKET;

allotment:
portion # portionedAllotment
| VARIABLE_NAME # portionVariable
| REMAINING # remainingAllotment;
portion # portionedAllotment
| VARIABLE_NAME_DEFAULT # portionVariable
| REMAINING # remainingAllotment;

source:
address = valueExpr ALLOWING UNBOUNDED OVERDRAFT # srcAccountUnboundedOverdraft
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 @@ -358,7 +358,12 @@ func (res *CheckResult) checkExpression(lit parser.ValueExpr, requiredType strin
res.assertHasType(lit, requiredType, TypeMonetary)
res.checkExpression(lit.Asset, TypeAsset)
res.checkExpression(lit.Amount, TypeNumber)
case *parser.AccountLiteral:
case *parser.AccountInterpLiteral:
for _, part := range lit.Parts {
if v, ok := part.(*parser.Variable); ok {
res.checkExpression(v, TypeAny)
}
}
res.assertHasType(lit, requiredType, TypeAccount)
case *parser.RatioLiteral:
res.assertHasType(lit, requiredType, TypePortion)
Expand Down Expand Up @@ -413,7 +418,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 @@ -423,18 +428,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
50 changes: 50 additions & 0 deletions internal/analysis/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,56 @@ send [EUR/2 $n] (
)
}

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, "$n", 1),
d1.Range,
)
}

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

Expand Down
11 changes: 11 additions & 0 deletions internal/analysis/hover.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,17 @@ func hoverOnExpression(lit parser.ValueExpr, position parser.Position) Hover {
}

switch lit := lit.(type) {
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.Variable:
return &VariableHover{
Range: lit.Range,
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 @@ -181,6 +181,33 @@ send [C 10] (
require.NotNil(t, resolved)
}

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)
}

func TestHoverOnDestinationInorderRemaining(t *testing.T) {
input := `vars { account $dest }

Expand Down
40 changes: 38 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,28 @@
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:
value, err := st.evaluateExpr(part)
if err != nil {
return nil, err
}

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

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L24-L25

Added lines #L24 - L25 were not covered by tests
strValue, err := castToString(value, expr.Range)
if err != nil {
return nil, err
}

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

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L28-L29

Added lines #L28 - L29 were not covered by tests
parts = append(parts, strValue)
}
}
name := strings.Join(parts, ":")
// TODO validate valid names
return AccountAddress(name), nil

case *parser.StringLiteral:
return String(expr.String), nil
case *parser.RatioLiteral:
Expand Down Expand Up @@ -124,3 +145,18 @@

return (*leftValue).evalSub(st, right)
}

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}

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

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/evaluate_expr.go#L158-L160

Added lines #L158 - L160 were not covered by tests
}
}
9 changes: 9 additions & 0 deletions internal/interpreter/interpreter_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,3 +193,12 @@
func (e ExperimentalFeature) Error() string {
return fmt.Sprintf("this feature is experimental. You need the '%s' feature flag to enable it", e.FlagName)
}

type CannotCastToString struct {
parser.Range
Value Value
}

func (e CannotCastToString) Error() string {
return fmt.Sprintf("Cannot cast this value to string: %s", e.Value)

Check warning on line 203 in internal/interpreter/interpreter_error.go

View check run for this annotation

Codecov / codecov/patch

internal/interpreter/interpreter_error.go#L202-L203

Added lines #L202 - L203 were not covered by tests
}
Loading