| Stacks of Tokens |
| A study in interfaces |
| |
| Sydney Go Meetup |
| 15 September 2016 |
| |
| Rob Pike |
| Google |
| https://go.dev/ |
| |
| * Background |
| |
| Spoke at Gophercon this year: [[/talks/2016/asm.slide]] |
| |
| That talk was about system design and portability. |
| |
| Today's talk is about its lexer. |
| |
| Spoke about lexing before: [[/talks/2011/lex.slide]] |
| |
| That talk showed a way to use concurrency to build a lexer. |
| |
| Today's talk is about interfaces. |
| |
| * Lexer |
| |
| A lexer, also called a scanner, breaks the input stream into distinct "tokens": |
| |
| - identifiers |
| - numbers |
| - quoted strings |
| - operators |
| - miscellaneous characters such as comma, colon. |
| |
| Each token has a _type_ and a _value_. |
| |
| * Example |
| |
| MOVQ R0, 4(R1) |
| |
| The lexer turns this into the stream |
| |
| - identifier `MOVQ` |
| - identifier `R0` |
| - character `,` |
| - number `4` |
| - character `(` |
| - identifier `R1` |
| - character `)` |
| |
| Spaces are ignored. |
| |
| The parser then reads these tokens to parse the input into a _parse_tree_. |
| |
| * Go's text/scanner package |
| |
| There is a nice, efficient lexer package in the Go standard library: |
| |
| - [[/pkg/text/scanner/][`text/scanner`]] |
| |
| It can do this job just fine. |
| |
| But.... that is not enough for the assembler because of |
| |
| * Backwards compatibility |
| |
| The new Go assembler had to be totally compatible with the ones it replaces, which used YACC and were written in C. (See [[/talks/2015/gogo.slide][]].) |
| |
| Each assembler (one per architecture) contained these lines at the end of `lex.c`: |
| |
| #include "../cc/lexbody" |
| #include "../cc/macbody" |
| |
| This gave the assemblers the same lexer as the C compiler. |
| The differences between C tokens and Go tokens are minor and can be handled, but.... |
| |
| |
| The C lexer brings in something problematic. |
| |
| * The C preprocessor |
| |
| The old assemblers had a C preprocessor built in! |
| An old-fashioned one, without `#if` and token pasting, but still: |
| |
| #include "file" |
| #define MAXBUF 512 |
| #define MULDIV(a, b, c) ((a)*(b)/(c)) |
| #ifdef MAXBUF |
| #endif |
| |
| The `text/scanner` package can't handle this. |
| But we need to do it to be compatible. |
| |
| This talk is about how to use Go's interfaces to do it. |
| |
| * An observation |
| |
| What is standard input? An input source. |
| |
| - read the input |
| |
| What is an included file? An input source. |
| |
| - read the file |
| |
| What is a macro invocation? An input source. |
| |
| - read the macro definition |
| |
| Sounds a lot like `io.Reader`. |
| |
| * Token reader |
| |
| We don't want bytes, we want tokens. (Why?) |
| |
| Instead of |
| |
| type Reader interface { |
| Read(p []byte) (n int, err error) |
| } |
| |
| we want something like |
| |
| type TokenReader interface { |
| ReadToken() (Token, error) |
| } |
| |
| In practice the parser needs something different from `Read`, but the basic idea works. |
| |
| We build a lexer around an interface that reads tokens. |
| |
| * The observation in practice |
| |
| What is standard input? An input source. |
| |
| - get tokens from the input |
| |
| What is an included file? An input source. |
| |
| - get tokens from the file |
| |
| What is a macro invocation? An input source. |
| |
| - get tokens from the macro definition |
| |
| Each of these implements the `TokenReader` interface. |
| |
| * TokenReader |
| |
| type TokenReader interface { |
| // Next returns the next token. |
| Next() ScanToken |
| // The following methods all refer to the most recent token returned by Next. |
| // Text returns the original string representation of the token. |
| Text() string |
| // File reports the source file name of the token. |
| File() string |
| // Line reports the source line number of the token. |
| Line() int |
| // Col reports the source column number of the token. |
| Col() int |
| } |
| |
| Parser calls `Next`, then can ask about the token: what is, where it is. |
| `ScanToken` is just `text/scanner.Token` with tweaks. |
| |
| Note: No `Peek`. This has no lookahead. |
| |
| * Tokenizer |
| |
| `Tokenizer`, the foundational `TokenReader`, turns a `text/scanner.Scanner` into a `TokenReader`. |
| |
| // A Tokenizer is a simple wrapping of text/scanner.Scanner, configured |
| // for our purposes and made a TokenReader. It forms the lowest level, |
| // turning text from readers into tokens. |
| type Tokenizer struct { |
| tok ScanToken // Most recent token. |
| s *scanner.Scanner |
| line int |
| fileName string |
| } |
| |
| func NewTokenizer(name string, r io.Reader, file *os.File) *Tokenizer |
| |
| Either the reader or the file may be nil. |
| |
| `Tokenizer` implements `TokenReader` |
| |
| * Tokenizer.Next |
| |
| To give the flavor: |
| |
| func (t *Tokenizer) Next() ScanToken { |
| s := t.s |
| for { |
| t.tok = ScanToken(s.Scan()) |
| if t.tok != scanner.Comment { |
| break |
| } |
| length := strings.Count(s.TokenText(), "\n") |
| t.line += length |
| histLine += length |
| // For now, just discard all comments. |
| } |
| // Special processing for '\n' etc. elided. |
| return t.tok |
| } |
| |
| * Tokenizer.Text |
| |
| func (t *Tokenizer) Text() string { |
| switch t.tok { |
| case LSH: // Special handling of odd tokens used by ARM. |
| return "<<" |
| case RSH: |
| return ">>" |
| case ARR: |
| return "->" |
| case ROT: |
| return "@>" |
| } |
| return t.s.TokenText() |
| } |
| |
| `LSH` etc. are the reason for `ScanToken`: the set of tokens is a superset of the underlying type `text/scanner.Token`. |
| |
| * Macro definitions |
| |
| It's easy to see how files work: `NewTokenizer` can do that. |
| |
| What about a macro definition? |
| |
| #define A(arg) 27+(arg) |
| |
| Becomes the tokens |
| |
| 27 + ( arg ) |
| |
| When we encounter `A(x)`, we substitute the argument and get |
| |
| 27 + ( x ) |
| |
| Use a slice of tokens and store them in a `Macro` struct. |
| |
| type Macro struct { |
| name string // The #define name. |
| args []string // Formal arguments. |
| tokens []Token // Body of macro. |
| } |
| |
| * Slice |
| |
| // A Slice reads from a slice of Tokens. |
| type Slice struct { |
| tokens []Token |
| fileName string |
| line int |
| pos int |
| } |
| |
| Implements `TokenReader`. |
| |
| func (s *Slice) Next() ScanToken { |
| s.pos++ |
| if s.pos >= len(s.tokens) { |
| return scanner.EOF |
| } |
| return s.tokens[s.pos].ScanToken |
| } |
| |
| To invoke a macro, substitute the _actuals_ for the _formals_ and make a `Slice`. |
| |
| * Command-line flags |
| |
| A command-line flag `-D` can define a macro before execution: |
| |
| go tool asm -D 'macro=value' file.s |
| |
| That's easy! |
| |
| var DFlag MultiFlag |
| flag.Var(&DFlag, "D", "predefined symbol D=identifier...") |
| |
| type MultiFlag []string // Implements flag.Value, allows multiple settings. |
| |
| predefine(DFlag) |
| |
| * Predefined macros |
| |
| // predefine installs the macros set by the -D flag on the command line. |
| func predefine(defines MultiFlag) map[string]*Macro { |
| macros := make(map[string]*Macro) |
| for _, name := range defines { |
| value := "1" |
| i := strings.IndexRune(name, '=') |
| if i > 0 { |
| name, value = name[:i], name[i+1:] |
| } |
| // Various error checks elided. |
| macros[name] = &Macro{ |
| name: name, |
| args: nil, // No arguments allowed. |
| tokens: Tokenize(value), // Turn the value into tokens. |
| } |
| } |
| return macros |
| } |
| |
| The return value is the initial symbol table of macros. |
| |
| * The big picture |
| |
| We know how to: |
| |
| - tokenize an input stream from text or `io.Reader` |
| - define a macro |
| - invoke a macro |
| |
| But how does it fit together? |
| Which implementation `TokenReader` does the parser see? |
| |
| Consider |
| |
| - `#include` names a file to process next |
| - macro invocation names a slice of tokens to process next |
| |
| It's a stack! Push new input, pop at EOF. |
| |
| * Stack |
| |
| // A Stack is a stack of TokenReaders. As the top TokenReader hits EOF, |
| // it resumes reading the next one down. |
| type Stack struct { |
| tr []TokenReader |
| } |
| |
| // Push adds tr to the top (end) of the input stack. (Popping happens automatically.) |
| func (s *Stack) Push(tr TokenReader) { |
| s.tr = append(s.tr, tr) |
| } |
| |
| func (s *Stack) Next() ScanToken { |
| tos := s.tr[len(s.tr)-1] |
| tok := tos.Next() |
| for tok == scanner.EOF && len(s.tr) > 1 { |
| // Pop the topmost item from the stack and resume with the next one down. |
| s.tr = s.tr[:len(s.tr)-1] |
| tok = s.Next() |
| } |
| return tok |
| } |
| |
| * The Input type |
| |
| // Input is the main input: a stack of readers and some macro definitions. |
| // It also handles #include processing (by pushing onto the input stack) |
| // and parses and instantiates macro definitions. |
| type Input struct { |
| Stack |
| includes []string // Directories in which to look for #includes |
| macros map[string]*Macro |
| text string // Text of last token returned by Next. |
| ... |
| } |
| |
| Note the embedding of `Stack`: `Input` is a wrapping of the `Stack` implementation of `TokenReader`. |
| The parser uses a single instance of `Input` as its `TokenReader`. |
| |
| * Example: #include processing |
| |
| Some error handling elided for brevity. |
| |
| func (in *Input) include() { |
| // Find and parse file name, which is next token, a string. |
| tok := in.Stack.Next() |
| name, _ := strconv.Unquote(in.Stack.Text()) |
| in.expectNewline("#include") // Checks that a newline comes now. |
| // Push tokenizer for file onto stack. |
| fd, err := os.Open(name) |
| if err != nil { |
| for _, dir := range in.includes { |
| fd, err = os.Open(filepath.Join(dir, name)) |
| if err == nil { |
| break |
| } |
| } |
| } |
| in.Push(NewTokenizer(name, fd, fd)) |
| } |
| |
| * Macro definition |
| |
| Macro definition is similar but more complex because of the variety of forms. |
| Must deal with constants, empty values, macros with arguments, etc. |
| |
| The end result is to build a `Macro` value and install it in `Input.macros`. |
| |
| * The final piece: Input.Next |
| |
| Here is the implementation of a CPP input stream using these types. |
| (Error handling mostly elided for brevity.) |
| |
| func (in *Input) Next() ScanToken { |
| // If we cannot generate a token after 100 macro invocations, we're in trouble. |
| for nesting := 0; nesting < 100; { |
| tok := in.Stack.Next() |
| switch tok { |
| case '#': |
| in.hash() |
| case scanner.Ident: |
| // Is it a macro name? |
| name := in.Stack.Text() |
| macro := in.macros[name] |
| if macro != nil { |
| nesting++ |
| in.invokeMacro(macro) |
| continue |
| } |
| fallthrough |
| default: |
| // Continued on next slide. |
| |
| * Input.Next part 2 |
| |
| func (in *Input) Next() ScanToken { |
| // Continued from previous slide. |
| default: |
| if tok == scanner.EOF && len(in.ifdefStack) > 0 { |
| // We're skipping text but have run out of input with no #endif. |
| in.Error("unclosed #ifdef or #ifndef") |
| } |
| if in.enabled() { |
| in.text = in.Stack.Text() |
| return tok |
| } |
| } |
| } |
| in.Error("recursive macro invocation") |
| return 0 |
| } |
| |
| * Initializing and running the lexer |
| |
| // NewInput returns an Input from the given path. |
| func NewInput(name string) *Input { |
| return &Input{ |
| // include directories: look in source dir, then -I directories. |
| includes: append([]string{filepath.Dir(name)}, IFlag...), |
| macros: predefine(DFlag), |
| } |
| } |
| |
| To run, call `in.Push` to put the input file (or `os.Stdin`) on the stack. |
| |
| Then the lexer runs until the `Stack` is empty. |
| |
| * Summary |
| |
| Interfaces give programs structure. |
| |
| Interfaces encourage design by composition. |
| |
| - We have an interface that is implemented by a stack of itself! |
| |
| Interfaces enable and enforce clean divisions between components. |
| |
| - The simple idea of a `TokenReader` let us implement `#include` files, `#define` macros, command-line flags, `#ifdef` and more with one simple interface. |
| |
| And a final tip of the hat to `text/scanner` under it all. |