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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ https://sealdice.github.io/dicescript/

[自定义骰点算符用法指南(正则模式)](./docs/CustomDiceRegex.md)

仓库内置的 shell 可以直接预加载脚本文件:

```
go run ./cmd ./script.ds
```

启动时会先执行脚本,再进入 REPL。


## 设计原则

Expand Down
45 changes: 38 additions & 7 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package main

import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
Expand All @@ -16,6 +18,29 @@ var (
historyFn = filepath.Join(os.TempDir(), ".dicescript_history")
)

func printRunResult(vm *ds.Context) {
rest := vm.RestInput
if rest != "" {
rest = fmt.Sprintf(" 剩余文本: %s", rest)
}
fmt.Printf("过程: %s\n", vm.GetDetailText())
fmt.Printf("结果: %s%s\n", vm.Ret.ToString(), rest)
fmt.Printf("栈顶: %d 层数:%d 算力: %d\n", vm.StackTop(), vm.Depth(), vm.NumOpCount)
}

func runScriptFile(vm *ds.Context, path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("读取脚本文件失败: %w", err)
}

data = bytes.TrimPrefix(data, []byte{0xEF, 0xBB, 0xBF})
if err := vm.Run(string(data)); err != nil {
return fmt.Errorf("执行脚本文件失败: %w", err)
}
return nil
}

func main() {
line := liner.NewLiner()
defer line.Close()
Expand Down Expand Up @@ -168,6 +193,16 @@ func main() {
return nil
}

if len(os.Args) > 1 {
scriptPath := os.Args[1]
if err := runScriptFile(vm, scriptPath); err != nil {
fmt.Printf("脚本加载失败: %s (%s)\n", scriptPath, err.Error())
} else {
fmt.Printf("已加载脚本: %s\n", scriptPath)
printRunResult(vm)
}
}

for {
if text, err := line.Prompt(">>> "); err == nil {
if strings.TrimSpace(text) == "" {
Expand All @@ -180,13 +215,7 @@ func main() {
if err != nil {
fmt.Printf("错误: %s\n", err.Error())
} else {
rest := vm.RestInput
if rest != "" {
rest = fmt.Sprintf(" 剩余文本: %s", rest)
}
fmt.Printf("过程: %s\n", vm.GetDetailText())
fmt.Printf("结果: %s%s\n", vm.Ret.ToString(), rest)
fmt.Printf("栈顶: %d 层数:%d 算力: %d\n", vm.StackTop(), vm.Depth(), vm.NumOpCount)
printRunResult(vm)
}

} else if err == liner.ErrPromptAborted {
Expand All @@ -197,6 +226,8 @@ func main() {
ccTimes += 1
fmt.Println("Input Ctrl-c once more to exit")
}
} else if err == io.EOF {
break
} else {
fmt.Print("Error reading line: ", err)
}
Expand Down
21 changes: 21 additions & 0 deletions cmd/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package main

import (
"path/filepath"
"testing"

ds "github.com/sealdice/dicescript"
"github.com/stretchr/testify/assert"
)

func TestRunScriptFile(t *testing.T) {
scriptPath := filepath.Join("testdata", "init.ds")

vm := ds.NewVM()
err := runScriptFile(vm, scriptPath)
assert.NoError(t, err)

err = vm.Run("add(base, 1)")
assert.NoError(t, err)
assert.Equal(t, "42", vm.Ret.ToString())
}
5 changes: 5 additions & 0 deletions cmd/testdata/init.ds
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
func add(a, b) {
return a + b;
}

base = 41
10 changes: 10 additions & 0 deletions docs/GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ DiceScript 被设计为一门专用于TRPG场景的脚本语言。

https://sealdice.github.io/dicescript/

#### 在本地 shell 里试用

仓库自带的 REPL 支持在启动时先执行一个脚本文件:

```bash
go run ./cmd ./script.ds
```

脚本执行完成后,会继续进入交互式 REPL,方便把函数或变量预加载进当前会话。

### 语法

DiceScript的语法从JS、golang和Python上各吸取了一点东西,不过不用担心,语法非常的简单。
Expand Down
85 changes: 80 additions & 5 deletions parser_errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@ var errMsgs = map[string]bilingualMsg{
"unclosedString": {"字符串未闭合", "Unclosed string literal"},
"missingExpr": {"'%c' 后需要表达式", "Expression expected after '%c'"},
"incomplete": {"表达式不完整", "Incomplete expression"},
"unexpectedChar": {"无法识别的字符 '%c'", "Unexpected character '%c'"},
"syntax": {"语法错误", "Syntax error"},
"incompleteIf": {"if 语句不完整,应写成: if 条件 { ... } [else { ... }]", "Incomplete if statement. Use: if condition { ... } [else { ... }]"},
"incompleteIfInTemplate": {
"{} 内的 if 语句不完整,应写成: { if 条件 { ... } [else { ... }] }",
"Incomplete if statement inside {}. Use: { if condition { ... } [else { ... }] }",
},
"unexpectedChar": {"无法识别的字符 '%c'", "Unexpected character '%c'"},
"syntax": {"语法错误", "Syntax error"},
}

// SetParseErrorLanguage 设置解析错误消息的语言
Expand All @@ -56,9 +61,9 @@ func SetParseErrorLanguage(lang int) {
}

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

// formatFriendlyError 生成友好的错误消息
Expand Down Expand Up @@ -86,6 +91,12 @@ func formatFriendlyErrorForLanguage(lang int, pos position, input []byte, expect
}

switch {
case detectTemplateIfSyntaxError(input, pos):
msg = errMsgs["incompleteIfInTemplate"]

case detectIfSyntaxError(input, pos):
msg = errMsgs["incompleteIf"]

case pos.offset == 0 && !isValidStartChar(char):
msg, fmtChar = errMsgs["invalidStart"], char

Expand Down Expand Up @@ -123,6 +134,70 @@ func formatFriendlyErrorForLanguage(lang int, pos position, input []byte, expect
return fmtErr(lang, pos, input, msg, fmtChar)
}

func detectIfSyntaxError(input []byte, pos position) bool {
trimmed := strings.TrimSpace(string(input))
if trimmed == "" {
return false
}

if !strings.HasPrefix(trimmed, "if") {
return false
}

fields := strings.Fields(trimmed)
if len(fields) == 0 || fields[0] != "if" {
return false
}

return !strings.Contains(trimmed, "{")
}

func detectTemplateIfSyntaxError(input []byte, pos position) bool {
text := string(input)
if !strings.Contains(text, "{") || !strings.Contains(text, "if") {
return false
}

if pos.offset >= len(text) || rune(text[pos.offset]) != '}' {
return false
}

lastOpen := strings.LastIndex(text[:pos.offset], "{")
if lastOpen == -1 {
return false
}

segment := strings.TrimSpace(text[lastOpen+1 : pos.offset])
if segment == "if" {
return true
}
if strings.HasPrefix(segment, "%") {
segment = strings.TrimSpace(strings.TrimPrefix(segment, "%"))
return segment == "if"
}
return false
}

func formatFriendlyParseError(lang int, p *parser, input []byte, fallback error) error {
if p == nil {
return fallback
}

expected := make([]string, 0, len(p.maxFailExpected))
seen := map[string]struct{}{}
for _, item := range p.maxFailExpected {
if _, ok := seen[item]; ok {
continue
}
seen[item] = struct{}{}
expected = append(expected, item)
}
if len(expected) == 0 && fallback != nil {
return fallback
}
return formatFriendlyErrorForLanguage(lang, p.maxFailPos, input, expected)
}

// fmtErr 格式化错误输出
func fmtErr(lang int, pos position, input []byte, msg bilingualMsg, char rune) error {
var sb strings.Builder
Expand Down
44 changes: 30 additions & 14 deletions parser_errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,24 @@ func TestIntegrationWithVM_MissingParen(t *testing.T) {
}
}

func TestIntegrationWithVM_IfError(t *testing.T) {
vm := NewVM()
err := vm.Run("if 1 ")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "if 语句不完整")
assert.Contains(t, err.Error(), "Incomplete if statement")
}
}

func TestIntegrationWithVM_TemplateIfError(t *testing.T) {
vm := NewVM()
err := vm.Run("`{ if }`")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "{} 内的 if 语句不完整")
assert.Contains(t, err.Error(), "Incomplete if statement inside {}")
}
}

func TestLanguageOptions_English(t *testing.T) {
vm := NewVM()
vm.Config.ParseErrorLanguage = ParseErrorLanguageEnglish
Expand Down Expand Up @@ -149,22 +167,20 @@ 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")
}
vmChinese := NewVM()
vmChinese.Config.ParseErrorLanguage = ParseErrorLanguageChinese
err := vmChinese.Run("/")
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(), "语法错误")
}
vmEnglish := NewVM()
vmEnglish.Config.ParseErrorLanguage = ParseErrorLanguageEnglish
err = vmEnglish.Run("/")
if assert.Error(t, err) {
assert.Contains(t, err.Error(), "Syntax Error")
assert.NotContains(t, err.Error(), "语法错误")
}
}

Expand Down
12 changes: 7 additions & 5 deletions roll.peg
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,8 @@ _diceType2 <- [dD] nos
// 3d
_diceType3 <- nos [dD]
// d / d优势 / d劣势
_diceType4 <- [dD] ("优势" / "優勢" / "劣势" / "劣勢" / !xidStart)
_diceBoundary <- !xidContinue !('[' / '.' / '(')
_diceType4 <- [dD] ("优势" / "優勢" / "劣势" / "劣勢" / _diceBoundary)

// XdY/dY/Xd 中的 dy + 后缀部分,跟上面 _diceTypeX 一一对应
_diceExpr1 <- [dD] { c.data.AddOp(typeDiceInit); c.data.AddOp(typeDiceSetTimes); } nos _diceMod? _diceModType2?
Expand All @@ -274,15 +275,16 @@ _wodDiceType <- nos _wodTypeMain / _wodTypeMain !xidContinue
_wodMain <- [aA] nos (([mM] nos { c.data.AddOp(typeWodSetPoints) }) / ([kK] nos { c.data.AddOp(typeWodSetThreshold) }) / ([qQ] nos { c.data.AddOp(typeWodSetThresholdQ) }))*

// COC规则,b奖励骰,p惩罚骰
_cocDiceType <- [pPbB] (nos !xidContinue / !xidContinue)
_diceCocBonus <- [bB] (nos !xidContinue / !xidContinue {c.data.PushIntNumber("1")}) detailEnd { c.data.AddOp(typeDiceCocBonus) }
_diceCocPenalty <- [pP] (nos !xidContinue / !xidContinue {c.data.PushIntNumber("1")}) detailEnd { c.data.AddOp(typeDiceCocPenalty) }
_cocDiceBoundary <- !xidContinue !('[' / '.' / '(')
_cocDiceType <- [pPbB] (nos _cocDiceBoundary / _cocDiceBoundary)
_diceCocBonus <- [bB] (nos _cocDiceBoundary / _cocDiceBoundary {c.data.PushIntNumber("1")}) detailEnd { c.data.AddOp(typeDiceCocBonus) }
_diceCocPenalty <- [pP] (nos _cocDiceBoundary / _cocDiceBoundary {c.data.PushIntNumber("1")}) detailEnd { c.data.AddOp(typeDiceCocPenalty) }

// 双十字规则
_dcDiceType <- nos [cC] nos ([mM] nos)*

// Fate规则
_fateDiceType <- [fF] !xidContinue
_fateDiceType <- [fF] _diceBoundary

exprDice <- &{return c.data.PrepareCustomDice(p)} detailStart { c.data.ConsumeCustomDice(p) } detailEnd { c.data.CommitCustomDice() }
/ &_diceType1 detailStart nos _diceExpr1 detailEnd { c.data.AddOp(typeDice); } _diceExprX*
Expand Down
Loading
Loading