Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -480,14 +480,15 @@ struct EvalContext {
| Flag | Meaning | Example |
|------|---------|---------|
| `%f` | All inputs | `main.c util.c` |
| `%i` | All inputs (alias) | `main.c util.c` |
| `%i` | Order-only inputs | `config.h` |
| `%o` | All outputs | `main.o` |
| `%O` | Output basename | `main` |
| `%b` | Basename with ext, every input | `main.c util.c` |
| `%B` | Basename no ext, every input | `main util` |
| `%e` | Extension (foreach, file must have one) | `c` |
| `%d` | Directory | `src` |
| `%Nf` | Nth input | `%1f` → first input |
| `%Ni` | Nth order-only input | `%1i` → first order-only input |

**Expansion pipeline:**

Expand Down Expand Up @@ -1037,7 +1038,7 @@ The index uses **instruction-based command storage** for significant space savin

**Instruction deduplication**: Bang macros like `!cc = |> $(CC) -c %f -o %o |>` produce the same instruction for all source files. With 1000 C files, instead of storing 1000 nearly-identical command strings, v8 stores 1 instruction + 1000 operand records.

**Lazy reconstruction**: Full command strings are computed on demand via `expand_instruction()`, which substitutes operand paths into the instruction pattern. CommandNode stores `instruction_id` (the pattern) plus explicit `inputs`/`outputs` operand vectors. This keeps index loading fast and avoids storing redundant expanded strings.
**Lazy reconstruction**: Full command strings are computed on demand via `expand_instruction()`, which substitutes operand paths into the instruction pattern. CommandNode stores `instruction_id` (the pattern) plus explicit `inputs`/`order_only_inputs`/`outputs` operand vectors. This keeps index loading fast and avoids storing redundant expanded strings.

Version history:
- v1: Initial format with full path strings
Expand Down
7 changes: 4 additions & 3 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ Pattern flags are placeholders expanded at build time.
| Flag | Description | Example |
|------|-------------|---------|
| `%f` | All input files; the rule must have at least one | `gcc %f` → `gcc foo.c bar.c` |
| `%i` | All inputs (alias for %f) | `gcc %i` → `gcc foo.c bar.c` |
| `%i` | The order-only inputs written after `\|`; the rule must have at least one, and only a command or display string may spell it | `: a.c \| z.h y.h \|> cat %i` → `cat z.h y.h` |
| `%o` | All output files; the rule must declare at least one | `ar rcs %o` → `ar rcs foo.a foo.map` |
| `%O` | The output without its extension, keeping its directory — command string or extra outputs section, and only with exactly one output | `sub/foo.so` → `sub/foo` |
| `%b` | Basename of every input, with extension; the rule must have at least one | `src/foo.c bar.c` → `foo.c bar.c` |
Expand All @@ -1022,14 +1022,14 @@ Pattern flags are placeholders expanded at build time.
| `%2f` | Second input file |
| `%1b` | Basename of the first input |
| `%1B` | Basename of the first input without its extension |
| `%1i` | First order-only input |
| `%1o` | First output file |
| `%2o` | Second output file |

The number must be at least 1 and below 99 (tup's guard rejects 99 itself, despite its message
naming the range 1-99), a letter must follow it, and that letter must be one of
`f`, `b`, `B`, `o` or `i` — anything else is a parse error rather than literal text. A number
that is in range but names an entry the rule does not have expands to nothing, as in tup.
`%Ni` (N-th order-only input) is not supported yet and is refused by name; see issue #426.

**Examples:**

Expand Down Expand Up @@ -2831,7 +2831,7 @@ CONFIG_RELEASE_LDFLAGS=-Wl,--gc-sections
| Flag | Description | Example Input | Result |
|------|-------------|---------------|--------|
| `%f` | All inputs (rule must have one) | `foo.c bar.c` | `foo.c bar.c` |
| `%i` | All inputs (alias) | `foo.c bar.c` | `foo.c bar.c` |
| `%i` | Order-only inputs (rule must have one; command or display string only) | `a.c \| z.h y.h` | `z.h y.h` |
| `%b` | Basename with ext, every input (rule must have one) | `src/foo.c bar.c` | `foo.c bar.c` |
| `%B` | Basename no ext, every input (rule must have one) | `src/foo.c bar.c` | `foo bar` |
| `%e` | Extension (foreach, file must have one) | `foo.c` | `c` |
Expand All @@ -2852,6 +2852,7 @@ CONFIG_RELEASE_LDFLAGS=-Wl,--gc-sections
| `%1f` | First input |
| `%2f` | Second input |
| `%3f` | Third input (etc.) |
| `%1i` | First order-only input |
| `%1o` | First output |
| `%2o` | Second output |
| `%3o` | Third output (etc.) |
Expand Down
10 changes: 8 additions & 2 deletions include/pup/core/instruction.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ enum class AtomKind : std::uint8_t {
Literal,
GroupRef,
AllInputs,
AllInputsAlias,
OrderOnlyInputs,
InputBase,
InputNoExt,
InputExt,
Expand All @@ -28,6 +28,7 @@ enum class AtomKind : std::uint8_t {
NthInput,
NthInputBase,
NthInputNoExt,
NthOrderOnlyInput,
NthOutput,
};

Expand Down Expand Up @@ -119,9 +120,11 @@ auto fold_instruction(Instruction const& atoms, Site const& site) -> StringId
site.append_group_ref(buf, atom.text());
break;
case AtomKind::AllInputs:
case AtomKind::AllInputsAlias:
site.append_all_inputs(buf);
break;
case AtomKind::OrderOnlyInputs:
site.append_order_only_inputs(buf);
break;
case AtomKind::InputBase:
site.append_input_base(buf);
break;
Expand Down Expand Up @@ -149,6 +152,9 @@ auto fold_instruction(Instruction const& atoms, Site const& site) -> StringId
case AtomKind::NthInputNoExt:
site.append_nth_input_noext(buf, atom.operand());
break;
case AtomKind::NthOrderOnlyInput:
site.append_nth_order_only_input(buf, atom.operand());
break;
case AtomKind::NthOutput:
site.append_nth_output(buf, atom.operand());
break;
Expand Down
12 changes: 9 additions & 3 deletions include/pup/graph/dag.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,17 @@ struct Guard {
/// Command node - represents build commands
/// Note: Type is determined by node_id::is_command(), not a stored field.
/// Command string is reconstructed on demand via expand_instruction() from
/// instruction atoms + explicit operand NodeIds (inputs/outputs).
/// instruction atoms + explicit operand NodeIds (inputs/order-only inputs/outputs).
struct CommandNode {
NodeId id = 0;

StringId display = StringId::Empty; ///< Display text (from ^ ^ markers, interned)
StringId source_dir = StringId::Empty; ///< Tupfile directory (relative to root, interned)
Instruction instruction = {}; ///< Instruction atoms (e.g. "gcc -c %f -o %o")

TokenList<NodeId> inputs = {}; ///< Operand file NodeIds for %f, %b and %B expansion, grouped by written token
TokenList<NodeId> outputs = {}; ///< Operand file NodeIds for %o expansion, grouped by written token
TokenList<NodeId> inputs = {}; ///< Operand file NodeIds for %f, %b and %B expansion, grouped by written token
TokenList<NodeId> order_only_inputs = {}; ///< The rule's own order-only operands for %i expansion, grouped by written token
TokenList<NodeId> outputs = {}; ///< Operand file NodeIds for %o expansion, grouped by written token

SortedIdVec exported_vars = {}; ///< Env vars to export to command (interned StringIds)

Expand Down Expand Up @@ -205,6 +206,7 @@ auto get(Graph const& graph, NodeId id) -> typename get_storage<Tag>::type;

/// Role tags for composite-property access via view<Tag>.
struct Inputs { };
struct OrderOnlyInputs { };
struct Outputs { };
struct ExportedVars { };

Expand All @@ -217,6 +219,10 @@ struct view_storage<Inputs> {
using type = TokenList<NodeId>;
};
template<>
struct view_storage<OrderOnlyInputs> {
using type = TokenList<NodeId>;
};
template<>
struct view_storage<Outputs> {
using type = TokenList<NodeId>;
};
Expand Down
6 changes: 4 additions & 2 deletions include/pup/index/entry.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ struct CommandEntry {
/// carried this record past a change to one of its deps without re-running it.
bool must_rerun = false;

TokenList<NodeId> inputs = {}; ///< Input file operands (for %f, %b and %B expansion), grouped by written token
TokenList<NodeId> outputs = {}; ///< Output file operands (for %o expansion), grouped by written token
TokenList<NodeId> inputs = {}; ///< Input file operands (for %f, %b and %B expansion), grouped by written token
TokenList<NodeId> order_only_inputs = {}; ///< The rule's own order-only operands (for %i expansion), grouped by written token
TokenList<NodeId> outputs = {}; ///< Output file operands (for %o expansion), grouped by written token

/// Convert to raw format for serialization
[[nodiscard]]
Expand All @@ -85,6 +86,7 @@ struct CommandEntry {
std::string_view display_str,
std::string_view env_str,
TokenList<NodeId> inputs,
TokenList<NodeId> order_only_inputs,
TokenList<NodeId> outputs,
std::size_t array_index
) -> Result<CommandEntry>;
Expand Down
12 changes: 10 additions & 2 deletions include/pup/index/format.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ inline constexpr auto INDEX_MAGIC = std::array<char, 4> { 'P', 'U', 'P', 'I' };
/// 255, so a rule with more operands recorded a prefix and the carried-forward record
/// stopped seeing the rest change (issue #365). A v21 index parses at the wrong offsets
/// under this layout, so it is not read.
/// 26 - A command's operand record carries a third list, the rule's own order-only operands
/// `%i` and `%Ni` spell, grouped by written token like the other two; before it `%i`
/// spelled the inputs (issue #461). A v25 record has no head words for it, so it is not read.
/// Bump when a stale reader would misparse the bytes — any layout change — or wrong-join a key
/// whose meaning changed; a change that only changes keys no-joins, and one re-run per affected
/// command repairs it (#333, #335, #343).
Expand All @@ -89,7 +92,7 @@ inline constexpr auto INDEX_MAGIC = std::array<char, 4> { 'P', 'U', 'P', 'I' };
/// `RawFileEntry::name_offset` is semantics-bearing on the same terms: `read_prior_paths` composes
/// it into the paths `clean`/`distclean` delete and `reject_shadowed_sources` refuses a build over,
/// so a name this reader cannot reproduce fails the record rather than reading as empty (#381).
inline constexpr auto INDEX_VERSION = std::uint32_t { 25 };
inline constexpr auto INDEX_VERSION = std::uint32_t { 26 };

/// The oldest version whose `RawHeader` and `RawFileEntry` bytes mean what today's mean, so a
/// record that old still says which paths it recorded as sources and which as generated even
Expand Down Expand Up @@ -232,6 +235,8 @@ enum class OperandRecordWord : std::size_t {
OutputCount = 1,
InputTokenCount = 2,
OutputTokenCount = 3,
OrderOnlyInputCount = 4,
OrderOnlyInputTokenCount = 5,
};

[[nodiscard]]
Expand All @@ -242,14 +247,17 @@ constexpr auto operand_word_category(OperandRecordWord word) -> std::string_view
return "inputs";
case OperandRecordWord::OutputCount:
return "outputs";
case OperandRecordWord::OrderOnlyInputCount:
return "order-only-inputs";
case OperandRecordWord::InputTokenCount:
case OperandRecordWord::OutputTokenCount:
case OperandRecordWord::OrderOnlyInputTokenCount:
return "operand-tokens";
}
return {};
}

inline constexpr auto OPERAND_RECORD_HEAD_WORDS = std::size_t { 4 };
inline constexpr auto OPERAND_RECORD_HEAD_WORDS = std::size_t { 6 };

static_assert(
!operand_word_category(static_cast<OperandRecordWord>(OPERAND_RECORD_HEAD_WORDS - 1)).empty(),
Expand Down
13 changes: 7 additions & 6 deletions include/pup/parser/eval.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -206,12 +206,13 @@ enum class PatternSection {

/// Pattern flags for command/output expansion
struct PatternFlags {
std::optional<std::string_view> input_ext = {}; ///< %e - the current foreach file's extension, absent outside foreach or when its name has no extension
std::string_view input_dir = {}; ///< %d - input directory
std::optional<std::string_view> glob_match = {}; ///< %g - the text the input's glob matched, absent when no glob produced it
TokenList<std::string_view> all_inputs = {}; ///< All inputs, for %f, %b, %B and their numbered forms, grouped by written token
TokenList<std::string_view> all_outputs = {}; ///< All outputs, for %o, %No and %O, grouped by written token
PatternSection section = PatternSection::Command; ///< decides where %o and %O are legal
std::optional<std::string_view> input_ext = {}; ///< %e - the current foreach file's extension, absent outside foreach or when its name has no extension
std::string_view input_dir = {}; ///< %d - input directory
std::optional<std::string_view> glob_match = {}; ///< %g - the text the input's glob matched, absent when no glob produced it
TokenList<std::string_view> all_inputs = {}; ///< All inputs, for %f, %b, %B and their numbered forms, grouped by written token
TokenList<std::string_view> order_only_inputs = {}; ///< The rule's own order-only inputs, for %i and %Ni, grouped by written token
TokenList<std::string_view> all_outputs = {}; ///< All outputs, for %o, %No and %O, grouped by written token
PatternSection section = PatternSection::Command; ///< decides where %o and %O are legal
};

/// Expand an expression, replacing variable references with values
Expand Down
34 changes: 29 additions & 5 deletions spec/requirements/operand-flags.ears.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,18 @@ number of the token it was written as rather than renumbering it for its own ite
If a rule spells `%No`, then putup shall expand it to every operand of the N-th output token,
joined by single spaces.

### REQ-OPERAND-ORDER-ONLY-INPUT-SPELLING

- conformance: tup-conformant
- reference: upstream passes the rule's own order-only name list to `tup_printf` as its own argument for the command and display strings (`do_rule`, `&r->order_only_inputs`; a bang macro's `bang_oo_inputs` are kept apart and never passed), and the `%i` branch walks that list appending `nle->path`, joined by single spaces; the `%N`-flag branch selects from the same list by order id. The list is built by the tokenizer the input list is built by, so a group is one entry spelled as its reference, a bin is its members, a glob its sorted matches, and a path named twice is two entries. Measured against tup: `: a.c | y.h z.h |>` writes `i=[y.h z.h]`; `| *.h sub/*.h` writes `i=[y.h z.h sub/s.h]` and `%2i` writes `sub/s.h`; `| <grp>` writes `i=[<grp>]` and `| sub/<grp>` writes `i=[sub/<grp>]`; `| {bn}` writes the bin's member; `| z.h z.h` writes `i=[z.h z.h]`; a rule in `sub/` writing `| ../z.h` writes `i=[../z.h]`; `!m = | y.h |> echo %i` called by `: a.c | z.h |> !m` writes `i=[z.h]`, and `!m = |> echo %2i` called by `: a.c | y.h z.h |> !m` writes `z.h`; `%2i` over one order-only input, and `%1i` over none, write nothing; a display string spells it too. putup spelled `%i` as the inputs, the same text as `%f`, at every expansion site, and refused `%Ni` as unsupported (#461)
- discharge: test "Scenario: A percent-i spells the rule's order-only inputs"
- discharge: test "Evaluator pattern expansion - %i spells the order-only inputs"

If a rule spells `%i` in its command or display string, then putup shall expand it to every
order-only operand the rule itself wrote after `|`, spelled as `%f` spells an input and joined by
single spaces; and `%Ni` to every operand of the N-th such token, or to nothing when the token
does not exist.

### REQ-OPERAND-DUPLICATE-INPUT-PRUNED

- conformance: tup-conformant
Expand Down Expand Up @@ -131,13 +143,25 @@ Tupfile.
If a rule spells a `%N`-flag whose letter is not one of `f`, `b`, `B`, `o` or `i`, then putup
shall reject the Tupfile.

### REQ-OPERAND-NUMBERED-ORDER-ONLY-UNSUPPORTED
### REQ-OPERAND-ORDER-ONLY-FLAG-COMMAND-ONLY

- conformance: deliberate-deviation
- reference: upstream expands `%Ni` to the N-th order-only input in a command string and refuses it elsewhere (`tup_printf`, the `%N`-flag branch, which is passed a null order-only list outside a command). putup does not bind that list yet and refuses `%Ni` everywhere, naming the issue; the alternative was to keep emitting it as literal text into a command line or a filename, which is the silent failure this area exists to end (#426)
- discharge: test "A numbered order-only input flag is refused as unsupported"
- conformance: tup-conformant
- reference: upstream passes a null order-only list to `tup_printf` for an output pattern and an input path (`parse_output_pattern`, `eval_path_list`), and both the `%i` branch and the `%N`-flag branch refuse on it with `%i is only valid in a command string`, the numbered one naming its number. Measured against tup: `|> %i.txt`, `|> out.txt | %i.txt` and `|> %1i.txt` are refused; `^ DISP[%i]^` is not (#461)
- discharge: test "Scenario: A percent-i in a rule with no order-only inputs or outside a command is refused"
- discharge: test "Evaluator pattern expansion - %i with no order-only inputs or outside a command is refused"

If a rule spells `%i` or `%Ni` in its outputs or extra outputs, then putup shall reject the
Tupfile.

### REQ-OPERAND-ORDER-ONLY-FLAG-NO-ORDER-ONLY-INPUTS

- conformance: tup-conformant
- reference: upstream refuses `%i` when the order-only name list is empty with `%i used in rule pattern and no order-only input files were specified` (`tup_printf`, the `i` branch, guarded on `ooinput_nl->num_entries == 0`), in the same function as the `%f` guard REQ-OPERAND-INPUT-FLAGS-NO-INPUTS conforms to; the numbered form never refuses, `%1i` over no order-only inputs expanding to nothing like `%1f`. The list is the rule's own, so a bang macro's order-only inputs do not fill it, and neither does a group written in the inputs section, which upstream treats as order-only for dependencies but not for `%i`. Measured against tup: a rule with inputs and none order-only, a rule with no inputs at all, `!m = | z.h |> echo %i` called by `: a.c |> !m`, `: a.c <grp> |> echo %i`, and a display string in a rule with no order-only inputs, all refused with the same message; `%1i` over none builds. putup expanded `%i` to the inputs and built (#461)
- discharge: test "Scenario: A percent-i in a rule with no order-only inputs or outside a command is refused"
- discharge: test "Evaluator pattern expansion - %i with no order-only inputs or outside a command is refused"

If a rule spells `%Ni`, then putup shall reject the Tupfile naming the issue that tracks it.
If a rule spells `%i` in its command or display string and it wrote no order-only inputs after
`|`, then putup shall reject the Tupfile.

### REQ-OPERAND-EXTENSION-FOREACH-ONLY

Expand Down
4 changes: 4 additions & 0 deletions src/cli/cmd_build.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,7 @@ auto serialize_command_nodes(
}

auto inputs = pup::graph::view<pup::graph::Inputs>(g, id);
auto order_only_inputs = pup::graph::view<pup::graph::OrderOnlyInputs>(g, id);
auto outputs = pup::graph::view<pup::graph::Outputs>(g, id);
auto& pool = pup::global_pool();

Expand All @@ -837,6 +838,7 @@ auto serialize_command_nodes(
.signature = pup::graph::compute_command_signature(g, id, state.path_cache),
.must_rerun = must_rerun_cmds.contains(id),
.inputs = std::move(inputs),
.order_only_inputs = std::move(order_only_inputs),
.outputs = std::move(outputs),
};
index.add_command(std::move(entry));
Expand Down Expand Up @@ -1427,6 +1429,7 @@ auto merge_out_of_scope_commands(
);
};
auto new_inputs = resolve_operands(cmd.inputs);
auto new_order_only_inputs = resolve_operands(cmd.order_only_inputs);
auto new_outputs = resolve_operands(cmd.outputs);
// An operand it could not carry means the record no longer describes what ran, so it
// keeps its outputs but stops claiming they are current.
Expand All @@ -1443,6 +1446,7 @@ auto merge_out_of_scope_commands(
.signature = cmd.signature,
.must_rerun = must_rerun,
.inputs = std::move(new_inputs),
.order_only_inputs = std::move(new_order_only_inputs),
.outputs = std::move(new_outputs),
});
old_to_new_cmd.set(cmd.id, new_cmd_id);
Expand Down
Loading
Loading