Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 1816] ファイルの概要

このコミットは、Go言語の標準ライブラリにおけるscannerパッケージの重要な変更を含んでいます。具体的には、字句解析器(スキャナー)が改行文字(\n)をコメントとして特別扱いする挙動を廃止し、通常の空白文字として扱うように修正されました。これにより、スキャナーの内部ロジックが簡素化され、より正確な行・列情報の追跡が可能になりました。また、テストコードの改善と、スキャナーの利用例を示す新しいヘルパー関数Tokenizeが追加されています。

コミット

- remove special handling of '\n' characters (used to be treated as comments
for pretty printer purposes - now properly ignored as white space since we
have line/col information)
- changed sample use in comment to an actually compiled function to make sure
sample is actually working
- added extra tests (checking line and column values, and the tokenize function)

R=rsc
DELTA=253  (61 added, 67 deleted, 125 changed)
OCL=26143
CL=26181

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/6f321e28f48c12e7dd9830198daefc2a8cfb410b

元コミット内容

commit 6f321e28f48c12e7dd9830198daefc2a8cfb410b
Author: Robert Griesemer <gri@golang.org>
Date:   Thu Mar 12 11:04:11 2009 -0700

    - remove special handling of '\n' characters (used to be treated as comments
    for pretty printer purposes - now properly ignored as white space since we
    have line/col information)
    - changed sample use in comment to an actually compiled function to make sure
    sample is actually working
    - added extra tests (checking line and column values, and the tokenize function)
    
    R=rsc
    DELTA=253  (61 added, 67 deleted, 125 changed)
    OCL=26143
    CL=26181
---
 src/lib/go/scanner.go      |  74 +++++-------\n src/lib/go/scanner_test.go | 294 +++++++++++++++++++++++----------------------
 2 files changed, 181 insertions(+), 187 deletions(-)

diff --git a/src/lib/go/scanner.go b/src/lib/go/scanner.go
index ccac8e1112..f665f10bab 100644
--- a/src/lib/go/scanner.go
+++ b/src/lib/go/scanner.go
@@ -4,23 +4,7 @@
 
 // A scanner for Go source text. Takes a []byte as source which can
 // then be tokenized through repeated calls to the Scan function.
-//
-// Sample use:
-//
-//	import "token"
-//	import "scanner"
-//
-//	func tokenize(src []byte) {
-//		var s scanner.Scanner;
-//		s.Init(src, nil /* no error handler */, false /* ignore comments */);
-//		for {
-//			pos, tok, lit := s.Scan();
-//			if tok == Scanner.EOF {
-//				return;
-//			}
-//			println(pos, token.TokenString(tok), string(lit));
-//		}
-//	}
+// For a sample use of a scanner, see the implementation of Tokenize.
 //
 package scanner
 
@@ -62,7 +46,7 @@ type Scanner struct {
 	scan_comments bool;  // if set, comments are reported as tokens
 
 	// scanning state
-	loc Location;  // location of ch
+	loc Location;  // location before ch (src[loc.Pos] == ch)
 	pos int;  // current reading position (position after ch)
 	ch int;  // one char look-ahead
 }
@@ -78,7 +62,7 @@ func (S *Scanner) next() {
 	\tswitch {
 	\tcase r == '\n':
 	\t\tS.loc.Line++;
-\t\t\tS.loc.Col = 1;
+\t\t\tS.loc.Col = 0;
 	\tcase r >= 0x80:
 	\t\t// not ASCII
 	\t\tr, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]);
@@ -94,9 +78,9 @@ func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) {
 // Init prepares the scanner S to tokenize the text src. Calls to Scan
 // will use the error handler err if they encounter a syntax error. The boolean
 // scan_comments specifies whether newline characters and comments should be
-// recognized and returned by Scan as token.COMMENT. If scan_comments is false,
-// they are treated as white space and ignored.
+// recognized and returned by Scan as token.COMMENT. If scan_comments is false,
+// they are treated as white space and ignored.
 //
 func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) {
 	S.src = src;
@@ -137,24 +121,6 @@ func (S *Scanner) expect(ch int) {
 }
 
 
-func (S *Scanner) skipWhitespace() {
-	for {
-	\tswitch S.ch {
-	\tcase '\t', '\r', ' ':
-	\t\t// nothing to do
-	\tcase '\n':
-	\t\tif S.scan_comments {
-	\t\t\treturn;
-	\t\t}
-	\tdefault:
-	\t\treturn;
-	\t}
-	\tS.next();
-	}
-	panic("UNREACHABLE");
-}
-
-
 func (S *Scanner) scanComment(loc Location) {
 	// first '/' already consumed
 
@@ -163,9 +129,7 @@ func (S *Scanner) scanComment(loc Location) {
 	\tfor S.ch >= 0 {
 	\t\tS.next();
 	\t\tif S.ch == '\n' {
-\t\t\t\t// '\n' terminates comment but we do not include
-\t\t\t\t// it in the comment (otherwise we don't see the
-\t\t\t\t// start of a newline in skipWhitespace()).
+\t\t\t\tS.next();  // '\n' belongs to the comment
 	\t\t\treturn;
 	\t\t}
 	\t}
@@ -412,14 +376,19 @@ func (S *Scanner) switch4(tok0, tok1, ch2, tok2, tok3 int) int {
 
 // Scan scans the next token and returns the token location loc,
 // the token tok, and the literal text lit corresponding to the
-// token.
+// token. The source end is indicated by token.EOF.
 //
 func (S *Scanner) Scan() (loc Location, tok int, lit []byte) {
 scan_again:
-\tS.skipWhitespace();
+\t// skip white space
+\tfor S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' {
+\t\tS.next();
+\t}
  
+\t// current token start
 	loc, tok = S.loc, token.ILLEGAL;
  
+\t// determine token value
 	\tswitch ch := S.ch; {\n \tcase isLetter(ch):\n \t\ttok = S.scanIdentifier();\n@@ -429,7 +398,6 @@ scan_again:\n \t\tS.next();  // always make progress\n \t\tswitch ch {\n \t\tcase -1  : tok = token.EOF;\n-\t\tcase '\n': tok = token.COMMENT;\n \t\tcase '"' : tok = token.STRING; S.scanString(loc);\n \t\tcase '\'': tok = token.CHAR; S.scanChar();\n \t\tcase '`' : tok = token.STRING; S.scanRawString(loc);\n@@ -487,3 +455,17 @@ scan_again:\n \n \treturn loc, tok, S.src[loc.Pos : S.loc.Pos];\n }\n+\n+\n+// Tokenize calls a function f with the token location, token value, and token\n+// text for each token in the source src. The other parameters have the same\n+// meaning as for the Init function. Tokenize keeps scanning until f returns\n+// false (usually when the token value is token.EOF).\n+//\n+func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {\n+\tvar s Scanner;\n+\ts.Init(src, err, scan_comments);\n+\tfor f(s.Scan()) {\n+\t\t// action happens in f\n+\t}\n+}\ndiff --git a/src/lib/go/scanner_test.go b/src/lib/go/scanner_test.go
index 94c2e51d53..221f01256e 100644
--- a/src/lib/go/scanner_test.go
+++ b/src/lib/go/scanner_test.go
@@ -31,7 +31,6 @@ func tokenclass(tok int) int {
 
 
 type elt struct {
-\tpos int;
 \ttok int;\n \tlit string;\n \tclass int;\n@@ -40,130 +39,120 @@ type elt struct {
 
 var tokens = [...]elt{\n \t// Special tokens\n-\telt{ 0, token.COMMENT, "/* a comment */", special },\n-\telt{ 0, token.COMMENT, "\n", special },\n+\telt{ token.COMMENT, "/* a comment */", special },\n+\telt{ token.COMMENT, "// a comment \n", special },\n \n \t// Identifiers and basic type literals\n-\telt{ 0, token.IDENT, "foobar", literal },\n-\telt{ 0, token.IDENT, "a۰۱۸", literal },\n-\telt{ 0, token.IDENT, "foo६४", literal },\n-\telt{ 0, token.IDENT, "bar9876", literal },\n-\telt{ 0, token.INT, "0", literal },\n-\telt{ 0, token.INT, "01234567", literal },\n-\telt{ 0, token.INT, "0xcafebabe", literal },\n-\telt{ 0, token.FLOAT, "0.", literal },\n-\telt{ 0, token.FLOAT, ".0", literal },\n-\telt{ 0, token.FLOAT, "3.14159265", literal },\n-\telt{ 0, token.FLOAT, "1e0", literal },\n-\telt{ 0, token.FLOAT, "1e+100", literal },\n-\telt{ 0, token.FLOAT, "1e-100", literal },\n-\telt{ 0, token.FLOAT, "2.71828e-1000", literal },\n-\telt{ 0, token.CHAR, "'a'", literal },\n-\telt{ 0, token.CHAR, "'\\000'", literal },\n-\telt{ 0, token.CHAR, "'\\xFF'", literal },\n-\telt{ 0, token.CHAR, "'\\uff16'", literal },\n-\telt{ 0, token.CHAR, "'\\U0000ff16'", literal },\n-\telt{ 0, token.STRING, "`foobar`", literal },\n+\telt{ token.IDENT, "foobar", literal },\n+\telt{ token.IDENT, "a۰۱۸", literal },\n+\telt{ token.IDENT, "foo६४", literal },\telt{ token.IDENT, "bar9876", literal },\n+\telt{ token.INT, "0", literal },\n+\telt{ token.INT, "01234567", literal },\n+\telt{ token.INT, "0xcafebabe", literal },\n+\telt{ token.FLOAT, "0.", literal },\n+\telt{ token.FLOAT, ".0", literal },\n+\telt{ token.FLOAT, "3.14159265", literal },\n+\telt{ token.FLOAT, "1e0", literal },\n+\telt{ token.FLOAT, "1e+100", literal },\n+\telt{ token.FLOAT, "1e-100", literal },\n+\telt{ token.FLOAT, "2.71828e-1000", literal },\n+\telt{ token.CHAR, "'a'", literal },\n+\telt{ token.CHAR, "'\\000'", literal },\n+\telt{ token.CHAR, "'\\xFF'", literal },\n+\telt{ token.CHAR, "'\\uff16'", literal },\n+\telt{ token.CHAR, "'\\U0000ff16'", literal },\n+\telt{ token.STRING, "`foobar`", literal },\n \n \t// Operators and delimitors\n-\telt{ 0, token.ADD, "+", operator },\n-\telt{ 0, token.SUB, "-", operator },\n-\telt{ 0, token.MUL, "*", operator },\n-\telt{ 0, token.QUO, "/", operator },\n-\telt{ 0, token.REM, "%", operator },\n-\n-\telt{ 0, token.AND, "&", operator },\n-\telt{ 0, token.OR, "|", operator },\n-\telt{ 0, token.XOR, "^", operator },\n-\telt{ 0, token.SHL, "<<", operator },\n-\telt{ 0, token.SHR, ">>", operator },\n-\n-\telt{ 0, token.ADD_ASSIGN, "+=", operator },\n-\telt{ 0, token.SUB_ASSIGN, "-=", operator },\n-\telt{ 0, token.MUL_ASSIGN, "*=", operator },\n-\telt{ 0, token.QUO_ASSIGN, "/=", operator },\n-\telt{ 0, token.REM_ASSIGN, "%=", operator },\n-\n-\telt{ 0, token.AND_ASSIGN, "&=", operator },\n-\telt{ 0, token.OR_ASSIGN, "|=", operator },\n-\telt{ 0, token.XOR_ASSIGN, "^=", operator },\n-\telt{ 0, token.SHL_ASSIGN, "<<=", operator },\n-\telt{ 0, token.SHR_ASSIGN, ">>=", operator },\n-\n-\telt{ 0, token.LAND, "&&", operator },\n-\telt{ 0, token.LOR, "||", operator },\n-\telt{ 0, token.ARROW, "<-", operator },\n-\telt{ 0, token.INC, "++", operator },\n-\telt{ 0, token.DEC, "--", operator },\n-\n-\telt{ 0, token.EQL, "==", operator },\n-\telt{ 0, token.LSS, "<", operator },\n-\telt{ 0, token.GTR, ">", operator },\n-\telt{ 0, token.ASSIGN, "=", operator },\n-\telt{ 0, token.NOT, "!", operator },\n-\n-\telt{ 0, token.NEQ, "!=", operator },\n-\telt{ 0, token.LEQ, "<=", operator },\n-\telt{ 0, token.GEQ, ">=", operator },\n-\telt{ 0, token.DEFINE, ":=", operator },\n-\telt{ 0, token.ELLIPSIS, "...", operator },\n-\n-\telt{ 0, token.LPAREN, "(", operator },\n-\telt{ 0, token.LBRACK, "[", operator },\n-\telt{ 0, token.LBRACE, "{", operator },\n-\telt{ 0, token.COMMA, ",", operator },\n-\telt{ 0, token.PERIOD, ".", operator },\n-\n-\telt{ 0, token.RPAREN, ")", operator },\n-\telt{ 0, token.RBRACK, "]", operator },\n-\telt{ 0, token.RBRACE, "}", operator },\n-\telt{ 0, token.SEMICOLON, ";", operator },\n-\telt{ 0, token.COLON, ":", operator },\n+\telt{ token.ADD, "+", operator },\n+\telt{ token.SUB, "-", operator },\n+\telt{ token.MUL, "*", operator },\n+\telt{ token.QUO, "/", operator },\n+\telt{ token.REM, "%", operator },\n+\n+\telt{ token.AND, "&", operator },\n+\telt{ token.OR, "|", operator },\n+\telt{ token.XOR, "^", operator },\n+\telt{ token.SHL, "<<", operator },\n+\telt{ token.SHR, ">>", operator },\n+\n+\telt{ token.ADD_ASSIGN, "+=", operator },\n+\telt{ token.SUB_ASSIGN, "-=", operator },\n+\telt{ token.MUL_ASSIGN, "*=", operator },\n+\telt{ token.QUO_ASSIGN, "/=", operator },\n+\telt{ token.REM_ASSIGN, "%=", operator },\n+\n+\telt{ token.AND_ASSIGN, "&=", operator },\n+\telt{ token.OR_ASSIGN, "|=", operator },\n+\telt{ token.XOR_ASSIGN, "^=", operator },\n+\telt{ token.SHL_ASSIGN, "<<=", operator },\n+\telt{ token.SHR_ASSIGN, ">>=", operator },\n+\n+\telt{ token.LAND, "&&", operator },\n+\telt{ token.LOR, "||", operator },\n+\telt{ token.ARROW, "<-", operator },\n+\telt{ token.INC, "++", operator },\n+\telt{ token.DEC, "--", operator },\n+\n+\telt{ token.EQL, "==", operator },\n+\telt{ token.LSS, "<", operator },\n+\telt{ token.GTR, ">", operator },\n+\telt{ token.ASSIGN, "=", operator },\n+\telt{ token.NOT, "!", operator },\n+\n+\telt{ token.NEQ, "!=", operator },\n+\telt{ token.LEQ, "<=", operator },\n+\telt{ token.GEQ, ">=", operator },\n+\telt{ token.DEFINE, ":=", operator },\n+\telt{ token.ELLIPSIS, "...", operator },\n+\n+\telt{ token.LPAREN, "(", operator },\n+\telt{ token.LBRACK, "[", operator },\n+\telt{ token.LBRACE, "{", operator },\n+\telt{ token.COMMA, ",", operator },\n+\telt{ token.PERIOD, ".", operator },\n+\n+\telt{ token.RPAREN, ")", operator },\n+\telt{ token.RBRACK, "]", operator },\n+\telt{ token.RBRACE, "}", operator },\n+\telt{ token.SEMICOLON, ";", operator },\n+\telt{ token.COLON, ":", operator },\n \n \t// Keywords\n-\telt{ 0, token.BREAK, "break", keyword },\n-\telt{ 0, token.CASE, "case", keyword },\n-\telt{ 0, token.CHAN, "chan", keyword },\n-\telt{ 0, token.CONST, "const", keyword },\n-\telt{ 0, token.CONTINUE, "continue", keyword },\n-\n-\telt{ 0, token.DEFAULT, "default", keyword },\n-\telt{ 0, token.DEFER, "defer", keyword },\n-\telt{ 0, token.ELSE, "else", keyword },\n-\telt{ 0, token.FALLTHROUGH, "fallthrough", keyword },\n-\telt{ 0, token.FOR, "for", keyword },\n-\n-\telt{ 0, token.FUNC, "func", keyword },\n-\telt{ 0, token.GO, "go", keyword },\n-\telt{ 0, token.GOTO, "goto", keyword },\n-\telt{ 0, token.IF, "if", keyword },\n-\telt{ 0, token.IMPORT, "import", keyword },\n-\n-\telt{ 0, token.INTERFACE, "interface", keyword },\n-\telt{ 0, token.MAP, "map", keyword },\n-\telt{ 0, token.PACKAGE, "package", keyword },\n-\telt{ 0, token.RANGE, "range", keyword },\n-\telt{ 0, token.RETURN, "return", keyword },\n-\n-\telt{ 0, token.SELECT, "select", keyword },\n-\telt{ 0, token.STRUCT, "struct", keyword },\n-\telt{ 0, token.SWITCH, "switch", keyword },\n-\telt{ 0, token.TYPE, "type", keyword },\n-\telt{ 0, token.VAR, "var", keyword },\n+\telt{ token.BREAK, "break", keyword },\n+\telt{ token.CASE, "case", keyword },\n+\telt{ token.CHAN, "chan", keyword },\n+\telt{ token.CONST, "const", keyword },\n+\telt{ token.CONTINUE, "continue", keyword },\n+\n+\telt{ token.DEFAULT, "default", keyword },\n+\telt{ token.DEFER, "defer", keyword },\n+\telt{ token.ELSE, "else", keyword },\n+\telt{ token.FALLTHROUGH, "fallthrough", keyword },\n+\telt{ token.FOR, "for", keyword },\n+\n+\telt{ token.FUNC, "func", keyword },\n+\telt{ token.GO, "go", keyword },\n+\telt{ token.GOTO, "goto", keyword },\n+\telt{ token.IF, "if", keyword },\n+\telt{ token.IMPORT, "import", keyword },\n+\n+\telt{ token.INTERFACE, "interface", keyword },\n+\telt{ token.MAP, "map", keyword },\n+\telt{ token.PACKAGE, "package", keyword },\n+\telt{ token.RANGE, "range", keyword },\n+\telt{ token.RETURN, "return", keyword },\n+\n+\telt{ token.SELECT, "select", keyword },\n+\telt{ token.STRUCT, "struct", keyword },\n+\telt{ token.SWITCH, "switch", keyword },\n+\telt{ token.TYPE, "type", keyword },\n+\telt{ token.VAR, "var", keyword },\n }\n \n \n-const whitespace = "  \t  ";  // to separate tokens\n-\n-func init() {\n-\t// set pos fields\n-\tpos := 0;\n-\tfor i := 0; i < len(tokens); i++ {\n-\t\ttokens[i].pos = pos;\n-\t\tpos += len(tokens[i].lit) + len(whitespace);\n-\t}\n-}\n-\n+const whitespace = "  \t  \n\n\n";  // to separate tokens\n \n type TestErrorHandler struct {\n \tt *testing.T\n@@ -174,38 +163,61 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {\n }\n \n \n+func NewlineCount(s string) int {\n+\tn := 0;\n+\tfor i := 0; i < len(s); i++ {\n+\t\tif s[i] == '\n' {\n+\t\t\tn++;\n+\t\t}\n+\t}\n+\treturn n;\n+}\n+\n+\n func Test(t *testing.T) {\n \t// make source\n \tvar src string;\n \tfor i, e := range tokens {\n \t\tsrc += e.lit + whitespace;\n \t}\n-\n-\t// set up scanner\n-\tvar s scanner.Scanner;\n-\ts.Init(io.StringBytes(src), &TestErrorHandler{t}, true);\n+\twhitespace_linecount := NewlineCount(whitespace);\n \n \t// verify scan\n-\tfor i, e := range tokens {\n-\t\tloc, tok, lit := s.Scan();\n-\t\tif loc.Pos != e.pos {\n-\t\t\tt.Errorf("bad position for %s: got %d, expected %d", e.lit, loc.Pos, e.pos);\n-\t\t}\n-\t\tif tok != e.tok {\n-\t\t\tt.Errorf("bad token for %s: got %s, expected %s", e.lit, token.TokenString(tok), token.TokenString(e.tok));\n-\t\t}\n-\t\tif token.IsLiteral(e.tok) && string(lit) != e.lit {\n-\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", e.lit, string(lit), e.lit);\n+\tindex := 0;\n+\teloc := scanner.Location{0, 1, 1};\n+\tscanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,\n+\t\tfunc (loc Location, tok int, litb []byte) bool {\n+\t\t\te := elt{token.EOF, "", special};\n+\t\t\tif index < len(tokens) {\n+\t\t\t\te = tokens[index];\n+\t\t\t}\n+\t\t\tlit := string(litb);\n+\t\t\tif tok == token.EOF {\n+\t\t\t\tlit = "<EOF>";\n+\t\t\t\teloc.Col = 0;\n+\t\t\t}\n+\t\t\tif loc.Pos != eloc.Pos {\n+\t\t\t\tt.Errorf("bad position for %s: got %d, expected %d", lit, loc.Pos, eloc.Pos);\n+\t\t\t}\n+\t\t\tif loc.Line != eloc.Line {\n+\t\t\t\tt.Errorf("bad line for %s: got %d, expected %d", lit, loc.Line, eloc.Line);\n+\t\t\t}\n+\t\t\tif loc.Col != eloc.Col {\n+\t\t\t\tt.Errorf("bad column for %s: got %d, expected %d", lit, loc.Col, eloc.Col);\n+\t\t\t}\n+\t\t\tif tok != e.tok {\n+\t\t\t\tt.Errorf("bad token for %s: got %s, expected %s", lit, token.TokenString(tok), token.TokenString(e.tok));\n+\t\t\t}\n+\t\t\tif token.IsLiteral(e.tok) && lit != e.lit {\n+\t\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", lit, lit, e.lit);\n+\t\t\t}\n+\t\t\tif tokenclass(tok) != e.class {\n+\t\t\t\tt.Errorf("bad class for %s: got %d, expected %d", lit, tokenclass(tok), e.class);\n+\t\t\t}\n+\t\t\teloc.Pos += len(lit) + len(whitespace);\n+\t\t\teloc.Line += NewlineCount(lit) + whitespace_linecount;\n+\t\t\tindex++;\n+\t\t\treturn tok != token.EOF;\n \t\t}\n-\t\tif tokenclass(tok) != e.class {\n-\t\t\tt.Errorf("bad class for %s: got %d, expected %d", e.lit, tokenclass(tok), e.class);\n-\t\t}\n-\t}\n-\tloc, tok, lit := s.Scan();\n-\tif tok != token.EOF {\n-\t\tt.Errorf("bad token at eof: got %s, expected EOF", token.TokenString(tok));\n-\t}\n-\tif tokenclass(tok) != special {\n-\t\tt.Errorf("bad class at eof: got %d, expected %d", tokenclass(tok), special);\n-\t}\n+\t);\n }\n```

## 変更の背景

このコミットが行われた背景には、Go言語の字句解析器(スキャナー)の設計と機能の進化があります。初期のGo言語のツールチェインでは、スキャナーが改行文字(`\n`)を特別なトークン(`token.COMMENT`)として扱っていました。これは、主にプリティプリンター(コード整形ツール)がソースコードの構造を再構築する際に、改行の位置を保持するための一時的な措置であったと考えられます。

しかし、スキャナーが正確な行と列の情報(`Location`構造体)を追跡する能力が向上するにつれて、改行文字をコメントとして特別扱いする必要性がなくなりました。改行は本質的に空白文字の一種であり、構文解析の段階でその位置情報が適切に利用されるべきです。この特別扱いを廃止することで、スキャナーのロジックが簡素化され、より一般的な空白文字の処理に統一することが可能になります。

また、`scanner`パッケージのドキュメントに記載されていたサンプルコードが、コメント形式であったため実際にコンパイルして動作確認することができませんでした。このコミットでは、そのサンプルを実際にコンパイル可能な関数として実装し、`Tokenize`という新しいヘルパー関数として提供することで、利用者がスキャナーの基本的な使い方をより簡単に理解し、テストできるように改善されています。

さらに、スキャナーの正確性を保証するために、行と列の値のチェックを含む新しいテストケースが追加されました。これは、改行文字の扱いが変更されたことによる潜在的なバグを防ぎ、スキャナーが常に正確な位置情報を提供することを保証するために不可欠な変更です。

## 前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

1.  **字句解析器(Lexer/Scanner/Tokenizer)**:
    コンパイラのフロントエンドの一部であり、ソースコードを読み込み、意味のある最小単位である「トークン(token)」のストリームに変換するプログラムです。例えば、`var x = 10;`というコードは、`var`(キーワード)、`x`(識別子)、`=`(代入演算子)、`10`(整数リテラル)、`;`(セミコロン)といったトークンに分割されます。

2.  **トークン(Token)**:
    字句解析器によって識別される、プログラミング言語における意味のある最小単位です。キーワード、識別子、演算子、リテラル、区切り文字などが含まれます。Go言語では、`go/token`パッケージで定義されています。

3.  **空白文字(Whitespace)**:
    ソースコードの可読性を高めるために使用される、プログラムの実行には影響しない文字の総称です。スペース、タブ、改行、キャリッジリターンなどが含まれます。字句解析器は通常、これらの空白文字をスキップします。

4.  **コメント(Comment)**:
    プログラマーがコードの説明やメモを記述するために使用するテキストで、コンパイラやインタプリタによって無視されます。Go言語では、`//`による行コメントと`/* ... */`によるブロックコメントがあります。

5.  **位置情報(Location/Position)**:
    ソースコード内の特定の文字やトークンがどこにあるかを示す情報です。通常、ファイル名、行番号、列番号、そしてファイル先頭からのオフセット(バイト数または文字数)で構成されます。Go言語の`go/scanner`パッケージでは`Location`構造体がこれに該当します。

6.  **Go言語の`go/scanner`パッケージ**:
    Go言語のソースコードを字句解析するためのパッケージです。`Scanner`型が字句解析器の主要な実装を提供し、`Scan`メソッドを呼び出すことで次のトークンを取得できます。

7.  **Go言語の`go/token`パッケージ**:
    Go言語のトークン定数を定義するパッケージです。`token.IDENT`(識別子)、`token.INT`(整数)、`token.ADD`(加算演算子)などが含まれます。

## 技術的詳細

このコミットの技術的詳細は、主に`scanner`パッケージの内部動作と、Go言語の字句解析における改行文字の扱いの変更に集約されます。

1.  **改行文字の扱いの一貫性**:
    以前の`scanner`では、`Scan`メソッド内で`case '\n': tok = token.COMMENT;`という特殊な処理があり、改行文字が`token.COMMENT`として扱われることがありました。これは、`scan_comments`フラグが`true`の場合に、改行もコメントとして報告されるという挙動を意味します。しかし、改行はコメントではなく、単なる空白文字として扱われるべきです。このコミットでは、この特殊なケースが削除され、改行文字は他の空白文字(スペース、タブ、キャリッジリターン)と同様に、`Scan`メソッドの冒頭で一括してスキップされるようになりました。これにより、字句解析器の動作がより直感的で一貫性のあるものになりました。

2.  **列番号の0ベース化**:
    `Scanner`構造体の`loc Location`フィールドは、現在の文字`ch`の**前**の位置を示すように定義が変更されました(`loc Location; // location before ch (src[loc.Pos] == ch)`)。これに伴い、`next()`メソッド内で改行文字を処理する際の列番号の初期化が`S.loc.Col = 1;`から`S.loc.Col = 0;`に変更されました。これは、Go言語の内部的な位置情報管理において、列番号が0ベースで扱われるようになったことを示唆しています。多くのプログラミング言語やエディタでは列番号が1ベースで表示されますが、内部処理では0ベースの方が計算が容易な場合があります。この変更により、スキャナーが報告する位置情報がより正確になり、内部的な整合性が向上しました。

3.  **`skipWhitespace`関数の廃止とインライン化**:
    以前は`skipWhitespace()`という独立した関数が存在し、`Scan`メソッドの冒頭で呼び出されていました。この関数は、空白文字をスキップする役割を担っていましたが、改行文字の特殊な扱い(`scan_comments`フラグによる条件分岐)が含まれていました。改行文字の特殊扱いが廃止されたことで、`skipWhitespace()`関数は不要となり、そのロジックは`Scan`メソッドの冒頭に直接インライン化されました。これにより、コードの呼び出しオーバーヘッドが削減され、`Scan`メソッドの処理フローがより明確になりました。

4.  **`Tokenize`ヘルパー関数の導入**:
    `scanner`パッケージに`Tokenize`という新しいトップレベル関数が追加されました。この関数は、ソースコード全体を字句解析し、見つかった各トークンに対してユーザーが指定したコールバック関数`f`を呼び出します。これは、スキャナーの基本的な使用パターンをカプセル化し、テストコードや簡単なツールでスキャナーを利用する際の利便性を向上させます。`Tokenize`関数は、内部で`Scanner`インスタンスを初期化し、`Scan`メソッドを繰り返し呼び出すことで、字句解析のループを抽象化しています。

5.  **テストの強化**:
    `scanner_test.go`ファイルでは、`elt`構造体から`pos`フィールドが削除され、`init()`関数による`pos`の計算も廃止されました。これは、スキャナーが提供する`Location`構造体(`Pos`, `Line`, `Col`)が十分に正確になったため、テストデータ内で別途オフセットを管理する必要がなくなったことを示しています。
    さらに、`Test`関数は`scanner.Tokenize`関数を利用するように書き換えられ、トークンの位置情報(行と列)の正確性を検証する新しいアサーションが追加されました。特に、`eloc.Line`と`eloc.Col`を動的に更新し、改行文字が正しく行と列のカウントに影響を与えることを確認しています。`NewlineCount`ヘルパー関数も追加され、テストの正確性を高めています。

これらの変更は、Go言語の字句解析器がより堅牢で、正確な位置情報を提供し、内部的にクリーンな設計へと進化していることを示しています。

## コアとなるコードの変更箇所

### `src/lib/go/scanner.go`

1.  **`Scanner`構造体の`loc`フィールドのコメント変更**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -62,7 +46,7 @@ type Scanner struct {
     	scan_comments bool;  // if set, comments are reported as tokens
     
     	// scanning state
    -	loc Location;  // location of ch
    +	loc Location;  // location before ch (src[loc.Pos] == ch)
     	pos int;  // current reading position (position after ch)
     	ch int;  // one char look-ahead
     }
    ```
    `loc`フィールドが`ch`の**前**の位置を示すという定義が明確化されました。

2.  **`next()`メソッドにおける列番号の初期化変更**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -78,7 +62,7 @@ func (S *Scanner) next() {
     	\tswitch {
     	\tcase r == '\n':
     	\t\tS.loc.Line++;
    -\t\t\tS.loc.Col = 1;
    +\t\t\tS.loc.Col = 0;
     	\tcase r >= 0x80:
     	\t\t// not ASCII
     	\t\tr, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]);
    ```
    改行時に列番号を0にリセットするようになりました。

3.  **`skipWhitespace()`関数の削除**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -137,24 +121,6 @@ func (S *Scanner) expect(ch int) {
     }
     
     
    -func (S *Scanner) skipWhitespace() {
    -	for {
    -	\tswitch S.ch {
    -	\tcase '\t', '\r', ' ':
    -	\t\t// nothing to do
    -	\tcase '\n':
    -	\t\tif S.scan_comments {
    -	\t\t\treturn;
    -	\t\t}
    -	\tdefault:
    -	\t\treturn;
    -	\t}
    -	\tS.next();
    -	}
    -	panic("UNREACHABLE");
    -}
    -
    -
     func (S *Scanner) scanComment(loc Location) {
     	// first '/' already consumed
     
    ```
    空白文字をスキップする独立した関数が削除されました。

4.  **`scanComment()`メソッドの改行処理変更**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -163,9 +129,7 @@ func (S *Scanner) scanComment(loc Location) {
     	\tfor S.ch >= 0 {
     	\t\tS.next();
     	\t\tif S.ch == '\n' {
    -\t\t\t\t// '\n' terminates comment but we do not include
    -\t\t\t\t// it in the comment (otherwise we don't see the
    -\t\t\t\t// start of a newline in skipWhitespace()).
    +\t\t\t\tS.next();  // '\n' belongs to the comment
     	\t\t\treturn;
     	\t\t}
     	\t}
    ```
    行コメントの終端で改行文字を消費するようになりました。

5.  **`Scan()`メソッドの空白文字スキップと改行の特殊処理削除**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -412,14 +376,19 @@ func (S *Scanner) switch4(tok0, tok1, ch2, tok2, tok3 int) int {
     
     // Scan scans the next token and returns the token location loc,
     // the token tok, and the literal text lit corresponding to the
    -// token.
    +// token. The source end is indicated by token.EOF.
     //
     func (S *Scanner) Scan() (loc Location, tok int, lit []byte) {
     scan_again:
    -\tS.skipWhitespace();
    +\t// skip white space
    +\tfor S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' {
    +\t\tS.next();
    +\t}
      
    +\t// current token start
     	loc, tok = S.loc, token.ILLEGAL;
      
    +\t// determine token value
     	\tswitch ch := S.ch; {\n \tcase isLetter(ch):\n \t\ttok = S.scanIdentifier();\n@@ -429,7 +398,6 @@ scan_again:\n \t\tS.next();  // always make progress\n \t\tswitch ch {\n \t\tcase -1  : tok = token.EOF;\n-\t\tcase '\n': tok = token.COMMENT;\n \t\tcase '"' : tok = token.STRING; S.scanString(loc);\n \t\tcase '\'': tok = token.CHAR; S.scanChar();\n \t\tcase '`' : tok = token.STRING; S.scanRawString(loc);\
    ```
    `skipWhitespace()`の呼び出しがインラインループに置き換えられ、`case '\n': tok = token.COMMENT;`が削除されました。

6.  **`Tokenize`関数の追加**:
    ```diff
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -487,3 +455,17 @@ scan_again:\
      
     	return loc, tok, S.src[loc.Pos : S.loc.Pos];
     }\n+\n+\n+// Tokenize calls a function f with the token location, token value, and token\n+// text for each token in the source src. The other parameters have the same\n+// meaning as for the Init function. Tokenize keeps scanning until f returns\n+// false (usually when the token value is token.EOF).\n+//\n+func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {\n+\tvar s Scanner;\n+\ts.Init(src, err, scan_comments);\n+\tfor f(s.Scan()) {\n+\t\t// action happens in f\n+\t}\n+}\
    ```
    新しいヘルパー関数`Tokenize`が追加されました。

### `src/lib/go/scanner_test.go`

1.  **`elt`構造体から`pos`フィールドの削除**:
    ```diff
    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -31,7 +31,6 @@ func tokenclass(tok int) int {
     
      
     type elt struct {
    -\tpos int;
      \ttok int;\n \tlit string;\n \tclass int;\n    ```
    テスト要素から位置オフセット情報が削除されました。

2.  **`tokens`配列の初期化から`pos`値の削除**:
    ```diff
    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -40,130 +39,120 @@ type elt struct {
      
     var tokens = [...]elt{\n \t// Special tokens\n-\telt{ 0, token.COMMENT, "/* a comment */", special },\n-\telt{ 0, token.COMMENT, "\n", special },\n+\telt{ token.COMMENT, "/* a comment */", special },\n+\telt{ token.COMMENT, "// a comment \n", special },
    ```
    `elt`の初期化から`0`(`pos`の値)が削除されました。

3.  **`init()`関数の削除**:
    ```diff
    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -154,14 +143,6 @@ func tokenclass(tok int) int {
      
      
    -const whitespace = "  \t  ";  // to separate tokens
    -
    -func init() {\n-\t// set pos fields\n-\tpos := 0;\n-\tfor i := 0; i < len(tokens); i++ {\n-\t\ttokens[i].pos = pos;\n-\t\tpos += len(tokens[i].lit) + len(whitespace);\n-\t}\n-}\n-\n+const whitespace = "  \t  \n\n\n";  // to separate tokens
    ```
    `pos`フィールドを初期化していた`init`関数が削除されました。`whitespace`定数に改行が追加されました。

4.  **`NewlineCount`関数の追加**:
    ```diff
    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -163,6 +154,17 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {
     }
      
      
    +func NewlineCount(s string) int {\n+\tn := 0;\n+\tfor i := 0; i < len(s); i++ {\n+\t\tif s[i] == '\n' {\n+\t\t\tn++;\n+\t\t}\n+\t}\n+\treturn n;\n+}\n+\n+\n     func Test(t *testing.T) {
    ```
    文字列中の改行数をカウントするヘルパー関数が追加されました。

5.  **`Test`関数の大幅な変更(`scanner.Tokenize`の使用と位置情報の検証)**:
    ```diff
    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -174,38 +163,61 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {
      func Test(t *testing.T) {\n \t// make source\n \tvar src string;\n \tfor i, e := range tokens {\n \t\tsrc += e.lit + whitespace;\n \t}\n-\n-\t// set up scanner\n-\tvar s scanner.Scanner;\n-\ts.Init(io.StringBytes(src), &TestErrorHandler{t}, true);\n+\twhitespace_linecount := NewlineCount(whitespace);\
      
     \t// verify scan\n-\tfor i, e := range tokens {\n-\t\tloc, tok, lit := s.Scan();\n-\t\tif loc.Pos != e.pos {\n-\t\t\tt.Errorf("bad position for %s: got %d, expected %d", e.lit, loc.Pos, e.pos);\n-\t\t}\n-\t\tif tok != e.tok {\n-\t\t\tt.Errorf("bad token for %s: got %s, expected %s", e.lit, token.TokenString(tok), token.TokenString(e.tok));\n-\t\t}\n-\t\tif token.IsLiteral(e.tok) && string(lit) != e.lit {\n-\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", e.lit, string(lit), e.lit);\n+\tindex := 0;\n+\teloc := scanner.Location{0, 1, 1};\n+\tscanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,\n+\t\tfunc (loc Location, tok int, litb []byte) bool {\n+\t\t\te := elt{token.EOF, "", special};\n+\t\t\tif index < len(tokens) {\n+\t\t\t\te = tokens[index];\n+\t\t\t}\n+\t\t\tlit := string(litb);\n+\t\t\tif tok == token.EOF {\n+\t\t\t\tlit = "<EOF>";\n+\t\t\t\teloc.Col = 0;\n+\t\t\t}\n+\t\t\tif loc.Pos != eloc.Pos {\n+\t\t\t\tt.Errorf("bad position for %s: got %d, expected %d", lit, loc.Pos, eloc.Pos);\n+\t\t\t}\n+\t\t\tif loc.Line != eloc.Line {\n+\t\t\t\tt.Errorf("bad line for %s: got %d, expected %d", lit, loc.Line, eloc.Line);\n+\t\t\t}\n+\t\t\tif loc.Col != eloc.Col {\n+\t\t\t\tt.Errorf("bad column for %s: got %d, expected %d", lit, loc.Col, eloc.Col);\n+\t\t\t}\n+\t\t\tif tok != e.tok {\n+\t\t\t\tt.Errorf("bad token for %s: got %s, expected %s", lit, token.TokenString(tok), token.TokenString(e.tok));\n+\t\t\t}\n+\t\t\tif token.IsLiteral(e.tok) && lit != e.lit {\n+\t\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", lit, lit, e.lit);\n+\t\t\t}\n+\t\t\tif tokenclass(tok) != e.class {\n+\t\t\t\tt.Errorf("bad class for %s: got %d, expected %d", lit, tokenclass(tok), e.class);\n+\t\t\t}\n+\t\t\teloc.Pos += len(lit) + len(whitespace);\n+\t\t\teloc.Line += NewlineCount(lit) + whitespace_linecount;\n+\t\t\tindex++;\n+\t\t\treturn tok != token.EOF;\n \t\t}\n-\t\tif tokenclass(tok) != e.class {\n-\t\t\tt.Errorf("bad class for %s: got %d, expected %d", e.lit, tokenclass(tok), e.class);\n-\t\t}\n-\t}\n-\tloc, tok, lit := s.Scan();\n-\tif tok != token.EOF {\n-\t\tt.Errorf("bad token at eof: got %s, expected EOF", token.TokenString(tok));\n-\t}\n-\tif tokenclass(tok) != special {\n-\t\tt.Errorf("bad class at eof: got %d, expected %d", tokenclass(tok), special);\n-\t}\n+\t);\n     }
    ```
    `scanner.Tokenize`を使用してテストが実行され、行と列の正確性が検証されるようになりました。

## コアとなるコードの解説

### `src/lib/go/scanner.go`

-   **`Scanner`構造体の`loc`フィールドのコメント変更**:
    `loc Location; // location before ch (src[loc.Pos] == ch)`
    この変更は、`Scanner`の内部状態における`loc`フィールドの役割を明確にしています。以前は単に`location of ch`とされていましたが、新しいコメントは`loc`が現在の文字`ch`の**直前**の位置を指すことを示しています。これは、字句解析器が文字を読み進める際に、常に次のトークンの開始位置を正確に把握するために重要な概念です。

-   **`next()`メソッドにおける列番号の初期化変更**:
    `S.loc.Col = 0;`
    `next()`メソッドは、スキャナーが次の文字を読み込む際に呼び出されます。改行文字(`\n`)を検出した場合、行番号をインクリメントし、列番号をリセットします。以前は`1`にリセットしていましたが、この変更により`0`にリセットされるようになりました。これは、Go言語の内部的な位置情報が0ベースの列番号を使用していることを示しており、より多くのプログラミング言語の内部表現と一致します。

-   **`skipWhitespace()`関数の削除と`Scan()`メソッドへのインライン化**:
    以前は`skipWhitespace()`という独立した関数があり、`Scan()`メソッドの冒頭で呼び出されていました。この関数は、スペース、タブ、キャリッジリターン、そして条件付きで改行文字をスキップしていました。
    変更後、`Scan()`メソッドの冒頭に以下のループが直接記述されました。
    ```go
    for S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' {
        S.next();
    }
    ```
    この変更により、`skipWhitespace()`関数が不要になり、コードの簡潔性が向上しました。また、改行文字が他の空白文字と同様に無条件にスキップされるようになり、`scan_comments`フラグによる特殊な挙動がなくなりました。

-   **`Scan()`メソッドからの`case '\n': tok = token.COMMENT;`の削除**:
    以前の`Scan()`メソッドの`switch`文には、改行文字を`token.COMMENT`として扱う特殊なケースがありました。
    ```go
    case '\n': tok = token.COMMENT;
    ```
    この行が削除されたことで、改行文字はもはやコメントとして扱われず、前述の空白文字スキップのロジックによって処理されるようになりました。これにより、スキャナーの動作がより標準的な字句解析の挙動に近づきました。

-   **`Tokenize`関数の追加**:
    ```go
    func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {
        var s Scanner;
        s.Init(src, err, scan_comments);
        for f(s.Scan()) {
            // action happens in f
        }
    }
    ```
    この新しい関数は、`scanner`パッケージの利用を簡素化するためのヘルパーです。ソースコード`src`を受け取り、内部で`Scanner`を初期化します。そして、`s.Scan()`を繰り返し呼び出し、その結果(トークンの位置、種類、リテラル)を引数として`f`というコールバック関数に渡します。`f`が`false`を返すまでこの処理を続けます。これにより、ユーザーは字句解析のループを自分で書くことなく、トークン処理のロジックに集中できるようになります。

### `src/lib/go/scanner_test.go`

-   **`elt`構造体からの`pos`フィールドの削除**:
    `type elt struct { tok int; lit string; class int; }`
    以前のテストでは、各トークンの期待されるファイル内オフセット(`pos`)を`elt`構造体で保持していました。しかし、スキャナーが正確な`Location`情報(`Pos`, `Line`, `Col`)を提供するようになったため、この`pos`フィールドは冗長となり削除されました。これにより、テストデータの定義が簡素化されました。

-   **`init()`関数の削除**:
    `init()`関数は、以前は`tokens`配列内の各`elt`の`pos`フィールドを計算して設定するために使用されていました。`pos`フィールドが削除されたため、この初期化関数も不要となり削除されました。

-   **`NewlineCount`関数の追加**:
    ```go
    func NewlineCount(s string) int {
        n := 0;
        for i := 0; i < len(s); i++ {
            if s[i] == '\n' {
                n++;
            }
        }
        return n;
    }
    ```
    このヘルパー関数は、与えられた文字列に含まれる改行文字の数を正確にカウントします。これは、テストにおいて期待される行番号を計算する際に使用され、スキャナーの行カウントの正確性を検証するために不可欠です。

-   **`Test`関数の変更**:
    `Test`関数は、`scanner.Tokenize`関数を利用するように全面的に書き換えられました。
    ```go
    scanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,
        func (loc Location, tok int, litb []byte) bool {
            // ... テストロジック ...
            if loc.Pos != eloc.Pos { ... }
            if loc.Line != eloc.Line { ... }
            if loc.Col != eloc.Col { ... }
            // ...
            eloc.Pos += len(lit) + len(whitespace);
            eloc.Line += NewlineCount(lit) + whitespace_linecount;
            index++;
            return tok != token.EOF;
        }
    );
    ```
    この新しいテストロジックでは、`eloc`(期待される`Location`)を動的に更新し、`loc.Pos`、`loc.Line`、`loc.Col`が期待値と一致するかを厳密にチェックしています。特に、`eloc.Line`の更新には`NewlineCount`関数が使用され、トークンリテラルと空白文字に含まれる改行が正確に行番号に反映されることを検証しています。これにより、スキャナーが提供する位置情報の正確性が大幅に向上したことがテストによって保証されています。

## 関連リンク

-   Go言語の`go/scanner`パッケージのドキュメント: [https://pkg.go.dev/go/scanner](https://pkg.go.dev/go/scanner)
-   Go言語の`go/token`パッケージのドキュメント: [https://pkg.go.dev/go/token](https://pkg.go.dev/go/token)
-   Go言語のソースコードリポジトリ: [https://github.com/golang/go](https://github.com/golang/go)

## 参考にした情報源リンク

-   コンパイラの字句解析に関する一般的な情報(例: Wikipediaの「字句解析」エントリなど)
-   Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(公開されている場合)
-   Go言語の`go/scanner`パッケージの過去のコミット履歴(GitHub上で確認可能)
-   Go言語の公式ドキュメントとチュートリアル
-   Go言語のソースコード自体
# [インデックス 1816] ファイルの概要

このコミットは、Go言語の標準ライブラリにおける`scanner`パッケージの重要な変更を含んでいます。具体的には、字句解析器(スキャナー)が改行文字(`\n`)をコメントとして特別扱いする挙動を廃止し、通常の空白文字として扱うように修正されました。これにより、スキャナーの内部ロジックが簡素化され、より正確な行・列情報の追跡が可能になりました。また、テストコードの改善と、スキャナーの利用例を示す新しいヘルパー関数`Tokenize`が追加されています。

## コミット

  • remove special handling of '\n' characters (used to be treated as comments for pretty printer purposes - now properly ignored as white space since we have line/col information)
  • changed sample use in comment to an actually compiled function to make sure sample is actually working
  • added extra tests (checking line and column values, and the tokenize function)

R=rsc DELTA=253 (61 added, 67 deleted, 125 changed) OCL=26143 CL=26181


## GitHub上でのコミットページへのリンク

[https://github.com/golang/go/commit/6f321e28f48c12e7dd9830198daefc2a8cfb410b](https://github.com/golang/go/commit/6f321e28f48c12e7dd9830198daefc2a8cfb410b)

## 元コミット内容

commit 6f321e28f48c12e7dd9830198daefc2a8cfb410b Author: Robert Griesemer gri@golang.org Date: Thu Mar 12 11:04:11 2009 -0700

- remove special handling of '\n' characters (used to be treated as comments
for pretty printer purposes - now properly ignored as white space since we
have line/col information)
- changed sample use in comment to an actually compiled function to make sure
sample is actually working
- added extra tests (checking line and column values, and the tokenize function)

R=rsc
DELTA=253  (61 added, 67 deleted, 125 changed)
OCL=26143
CL=26181

src/lib/go/scanner.go | 74 +++++-------\n src/lib/go/scanner_test.go | 294 +++++++++++++++++++++++---------------------- 2 files changed, 181 insertions(+), 187 deletions(-)

diff --git a/src/lib/go/scanner.go b/src/lib/go/scanner.go index ccac8e1112..f665f10bab 100644 --- a/src/lib/go/scanner.go +++ b/src/lib/go/scanner.go @@ -4,23 +4,7 @@

// A scanner for Go source text. Takes a []byte as source which can // then be tokenized through repeated calls to the Scan function. -// -// Sample use: -// -// import "token" -// import "scanner" -// -// func tokenize(src []byte) { -// var s scanner.Scanner; -// s.Init(src, nil /* no error handler /, false / ignore comments */); -// for { -// pos, tok, lit := s.Scan(); -// if tok == Scanner.EOF { -// return; -// } -// println(pos, token.TokenString(tok), string(lit)); -// }\n-// }\n+// For a sample use of a scanner, see the implementation of Tokenize. // package scanner

@@ -62,7 +46,7 @@ type Scanner struct { scan_comments bool; // if set, comments are reported as tokens

// scanning state

-\tloc Location; // location of ch +\tloc Location; // location before ch (src[loc.Pos] == ch) pos int; // current reading position (position after ch) ch int; // one char look-ahead } @@ -78,7 +62,7 @@ func (S *Scanner) next() { \tswitch { \tcase r == '\n': \t\tS.loc.Line++; -\t\t\tS.loc.Col = 1; +\t\t\tS.loc.Col = 0; \tcase r >= 0x80: \t\t// not ASCII \t\tr, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]); @@ -94,9 +78,9 @@ func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) { // Init prepares the scanner S to tokenize the text src. Calls to Scan // will use the error handler err if they encounter a syntax error. The boolean // scan_comments specifies whether newline characters and comments should be -// recognized and returned by Scan as token.COMMENT. If scan_comments is false, -// they are treated as white space and ignored. +// recognized and returned by Scan as token.COMMENT. If scan_comments is false, +// they are treated as white space and ignored. // func (S *Scanner) Init(src []byte, err ErrorHandler, scan_comments bool) { S.src = src; @@ -137,24 +121,6 @@ func (S *Scanner) expect(ch int) { }

-func (S *Scanner) skipWhitespace() {

  • for {
  • \tswitch S.ch {
  • \tcase '\t', '\r', ' ':
  • \t\t// nothing to do
  • \tcase '\n':
  • \t\tif S.scan_comments {
  • \t\t\treturn;
  • \t\t}
  • \tdefault:
  • \t\treturn;
  • \t}
  • \tS.next();
  • }
  • panic("UNREACHABLE"); -}

func (S *Scanner) scanComment(loc Location) { // first '/' already consumed

@@ -163,9 +129,7 @@ func (S *Scanner) scanComment(loc Location) { \tfor S.ch >= 0 { \t\tS.next(); \t\tif S.ch == '\n' { -\t\t\t\t// '\n' terminates comment but we do not include -\t\t\t\t// it in the comment (otherwise we don't see the -\t\t\t\t// start of a newline in skipWhitespace()). +\t\t\t\tS.next(); // '\n' belongs to the comment \t\t\treturn; \t\t} \t} @@ -412,14 +376,19 @@ func (S *Scanner) switch4(tok0, tok1, ch2, tok2, tok3 int) int {

// Scan scans the next token and returns the token location loc, // the token tok, and the literal text lit corresponding to the -// token. +// token. The source end is indicated by token.EOF. // func (S *Scanner) Scan() (loc Location, tok int, lit []byte) { scan_again: -\tS.skipWhitespace(); +\t// skip white space +\tfor S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' { +\t\tS.next(); +\t}

+\t// current token start loc, tok = S.loc, token.ILLEGAL;

+\t// determine token value \tswitch ch := S.ch; {\n \tcase isLetter(ch):\n \t\ttok = S.scanIdentifier();\n@@ -429,7 +398,6 @@ scan_again:\n \t\tS.next(); // always make progress\n \t\tswitch ch {\n \t\tcase -1 : tok = token.EOF;\n-\t\tcase '\n': tok = token.COMMENT;\n \t\tcase '"' : tok = token.STRING; S.scanString(loc);\n \t\tcase ''': tok = token.CHAR; S.scanChar();\n \t\tcase '`' : tok = token.STRING; S.scanRawString(loc);\n@@ -487,3 +455,17 @@ scan_again:
\n \treturn loc, tok, S.src[loc.Pos : S.loc.Pos];\n }\n+\n+\n+// Tokenize calls a function f with the token location, token value, and token\n+// text for each token in the source src. The other parameters have the same\n+// meaning as for the Init function. Tokenize keeps scanning until f returns\n+// false (usually when the token value is token.EOF).\n+//\n+func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {\n+\tvar s Scanner;\n+\ts.Init(src, err, scan_comments);\n+\tfor f(s.Scan()) {\n+\t\t// action happens in f\n+\t}\n+}\ndiff --git a/src/lib/go/scanner_test.go b/src/lib/go/scanner_test.go index 94c2e51d53..221f01256e 100644 --- a/src/lib/go/scanner_test.go +++ b/src/lib/go/scanner_test.go @@ -31,7 +31,6 @@ func tokenclass(tok int) int {

type elt struct { -\tpos int; \ttok int;\n \tlit string;\n \tclass int;\n@@ -40,130 +39,120 @@ type elt struct {

var tokens = [...]elt{\n \t// Special tokens\n-\telt{ 0, token.COMMENT, "/* a comment /", special },\n-\telt{ 0, token.COMMENT, "\n", special },\n+\telt{ token.COMMENT, "/ a comment /", special },\n+\telt{ token.COMMENT, "// a comment \n", special },\n \n \t// Identifiers and basic type literals\n-\telt{ 0, token.IDENT, "foobar", literal },\n-\telt{ 0, token.IDENT, "a۰۱۸", literal },\n-\telt{ 0, token.IDENT, "foo६४", literal },\n-\telt{ 0, token.IDENT, "bar9876", literal },\n-\telt{ 0, token.INT, "0", literal },\n-\telt{ 0, token.INT, "01234567", literal },\n-\telt{ 0, token.INT, "0xcafebabe", literal },\n-\telt{ 0, token.FLOAT, "0.", literal },\n-\telt{ 0, token.FLOAT, ".0", literal },\n-\telt{ 0, token.FLOAT, "3.14159265", literal },\n-\telt{ 0, token.FLOAT, "1e0", literal },\n-\telt{ 0, token.FLOAT, "1e+100", literal },\n-\telt{ 0, token.FLOAT, "1e-100", literal },\n-\telt{ 0, token.FLOAT, "2.71828e-1000", literal },\n-\telt{ 0, token.CHAR, "'a'", literal },\n-\telt{ 0, token.CHAR, "'\000'", literal },\n-\telt{ 0, token.CHAR, "'\xFF'", literal },\n-\telt{ 0, token.CHAR, "'\uff16'", literal },\n-\telt{ 0, token.CHAR, "'\U0000ff16'", literal },\n-\telt{ 0, token.STRING, "foobar", literal },\n+\telt{ token.IDENT, "foobar", literal },\n+\telt{ token.IDENT, "a۰۱۸", literal },\n+\telt{ token.IDENT, "foo६४", literal },\telt{ token.IDENT, "bar9876", literal },\n+\telt{ token.INT, "0", literal },\n+\telt{ token.INT, "01234567", literal },\n+\telt{ token.INT, "0xcafebabe", literal },\n+\telt{ token.FLOAT, "0.", literal },\n+\telt{ token.FLOAT, ".0", literal },\n+\telt{ token.FLOAT, "3.14159265", literal },\n+\telt{ token.FLOAT, "1e0", literal },\n+\telt{ token.FLOAT, "1e+100", literal },\n+\telt{ token.FLOAT, "1e-100", literal },\n+\telt{ token.FLOAT, "2.71828e-1000", literal },\n+\telt{ token.CHAR, "'a'", literal },\n+\telt{ token.CHAR, "'\000'", literal },\n+\telt{ token.CHAR, "'\xFF'", literal },\n+\telt{ token.CHAR, "'\uff16'", literal },\n+\telt{ token.CHAR, "'\U0000ff16'", literal },\n+\telt{ token.STRING, "foobar", literal },\n \n \t// Operators and delimitors\n-\telt{ 0, token.ADD, "+", operator },\n-\telt{ 0, token.SUB, "-", operator },\n-\telt{ 0, token.MUL, "", operator },\n-\telt{ 0, token.QUO, "/", operator },\n-\telt{ 0, token.REM, "%", operator },\n-\n-\telt{ 0, token.AND, "&", operator },\n-\telt{ 0, token.OR, "|", operator },\n-\telt{ 0, token.XOR, "^", operator },\n-\telt{ 0, token.SHL, "<<", operator },\n-\telt{ 0, token.SHR, ">>", operator },\n-\n-\telt{ 0, token.ADD_ASSIGN, "+=", operator },\n-\telt{ 0, token.SUB_ASSIGN, "-=", operator },\n-\telt{ 0, token.MUL_ASSIGN, "=", operator },\n-\telt{ 0, token.QUO_ASSIGN, "/=", operator },\n-\telt{ 0, token.REM_ASSIGN, "%=", operator },\n-\n-\telt{ 0, token.AND_ASSIGN, "&=", operator },\n-\telt{ 0, token.OR_ASSIGN, "|=", operator },\n-\telt{ 0, token.XOR_ASSIGN, "^=", operator },\n-\telt{ 0, token.SHL_ASSIGN, "<<=", operator },\n-\telt{ 0, token.SHR_ASSIGN, ">>=", operator },\n-\n-\telt{ 0, token.LAND, "&&", operator },\n-\telt{ 0, token.LOR, "||", operator },\n-\telt{ 0, token.ARROW, "<-", operator },\n-\telt{ 0, token.INC, "++", operator },\n-\telt{ 0, token.DEC, "--", operator },\n-\n-\telt{ 0, token.EQL, "==", operator },\n-\telt{ 0, token.LSS, "<", operator },\n-\telt{ 0, token.GTR, ">", operator },\n-\telt{ 0, token.ASSIGN, "=", operator },\n-\telt{ 0, token.NOT, "!", operator },\n-\n-\telt{ 0, token.NEQ, "!=", operator },\n-\telt{ 0, token.LEQ, "<=", operator },\n-\telt{ 0, token.GEQ, ">=", operator },\n-\telt{ 0, token.DEFINE, ":=", operator },\n-\telt{ 0, token.ELLIPSIS, "...", operator },\n-\n-\telt{ 0, token.LPAREN, "(", operator },\n-\telt{ 0, token.LBRACK, "[", operator },\n-\telt{ 0, token.LBRACE, "{", operator },\n-\telt{ 0, token.COMMA, ",", operator },\n-\telt{ 0, token.PERIOD, ".", operator },\n-\n-\telt{ 0, token.RPAREN, ")", operator },\n-\telt{ 0, token.RBRACK, "]", operator },\n-\telt{ 0, token.RBRACE, "}", operator },\n-\telt{ 0, token.SEMICOLON, ";", operator },\n-\telt{ 0, token.COLON, ":", operator },\n+\telt{ token.ADD, "+", operator },\n+\telt{ token.SUB, "-", operator },\n+\telt{ token.MUL, "", operator },\n+\telt{ token.QUO, "/", operator },\n+\telt{ token.REM, "%", operator },\n+\n+\telt{ token.AND, "&", operator },\n+\telt{ token.OR, "|", operator },\n+\telt{ token.XOR, "^", operator },\n+\telt{ token.SHL, "<<", operator },\n+\telt{ token.SHR, ">>", operator },\n+\n+\telt{ token.ADD_ASSIGN, "+=", operator },\n+\telt{ token.SUB_ASSIGN, "-=", operator },\n+\telt{ token.MUL_ASSIGN, "*=", operator },\n+\telt{ token.QUO_ASSIGN, "/=", operator },\n+\telt{ token.REM_ASSIGN, "%=", operator },\n+\n+\telt{ token.AND_ASSIGN, "&=", operator },\n+\telt{ token.OR_ASSIGN, "|=", operator },\n+\telt{ token.XOR_ASSIGN, "^=", operator },\n+\telt{ token.SHL_ASSIGN, "<<=", operator },\n+\telt{ token.SHR_ASSIGN, ">>=", operator },\n+\n+\telt{ token.LAND, "&&", operator },\n+\telt{ token.LOR, "||", operator },\n+\telt{ token.ARROW, "<-", operator },\n+\telt{ token.INC, "++", operator },\n+\telt{ token.DEC, "--", operator },\n+\n+\telt{ token.EQL, "==", operator },\n+\telt{ token.LSS, "<", operator },\n+\telt{ token.GTR, ">", operator },\n+\telt{ token.ASSIGN, "=", operator },\n+\telt{ token.NOT, "!", operator },\n+\n+\telt{ token.NEQ, "!=", operator },\n+\telt{ token.LEQ, "<=", operator },\n+\telt{ token.GEQ, ">=", operator },\n+\telt{ token.DEFINE, ":=", operator },\n+\telt{ token.ELLIPSIS, "...", operator },\n+\n+\telt{ token.LPAREN, "(", operator },\n+\telt{ token.LBRACK, "[", operator },\n+\telt{ token.LBRACE, "{", operator },\n+\telt{ token.COMMA, ",", operator },\n+\telt{ token.PERIOD, ".", operator },\n+\n+\telt{ token.RPAREN, ")", operator },\n+\telt{ token.RBRACK, "]", operator },\n+\telt{ token.RBRACE, "}", operator },\n+\telt{ token.SEMICOLON, ";", operator },\n+\telt{ token.COLON, ":", operator },\n \n \t// Keywords\n-\telt{ 0, token.BREAK, "break", keyword },\n-\telt{ 0, token.CASE, "case", keyword },\n-\telt{ 0, token.CHAN, "chan", keyword },\n-\telt{ 0, token.CONST, "const", keyword },\n-\telt{ 0, token.CONTINUE, "continue", keyword },\n-\n-\telt{ 0, token.DEFAULT, "default", keyword },\n-\telt{ 0, token.DEFER, "defer", keyword },\n-\telt{ 0, token.ELSE, "else", keyword },\n-\telt{ 0, token.FALLTHROUGH, "fallthrough", keyword },\n-\telt{ 0, token.FOR, "for", keyword },\n-\n-\telt{ 0, token.FUNC, "func", keyword },\n-\telt{ 0, token.GO, "go", keyword },\n-\telt{ 0, token.GOTO, "goto", keyword },\n-\telt{ 0, token.IF, "if", keyword },\n-\telt{ 0, token.IMPORT, "import", keyword },\n-\n-\telt{ 0, token.INTERFACE, "interface", keyword },\n-\telt{ 0, token.MAP, "map", keyword },\n-\telt{ 0, token.PACKAGE, "package", keyword },\n-\telt{ 0, token.RANGE, "range", keyword },\n-\telt{ 0, token.RETURN, "return", keyword },\n-\n-\telt{ 0, token.SELECT, "select", keyword },\n-\telt{ 0, token.STRUCT, "struct", keyword },\n-\telt{ 0, token.SWITCH, "switch", keyword },\n-\telt{ 0, token.TYPE, "type", keyword },\n-\telt{ 0, token.VAR, "var", keyword },\n+\telt{ token.BREAK, "break", keyword },\n+\telt{ token.CASE, "case", keyword },\n+\telt{ token.CHAN, "chan", keyword },\n+\telt{ token.CONST, "const", keyword },\n+\telt{ token.CONTINUE, "continue", keyword },\n+\n+\telt{ token.DEFAULT, "default", keyword },\n+\telt{ token.DEFER, "defer", keyword },\n+\telt{ token.ELSE, "else", keyword },\n+\telt{ token.FALLTHROUGH, "fallthrough", keyword },\n+\telt{ token.FOR, "for", keyword },\n+\n+\telt{ token.FUNC, "func", keyword },\n+\telt{ token.GO, "go", keyword },\n+\telt{ token.GOTO, "goto", keyword },\n+\telt{ token.IF, "if", keyword },\n+\telt{ token.IMPORT, "import", keyword },\n+\n+\telt{ token.INTERFACE, "interface", keyword },\n+\telt{ token.MAP, "map", keyword },\n+\telt{ token.PACKAGE, "package", keyword },\n+\telt{ token.RANGE, "range", keyword },\n+\telt{ token.RETURN, "return", keyword },\n+\n+\telt{ token.SELECT, "select", keyword },\n+\telt{ token.STRUCT, "struct", keyword },\n+\telt{ token.SWITCH, "switch", keyword },\n+\telt{ token.TYPE, "type", keyword },\n+\telt{ token.VAR, "var", keyword },\n }\n \n \n-const whitespace = " \t "; // to separate tokens\n-\n-func init() {\n-\t// set pos fields\n-\tpos := 0;\n-\tfor i := 0; i < len(tokens); i++ {\n-\t\ttokens[i].pos = pos;\n-\t\tpos += len(tokens[i].lit) + len(whitespace);\n-\t}\n-}\n-\n+const whitespace = " \t \n\n\n"; // to separate tokens\n \n type TestErrorHandler struct {\n \tt *testing.T\n@@ -174,38 +163,61 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {\n }\n \n \n+func NewlineCount(s string) int {\n+\tn := 0;\n+\tfor i := 0; i < len(s); i++ {\n+\t\tif s[i] == '\n' {\n+\t\t\tn++;\n+\t\t}\n+\t}\n+\treturn n;\n+}\n+\n+\n func Test(t *testing.T) {\n \t// make source\n \tvar src string;\n \tfor i, e := range tokens {\n \t\tsrc += e.lit + whitespace;\n \t}\n-\n-\t// set up scanner\n-\tvar s scanner.Scanner;\n-\ts.Init(io.StringBytes(src), &TestErrorHandler{t}, true);\n+\twhitespace_linecount := NewlineCount(whitespace);\n \n \t// verify scan\n-\tfor i, e := range tokens {\n-\t\tloc, tok, lit := s.Scan();\n-\t\tif loc.Pos != e.pos {\n-\t\t\tt.Errorf("bad position for %s: got %d, expected %d", e.lit, loc.Pos, e.pos);\n-\t\t}\n-\t\tif tok != e.tok {\n-\t\t\tt.Errorf("bad token for %s: got %s, expected %s", e.lit, token.TokenString(tok), token.TokenString(e.tok));\n-\t\t}\n-\t\tif token.IsLiteral(e.tok) && string(lit) != e.lit {\n-\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", e.lit, string(lit), e.lit);\n+\tindex := 0;\n+\teloc := scanner.Location{0, 1, 1};\n+\tscanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,\n+\t\tfunc (loc Location, tok int, litb []byte) bool {\n+\t\t\te := elt{token.EOF, "", special};\n+\t\t\tif index < len(tokens) {\n+\t\t\t\te = tokens[index];\n+\t\t\t}\n+\t\t\tlit := string(litb);\n+\t\t\tif tok == token.EOF {\n+\t\t\t\tlit = "";\n+\t\t\t\teloc.Col = 0;\n+\t\t\t}\n+\t\t\tif loc.Pos != eloc.Pos {\n+\t\t\t\tt.Errorf("bad position for %s: got %d, expected %d", lit, loc.Pos, eloc.Pos);\n+\t\t\t}\n+\t\t\tif loc.Line != eloc.Line {\n+\t\t\t\tt.Errorf("bad line for %s: got %d, expected %d", lit, loc.Line, eloc.Line);\n+\t\t\t}\n+\t\t\tif loc.Col != eloc.Col {\n+\t\t\t\tt.Errorf("bad column for %s: got %d, expected %d", lit, loc.Col, eloc.Col);\n+\t\t\t}\n+\t\t\tif tok != e.tok {\n+\t\t\t\tt.Errorf("bad token for %s: got %s, expected %s", lit, token.TokenString(tok), token.TokenString(e.tok));\n+\t\t\t}\n+\t\t\tif token.IsLiteral(e.tok) && lit != e.lit {\n+\t\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", lit, lit, e.lit);\n+\t\t\t}\n+\t\t\tif tokenclass(tok) != e.class {\n+\t\t\t\tt.Errorf("bad class for %s: got %d, expected %d", lit, tokenclass(tok), e.class);\n+\t\t\t}\n+\t\t\teloc.Pos += len(lit) + len(whitespace);\n+\t\t\teloc.Line += NewlineCount(lit) + whitespace_linecount;\n+\t\t\tindex++;\n+\t\t\treturn tok != token.EOF;\n \t\t}\n-\t\tif tokenclass(tok) != e.class {\n-\t\t\tt.Errorf("bad class for %s: got %d, expected %d", e.lit, tokenclass(tok), e.class);\n-\t\t}\n-\t}\n-\tloc, tok, lit := s.Scan();\n-\tif tok != token.EOF {\n-\t\tt.Errorf("bad token at eof: got %s, expected EOF", token.TokenString(tok));\n-\t}\n-\tif tokenclass(tok) != special {\n-\t\tt.Errorf("bad class at eof: got %d, expected %d", tokenclass(tok), special);\n-\t}\n+\t);\n }\n```

変更の背景

このコミットが行われた背景には、Go言語の字句解析器(スキャナー)の設計と機能の進化があります。初期のGo言語のツールチェインでは、スキャナーが改行文字(\n)を特別なトークン(token.COMMENT)として扱っていました。これは、主にプリティプリンター(コード整形ツール)がソースコードの構造を再構築する際に、改行の位置を保持するための一時的な措置であったと考えられます。

しかし、スキャナーが正確な行と列の情報(Location構造体)を追跡する能力が向上するにつれて、改行文字をコメントとして特別扱いする必要性がなくなりました。改行は本質的に空白文字の一種であり、構文解析の段階でその位置情報が適切に利用されるべきです。この特別扱いを廃止することで、スキャナーのロジックが簡素化され、より一般的な空白文字の処理に統一することが可能になります。

また、scannerパッケージのドキュメントに記載されていたサンプルコードが、コメント形式であったため実際にコンパイルして動作確認することができませんでした。このコミットでは、そのサンプルを実際にコンパイル可能な関数として実装し、Tokenizeという新しいヘルパー関数として提供することで、利用者がスキャナーの基本的な使い方をより簡単に理解し、テストできるように改善されています。

さらに、スキャナーの正確性を保証するために、行と列の値のチェックを含む新しいテストケースが追加されました。これは、改行文字の扱いが変更されたことによる潜在的なバグを防ぎ、スキャナーが常に正確な位置情報を提供することを保証するために不可欠な変更です。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. 字句解析器(Lexer/Scanner/Tokenizer): コンパイラのフロントエンドの一部であり、ソースコードを読み込み、意味のある最小単位である「トークン(token)」のストリームに変換するプログラムです。例えば、var x = 10;というコードは、var(キーワード)、x(識別子)、=(代入演算子)、10(整数リテラル)、;(セミコロン)といったトークンに分割されます。

  2. トークン(Token): 字句解析器によって識別される、プログラミング言語における意味のある最小単位です。キーワード、識別子、演算子、リテラル、区切り文字などが含まれます。Go言語では、go/tokenパッケージで定義されています。

  3. 空白文字(Whitespace): ソースコードの可読性を高めるために使用される、プログラムの実行には影響しない文字の総称です。スペース、タブ、改行、キャリッジリターンなどが含まれます。字句解析器は通常、これらの空白文字をスキップします。

  4. コメント(Comment): プログラマーがコードの説明やメモを記述するために使用するテキストで、コンパイラやインタプリタによって無視されます。Go言語では、//による行コメントと/* ... */によるブロックコメントがあります。

  5. 位置情報(Location/Position): ソースコード内の特定の文字やトークンがどこにあるかを示す情報です。通常、ファイル名、行番号、列番号、そしてファイル先頭からのオフセット(バイト数または文字数)で構成されます。Go言語のgo/scannerパッケージではLocation構造体がこれに該当します。

  6. Go言語のgo/scannerパッケージ: Go言語のソースコードを字句解析するためのパッケージです。Scanner型が字句解析器の主要な実装を提供し、Scanメソッドを呼び出すことで次のトークンを取得できます。

  7. Go言語のgo/tokenパッケージ: Go言語のトークン定数を定義するパッケージです。token.IDENT(識別子)、token.INT(整数)、token.ADD(加算演算子)などが含まれます。

技術的詳細

このコミットの技術的詳細は、主にscannerパッケージの内部動作と、Go言語の字句解析における改行文字の扱いの変更に集約されます。

  1. 改行文字の扱いの一貫性: 以前のscannerでは、Scanメソッド内でcase '\n': tok = token.COMMENT;という特殊な処理があり、改行文字がtoken.COMMENTとして扱われることがありました。これは、scan_commentsフラグがtrueの場合に、改行もコメントとして報告されるという挙動を意味します。しかし、改行はコメントではなく、単なる空白文字として扱われるべきです。このコミットでは、この特殊なケースが削除され、改行文字は他の空白文字(スペース、タブ、キャリッジリターン)と同様に、Scanメソッドの冒頭で一括してスキップされるようになりました。これにより、字句解析器の動作がより直感的で一貫性のあるものになりました。

  2. 列番号の0ベース化: Scanner構造体のloc Locationフィールドは、現在の文字chの位置を示すように定義が変更されました(loc Location; // location before ch (src[loc.Pos] == ch))。これに伴い、next()メソッド内で改行文字を処理する際の列番号の初期化がS.loc.Col = 1;からS.loc.Col = 0;に変更されました。これは、Go言語の内部的な位置情報管理において、列番号が0ベースで扱われるようになったことを示唆しています。多くのプログラミング言語やエディタでは列番号が1ベースで表示されますが、内部処理では0ベースの方が計算が容易な場合があります。この変更により、スキャナーが報告する位置情報がより正確になり、内部的な整合性が向上しました。

  3. skipWhitespace関数の廃止とインライン化: 以前はskipWhitespace()という独立した関数が存在し、Scanメソッドの冒頭で呼び出されていました。この関数は、空白文字をスキップする役割を担っていましたが、改行文字の特殊な扱い(scan_commentsフラグによる条件分岐)が含まれていました。改行文字の特殊扱いが廃止されたことで、skipWhitespace()関数は不要となり、そのロジックはScanメソッドの冒頭に直接インライン化されました。これにより、コードの呼び出しオーバーヘッドが削減され、Scanメソッドの処理フローがより明確になりました。

  4. Tokenizeヘルパー関数の導入: scannerパッケージにTokenizeという新しいトップレベル関数が追加されました。この関数は、ソースコード全体を字句解析し、見つかった各トークンに対してユーザーが指定したコールバック関数fを呼び出します。これは、スキャナーの基本的な使用パターンをカプセル化し、テストコードや簡単なツールでスキャナーを利用する際の利便性を向上させます。Tokenize関数は、内部でScannerインスタンスを初期化し、Scanメソッドを繰り返し呼び出すことで、字句解析のループを抽象化します。

  5. テストの強化: scanner_test.goファイルでは、elt構造体からposフィールドが削除され、init()関数によるposの計算も廃止されました。これは、スキャナーが提供するLocation構造体(Pos, Line, Col)が十分に正確になったため、テストデータ内で別途オフセットを管理する必要がなくなったことを示しています。 さらに、Test関数はscanner.Tokenize関数を利用するように書き換えられ、トークンの位置情報(行と列)の正確性を検証する新しいアサーションが追加されました。特に、eloc.Lineeloc.Colを動的に更新し、改行文字が正しく行と列のカウントに影響を与えることを確認しています。NewlineCountヘルパー関数も追加され、テストの正確性を高めています。

これらの変更は、Go言語の字句解析器がより堅牢で、正確な位置情報を提供し、内部的にクリーンな設計へと進化していることを示しています。

コアとなるコードの変更箇所

src/lib/go/scanner.go

  1. Scanner構造体のlocフィールドのコメント変更:

    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -62,7 +46,7 @@ type Scanner struct {
     	scan_comments bool;  // if set, comments are reported as tokens
     
     	// scanning state
    -	loc Location;  // location of ch
    +	loc Location;  // location before ch (src[loc.Pos] == ch)
     	pos int;  // current reading position (position after ch)
     	ch int;  // one char look-ahead
     }
    

    locフィールドがchの位置を示すという定義が明確化されました。

  2. next()メソッドにおける列番号の初期化変更:

    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -78,7 +62,7 @@ func (S *Scanner) next() {
     	\tswitch {
     	\tcase r == '\n':
     	\t\tS.loc.Line++;
    

-\t\t\tS.loc.Col = 1; +\t\t\tS.loc.Col = 0; \tcase r >= 0x80: \t\t// not ASCII \t\tr, w = utf8.DecodeRune(S.src[S.pos : len(S.src)]); ``` 改行時に列番号を0にリセットするようになりました。

  1. skipWhitespace()関数の削除:
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -137,24 +121,6 @@ func (S *Scanner) expect(ch int) {
     }
     
     
    -func (S *Scanner) skipWhitespace() {
    -	for {
    -	\tswitch S.ch {
    -	\tcase '\t', '\r', ' ':
    -	\t\t// nothing to do
    -	\tcase '\n':
    -	\t\tif S.scan_comments {
    -	\t\t\treturn;
    -	\t\t}
    -	\tdefault:
    -	\t\treturn;
    -	\t}
    -	\tS.next();
    -	}
    -	panic("UNREACHABLE");
    -}
    -
    -
     func (S *Scanner) scanComment(loc Location) {
     	// first '/' already consumed
    
    

@@ -163,9 +129,7 @@ func (S *Scanner) scanComment(loc Location) { \tfor S.ch >= 0 { \t\tS.next(); \t\tif S.ch == '\n' { -\t\t\t\t// '\n' terminates comment but we do not include -\t\t\t\t// it in the comment (otherwise we don't see the -\t\t\t\t// start of a newline in skipWhitespace()). +\t\t\t\tS.next(); // '\n' belongs to the comment \t\t\treturn; \t\t} \t} ``` 行コメントの終端で改行文字を消費するようになりました。

  1. Scan()メソッドの空白文字スキップと改行の特殊処理削除:
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -412,14 +376,19 @@ func (S *Scanner) switch4(tok0, tok1, ch2, tok2, tok3 int) int {
     
     // Scan scans the next token and returns the token location loc,
     // the token tok, and the literal text lit corresponding to the
    

-// token. +// token. The source end is indicated by token.EOF. // func (S *Scanner) Scan() (loc Location, tok int, lit []byte) { scan_again: -\tS.skipWhitespace(); +\t// skip white space +\tfor S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' { +\t\tS.next(); +\t}

+\t// current token start loc, tok = S.loc, token.ILLEGAL;

+\t// determine token value \tswitch ch := S.ch; {\n \tcase isLetter(ch):\n \t\ttok = S.scanIdentifier();\n@@ -429,7 +398,6 @@ scan_again:
\t\tS.next(); // always make progress\n \t\tswitch ch {\n \t\tcase -1 : tok = token.EOF;\n-\t\tcase '\n': tok = token.COMMENT;\n \t\tcase '"' : tok = token.STRING; S.scanString(loc);\n \t\tcase ''': tok = token.CHAR; S.scanChar();\n \t\tcase '' : tok = token.STRING; S.scanRawString(loc);\ ``` skipWhitespace()の呼び出しがインラインループに置き換えられ、case '\n': tok = token.COMMENT;`が削除されました。

  1. Tokenize関数の追加:
    --- a/src/lib/go/scanner.go
    +++ b/src/lib/go/scanner.go
    @@ -487,3 +455,17 @@ scan_again:\
      
     	return loc, tok, S.src[loc.Pos : S.loc.Pos];\n }\n+\n+\n+// Tokenize calls a function f with the token location, token value, and token\n+// text for each token in the source src. The other parameters have the same\n+// meaning as for the Init function. Tokenize keeps scanning until f returns\n+// false (usually when the token value is token.EOF).\n+//\n+func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {\n+\tvar s Scanner;\n+\ts.Init(src, err, scan_comments);\n+\tfor f(s.Scan()) {\n+\t\t// action happens in f\n+\t}\n+}\
    
    新しいヘルパー関数Tokenizeが追加されました。

src/lib/go/scanner_test.go

  1. elt構造体からposフィールドの削除:

    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -31,7 +31,6 @@ func tokenclass(tok int) int {
     
      
     type elt struct {
    -\tpos int;
      \ttok int;\n \tlit string;\n \tclass int;\n    ```
    テスト要素から位置オフセット情報が削除されました。
    
    
  2. tokens配列の初期化からpos値の削除:

    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -40,130 +39,120 @@ type elt struct {
      
     var tokens = [...]elt{\n \t// Special tokens\n-\telt{ 0, token.COMMENT, "/* a comment */", special },\n-\telt{ 0, token.COMMENT, "\n", special },\n+\telt{ token.COMMENT, "/* a comment */", special },\n+\telt{ token.COMMENT, "// a comment \n", special },
    

    eltの初期化から0posの値)が削除されました。

  3. init()関数の削除:

    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -154,14 +143,6 @@ func tokenclass(tok int) int {
      
      
    -const whitespace = "  \t  ";  // to separate tokens
    -
    -func init() {\n-\t// set pos fields\n-\tpos := 0;\n-\tfor i := 0; i < len(tokens); i++ {\n-\t\ttokens[i].pos = pos;\n-\t\tpos += len(tokens[i].lit) + len(whitespace);\n-\t}\n-}\n-\n+const whitespace = "  \t  \n\n\n";  // to separate tokens
    

    posフィールドを初期化していたinit関数が削除されました。whitespace定数に改行が追加されました。

  4. NewlineCount関数の追加:

    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -163,6 +154,17 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {
     }
      
      
    +func NewlineCount(s string) int {\n+\tn := 0;\n+\tfor i := 0; i < len(s); i++ {\n+\t\tif s[i] == '\n' {\n+\t\t\tn++;\n+\t\t}\n+\t}\n+\treturn n;\n+}\n+\n+\n     func Test(t *testing.T) {
    

    文字列中の改行数をカウントするヘルパー関数が追加されました。

  5. Test関数の大幅な変更(scanner.Tokenizeの使用と位置情報の検証):

    --- a/src/lib/go/scanner_test.go
    +++ b/src/lib/go/scanner_test.go
    @@ -174,38 +163,61 @@ func (h *TestErrorHandler) Error(loc scanner.Location, msg string) {
      func Test(t *testing.T) {\n \t// make source\n \tvar src string;\n \tfor i, e := range tokens {\n \t\tsrc += e.lit + whitespace;\n \t}\n-\n-\t// set up scanner\n-\tvar s scanner.Scanner;\n-\ts.Init(io.StringBytes(src), &TestErrorHandler{t}, true);\n+\twhitespace_linecount := NewlineCount(whitespace);\
      
     \t// verify scan\n-\tfor i, e := range tokens {\n-\t\tloc, tok, lit := s.Scan();\n-\t\tif loc.Pos != e.pos {\n-\t\t\tt.Errorf("bad position for %s: got %d, expected %d", e.lit, loc.Pos, e.pos);\n-\t\t}\n-\t\tif tok != e.tok {\n-\t\t\tt.Errorf("bad token for %s: got %s, expected %s", e.lit, token.TokenString(tok), token.TokenString(e.tok));\n-\t\t}\n-\t\tif token.IsLiteral(e.tok) && string(lit) != e.lit {\n-\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", e.lit, string(lit), e.lit);\n+\tindex := 0;\n+\teloc := scanner.Location{0, 1, 1};\n+\tscanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,\n+\t\tfunc (loc Location, tok int, litb []byte) bool {\n+\t\t\te := elt{token.EOF, "", special};\n+\t\t\tif index < len(tokens) {\n+\t\t\t\te = tokens[index];\n+\t\t\t}\n+\t\t\tlit := string(litb);\n+\t\t\tif tok == token.EOF {\n+\t\t\t\tlit = "<EOF>";\n+\t\t\t\teloc.Col = 0;\n+\t\t\t}\n+\t\t\tif loc.Pos != eloc.Pos {\n+\t\t\t\tt.Errorf("bad position for %s: got %d, expected %d", lit, loc.Pos, eloc.Pos);\n+\t\t\t}\n+\t\t\tif loc.Line != eloc.Line {\n+\t\t\t\tt.Errorf("bad line for %s: got %d, expected %d", lit, loc.Line, eloc.Line);\n+\t\t\t}\n+\t\t\tif loc.Col != eloc.Col {\n+\t\t\t\tt.Errorf("bad column for %s: got %d, expected %d", lit, loc.Col, eloc.Col);\n+\t\t\t}\n+\t\t\tif tok != e.tok {\n+\t\t\t\tt.Errorf("bad token for %s: got %s, expected %s", lit, token.TokenString(tok), token.TokenString(e.tok));\n+\t\t\t}\n+\t\t\tif token.IsLiteral(e.tok) && lit != e.lit {\n+\t\t\t\tt.Errorf("bad literal for %s: got %s, expected %s", lit, lit, e.lit);\n+\t\t\t}\n+\t\t\tif tokenclass(tok) != e.class {\n+\t\t\t\tt.Errorf("bad class for %s: got %d, expected %d", lit, tokenclass(tok), e.class);\n+\t\t\t}\n+\t\t\teloc.Pos += len(lit) + len(whitespace);\n+\t\t\teloc.Line += NewlineCount(lit) + whitespace_linecount;\n+\t\t\tindex++;\n+\t\t\treturn tok != token.EOF;\n \t\t}\n-\t\tif tokenclass(tok) != e.class {\n-\t\t\tt.Errorf("bad class for %s: got %d, expected %d", e.lit, tokenclass(tok), e.class);\n-\t\t}\n-\t}\n-\tloc, tok, lit := s.Scan();\n-\tif tok != token.EOF {\n-\t\tt.Errorf("bad token at eof: got %s, expected EOF", token.TokenString(tok));\n-\t}\n-\tif tokenclass(tok) != special {\n-\t\tt.Errorf("bad class at eof: got %d, expected %d", tokenclass(tok), special);\n-\t}\n+\t);\n }\n```
    
    

コアとなるコードの解説

src/lib/go/scanner.go

  • Scanner構造体のlocフィールドのコメント変更: loc Location; // location before ch (src[loc.Pos] == ch) この変更は、Scannerの内部状態におけるlocフィールドの役割を明確にしています。以前は単にlocation of chとされていましたが、新しいコメントはlocが現在の文字ch直前の位置を指すことを示しています。これは、字句解析器が文字を読み進める際に、常に次のトークンの開始位置を正確に把握するために重要な概念です。

  • next()メソッドにおける列番号の初期化変更: S.loc.Col = 0; next()メソッドは、スキャナーが次の文字を読み込む際に呼び出されます。改行文字(\n)を検出した場合、行番号をインクリメントし、列番号をリセットします。以前は1にリセットしていましたが、この変更により0にリセットされるようになりました。これは、Go言語の内部的な位置情報が0ベースの列番号を使用していることを示しており、より多くのプログラミング言語の内部表現と一致します。

  • skipWhitespace()関数の削除とScan()メソッドへのインライン化: 以前はskipWhitespace()という独立した関数があり、Scan()メソッドの冒頭で呼び出されていました。この関数は、スペース、タブ、キャリッジリターン、そして条件付きで改行文字をスキップしていました。 変更後、Scan()メソッドの冒頭に以下のループが直接記述されました。

    for S.ch == ' ' || S.ch == '\t' || S.ch == '\n' || S.ch == '\r' {
        S.next();
    }
    

    この変更により、skipWhitespace()関数が不要になり、コードの簡潔性が向上しました。また、改行文字が他の空白文字と同様に無条件にスキップされるようになり、scan_commentsフラグによる特殊な挙動がなくなりました。

  • Scan()メソッドからのcase '\n': tok = token.COMMENT;の削除: 以前のScan()メソッドのswitch文には、改行文字をtoken.COMMENTとして扱う特殊なケースがありました。

    case '\n': tok = token.COMMENT;
    

    この行が削除されたことで、改行文字はもはやコメントとして扱われず、前述の空白文字スキップのロジックによって処理されるようになりました。これにより、スキャナーの動作がより標準的な字句解析の挙動に近づきました。

  • Tokenize関数の追加:

    func Tokenize(src []byte, err ErrorHandler, scan_comments bool, f func (loc Location, tok int, lit []byte) bool) {
        var s Scanner;
        s.Init(src, err, scan_comments);
        for f(s.Scan()) {
            // action happens in f
        }
    }
    

    この新しい関数は、scannerパッケージの利用を簡素化するためのヘルパーです。ソースコードsrcを受け取り、内部でScannerを初期化します。そして、s.Scan()を繰り返し呼び出し、その結果(トークンの位置、種類、リテラル)を引数としてfというコールバック関数に渡します。ffalseを返すまでこの処理を続けます。これにより、ユーザーは字句解析のループを自分で書くことなく、トークン処理のロジックに集中できるようになります。

src/lib/go/scanner_test.go

  • elt構造体からのposフィールドの削除: type elt struct { tok int; lit string; class int; } 以前のテストでは、各トークンの期待されるファイル内オフセット(pos)をelt構造体で保持していました。しかし、スキャナーが正確なLocation情報(Pos, Line, Col)を提供するようになったため、このposフィールドは冗長となり削除されました。これにより、テストデータの定義が簡素化されました。

  • init()関数の削除: init()関数は、以前はtokens配列内の各eltposフィールドを計算して設定するために使用されていました。posフィールドが削除されたため、この初期化関数も不要となり削除されました。

  • NewlineCount関数の追加:

    func NewlineCount(s string) int {
        n := 0;
        for i := 0; i < len(s); i++ {
            if s[i] == '\n' {
                n++;
            }
        }
        return n;
    }
    

    このヘルパー関数は、与えられた文字列に含まれる改行文字の数を正確にカウントします。これは、テストにおいて期待される行番号を計算する際に使用され、スキャナーの行カウントの正確性を検証するために不可欠です。

  • Test関数の変更: Test関数は、scanner.Tokenize関数を利用するように全面的に書き換えられました。

    scanner.Tokenize(io.StringBytes(src), &TestErrorHandler{t}, true,
        func (loc Location, tok int, litb []byte) bool {
            // ... テストロジック ...
            if loc.Pos != eloc.Pos { ... }
            if loc.Line != eloc.Line { ... }
            if loc.Col != eloc.Col { ... }
            // ...
            eloc.Pos += len(lit) + len(whitespace);
            eloc.Line += NewlineCount(lit) + whitespace_linecount;
            index++;
            return tok != token.EOF;
        }
    );
    

    この新しいテストロジックでは、eloc(期待されるLocation)を動的に更新し、loc.Posloc.Lineloc.Colが期待値と一致するかを厳密にチェックしています。特に、eloc.Lineの更新にはNewlineCount関数が使用され、トークンリテラルと空白文字に含まれる改行が正確に行番号に反映されることを検証しています。これにより、スキャナーが提供する位置情報の正確性が大幅に向上したことがテストによって保証されています。

関連リンク

参考にした情報源リンク

  • コンパイラの字句解析に関する一般的な情報(例: Wikipediaの「字句解析」エントリなど)
  • Go言語の初期の設計に関する議論やメーリングリストのアーカイブ(公開されている場合)
  • Go言語のgo/scannerパッケージの過去のコミット履歴(GitHub上で確認可能)
  • Go言語の公式ドキュメントとチュートリアル
  • Go言語のソースコード自体