blob: 4d71c8190a8f83f8eb398bcb3f57d9bd51bb1f2e [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This file implements the encoding of source positions.
package src
import (
"bytes"
"fmt"
"io"
)
// A Pos encodes a source position consisting of a (line, column) number pair
// and a position base. A zero Pos is a ready to use "unknown" position (nil
// position base and zero line number).
//
// The (line, column) values refer to a position in a file independent of any
// position base ("absolute" file position).
//
// The position base is used to determine the "relative" position, that is the
// filename and line number relative to the position base. If the base refers
// to the current file, there is no difference between absolute and relative
// positions. If it refers to a //line directive, a relative position is relative
// to that directive. A position base in turn contains the position at which it
// was introduced in the current file.
type Pos struct {
base *PosBase
lico
}
// NoPos is a valid unknown position.
var NoPos Pos
// MakePos creates a new Pos value with the given base, and (file-absolute)
// line and column.
func MakePos(base *PosBase, line, col uint) Pos {
return Pos{base, makeLico(line, col)}
}
// IsKnown reports whether the position p is known.
// A position is known if it either has a non-nil
// position base, or a non-zero line number.
func (p Pos) IsKnown() bool {
return p.base != nil || p.Line() != 0
}
// Before reports whether the position p comes before q in the source.
// For positions in different files, ordering is by filename.
func (p Pos) Before(q Pos) bool {
n, m := p.Filename(), q.Filename()
return n < m || n == m && p.lico < q.lico
}
// After reports whether the position p comes after q in the source.
// For positions in different files, ordering is by filename.
func (p Pos) After(q Pos) bool {
n, m := p.Filename(), q.Filename()
return n > m || n == m && p.lico > q.lico
}
func (p Pos) LineNumber() string {
if !p.IsKnown() {
return "?"
}
return p.lico.lineNumber()
}
func (p Pos) LineNumberHTML() string {
if !p.IsKnown() {
return "?"
}
return p.lico.lineNumberHTML()
}
// Filename returns the name of the actual file containing this position.
func (p Pos) Filename() string { return p.base.Pos().RelFilename() }
// Base returns the position base.
func (p Pos) Base() *PosBase { return p.base }
// SetBase sets the position base.
func (p *Pos) SetBase(base *PosBase) { p.base = base }
// RelFilename returns the filename recorded with the position's base.
func (p Pos) RelFilename() string { return p.base.Filename() }
// RelLine returns the line number relative to the position's base.
func (p Pos) RelLine() uint {
b := p.base
if b.Line() == 0 {
// base line is unknown => relative line is unknown
return 0
}
return b.Line() + (p.Line() - b.Pos().Line())
}
// RelCol returns the column number relative to the position's base.
func (p Pos) RelCol() uint {
b := p.base
if b.Col() == 0 {
// base column is unknown => relative column is unknown
// (the current specification for line directives requires
// this to apply until the next PosBase/line directive,
// not just until the new newline)
return 0
}
if p.Line() == b.Pos().Line() {
// p on same line as p's base => column is relative to p's base
return b.Col() + (p.Col() - b.Pos().Col())
}
return p.Col()
}
// AbsFilename() returns the absolute filename recorded with the position's base.
func (p Pos) AbsFilename() string { return p.base.AbsFilename() }
// FileIndex returns the file index of the position's base's absolute
// filename within the PosTable that it was registered.
func (p Pos) FileIndex() int { return p.base.FileIndex() }
func (p Pos) String() string {
return p.Format(true, true)
}
// Format formats a position as "filename:line" or "filename:line:column",
// controlled by the showCol flag and if the column is known (!= 0).
// For positions relative to line directives, the original position is
// shown as well, as in "filename:line[origfile:origline:origcolumn] if
// showOrig is set.
func (p Pos) Format(showCol, showOrig bool) string {
buf := new(bytes.Buffer)
p.WriteTo(buf, showCol, showOrig)
return buf.String()
}
// WriteTo a position to w, formatted as Format does.
func (p Pos) WriteTo(w io.Writer, showCol, showOrig bool) {
if !p.IsKnown() {
io.WriteString(w, "<unknown line number>")
return
}
if b := p.base; b == b.Pos().base {
// base is file base (incl. nil)
format(w, p.Filename(), p.Line(), p.Col(), showCol)
return
}
// base is relative
// Print the column only for the original position since the
// relative position's column information may be bogus (it's
// typically generated code and we can't say much about the
// original source at that point but for the file:line info
// that's provided via a line directive).
// TODO(gri) This may not be true if we have an inlining base.
// We may want to differentiate at some point.
format(w, p.RelFilename(), p.RelLine(), p.RelCol(), showCol)
if showOrig {
io.WriteString(w, "[")
format(w, p.Filename(), p.Line(), p.Col(), showCol)
io.WriteString(w, "]")
}
}
// format formats a (filename, line, col) tuple as "filename:line" (showCol
// is false or col == 0) or "filename:line:column" (showCol is true and col != 0).
func format(w io.Writer, filename string, line, col uint, showCol bool) {
io.WriteString(w, filename)
io.WriteString(w, ":")
fmt.Fprint(w, line)
// col == 0 and col == colMax are interpreted as unknown column values
if showCol && 0 < col && col < colMax {
io.WriteString(w, ":")
fmt.Fprint(w, col)
}
}
// formatstr wraps format to return a string.
func formatstr(filename string, line, col uint, showCol bool) string {
buf := new(bytes.Buffer)
format(buf, filename, line, col, showCol)
return buf.String()
}
// ----------------------------------------------------------------------------
// PosBase
// A PosBase encodes a filename and base position.
// Typically, each file and line directive introduce a PosBase.
type PosBase struct {
pos Pos // position at which the relative position is (line, col)
filename string // file name used to open source file, for error messages
absFilename string // absolute file name, for PC-Line tables
line, col uint // relative line, column number at pos
inl int // inlining index (see cmd/internal/obj/inl.go)
fileIndex int // index of absFilename within PosTable.FileTable
}
// NewFileBase returns a new *PosBase for a file with the given (relative and
// absolute) filenames.
func NewFileBase(filename, absFilename string) *PosBase {
base := &PosBase{
filename: filename,
absFilename: absFilename,
line: 1,
col: 1,
inl: -1,
fileIndex: -1,
}
base.pos = MakePos(base, 1, 1)
return base
}
// NewLinePragmaBase returns a new *PosBase for a line directive of the form
//
// //line filename:line:col
// /*line filename:line:col*/
//
// at position pos.
func NewLinePragmaBase(pos Pos, filename, absFilename string, line, col uint) *PosBase {
return &PosBase{pos, filename, absFilename, line, col, -1, -1}
}
// NewInliningBase returns a copy of the orig PosBase with the given inlining
// index. If orig == nil, NewInliningBase panics.
func NewInliningBase(orig *PosBase, inlTreeIndex int) *PosBase {
if orig == nil {
panic("no old PosBase")
}
base := *orig
base.inl = inlTreeIndex
base.fileIndex = -1
if orig == orig.pos.base {
base.pos.base = &base
}
return &base
}
var noPos Pos
// Pos returns the position at which base is located.
// If b == nil, the result is the zero position.
func (b *PosBase) Pos() *Pos {
if b != nil {
return &b.pos
}
return &noPos
}
// Filename returns the filename recorded with the base.
// If b == nil, the result is the empty string.
func (b *PosBase) Filename() string {
if b != nil {
return b.filename
}
return ""
}
// AbsFilename returns the absolute filename recorded with the base.
// If b == nil, the result is the empty string.
func (b *PosBase) AbsFilename() string {
if b != nil {
return b.absFilename
}
return ""
}
// FileSymPrefix is the linker symbol prefix that used to be used for
// linker pseudo-symbols representing file names.
const FileSymPrefix = "gofile.."
// FileIndex returns the index of the base's absolute filename within
// its PosTable's FileTable. It panics if it hasn't been registered
// with a PosTable. If b == nil, the result is -1.
func (b *PosBase) FileIndex() int {
if b != nil {
if b.fileIndex < 0 {
panic("PosBase has no file index")
}
return b.fileIndex
}
return -1
}
// Line returns the line number recorded with the base.
// If b == nil, the result is 0.
func (b *PosBase) Line() uint {
if b != nil {
return b.line
}
return 0
}
// Col returns the column number recorded with the base.
// If b == nil, the result is 0.
func (b *PosBase) Col() uint {
if b != nil {
return b.col
}
return 0
}
// InliningIndex returns the index into the global inlining
// tree recorded with the base. If b == nil or the base has
// not been inlined, the result is < 0.
func (b *PosBase) InliningIndex() int {
if b != nil {
return b.inl
}
return -1
}
// ----------------------------------------------------------------------------
// lico
// A lico is a compact encoding of a LIne and COlumn number.
type lico uint32
// Layout constants: 20 bits for line, 8 bits for column, 2 for isStmt, 2 for pro/epilogue
// (If this is too tight, we can either make lico 64b wide,
// or we can introduce a tiered encoding where we remove column
// information as line numbers grow bigger; similar to what gcc
// does.)
// The bitfield order is chosen to make IsStmt be the least significant
// part of a position; its use is to communicate statement edges through
// instruction scrambling in code generation, not to impose an order.
// TODO: Prologue and epilogue are perhaps better handled as pseudo-ops for the assembler,
// because they have almost no interaction with other uses of the position.
const (
lineBits, lineMax = 20, 1<<lineBits - 2
bogusLine = 1 // Used to disrupt infinite loops to prevent debugger looping
isStmtBits, isStmtMax = 2, 1<<isStmtBits - 1
xlogueBits, xlogueMax = 2, 1<<xlogueBits - 1
colBits, colMax = 32 - lineBits - xlogueBits - isStmtBits, 1<<colBits - 1
isStmtShift = 0
isStmtMask = isStmtMax << isStmtShift
xlogueShift = isStmtBits + isStmtShift
xlogueMask = xlogueMax << xlogueShift
colShift = xlogueBits + xlogueShift
lineShift = colBits + colShift
)
const (
// It is expected that the front end or a phase in SSA will usually generate positions tagged with
// PosDefaultStmt, but note statement boundaries with PosIsStmt. Simple statements will have a single
// boundary; for loops with initialization may have one for their entry and one for their back edge
// (this depends on exactly how the loop is compiled; the intent is to provide a good experience to a
// user debugging a program; the goal is that a breakpoint set on the loop line fires both on entry
// and on iteration). Proper treatment of non-gofmt input with multiple simple statements on a single
// line is TBD.
//
// Optimizing compilation will move instructions around, and some of these will become known-bad as
// step targets for debugging purposes (examples: register spills and reloads; code generated into
// the entry block; invariant code hoisted out of loops) but those instructions will still have interesting
// positions for profiling purposes. To reflect this these positions will be changed to PosNotStmt.
//
// When the optimizer removes an instruction marked PosIsStmt; it should attempt to find a nearby
// instruction with the same line marked PosDefaultStmt to be the new statement boundary. I.e., the
// optimizer should make a best-effort to conserve statement boundary positions, and might be enhanced
// to note when a statement boundary is not conserved.
//
// Code cloning, e.g. loop unrolling or loop unswitching, is an exception to the conservation rule
// because a user running a debugger would expect to see breakpoints active in the copies of the code.
//
// In non-optimizing compilation there is still a role for PosNotStmt because of code generation
// into the entry block. PosIsStmt statement positions should be conserved.
//
// When code generation occurs any remaining default-marked positions are replaced with not-statement
// positions.
//
PosDefaultStmt uint = iota // Default; position is not a statement boundary, but might be if optimization removes the designated statement boundary
PosIsStmt // Position is a statement boundary; if optimization removes the corresponding instruction, it should attempt to find a new instruction to be the boundary.
PosNotStmt // Position should not be a statement boundary, but line should be preserved for profiling and low-level debugging purposes.
)
type PosXlogue uint
const (
PosDefaultLogue PosXlogue = iota
PosPrologueEnd
PosEpilogueBegin
)
func makeLicoRaw(line, col uint) lico {
return lico(line<<lineShift | col<<colShift)
}
// This is a not-position that will not be elided.
// Depending on the debugger (gdb or delve) it may or may not be displayed.
func makeBogusLico() lico {
return makeLicoRaw(bogusLine, 0).withIsStmt()
}
func makeLico(line, col uint) lico {
if line >= lineMax {
// cannot represent line, use max. line so we have some information
line = lineMax
// Drop column information if line number saturates.
// Ensures line+col is monotonic. See issue 51193.
col = 0
}
if col > colMax {
// cannot represent column, use max. column so we have some information
col = colMax
}
// default is not-sure-if-statement
return makeLicoRaw(line, col)
}
func (x lico) Line() uint { return uint(x) >> lineShift }
func (x lico) SameLine(y lico) bool { return 0 == (x^y)&^lico(1<<lineShift-1) }
func (x lico) Col() uint { return uint(x) >> colShift & colMax }
func (x lico) IsStmt() uint {
if x == 0 {
return PosNotStmt
}
return uint(x) >> isStmtShift & isStmtMax
}
func (x lico) Xlogue() PosXlogue {
return PosXlogue(uint(x) >> xlogueShift & xlogueMax)
}
// withNotStmt returns a lico for the same location, but not a statement
func (x lico) withNotStmt() lico {
return x.withStmt(PosNotStmt)
}
// withDefaultStmt returns a lico for the same location, with default isStmt
func (x lico) withDefaultStmt() lico {
return x.withStmt(PosDefaultStmt)
}
// withIsStmt returns a lico for the same location, tagged as definitely a statement
func (x lico) withIsStmt() lico {
return x.withStmt(PosIsStmt)
}
// withXlogue attaches a prologue/epilogue attribute to a lico
func (x lico) withXlogue(xlogue PosXlogue) lico {
if x == 0 {
if xlogue == 0 {
return x
}
// Normalize 0 to "not a statement"
x = lico(PosNotStmt << isStmtShift)
}
return lico(uint(x) & ^uint(xlogueMax<<xlogueShift) | (uint(xlogue) << xlogueShift))
}
// withStmt returns a lico for the same location with specified is_stmt attribute
func (x lico) withStmt(stmt uint) lico {
if x == 0 {
return lico(0)
}
return lico(uint(x) & ^uint(isStmtMax<<isStmtShift) | (stmt << isStmtShift))
}
func (x lico) lineNumber() string {
return fmt.Sprintf("%d", x.Line())
}
func (x lico) lineNumberHTML() string {
if x.IsStmt() == PosDefaultStmt {
return fmt.Sprintf("%d", x.Line())
}
style, pfx := "b", "+"
if x.IsStmt() == PosNotStmt {
style = "s" // /strike not supported in HTML5
pfx = ""
}
return fmt.Sprintf("<%s>%s%d</%s>", style, pfx, x.Line(), style)
}
func (x lico) atColumn1() lico {
return makeLico(x.Line(), 1).withIsStmt()
}