Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,6 @@ https://sealdice.github.io/dicescript/

[更新记录](./docs/CHANGELOG.md)

## TODO

* computed 的repr格式无法读入

## 开发

如果修改了文法,使用这个工具重新生成:
Expand Down
42 changes: 38 additions & 4 deletions builtin_functions.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,32 @@ func funcLoadRaw(ctx *Context, this *VMValue, params []*VMValue) *VMValue {
return funcLoadBase(ctx, this, params, true)
}

func funcLoadRawAttr(ctx *Context, this *VMValue, params []*VMValue) *VMValue {
obj := params[0]
attr := params[1]
if attr.TypeId != VMTypeString {
ctx.Error = errors.New("(loadRawAttr)类型错误: 参数2类型必须为str")
return nil
}

ret := obj.attrGetRaw(ctx, attr.Value.(string), true)
if ctx.Error != nil {
return nil
}
return ret
}

func funcLoadRawItem(ctx *Context, this *VMValue, params []*VMValue) *VMValue {
obj := params[0]
index := params[1]

ret := obj.itemGetRaw(ctx, index)
if ctx.Error != nil {
return nil
}
return ret
}

func funcStore(ctx *Context, this *VMValue, params []*VMValue) *VMValue {
name := params[0]
if name.TypeId != VMTypeString {
Expand Down Expand Up @@ -204,10 +230,12 @@ var builtinValues = map[string]*VMValue{
"toStr": nnf(&ndf{"toStr", []string{"value"}, nil, nil, funcToStr}),
"toBool": nnf(&ndf{"toBool", []string{"value"}, nil, nil, funcToBool}),

"repr": nnf(&ndf{"repr", []string{"value"}, nil, nil, funcRepr}),
"load": nnf(&ndf{"load", []string{"value"}, nil, nil, nil}),
"loadRaw": nnf(&ndf{"loadRaw", []string{"value"}, nil, nil, nil}),
"store": nnf(&ndf{"store", []string{"name", "value"}, nil, nil, nil}),
"repr": nnf(&ndf{"repr", []string{"value"}, nil, nil, funcRepr}),
"load": nnf(&ndf{"load", []string{"value"}, nil, nil, nil}),
"loadRaw": nnf(&ndf{"loadRaw", []string{"value"}, nil, nil, nil}),
"loadRawAttr": nnf(&ndf{"loadRawAttr", []string{"obj", "name"}, nil, nil, nil}),
"loadRawItem": nnf(&ndf{"loadRawItem", []string{"obj", "index"}, nil, nil, nil}),
"store": nnf(&ndf{"store", []string{"name", "value"}, nil, nil, nil}),

// TODO: roll()

Expand All @@ -225,6 +253,12 @@ func _init() bool {
nfd, _ = builtinValues["loadRaw"].ReadNativeFunctionData()
nfd.NativeFunc = funcLoadRaw

nfd, _ = builtinValues["loadRawAttr"].ReadNativeFunctionData()
nfd.NativeFunc = funcLoadRawAttr

nfd, _ = builtinValues["loadRawItem"].ReadNativeFunctionData()
nfd.NativeFunc = funcLoadRawItem

nfd, _ = builtinValues["store"].ReadNativeFunctionData()
nfd.NativeFunc = funcStore
return false
Expand Down
37 changes: 37 additions & 0 deletions builtin_functions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,43 @@ func TestNativeFunctionLoadRaw(t *testing.T) {
assert.Error(t, err)
}

func TestNativeFunctionLoadRawAttr(t *testing.T) {
vm := NewVM()
err := vm.Run("a = &(1+2); obj = {'a': &a, 'x': {'a': &a}}; typeId(loadRawAttr(obj, 'a'))")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ni(IntType(VMTypeComputedValue))))
}

vm = NewVM()
err = vm.Run("a = &(1+2); obj = {'a': &a, 'x': {'a': &a}}; repr(loadRawAttr(loadRawItem(obj, 'x'), 'a'))")
if assert.NoError(t, err) {
repr, ok := vm.Ret.ReadString()
if assert.True(t, ok) {
assert.Equal(t, "&(1+2)", repr)
}
}

vm = NewVM()
err = vm.Run("a = &(1+2); obj = {'a': &a}; obj.a")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ni(3)))
}
}

func TestNativeFunctionLoadRawItem(t *testing.T) {
vm := NewVM()
err := vm.Run("a = &(1+2); obj = {'a': &a}; typeId(loadRawItem(obj, 'a'))")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ni(IntType(VMTypeComputedValue))))
}

vm = NewVM()
err = vm.Run("a = &(1+2); arr = [&a]; typeId(loadRawItem(arr, 0))")
if assert.NoError(t, err) {
assert.True(t, valueEqual(vm.Ret, ni(IntType(VMTypeComputedValue))))
}
}

func TestNativeFunctionAbs(t *testing.T) {
vm := NewVM()
assert.True(t, valueEqual(funcAbs(vm, nil, []*VMValue{ni(-5)}), ni(5)))
Expand Down
5 changes: 0 additions & 5 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,23 +51,18 @@ func main() {
vm.Config.HookValueLoadPre = func(ctx *ds.Context, name string) (string, *ds.VMValue) {
re := regexp.MustCompile(`^(困难|极难|大成功|常规|失败|困難|極難|常規|失敗)?([^\d]+)(\d+)?$`)
m := re.FindStringSubmatch(name)
var cocFlagVarPrefix string

if len(m) > 0 {
if m[1] != "" {
cocFlagVarPrefix = m[1]
name = name[len(m[1]):]
}

// 有末值时覆盖,有初值时
if m[3] != "" {
v, _ := strconv.ParseInt(m[3], 10, 64)
fmt.Println("COC值:", name, cocFlagVarPrefix)
return name, ds.NewIntVal(ds.IntType(v))
}
}

fmt.Println("COC值:", name, cocFlagVarPrefix)
return name, nil
}

Expand Down
51 changes: 51 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,36 @@ null代表空值。
&a.x = 5
```

也可以直接写 computed 字面量:

```
a = &(1d6 + 2)
repr(loadRaw('a')) // => &(1d6 + 2)
```

普通读取会自动执行 computed,不只变量名如此,成员读取现在也是一致的:

```
a = &(1d6 + 2)
obj = {'a': &a}
arr = [&a]

a // 执行 computed
obj.a // 也会执行 computed
obj['a'] // 也会执行 computed
arr[0] // 也会执行 computed
```

如果你需要从字典、数组或对象里拿到“公式本体”,而不是计算结果,可以使用新的 raw 读取 API:

```
loadRawAttr(obj, 'a') // 读取 obj.a,但不自动执行 computed
loadRawItem(obj, 'a') // 读取 obj['a'],但不自动执行 computed
loadRawItem(arr, 0) // 读取 arr[0],但不自动执行 computed

repr(loadRawAttr(obj, 'a')) // => &(1d6 + 2)
```

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

> this的解释请参考:https://www.runoob.com/js/js-this.html ,在DiceScript中this的用法基本与JS相同。
Expand Down Expand Up @@ -469,6 +499,24 @@ d['v4'] = 5

请特别注意,字典的键必须为字符串,实际操作中也允许数字类型,但是会自动转换为字符串。

字典函数:
```
d = {'v1': 1, 'v2': &(1d6 + 2)}

d.len() // 键值对数量,2
d.keys() // 所有键,如 ['v1', 'v2']
d.values() // 所有值,如 [1, &(1d6 + 2)],其中 computed 会在后续使用时正常求值
d.items() // 所有键值对,如 [['v1', 1], ['v2', &(1d6 + 2)]]
d.has('v1') // 是否存在该键,1
d.has('v3') // 不存在时返回 0
d.get('v1') // 读取键对应的值,1
d.get('v3', 100) // 键不存在时返回默认值,100
d.getRaw('v2') // 读取原始值,不自动执行 computed
repr(d.getRaw('v2')) // => &(1d6 + 2)
```

注意:这些方法只检查当前字典本身,不会沿 `__proto__` 继续查找。


#### 函数

Expand Down Expand Up @@ -633,6 +681,9 @@ bool(obj) // 将对象二值化,结果为0或1
repr(obj) // 将对象转化为供解释器读取的形式,类似于python的同名函数
load(name) // 读取变量名为name的变量,拿到其值
loadRaw(name) // 读取变量名为name的变量,与load()不同,如果该变量是计算类型,那么不会返回计算后结果
// 对 computed,repr(loadRaw(name)) 现在会得到可再次读入的 &(expr) 形式
loadRawAttr(obj, name) // 读取 obj.name,但不自动执行 computed。name 必须是字符串
loadRawItem(obj, key) // 读取 obj[key],但不自动执行 computed

repr(obj) // 将对象转化为供解释器读取的形式
load(name) // 根据给出的名字,获取对象。 load('a') == a
Expand Down
25 changes: 15 additions & 10 deletions parser_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,25 @@ var errMsgs = map[string]bilingualMsg{
"syntax": {"语法错误", "Syntax error"},
}

func init() {
// 注册错误格式化钩子
ErrorFormatter = formatFriendlyError
}

// SetParseErrorLanguage 设置解析错误消息的语言
func SetParseErrorLanguage(lang int) {
parseErrorLanguage = lang
}

func parseErrorFormatterOption(lang int) option {
return noMatchErrorFormatter(func(pos position, input []byte, expected []string) error {
return formatFriendlyErrorForLanguage(lang, pos, input, expected)
})
}

// formatFriendlyError 生成友好的错误消息
func formatFriendlyError(pos position, input []byte, expected []string) error {
return formatFriendlyErrorForLanguage(parseErrorLanguage, pos, input, expected)
}

func formatFriendlyErrorForLanguage(lang int, pos position, input []byte, expected []string) error {
if len(input) == 0 {
return fmtErr(pos, input, errMsgs["empty"], 0)
return fmtErr(lang, pos, input, errMsgs["empty"], 0)
}

var char rune
Expand Down Expand Up @@ -115,15 +120,15 @@ func formatFriendlyError(pos position, input []byte, expected []string) error {
msg = errMsgs["syntax"]
}

return fmtErr(pos, input, msg, fmtChar)
return fmtErr(lang, pos, input, msg, fmtChar)
}

// fmtErr 格式化错误输出
func fmtErr(pos position, input []byte, msg bilingualMsg, char rune) error {
func fmtErr(lang int, pos position, input []byte, msg bilingualMsg, char rune) error {
var sb strings.Builder

// 标题
switch parseErrorLanguage {
switch lang {
case ParseErrorLanguageChinese:
sb.WriteString("语法错误\n")
case ParseErrorLanguageEnglish:
Expand Down Expand Up @@ -156,7 +161,7 @@ func fmtErr(pos position, input []byte, msg bilingualMsg, char rune) error {
}

// 位置和消息
switch parseErrorLanguage {
switch lang {
case ParseErrorLanguageChinese:
sb.WriteString(fmt.Sprintf(" 位置 %d:%d - %s", pos.line, pos.col, cn))
case ParseErrorLanguageEnglish:
Expand Down
20 changes: 20 additions & 0 deletions parser_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,26 @@ func TestLanguageOptions_Bilingual(t *testing.T) {
}
}

func TestParseErrorFormatterOptionIsParserScoped(t *testing.T) {
pChinese := newParser("", []byte("/"), parseErrorFormatterOption(ParseErrorLanguageChinese))
if assert.NotNil(t, pChinese.noMatchErrorFormatter) {
err := pChinese.noMatchErrorFormatter(position{line: 1, col: 1, offset: 0}, []byte("/"), []string{"expr"})
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "语法错误")
assert.NotContains(t, err.Error(), "Syntax Error")
}
}

pEnglish := newParser("", []byte("/"), parseErrorFormatterOption(ParseErrorLanguageEnglish))
if assert.NotNil(t, pEnglish.noMatchErrorFormatter) {
err := pEnglish.noMatchErrorFormatter(position{line: 1, col: 1, offset: 0}, []byte("/"), []string{"expr"})
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "Syntax Error")
assert.NotContains(t, err.Error(), "语法错误")
}
}
}

func TestErrorContext_ShowsCodeAndPointer(t *testing.T) {
vm := NewVM()
// 使用一个确实会产生错误的表达式
Expand Down
2 changes: 2 additions & 0 deletions roll.peg
Original file line number Diff line number Diff line change
Expand Up @@ -319,11 +319,13 @@ value_id_without_colon <- id:identifierWithoutColon sp { c.data.WriteCode(typeLo

value_array_range <- '[' sp exprRoot ".." sp exprRoot ']' sp { c.data.AddOp(typePushRange) }
value_array <- '[' sp { c.data.CounterPush(); c.data.CounterAdd(1) } exprRoot (',' sp exprRoot {c.data.CounterAdd(1)} )* ']' sp { c.data.PushArray(c.data.CounterPop()) }
value_computed <- '&' parenOpen { c.data.CodePush(p.pt.offset) } text:< exprRoot > parenClose { c.data.AddStoreComputedOnStack(text.(string)) }

value <- "true" sp { c.data.PushIntNumber("1"); }
/ "false" sp { c.data.PushIntNumber("0"); }
/ "null" sp { c.data.PushNull() }
/ "this" sp { c.data.PushThis() } item_get attr_get
/ &value_computed value_computed attr_get
/ '&' id:identifier sp { c.data.WriteCode(typeLoadNameRaw, id.(string)); } attr_get

/ float
Expand Down
Loading
Loading