aboutsummaryrefslogtreecommitdiff
path: root/src/urubu/cmd
diff options
context:
space:
mode:
Diffstat (limited to 'src/urubu/cmd')
-rw-r--r--src/urubu/cmd/ucdgen/main.go98
-rw-r--r--src/urubu/cmd/vartan-go/generate.go98
-rw-r--r--src/urubu/cmd/vartan-go/main.go14
-rw-r--r--src/urubu/cmd/vartan/compile.go190
-rw-r--r--src/urubu/cmd/vartan/main.go14
-rw-r--r--src/urubu/cmd/vartan/parse.go110
-rw-r--r--src/urubu/cmd/vartan/root.go22
-rw-r--r--src/urubu/cmd/vartan/show.go295
-rw-r--r--src/urubu/cmd/vartan/test.go50
9 files changed, 891 insertions, 0 deletions
diff --git a/src/urubu/cmd/ucdgen/main.go b/src/urubu/cmd/ucdgen/main.go
new file mode 100644
index 0000000..d5599a6
--- /dev/null
+++ b/src/urubu/cmd/ucdgen/main.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "strings"
+ "text/template"
+
+ "urubu/ucd"
+)
+
+func main() {
+ err := gen()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "%v\n", err)
+ os.Exit(1)
+ }
+}
+
+func gen() error {
+ var propValAliases *ucd.PropertyValueAliases
+ {
+ resp, err := http.Get("https://www.unicode.org/Public/13.0.0/ucd/PropertyValueAliases.txt")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ propValAliases, err = ucd.ParsePropertyValueAliases(resp.Body)
+ if err != nil {
+ return err
+ }
+ }
+ var unicodeData *ucd.UnicodeData
+ {
+ resp, err := http.Get("https://www.unicode.org/Public/13.0.0/ucd/UnicodeData.txt")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ unicodeData, err = ucd.ParseUnicodeData(resp.Body, propValAliases)
+ if err != nil {
+ return err
+ }
+ }
+ var scripts *ucd.Scripts
+ {
+ resp, err := http.Get("https://www.unicode.org/Public/13.0.0/ucd/Scripts.txt")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ scripts, err = ucd.ParseScripts(resp.Body, propValAliases)
+ if err != nil {
+ return err
+ }
+ }
+ var propList *ucd.PropList
+ {
+ resp, err := http.Get("https://www.unicode.org/Public/13.0.0/ucd/PropList.txt")
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+ propList, err = ucd.ParsePropList(resp.Body)
+ if err != nil {
+ return err
+ }
+ }
+ tmpl, err := template.ParseFiles("../ucd/codepoint.go.tmpl")
+ if err != nil {
+ return err
+ }
+ var b strings.Builder
+ err = tmpl.Execute(&b, struct {
+ GeneratorName string
+ UnicodeData *ucd.UnicodeData
+ Scripts *ucd.Scripts
+ PropList *ucd.PropList
+ PropertyValueAliases *ucd.PropertyValueAliases
+ }{
+ GeneratorName: "generator/main.go",
+ UnicodeData: unicodeData,
+ Scripts: scripts,
+ PropList: propList,
+ PropertyValueAliases: propValAliases,
+ })
+ if err != nil {
+ return err
+ }
+ f, err := os.OpenFile("../ucd/codepoint.go", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ fmt.Fprint(f, b.String())
+ return nil
+}
diff --git a/src/urubu/cmd/vartan-go/generate.go b/src/urubu/cmd/vartan-go/generate.go
new file mode 100644
index 0000000..8f2da84
--- /dev/null
+++ b/src/urubu/cmd/vartan-go/generate.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+
+ "urubu/driver/lexer"
+ "urubu/driver/parser"
+ spec "urubu/spec/grammar"
+)
+
+func runGenerate(args []string) error {
+ cgram, err := readCompiledGrammar(args[0])
+ if err != nil {
+ return fmt.Errorf("Cannot read a compiled grammar: %w", err)
+ }
+
+ {
+ b, err := lexer.GenLexer(cgram.Lexical, "main")
+ if err != nil {
+ return fmt.Errorf("Failed to generate a lexer: %w", err)
+ }
+
+ filePath := fmt.Sprintf("%v_lexer.go", cgram.Name)
+
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return fmt.Errorf("Failed to create an output file: %v", err)
+ }
+ defer f.Close()
+
+ _, err = f.Write(b)
+ if err != nil {
+ return fmt.Errorf("Failed to write lexer source code: %v", err)
+ }
+ }
+
+ {
+ b, err := parser.GenParser(cgram, "main")
+ if err != nil {
+ return fmt.Errorf("Failed to generate a parser: %w", err)
+ }
+
+ filePath := fmt.Sprintf("%v_parser.go", cgram.Name)
+
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return fmt.Errorf("Failed to create an output file: %v", err)
+ }
+ defer f.Close()
+
+ _, err = f.Write(b)
+ if err != nil {
+ return fmt.Errorf("Failed to write parser source code: %v", err)
+ }
+ }
+
+ {
+ b, err := parser.GenSemanticAction("main")
+ if err != nil {
+ return fmt.Errorf("Failed to generate a semantic action set: %w", err)
+ }
+
+ filePath := fmt.Sprintf("%v_semantic_action.go", cgram.Name)
+
+ f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return fmt.Errorf("Failed to create an output file: %v", err)
+ }
+ defer f.Close()
+
+ _, err = f.Write(b)
+ if err != nil {
+ return fmt.Errorf("Failed to write semantic action source code: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func readCompiledGrammar(path string) (*spec.CompiledGrammar, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ data, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+ cgram := &spec.CompiledGrammar{}
+ err = json.Unmarshal(data, cgram)
+ if err != nil {
+ return nil, err
+ }
+ return cgram, nil
+}
diff --git a/src/urubu/cmd/vartan-go/main.go b/src/urubu/cmd/vartan-go/main.go
new file mode 100644
index 0000000..315e7b3
--- /dev/null
+++ b/src/urubu/cmd/vartan-go/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func main() {
+ err := runGenerate(os.Args[1:])
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/src/urubu/cmd/vartan/compile.go b/src/urubu/cmd/vartan/compile.go
new file mode 100644
index 0000000..79fa9ef
--- /dev/null
+++ b/src/urubu/cmd/vartan/compile.go
@@ -0,0 +1,190 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ verr "urubu/error"
+ "urubu/grammar"
+ spec "urubu/spec/grammar"
+ "urubu/spec/grammar/parser"
+)
+
+
+
+func runCompile(args []string) (retErr error) {
+ var tmpDirPath string
+ defer func() {
+ if tmpDirPath == "" {
+ return
+ }
+ os.RemoveAll(tmpDirPath)
+ }()
+
+ var grmPath string
+ if len(args) > 0 {
+ grmPath = args[0]
+ }
+ defer func() {
+ if retErr != nil {
+ specErrs, ok := retErr.(verr.SpecErrors)
+ if ok {
+ for _, err := range specErrs {
+ if len(args) > 0 {
+ err.FilePath = grmPath
+ err.SourceName = grmPath
+ } else {
+ err.FilePath = grmPath
+ err.SourceName = "stdin"
+ }
+ }
+ }
+ }
+ }()
+
+ if grmPath == "" {
+ var err error
+ tmpDirPath, err = os.MkdirTemp("", "vartan-compile-*")
+ if err != nil {
+ return err
+ }
+
+ src, err := io.ReadAll(os.Stdin)
+ if err != nil {
+ return err
+ }
+
+ grmPath = filepath.Join(tmpDirPath, "stdin.vartan")
+ err = os.WriteFile(grmPath, src, 0600)
+ if err != nil {
+ return err
+ }
+ }
+
+ gram, report, err := readGrammar(grmPath)
+ if err != nil {
+ return err
+ }
+
+ err = writeCompiledGrammarAndReport(gram, report, "")
+ if err != nil {
+ return fmt.Errorf("Cannot write an output files: %w", err)
+ }
+
+ var implicitlyResolvedCount int
+ for _, s := range report.States {
+ for _, c := range s.SRConflict {
+ if c.ResolvedBy == grammar.ResolvedByShift.Int() {
+ implicitlyResolvedCount++
+ }
+ }
+ for _, c := range s.RRConflict {
+ if c.ResolvedBy == grammar.ResolvedByProdOrder.Int() {
+ implicitlyResolvedCount++
+ }
+ }
+ }
+ if implicitlyResolvedCount > 0 {
+ fmt.Fprintf(os.Stdout, "%v conflicts\n", implicitlyResolvedCount)
+ }
+
+ return nil
+}
+
+func readGrammar(path string) (*spec.CompiledGrammar, *spec.Report, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, nil, fmt.Errorf("Cannot open the grammar file %s: %w", path, err)
+ }
+ defer f.Close()
+
+ ast, err := parser.Parse(f)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ b := grammar.GrammarBuilder{
+ AST: ast,
+ }
+ return b.Build(grammar.EnableReporting())
+}
+
+// writeCompiledGrammarAndReport writes a compiled grammar and a report to a files located at a specified path.
+// This function selects one of the following output methods depending on how the path is specified.
+//
+// 1. When the path is a directory path, this function writes the compiled grammar and the report to
+// <path>/<grammar-name>.json and <path>/<grammar-name>-report.json files, respectively.
+// <grammar-name>-report.json as the output files.
+// 2. When the path is a file path or a non-exitent path, this function asumes that the path represents a file
+// path for the compiled grammar. Then it also writes the report in the same directory as the compiled grammar.
+// The report file is named <grammar-name>.json.
+// 3. When the path is an empty string, this function writes the compiled grammar to the stdout and writes
+// the report to a file named <current-directory>/<grammar-name>-report.json.
+func writeCompiledGrammarAndReport(cgram *spec.CompiledGrammar, report *spec.Report, path string) error {
+ cgramPath, reportPath, err := makeOutputFilePaths(cgram.Name, path)
+ if err != nil {
+ return err
+ }
+
+ {
+ var cgramW io.Writer
+ if cgramPath != "" {
+ cgramFile, err := os.OpenFile(cgramPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return err
+ }
+ defer cgramFile.Close()
+ cgramW = cgramFile
+ } else {
+ cgramW = os.Stdout
+ }
+
+ b, err := json.Marshal(cgram)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(cgramW, "%v\n", string(b))
+ }
+
+ {
+ reportFile, err := os.OpenFile(reportPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+ if err != nil {
+ return err
+ }
+ defer reportFile.Close()
+
+ b, err := json.Marshal(report)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(reportFile, "%v\n", string(b))
+ }
+
+ return nil
+}
+
+func makeOutputFilePaths(gramName string, path string) (string, string, error) {
+ reportFileName := gramName + "-report.json"
+
+ if path == "" {
+ wd, err := os.Getwd()
+ if err != nil {
+ return "", "", err
+ }
+ return "", filepath.Join(wd, reportFileName), nil
+ }
+
+ fi, err := os.Stat(path)
+ if err != nil && !os.IsNotExist(err) {
+ return "", "", err
+ }
+ if os.IsNotExist(err) || !fi.IsDir() {
+ dir, _ := filepath.Split(path)
+ return path, filepath.Join(dir, reportFileName), nil
+ }
+
+ return filepath.Join(path, gramName+".json"), filepath.Join(path, reportFileName), nil
+}
diff --git a/src/urubu/cmd/vartan/main.go b/src/urubu/cmd/vartan/main.go
new file mode 100644
index 0000000..98f98e1
--- /dev/null
+++ b/src/urubu/cmd/vartan/main.go
@@ -0,0 +1,14 @@
+package main
+
+import (
+ "fmt"
+ "os"
+)
+
+func main() {
+ err := Execute()
+ if err != nil {
+ fmt.Fprintln(os.Stderr, err)
+ os.Exit(1)
+ }
+}
diff --git a/src/urubu/cmd/vartan/parse.go b/src/urubu/cmd/vartan/parse.go
new file mode 100644
index 0000000..9c5fd9c
--- /dev/null
+++ b/src/urubu/cmd/vartan/parse.go
@@ -0,0 +1,110 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+
+ driver "urubu/driver/parser"
+ spec "urubu/spec/grammar"
+)
+
+
+
+func runParse(args []string) error {
+ cg, err := readCompiledGrammar(args[0])
+ if err != nil {
+ return fmt.Errorf("Cannot read a compiled grammar: %w", err)
+ }
+
+ src := os.Stdin
+ gram := driver.NewGrammar(cg)
+
+ tb := driver.NewDefaultSyntaxTreeBuilder()
+ treeAct := driver.NewCSTActionSet(gram, tb)
+
+ opts := []driver.ParserOption{}
+ opts = append(opts, driver.SemanticAction(treeAct))
+
+ toks, err := driver.NewTokenStream(cg, src)
+ if err != nil {
+ return err
+ }
+
+ p, err := driver.NewParser(toks, gram, opts...)
+ if err != nil {
+ return err
+ }
+
+ err = p.Parse()
+ if err != nil {
+ return err
+ }
+
+ // A parser can construct a parse tree even if syntax errors occur.
+ // When therer is a parse tree, print it.
+ if tree := tb.Tree(); tree != nil {
+ b, err := json.Marshal(tree)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintln(os.Stdout, string(b))
+ }
+
+ if len(p.SyntaxErrors()) > 0 {
+ var b strings.Builder
+ synErrs := p.SyntaxErrors()
+ writeSyntaxErrorMessage(&b, cg, synErrs[0])
+ for _, synErr := range synErrs[1:] {
+ fmt.Fprintf(&b, "\n")
+ writeSyntaxErrorMessage(&b, cg, synErr)
+ }
+ if b.Len() > 0 {
+ return fmt.Errorf(b.String())
+ }
+ }
+
+ return nil
+}
+
+func readCompiledGrammar(path string) (*spec.CompiledGrammar, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ data, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+ cg := &spec.CompiledGrammar{}
+ err = json.Unmarshal(data, cg)
+ if err != nil {
+ return nil, err
+ }
+ return cg, nil
+}
+
+func writeSyntaxErrorMessage(b *strings.Builder, cgram *spec.CompiledGrammar, synErr *driver.SyntaxError) {
+ fmt.Fprintf(b, "%v:%v: %v: ", synErr.Row+1, synErr.Col+1, synErr.Message)
+
+ tok := synErr.Token
+ switch {
+ case tok.EOF():
+ fmt.Fprintf(b, "<eof>")
+ case tok.Invalid():
+ fmt.Fprintf(b, "'%v' (<invalid>)", string(tok.Lexeme()))
+ default:
+ if kind := cgram.Syntactic.Terminals[tok.TerminalID()]; kind != "" {
+ fmt.Fprintf(b, "'%v' (%v)", string(tok.Lexeme()), kind)
+ } else {
+ fmt.Fprintf(b, "'%v'", string(tok.Lexeme()))
+ }
+ }
+
+ fmt.Fprintf(b, ": expected: %v", synErr.ExpectedTerminals[0])
+ for _, t := range synErr.ExpectedTerminals[1:] {
+ fmt.Fprintf(b, ", %v", t)
+ }
+}
diff --git a/src/urubu/cmd/vartan/root.go b/src/urubu/cmd/vartan/root.go
new file mode 100644
index 0000000..3dda70c
--- /dev/null
+++ b/src/urubu/cmd/vartan/root.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "os"
+)
+
+func Execute() error {
+ cmd := os.Args[1]
+ args := os.Args[2:]
+
+ if cmd == "compile" {
+ return runCompile(args)
+ } else if cmd == "parse" {
+ return runParse(args)
+ } else if cmd == "show" {
+ return runShow(args)
+ } else if cmd == "test" {
+ return runTest(args)
+ }
+
+ return nil // FIXME
+}
diff --git a/src/urubu/cmd/vartan/show.go b/src/urubu/cmd/vartan/show.go
new file mode 100644
index 0000000..cab63a0
--- /dev/null
+++ b/src/urubu/cmd/vartan/show.go
@@ -0,0 +1,295 @@
+package main
+
+import (
+ "encoding/json"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "text/template"
+
+ "urubu/grammar"
+ spec "urubu/spec/grammar"
+)
+
+
+
+func runShow(args []string) error {
+ report, err := readReport(args[0])
+ if err != nil {
+ return err
+ }
+
+ err = writeReport(os.Stdout, report)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func readReport(path string) (*spec.Report, error) {
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("Cannot open the report %s: %w", path, err)
+ }
+ defer f.Close()
+
+ d, err := io.ReadAll(f)
+ if err != nil {
+ return nil, err
+ }
+
+ report := &spec.Report{}
+ err = json.Unmarshal(d, report)
+ if err != nil {
+ return nil, err
+ }
+
+ return report, nil
+}
+
+const reportTemplate = `# Conflicts
+
+{{ printConflictSummary . }}
+
+# Terminals
+
+{{ range slice .Terminals 1 -}}
+{{ printTerminal . }}
+{{ end }}
+# Productions
+
+{{ range slice .Productions 1 -}}
+{{ printProduction . }}
+{{ end }}
+# States
+{{ range .States }}
+## State {{ .Number }}
+
+{{ range .Kernel -}}
+{{ printItem . }}
+{{ end }}
+{{ range .Shift -}}
+{{ printShift . }}
+{{ end -}}
+{{ range .Reduce -}}
+{{ printReduce . }}
+{{ end -}}
+{{ range .GoTo -}}
+{{ printGoTo . }}
+{{ end }}
+{{ range .SRConflict -}}
+{{ printSRConflict . }}
+{{ end -}}
+{{ range .RRConflict -}}
+{{ printRRConflict . }}
+{{ end -}}
+{{ end }}`
+
+func writeReport(w io.Writer, report *spec.Report) error {
+ termName := func(sym int) string {
+ return report.Terminals[sym].Name
+ }
+
+ nonTermName := func(sym int) string {
+ return report.NonTerminals[sym].Name
+ }
+
+ termAssoc := func(sym int) string {
+ switch report.Terminals[sym].Associativity {
+ case "l":
+ return "left"
+ case "r":
+ return "right"
+ default:
+ return "no"
+ }
+ }
+
+ prodAssoc := func(prod int) string {
+ switch report.Productions[prod].Associativity {
+ case "l":
+ return "left"
+ case "r":
+ return "right"
+ default:
+ return "no"
+ }
+ }
+
+ fns := template.FuncMap{
+ "printConflictSummary": func(report *spec.Report) string {
+ var implicitlyResolvedCount int
+ var explicitlyResolvedCount int
+ for _, s := range report.States {
+ for _, c := range s.SRConflict {
+ if c.ResolvedBy == grammar.ResolvedByShift.Int() {
+ implicitlyResolvedCount++
+ } else {
+ explicitlyResolvedCount++
+ }
+ }
+ for _, c := range s.RRConflict {
+ if c.ResolvedBy == grammar.ResolvedByProdOrder.Int() {
+ implicitlyResolvedCount++
+ } else {
+ explicitlyResolvedCount++
+ }
+ }
+ }
+
+ var b strings.Builder
+ if implicitlyResolvedCount == 1 {
+ fmt.Fprintf(&b, "%v conflict occurred and resolved implicitly.\n", implicitlyResolvedCount)
+ } else if implicitlyResolvedCount > 1 {
+ fmt.Fprintf(&b, "%v conflicts occurred and resolved implicitly.\n", implicitlyResolvedCount)
+ }
+ if explicitlyResolvedCount == 1 {
+ fmt.Fprintf(&b, "%v conflict occurred and resolved explicitly.\n", explicitlyResolvedCount)
+ } else if explicitlyResolvedCount > 1 {
+ fmt.Fprintf(&b, "%v conflicts occurred and resolved explicitly.\n", explicitlyResolvedCount)
+ }
+ if implicitlyResolvedCount == 0 && explicitlyResolvedCount == 0 {
+ fmt.Fprintf(&b, "No conflict")
+ }
+ return b.String()
+ },
+ "printTerminal": func(term spec.Terminal) string {
+ var prec string
+ if term.Precedence != 0 {
+ prec = fmt.Sprintf("%2v", term.Precedence)
+ } else {
+ prec = " -"
+ }
+
+ var assoc string
+ if term.Associativity != "" {
+ assoc = term.Associativity
+ } else {
+ assoc = "-"
+ }
+
+ return fmt.Sprintf("%4v %v %v %v", term.Number, prec, assoc, term.Name)
+ },
+ "printProduction": func(prod spec.Production) string {
+ var prec string
+ if prod.Precedence != 0 {
+ prec = fmt.Sprintf("%2v", prod.Precedence)
+ } else {
+ prec = " -"
+ }
+
+ var assoc string
+ if prod.Associativity != "" {
+ assoc = prod.Associativity
+ } else {
+ assoc = "-"
+ }
+
+ var b strings.Builder
+ fmt.Fprintf(&b, "%v →", nonTermName(prod.LHS))
+ if len(prod.RHS) > 0 {
+ for _, e := range prod.RHS {
+ if e > 0 {
+ fmt.Fprintf(&b, " %v", termName(e))
+ } else {
+ fmt.Fprintf(&b, " %v", nonTermName(e*-1))
+ }
+ }
+ } else {
+ fmt.Fprintf(&b, " ε")
+ }
+
+ return fmt.Sprintf("%4v %v %v %v", prod.Number, prec, assoc, b.String())
+ },
+ "printItem": func(item spec.Item) string {
+ prod := report.Productions[item.Production]
+
+ var b strings.Builder
+ fmt.Fprintf(&b, "%v →", nonTermName(prod.LHS))
+ for i, e := range prod.RHS {
+ if i == item.Dot {
+ fmt.Fprintf(&b, " ・")
+ }
+ if e > 0 {
+ fmt.Fprintf(&b, " %v", termName(e))
+ } else {
+ fmt.Fprintf(&b, " %v", nonTermName(e*-1))
+ }
+ }
+ if item.Dot >= len(prod.RHS) {
+ fmt.Fprintf(&b, " ・")
+ }
+
+ return fmt.Sprintf("%4v %v", prod.Number, b.String())
+ },
+ "printShift": func(tran spec.Transition) string {
+ return fmt.Sprintf("shift %4v on %v", tran.State, termName(tran.Symbol))
+ },
+ "printReduce": func(reduce spec.Reduce) string {
+ var b strings.Builder
+ {
+ fmt.Fprintf(&b, "%v", termName(reduce.LookAhead[0]))
+ for _, a := range reduce.LookAhead[1:] {
+ fmt.Fprintf(&b, ", %v", termName(a))
+ }
+ }
+ return fmt.Sprintf("reduce %4v on %v", reduce.Production, b.String())
+ },
+ "printGoTo": func(tran spec.Transition) string {
+ return fmt.Sprintf("goto %4v on %v", tran.State, nonTermName(tran.Symbol))
+ },
+ "printSRConflict": func(sr spec.SRConflict) string {
+ var adopted string
+ switch {
+ case sr.AdoptedState != nil:
+ adopted = fmt.Sprintf("shift %v", *sr.AdoptedState)
+ case sr.AdoptedProduction != nil:
+ adopted = fmt.Sprintf("reduce %v", *sr.AdoptedProduction)
+ }
+ var resolvedBy string
+ switch sr.ResolvedBy {
+ case grammar.ResolvedByPrec.Int():
+ if sr.AdoptedState != nil {
+ resolvedBy = fmt.Sprintf("symbol %v has higher precedence than production %v", termName(sr.Symbol), sr.Production)
+ } else {
+ resolvedBy = fmt.Sprintf("production %v has higher precedence than symbol %v", sr.Production, termName(sr.Symbol))
+ }
+ case grammar.ResolvedByAssoc.Int():
+ if sr.AdoptedState != nil {
+ resolvedBy = fmt.Sprintf("symbol %v and production %v has the same precedence, and symbol %v has %v associativity", termName(sr.Symbol), sr.Production, termName(sr.Symbol), termAssoc(sr.Symbol))
+ } else {
+ resolvedBy = fmt.Sprintf("production %v and symbol %v has the same precedence, and production %v has %v associativity", sr.Production, termName(sr.Symbol), sr.Production, prodAssoc(sr.Production))
+ }
+ case grammar.ResolvedByShift.Int():
+ resolvedBy = fmt.Sprintf("symbol %v and production %v don't define a precedence comparison (default rule)", sr.Symbol, sr.Production)
+ default:
+ resolvedBy = "?" // This is a bug.
+ }
+ return fmt.Sprintf("shift/reduce conflict (shift %v, reduce %v) on %v: %v adopted because %v", sr.State, sr.Production, termName(sr.Symbol), adopted, resolvedBy)
+ },
+ "printRRConflict": func(rr spec.RRConflict) string {
+ var resolvedBy string
+ switch rr.ResolvedBy {
+ case grammar.ResolvedByProdOrder.Int():
+ resolvedBy = fmt.Sprintf("production %v and %v don't define a precedence comparison (default rule)", rr.Production1, rr.Production2)
+ default:
+ resolvedBy = "?" // This is a bug.
+ }
+ return fmt.Sprintf("reduce/reduce conflict (%v, %v) on %v: reduce %v adopted because %v", rr.Production1, rr.Production2, termName(rr.Symbol), rr.AdoptedProduction, resolvedBy)
+ },
+ }
+
+ tmpl, err := template.New("").Funcs(fns).Parse(reportTemplate)
+ if err != nil {
+ return err
+ }
+
+ err = tmpl.Execute(w, report)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
diff --git a/src/urubu/cmd/vartan/test.go b/src/urubu/cmd/vartan/test.go
new file mode 100644
index 0000000..eb24c0c
--- /dev/null
+++ b/src/urubu/cmd/vartan/test.go
@@ -0,0 +1,50 @@
+package main
+
+import (
+ "errors"
+ "fmt"
+ "os"
+
+ "urubu/tester"
+)
+
+
+
+func runTest(args []string) error {
+ gram, _, err := readGrammar(args[0])
+ if err != nil {
+ return fmt.Errorf("Cannot read a grammar: %w", err)
+ }
+
+ var cs []*tester.TestCaseWithMetadata
+ {
+ cs = tester.ListTestCases(args[1])
+ errOccurred := false
+ for _, c := range cs {
+ if c.Error != nil {
+ fmt.Fprintf(os.Stderr, "Failed to read a test case or a directory: %v\n%v\n", c.FilePath, c.Error)
+ errOccurred = true
+ }
+ }
+ if errOccurred {
+ return errors.New("Cannot run test")
+ }
+ }
+
+ t := &tester.Tester{
+ Grammar: gram,
+ Cases: cs,
+ }
+ rs := t.Run()
+ testFailed := false
+ for _, r := range rs {
+ fmt.Fprintln(os.Stdout, r)
+ if r.Error != nil {
+ testFailed = true
+ }
+ }
+ if testFailed {
+ return errors.New("Test failed")
+ }
+ return nil
+}