feat(parser): macro expansion, conditionals and include splicing with -I
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+146
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Constant-expression folding for operands. The toolchain's assembler
|
||||
// evaluates arithmetic in every operand position, and macro-heavy GOROOT
|
||||
// sources lean on it: parameterised bodies carry offsets like
|
||||
// ((index*4)+0)(base), immediates like $(32-shift) and masks like
|
||||
// $~63 or $(1<<0|1<<9). Substituting the parameters textually therefore
|
||||
// leaves constant arithmetic behind, and the parser folds it here, keeping
|
||||
// the operand AST identical to what the same literals written out would
|
||||
// produce. Anything that is not a closed integer expression fails to fold
|
||||
// and falls through to the ordinary operand paths.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||
)
|
||||
|
||||
// foldExpr evaluates the constant integer expression at the head of ts and
|
||||
// returns its value together with the unconsumed tokens. ok is false when
|
||||
// the tokens do not form an expression, which is the callers' signal to use
|
||||
// the ordinary parsing paths.
|
||||
func foldExpr(ts []token.Token) (val int64, rest []token.Token, ok bool) {
|
||||
v, rest, ok := foldAdd(ts)
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
return v, rest, true
|
||||
}
|
||||
|
||||
// foldAdd parses addition-level expressions: +, - and | bind loosest, the
|
||||
// Plan 9 convention that makes x<<1|3 read as (x<<1)|3.
|
||||
func foldAdd(ts []token.Token) (int64, []token.Token, bool) {
|
||||
v, rest, ok := foldMul(ts)
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
for len(rest) > 0 {
|
||||
kind := rest[0].Kind
|
||||
if kind != token.Plus && kind != token.Minus && kind != token.Pipe {
|
||||
return v, rest, true
|
||||
}
|
||||
w, r2, ok := foldMul(rest[1:])
|
||||
if !ok {
|
||||
return v, rest, true
|
||||
}
|
||||
switch kind {
|
||||
case token.Plus:
|
||||
v += w
|
||||
case token.Minus:
|
||||
v -= w
|
||||
case token.Pipe:
|
||||
v |= w
|
||||
}
|
||||
rest = r2
|
||||
}
|
||||
return v, rest, true
|
||||
}
|
||||
|
||||
// foldMul parses multiplication-level expressions: *, / and the bit
|
||||
// operators &, << and >>.
|
||||
func foldMul(ts []token.Token) (int64, []token.Token, bool) {
|
||||
v, rest, ok := foldFactor(ts)
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
for len(rest) > 0 {
|
||||
switch rest[0].Kind {
|
||||
case token.Star:
|
||||
w, r2, ok := foldFactor(rest[1:])
|
||||
if !ok {
|
||||
return v, rest, true
|
||||
}
|
||||
v *= w
|
||||
rest = r2
|
||||
case token.Slash:
|
||||
w, r2, ok := foldFactor(rest[1:])
|
||||
if !ok || w == 0 {
|
||||
return v, rest, true
|
||||
}
|
||||
v /= w
|
||||
rest = r2
|
||||
case token.Ampersand:
|
||||
w, r2, ok := foldFactor(rest[1:])
|
||||
if !ok {
|
||||
return v, rest, true
|
||||
}
|
||||
v &= w
|
||||
rest = r2
|
||||
case token.LShift:
|
||||
w, r2, ok := foldFactor(rest[1:])
|
||||
if !ok || w < 0 || w >= 64 {
|
||||
return v, rest, true
|
||||
}
|
||||
v <<= uint(w)
|
||||
rest = r2
|
||||
case token.RShift:
|
||||
w, r2, ok := foldFactor(rest[1:])
|
||||
if !ok || w < 0 || w >= 64 {
|
||||
return v, rest, true
|
||||
}
|
||||
v >>= uint(w)
|
||||
rest = r2
|
||||
default:
|
||||
return v, rest, true
|
||||
}
|
||||
}
|
||||
return v, rest, true
|
||||
}
|
||||
|
||||
// foldFactor parses a number, a parenthesised expression, or a unary sign
|
||||
// or complement.
|
||||
func foldFactor(ts []token.Token) (int64, []token.Token, bool) {
|
||||
if len(ts) == 0 {
|
||||
return 0, ts, false
|
||||
}
|
||||
switch ts[0].Kind {
|
||||
case token.Number:
|
||||
v, ok := tryInt(ts[0].Text)
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
return v, ts[1:], true
|
||||
case token.LParen:
|
||||
v, rest, ok := foldAdd(ts[1:])
|
||||
if !ok || len(rest) == 0 || rest[0].Kind != token.RParen {
|
||||
return 0, ts, false
|
||||
}
|
||||
return v, rest[1:], true
|
||||
case token.Minus:
|
||||
v, rest, ok := foldFactor(ts[1:])
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
return -v, rest, true
|
||||
case token.Plus:
|
||||
return foldFactor(ts[1:])
|
||||
case token.Tilde:
|
||||
v, rest, ok := foldFactor(ts[1:])
|
||||
if !ok {
|
||||
return 0, ts, false
|
||||
}
|
||||
return ^v, rest, true
|
||||
}
|
||||
return 0, ts, false
|
||||
}
|
||||
@@ -408,6 +408,18 @@ func parseImmediate(g []token.Token) ast.Immediate {
|
||||
return imm
|
||||
}
|
||||
}
|
||||
// A constant expression introduced by '(' or '~'. Textual macro
|
||||
// substitution leaves arithmetic such as $(32-shift) and $~63 behind,
|
||||
// and the toolchain evaluates it in place; only shapes the ordinary
|
||||
// paths below cannot read reach the folder, so every existing form
|
||||
// keeps its exact parse.
|
||||
if g[0].Kind == token.LParen || g[0].Kind == token.Tilde {
|
||||
if v, rest, ok := foldExpr(g); ok && len(rest) == 0 {
|
||||
imm.Val = v
|
||||
imm.HasVal = true
|
||||
return imm
|
||||
}
|
||||
}
|
||||
i := 0
|
||||
if g[i].Kind == token.Minus {
|
||||
imm.Neg = true
|
||||
@@ -459,6 +471,17 @@ func parseAddress(g []token.Token) ast.Address {
|
||||
}
|
||||
|
||||
i := 0
|
||||
// A parenthesised constant expression as the displacement: substituted
|
||||
// macro bodies carry ((index*4)+0)(base) shapes. As with the signed
|
||||
// number path below, the value is committed only when a base group
|
||||
// follows.
|
||||
if i < len(g) && g[i].Kind == token.LParen {
|
||||
if v, rest, ok := foldExpr(g[i:]); ok && len(rest) > 0 && rest[0].Kind == token.LParen {
|
||||
addr.Offset = v
|
||||
addr.HasOff = true
|
||||
i = len(g) - len(rest)
|
||||
}
|
||||
}
|
||||
// Optional leading displacement before a '(' base group. A sign pushes
|
||||
// the parenthesis one token further out: -4(DX) has it at i+2.
|
||||
if isSignedNumber(g, i) {
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The preprocessor turns #define and #include directives into the token
|
||||
// stream the parser really sees, the way the Go toolchain's assembler does:
|
||||
// object and parameterised macros expand at the point of use, and an
|
||||
// #include splices the named file's lines in place of the directive. The
|
||||
// pass runs only on the assembly path (gasm asm, diff, the corpus audit),
|
||||
// where the result is machine code; parsing for the linter, formatter and
|
||||
// language server keeps the raw file so their view of #define lines, and
|
||||
// therefore their macro-aware behaviour, is unchanged.
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/token"
|
||||
)
|
||||
|
||||
// Options controls the optional preprocessing applied before a file is
|
||||
// parsed. The zero value reproduces Parse exactly.
|
||||
type Options struct {
|
||||
// IncludeDirs lists the -I directories searched for #include files,
|
||||
// in order, after the including file's own directory.
|
||||
IncludeDirs []string
|
||||
// Expand enables macro expansion, include splicing and the
|
||||
// statement-separator reading of ';' that the expanded bodies rely on.
|
||||
Expand bool
|
||||
}
|
||||
|
||||
// ParseWithOptions parses src like Parse, optionally preprocessing it first.
|
||||
// The returned file is usable even when errors is non-empty.
|
||||
func ParseWithOptions(path, src string, opts Options) (*ast.File, []error) {
|
||||
tokens := lexer.Tokenize(src)
|
||||
var lines [][]token.Token
|
||||
var errs []error
|
||||
if opts.Expand {
|
||||
pp := &preproc{opts: opts, macros: map[string]*macroDef{}}
|
||||
lines = pp.fileLines(path, tokens, token.Position{})
|
||||
errs = pp.errs
|
||||
} else {
|
||||
lines = splitLines(tokens)
|
||||
}
|
||||
p := &state{path: path}
|
||||
p.parse(lines)
|
||||
return p.file, append(errs, p.errs...)
|
||||
}
|
||||
|
||||
// maxExpansionDepth bounds recursive macro expansion; the toolchain's
|
||||
// assembler gives up after 100 nested invocations without producing a token.
|
||||
const maxExpansionDepth = 100
|
||||
|
||||
// textflagHeader names the one header gasm does not splice: its flag macros
|
||||
// (NOSPLIT, RODATA, …) are consumed by name throughout gasm's parser,
|
||||
// encoders and linter, and expanding them to their numeric constants would
|
||||
// leave every consumer blind to them.
|
||||
const textflagHeader = "textflag.h"
|
||||
|
||||
// macroDef is one #define. A nil args slice is an object macro; a non-nil
|
||||
// (possibly empty) one is parameterised, the C distinction between
|
||||
// "#define A(x)" and "#define A (x)".
|
||||
type macroDef struct {
|
||||
name string
|
||||
args []string
|
||||
body []token.Token
|
||||
}
|
||||
|
||||
// preproc carries the state of one expansion pass: the live macro table, the
|
||||
// chain of files currently being read, for cycle detection, and the
|
||||
// conditional-inclusion stack of #ifdef regions.
|
||||
type preproc struct {
|
||||
opts Options
|
||||
macros map[string]*macroDef
|
||||
errs []error
|
||||
stack []string // absolute paths of files being read, innermost last
|
||||
ifdefStack []bool // one entry per open #ifdef/#ifndef, its truth
|
||||
}
|
||||
|
||||
// enabled reports whether the position being read is inside a live
|
||||
// conditional branch. Directives inside a disabled branch contribute
|
||||
// nothing, and its content lines are dropped, exactly as the toolchain's
|
||||
// input stack does.
|
||||
func (pp *preproc) enabled() bool {
|
||||
return len(pp.ifdefStack) == 0 || pp.ifdefStack[len(pp.ifdefStack)-1]
|
||||
}
|
||||
|
||||
func (pp *preproc) errorf(pos token.Position, format string, args ...any) {
|
||||
pp.errs = append(pp.errs, Error{Pos: pos, Msg: fmt.Sprintf(format, args...)})
|
||||
}
|
||||
|
||||
// fileLines tokenizes and preprocesses one file into logical lines.
|
||||
// Directive lines are kept (the parser records them for the tooling);
|
||||
// #include lines are replaced by the included file's lines. includePos is
|
||||
// the position of the #include that pulled this file in, zero for the
|
||||
// top-level file, and only serves cycle diagnostics.
|
||||
func (pp *preproc) fileLines(path string, tokens []token.Token, includePos token.Position) [][]token.Token {
|
||||
abs, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
abs = filepath.Clean(path)
|
||||
}
|
||||
if slices.Contains(pp.stack, abs) {
|
||||
if includePos.IsValid() {
|
||||
pp.errorf(includePos, "#include %q: include cycle (%s is already being read)", path, filepath.Base(path))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
pp.stack = append(pp.stack, abs)
|
||||
|
||||
var out [][]token.Token
|
||||
for _, line := range splitLines(tokens) {
|
||||
if len(line) == 0 {
|
||||
out = append(out, line)
|
||||
continue
|
||||
}
|
||||
if line[0].Kind == token.Hash {
|
||||
out = append(out, pp.directive(line, filepath.Dir(path))...)
|
||||
continue
|
||||
}
|
||||
if !pp.enabled() {
|
||||
continue
|
||||
}
|
||||
out = append(out, splitOnSemicolons(pp.expandTokens(line))...)
|
||||
}
|
||||
pp.stack = pp.stack[:len(pp.stack)-1]
|
||||
if len(pp.stack) == 0 && len(pp.ifdefStack) > 0 {
|
||||
// The stack is per-input, shared across includes, so only the
|
||||
// top-level file's end can decide the input was left unclosed.
|
||||
pp.errorf(token.Position{Line: 1, Column: 1}, "unclosed #ifdef or #ifndef")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// directive processes one '#' line and returns the lines to keep in the
|
||||
// stream: every directive line is kept as-is for the parser (which records
|
||||
// it), except #include, which is replaced by the spliced content.
|
||||
// Conditionals are tracked on every line; every other directive is inert
|
||||
// inside a disabled branch.
|
||||
func (pp *preproc) directive(line []token.Token, dir string) [][]token.Token {
|
||||
if len(line) < 2 || line[1].Kind != token.Ident {
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
switch line[1].Text {
|
||||
case "ifdef", "ifndef":
|
||||
pp.ifdef(line, line[1].Text == "ifndef")
|
||||
case "else":
|
||||
pp.elseBranch(line)
|
||||
case "endif":
|
||||
pp.endif(line)
|
||||
case "define":
|
||||
if pp.enabled() {
|
||||
pp.define(line)
|
||||
}
|
||||
case "undef":
|
||||
if pp.enabled() {
|
||||
pp.undef(line)
|
||||
}
|
||||
case "include":
|
||||
if pp.enabled() {
|
||||
return pp.include(line, dir)
|
||||
}
|
||||
default:
|
||||
// #line and unknown directives are recorded but not interpreted:
|
||||
// conservative support keeps the parser's view intact and files
|
||||
// using them fail on their content, not silently.
|
||||
}
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
|
||||
// ifdef handles "#ifdef NAME" and "#ifndef NAME", pushing the branch's truth
|
||||
// onto the conditional stack. A branch opened inside a disabled region is
|
||||
// itself disabled, however the name resolves.
|
||||
func (pp *preproc) ifdef(line []token.Token, inverted bool) {
|
||||
truth := false
|
||||
if len(line) >= 3 && line[2].Kind == token.Ident {
|
||||
_, defined := pp.macros[line[2].Text]
|
||||
truth = defined != inverted
|
||||
} else {
|
||||
pp.errorf(line[0].Pos, "expected identifier after #%s", line[1].Text)
|
||||
}
|
||||
if !pp.enabled() {
|
||||
truth = false
|
||||
}
|
||||
pp.ifdefStack = append(pp.ifdefStack, truth)
|
||||
}
|
||||
|
||||
// elseBranch flips the innermost conditional's truth, but only when the
|
||||
// region enclosing it is itself live: the toolchain keeps outer overrides.
|
||||
func (pp *preproc) elseBranch(line []token.Token) {
|
||||
if len(pp.ifdefStack) == 0 {
|
||||
pp.errorf(line[0].Pos, "unmatched #else")
|
||||
return
|
||||
}
|
||||
if len(pp.ifdefStack) == 1 || pp.ifdefStack[len(pp.ifdefStack)-2] {
|
||||
pp.ifdefStack[len(pp.ifdefStack)-1] = !pp.ifdefStack[len(pp.ifdefStack)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// endif closes the innermost conditional.
|
||||
func (pp *preproc) endif(line []token.Token) {
|
||||
if len(pp.ifdefStack) == 0 {
|
||||
pp.errorf(line[0].Pos, "unmatched #endif")
|
||||
return
|
||||
}
|
||||
pp.ifdefStack = pp.ifdefStack[:len(pp.ifdefStack)-1]
|
||||
}
|
||||
|
||||
// define parses "#define NAME[(formals)] body" into the macro table. The
|
||||
// body runs to the end of the logical line (the lexer has already spliced
|
||||
// backslash continuations) and stops at a comment, which never expands.
|
||||
func (pp *preproc) define(line []token.Token) {
|
||||
if len(line) < 3 || line[2].Kind != token.Ident {
|
||||
return
|
||||
}
|
||||
name := line[2]
|
||||
args := []string(nil)
|
||||
body := line[3:]
|
||||
// The definition is parameterised only when '(' follows the name
|
||||
// directly; the toolchain separates "#define A(x)" from
|
||||
// "#define A (x)" by adjacency, and so does the column check here.
|
||||
if len(body) > 0 && body[0].Kind == token.LParen &&
|
||||
body[0].Pos.Column == name.Pos.Column+utf8.RuneCountInString(name.Text) {
|
||||
args = []string{}
|
||||
i := 1
|
||||
for i < len(body) && body[i].Kind != token.RParen {
|
||||
if body[i].Kind == token.Ident {
|
||||
args = append(args, body[i].Text)
|
||||
}
|
||||
i++
|
||||
}
|
||||
if i < len(body) {
|
||||
body = body[i+1:]
|
||||
} else {
|
||||
body = nil
|
||||
}
|
||||
}
|
||||
if i := slices.IndexFunc(body, func(t token.Token) bool { return t.Kind == token.Comment }); i >= 0 {
|
||||
body = body[:i]
|
||||
}
|
||||
if _, exists := pp.macros[name.Text]; exists {
|
||||
// The toolchain refuses redefinition, so a file the oracle accepts
|
||||
// never redefines; failing here keeps that contract visible.
|
||||
pp.errorf(name.Pos, "redefinition of macro %s", name.Text)
|
||||
}
|
||||
pp.macros[name.Text] = ¯oDef{name: name.Text, args: args, body: pp.bodyWithBreaks(body)}
|
||||
|
||||
}
|
||||
|
||||
// bodyWithBreaks records the statement boundaries the continuations carry.
|
||||
// The lexer splices backslash-continued lines into one logical line, but the
|
||||
// toolchain keeps the newline as a token in the stored body, which is how a
|
||||
// multi-instruction body without semicolons (the arm64 style) still splits
|
||||
// into statements on expansion. A line change inside the logical line is
|
||||
// exactly a continuation, so the boundary is restored from the positions.
|
||||
func (pp *preproc) bodyWithBreaks(body []token.Token) []token.Token {
|
||||
out := make([]token.Token, 0, len(body))
|
||||
for i, t := range body {
|
||||
if i > 0 && t.Pos.Line != body[i-1].Pos.Line {
|
||||
out = append(out, token.Token{Kind: token.Newline, Text: "\n", Pos: t.Pos, End: t.Pos})
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// undef handles "#undef NAME", which the toolchain honours and requires to
|
||||
// name a defined macro.
|
||||
func (pp *preproc) undef(line []token.Token) {
|
||||
if len(line) < 3 || line[2].Kind != token.Ident {
|
||||
return
|
||||
}
|
||||
if _, ok := pp.macros[line[2].Text]; !ok {
|
||||
pp.errorf(line[2].Pos, "#undef for undefined macro %s", line[2].Text)
|
||||
return
|
||||
}
|
||||
delete(pp.macros, line[2].Text)
|
||||
}
|
||||
|
||||
// include resolves and splices "#include \"file\"". A header that cannot be
|
||||
// read keeps the directive line in the stream, with a diagnostic.
|
||||
func (pp *preproc) include(line []token.Token, dir string) [][]token.Token {
|
||||
if len(line) < 3 || line[2].Kind != token.String {
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
header := line[2]
|
||||
name, err := strconv.Unquote(header.Text)
|
||||
if err != nil {
|
||||
pp.errorf(header.Pos, "unquoting include file name: %v", err)
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
if filepath.Base(name) == textflagHeader {
|
||||
// Flag macros are handled natively (see textflagHeader); the
|
||||
// directive stays so tools still see the include.
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
resolved, ok := pp.resolve(name, dir)
|
||||
if !ok {
|
||||
searched := append([]string{dir}, pp.opts.IncludeDirs...)
|
||||
pp.errorf(header.Pos, "#include %q: file not found (searched %s)", name, strings.Join(searched, ", "))
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
src, err := os.ReadFile(resolved)
|
||||
if err != nil {
|
||||
pp.errorf(header.Pos, "#include %q: %v", name, err)
|
||||
return [][]token.Token{line}
|
||||
}
|
||||
return pp.fileLines(resolved, lexer.Tokenize(string(src)), header.Pos)
|
||||
}
|
||||
|
||||
// resolve looks an include name up the way the toolchain does: as written
|
||||
// (relative to the working directory), then relative to the including
|
||||
// file's directory, then in each -I directory in order.
|
||||
func (pp *preproc) resolve(name, dir string) (string, bool) {
|
||||
candidates := []string{name}
|
||||
if !filepath.IsAbs(name) {
|
||||
candidates = append(candidates, filepath.Join(dir, name))
|
||||
for _, d := range pp.opts.IncludeDirs {
|
||||
candidates = append(candidates, filepath.Join(d, name))
|
||||
}
|
||||
}
|
||||
for _, c := range candidates {
|
||||
if st, err := os.Stat(c); err == nil && !st.IsDir() {
|
||||
return c, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
// expandTokens expands every macro invocation in a token sequence,
|
||||
// recursively, with a depth guard. A body is spliced into the sequence in
|
||||
// place and rescanned, the way the toolchain's input stack re-reads pushed
|
||||
// tokens: an object macro may name a parameterised one, and the argument
|
||||
// list of the expansion may then come from the tokens that follow.
|
||||
func (pp *preproc) expandTokens(in []token.Token) []token.Token {
|
||||
s := in
|
||||
i := 0
|
||||
consecutive := 0
|
||||
for i < len(s) {
|
||||
t := s[i]
|
||||
if t.Kind != token.Ident {
|
||||
i++
|
||||
consecutive = 0
|
||||
continue
|
||||
}
|
||||
def := pp.macros[t.Text]
|
||||
if def == nil {
|
||||
i++
|
||||
consecutive = 0
|
||||
continue
|
||||
}
|
||||
// The guard mirrors the toolchain's: 100 nested invocations in a
|
||||
// row without a plain token between them means recursion.
|
||||
consecutive++
|
||||
if consecutive > maxExpansionDepth {
|
||||
pp.errorf(t.Pos, "recursive macro invocation (deeper than %d levels)", maxExpansionDepth)
|
||||
return nil
|
||||
}
|
||||
if def.args == nil {
|
||||
s = append(s[:i], append(restamp(def.body, t.Pos), s[i+1:]...)...)
|
||||
continue
|
||||
}
|
||||
// A parameterised macro invoked without its parentheses stands
|
||||
// unexpanded, naming itself, as in the toolchain.
|
||||
if i+1 >= len(s) || s[i+1].Kind != token.LParen {
|
||||
i++
|
||||
consecutive = 0
|
||||
continue
|
||||
}
|
||||
args, next := pp.collectArgs(s, i+1, t)
|
||||
if args == nil {
|
||||
return nil
|
||||
}
|
||||
// A zero-argument macro may be invoked as NAME().
|
||||
if len(def.args) == 0 && len(args) == 1 && len(args[0]) == 0 {
|
||||
args = nil
|
||||
}
|
||||
if len(args) != len(def.args) {
|
||||
pp.errorf(t.Pos, "wrong arg count for macro %s: got %d, want %d", t.Text, len(args), len(def.args))
|
||||
i = next
|
||||
consecutive = 0
|
||||
continue
|
||||
}
|
||||
sub := make([]token.Token, 0, len(def.body))
|
||||
for _, bt := range def.body {
|
||||
if bt.Kind == token.Ident {
|
||||
if k := slices.Index(def.args, bt.Text); k >= 0 {
|
||||
sub = append(sub, restamp(args[k], t.Pos)...)
|
||||
continue
|
||||
}
|
||||
}
|
||||
sub = append(sub, bt)
|
||||
}
|
||||
s = append(s[:i], append(sub, s[next:]...)...)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// collectArgs reads the actual argument tokens of an invocation; the opening
|
||||
// parenthesis is at start. Commas separate arguments except inside nested
|
||||
// parentheses. A nil result means the list was unterminated, which is a
|
||||
// diagnostic.
|
||||
func (pp *preproc) collectArgs(in []token.Token, start int, name token.Token) ([][]token.Token, int) {
|
||||
var args [][]token.Token
|
||||
var cur []token.Token
|
||||
nesting := 0
|
||||
for i := start + 1; i < len(in); i++ {
|
||||
t := in[i]
|
||||
switch t.Kind {
|
||||
case token.LParen:
|
||||
nesting++
|
||||
cur = append(cur, t)
|
||||
case token.RParen:
|
||||
if nesting == 0 {
|
||||
return append(args, cur), i + 1
|
||||
}
|
||||
nesting--
|
||||
cur = append(cur, t)
|
||||
case token.Comma:
|
||||
if nesting == 0 {
|
||||
args = append(args, cur)
|
||||
cur = nil
|
||||
continue
|
||||
}
|
||||
cur = append(cur, t)
|
||||
case token.Comment:
|
||||
pp.errorf(name.Pos, "unterminated arg list invoking macro %s", name.Text)
|
||||
return nil, i
|
||||
default:
|
||||
cur = append(cur, t)
|
||||
}
|
||||
}
|
||||
pp.errorf(name.Pos, "unterminated arg list invoking macro %s", name.Text)
|
||||
return nil, len(in)
|
||||
}
|
||||
|
||||
// restamp copies body tokens to the invocation's position, so diagnostics
|
||||
// and the line table point where the macro was used, as the toolchain's
|
||||
// input stack does.
|
||||
func restamp(body []token.Token, pos token.Position) []token.Token {
|
||||
out := make([]token.Token, len(body))
|
||||
for i, t := range body {
|
||||
t.Pos, t.End = pos, pos
|
||||
out[i] = t
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// splitOnSemicolons breaks a token sequence at ';' statement separators and
|
||||
// at the Newline markers that record continuation boundaries inside macro
|
||||
// bodies, producing the logical lines the parser expects. The separators
|
||||
// carry no meaning beyond the break, so the pieces are exactly what the same
|
||||
// statements on separate lines would produce.
|
||||
func splitOnSemicolons(ts []token.Token) [][]token.Token {
|
||||
var out [][]token.Token
|
||||
start := 0
|
||||
for i, t := range ts {
|
||||
if t.Kind == token.Semicolon || t.Kind == token.Newline {
|
||||
if i > start {
|
||||
out = append(out, ts[start:i])
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
if start < len(ts) {
|
||||
out = append(out, ts[start:])
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,552 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
|
||||
)
|
||||
|
||||
// expand parses src with preprocessing enabled and returns the first TEXT's
|
||||
// body instructions as "MNEMONIC operand|operand" strings, the shape the
|
||||
// expansion assertions below compare against. Runs of spaces are
|
||||
// collapsed: Raw renders a token group as its tokens joined with single
|
||||
// spaces, so "$(32-7)" arrives as "$ ( 32 - 7 )" and the comparison must
|
||||
// not depend on that spelling.
|
||||
func expand(t *testing.T, src string) (*ast.File, []string) {
|
||||
t.Helper()
|
||||
f, errs := ParseWithOptions("t_amd64.s", src, Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
ts := texts(f)
|
||||
if len(ts) == 0 {
|
||||
t.Fatalf("no TEXT in:\n%s", src)
|
||||
}
|
||||
var got []string
|
||||
for _, s := range ts[0].Body {
|
||||
in, ok := s.(*ast.Instr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
var ops []string
|
||||
for _, op := range in.Operands {
|
||||
ops = append(ops, op.Raw)
|
||||
}
|
||||
line := in.Mnemonic.Text + " " + strings.Join(ops, ", ")
|
||||
got = append(got, strings.ReplaceAll(line, " ", ""))
|
||||
}
|
||||
return f, got
|
||||
}
|
||||
|
||||
func wantLines(t *testing.T, got []string, want ...string) {
|
||||
t.Helper()
|
||||
strip := func(lines []string) string {
|
||||
var out []string
|
||||
for _, l := range lines {
|
||||
out = append(out, strings.ReplaceAll(l, " ", ""))
|
||||
}
|
||||
return strings.Join(out, "\n")
|
||||
}
|
||||
if strip(got) != strip(want) {
|
||||
t.Errorf("expanded body:\n %s\nwant:\n %s", strings.Join(got, "\n "), strings.Join(want, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
func TestObjectMacroExpandsAtUse(t *testing.T) {
|
||||
_, got := expand(t, `
|
||||
#define REGTMP CX
|
||||
#define TWICE ADDQ CX, AX; ADDQ CX, AX
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVQ 8(SP), REGTMP
|
||||
TWICE
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"MOVQ 8(SP), CX",
|
||||
"ADDQ CX, AX",
|
||||
"ADDQ CX, AX",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestParameterisedMacroSubstitutesArguments(t *testing.T) {
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
#define ROUND1(a, index, const, shift) \
|
||||
ADDQ $const, a; \
|
||||
MOVW (index*4)(SP), a; \
|
||||
RORQ $(32-shift), a
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
ROUND1(AX, 3, 0xd76aa478, 7)
|
||||
RET
|
||||
`, Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
body := texts(f)[0].Body
|
||||
add := body[0].(*ast.Instr)
|
||||
if add.Mnemonic.Text != "ADDQ" || !add.Operands[0].Imm.HasVal ||
|
||||
add.Operands[0].Imm.Val != 0xd76aa478 || add.Operands[1].Addr.Sym == nil ||
|
||||
add.Operands[1].Addr.Sym.Name != "AX" {
|
||||
t.Errorf("ADDQ operands substituted wrong: %+v %+v", add.Operands[0].Imm, add.Operands[1].Addr)
|
||||
}
|
||||
mov := body[1].(*ast.Instr)
|
||||
if addr := mov.Operands[0].Addr; !addr.HasOff || addr.Offset != 12 {
|
||||
t.Errorf("MOVW offset = %+v, want 12 from 3*4", addr)
|
||||
}
|
||||
ror := body[2].(*ast.Instr)
|
||||
if !ror.Operands[0].Imm.HasVal || ror.Operands[0].Imm.Val != 25 {
|
||||
t.Errorf("RORQ immediate = %+v, want 25 from (32-7)", ror.Operands[0].Imm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMacroArgumentsKeepCommasInParens(t *testing.T) {
|
||||
// An argument may itself be an unparenthesised expression: the tokens
|
||||
// substitute verbatim and the parser folds the result, as the
|
||||
// toolchain's parser does.
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
#define LOAD(dst, off) MOVQ off(SP), dst
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
LOAD(AX, 1*8)
|
||||
RET
|
||||
`, Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
in := texts(f)[0].Body[0].(*ast.Instr)
|
||||
addr := in.Operands[0].Addr
|
||||
if !addr.HasOff || addr.Offset != 8 {
|
||||
t.Errorf("offset = %+v, want 8", addr)
|
||||
}
|
||||
if sym := in.Operands[1].Addr.Sym; sym == nil || sym.Name != "AX" {
|
||||
t.Errorf("destination = %+v, want AX", in.Operands[1].Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedMacroInvocations(t *testing.T) {
|
||||
// An object macro naming a parameterised one, and a parameterised body
|
||||
// invoking another parameterised macro: the toolchain's input stack
|
||||
// rescans substituted tokens, and so does expansion here.
|
||||
_, got := expand(t, `
|
||||
#define DOUBLE(x) ADDQ x, x
|
||||
#define TWICE2 DOUBLE
|
||||
#define FOUR(a, b) DOUBLE(a); DOUBLE(b)
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
TWICE2(AX)
|
||||
FOUR(AX, CX)
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"ADDQ AX, AX",
|
||||
"ADDQ AX, AX",
|
||||
"ADDQ CX, CX",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestMultiLineBodySplitsWithoutSemicolons(t *testing.T) {
|
||||
// The arm64 style: backslash-continued lines with no semicolons. The
|
||||
// continuation newline is a statement boundary, as in the toolchain.
|
||||
_, got := expand(t, `
|
||||
#define PAIR \
|
||||
ADDQ AX, AX \
|
||||
MOVQ AX, CX
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
PAIR
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"ADDQ AX, AX",
|
||||
"MOVQ AX, CX",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestZeroArgumentMacro(t *testing.T) {
|
||||
_, got := expand(t, `
|
||||
#define BARRIER()
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
BARRIER()
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got, "RET")
|
||||
}
|
||||
|
||||
func TestParameterisedWithoutParensStandsAsName(t *testing.T) {
|
||||
// A parameterised macro invoked without its parentheses names itself,
|
||||
// which the parser then reports as an unknown instruction rather than
|
||||
// silently expanding nothing.
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
#define M(x) ADDQ x, x
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
M
|
||||
RET
|
||||
`, Options{Expand: true})
|
||||
if len(errs) != 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
fn := texts(f)[0]
|
||||
if len(fn.Body) == 0 {
|
||||
t.Fatal("body empty")
|
||||
}
|
||||
in, ok := fn.Body[0].(*ast.Instr)
|
||||
if !ok || in.Mnemonic.Text != "M" {
|
||||
t.Fatalf("bare parameterised macro did not stand as its name: %+v", fn.Body[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefinitionScoping(t *testing.T) {
|
||||
// A definition applies from its point onward: the use before the
|
||||
// #define stays untouched.
|
||||
_, got := expand(t, `
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
SPECIAL
|
||||
#define SPECIAL ADDQ AX, AX
|
||||
SPECIAL
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"SPECIAL",
|
||||
"ADDQ AX, AX",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestUndefRemovesMacro(t *testing.T) {
|
||||
_, got := expand(t, `
|
||||
#define TEMP AX
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
TEMP
|
||||
#undef TEMP
|
||||
TEMP
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"AX",
|
||||
"TEMP",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestUndefUndefinedMacroIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#undef NOSUCH\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "undefined macro NOSUCH") {
|
||||
t.Fatalf("#undef of an undefined macro: got %v, want an error naming it", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRedefinitionIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#define A X\n#define A Y\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "redefinition of macro A") {
|
||||
t.Fatalf("redefinition: got %v, want an error", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecursiveMacroIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#define A B\n#define B A\nTEXT ·f(SB), NOSPLIT, $0\n\tA\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "recursive macro invocation") {
|
||||
t.Fatalf("recursion: got %v, want a recursive-macro error, not a hang", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrongArgumentCountIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#define M(a, b) ADDQ a, b\nTEXT ·f(SB), NOSPLIT, $0\n\tM(AX)\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "wrong arg count for macro M") {
|
||||
t.Fatalf("arg count: got %v, want an error", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConditionalsSelectOneBranch(t *testing.T) {
|
||||
_, got := expand(t, `
|
||||
#define MODE2
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
#ifdef MODE2
|
||||
ADDQ AX, AX
|
||||
#else
|
||||
SUBQ AX, AX
|
||||
#endif
|
||||
#ifndef MODE2
|
||||
SUBQ CX, CX
|
||||
#else
|
||||
ADDQ CX, CX
|
||||
#endif
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got,
|
||||
"ADDQ AX, AX",
|
||||
"ADDQ CX, CX",
|
||||
"RET",
|
||||
)
|
||||
}
|
||||
|
||||
func TestConditionalsHideDefinitionsAndIncludes(t *testing.T) {
|
||||
// A definition inside a disabled branch must not exist, and an
|
||||
// unresolvable include there must not be followed.
|
||||
_, got := expand(t, `
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
#ifdef NOTDEFINED
|
||||
#define HIDEN ADDQ AX, AX
|
||||
#include "nowhere.h"
|
||||
#endif
|
||||
HIDEN
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got, "HIDEN", "RET")
|
||||
}
|
||||
|
||||
func TestUnclosedConditionalIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#ifdef X\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unclosed #ifdef") {
|
||||
t.Fatalf("unclosed conditional: got %v, want an error", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmatchedConditionalDelimitersAreErrors(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#endif\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unmatched #endif") {
|
||||
t.Fatalf("unmatched #endif: got %v, want an error", errs)
|
||||
}
|
||||
_, errs = ParseWithOptions("t_amd64.s", "#else\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n", Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "unmatched #else") {
|
||||
t.Fatalf("unmatched #else: got %v, want an error", errs)
|
||||
}
|
||||
}
|
||||
|
||||
// includeTree writes a directory of include files and returns its path.
|
||||
func includeTree(t *testing.T, files map[string]string) string {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
for name, content := range files {
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
func TestIncludeSplicesAndDefinesAreShared(t *testing.T) {
|
||||
dir := includeTree(t, map[string]string{
|
||||
"consts.h": "#define KONST $42\n",
|
||||
})
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
#include "consts.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
MOVQ KONST, AX
|
||||
RET
|
||||
`, Options{Expand: true, IncludeDirs: []string{dir}})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
in := texts(f)[0].Body[0].(*ast.Instr)
|
||||
if in.Mnemonic.Text != "MOVQ" || strings.ReplaceAll(in.Operands[0].Raw, " ", "") != "$42" {
|
||||
t.Fatalf("include splicing failed: %+v", in)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeResolutionOrder(t *testing.T) {
|
||||
// The including file's directory wins over the -I list, and the -I list
|
||||
// is searched in order.
|
||||
src := includeTree(t, map[string]string{
|
||||
"inc/main.s": "#include \"which.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||
"inc/which.h": "#define WHO ONE\n",
|
||||
"first/which.h": "#define WHO TWO\n",
|
||||
"second/which.h": "#define WHO THREE\n",
|
||||
})
|
||||
main := filepath.Join(src, "inc", "main.s")
|
||||
body, err := os.ReadFile(main)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The header exists in the including file's directory and in two -I
|
||||
// directories; the source-directory copy must win.
|
||||
f, errs := ParseWithOptions(main, string(body), Options{Expand: true, IncludeDirs: []string{
|
||||
filepath.Join(src, "first"), filepath.Join(src, "second"),
|
||||
}})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
found := false
|
||||
for _, d := range f.Decls {
|
||||
if pp, ok := d.(*ast.Preproc); ok && strings.Contains(pp.Raw, "define WHO ONE") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Error("the including file's directory did not win include resolution")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeSearchesIncludeDirsInOrder(t *testing.T) {
|
||||
src := includeTree(t, map[string]string{
|
||||
"inc/main.s": "#include \"which.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||
"first/which.h": "#define WHO TWO\n",
|
||||
"second/which.h": "#define WHO THREE\n",
|
||||
})
|
||||
main := filepath.Join(src, "inc", "main.s")
|
||||
body, err := os.ReadFile(main)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f, errs := ParseWithOptions(main, string(body), Options{Expand: true, IncludeDirs: []string{
|
||||
filepath.Join(src, "first"), filepath.Join(src, "second"),
|
||||
}})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
for _, d := range f.Decls {
|
||||
if pp, ok := d.(*ast.Preproc); ok && strings.Contains(pp.Raw, "define WHO THREE") {
|
||||
t.Error("the second -I directory was searched before the first")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncludeCycleIsDetected(t *testing.T) {
|
||||
src := includeTree(t, map[string]string{
|
||||
"a.s": "#include \"b.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||
"b.s": "#include \"a.s\"\n",
|
||||
})
|
||||
_, errs := ParseWithOptions(filepath.Join(src, "a.s"), "#include \"b.s\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||
Options{Expand: true})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), "include cycle") {
|
||||
t.Fatalf("include cycle: got %v, want a cycle diagnostic, not a hang", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnresolvableIncludeIsAnError(t *testing.T) {
|
||||
_, errs := ParseWithOptions("t_amd64.s", "#include \"nothere.h\"\nTEXT ·f(SB), NOSPLIT, $0\n\tRET\n",
|
||||
Options{Expand: true, IncludeDirs: []string{t.TempDir()}})
|
||||
if len(errs) == 0 || !strings.Contains(errs[0].Error(), `#include "nothere.h"`) {
|
||||
t.Fatalf("missing include: got %v, want a clear diagnostic", errs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTextflagHeaderIsNeverSpliced(t *testing.T) {
|
||||
// textflag.h resolves nowhere here, yet the file must parse: the flag
|
||||
// names are consumed natively and the include stays in the tree.
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
#include "textflag.h"
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
RET
|
||||
`, Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
hasInclude := false
|
||||
for _, d := range f.Decls {
|
||||
if _, ok := d.(*ast.Include); ok {
|
||||
hasInclude = true
|
||||
}
|
||||
}
|
||||
if !hasInclude {
|
||||
t.Error("textflag.h include was dropped from the tree")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSemicolonSplitsRawLinesToo(t *testing.T) {
|
||||
_, got := expand(t, `
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
BYTE $0x0f; BYTE $0x1f
|
||||
RET
|
||||
`)
|
||||
wantLines(t, got, "BYTE $0x0f", "BYTE $0x1f", "RET")
|
||||
}
|
||||
|
||||
func TestParseUnchangedWithoutExpand(t *testing.T) {
|
||||
// Without Expand the preprocessor must not exist: a macro invocation
|
||||
// stays an unexpanded instruction line and ';' keeps the old parse.
|
||||
f, errs := Parse("t_amd64.s", `
|
||||
#define TWICE ADDQ AX, AX
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
TWICE
|
||||
BYTE $0x0f; BYTE $0x1f
|
||||
RET
|
||||
`)
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
fn := texts(f)[0]
|
||||
var mnemonics []string
|
||||
for _, s := range fn.Body {
|
||||
if in, ok := s.(*ast.Instr); ok {
|
||||
mnemonics = append(mnemonics, in.Mnemonic.Text)
|
||||
}
|
||||
}
|
||||
if strings.Join(mnemonics, " ") != "TWICE BYTE RET" {
|
||||
t.Errorf("non-expanding parse changed: %v", mnemonics)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantExpressionFolding(t *testing.T) {
|
||||
// The shapes substituted macro bodies leave behind: parenthesised
|
||||
// arithmetic in immediates and displacements, tilde complements. The
|
||||
// assertions read the semantic fields; Raw keeps the operand's tokens
|
||||
// in the canonicalised rendering, not the folded values.
|
||||
f, errs := ParseWithOptions("t_amd64.s", `
|
||||
TEXT ·f(SB), NOSPLIT, $0
|
||||
RORQ $(32-7), AX
|
||||
ANDQ $~63, AX
|
||||
MOVQ ((2*4)+0)(SP), AX
|
||||
MOVQ $((1<<3)|(1<<1)), AX
|
||||
RET
|
||||
`, Options{Expand: true})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
body := texts(f)[0].Body
|
||||
ror := body[0].(*ast.Instr)
|
||||
if !ror.Operands[0].Imm.HasVal || ror.Operands[0].Imm.Val != 25 {
|
||||
t.Errorf("RORQ immediate = %+v, want 25", ror.Operands[0].Imm)
|
||||
}
|
||||
and := body[1].(*ast.Instr)
|
||||
if !and.Operands[0].Imm.HasVal || and.Operands[0].Imm.Val != -64 {
|
||||
t.Errorf("ANDQ immediate = %+v, want -64", and.Operands[0].Imm)
|
||||
}
|
||||
mov := body[2].(*ast.Instr)
|
||||
addr := mov.Operands[0].Addr
|
||||
if !addr.HasOff || addr.Offset != 8 || addr.Base != "SP" {
|
||||
t.Errorf("MOVQ address = %+v, want 8(SP)", addr)
|
||||
}
|
||||
mov2 := body[3].(*ast.Instr)
|
||||
if !mov2.Operands[0].Imm.HasVal || mov2.Operands[0].Imm.Val != 10 {
|
||||
t.Errorf("MOVQ immediate = %+v, want 10", mov2.Operands[0].Imm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConstantExpressionFoldsWithoutExpand(t *testing.T) {
|
||||
// Folding is a parser capability, not a preprocessing one: a
|
||||
// hand-written $(32-7) folds the same way with expansion off.
|
||||
f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tRORQ $(32-7), AX\n\tRET\n", Options{})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
in := texts(f)[0].Body[0].(*ast.Instr)
|
||||
if !in.Operands[0].Imm.HasVal || in.Operands[0].Imm.Val != 25 {
|
||||
t.Errorf("Imm = %+v, want 25", in.Operands[0].Imm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotAnExpressionFallsBack(t *testing.T) {
|
||||
// Symbol immediates and floats must keep their ordinary parse.
|
||||
f, errs := ParseWithOptions("t_amd64.s", "TEXT ·f(SB), NOSPLIT, $0\n\tMOVQ $1.5, AX\n\tMOVQ $·sym(SB), AX\n\tRET\n", Options{})
|
||||
if len(errs) > 0 {
|
||||
t.Fatalf("parse: %v", errs)
|
||||
}
|
||||
fn := texts(f)[0]
|
||||
mov1 := fn.Body[0].(*ast.Instr)
|
||||
if mov1.Operands[0].Imm.HasVal || mov1.Operands[0].Imm.Float != "1.5" {
|
||||
t.Errorf("float immediate parsed as %+v", mov1.Operands[0].Imm)
|
||||
}
|
||||
mov2 := fn.Body[1].(*ast.Instr)
|
||||
if mov2.Operands[0].Imm.Sym == nil {
|
||||
t.Errorf("symbol immediate parsed as %+v", mov2.Operands[0].Imm)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user