Skip to content

Commit f692fee

Browse files
committed
Implemented account interpolation syntax (#44)
refactor: inject io.Stdin and io.Stdout as interfaces WIP refactor: rewritten LS and improved testing capabilities chore: added test fix: fixed thread safety remove example code refactor: move bindings in their separate module chore: add documentation refactor: simplify code refactor: renamed struct feat: added request/response structs implementation refactor: removing jsonrpc2 dependency for request/response refactor: removed sourcegraph/jsonrpc2 dependency refactor: change module name refactor: extract testing utility refactor: change func signature chore: added jsonrpc server test test: prevent data races in tests refactor: extracted test utility test: test jsonrpc notifications refactor: changed api fix: fix request/response (un)marshaling refactor: exposed functions and changed sig test: finally testing the ls handlers properly test: added new test fix: fix test by enforcing deterministic ordering of symbols refactor: rename test test: add more hover tests empty commit to trigger codecov test: add textDocument/definition test fix: improve error handling handling server exit fix close channels refactor
1 parent ca9646b commit f692fee

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3550
-1560
lines changed

.github/workflows/checks.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
run: go build -v ./...
2121

2222
- name: Test
23-
run: go test -v ./... -coverprofile=coverage.txt
23+
run: go test -race -timeout 500ms -v ./... -coverprofile=coverage.txt
2424

2525
- name: Upload results to Codecov
2626
uses: codecov/codecov-action@v4

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

go.mod

-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ require (
2626
github.com/kr/text v0.2.0 // indirect
2727
github.com/maruel/natural v1.1.1 // indirect
2828
github.com/rogpeppe/go-internal v1.12.0 // indirect
29-
github.com/sourcegraph/jsonrpc2 v0.2.0
3029
github.com/stretchr/testify v1.9.0
3130
github.com/tidwall/gjson v1.17.1 // indirect
3231
github.com/tidwall/match v1.1.1 // indirect

go.sum

-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,6 @@ github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxI
1616
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
1717
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
1818
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
19-
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
20-
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
21-
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
2219
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
2320
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
2421
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -38,8 +35,6 @@ github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/f
3835
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
3936
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
4037
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
41-
github.com/sourcegraph/jsonrpc2 v0.2.0 h1:KjN/dC4fP6aN9030MZCJs9WQbTOjWHhrtKVpzzSrr/U=
42-
github.com/sourcegraph/jsonrpc2 v0.2.0/go.mod h1:ZafdZgk/axhT1cvZAPOhw+95nz2I/Ra5qMlU4gTRwIo=
4338
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
4439
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
4540
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=

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/document_symbols.go

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package analysis
22

33
import (
4+
"slices"
5+
46
"github.com/formancehq/numscript/internal/parser"
57
)
68

@@ -20,7 +22,7 @@ type DocumentSymbol struct {
2022
Kind DocumentSymbolKind
2123
}
2224

23-
// Note: Results are not sorted
25+
// results are sorted by start position
2426
func (r *CheckResult) GetSymbols() []DocumentSymbol {
2527
var symbols []DocumentSymbol
2628
for k, v := range r.declaredVars {
@@ -31,8 +33,15 @@ func (r *CheckResult) GetSymbols() []DocumentSymbol {
3133
Range: v.Name.Range,
3234
SelectionRange: v.Name.Range,
3335
})
34-
3536
}
3637

38+
slices.SortFunc(symbols, func(a, b DocumentSymbol) int {
39+
if a.Range.Start.GtEq(b.Range.Start) {
40+
return 1
41+
} else {
42+
return -1
43+
}
44+
})
45+
3746
return symbols
3847
}

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/cmd/lsp.go

+1-5
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package cmd
22

33
import (
44
"github.com/formancehq/numscript/internal/lsp"
5-
"github.com/formancehq/numscript/internal/lsp/language_server"
65

76
"github.com/spf13/cobra"
87
)
@@ -13,9 +12,6 @@ var lspCmd = &cobra.Command{
1312
Long: "Run the lsp server. This command is usually meant to be used for editors integration.",
1413
Hidden: true,
1514
Run: func(cmd *cobra.Command, args []string) {
16-
language_server.RunServer(language_server.ServerArgs[lsp.State]{
17-
InitialState: lsp.InitialState,
18-
Handler: lsp.Handle,
19-
})
15+
lsp.RunServer()
2016
},
2117
}

0 commit comments

Comments
 (0)