Skip to content

Commit

Permalink
fix: 修正fstring中语句的溢出问题,并加入兼容选项
Browse files Browse the repository at this point in the history
  • Loading branch information
fy0 committed Jun 14, 2024
1 parent 942ef93 commit 4caa31e
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 42 deletions.
11 changes: 11 additions & 0 deletions bytecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,10 @@ const (
typeJeDup
typeReturn

typeFStringBlockPush // fstring标记 用于栈平衡
typeFStringBlockPop
typeV1IfMark // 特殊兼容标记

typeStSetName
typeStModify
typeStX0
Expand Down Expand Up @@ -282,6 +286,13 @@ func (code *ByteCode) CodeString() string {
case typeReturn:
return "ret"

case typeFStringBlockPush:
return "fstr.block.push"
case typeFStringBlockPop:
return "fstr.block.pop"
case typeV1IfMark:
return "v1.if.mark"

case typeStSetName:
return "st.set"
case typeStModify:
Expand Down
6 changes: 3 additions & 3 deletions roll.peg
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ stmtWhile <- "while" { c.data.LoopBegin(); c.data.OffsetPush() } sp1x exprRoot s

block <- ( '{' sp '}' / '{' sp stmtRoot '}' ) sp
stmtElse <- "else" (sp block / sp1x stmtIf)
stmtIf <- "if" sp1x exprRoot sp { c.data.AddOp(typeJne); c.data.OffsetPush() } block { c.data.AddOp(typeJmp); c.data.OffsetPopAndSet(); c.data.OffsetPush(); }
stmtElse? { c.data.OffsetPopAndSet(); }
stmtIf <- "if" sp1x (exprRoot sp { c.data.AddOp(typeJne); c.data.OffsetPush() } block { c.data.AddOp(typeJmp); c.data.OffsetPopAndSet(); c.data.OffsetPush(); }
stmtElse? { c.data.OffsetPopAndSet(); if c.data.Config.EnableV1IfCompatible { c.data.AddOp(typeV1IfMark) } } / &{ p.addErr(errors.New("不符合if语法: if expr {...} [else {...}]")); return false; })

// 'if' exprRoot block
// ('else' block)?
Expand Down Expand Up @@ -338,7 +338,7 @@ strPart3 <- text:< (escape / (![{'\\\n\r].))+ > { c.data.PushStr(text.(string));

fstringExpr1 <- exprRoot {c.data.CounterAdd(1)} / &{ p.addErr(errors.New("{} 内必须是一个表达式")); return false; }
fstringExpr <- '{' sp fstringExpr1 sp ('}' / &{ p.addErr(errors.New("无法处理字符 " + string(p.pt.rn))); return false })
fstringStmt <- "{%" sp stmtRoot { c.data.WriteCode(typePop, nil) } sp "%}"
fstringStmt <- "{%" sp ({ c.data.AddOp(typeFStringBlockPush) } stmtRoot { c.data.AddOp(typeFStringBlockPop); c.data.CounterAdd(1) } / &{ p.addErr(errors.New("{% %} 内必须是语句块或表达式")); return false; }) sp "%}"

fstring <- (
('\'' '\'' { c.data.PushStr("") })
Expand Down
110 changes: 74 additions & 36 deletions roll.peg.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

45 changes: 45 additions & 0 deletions rollvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,19 @@ func (ctx *Context) IsCalculateExists() bool {
return false
}

// IsV1IfCompatibleExists 是否存在v1的fstring-if兼容指令
func (ctx *Context) IsV1IfCompatibleExists() bool {
for _, i := range ctx.code {
switch i.T {
case typeV1IfMark:
return true
}
}
return false
}

func (ctx *Context) RunAfterParsed() error {
ctx.V1IfCompatibleCount = 0
// 以下为eval
ctx.evaluate()
if ctx.Error != nil {
Expand Down Expand Up @@ -343,6 +355,9 @@ func (ctx *Context) evaluate() {
e.top += 1
}

var fstrBlockStack [5]int
var fstrBlockIndex int

startTime := time.Now().UnixMilli()
for opIndex := 0; opIndex < e.codeIndex; opIndex += 1 {
numOpCountAdd(1)
Expand Down Expand Up @@ -836,6 +851,36 @@ func (ctx *Context) evaluate() {
details[len(details)-1].Tag = "dice-dc"
stackPush(ret)

case typeFStringBlockPush:
if fstrBlockIndex >= 4 {
ctx.Error = errors.New("字符串模板嵌套层数过多")
return
}
fstrBlockStack[fstrBlockIndex] = e.top
fstrBlockIndex += 1
case typeFStringBlockPop:
newTop := fstrBlockStack[fstrBlockIndex-1]
var v *VMValue
if newTop != e.top {
v = stackPop()
}
e.top = newTop
fstrBlockIndex -= 1
if v != nil {
stackPush(v)
} else {
stackPush(NewStrVal(""))
}
case typeV1IfMark:
// 满足条件: 首先在fstring中,其次栈里目前有东西
if fstrBlockIndex > 0 {
newTop := fstrBlockStack[fstrBlockIndex-1]
if newTop != e.top {
stackPush(NewStrVal("")) // 填入空字符串,模拟v1行为
ctx.V1IfCompatibleCount += 1
}
}

case typeStSetName:
stName, stVal := stackPop2()
if e.Config.CallbackSt != nil {
Expand Down
54 changes: 51 additions & 3 deletions rollvm_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package dicescript

import (
"fmt"
"regexp"
"strconv"
"strings"
Expand Down Expand Up @@ -58,9 +59,9 @@ func TestValueDefineStr(t *testing.T) {
simpleExecute(t, "`12{3 }` ", ns("123"))
simpleExecute(t, "`12{ 3}` ", ns("123"))
simpleExecute(t, "`12{'3'}` ", ns("123"))
simpleExecute(t, "`12{% 3 %}` ", ns("12"))
simpleExecute(t, "`12{% 3 %}` ", ns("123"))
simpleExecute(t, `"123"`, ns("123"))
simpleExecute(t, "\x1e"+"12{% 3 %}"+"\x1e", ns("12"))
simpleExecute(t, "\x1e"+"12{% 3 %}"+"\x1e", ns("123"))

simpleExecute(t, `"12\n3"`, ns("12\n3"))
simpleExecute(t, `"12\r3"`, ns("12\r3"))
Expand Down Expand Up @@ -1534,7 +1535,7 @@ func TestFStringBlock(t *testing.T) {
var err error
err = vm.Run("`{% a=2; b=3 %}4`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("4")))
assert.True(t, valueEqual(vm.Ret, ns("34")))
}

err = vm.Run("`{ if b=3 {} }`")
Expand All @@ -1552,3 +1553,50 @@ func TestFStringIf(t *testing.T) {
err = vm.Run("`{ if }`")
assert.Contains(t, err.Error(), "{} 内必须是一个表达式")
}

func TestFStringStackOverflowBug(t *testing.T) {
// `{1} {2} {% 3;4;5;6 %}`
// 的结果会成为 345 或 456(根据版本不同)
// 这是栈不平衡的体现
vm := NewVM()
err := vm.Run("`{1} {2} {% 3;4;5;6 %}`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("1 2 6")))
}
}

func TestFStringStackOverflowBug2(t *testing.T) {
// `{1} {2} {% if false {} %}`
// 会报错,因为没有任何返回
vm := NewVM()
err := vm.Run("`{1} {2} {% if false {} %}`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("1 2 ")))
}
}

func TestIfError(t *testing.T) {
vm := NewVM()
var err error

Check failure on line 1580 in rollvm_test.go

View workflow job for this annotation

GitHub Actions / lint

S1021: should merge variable declaration with assignment on next line (gosimple)
err = vm.Run("if 1 ")
fmt.Println(err)
assert.Contains(t, err.Error(), "不符合if语法")
}

func TestFStringV1IfCompatible(t *testing.T) {
// `1 {% if 1 {'test'} %} 2`
// 在v1中会返回1 2,中间的if语句执行后栈中是空的
// 但是v2改为不进行栈平衡,所以会得到1 test 2,这个兼容选项用于模拟这一行为
vm := NewVM()
err := vm.Run("`1 {% if 1 {'test'} %} 2`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("1 test 2")))
}

vm.Config.EnableV1IfCompatible = true
err = vm.Run("`1 {% if 1 {'test'} %} 2`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("1 2")))
assert.Equal(t, vm.V1IfCompatibleCount, 1)
}
}
4 changes: 4 additions & 0 deletions types.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ type RollConfig struct {
PrintBytecode bool // 执行时打印字节码
IgnoreDiv0 bool // 当div0时暂不报错

EnableV1IfCompatible bool // 一种特殊的兼容,fstring中如果最后一个语句是if那么此项为空字符串

// 以下尚未实现
DiceMinMode bool // 骰子以最小值结算,用于获取下界
DiceMaxMode bool // 以最大值结算 获取上界
Expand Down Expand Up @@ -140,6 +142,8 @@ type Context struct {
NumOpCount IntType // 算力计数
// CocFlagVarPrefix string // 解析过程中出现,当VarNumber开启时有效,可以是困难极难常规大成功

V1IfCompatibleCount int

Config RollConfig // 标记
Error error // 报错信息

Expand Down

0 comments on commit 4caa31e

Please sign in to comment.