Skip to content

Commit ca9646b

Browse files
authored
Implement a "/" operator (#39)
* feat: edited the antlr grammar, updated the AST, the parser and its snapshots * fix: updated percentageLiteral.ToRatio() method * feat: implemented "/" operator in interpreter * test: added test with variable numerator and denominator * feat: implemented static type analysis of "/" expressions * chore: fix indentation * refactor: change ast node definition * feat: improve static analysis in allotment * feat: implemented hover for div operator * feat: better diagnostic description * chore: removed unused comment * refactor: renamed file * fix: handle divide by zero runtime err * fix: check div by zero in diagnostics * test: added test * test: add static analysis test with perc literal * test: add op precedence snap test
1 parent 0e57c0a commit ca9646b

24 files changed

+1591
-1270
lines changed

Numscript.g4

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ EQ: '=';
3131
STAR: '*';
3232
MINUS: '-';
3333

34-
RATIO_PORTION_LITERAL: [0-9]+ [ ]? '/' [ ]? [0-9]+;
3534
PERCENTAGE_PORTION_LITERAL: [0-9]+ ('.' [0-9]+)? '%';
3635

3736
STRING: '"' ('\\"' | ~[\r\n"])* '"';
@@ -45,18 +44,15 @@ ASSET: [A-Z][A-Z0-9]* ('/' [0-9]+)?;
4544
monetaryLit:
4645
LBRACKET (asset = valueExpr) (amt = valueExpr) RBRACKET;
4746
48-
portion:
49-
RATIO_PORTION_LITERAL # ratio
50-
| PERCENTAGE_PORTION_LITERAL # percentage;
51-
5247
valueExpr:
5348
VARIABLE_NAME # variableExpr
5449
| ASSET # assetLiteral
5550
| STRING # stringLiteral
5651
| ACCOUNT # accountLiteral
5752
| NUMBER # numberLiteral
53+
| PERCENTAGE_PORTION_LITERAL # percentagePortionLiteral
5854
| monetaryLit # monetaryLiteral
59-
| portion # portionLiteral
55+
| left = valueExpr op = '/' right = valueExpr # infixExpr
6056
| left = valueExpr op = ('+' | '-') right = valueExpr # infixExpr
6157
| '(' valueExpr ')' # parenthesizedExpr;
6258
@@ -74,9 +70,8 @@ program: varsDeclaration? statement* EOF;
7470
sentAllLit: LBRACKET (asset = valueExpr) STAR RBRACKET;
7571
7672
allotment:
77-
portion # portionedAllotment
78-
| VARIABLE_NAME # portionVariable
79-
| REMAINING # remainingAllotment;
73+
valueExpr # portionedAllotment
74+
| REMAINING # remainingAllotment;
8075
8176
source:
8277
address = valueExpr ALLOWING UNBOUNDED OVERDRAFT # srcAccountUnboundedOverdraft

internal/analysis/check.go

Lines changed: 118 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,11 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr) string {
373373
case parser.InfixOperatorMinus:
374374
return res.checkInfixOverload(lit, []string{TypeNumber, TypeMonetary})
375375

376+
case parser.InfixOperatorDiv:
377+
res.checkExpression(lit.Left, TypeNumber)
378+
res.checkExpression(lit.Right, TypeNumber)
379+
return TypePortion
380+
376381
default:
377382
// we should never get here
378383
// but just to be sure
@@ -383,7 +388,7 @@ func (res *CheckResult) checkTypeOf(lit parser.ValueExpr) string {
383388

384389
case *parser.AccountLiteral:
385390
return TypeAccount
386-
case *parser.RatioLiteral:
391+
case *parser.PercentageLiteral:
387392
return TypePortion
388393
case *parser.AssetLiteral:
389394
return TypeAsset
@@ -525,18 +530,22 @@ func (res *CheckResult) checkSource(source parser.Source) {
525530
}
526531

527532
var remainingAllotment *parser.RemainingAllotment = nil
528-
var variableLiterals []parser.Variable
533+
var variableLiterals []parser.ValueExpr
529534

530535
sum := big.NewRat(0, 1)
531536
for i, allottedItem := range source.Items {
532537
isLast := i == len(source.Items)-1
533538

534539
switch allotment := allottedItem.Allotment.(type) {
535-
case *parser.Variable:
536-
variableLiterals = append(variableLiterals, *allotment)
537-
res.checkExpression(allotment, TypePortion)
538-
case *parser.RatioLiteral:
539-
sum.Add(sum, allotment.ToRatio())
540+
case *parser.ValueExprAllotment:
541+
res.checkExpression(allotment.Value, TypePortion)
542+
rat := res.tryEvaluatingPortionExpr(allotment.Value)
543+
if rat == nil {
544+
variableLiterals = append(variableLiterals, allotment.Value)
545+
} else {
546+
sum.Add(sum, rat)
547+
}
548+
540549
case *parser.RemainingAllotment:
541550
if isLast {
542551
remainingAllotment = allotment
@@ -560,6 +569,92 @@ func (res *CheckResult) checkSource(source parser.Source) {
560569
}
561570
}
562571

572+
// Try evaluating an expression, if it can be done statically.
573+
//
574+
// Returns nil when the expression contains variables, fn calls, or anything
575+
// that cannot be computed statically.
576+
//
577+
// For example:
578+
//
579+
// 1 + 2 => 3
580+
// 1 + $x => nil
581+
func (res CheckResult) tryEvaluatingNumberExpr(expr parser.ValueExpr) *big.Int {
582+
switch expr := expr.(type) {
583+
584+
case *parser.NumberLiteral:
585+
return big.NewInt(int64(expr.Number))
586+
587+
case *parser.BinaryInfix:
588+
switch expr.Operator {
589+
case parser.InfixOperatorPlus:
590+
left := res.tryEvaluatingNumberExpr(expr.Left)
591+
if left == nil {
592+
return nil
593+
}
594+
right := res.tryEvaluatingNumberExpr(expr.Right)
595+
if right == nil {
596+
return nil
597+
}
598+
return new(big.Int).Add(left, right)
599+
600+
case parser.InfixOperatorMinus:
601+
left := res.tryEvaluatingNumberExpr(expr.Left)
602+
if left == nil {
603+
return nil
604+
}
605+
right := res.tryEvaluatingNumberExpr(expr.Right)
606+
if right == nil {
607+
return nil
608+
}
609+
return new(big.Int).Sub(left, right)
610+
611+
default:
612+
return nil
613+
}
614+
615+
default:
616+
return nil
617+
}
618+
}
619+
620+
// Same as analysis.tryEvaluatingNumberExpr, for portion
621+
func (res *CheckResult) tryEvaluatingPortionExpr(expr parser.ValueExpr) *big.Rat {
622+
switch expr := expr.(type) {
623+
case *parser.PercentageLiteral:
624+
return expr.ToRatio()
625+
626+
case *parser.BinaryInfix:
627+
switch expr.Operator {
628+
case parser.InfixOperatorDiv:
629+
right := res.tryEvaluatingNumberExpr(expr.Right)
630+
if right == nil {
631+
return nil
632+
}
633+
634+
if right.Cmp(big.NewInt(0)) == 0 {
635+
res.Diagnostics = append(res.Diagnostics, Diagnostic{
636+
Kind: &DivByZero{},
637+
Range: expr.Range,
638+
})
639+
return nil
640+
}
641+
642+
left := res.tryEvaluatingNumberExpr(expr.Left)
643+
if left == nil {
644+
return nil
645+
}
646+
647+
return new(big.Rat).SetFrac(left, right)
648+
649+
default:
650+
return nil
651+
}
652+
653+
default:
654+
return nil
655+
}
656+
}
657+
563658
func (res *CheckResult) checkDestination(destination parser.Destination) {
564659
if destination == nil {
565660
return
@@ -585,18 +680,25 @@ func (res *CheckResult) checkDestination(destination parser.Destination) {
585680

586681
case *parser.DestinationAllotment:
587682
var remainingAllotment *parser.RemainingAllotment
588-
var variableLiterals []parser.Variable
683+
var variableLiterals []parser.ValueExpr
589684
sum := big.NewRat(0, 1)
590685

591686
for i, allottedItem := range destination.Items {
592687
isLast := i == len(destination.Items)-1
593688

594689
switch allotment := allottedItem.Allotment.(type) {
595-
case *parser.Variable:
596-
variableLiterals = append(variableLiterals, *allotment)
597-
res.checkExpression(allotment, TypePortion)
598-
case *parser.RatioLiteral:
599-
sum.Add(sum, allotment.ToRatio())
690+
case *parser.ValueExprAllotment:
691+
res.checkExpression(allotment.Value, TypePortion)
692+
rat := res.tryEvaluatingPortionExpr(allotment.Value)
693+
if rat == nil {
694+
variableLiterals = append(variableLiterals, allotment.Value)
695+
} else {
696+
sum.Add(sum, rat)
697+
}
698+
699+
// res.checkExpression(allotment, TypePortion)
700+
// case *parser.PortionLiteral:
701+
// sum.Add(sum, allotment.ToRatio())
600702
case *parser.RemainingAllotment:
601703
if isLast {
602704
remainingAllotment = allotment
@@ -628,7 +730,7 @@ func (res *CheckResult) checkHasBadAllotmentSum(
628730
sum big.Rat,
629731
rng parser.Range,
630732
remaining *parser.RemainingAllotment,
631-
variableLiterals []parser.Variable,
733+
variableLiterals []parser.ValueExpr,
632734
) {
633735
cmp := sum.Cmp(big.NewRat(1, 1))
634736
switch cmp {
@@ -640,7 +742,7 @@ func (res *CheckResult) checkHasBadAllotmentSum(
640742
if cmp == -1 && len(variableLiterals) == 1 {
641743
var value big.Rat
642744
res.Diagnostics = append(res.Diagnostics, Diagnostic{
643-
Range: variableLiterals[0].Range,
745+
Range: variableLiterals[0].GetRange(),
644746
Kind: &FixedPortionVariable{
645747
Value: *value.Sub(big.NewRat(1, 1), &sum),
646748
},
@@ -658,7 +760,7 @@ func (res *CheckResult) checkHasBadAllotmentSum(
658760
case 0:
659761
for _, varLit := range variableLiterals {
660762
res.Diagnostics = append(res.Diagnostics, Diagnostic{
661-
Range: varLit.Range,
763+
Range: varLit.GetRange(),
662764
Kind: &FixedPortionVariable{
663765
Value: *big.NewRat(0, 1),
664766
},

0 commit comments

Comments
 (0)