Skip to content

Commit

Permalink
feat: while和if现在会在语句块执行完后将栈恢复执行前状态
Browse files Browse the repository at this point in the history
  • Loading branch information
fy0 committed Aug 8, 2024
1 parent fe03d28 commit e177356
Show file tree
Hide file tree
Showing 7 changed files with 103 additions and 70 deletions.
26 changes: 17 additions & 9 deletions GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ DiceScript 允许两种数字类型,整数和浮点数,足以应对各种向
1.0
-12.34
0.0314159
.0314159//DiceScript会在这样的数字前加上0,本例等于0.0314159
.0314159 // DiceScript会在这样的数字前加上0,本例等于0.0314159
```

此外,DiceScript没有布尔类型,true的值为整数1,false的值为整数0。
Expand Down Expand Up @@ -310,18 +310,21 @@ text = 'Hello, ' + 'world' + '!'
```
`玩家目前状态:{%
if float(hp) / hpmax < 0.1 {
'生命垂危'
stat = '生命垂危'
} else {
'还顶得住'
stat = '还顶得住'
}
%}`
stat // 最后一项非语句块内容会被输出
%}`
```

注意,{}中只允许写**表达式**,表达式可以看成是不带if while的句子,较为抽象的定义说法是不带语句块的句子
{} 和 {% %} 的行为是相同的,都是输出其中的内容

而 {% %} 中允许写语句块。目前的语句语法有四种:if while return 函数定义
不过,推荐在{}中写简短的**表达式**,表达式可以看成是不带if while的句子,较为抽象的定义说法是不带语句块的句子。

{% %} 段落会返回里面最后一个表达式的值,如果没有值会自动补空字符串。
而 {% %} 推荐复杂一些的逻辑,这样会容易区分一些。目前的语句语法有四种:if while return 函数定义

{} / {% %} 段落会返回里面最后一个表达式的值,如果没有值会自动补空字符串。


第二种隐藏的模板语法,用的符号是 \x1e,他跟`的作用完全相同,也支持 f-string。
Expand All @@ -341,7 +344,7 @@ User: /text 我的生命值是{hp}
vm.Run("\x1e" + input + "\x1e")
```

如果将上面代码中的"\x1e"换成\`,效果是一样的,但是用户就不能在文本里输入`了
如果将上面代码中的"\x1e"换成\`,效果是一样的,但是这限制了用户在文本中输入`字符,因此使用一个不可见字符替代


#### null
Expand Down Expand Up @@ -370,7 +373,7 @@ null代表空值。

这里的this是指该变量内部的一个空间,如果你有其他编程语言经验,可以理解为函数的内部变量。

>this的解释请参考:https://www.runoob.com/js/js-this.html ,在DiceScript中this的用法基本与JS相同。
> this的解释请参考:https://www.runoob.com/js/js-this.html ,在DiceScript中this的用法基本与JS相同。
```
a // 结果为:a = 5[a.x = 5] + d10
Expand Down Expand Up @@ -627,9 +630,14 @@ float(num) // 转化为float类型
str(obj) // 转化为str类型
bool(obj) // 将对象二值化,结果为0或1
repr(obj) // 将对象转化为供解释器读取的形式,类似于python的同名函数
load(name) // 读取变量名为name的变量,拿到其值
loadRaw(name) // 读取变量名为name的变量,与load()不同,如果该变量是计算类型,那么不会返回计算后结果
repr(obj) // 将对象转化为供解释器读取的形式
load(name) // 根据给出的名字,获取对象。 load('a') == a
dir(obj) // 查看这个对象的方法函数,可用于字典、数组等
typeId(obj) // 获取某个对象的类型ID,值为数字
```


Expand Down
11 changes: 8 additions & 3 deletions bytecode.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,9 @@ const (

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

typeBlockPush
typeBlockPop

typeStSetName
typeStModify
Expand Down Expand Up @@ -286,12 +288,15 @@ func (code *ByteCode) CodeString() string {
case typeReturn:
return "ret"

case typeBlockPush:
return "block.push"
case typeBlockPop:
return "block.pop"

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

case typeStSetName:
return "st.set"
Expand Down
8 changes: 4 additions & 4 deletions roll.peg
Original file line number Diff line number Diff line change
Expand Up @@ -94,17 +94,17 @@ stmtContinue <- "continue" sp {
stmtReturn <- "return" sp1x exprRoot { c.data.AddOp(typeReturn); }
/ "return" sp { c.data.PushNull(); c.data.AddOp(typeReturn); }

stmtWhile <- "while" { c.data.LoopBegin(); c.data.OffsetPush() } sp1x exprRoot sp { c.data.AddOp(typeJne); c.data.OffsetPush() }
block { c.data.AddOp(typeJmp); c.data.OffsetPush(); c.data.OffsetJmpSetX(0, 2, true); c.data.OffsetJmpSetX(1, 1, false); c.data.ContinueSet(2); c.data.BreakSet(); c.data.OffsetPopN(3);c.data.LoopEnd(); }
stmtWhile <- "while" sp1x { c.data.AddOp(typeBlockPush); c.data.LoopBegin(); c.data.OffsetPush() } exprRoot sp { c.data.AddOp(typeJne); c.data.OffsetPush() }
block { c.data.AddOp(typeJmp); c.data.OffsetPush(); c.data.OffsetJmpSetX(0, 2, true); c.data.OffsetJmpSetX(1, 1, false); c.data.ContinueSet(2); c.data.BreakSet(); c.data.OffsetPopN(3);c.data.LoopEnd(); c.data.AddOp(typeBlockPop) }
// push xxx // 这里是while后面的exprRoot
// jne 1
// ...
// jmp -3 // 跳回开始点

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

// 'if' exprRoot block
// ('else' block)?
Expand Down
25 changes: 15 additions & 10 deletions roll.peg.go

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

54 changes: 27 additions & 27 deletions rollvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,19 +110,7 @@ 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
ctx.IsComputedLoaded = false
// 以下为eval
ctx.evaluate()
Expand Down Expand Up @@ -427,9 +415,12 @@ func (ctx *Context) evaluate() {
return 0
}

var fstrBlockStack [5]int
var fstrBlockStack [20]int
var fstrBlockIndex int

var blockStack [20]int // TODO: 如果在while循环中return会使得 blockIndex+1,用完之后就不能用了
var blockIndex int

startTime := time.Now().UnixMilli()
for opIndex := 0; opIndex < e.codeIndex; opIndex += 1 {
numOpCountAdd(1)
Expand Down Expand Up @@ -902,7 +893,7 @@ func (ctx *Context) evaluate() {
return
}

num, detail := RollCommon(ctx.randSrc, diceState.times, bInt, diceState.min, diceState.max, diceState.isKeepLH, diceState.lowNum, diceState.highNum, getRollMode())
num, detail := RollCommon(ctx.RandSrc, diceState.times, bInt, diceState.min, diceState.max, diceState.isKeepLH, diceState.lowNum, diceState.highNum, getRollMode())
diceStateIndex -= 1

ret := NewIntVal(num)
Expand All @@ -912,7 +903,7 @@ func (ctx *Context) evaluate() {
stackPush(ret)

case typeDiceFate:
sum, detail := RollFate(ctx.randSrc, getRollMode())
sum, detail := RollFate(ctx.RandSrc, getRollMode())
ret := NewIntVal(sum)
details[len(details)-1].Ret = ret
details[len(details)-1].Text = detail
Expand All @@ -928,7 +919,7 @@ func (ctx *Context) evaluate() {
}

isBonus := code.T == typeDiceCocBonus
r, detailText := RollCoC(ctx.randSrc, isBonus, diceNum, getRollMode())
r, detailText := RollCoC(ctx.RandSrc, isBonus, diceNum, getRollMode())
ret := NewIntVal(r)
details[len(details)-1].Ret = ret
details[len(details)-1].Text = detailText
Expand Down Expand Up @@ -967,7 +958,7 @@ func (ctx *Context) evaluate() {
return
}

num, _, _, detailText := RollWoD(ctx.randSrc, v.MustReadInt(), wodState.pool, wodState.points, wodState.threshold, wodState.isGE, getRollMode())
num, _, _, detailText := RollWoD(ctx.RandSrc, v.MustReadInt(), wodState.pool, wodState.points, wodState.threshold, wodState.isGE, getRollMode())
ret := NewIntVal(num)
details[len(details)-1].Ret = ret
details[len(details)-1].Text = detailText
Expand Down Expand Up @@ -995,14 +986,32 @@ func (ctx *Context) evaluate() {
details[len(details)-1].Tag = "dice-dc"
stackPush(ret)

case typeBlockPush:
if blockIndex > 20 {
ctx.Error = errors.New("语句块嵌套层数过多")
return
}
blockStack[blockIndex] = e.top
blockIndex += 1
case typeBlockPop:
newTop := blockStack[blockIndex-1]
e.top = newTop
blockIndex -= 1
if fstrBlockIndex > 0 {
stackPush(NewStrVal("")) // 在fstring中返回空字符串
} else {
stackPush(NewNullVal())
}

case typeFStringBlockPush:
if fstrBlockIndex >= 4 {
if fstrBlockIndex > 20 {
ctx.Error = errors.New("字符串模板嵌套层数过多")
return
}
fstrBlockStack[fstrBlockIndex] = e.top
fstrBlockIndex += 1
case typeFStringBlockPop:
// 不管栈里多少东西,一律清空
newTop := fstrBlockStack[fstrBlockIndex-1]
var v *VMValue
if newTop != e.top {
Expand All @@ -1015,15 +1024,6 @@ func (ctx *Context) evaluate() {
} 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()
Expand Down
33 changes: 26 additions & 7 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 @@ -1659,20 +1660,38 @@ func TestIfError(t *testing.T) {
}

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")))
assert.True(t, valueEqual(vm.Ret, ns("1 2")))
}
}

vm.Config.EnableV1IfCompatible = true
err = vm.Run("`1 {% if 1 {'test'} %} 2`")
func TestFStringV1IfCompatibleWhile(t *testing.T) {
// while 不返回内容,所以如果是字符串模板的最后一项,无输出
vm := NewVM()
err := vm.Run("`1 {% x = 0; while x < 5 { x = x + 1 } %} 2`")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ns("1 2")))
assert.Equal(t, vm.V1IfCompatibleCount, 1)
}
}

func TestReturnValueWithWhile(t *testing.T) {
// while 不返回内容,所以如果是字符串模板的最后一项,无输出
vm := NewVM()
err := vm.Run("x = 0; while x < 5 { x = x + 1 }")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, NewNullVal()))
}
}

func TestReturnValueWithIf(t *testing.T) {
// while 不返回内容,所以如果是字符串模板的最后一项,无输出
vm := NewVM()
err := vm.Run("x = 0; if 1 { x = 10 }")
if assert.NoError(t, err) {
fmt.Println(vm.Ret.ToString())
assert.True(t, valueEqual(vm.Ret, NewNullVal()))
}
}

Expand Down
Loading

0 comments on commit e177356

Please sign in to comment.