Skip to content

Commit 12977d3

Browse files
authored
Implemented account interpolation syntax (#44)
1 parent ca9646b commit 12977d3

30 files changed

+2215
-1295
lines changed

Lexer.g4

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
lexer grammar Lexer;
2+
WS: [ \t\r\n]+ -> skip;
3+
NEWLINE: [\r\n]+;
4+
MULTILINE_COMMENT: '/*' (MULTILINE_COMMENT | .)*? '*/' -> skip;
5+
LINE_COMMENT: '//' .*? NEWLINE -> skip;
6+
7+
VARS: 'vars';
8+
MAX: 'max';
9+
SOURCE: 'source';
10+
DESTINATION: 'destination';
11+
SEND: 'send';
12+
FROM: 'from';
13+
UP: 'up';
14+
TO: 'to';
15+
REMAINING: 'remaining';
16+
ALLOWING: 'allowing';
17+
UNBOUNDED: 'unbounded';
18+
OVERDRAFT: 'overdraft';
19+
ONEOF: 'oneof';
20+
KEPT: 'kept';
21+
SAVE: 'save';
22+
LPARENS: '(';
23+
RPARENS: ')';
24+
LBRACKET: '[';
25+
RBRACKET: ']';
26+
LBRACE: '{';
27+
RBRACE: '}';
28+
COMMA: ',';
29+
EQ: '=';
30+
STAR: '*';
31+
PLUS: '+';
32+
MINUS: '-';
33+
DIV: '/';
34+
35+
PERCENTAGE_PORTION_LITERAL: [0-9]+ ('.' [0-9]+)? '%';
36+
37+
STRING: '"' ('\\"' | ~[\r\n"])* '"';
38+
39+
IDENTIFIER: [a-z]+ [a-z_]*;
40+
NUMBER: MINUS? [0-9]+ ('_' [0-9]+)*;
41+
ASSET: [A-Z][A-Z0-9]* ('/' [0-9]+)?;
42+
43+
ACCOUNT_START: '@' -> pushMode(ACCOUNT_MODE);
44+
COLON: ':' -> pushMode(ACCOUNT_MODE);
45+
fragment VARIABLE_NAME_FRAGMENT: '$' [a-z_]+ [a-z0-9_]*;
46+
47+
mode ACCOUNT_MODE;
48+
ACCOUNT_TEXT: [a-zA-Z0-9_-]+ -> popMode;
49+
VARIABLE_NAME_ACC: VARIABLE_NAME_FRAGMENT -> popMode;
50+
51+
mode DEFAULT_MODE;
52+
VARIABLE_NAME: VARIABLE_NAME_FRAGMENT;

Numscript.g4

+22-55
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,27 @@
11
grammar Numscript;
22

3-
// Tokens
4-
WS: [ \t\r\n]+ -> skip;
5-
NEWLINE: [\r\n]+;
6-
MULTILINE_COMMENT: '/*' (MULTILINE_COMMENT | .)*? '*/' -> skip;
7-
LINE_COMMENT: '//' .*? NEWLINE -> skip;
8-
9-
VARS: 'vars';
10-
MAX: 'max';
11-
SOURCE: 'source';
12-
DESTINATION: 'destination';
13-
SEND: 'send';
14-
FROM: 'from';
15-
UP: 'up';
16-
TO: 'to';
17-
REMAINING: 'remaining';
18-
ALLOWING: 'allowing';
19-
UNBOUNDED: 'unbounded';
20-
OVERDRAFT: 'overdraft';
21-
KEPT: 'kept';
22-
SAVE: 'save';
23-
LPARENS: '(';
24-
RPARENS: ')';
25-
LBRACKET: '[';
26-
RBRACKET: ']';
27-
LBRACE: '{';
28-
RBRACE: '}';
29-
COMMA: ',';
30-
EQ: '=';
31-
STAR: '*';
32-
MINUS: '-';
33-
34-
PERCENTAGE_PORTION_LITERAL: [0-9]+ ('.' [0-9]+)? '%';
35-
36-
STRING: '"' ('\\"' | ~[\r\n"])* '"';
37-
38-
IDENTIFIER: [a-z]+ [a-z_]*;
39-
NUMBER: MINUS? [0-9]+ ('_' [0-9]+)*;
40-
VARIABLE_NAME: '$' [a-z_]+ [a-z0-9_]*;
41-
ACCOUNT: '@' [a-zA-Z0-9_-]+ (':' [a-zA-Z0-9_-]+)*;
42-
ASSET: [A-Z][A-Z0-9]* ('/' [0-9]+)?;
3+
options {
4+
tokenVocab = 'Lexer';
5+
}
436

447
monetaryLit:
458
LBRACKET (asset = valueExpr) (amt = valueExpr) RBRACKET;
469

10+
accountLiteralPart:
11+
ACCOUNT_TEXT # accountTextPart
12+
| VARIABLE_NAME_ACC # accountVarPart;
13+
4714
valueExpr:
48-
VARIABLE_NAME # variableExpr
49-
| ASSET # assetLiteral
50-
| STRING # stringLiteral
51-
| ACCOUNT # accountLiteral
52-
| NUMBER # numberLiteral
53-
| PERCENTAGE_PORTION_LITERAL # percentagePortionLiteral
54-
| monetaryLit # monetaryLiteral
55-
| left = valueExpr op = '/' right = valueExpr # infixExpr
56-
| left = valueExpr op = ('+' | '-') right = valueExpr # infixExpr
57-
| '(' valueExpr ')' # parenthesizedExpr;
15+
VARIABLE_NAME # variableExpr
16+
| ASSET # assetLiteral
17+
| STRING # stringLiteral
18+
| ACCOUNT_START accountLiteralPart (COLON accountLiteralPart)* # accountLiteral
19+
| NUMBER # numberLiteral
20+
| PERCENTAGE_PORTION_LITERAL # percentagePortionLiteral
21+
| monetaryLit # monetaryLiteral
22+
| left = valueExpr op = DIV right = valueExpr # infixExpr
23+
| left = valueExpr op = (PLUS | MINUS) right = valueExpr # infixExpr
24+
| LPARENS valueExpr RPARENS # parenthesizedExpr;
5825

5926
functionCallArgs: valueExpr ( COMMA valueExpr)*;
6027
functionCall:
@@ -80,7 +47,7 @@ source:
8047
| valueExpr # srcAccount
8148
| LBRACE allotmentClauseSrc+ RBRACE # srcAllotment
8249
| LBRACE source* RBRACE # srcInorder
83-
| 'oneof' LBRACE source+ RBRACE # srcOneof
50+
| ONEOF LBRACE source+ RBRACE # srcOneof
8451
| MAX cap = valueExpr FROM source # srcCapped;
8552
allotmentClauseSrc: allotment FROM source;
8653

@@ -90,10 +57,10 @@ keptOrDestination:
9057
destinationInOrderClause: MAX valueExpr keptOrDestination;
9158

9259
destination:
93-
valueExpr # destAccount
94-
| LBRACE allotmentClauseDest+ RBRACE # destAllotment
95-
| LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destInorder
96-
| 'oneof' LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destOneof;
60+
valueExpr # destAccount
61+
| LBRACE allotmentClauseDest+ RBRACE # destAllotment
62+
| LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destInorder
63+
| ONEOF LBRACE destinationInOrderClause* REMAINING keptOrDestination RBRACE # destOneof;
9764
allotmentClauseDest: allotment keptOrDestination;
9865

9966
sentValue: valueExpr # sentLiteral | sentAllLit # sentAll;

generate-parser.sh

+2-1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
antlr4 -Dlanguage=Go Numscript.g4 -o internal/parser/antlr
1+
antlr4 -Dlanguage=Go Lexer.g4 Numscript.g4 -o internal/parser/antlrParser -package antlrParser
2+
mv internal/parser/antlrParser/_lexer.go internal/parser/antlrParser/lexer.go

internal/analysis/check.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -386,7 +386,12 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr) string {
386386
return TypeAny
387387
}
388388

389-
case *parser.AccountLiteral:
389+
case *parser.AccountInterpLiteral:
390+
for _, part := range lit.Parts {
391+
if v, ok := part.(*parser.Variable); ok {
392+
res.checkExpression(v, TypeAny)
393+
}
394+
}
390395
return TypeAccount
391396
case *parser.PercentageLiteral:
392397
return TypePortion
@@ -459,7 +464,7 @@ func (res *CheckResult) checkSource(source parser.Source) {
459464
switch source := source.(type) {
460465
case *parser.SourceAccount:
461466
res.checkExpression(source.ValueExpr, TypeAccount)
462-
if account, ok := source.ValueExpr.(*parser.AccountLiteral); ok {
467+
if account, ok := source.ValueExpr.(*parser.AccountInterpLiteral); ok {
463468
if account.IsWorld() && res.unboundedSend {
464469
res.Diagnostics = append(res.Diagnostics, Diagnostic{
465470
Range: source.GetRange(),
@@ -469,18 +474,18 @@ func (res *CheckResult) checkSource(source parser.Source) {
469474
res.unboundedAccountInSend = account
470475
}
471476

472-
if _, emptied := res.emptiedAccount[account.Name]; emptied && !account.IsWorld() {
477+
if _, emptied := res.emptiedAccount[account.String()]; emptied && !account.IsWorld() {
473478
res.Diagnostics = append(res.Diagnostics, Diagnostic{
474-
Kind: &EmptiedAccount{Name: account.Name},
479+
Kind: &EmptiedAccount{Name: account.String()},
475480
Range: account.Range,
476481
})
477482
}
478483

479-
res.emptiedAccount[account.Name] = struct{}{}
484+
res.emptiedAccount[account.String()] = struct{}{}
480485
}
481486

482487
case *parser.SourceOverdraft:
483-
if accountLiteral, ok := source.Address.(*parser.AccountLiteral); ok && accountLiteral.IsWorld() {
488+
if accountLiteral, ok := source.Address.(*parser.AccountInterpLiteral); ok && accountLiteral.IsWorld() {
484489
res.Diagnostics = append(res.Diagnostics, Diagnostic{
485490
Range: accountLiteral.Range,
486491
Kind: &InvalidWorldOverdraft{},

internal/analysis/check_test.go

+48
Original file line numberDiff line numberDiff line change
@@ -1790,3 +1790,51 @@ func TestCheckMinus(t *testing.T) {
17901790
}, diagnostics)
17911791
})
17921792
}
1793+
1794+
func TestNoUnusedOnStringInterp(t *testing.T) {
1795+
t.Parallel()
1796+
1797+
input := `vars { number $id }
1798+
send [EUR/2 *] (
1799+
source = @user:$id:pending
1800+
destination = @dest
1801+
)`
1802+
1803+
program := parser.Parse(input).Value
1804+
1805+
diagnostics := analysis.CheckProgram(program).Diagnostics
1806+
require.Empty(t, diagnostics)
1807+
1808+
}
1809+
1810+
func TestWrongTypeInsideAccountInterp(t *testing.T) {
1811+
t.Skip("TODO formalize a better type system to model this easy")
1812+
1813+
t.Parallel()
1814+
1815+
input := `vars { monetary $m }
1816+
send [EUR/2 *] (
1817+
source = @user:$m
1818+
destination = @dest
1819+
)`
1820+
1821+
program := parser.Parse(input).Value
1822+
1823+
diagnostics := analysis.CheckProgram(program).Diagnostics
1824+
1825+
require.Len(t, diagnostics, 1, "diagnostics=%#v\n", diagnostics)
1826+
1827+
d1 := diagnostics[0]
1828+
assert.Equal(t,
1829+
&analysis.TypeMismatch{
1830+
Expected: "number|account|string",
1831+
Got: "monetary",
1832+
},
1833+
d1.Kind,
1834+
)
1835+
1836+
assert.Equal(t,
1837+
parser.RangeOfIndexed(input, "$m", 1),
1838+
d1.Range,
1839+
)
1840+
}

internal/analysis/hover.go

+10
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,16 @@ func hoverOnExpression(lit parser.ValueExpr, position parser.Position) Hover {
150150
Range: lit.Range,
151151
Node: lit,
152152
}
153+
case *parser.AccountInterpLiteral:
154+
for _, part := range lit.Parts {
155+
if v, ok := part.(*parser.Variable); ok {
156+
157+
hover := hoverOnExpression(v, position)
158+
if hover != nil {
159+
return hover
160+
}
161+
}
162+
}
153163
case *parser.MonetaryLiteral:
154164
hover := hoverOnExpression(lit.Amount, position)
155165
if hover != nil {

internal/analysis/hover_test.go

+27
Original file line numberDiff line numberDiff line change
@@ -496,3 +496,30 @@ func TestHoverFaultTolerance(t *testing.T) {
496496
require.Nil(t, hover)
497497
})
498498
}
499+
500+
func TestHoverOnStringInterp(t *testing.T) {
501+
502+
input := `vars { number $id }
503+
send [ASSET *] (
504+
source = @world
505+
destination = @user:$id
506+
)
507+
`
508+
509+
rng := parser.RangeOfIndexed(input, "$id", 1)
510+
511+
program := parser.Parse(input).Value
512+
hover := analysis.HoverOn(program, rng.Start)
513+
require.NotNil(t, hover)
514+
515+
variableHover, ok := hover.(*analysis.VariableHover)
516+
require.True(t, ok, "Expected VariableHover")
517+
518+
require.Equal(t, rng, variableHover.Range)
519+
520+
checkResult := analysis.CheckProgram(program)
521+
require.NotNil(t, variableHover.Node)
522+
523+
resolved := checkResult.ResolveVar(variableHover.Node)
524+
require.NotNil(t, resolved)
525+
}

internal/interpreter/evaluate_expr.go

+42-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package interpreter
22

33
import (
44
"math/big"
5+
"strings"
56

67
"github.com/formancehq/numscript/internal/parser"
78
"github.com/formancehq/numscript/internal/utils"
@@ -11,8 +12,32 @@ func (st *programState) evaluateExpr(expr parser.ValueExpr) (Value, InterpreterE
1112
switch expr := expr.(type) {
1213
case *parser.AssetLiteral:
1314
return Asset(expr.Asset), nil
14-
case *parser.AccountLiteral:
15-
return AccountAddress(expr.Name), nil
15+
case *parser.AccountInterpLiteral:
16+
var parts []string
17+
for _, part := range expr.Parts {
18+
switch part := part.(type) {
19+
case parser.AccountTextPart:
20+
parts = append(parts, part.Name)
21+
case *parser.Variable:
22+
err := st.checkFeatureFlag(ExperimentalAccountInterpolationFlag)
23+
if err != nil {
24+
return nil, err
25+
}
26+
27+
value, err := st.evaluateExpr(part)
28+
if err != nil {
29+
return nil, err
30+
}
31+
strValue, err := castToString(value, expr.Range)
32+
if err != nil {
33+
return nil, err
34+
}
35+
parts = append(parts, strValue)
36+
}
37+
}
38+
name := strings.Join(parts, ":")
39+
return NewAccountAddress(name)
40+
1641
case *parser.StringLiteral:
1742
return String(expr.String), nil
1843
case *parser.PercentageLiteral:
@@ -149,3 +174,18 @@ func (st *programState) divOp(rng parser.Range, left parser.ValueExpr, right par
149174

150175
return Portion(*rat), nil
151176
}
177+
178+
func castToString(v Value, rng parser.Range) (string, InterpreterError) {
179+
switch v := v.(type) {
180+
case AccountAddress:
181+
return v.String(), nil
182+
case String:
183+
return v.String(), nil
184+
case MonetaryInt:
185+
return v.String(), nil
186+
187+
default:
188+
// No asset nor ratio can be implicitly cast to string
189+
return "", CannotCastToString{Value: v, Range: rng}
190+
}
191+
}

internal/interpreter/interpreter.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func parseVar(type_ string, rawValue string, r parser.Range) (Value, Interpreter
8989
case analysis.TypeMonetary:
9090
return parseMonetary(rawValue)
9191
case analysis.TypeAccount:
92-
return AccountAddress(rawValue), nil
92+
return NewAccountAddress(rawValue)
9393
case analysis.TypePortion:
9494
bi, err := ParsePortionSpecific(rawValue)
9595
if err != nil {
@@ -182,6 +182,7 @@ type FeatureFlag = string
182182
const (
183183
ExperimentalOverdraftFunctionFeatureFlag FeatureFlag = "experimental-overdraft-function"
184184
ExperimentalOneofFeatureFlag FeatureFlag = "experimental-oneof"
185+
ExperimentalAccountInterpolationFlag FeatureFlag = "experimental-account-interpolation"
185186
)
186187

187188
func RunProgram(

0 commit comments

Comments
 (0)