blob: 61f82e6c01f8329cfe553d3165ff3cc213bd43a7 [file] [log] [blame]
Jiri Simsad7616c92015-03-24 23:44:30 -07001// Copyright 2015 The Vanadium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
Todd Wang232d6492015-02-25 18:04:54 -08005package compile
6
7import (
8 "fmt"
9 "regexp"
10 "strconv"
11
12 "v.io/v23/i18n"
13 "v.io/v23/vdl"
Jiri Simsaffceefa2015-02-28 11:03:34 -080014 "v.io/x/ref/lib/vdl/parse"
Todd Wang232d6492015-02-25 18:04:54 -080015)
16
17// ErrorDef represents a user-defined error definition in the compiled results.
18type ErrorDef struct {
19 NamePos // name, parse position and docs
20 Exported bool // is this error definition exported?
21 ID string // error ID
22 RetryCode vdl.WireRetryCode // retry action to be performed by client
23 Params []*Field // list of positional parameter names and types
24 Formats []LangFmt // list of language / format pairs
25 English string // English format text from Formats
26}
27
28// LangFmt represents a language / format string pair.
29type LangFmt struct {
30 Lang i18n.LangID // IETF language tag
31 Fmt string // i18n format string in the given language.
32}
33
34func (x *ErrorDef) String() string {
35 return fmt.Sprintf("%+v", *x)
36}
37
38// compileErrorDefs fills in pkg with compiled error definitions.
39func compileErrorDefs(pkg *Package, pfiles []*parse.File, env *Env) {
40 for index := range pkg.Files {
41 file, pfile := pkg.Files[index], pfiles[index]
42 for _, ped := range pfile.ErrorDefs {
43 name, detail := ped.Name, identDetail("error", file, ped.Pos)
Todd Wang53a4e2e2015-03-18 10:54:54 -070044 export, err := validIdent(name, reservedNormal)
Todd Wang232d6492015-02-25 18:04:54 -080045 if err != nil {
46 env.prefixErrorf(file, ped.Pos, err, "error %s invalid name", name)
47 continue
48 }
49 if err := file.DeclareIdent(name, detail); err != nil {
50 env.prefixErrorf(file, ped.Pos, err, "error %s name conflict", name)
51 continue
52 }
53 id := pkg.Path + "." + name
54 ed := &ErrorDef{NamePos: NamePos(ped.NamePos), Exported: export, ID: id}
55 defineErrorActions(ed, name, ped.Actions, file, env)
56 ed.Params = defineErrorParams(name, ped.Params, file, env)
57 ed.Formats = defineErrorFormats(name, ped.Formats, ed.Params, file, env)
58 // We require the "en" base language for at least one of the Formats, and
59 // favor "en-US" if it exists. This requirement is an attempt to ensure
60 // there is at least one common language across all errors.
61 for _, lf := range ed.Formats {
62 if lf.Lang == i18n.LangID("en-US") {
63 ed.English = lf.Fmt
64 break
65 }
66 if ed.English == "" && i18n.BaseLangID(lf.Lang) == i18n.LangID("en") {
67 ed.English = lf.Fmt
68 }
69 }
70 if ed.English == "" {
71 env.Errorf(file, ed.Pos, "error %s invalid (must define at least one English format)", name)
72 continue
73 }
74 file.ErrorDefs = append(file.ErrorDefs, ed)
75 }
76 }
77}
78
79func defineErrorActions(ed *ErrorDef, name string, pactions []parse.StringPos, file *File, env *Env) {
80 // We allow multiple actions to be specified in the parser, so that it's easy
81 // to add new actions in the future.
82 seenRetry := false
83 for _, pact := range pactions {
84 if retry, err := vdl.WireRetryCodeFromString(pact.String); err == nil {
85 if seenRetry {
86 env.Errorf(file, pact.Pos, "error %s action %s invalid (retry action specified multiple times)", name, pact.String)
87 continue
88 }
89 seenRetry = true
90 ed.RetryCode = retry
91 continue
92 }
93 env.Errorf(file, pact.Pos, "error %s action %s invalid (unknown action)", name, pact.String)
94 }
95}
96
97func defineErrorParams(name string, pparams []*parse.Field, file *File, env *Env) []*Field {
98 var params []*Field
99 seen := make(map[string]*parse.Field)
100 for _, pparam := range pparams {
101 pname, pos := pparam.Name, pparam.Pos
102 if pname == "" {
103 env.Errorf(file, pos, "error %s invalid (parameters must be named)", name)
104 return nil
105 }
106 if dup := seen[pname]; dup != nil {
107 env.Errorf(file, pos, "error %s param %s duplicate name (previous at %s)", name, pname, dup.Pos)
108 continue
109 }
110 seen[pname] = pparam
Todd Wang53a4e2e2015-03-18 10:54:54 -0700111 if _, err := validIdent(pname, reservedFirstRuneLower); err != nil {
Todd Wang232d6492015-02-25 18:04:54 -0800112 env.prefixErrorf(file, pos, err, "error %s param %s invalid", name, pname)
113 continue
114 }
115 param := &Field{NamePos(pparam.NamePos), compileType(pparam.Type, file, env)}
116 params = append(params, param)
117 }
118 return params
119}
120
121func defineErrorFormats(name string, plfs []parse.LangFmt, params []*Field, file *File, env *Env) []LangFmt {
122 var lfs []LangFmt
123 seen := make(map[i18n.LangID]parse.LangFmt)
124 for _, plf := range plfs {
125 pos, lang, fmt := plf.Pos(), i18n.LangID(plf.Lang.String), plf.Fmt.String
126 if lang == "" {
127 env.Errorf(file, pos, "error %s has empty language identifier", name)
128 continue
129 }
130 if dup, ok := seen[lang]; ok {
131 env.Errorf(file, pos, "error %s duplicate language %s (previous at %s)", name, lang, dup.Pos())
132 continue
133 }
134 seen[lang] = plf
135 xfmt, err := xlateErrorFormat(fmt, params)
136 if err != nil {
137 env.prefixErrorf(file, pos, err, "error %s language %s format invalid", name, lang)
138 continue
139 }
140 lfs = append(lfs, LangFmt{lang, xfmt})
141 }
142 return lfs
143}
144
145// xlateErrorFormat translates the user-supplied format into the format
146// expected by i18n, mainly translating parameter names into numeric indexes.
147func xlateErrorFormat(format string, params []*Field) (string, error) {
148 const prefix = "{1:}{2:}"
149 if format == "" {
150 return prefix, nil
151 }
152 // Create a map from param name to index. The index numbering starts at 3,
153 // since the first two params are the component and op name, and i18n formats
154 // use 1-based indices.
155 pmap := make(map[string]string)
156 for ix, param := range params {
157 pmap[param.Name] = strconv.Itoa(ix + 3)
158 }
159 tagRE, err := regexp.Compile(`\{\:?([0-9a-zA-Z_]+)\:?\}`)
160 if err != nil {
161 return "", err
162 }
163 result, pos := prefix+" ", 0
164 for _, match := range tagRE.FindAllStringSubmatchIndex(format, -1) {
165 // The tag submatch indices are available as match[2], match[3]
166 if len(match) != 4 || match[2] < pos || match[2] > match[3] {
167 return "", fmt.Errorf("internal error: bad regexp indices %v", match)
168 }
169 beg, end := match[2], match[3]
170 tag := format[beg:end]
171 if tag == "_" {
172 continue // Skip underscore tags.
173 }
174 if _, err := strconv.Atoi(tag); err == nil {
175 continue // Skip number tags.
176 }
177 xtag, ok := pmap[tag]
178 if !ok {
179 return "", fmt.Errorf("unknown param %q", tag)
180 }
181 // Replace tag with xtag in the result.
182 result += format[pos:beg]
183 result += xtag
184 pos = end
185 }
186 if end := len(format); pos < end {
187 result += format[pos:end]
188 }
189 return result, nil
190}