Go 코드 심층 분석
This article is originally posted here.
문법 차원에서 원본 코드를 분석하면 다양한 방식으로 인코딩할 수 있습니다.이를 위해 텍스트는 대부분의 언어에서 쉽게 처리할 수 있도록 거의 항상 AST로 변환됩니다.
당신들 중 일부가 알 수 있는 바와 같이 Go는 강력한 가방
go/parser
을 가지고 있습니다. 이 가방이 있으면 원본 코드를 AST로 비교적 쉽게 변환할 수 있습니다.그러나 나는 그것이 어떻게 작동하는지 궁금해서 API를 읽기 시작해야 내 생각을 만족시킬 수 있다는 것을 깨달았다.
본고에서 저는 그 API의 실현을 읽고 어떻게 바뀌었는지 알려드리겠습니다.
Go 언어에 익숙하지 않은 사람이라도 브라우저의 탭을 닫을 필요가 없습니다. 이것은 프로그래밍 언어를 어떻게 분석하는지 이해하기에 충분한 통용적인 문장이기 때문입니다.
본고는 컴파일러와 해석기를 이해하고 정태 분석을 깊이 연구하는 첫걸음이기도 하다.
AST 회사
우리는 읽기 실현에 필요한 지식부터 시작합시다.
AST(추상 구문 트리)는 무엇입니까?기준 Wikipedia:
In computer science, an abstract syntax tree (AST), or just syntax tree, is a tree representation of the abstract syntactic structure of source code written in a programming language. Each node of the tree denotes a construct occurring in the source code.
대부분의 컴파일러와 해석기는 AST를 원본 코드의 내부 표시로 사용한다.AST는 일반적으로 구문 트리에서 세미콜론, 줄 바꿈, 공백, 대괄호, 네모난 괄호, 원괄호 등을 생략합니다.
AST를 사용하면 다음을 수행할 수 있습니다.
AST로 변환하는 방법
순수한 텍스트는 우리에게 매우 간단하지만, 기계적으로 이보다 더 처리하기 어려운 것은 없다.따라서 먼저 어법기로 텍스트에 대해 어법 분석을 해야 한다.일반적인 프로세스는 해석기에 전달하고 AST를 읽어들이는 것입니다.
나는 여기에서 어떤 해석기에도 사용할 수 있는 일반적인 AST 형식이 없다고 지적하고 싶다.
예를 들어, Go의
x + 2
표현은 다음과 같습니다.*ast.BinaryExpr {
. X: *ast.Ident {
. . NamePos: 1
. . Name: "x"
. . Obj: *ast.Object {
. . . Kind: bad
. . . Name: ""
. . }
. }
. OpPos: 3
. Op: +
. Y: *ast.BasicLit {
. . ValuePos: 5
. . Kind: INT
. . Value: "2"
. }
}
어휘 분석
앞에서 말한 바와 같이, 분석은 일반적으로 텍스트를lexer에 전달한 다음에 영패를 얻는다.영패는 문자열로 지정되고 이로써 식별되는 의미를 가진다.
go/scanner.Scanner
Go의 lexer를 담당합니다.확실한 의미는 무엇입니까?눈으로 본 것이 사실이다.
가령 네가 썼다면
package main
const s = "foo"
이것은 표시할 때 발생하는 상황입니다.PACKAGE(package)
IDENT(main)
CONST(const)
IDENT(s)
ASSIGN(=)
STRING("foo")
EOF()
Go의 모든 영패here를 정의합니다.분석 API 분리
Go 소스 파일을 AST로 변환하려면 다음과 같이
go/parser.ParseFile
를 호출합니다.fset := token.NewFileSet()
f, _ := parser.ParseFile(fset, "foo.go", nil, parser.ParseComments)
현재 우리는 이미 전 장에서 전환 절차를 이해했고 이 방법의 내부 실현을 실제적으로 읽을 수 있습니다!Go 버전은 1.14.1입니다.스캐너.Scan() - 구문 분석 방법
Go는 어휘 분석을 어떻게 합니까?앞에서 설명한 바와 같이,
go/scanner.Scanner
Go의 lexer를 담당합니다.따라서 우선
Scanner.Scan()
방법을 자세히 살펴보자. parser.ParseFile()
내부에서 호출된다.scanner/scanner.go
func (s *Scanner) Scan() (pos token.Pos, tok token.Token, lit string) {
... // Omission
switch ch := s.ch; {
case isLetter(ch):
lit = s.scanIdentifier()
if len(lit) > 1 {
// keywords are longer than one letter - avoid lookup otherwise
tok = token.Lookup(lit)
switch tok {
case token.IDENT, token.BREAK, token.CONTINUE, token.FALLTHROUGH, token.RETURN:
insertSemi = true
}
} else {
... // Omission
}
ch
는 스캐너가 보유한 현재 문자입니다.Scanner.Scan()
호출 Scanner.next()
을 통해 다음 문자로 이동하여 ch
을 채웁니다. 이 문자가 식별자 이름으로 사용할 수 있다면.상기 코드는
ch
가 자모인 경우에 적용된다.표지부호로 사용할 수 없는 문자가 나타나면 전진을 멈추고 영패의 유형을 정합니다.캐릭터에 따라 하나의 영패의 시작 위치와 끝 위치를 정하는 방법이 다르다.예를 들어, 문자열은 나타날 때까지 계속 진행됩니다
"
.scanner/scanner.go
case '"':
insertSemi = true
tok = token.STRING
lit = s.scanString()
func (s *Scanner) scanString() string {
// '"' opening already consumed
offs := s.offset - 1
for {
ch := s.ch
if ch == '\n' || ch < 0 {
s.error(offs, "string literal not terminated")
break
}
s.next()
if ch == '"' {
break
}
if ch == '\\' {
s.scanEscape('"')
}
}
return string(s.src[offs:s.offset])
}
마지막은 스캐너다.방법은 이미 식별된 영패를 되돌려줍니다.분석 중
파일을 분석하기 전에 Go의 파일 구조를 살펴보겠습니다.
기준 The Go Programming Language Specification - Source file organization:
Each source file consists of a package clause defining the package to which it belongs, followed by a possibly empty set of import declarations that declare packages whose contents it wishes to use, followed by a possibly empty set of declarations of functions, types, variables, and constants.
즉, 구조는 다음과 같습니다.
parser.parseFile()
파일 끝까지 설명을 반복합니다.parser/parser.go
for p.tok != token.EOF {
decls = append(decls, p.parseDecl(declStart))
}
다음으로 봅시다parser.parseDecl
.해상도.parseDecl() - 선언된 구문을 해석하는 방법
parser.parseDecl()
반환ast.Decl
, 구문 트리의 노드, Go 소스 코드의 설명을 표시합니다.parser/parser.go
func (p *parser) parseDecl(sync map[token.Token]bool) ast.Decl {
if p.trace {
defer un(trace(p, "Declaration"))
}
var f parseSpecFunction
switch p.tok {
case token.CONST, token.VAR:
f = p.parseValueSpec
case token.TYPE:
f = p.parseTypeSpec
case token.FUNC:
return p.parseFuncDecl()
default:
pos := p.pos
p.errorExpected(pos, "declaration")
p.advance(sync)
return &ast.BadDecl{From: pos, To: p.pos}
}
return p.parseGenDecl(p.tok, f)
}
그것은 영패를 두루 훑어보고 키워드마다 다른 처리를 한다.깊이 있게 살펴보겠습니다parseFuncDecl()
.parser/parser.go
if p.tok == token.LPAREN {
recv = p.parseParameters(scope, false)
}
ident := p.parseIdent()
params, results := p.parseSignature(scope)
var body *ast.BlockStmt
if p.tok == token.LBRACE {
body = p.parseBody(scope)
p.expectSemi()
} else if p.tok == token.SEMICOLON {
p.next()
내부에서는 호출Scanner.Scan()
을 통해 영패를 추진한다. - 앞에서 이 점을 상세히 소개했다.token.LPAREN
은(
을 나타내기 때문에 (
을 찾으면 매개변수를 해석하는 것을 볼 수 있습니다.token.LBRACE
은{
을 나타내므로 {
을 찾으면 함수체를 해석하기 시작하는 것을 볼 수 있습니다..
.
.
아이고, 이런 속도로, 영원한 시간이 필요할 거야...
총결산
내가 영패를 해석하는 것은 내가 무서워했던 컴파일러와 해석기에 더욱 가깝게 느껴진다.
나도 Writing A Compiler In Go과Writing An Interpreter In Go를 섭렵하고 싶다.
Reference
이 문제에 관하여(Go 코드 심층 분석), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/nakabonne/digging-deeper-into-the-analysis-of-go-code-31af텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)