[lld] [ELF] Handle INCLUDE like a call stack (PR #193427)

Fangrui Song via llvm-commits llvm-commits at lists.llvm.org
Wed Apr 22 18:59:46 PDT 2026


https://github.com/MaskRay updated https://github.com/llvm/llvm-project/pull/193427

>From 6a20a9371a8e03d373fb2c939f2c43f0fcb7b878 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Tue, 21 Apr 2026 00:04:52 -0700
Subject: [PATCH 1/3] [ELF] Handle INCLUDE like a call stack

The lexer maintains a stack of buffers, which allows a construct
started in an INCLUDE'd file to be closed by the parent. This produces
spurious acceptance of malformed scripts (e.g. a bare assignment with
no trailing `;` in the include, terminated by the parent's `;` after
`INCLUDE`) and undefined-behavior span computations in
`readAssignment`'s `commandString` (issue #190376).

Force each INCLUDE to fully parse its own content, similar to a call
stack frame. `ScriptLexer::lex` no longer auto-pops on EOF; the
`buffers` member is gone. `readInclude` takes a `function_ref<void()>`
callback, and the four call sites (top-level, SECTIONS, output
section, MEMORY) pass a context-appropriate parser.

With this, each buffer contains complete parser structures by
construction, so the `[oldS, curTok)` pointer range in
`readAssignment` no longer needs a guard.
---
 lld/ELF/ScriptLexer.cpp                       |  28 +--
 lld/ELF/ScriptLexer.h                         |   7 +-
 lld/ELF/ScriptParser.cpp                      | 219 ++++++++++--------
 .../ELF/linkerscript/include-mid-construct.s  |  18 ++
 lld/test/ELF/linkerscript/memory-include.test |  17 ++
 .../linkerscript/output-section-include.test  |   9 +
 .../ELF/linkerscript/section-include.test     |  17 ++
 7 files changed, 198 insertions(+), 117 deletions(-)
 create mode 100644 lld/test/ELF/linkerscript/include-mid-construct.s

diff --git a/lld/ELF/ScriptLexer.cpp b/lld/ELF/ScriptLexer.cpp
index c16a70ba1ce8f..4f4610434a534 100644
--- a/lld/ELF/ScriptLexer.cpp
+++ b/lld/ELF/ScriptLexer.cpp
@@ -52,7 +52,7 @@ ScriptLexer::Buffer::Buffer(Ctx &ctx, MemoryBufferRef mb)
 }
 
 ScriptLexer::ScriptLexer(Ctx &ctx, MemoryBufferRef mb)
-    : ctx(ctx), curBuf(ctx, mb), mbs(1, mb) {
+    : ctx(ctx), curBuf(ctx, mb) {
   activeFilenames.insert(mb.getBufferIdentifier());
 }
 
@@ -93,15 +93,10 @@ void ScriptLexer::lex() {
     StringRef &s = curBuf.s;
     s = skipSpace(s);
     if (s.empty()) {
-      // If this buffer is from an INCLUDE command, switch to the "return
-      // value"; otherwise, mark EOF.
-      if (buffers.empty()) {
-        eof = true;
-        return;
-      }
-      activeFilenames.erase(curBuf.filename);
-      curBuf = buffers.pop_back_val();
-      continue;
+      // If this buffer is from an INCLUDE, the caller is responsible for
+      // popping to the parent buffer.
+      eof = true;
+      return;
     }
     curTokState = lexState;
 
@@ -276,16 +271,7 @@ ScriptLexer::Token ScriptLexer::till(StringRef tok) {
   return {};
 }
 
-// Returns true if S encloses T.
-static bool encloses(StringRef s, StringRef t) {
-  return s.bytes_begin() <= t.bytes_begin() && t.bytes_end() <= s.bytes_end();
-}
-
 MemoryBufferRef ScriptLexer::getCurrentMB() {
-  // Find input buffer containing the current token.
-  assert(!mbs.empty());
-  for (MemoryBufferRef mb : mbs)
-    if (encloses(mb.getBuffer(), curBuf.s))
-      return mb;
-  llvm_unreachable("getCurrentMB: failed to find a token");
+  return MemoryBufferRef(StringRef(curBuf.begin, curBuf.s.end() - curBuf.begin),
+                         curBuf.filename);
 }
diff --git a/lld/ELF/ScriptLexer.h b/lld/ELF/ScriptLexer.h
index ba49155b9dc88..9fce5734edea1 100644
--- a/lld/ELF/ScriptLexer.h
+++ b/lld/ELF/ScriptLexer.h
@@ -14,7 +14,6 @@
 #include "llvm/ADT/SmallVector.h"
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/MemoryBufferRef.h"
-#include <vector>
 
 namespace lld::elf {
 struct Ctx;
@@ -34,9 +33,9 @@ class ScriptLexer {
     Buffer(Ctx &ctx, MemoryBufferRef mb);
   };
   Ctx &ctx;
-  // The current buffer and parent buffers due to INCLUDE.
+  // The currently lexed buffer. INCLUDE runs a nested parse on a new `Buffer`,
+  // similar to a call stack frame.
   Buffer curBuf;
-  SmallVector<Buffer, 0> buffers;
 
   // Used to detect INCLUDE() cycles.
   llvm::DenseSet<StringRef> activeFilenames;
@@ -82,8 +81,6 @@ class ScriptLexer {
   std::string getCurrentLocation();
   MemoryBufferRef getCurrentMB();
 
-  std::vector<MemoryBufferRef> mbs;
-
 private:
   StringRef getLine();
   size_t getColumnNumber();
diff --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index 508d8f40f5304..8b03da52950d4 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -59,9 +59,11 @@ class ScriptParser final : ScriptLexer {
   void readEntry();
   void readExtern();
   void readGroup();
-  void readInclude();
+  void readInclude(llvm::function_ref<void()> parse);
+  void readIncludedBody(llvm::function_ref<void(StringRef)> stmt);
   void readInput();
   void readMemory();
+  void readMemoryStmt(StringRef tok);
   void readOutput();
   void readOutputArch();
   void readOutputFormat();
@@ -70,6 +72,8 @@ class ScriptParser final : ScriptLexer {
   void readRegionAlias();
   void readSearchDir();
   void readSections();
+  void readSectionsStmt(SmallVectorImpl<SectionCommand *> &v, StringRef tok);
+  void readOutputSectionStmt(OutputSection &osec, StringRef tok);
   void readTarget();
   void readVersion();
   void readVersionScriptCommand();
@@ -249,7 +253,7 @@ void ScriptParser::readLinkerScript() {
     } else if (tok == "GROUP") {
       readGroup();
     } else if (tok == "INCLUDE") {
-      readInclude();
+      readInclude([&] { readLinkerScript(); });
     } else if (tok == "INPUT") {
       readInput();
     } else if (tok == "MEMORY") {
@@ -392,22 +396,41 @@ void ScriptParser::readGroup() {
     ++ctx.driver.nextGroupId;
 }
 
-void ScriptParser::readInclude() {
+void ScriptParser::readInclude(llvm::function_ref<void()> parse) {
   StringRef name = readName();
   if (!activeFilenames.insert(name).second) {
     setError("there is a cycle in linker script INCLUDEs");
     return;
   }
 
-  if (std::optional<std::string> path = searchScript(ctx, name)) {
-    if (std::optional<MemoryBufferRef> mb = readFile(ctx, *path)) {
-      buffers.push_back(curBuf);
-      curBuf = Buffer(ctx, *mb);
-      mbs.push_back(*mb);
-    }
+  std::optional<std::string> path = searchScript(ctx, name);
+  if (!path) {
+    setError("cannot find linker script " + name);
+    return;
+  }
+  std::optional<MemoryBufferRef> mb = readFile(ctx, *path);
+  if (!mb)
     return;
+
+  SaveAndRestore savedBuf(curBuf, Buffer(ctx, *mb));
+  SaveAndRestore savedPrevTok(prevTok, StringRef());
+  SaveAndRestore savedPrevTokLine(prevTokLine, size_t(1));
+  parse();
+
+  // parse() leaves `eof` true on normal completion; reset so the parent
+  // buffer continues to be lexed.
+  eof = false;
+  activeFilenames.erase(name);
+}
+
+// Drive `stmt` until EOF of the nested buffer.
+void ScriptParser::readIncludedBody(llvm::function_ref<void(StringRef)> stmt) {
+  while (!atEOF()) {
+    StringRef tok = next();
+    if (atEOF())
+      return;
+    stmt(tok);
   }
-  setError("cannot find linker script " + name);
 }
 
 void ScriptParser::readInput() {
@@ -655,26 +678,8 @@ void ScriptParser::readOverwriteSections() {
 void ScriptParser::readSections() {
   expect("{");
   SmallVector<SectionCommand *, 0> v;
-  while (auto tok = till("}")) {
-    if (tok == "OVERLAY") {
-      for (SectionCommand *cmd : readOverlay())
-        v.push_back(cmd);
-      continue;
-    }
-    if (tok == "CLASS") {
-      v.push_back(readSectionClassDescription());
-      continue;
-    }
-    if (tok == "INCLUDE") {
-      readInclude();
-      continue;
-    }
-
-    if (SectionCommand *cmd = readAssignment(tok))
-      v.push_back(cmd);
-    else
-      v.push_back(readOutputSectionDescription(tok));
-  }
+  while (auto tok = till("}"))
+    readSectionsStmt(v, tok);
 
   // If DATA_SEGMENT_RELRO_END is absent, for sections after DATA_SEGMENT_ALIGN,
   // the relro fields should be cleared.
@@ -705,6 +710,29 @@ void ScriptParser::readSections() {
     ctx.script->insertCommands.push_back({std::move(names), isAfter, where});
 }
 
+void ScriptParser::readSectionsStmt(SmallVectorImpl<SectionCommand *> &v,
+                                    StringRef tok) {
+  if (tok == "OVERLAY") {
+    for (SectionCommand *cmd : readOverlay())
+      v.push_back(cmd);
+    return;
+  }
+  if (tok == "CLASS") {
+    v.push_back(readSectionClassDescription());
+    return;
+  }
+  if (tok == "INCLUDE") {
+    readInclude([&] {
+      readIncludedBody([&](StringRef t) { readSectionsStmt(v, t); });
+    });
+    return;
+  }
+  if (SectionCommand *cmd = readAssignment(tok))
+    v.push_back(cmd);
+  else
+    v.push_back(readOutputSectionDescription(tok));
+}
+
 void ScriptParser::readTarget() {
   // TARGET(foo) is an alias for "--format foo". Unlike GNU linkers,
   // we accept only a limited set of BFD names (i.e. "elf" or "binary")
@@ -1033,44 +1061,8 @@ OutputDesc *ScriptParser::readOutputSectionDescription(StringRef outSec) {
     osec->constraint = ConstraintKind::ReadWrite;
   expect("{");
 
-  while (auto tok = till("}")) {
-    if (tok == ";") {
-      // Empty commands are allowed. Do nothing here.
-    } else if (SymbolAssignment *assign = readAssignment(tok)) {
-      osec->commands.push_back(assign);
-    } else if (ByteCommand *data = readByteCommand(tok)) {
-      osec->commands.push_back(data);
-    } else if (tok == "CONSTRUCTORS") {
-      // CONSTRUCTORS is a keyword to make the linker recognize C++ ctors/dtors
-      // by name. This is for very old file formats such as ECOFF/XCOFF.
-      // For ELF, we should ignore.
-    } else if (tok == "FILL") {
-      // We handle the FILL command as an alias for =fillexp section attribute,
-      // which is different from what GNU linkers do.
-      // https://sourceware.org/binutils/docs/ld/Output-Section-Data.html
-      if (peek() != "(")
-        setError("( expected, but got " + peek());
-      osec->filler = readFill();
-    } else if (tok == "SORT") {
-      readSort();
-    } else if (tok == "INCLUDE") {
-      readInclude();
-    } else if (tok == "(" || tok == ")") {
-      setError("expected filename pattern");
-    } else if (peek() == "(") {
-      osec->commands.push_back(readInputSectionDescription(tok));
-    } else {
-      // We have a file name and no input sections description. It is not a
-      // commonly used syntax, but still acceptable. In that case, all sections
-      // from the file will be included.
-      // FIXME: GNU ld permits INPUT_SECTION_FLAGS to be used here. We do not
-      // handle this case here as it will already have been matched by the
-      // case above.
-      auto *isd = make<InputSectionDescription>(tok);
-      isd->sectionPatterns.push_back({{}, StringMatcher("*")});
-      osec->commands.push_back(isd);
-    }
-  }
+  while (auto tok = till("}"))
+    readOutputSectionStmt(*osec, tok);
 
   if (consume(">"))
     osec->memoryRegionName = std::string(readName());
@@ -1100,6 +1092,47 @@ OutputDesc *ScriptParser::readOutputSectionDescription(StringRef outSec) {
   return cmd;
 }
 
+void ScriptParser::readOutputSectionStmt(OutputSection &osec, StringRef tok) {
+  if (tok == ";") {
+    // Empty commands are allowed. Do nothing here.
+  } else if (SymbolAssignment *assign = readAssignment(tok)) {
+    osec.commands.push_back(assign);
+  } else if (ByteCommand *data = readByteCommand(tok)) {
+    osec.commands.push_back(data);
+  } else if (tok == "CONSTRUCTORS") {
+    // CONSTRUCTORS is a keyword to make the linker recognize C++ ctors/dtors
+    // by name. This is for very old file formats such as ECOFF/XCOFF.
+    // For ELF, we should ignore.
+  } else if (tok == "FILL") {
+    // We handle the FILL command as an alias for =fillexp section attribute,
+    // which is different from what GNU linkers do.
+    // https://sourceware.org/binutils/docs/ld/Output-Section-Data.html
+    if (peek() != "(")
+      setError("( expected, but got " + peek());
+    osec.filler = readFill();
+  } else if (tok == "SORT") {
+    readSort();
+  } else if (tok == "INCLUDE") {
+    readInclude([&] {
+      readIncludedBody([&](StringRef t) { readOutputSectionStmt(osec, t); });
+    });
+  } else if (tok == "(" || tok == ")") {
+    setError("expected filename pattern");
+  } else if (peek() == "(") {
+    osec.commands.push_back(readInputSectionDescription(tok));
+  } else {
+    // We have a file name and no input sections description. It is not a
+    // commonly used syntax, but still acceptable. In that case, all sections
+    // from the file will be included.
+    // FIXME: GNU ld permits INPUT_SECTION_FLAGS to be used here. We do not
+    // handle this case here as it will already have been matched by the
+    // case above.
+    auto *isd = make<InputSectionDescription>(tok);
+    isd->sectionPatterns.push_back({{}, StringMatcher("*")});
+    osec.commands.push_back(isd);
+  }
+}
+
 // Reads a `=<fillexp>` expression and returns its value as a big-endian number.
 // https://sourceware.org/binutils/docs/ld/Output-Section-Fill.html
 // We do not support using symbols in such expressions.
@@ -1835,32 +1868,36 @@ Expr ScriptParser::readMemoryAssignment(StringRef s1, StringRef s2,
 // MEMORY { name [(attr)] : ORIGIN = origin, LENGTH = len ... }
 void ScriptParser::readMemory() {
   expect("{");
-  while (auto tok = till("}")) {
-    if (tok == "INCLUDE") {
-      readInclude();
-      continue;
-    }
-
-    uint32_t flags = 0;
-    uint32_t invFlags = 0;
-    uint32_t negFlags = 0;
-    uint32_t negInvFlags = 0;
-    if (consume("(")) {
-      readMemoryAttributes(flags, invFlags, negFlags, negInvFlags);
-      expect(")");
-    }
-    expect(":");
+  while (auto tok = till("}"))
+    readMemoryStmt(tok);
+}
 
-    Expr origin = readMemoryAssignment("ORIGIN", "org", "o");
-    expect(",");
-    Expr length = readMemoryAssignment("LENGTH", "len", "l");
+void ScriptParser::readMemoryStmt(StringRef tok) {
+  if (tok == "INCLUDE") {
+    readInclude(
+        [&] { readIncludedBody([&](StringRef t) { readMemoryStmt(t); }); });
+    return;
+  }
 
-    // Add the memory region to the region map.
-    MemoryRegion *mr = make<MemoryRegion>(tok, origin, length, flags, invFlags,
-                                          negFlags, negInvFlags);
-    if (!ctx.script->memoryRegions.insert({tok, mr}).second)
-      setError("region '" + tok + "' already defined");
+  uint32_t flags = 0;
+  uint32_t invFlags = 0;
+  uint32_t negFlags = 0;
+  uint32_t negInvFlags = 0;
+  if (consume("(")) {
+    readMemoryAttributes(flags, invFlags, negFlags, negInvFlags);
+    expect(")");
   }
+  expect(":");
+
+  Expr origin = readMemoryAssignment("ORIGIN", "org", "o");
+  expect(",");
+  Expr length = readMemoryAssignment("LENGTH", "len", "l");
+
+  // Add the memory region to the region map.
+  MemoryRegion *mr = make<MemoryRegion>(tok, origin, length, flags, invFlags,
+                                        negFlags, negInvFlags);
+  if (!ctx.script->memoryRegions.insert({tok, mr}).second)
+    setError("region '" + tok + "' already defined");
 }
 
 // This function parses the attributes used to match against section
diff --git a/lld/test/ELF/linkerscript/include-mid-construct.s b/lld/test/ELF/linkerscript/include-mid-construct.s
new file mode 100644
index 0000000000000..951c72fc93860
--- /dev/null
+++ b/lld/test/ELF/linkerscript/include-mid-construct.s
@@ -0,0 +1,18 @@
+# REQUIRES: x86
+# RUN: rm -rf %t && split-file %s %t && cd %t
+# RUN: llvm-mc -filetype=obj -triple=x86_64 a.s -o a.o
+
+## A stray ';' in the parent after INCLUDE cannot complete the inner assignment.
+# RUN: not ld.lld a.o -T top.lds 2>&1 | FileCheck %s --check-prefix=TOP
+# TOP: error: inc-top.lds:1: unexpected EOF
+
+#--- top.lds
+INCLUDE "inc-top.lds";
+
+#--- inc-top.lds
+foo = 1
+
+#--- a.s
+.globl _start
+_start:
+  ret
diff --git a/lld/test/ELF/linkerscript/memory-include.test b/lld/test/ELF/linkerscript/memory-include.test
index 6c6a89e45787a..2d938d6c7fa93 100644
--- a/lld/test/ELF/linkerscript/memory-include.test
+++ b/lld/test/ELF/linkerscript/memory-include.test
@@ -17,6 +17,17 @@
 # EMPTY:      LOAD {{.*}} 0x0000000000001000 0x0000000000001000 {{.*}} R E
 # EMPTY-NEXT: LOAD {{.*}} 0x0000000000002000 0x0000000000002000 {{.*}} RW
 
+## A region declaration truncated mid-expression cannot be completed by the
+## parent MEMORY { ... }.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
+## A stray '}' in the include cannot close the parent MEMORY { ... }.
+# RUN: cp brace.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=BRACE
+# BRACE: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .section .text,"ax"
 .global _start
@@ -54,3 +65,9 @@ SECTIONS {
 }
 
 #--- inc-empty.lds
+
+#--- trunc.lds
+RAM3 : ORIGIN = 0x4000, LENGTH
+
+#--- brace.lds
+}
diff --git a/lld/test/ELF/linkerscript/output-section-include.test b/lld/test/ELF/linkerscript/output-section-include.test
index 6cdf07ca7007e..a6392b315870c 100644
--- a/lld/test/ELF/linkerscript/output-section-include.test
+++ b/lld/test/ELF/linkerscript/output-section-include.test
@@ -16,6 +16,12 @@
 # RUN: llvm-objdump --section-headers a.out | FileCheck %s --check-prefix=CHECK2
 # CHECK2: .data         00000010 0000000000002000 DATA
 
+## A BYTE() with an unclosed paren in the include cannot be completed by the
+## parent output-section body.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .section .text,"ax"
 .global _start
@@ -42,3 +48,6 @@ SECTIONS {
 
 #--- full.lds
 QUAD(0)
+
+#--- trunc.lds
+BYTE(42
diff --git a/lld/test/ELF/linkerscript/section-include.test b/lld/test/ELF/linkerscript/section-include.test
index 16ed19f455d50..88c5f84de0b5f 100644
--- a/lld/test/ELF/linkerscript/section-include.test
+++ b/lld/test/ELF/linkerscript/section-include.test
@@ -19,6 +19,17 @@
 # CHECK2-NEXT: .data2        00000008 0000000000002008 DATA
 # CHECK2-NEXT: .data3        00000008 0000000000002010 DATA
 
+## An unclosed output section in the include cannot be closed by the outer
+## SECTIONS { ... } '}'.
+# RUN: cp trunc.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=TRUNC
+# TRUNC: error: inc.lds:1: unexpected EOF
+
+## A stray '}' in the include cannot close the parent SECTIONS { ... }.
+# RUN: cp brace.lds inc.lds
+# RUN: not ld.lld -T a.lds a.o 2>&1 | FileCheck %s --check-prefix=BRACE
+# BRACE: error: inc.lds:1: unexpected EOF
+
 #--- a.s
 .global _start
 _start: nop
@@ -43,3 +54,9 @@ SECTIONS {
 
 #--- full.lds
 .data2 : { QUAD(0) } > RAM
+
+#--- trunc.lds
+.text : { *(.text*)
+
+#--- brace.lds
+}

>From 5f71c6940298533445b499d303522f242604dba9 Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Wed, 22 Apr 2026 10:03:55 -0700
Subject: [PATCH 2/3] rename parameter simplify readLinkerScript

---
 lld/ELF/ScriptParser.cpp | 105 ++++++++++++++++++++-------------------
 1 file changed, 54 insertions(+), 51 deletions(-)

diff --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index 8b03da52950d4..829d34b1b63b4 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -60,8 +60,8 @@ class ScriptParser final : ScriptLexer {
   void readExtern();
   void readGroup();
   void readInclude(llvm::function_ref<void()> parse);
-  void readIncludedBody(llvm::function_ref<void(StringRef)> stmt);
   void readInput();
+  void readLinkerScriptStmt(StringRef tok);
   void readMemory();
   void readMemoryStmt(StringRef tok);
   void readOutput();
@@ -74,6 +74,7 @@ class ScriptParser final : ScriptLexer {
   void readSections();
   void readSectionsStmt(SmallVectorImpl<SectionCommand *> &v, StringRef tok);
   void readOutputSectionStmt(OutputSection &osec, StringRef tok);
+  void readIncludedBody(llvm::function_ref<void(StringRef)> readStmt);
   void readTarget();
   void readVersion();
   void readVersionScriptCommand();
@@ -239,54 +240,55 @@ void ScriptParser::readVersion() {
 }
 
 void ScriptParser::readLinkerScript() {
-  while (!atEOF()) {
-    StringRef tok = next();
-    if (atEOF())
-      break;
-    if (tok == ";")
-      continue;
+  readIncludedBody([&](StringRef t) { readLinkerScriptStmt(t); });
+}
 
-    if (tok == "ENTRY") {
-      readEntry();
-    } else if (tok == "EXTERN") {
-      readExtern();
-    } else if (tok == "GROUP") {
-      readGroup();
-    } else if (tok == "INCLUDE") {
-      readInclude([&] { readLinkerScript(); });
-    } else if (tok == "INPUT") {
-      readInput();
-    } else if (tok == "MEMORY") {
-      readMemory();
-    } else if (tok == "OUTPUT") {
-      readOutput();
-    } else if (tok == "OUTPUT_ARCH") {
-      readOutputArch();
-    } else if (tok == "OUTPUT_FORMAT") {
-      readOutputFormat();
-    } else if (tok == "OVERWRITE_SECTIONS") {
-      readOverwriteSections();
-    } else if (tok == "PHDRS") {
-      readPhdrs();
-    } else if (tok == "REGION_ALIAS") {
-      readRegionAlias();
-    } else if (tok == "SEARCH_DIR") {
-      readSearchDir();
-    } else if (tok == "SECTIONS") {
-      readSections();
-    } else if (tok == "TARGET") {
-      readTarget();
-    } else if (tok == "VERSION") {
-      readVersion();
-    } else if (tok == "NOCROSSREFS") {
-      readNoCrossRefs(/*to=*/false);
-    } else if (tok == "NOCROSSREFS_TO") {
-      readNoCrossRefs(/*to=*/true);
-    } else if (SymbolAssignment *cmd = readAssignment(tok)) {
-      ctx.script->sectionCommands.push_back(cmd);
-    } else {
-      setError("unknown directive: " + tok);
-    }
+void ScriptParser::readLinkerScriptStmt(StringRef tok) {
+  if (tok == ";")
+    return;
+
+  if (tok == "ENTRY") {
+    readEntry();
+  } else if (tok == "EXTERN") {
+    readExtern();
+  } else if (tok == "GROUP") {
+    readGroup();
+  } else if (tok == "INCLUDE") {
+    readInclude([&] {
+      readIncludedBody([&](StringRef t) { readLinkerScriptStmt(t); });
+    });
+  } else if (tok == "INPUT") {
+    readInput();
+  } else if (tok == "MEMORY") {
+    readMemory();
+  } else if (tok == "OUTPUT") {
+    readOutput();
+  } else if (tok == "OUTPUT_ARCH") {
+    readOutputArch();
+  } else if (tok == "OUTPUT_FORMAT") {
+    readOutputFormat();
+  } else if (tok == "OVERWRITE_SECTIONS") {
+    readOverwriteSections();
+  } else if (tok == "PHDRS") {
+    readPhdrs();
+  } else if (tok == "REGION_ALIAS") {
+    readRegionAlias();
+  } else if (tok == "SEARCH_DIR") {
+    readSearchDir();
+  } else if (tok == "SECTIONS") {
+    readSections();
+  } else if (tok == "TARGET") {
+    readTarget();
+  } else if (tok == "VERSION") {
+    readVersion();
+  } else if (tok == "NOCROSSREFS") {
+    readNoCrossRefs(/*to=*/false);
+  } else if (tok == "NOCROSSREFS_TO") {
+    readNoCrossRefs(/*to=*/true);
+  } else if (SymbolAssignment *cmd = readAssignment(tok)) {
+    ctx.script->sectionCommands.push_back(cmd);
+  } else {
+    setError("unknown directive: " + tok);
   }
 }
 
@@ -423,13 +425,14 @@ void ScriptParser::readInclude(llvm::function_ref<void()> parse) {
   activeFilenames.erase(name);
 }
 
-// Drive `stmt` until EOF of the nested buffer.
-void ScriptParser::readIncludedBody(llvm::function_ref<void(StringRef)> stmt) {
+// Drive `readStmt` on each token until EOF of the current buffer.
+void ScriptParser::readIncludedBody(
+    llvm::function_ref<void(StringRef)> readStmt) {
   while (!atEOF()) {
     StringRef tok = next();
     if (atEOF())
       return;
-    stmt(tok);
+    readStmt(tok);
   }
 }
 

>From 2d0f6ba64b1f458a6d6194e01f64bebe0895a60a Mon Sep 17 00:00:00 2001
From: Fangrui Song <i at maskray.me>
Date: Wed, 22 Apr 2026 10:06:53 -0700
Subject: [PATCH 3/3] rename readIncludedBody as it is reused by
 readLinkerScript

---
 lld/ELF/ScriptParser.cpp | 22 +++++++++-------------
 1 file changed, 9 insertions(+), 13 deletions(-)

diff --git a/lld/ELF/ScriptParser.cpp b/lld/ELF/ScriptParser.cpp
index 829d34b1b63b4..8a7059d17cb2e 100644
--- a/lld/ELF/ScriptParser.cpp
+++ b/lld/ELF/ScriptParser.cpp
@@ -74,7 +74,7 @@ class ScriptParser final : ScriptLexer {
   void readSections();
   void readSectionsStmt(SmallVectorImpl<SectionCommand *> &v, StringRef tok);
   void readOutputSectionStmt(OutputSection &osec, StringRef tok);
-  void readIncludedBody(llvm::function_ref<void(StringRef)> readStmt);
+  void readStmts(llvm::function_ref<void(StringRef)> readStmt);
   void readTarget();
   void readVersion();
   void readVersionScriptCommand();
@@ -240,7 +240,7 @@ void ScriptParser::readVersion() {
 }
 
 void ScriptParser::readLinkerScript() {
-  readIncludedBody([&](StringRef t) { readLinkerScriptStmt(t); });
+  readStmts([&](StringRef t) { readLinkerScriptStmt(t); });
 }
 
 void ScriptParser::readLinkerScriptStmt(StringRef tok) {
@@ -254,9 +254,8 @@ void ScriptParser::readLinkerScriptStmt(StringRef tok) {
   } else if (tok == "GROUP") {
     readGroup();
   } else if (tok == "INCLUDE") {
-    readInclude([&] {
-      readIncludedBody([&](StringRef t) { readLinkerScriptStmt(t); });
-    });
+    readInclude(
+        [&] { readStmts([&](StringRef t) { readLinkerScriptStmt(t); }); });
   } else if (tok == "INPUT") {
     readInput();
   } else if (tok == "MEMORY") {
@@ -426,8 +425,7 @@ void ScriptParser::readInclude(llvm::function_ref<void()> parse) {
 }
 
 // Drive `readStmt` on each token until EOF of the current buffer.
-void ScriptParser::readIncludedBody(
-    llvm::function_ref<void(StringRef)> readStmt) {
+void ScriptParser::readStmts(llvm::function_ref<void(StringRef)> readStmt) {
   while (!atEOF()) {
     StringRef tok = next();
     if (atEOF())
@@ -725,9 +723,8 @@ void ScriptParser::readSectionsStmt(SmallVectorImpl<SectionCommand *> &v,
     return;
   }
   if (tok == "INCLUDE") {
-    readInclude([&] {
-      readIncludedBody([&](StringRef t) { readSectionsStmt(v, t); });
-    });
+    readInclude(
+        [&] { readStmts([&](StringRef t) { readSectionsStmt(v, t); }); });
     return;
   }
   if (SectionCommand *cmd = readAssignment(tok))
@@ -1117,7 +1114,7 @@ void ScriptParser::readOutputSectionStmt(OutputSection &osec, StringRef tok) {
     readSort();
   } else if (tok == "INCLUDE") {
     readInclude([&] {
-      readIncludedBody([&](StringRef t) { readOutputSectionStmt(osec, t); });
+      readStmts([&](StringRef t) { readOutputSectionStmt(osec, t); });
     });
   } else if (tok == "(" || tok == ")") {
     setError("expected filename pattern");
@@ -1877,8 +1874,7 @@ void ScriptParser::readMemory() {
 
 void ScriptParser::readMemoryStmt(StringRef tok) {
   if (tok == "INCLUDE") {
-    readInclude(
-        [&] { readIncludedBody([&](StringRef t) { readMemoryStmt(t); }); });
+    readInclude([&] { readStmts([&](StringRef t) { readMemoryStmt(t); }); });
     return;
   }
 



More information about the llvm-commits mailing list