Skip to content

Commit 5997dcc

Browse files
donislawdevclaude
andcommitted
The ceiling refusal says the whole run's files, and the list of sizes is core's
- A run past the ceiling on files is told every file its targets ask for. The reader and the engine both refuse where the run crosses the ceiling and said the running total there, so ten targets of a million were told they asked for two million. The reader now adds its one refusal once every target is counted, still at the target that crossed, and the engine counts every target's files when it refuses. The number is an int64, since a recipe of a megabyte can hold more targets of a million than an int of 32 bits counts. - The list of files of one size is a single function in core, which engine.Uniform and the recipe reader both use, rather than a copy in each. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 7f12ae8 commit 5997dcc

7 files changed

Lines changed: 111 additions & 54 deletions

File tree

‎internal/core/limits.go‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,17 @@ var (
6464
"Or give the run more memory through the memory setting")
6565
)
6666

67-
// TooManyFilesInRun is what a run past MaxFilesPerRun asks for: the files it
68-
// came to at the target that crossed the ceiling, across all the targets it
69-
// has.
67+
// TooManyFilesInRun is what a run past MaxFilesPerRun asks for: every file of
68+
// every target, across all of them.
7069
//
7170
// Both the recipe reader and the engine count the run, and a recipe refused by
7271
// either reads the same - so the sentence is written here, once, rather than
73-
// beside each of them (O302).
74-
func TooManyFilesInRun(files, targets int) Said {
72+
// beside each of them (O302). The number is the whole run's, not the running
73+
// total at the target that crossed the ceiling where both of them refuse: ten
74+
// targets of a million were told they asked for two million (review of #219).
75+
// An int64 because a recipe of a megabyte holds tens of thousands of targets
76+
// of a million, which is past what an int of 32 bits counts.
77+
func TooManyFilesInRun(files int64, targets int) Said {
7578
return Says("core.TooManyFilesInRun", "this run asks for %s across %s",
7679
A("Files", SaysN("core.FilesInRun", "%d file", "%d files", A("Count", files))),
7780
A("Targets", SaysN("core.TargetsInRun", "%d target", "%d targets", A("Count", targets))))

‎internal/core/size.go‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,3 +332,23 @@ func BoundarySizes(limit int64) ([]int64, error) {
332332
}
333333
return []int64{limit - 1, limit, limit + 1}, nil
334334
}
335+
336+
// Uniform is n files of the same size, in a list reserved at its length.
337+
//
338+
// Here because the recipe reader builds the same list as the engine and cannot
339+
// import it, and two copies of one list were what a review of #219 named.
340+
// Reserved rather than grown: one append at a time, the list for a million
341+
// files cost 40 B a file to build for 8 B a file kept - every growth leaves the
342+
// last array to the collector - and the batch screen builds it on every key
343+
// (O302, measured on 2026-11-04: 41.7 MB for a target of a million, 8.0 MB
344+
// reserved).
345+
func Uniform(n int, bytes int64) []int64 {
346+
if n <= 0 {
347+
return nil
348+
}
349+
out := make([]int64, n)
350+
for i := range out {
351+
out[i] = bytes
352+
}
353+
return out
354+
}

‎internal/engine/engine.go‎

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -100,17 +100,9 @@ type SizeRange struct {
100100
Min, Max int64
101101
}
102102

103-
// Uniform is n files of the same size, which is what most targets ask for.
104-
func Uniform(n int, bytes int64) []int64 {
105-
if n <= 0 {
106-
return nil
107-
}
108-
out := make([]int64, n)
109-
for i := range out {
110-
out[i] = bytes
111-
}
112-
return out
113-
}
103+
// Uniform is n files of the same size, which is what most targets ask for. It
104+
// is core's, under the name the engine's callers reach for.
105+
func Uniform(n int, bytes int64) []int64 { return core.Uniform(n, bytes) }
114106

115107
// Options are the settings of one run.
116108
type Options struct {
@@ -486,7 +478,7 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF
486478
// reader's is.
487479
return nil, &RecipeError{
488480
Setting: core.TargetAddress(i+1, SettingCount),
489-
Detail: core.TooManyFilesInRun(totalFiles, len(targets)),
481+
Detail: core.TooManyFilesInRun(filesIn(targets), len(targets)),
490482
Because: core.TooManyFilesWhy,
491483
Remedy: core.TooManyFilesFix}
492484
}
@@ -498,6 +490,17 @@ func PlanContext(ctx context.Context, targets []Target, opt Options) ([]PlannedF
498490
return pl.out, nil
499491
}
500492

493+
// filesIn is every file the targets ask for, which is what a refusal of the
494+
// run's ceiling says - rather than the running total at the target that
495+
// crossed it, which is where the refusal is made.
496+
func filesIn(targets []Target) int64 {
497+
var n int64
498+
for i := range targets {
499+
n += int64(len(targets[i].Sizes))
500+
}
501+
return n
502+
}
503+
501504
// Run writes a planned set of files.
502505
//
503506
// Each file is written under a temporary name and only then renamed, so the

‎internal/guard/mutation/entries.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2609,17 +2609,33 @@
26092609
# The engine's address: the merged list, preset's targets first.
26102610
"the run's ceiling names the target by the merged list rather than the file",
26112611
"internal/recipe/recipe.go",
2612-
"p.add(at(t.ID).of(KeyCount), core.TooManyFilesInRun(planned+files, len(raw.Targets)),",
2613-
"p.add(targetSpot(i, t.ID).of(KeyCount), core.TooManyFilesInRun(planned+files, len(raw.Targets)),",
2612+
"crossedAt = at(t.ID).of(KeyCount)",
2613+
"crossedAt = targetSpot(i, t.ID).of(KeyCount)",
26142614
"TestTheTargetThatCrossesTheRunCeilingIsPlacedInTheFile",
26152615
),
26162616
(
26172617
"the run's ceiling marks the size of the batch that crossed it rather than its count",
26182618
"internal/recipe/recipe.go",
2619-
"p.add(at(t.ID).of(KeyCount), core.TooManyFilesInRun(planned+files, len(raw.Targets)),",
2620-
"p.add(at(t.ID).of(KeySize), core.TooManyFilesInRun(planned+files, len(raw.Targets)),",
2619+
"crossedAt = at(t.ID).of(KeyCount)",
2620+
"crossedAt = at(t.ID).of(KeySize)",
26212621
"TestARefusalAboutTheWholeRunMarksTheBatchThatCausedIt",
26222622
),
2623+
(
2624+
# Review of #219: the running total where the run crossed is what both
2625+
# layers said, and ten targets of a million were told two million.
2626+
"the run's ceiling says the files up to the target that crossed it rather than the whole run's",
2627+
"internal/recipe/recipe.go",
2628+
"p.add(crossedAt, core.TooManyFilesInRun(asked, len(raw.Targets)),",
2629+
"p.add(crossedAt, core.TooManyFilesInRun(int64(planned), len(raw.Targets)),",
2630+
"TestTheRunCeilingSaysWhatTheWholeRunAsksFor",
2631+
),
2632+
(
2633+
"the engine's ceiling says the files up to the target that crossed it rather than the whole run's",
2634+
"internal/engine/engine.go",
2635+
"Detail: core.TooManyFilesInRun(filesIn(targets), len(targets)),",
2636+
"Detail: core.TooManyFilesInRun(int64(totalFiles), len(targets)),",
2637+
"TestTheEngineCountsTheWholeRunOfTargetsHandedToItDirectly",
2638+
),
26232639
(
26242640
"a target past the run's ceiling has its list built before it is refused",
26252641
"internal/recipe/target.go",
@@ -2628,10 +2644,12 @@
26282644
"TestATargetPastTheRunCeilingIsNotBuilt",
26292645
),
26302646
(
2647+
# In core since the review of #219, which named the reader's own copy of
2648+
# engine.Uniform. Both now take this one.
26312649
"a target's list of sizes is grown one file at a time again",
2632-
"internal/recipe/target.go",
2633-
"\tsizes := make([]int64, count)\n\tfor i := range sizes {\n\t\tsizes[i] = each\n\t}",
2634-
"\tvar sizes []int64\n\tfor range count {\n\t\tsizes = append(sizes, each)\n\t}",
2650+
"internal/core/size.go",
2651+
"\tout := make([]int64, n)\n\tfor i := range out {\n\t\tout[i] = bytes\n\t}",
2652+
"\tvar out []int64\n\tfor range n {\n\t\tout = append(out, bytes)\n\t}",
26352653
"TestATargetsListOfSizesIsReservedAtItsLength",
26362654
),
26372655
(

‎internal/guard/runceiling_test.go‎

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,23 @@ func TestTheRunCeilingSitsExactlyAtAMillion(t *testing.T) {
134134
}
135135
}
136136

137+
// The refusal says the files the whole run asks for, at the target that
138+
// crossed the ceiling.
139+
//
140+
// Both are refused where the run crosses, so the running total there was the
141+
// number they said - ten targets of a million were told they asked for two
142+
// million across ten targets (review of #219, measured with the binary before
143+
// O302 as well). The target after the one that crosses is what tells the two
144+
// numbers apart.
145+
func TestTheRunCeilingSaysWhatTheWholeRunAsksFor(t *testing.T) {
146+
src := targetsOf(1, core.MaxFilesPerRun, core.MaxFilesPerRun)
147+
ceilingAt(t, src, 2)
148+
_, err := recipe.Parse(src, "whole run")
149+
if !strings.Contains(err.Error(), "2000001 files across 3 targets") {
150+
t.Errorf("the refusal does not say the files the whole run asks for, 2000001:\n%v", err)
151+
}
152+
}
153+
137154
// A run past the ceiling is reported together with whatever else is wrong.
138155
//
139156
// Every problem of a recipe arrives at once (RC7). While the engine counted the
@@ -241,19 +258,25 @@ targets:
241258
// targets by whoever calls it, though, and planCapacity reserves nothing past
242259
// the ceiling on the word of this refusal. The memory setting is there so a
243260
// build that lost the refusal plans until the plan ceiling stops it, a few
244-
// megabytes in, rather than a million files.
261+
// megabytes in, rather than a million files. A third target follows the one
262+
// that crosses, so the number the refusal says is asked to be the whole run's
263+
// rather than the running total where it stopped.
245264
func TestTheEngineCountsTheWholeRunOfTargetsHandedToItDirectly(t *testing.T) {
246265
targets := []engine.Target{
247266
{ID: "small", Format: "txt", Sizes: engine.Uniform(1, 1024)},
248267
{ID: "big", Format: "txt", Sizes: engine.Uniform(core.MaxFilesPerRun, 1024)},
268+
{ID: "after", Format: "txt", Sizes: engine.Uniform(core.MaxFilesPerRun, 1024)},
249269
}
250270
_, err := engine.Plan(targets, engine.Options{OutDir: t.TempDir(), ManifestName: "manifest.json", Memory: core.MemoryOf(32 << 20)})
251271
if err == nil {
252-
t.Fatal("a run of one file more than the ceiling was planned")
272+
t.Fatal("a run of a million files more than the ceiling was planned")
253273
}
254-
if !strings.Contains(err.Error(), "1000001 files across 2 targets") {
274+
if !strings.Contains(err.Error(), "files across 3 targets") {
255275
t.Fatalf("the run was refused for something other than its count of files: %v", err)
256276
}
277+
if !strings.Contains(err.Error(), "2000001 files across 3 targets") {
278+
t.Errorf("the refusal does not say the files the whole run asks for, 2000001: %v", err)
279+
}
257280
var about interface{ AboutSetting() string }
258281
if !errors.As(err, &about) || about.AboutSetting() != core.TargetAddress(2, engine.SettingCount) {
259282
t.Errorf("the refusal is not addressed to the target that crossed the ceiling, %s: %v",

‎internal/recipe/recipe.go‎

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -448,17 +448,21 @@ func (raw rawRecipe) readTargets(p *problems, def Defaults, fromPreset int) []Ta
448448
// adds more than the ceiling: a count past it is refused as the target's
449449
// own and asks for nothing.
450450
planned := 0
451+
// What every target asks for, which is the number the refusal says. The
452+
// running total stops at the target that crossed the ceiling, and ten
453+
// targets of a million were told they asked for two (review of #219).
454+
var asked int64
455+
// The target that takes the run past the ceiling, which is the one
456+
// somebody can shrink. Only one is: every target after it is past the
457+
// ceiling as well, and saying so of each would bury the one that matters.
458+
crossedAt := ""
451459
for i, rt := range raw.Targets {
452460
at := spotOfTarget(i, fromPreset)
453461
room := core.MaxFilesPerRun - planned
454462
t, files := rt.validate(p, at, def, room)
455-
// Refused at the target that takes the run past the ceiling, which is
456-
// the one somebody can shrink, and in the engine's words. Once, because
457-
// every target after it is past the ceiling as well, and saying so of
458-
// each would bury the one that matters.
463+
asked += int64(files)
459464
if room >= 0 && files > room {
460-
p.add(at(t.ID).of(KeyCount), core.TooManyFilesInRun(planned+files, len(raw.Targets)),
461-
core.TooManyFilesWhy, core.TooManyFilesFix)
465+
crossedAt = at(t.ID).of(KeyCount)
462466
}
463467
planned = min(planned+files, core.MaxFilesPerRun+1)
464468
if t.ID != "" {
@@ -470,6 +474,10 @@ func (raw rawRecipe) readTargets(p *problems, def Defaults, fromPreset int) []Ta
470474
}
471475
targets = append(targets, t)
472476
}
477+
// Said once every target is counted, in the engine's words.
478+
if crossedAt != "" {
479+
p.add(crossedAt, core.TooManyFilesInRun(asked, len(raw.Targets)), core.TooManyFilesWhy, core.TooManyFilesFix)
480+
}
473481
return targets
474482
}
475483

‎internal/recipe/target.go‎

Lines changed: 3 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) {
245245
core.Says("recipe.UseSettingSuchAs2mb512kb", "use {a} {setting} such as 2mb, 512kb or a plain byte count"))
246246
break
247247
}
248-
t.Sizes = sizesOf(count, n)
248+
t.Sizes = core.Uniform(count, n)
249249

250250
case rt.SizeRange != nil:
251251
// Placed above contains for the same reason size is: a stated size
@@ -268,15 +268,15 @@ func (rt rawTarget) resolveSize(p *problems, where spot, count int, t *Target) {
268268
// The values stay zero here and the engine replaces them. What this
269269
// list carries at this point is the number of files, exactly as it
270270
// does for contains.
271-
t.Sizes = sizesOf(count, 0)
271+
t.Sizes = core.Uniform(count, 0)
272272

273273
case rt.Contains != nil:
274274
// The fourth way of declaring a size, and not an exception to the rule.
275275
// Every member has its own size, so the total follows from the parts
276276
// and a dry run still reports a number without generating anything.
277277
// See ARCHITECTURE.md section 9.
278278
t.SizeFromContents = true
279-
t.Sizes = sizesOf(count, 0)
279+
t.Sizes = core.Uniform(count, 0)
280280

281281
default:
282282
p.add(where.of("size"), core.Says("recipe.HasNoSetting", "%s has no {setting}", core.A("At", where)),
@@ -400,24 +400,6 @@ func boundaryText(p *problems, where spot, v *scalar) string {
400400
return s
401401
}
402402

403-
// sizesOf is count files of one size, in a list reserved at its length.
404-
//
405-
// Grown one append at a time, the list for a million files cost 40 B a file to
406-
// build for 8 B a file kept - every growth leaves the last array to the
407-
// collector - and the batch screen builds it on every key (O302, measured
408-
// 2026-11-04: 41.7 MB for one target of a million). engine.Uniform is the same
409-
// list on the side of the engine, which this package cannot import.
410-
func sizesOf(count int, each int64) []int64 {
411-
if count <= 0 {
412-
return nil
413-
}
414-
sizes := make([]int64, count)
415-
for i := range sizes {
416-
sizes[i] = each
417-
}
418-
return sizes
419-
}
420-
421403
// boundarySizes turns a limit into the three sizes a boundary set means.
422404
//
423405
// This is the case the whole tool is pointed at: an application says it

0 commit comments

Comments
 (0)