blob: 32657e2eecec1d231af83d336e69df0f926079a4 [file] [log] [blame]
Nigel Tao1aa50162020-02-05 17:17:40 +11001// Copyright 2020 The Wuffs Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
Nigel Tao788479d2021-08-22 10:52:51 +100015//go:build ignore
Nigel Tao1aa50162020-02-05 17:17:40 +110016// +build ignore
17
Nigel Taof473ffa2020-12-18 22:10:16 +110018// Deprecated: unused as of commit 695a6815 "Remove gif.config_decoder".
19
Nigel Tao1aa50162020-02-05 17:17:40 +110020// TODO: consider renaming this from script/preprocess-wuffs.go to
21// cmd/wuffspreprocess, making it a "go install"able command line tool.
22
23package main
24
25// preprocess-wuffs.go generates target Wuffs files based on source Wuffs
26// files. It is conceptually similar to, but weaker than, the C language's
27// preprocessor's #ifdef mechanism. It is a stand-alone tool, not built into
28// the Wuffs compiler. It runs relatively infrequently (not at every compile)
29// and preprocessed output is checked into the repository.
30//
31// Preprocessing is separate from compilation, unlike C, for multiple reasons:
32//
33// - It simplifies the Wuffs language per se, which makes it easier to write
34// other tools for Wuffs programs, such as an independent implementation of
35// the type checker or a tool that converts from Wuffs programs to the input
36// format of a formal verifier.
37//
38// - Having an explicit file containing the preprocessed output helps keep the
39// programmer aware of the cost (increased source size is correlated with
40// increased binary size) of generating code. Other programming languages
41// make it very easy (in terms of lines of code written and checked in),
42// possibly too easy, to produce lots of object code, especiallly when
43// monomorphizing favors run-time performance over binary size.
44//
45// - Writing the generated code to disk can help debug that generated code.
46//
47// It is the programmer's responsibility to re-run the preprocessor to
48// re-generate the target files whenever the source file changes, similar to
49// the Go language's "go generate" (https://blog.golang.org/generate).
50// Naturally, this can be automated to some extent, e.g. through Makefiles or
51// git hooks (when combined with the -fail-on-change flag).
52//
53// --------
54//
55// Usage:
56//
57// go run preprocess-wuffs.go a.wuffs b*.wuffs dir3 dir4
58//
59// This scans all of the files or directories (recursively, albeit skipping
60// dot-files) passed for Wuffs-preprocessor directives (see below). If no files
61// or directories are passed, it scans ".", the current working directory.
62//
63// The optional -fail-on-change flag means to fail (with a non-zero exit code)
64// if the target files' contents would change.
65//
66// --------
67//
68// Directives:
69//
70// Preprocessing is line-based, and lines of interest all start with optional
71// whitespace and then "//#", slash slash hash, e.g. "//#USE etc".
72//
73// The first directive must be #USE, which mentions the name of this program
74// and then lists the files to generate.
75//
76// Other directives are grouped into blocks:
77// - One or more "#WHEN FOO filename1 filename2" lines, and then
78// - One "#DONE FOO" line.
79//
80// The "FOO" names are arbitrary but must be unique (in a file), preventing
81// nested blocks. A good text editor can also quickly cycle through the #WHEN
82// and #DONE directives for any given block by searching for that unique name.
83// By convention, the names look like "PREPROC123". The "123" suffix is for
84// uniqueness. The names' ordering, number-suffixed or not, does not matter.
85//
86// A #WHEN's filenames detail which target files are active: the subset of the
87// #USE directive's filenames that the subsequent lines (up until the next
88// #WHEN or #DONE) apply to. A #WHEN's filenames may be empty, in which case
89// the subsequent lines are part of the source file but none of the generated
90// target files.
91//
92// A #REPLACE directive adds a simple find/replace filter to the active
93// targets, applied to every subsequent generated line. A target may have
94// multiple filters, which are applied sequentially. Filters are conceptually
95// similar to a sed script, but the mechanism is trivial: for each input line,
96// the first exact sub-string match (if any) is replaced.
97//
98// Lines that aren't directives (that don't start with whitespace then "//#")
99// are simply copied (after per-target filtering) to either all active targets
100// (when within a block) or to all targets (otherwise).
101//
102// The ## directive (e.g. "//## apple banana") is, like all directives, a "//"
103// comment in the source file, but the "//##" is stripped and the remainder
104// ("apple banana") is treated as a non-directive line, copied and filtered per
105// the previous paragraph.
106//
107// For an example, look for "PREPROC" in the std/gif/decode_gif.wuffs file, and
108// try "diff std/gif/decode_{gif,config}.wuffs".
109
110import (
111 "bytes"
112 "flag"
113 "fmt"
114 "io/ioutil"
115 "os"
116 "path/filepath"
117 "runtime"
118 "sort"
119 "strings"
120
121 "github.com/google/wuffs/lang/render"
122
123 t "github.com/google/wuffs/lang/token"
124)
125
126var (
127 focFlag = flag.Bool("fail-on-change", false,
128 "fail (with a non-zero exit code) if the target files' contents would change")
129)
130
131func main() {
132 if err := main1(); err != nil {
133 os.Stderr.WriteString(err.Error() + "\n")
134 os.Exit(1)
135 }
136}
137
138func main1() error {
139 flag.Parse()
140
141 if flag.NArg() == 0 {
142 if err := filepath.Walk(".", walk); err != nil {
143 return err
144 }
145 } else {
146 for i := 0; i < flag.NArg(); i++ {
147 arg := flag.Arg(i)
148 switch dir, err := os.Stat(arg); {
149 case err != nil:
150 return err
151 case dir.IsDir():
152 if err := filepath.Walk(arg, walk); err != nil {
153 return err
154 }
155 default:
156 if err := do(arg); err != nil {
157 return err
158 }
159 }
160 }
161 }
162
163 sortedFilenames := []string(nil)
164 for filename := range globalTargets {
165 sortedFilenames = append(sortedFilenames, filename)
166 }
167 sort.Strings(sortedFilenames)
168 for _, filename := range sortedFilenames {
169 contents := globalTargets[filename]
170 if x, err := ioutil.ReadFile(filename); (err == nil) && bytes.Equal(x, contents) {
171 fmt.Printf("gen unchanged: %s\n", filename)
172
173 continue
174 }
175 if *focFlag {
176 return fmt.Errorf("fail-on-change: %s\n", filename)
177 }
178 if err := writeFile(filename, contents); err != nil {
179 return fmt.Errorf("writing %s: %v", filename, err)
180 }
181 fmt.Printf("gen wrote: %s\n", filename)
182 }
183
184 return nil
185}
186
187func isWuffsFile(info os.FileInfo) bool {
188 name := info.Name()
189 return !info.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".wuffs")
190}
191
192func walk(filename string, info os.FileInfo, err error) error {
193 if (err == nil) && isWuffsFile(info) {
194 err = do(filename)
195 }
196 // Don't complain if a file was deleted in the meantime (i.e. the directory
197 // changed concurrently while running this program).
198 if (err != nil) && !os.IsNotExist(err) {
199 return err
200 }
201 return nil
202}
203
204var (
205 directiveDone = []byte(`//#DONE `)
206 directiveHash = []byte(`//## `)
207 directiveReplace = []byte(`//#REPLACE `)
208 directiveUse = []byte(`//#USE "go run preprocess-wuffs.go" TO MAKE `)
209 directiveWhen = []byte(`//#WHEN `)
210
211 _with_ = []byte(" WITH ")
212 space = []byte(" ")
213
214 // globalTargets map from filenames to contents.
215 globalTargets = map[string][]byte{}
216)
217
218type target struct {
219 buffer *bytes.Buffer
220 filters []filter
221}
222
223func (t *target) write(s []byte) {
224 for _, f := range t.filters {
225 i := bytes.Index(s, f.find)
226 if i < 0 {
227 continue
228 }
229 x := []byte(nil)
230 x = append(x, s[:i]...)
231 x = append(x, f.replace...)
232 x = append(x, s[i+len(f.find):]...)
233 s = x
234 }
235 t.buffer.Write(s)
236}
237
238type filter struct {
239 find []byte
240 replace []byte
241}
242
243func do(filename string) error {
244 src, err := ioutil.ReadFile(filename)
245 if err != nil {
246 return err
247 }
248 if !bytes.Contains(src, directiveUse) {
249 return nil
250 }
251 localTargets := map[string]*target(nil)
252 activeTargets := []*target(nil)
253 usedBlockNames := map[string]bool{}
254 blockName := []byte(nil) // Typically something like "PREPROC123".
255 prefix := []byte(nil) // Source file contents up to the "//#USE" directive.
256
257 for remaining := src; len(remaining) > 0; {
258 line := remaining
259 if i := bytes.IndexByte(remaining, '\n'); i >= 0 {
260 line, remaining = remaining[:i+1], remaining[i+1:]
261 } else {
262 remaining = nil
263 }
264
265 ppLine := parsePreprocessorLine(line)
266 if ppLine == nil {
267 if localTargets == nil {
268 prefix = append(prefix, line...)
269 } else {
270 for _, t := range activeTargets {
271 t.write(line)
272 }
273 }
274 continue
275 }
276
277 if bytes.HasPrefix(ppLine, directiveUse) {
278 if localTargets != nil {
279 return fmt.Errorf("multiple #USE directives")
280 }
281 err := error(nil)
282 localTargets, err = parseUse(filename, ppLine[len(directiveUse):])
283 if err != nil {
284 return err
285 }
286
287 activeTargets = activeTargets[:0]
288 for _, t := range localTargets {
289 activeTargets = append(activeTargets, t)
290 t.write(prefix)
291 }
292 prefix = nil
293 continue
294 }
295
296 if localTargets == nil {
297 return fmt.Errorf("missing #USE directive")
298 }
299
300 switch {
301 case bytes.HasPrefix(ppLine, directiveDone):
302 arg := ppLine[len(directiveDone):]
303 if blockName == nil {
304 return fmt.Errorf("bad #DONE directive without #WHEN directive")
305 } else if !bytes.Equal(blockName, arg) {
306 return fmt.Errorf("bad directive name: %q", arg)
307 }
308 activeTargets = activeTargets[:0]
309 for _, t := range localTargets {
310 activeTargets = append(activeTargets, t)
311 }
312 blockName = nil
313
314 case bytes.HasPrefix(ppLine, directiveHash):
315 indent := []byte(nil)
316 if i := bytes.IndexByte(line, '/'); i >= 0 {
317 indent = line[:i]
318 }
319 if blockName == nil {
320 for _, t := range localTargets {
321 t.buffer.Write(indent)
322 t.write(ppLine[len(directiveHash):])
323 t.buffer.WriteByte('\n')
324 }
325 } else {
326 for _, t := range activeTargets {
327 t.buffer.Write(indent)
328 t.write(ppLine[len(directiveHash):])
329 t.buffer.WriteByte('\n')
330 }
331 }
332
333 case bytes.HasPrefix(ppLine, directiveReplace):
334 f := parseReplace(ppLine[len(directiveReplace):])
335 if (f.find == nil) || (f.replace == nil) {
336 return fmt.Errorf("bad #REPLACE directive: %q", ppLine)
337 }
338 for _, t := range activeTargets {
339 t.filters = append(t.filters, f)
340 }
341
342 case bytes.HasPrefix(ppLine, directiveWhen):
343 args := bytes.Split(ppLine[len(directiveWhen):], space)
344 if len(args) == 0 {
345 return fmt.Errorf("bad #WHEN directive: %q", ppLine)
346 }
347 if blockName == nil {
348 blockName = args[0]
349 if bn := string(blockName); usedBlockNames[bn] {
350 return fmt.Errorf("duplicate directive name: %q", bn)
351 } else {
352 usedBlockNames[bn] = true
353 }
354 } else if !bytes.Equal(blockName, args[0]) {
355 return fmt.Errorf("bad directive name: %q", args[0])
356 }
357
358 dir := filepath.Dir(filename)
359 activeTargets = activeTargets[:0]
360 for _, arg := range args[1:] {
361 t := localTargets[filepath.Join(dir, string(arg))]
362 if t == nil {
363 return fmt.Errorf("bad #WHEN filename: %q", arg)
364 }
365 activeTargets = append(activeTargets, t)
366 }
367
368 default:
369 return fmt.Errorf("bad directive: %q", ppLine)
370 }
371 }
372
373 if blockName != nil {
374 return fmt.Errorf("missing #DONE directive: %q", blockName)
375 }
376
377 for absFilename, t := range localTargets {
378 globalTargets[absFilename] = wuffsfmt(t.buffer.Bytes())
379 }
380 return nil
381}
382
383func wuffsfmt(src []byte) []byte {
384 tm := &t.Map{}
385 tokens, comments, err := t.Tokenize(tm, "placeholder.filename", src)
386 if err != nil {
387 return src
388 }
389 dst := &bytes.Buffer{}
390 if err := render.Render(dst, tm, tokens, comments); err != nil {
391 return src
392 }
393 return dst.Bytes()
394}
395
396const chmodSupported = runtime.GOOS != "windows"
397
398func writeFile(filename string, b []byte) error {
399 f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename))
400 if err != nil {
401 return err
402 }
403 if chmodSupported {
404 if info, err := os.Stat(filename); err == nil {
405 f.Chmod(info.Mode().Perm())
406 }
407 }
408 _, werr := f.Write(b)
409 cerr := f.Close()
410 if werr != nil {
411 os.Remove(f.Name())
412 return werr
413 }
414 if cerr != nil {
415 os.Remove(f.Name())
416 return cerr
417 }
418 return os.Rename(f.Name(), filename)
419}
420
421func parsePreprocessorLine(line []byte) []byte {
422 // Look for "//#", slash slash hash.
423 line = stripLeadingWhitespace(line)
424 if (len(line) >= 3) && (line[0] == '/') && (line[1] == '/') && (line[2] == '#') {
425 return bytes.TrimSpace(line)
426 }
427 return nil
428}
429
430func parseReplace(ppLine []byte) filter {
431 s0, ppLine := parseString(ppLine)
432 if s0 == nil {
433 return filter{}
434 }
435 if !bytes.HasPrefix(ppLine, _with_) {
436 return filter{}
437 }
438 ppLine = ppLine[len(_with_):]
439 s1, ppLine := parseString(ppLine)
440 if (s1 == nil) || (len(ppLine) != 0) {
441 return filter{}
442 }
443 return filter{
444 find: s0,
445 replace: s1,
446 }
447}
448
449func parseString(line []byte) (s []byte, remaining []byte) {
450 line = stripLeadingWhitespace(line)
451 if (len(line) == 0) || (line[0] != '"') {
452 return nil, line
453 }
454 line = line[1:]
455 i := bytes.IndexByte(line, '"')
456 if i < 0 {
457 return nil, line
458 }
459 if bytes.IndexByte(line[:i], '\\') >= 0 {
460 return nil, line
461 }
462 return line[:i], line[i+1:]
463}
464
465func parseUse(srcFilename string, ppLine []byte) (map[string]*target, error) {
466 absSrcFilename := filepath.Clean(srcFilename)
467 localTargets := map[string]*target{}
468 dir := filepath.Dir(srcFilename)
469 for _, relFilename := range bytes.Split(ppLine, space) {
470 if len(relFilename) == 0 {
471 continue
472 }
473 if !validFilename(relFilename) {
474 return nil, fmt.Errorf("invalid filename: %q", string(relFilename))
475 }
476 absFilename := filepath.Join(dir, string(relFilename))
477 if _, ok := globalTargets[absFilename]; ok {
478 return nil, fmt.Errorf("duplicate filename: %q", absFilename)
479 }
480 if absFilename == absSrcFilename {
481 return nil, fmt.Errorf("self-referential filename: %q", absFilename)
482 }
483
484 buf := &bytes.Buffer{}
485 buf.WriteString(
486 "// This file was automatically generated by \"preprocess-wuffs.go\".\n\n")
487 buf.WriteString("// --------\n\n")
488 localTargets[absFilename] = &target{buffer: buf}
489 }
490 return localTargets, nil
491}
492
493func stripLeadingWhitespace(s []byte) []byte {
494 for (len(s) > 0) && (s[0] <= ' ') {
495 s = s[1:]
496 }
497 return s
498}
499
500func validFilename(s []byte) bool {
501 if (len(s) == 0) || (s[0] == '.') {
502 return false
503 }
504 for _, c := range s {
505 if (c <= ' ') || (c == '/') || (c == '\\') || (c == ':') {
506 return false
507 }
508 }
509 return true
510}