commit 884b91f

shrub  ·  2026-05-13 07:41:31 +0000 UTC
parent 4aa3f55
fix suffix rule chaining and diffrentiate between implcitly inferred and explicit prereqs
12 files changed,  +311, -108
M TODO
M TODO
+4, -2
 1@@ -13,14 +13,12 @@ targets:
 2 a lot of things like .PRECIOUS etc dont map to ninja, those are non-goals
 3 
 4 functions:
 5- abspath
 6  flavor
 7  file
 8  error
 9  warning
10  intcmp
11  let
12- realpath
13   
14 directives
15  undefine
16@@ -31,3 +29,7 @@ directives
17 special vars
18  .VARIABLES
19 
20+other
21+ feature gate flags like -M posix2024, -M posix2008, -M gnu
22+ eventually handle bsd make and kati-specific features once we have feature gating. never evaluate two feature sets at once
23+ 
+12, -5
 1@@ -63,9 +63,12 @@ issource(const char *path)
 2 static int
 3 iscompile(const struct Target *t)
 4 {
 5-	if (t->recipes.n != 1 || t->prereqs.n != 1)
 6+	const char *src;
 7+
 8+	if (t->recipes.n != 1 || totalprereqs(t) != 1)
 9 		return 0;
10-	if (!issource(t->prereqs.v[0]))
11+	src = firstprereq(t);
12+	if (!src || !issource(src))
13 		return 0;
14 	if (strstr(t->recipes.v[0].body, " -c ") || strstr(t->recipes.v[0].body, "\t-c ") ||
15 	    strstr(t->recipes.v[0].body, " -c\t") || hassuffix(t->recipes.v[0].body, " -c"))
16@@ -102,9 +105,13 @@ translateauto(const char *s, const struct Target *t)
17 		} else if (s[i + 1] == '@') {
18 			val = t->name;
19 		} else if (s[i + 1] == '<') {
20-			val = t->prereqs.n ? t->prereqs.v[0] : "";
21+			val = firstprereq(t);
22+			if (!val)
23+				val = "";
24 		} else if (s[i + 1] == '^' || s[i + 1] == '+' || s[i + 1] == '?') {
25-			val = t->prereqs.n ? t->prereqs.v[0] : "";
26+			val = firstprereq(t);
27+			if (!val)
28+				val = "";
29 		}
30 
31 		if (!val) {
32@@ -164,7 +171,7 @@ gencompcmd(const struct Graph *graph, const char *path)
33 		emitjson(fp, cmd);
34 		fprintf(fp, "\",\n");
35 		fprintf(fp, "    \"file\": \"");
36-		emitjson(fp, graph->v[i].prereqs.v[0]);
37+		emitjson(fp, firstprereq(&graph->v[i]));
38 		fprintf(fp, "\",\n");
39 		fprintf(fp, "    \"output\": \"");
40 		emitjson(fp, graph->v[i].name);
+7, -1
 1@@ -35,7 +35,7 @@ emitnode(FILE *fp, size_t i, const struct Target *t)
 2 	fprintf(fp, "\"");
 3 	if (t->recipes.n > 0) {
 4 		fprintf(fp, ", shape=box");
 5-	} else if (t->prereqs.n > 0 || t->order_only.n > 0) {
 6+	} else if (t->prereqs.n > 0 || t->impprereqs.n > 0 || t->order_only.n > 0) {
 7 		fprintf(fp, ", shape=box, style=dashed");
 8 	} else {
 9 		fprintf(fp, ", shape=box");
10@@ -88,6 +88,12 @@ emitedges(FILE *fp, const struct Graph *graph, size_t i, const struct Target *t)
11 			continue;
12 		fprintf(fp, "  n%zu -> n%zu;\n", (size_t)(dep - graph->v), i);
13 	}
14+	for (j = 0; j < t->impprereqs.n; j++) {
15+		dep = findctarget(graph, t->impprereqs.v[j]);
16+		if (!dep)
17+			continue;
18+		fprintf(fp, "  n%zu -> n%zu;\n", (size_t)(dep - graph->v), i);
19+	}
20 	for (j = 0; j < t->order_only.n; j++) {
21 		dep = findctarget(graph, t->order_only.v[j]);
22 		if (!dep)
+17, -2
 1@@ -147,6 +147,13 @@ joinlocalprereqs(const struct Target *t)
 2 	char *s;
 3 
 4 	memset(&list, 0, sizeof(list));
 5+	for (i = 0; i < t->impprereqs.n; i++) {
 6+		char *name;
 7+
 8+		name = localpath(t, t->impprereqs.v[i]);
 9+		addstr(&list, name);
10+		free(name);
11+	}
12 	for (i = 0; i < t->prereqs.n; i++) {
13 		char *name;
14 
15@@ -191,7 +198,10 @@ translateauto(const char *s, const struct Target *t)
16 			val = localpath(t, t->name);
17 			mustfree = 1;
18 		} else if (s[i + 1] == '<') {
19-			val = t->prereqs.n ? localpath(t, t->prereqs.v[0]) : xstrdup("");
20+			const char *prereq;
21+
22+			prereq = firstprereq(t);
23+			val = prereq ? localpath(t, prereq) : xstrdup("");
24 			mustfree = 1;
25 		} else if (s[i + 1] == '^' || s[i + 1] == '+' || s[i + 1] == '?') {
26 			val = joinlocalprereqs(t);
27@@ -305,6 +315,8 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
28 			fprintf(fp, "build %s: r%d", graph->v[i].name, id);
29 			if (graph->v[i].phony)
30 				fprintf(fp, " __shin_always_build__");
31+			for (j = 0; j < graph->v[i].impprereqs.n; j++)
32+				fprintf(fp, " %s", graph->v[i].impprereqs.v[j]);
33 			for (j = 0; j < graph->v[i].prereqs.n; j++)
34 				fprintf(fp, " %s", graph->v[i].prereqs.v[j]);
35 			if (graph->v[i].order_only.n) {
36@@ -317,10 +329,13 @@ genninjafile(const struct Graph *graph, const char *path, const char *prefix, in
37 				fprintf(fp, "  description = [%s] build %s\n\n", prefix, graph->v[i].name);
38 			else
39 				fprintf(fp, "  description = build %s\n\n", graph->v[i].name);
40-		} else if (graph->v[i].defined || graph->v[i].prereqs.n > 0 || graph->v[i].order_only.n > 0) {
41+		} else if (graph->v[i].defined || graph->v[i].prereqs.n > 0 ||
42+		           graph->v[i].impprereqs.n > 0 || graph->v[i].order_only.n > 0) {
43 			fprintf(fp, "build %s: phony", graph->v[i].name);
44 			if (graph->v[i].phony)
45 				fprintf(fp, " __shin_always_build__");
46+			for (j = 0; j < graph->v[i].impprereqs.n; j++)
47+				fprintf(fp, " %s", graph->v[i].impprereqs.v[j]);
48 			for (j = 0; j < graph->v[i].prereqs.n; j++)
49 				fprintf(fp, " %s", graph->v[i].prereqs.v[j]);
50 			if (graph->v[i].order_only.n) {
+1, -0
1@@ -170,6 +170,7 @@ dumpgraph(const struct Graph *graph)
2 			printf(" [owner=%s]", t->owner);
3 		putchar('\n');
4 		dumpstrlist("prereqs", &t->prereqs, 2);
5+		dumpstrlist("implicit-prereqs", &t->impprereqs, 2);
6 		dumpstrlist("order-only", &t->order_only, 2);
7 		dumprecipes(&t->recipes, 2);
8 	}
+7, -8
 1@@ -171,17 +171,16 @@ instpatrule(const struct PatRules *rules, const struct Graph *graph, struct Targ
 2 			free(stem);
 3 			continue;
 4 		}
 5-		/* prepend so $< is the pattern source, not an extra dep
 6-		 * added by an explicit rule for the same target */
 7+		/* prepend so $< is the inferred pattern source, before explicit prereqs */
 8 		if (rules->v[i].prereqs.n > 0) {
 9 			size_t np = rules->v[i].prereqs.n;
10-			t->prereqs.v = xrealloc(t->prereqs.v,
11-			                        (t->prereqs.n + np) * sizeof(t->prereqs.v[0]));
12-			memmove(t->prereqs.v + np, t->prereqs.v,
13-			        t->prereqs.n * sizeof(t->prereqs.v[0]));
14+			t->impprereqs.v = xrealloc(t->impprereqs.v,
15+			                           (t->impprereqs.n + np) * sizeof(t->impprereqs.v[0]));
16+			memmove(t->impprereqs.v + np, t->impprereqs.v,
17+			        t->impprereqs.n * sizeof(t->impprereqs.v[0]));
18 			for (j = 0; j < np; j++)
19-				t->prereqs.v[j] = applystem(rules->v[i].prereqs.v[j], stem);
20-			t->prereqs.n += np;
21+				t->impprereqs.v[j] = applystem(rules->v[i].prereqs.v[j], stem);
22+			t->impprereqs.n += np;
23 		}
24 		for (j = 0; j < rules->v[i].order_only.n; j++) {
25 			char *s;
+69, -44
  1@@ -241,7 +241,7 @@ addtassign(struct GraphState *gs, const struct AssignNode *assign)
  2 int
  3 buildgraph(const struct RuleSet *ruleset, struct Graph *graph)
  4 {
  5-	size_t i, j, start;
  6+	size_t i, j;
  7 	struct GraphState gs;
  8 	struct EvalCtx ctx;
  9 
 10@@ -291,12 +291,18 @@ buildgraph(const struct RuleSet *ruleset, struct Graph *graph)
 11 		}
 12 	}
 13 
 14-	start = 0;
 15-	while (start < graph->n) {
 16-		size_t current_n = graph->n;
 17-		for (i = start; i < current_n; i++) {
 18+	for (;;) {
 19+		int changed;
 20+
 21+		changed = 0;
 22+		for (i = 0; i < graph->n; i++) {
 23 			struct Target *t = &graph->v[i];
 24 			size_t nprereqs;
 25+			size_t old_graph_n, old_recipes_n, old_impprereqs_n;
 26+
 27+			old_graph_n = graph->n;
 28+			old_recipes_n = t->recipes.n;
 29+			old_impprereqs_n = t->impprereqs.n;
 30 			/* for targets with no recipe, try a pattern rule or
 31 			 * suffix rule. existing prereqs dont block this,
 32 			 * the rule may still supply the recipe */
 33@@ -317,9 +323,17 @@ buildgraph(const struct RuleSet *ruleset, struct Graph *graph)
 34 				if (targetctx.errors)
 35 					ctx.errors += targetctx.errors;
 36 			}
 37-			nprereqs = t->prereqs.n;
 38+			if (graph->n != old_graph_n || t->recipes.n != old_recipes_n ||
 39+			    t->impprereqs.n != old_impprereqs_n)
 40+				changed = 1;
 41+			nprereqs = totalprereqs(t);
 42 			for (j = 0; j < nprereqs; j++) {
 43-				const char *prereq = t->prereqs.v[j];
 44+				const char *prereq;
 45+
 46+				if (j < t->impprereqs.n)
 47+					prereq = t->impprereqs.v[j];
 48+				else
 49+					prereq = t->prereqs.v[j - t->impprereqs.n];
 50 
 51 				if (!findtarget(graph, prereq)) {
 52 					struct Target *nt;
 53@@ -330,11 +344,13 @@ buildgraph(const struct RuleSet *ruleset, struct Graph *graph)
 54 					nt->name = intern(prereq);
 55 					if (hasword(gs.phony, nt->name))
 56 						nt->phony = 1;
 57+					changed = 1;
 58 					t = &graph->v[i];
 59 				}
 60 			}
 61 		}
 62-		start = current_n;
 63+		if (!changed)
 64+			break;
 65 	}
 66 
 67 	for (i = 0; i < gs.ntas; i++) {
 68@@ -370,43 +386,51 @@ expandgraph(struct Graph *graph)
 69 		memset(&new_recipes, 0, sizeof(new_recipes));
 70 		ctx.env = &t->env;
 71 		ctx.auto_target = t->name;
 72-		ctx.auto_prereqs = &t->prereqs;
 73-		ctx.avoid_io = 1;
 74-		ctx.side_effects = &side_effects;
 75-
 76-		for (k = 0; k < t->recipes.n; k++) {
 77-			struct Recipe *r = &t->recipes.v[k];
 78-			char *exp;
 79-
 80-			freestrs(&side_effects);
 81-			exp = expandstr(&ctx, r->body);
 82-			for (j = 0; j < side_effects.n; j++) {
 83-				struct Recipe *nr;
 84-
 85-				new_recipes.v = xrealloc(new_recipes.v,
 86-				                         (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
 87-				nr = &new_recipes.v[new_recipes.n++];
 88-				memset(nr, 0, sizeof(*nr));
 89-				nr->body = xstrdup(side_effects.v[j]);
 90-				nr->silent = r->silent;
 91-				nr->ignore = r->ignore;
 92-			}
 93-			if (exp[0]) {
 94-				struct Recipe *nr;
 95-
 96-				new_recipes.v = xrealloc(new_recipes.v,
 97-				                         (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
 98-				nr = &new_recipes.v[new_recipes.n++];
 99-				*nr = *r;
100-				nr->body = exp;
101-				freesubmake(&nr->sm);
102-				nr->submake = parsesubmake(&nr->sm, nr->body);
103-				free(r->body);
104-				r->body = 0;
105-				memset(&r->sm, 0, sizeof(r->sm));
106-			} else {
107-				free(exp);
108+		{
109+			struct StrList allprereqs;
110+
111+			memset(&allprereqs, 0, sizeof(allprereqs));
112+			addwords(&allprereqs, &t->impprereqs);
113+			addwords(&allprereqs, &t->prereqs);
114+			ctx.auto_prereqs = &allprereqs;
115+			ctx.avoid_io = 1;
116+			ctx.side_effects = &side_effects;
117+
118+			for (k = 0; k < t->recipes.n; k++) {
119+				struct Recipe *r = &t->recipes.v[k];
120+				char *exp;
121+
122+				freestrs(&side_effects);
123+				exp = expandstr(&ctx, r->body);
124+				for (j = 0; j < side_effects.n; j++) {
125+					struct Recipe *nr;
126+
127+					new_recipes.v = xrealloc(new_recipes.v,
128+					                         (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
129+					nr = &new_recipes.v[new_recipes.n++];
130+					memset(nr, 0, sizeof(*nr));
131+					nr->body = xstrdup(side_effects.v[j]);
132+					nr->silent = r->silent;
133+					nr->ignore = r->ignore;
134+				}
135+				if (exp[0]) {
136+					struct Recipe *nr;
137+
138+					new_recipes.v = xrealloc(new_recipes.v,
139+					                         (new_recipes.n + 1) * sizeof(new_recipes.v[0]));
140+					nr = &new_recipes.v[new_recipes.n++];
141+					*nr = *r;
142+					nr->body = exp;
143+					freesubmake(&nr->sm);
144+					nr->submake = parsesubmake(&nr->sm, nr->body);
145+					free(r->body);
146+					r->body = 0;
147+					memset(&r->sm, 0, sizeof(r->sm));
148+				} else {
149+					free(exp);
150+				}
151 			}
152+			freestrs(&allprereqs);
153 		}
154 
155 		freestrs(&side_effects);
156@@ -431,6 +455,7 @@ freegraph(struct Graph *graph)
157 	for (i = 0; i < graph->n; i++) {
158 		free(graph->v[i].owner);
159 		freestrs(&graph->v[i].prereqs);
160+		freestrs(&graph->v[i].impprereqs);
161 		freestrs(&graph->v[i].order_only);
162 		freerecipes(&graph->v[i].recipes);
163 		freeenv(&graph->v[i].env);
+4, -0
 1@@ -151,6 +151,8 @@ markreachable(const struct Graph *graph, const char *name, unsigned char *seen)
 2 	seen[idx] = 1;
 3 	for (i = 0; i < t->prereqs.n; i++)
 4 		markreachable(graph, t->prereqs.v[i], seen);
 5+	for (i = 0; i < t->impprereqs.n; i++)
 6+		markreachable(graph, t->impprereqs.v[i], seen);
 7 	for (i = 0; i < t->order_only.n; i++)
 8 		markreachable(graph, t->order_only.v[i], seen);
 9 }
10@@ -292,6 +294,7 @@ scopegraph(struct Graph *graph, const char *prefix)
11 		else
12 			graph->v[i].owner = 0;
13 		scopelist(&graph->v[i].prereqs, prefix);
14+		scopelist(&graph->v[i].impprereqs, prefix);
15 		scopelist(&graph->v[i].order_only, prefix);
16 		scoperecipes(&graph->v[i].recipes, prefix);
17 	}
18@@ -330,6 +333,7 @@ mergetarget(struct Graph *graph, const struct Target *src)
19 	if (src->defined)
20 		dst->dcolon = src->dcolon;
21 	addwords(&dst->prereqs, &src->prereqs);
22+	addwords(&dst->impprereqs, &src->impprereqs);
23 	addwords(&dst->order_only, &src->order_only);
24 	addrecipes(&dst->recipes, &src->recipes);
25 	return 0;
+4, -0
 1@@ -59,6 +59,7 @@ void addnode(struct NodeList *list, struct Node node);
 2 void splitwords(struct StrList *out, const char *s, size_t n);
 3 char *cat3(const char *a, const char *b, const char *c);
 4 void addwords(struct StrList *dest, const struct StrList *src);
 5+void adduniqwords(struct StrList *dest, const struct StrList *src);
 6 void addrecipe(struct RecipeList *dest, const char *raw);
 7 void addrecipes(struct RecipeList *dest, const struct RecipeList *src);
 8 void freestrs(struct StrList *list);
 9@@ -70,6 +71,9 @@ void envsetvar(struct Env *env, const char *name, char *val, int simple, enum Or
10                int exported);
11 struct Target *findtarget(struct Graph *graph, const char *name);
12 const struct Target *findctarget(const struct Graph *graph, const char *name);
13+const char *firstprereq(const struct Target *t);
14+char *joinallprereqs(const struct Target *t, const char *sep);
15+size_t totalprereqs(const struct Target *t);
16 void initspecialtargets(struct SpecialTargets *targets);
17 void updatespecialassign(struct SpecialTargets *targets, const char *lhs, const char *rhs);
18 int handlespecialrule(struct SpecialTargets *targets, const struct RuleNode *rule);
+141, -45
  1@@ -7,6 +7,8 @@
  2 
  3 extern char **environ;
  4 
  5+static int matchsuf(const char *suf, const char *name, char **stem);
  6+
  7 static int
  8 sufeq(const char *a, const char *b)
  9 {
 10@@ -34,11 +36,71 @@ sufactive(const struct SuffixList *list, const char *suf)
 11 static int
 12 pathorargetexists(const struct Graph *graph, const char *name)
 13 {
 14-	if (findctarget(graph, name))
 15+	const struct Target *t;
 16+
 17+	t = findctarget(graph, name);
 18+	if (t && (t->defined || t->recipes.n > 0 || totalprereqs(t) > 0))
 19 		return 1;
 20 	return access(name, F_OK) == 0;
 21 }
 22 
 23+/* do multi layer suf rule inference: check whether a missing suffix rule
 24+ * source can be created by another suffix rule or suffix rule chain, so
 25+ * we can infer things like .ssa -> .s -> .o without inventing fkae prereqs 
 26+ * such as all.scd. (examples from hare compiler makefile which makes 
 27+ * quite heavy use of this) */
 28+static int
 29+sufbuildable(const struct SufRules *rules, const struct Graph *graph, const char *name, size_t depth)
 30+{
 31+	size_t i;
 32+
 33+	if (pathorargetexists(graph, name))
 34+		return 1;
 35+	if (depth > rules->active.n + rules->nsufs + rules->nsinglesuf)
 36+		return 0;
 37+
 38+	for (i = 0; i < rules->nsufs; i++) {
 39+		size_t j;
 40+		char *stem;
 41+
 42+		if (sufidx(&rules->active, rules->sufs[i].to) < 0 ||
 43+		    sufidx(&rules->active, rules->sufs[i].from) < 0)
 44+			continue;
 45+		if (!matchsuf(rules->sufs[i].to, name, &stem))
 46+			continue;
 47+		j = strlen(stem);
 48+		free(stem);
 49+		{
 50+			char *src;
 51+			int ok;
 52+
 53+			src = xmalloc(j + strlen(rules->sufs[i].from) + 1);
 54+			memcpy(src, name, j);
 55+			strcpy(src + j, rules->sufs[i].from);
 56+			ok = sufbuildable(rules, graph, src, depth + 1);
 57+			free(src);
 58+			if (ok)
 59+				return 1;
 60+		}
 61+	}
 62+
 63+	if (strchr(name, '.'))
 64+		return 0;
 65+	for (i = 0; i < rules->nsinglesuf; i++) {
 66+		char *src;
 67+		int ok;
 68+
 69+		if (sufidx(&rules->active, rules->singlesuf[i].from) < 0)
 70+			continue;
 71+		src = cat3(name, "", rules->singlesuf[i].from);
 72+		ok = sufbuildable(rules, graph, src, depth + 1);
 73+		free(src);
 74+		if (ok)
 75+			return 1;
 76+	}
 77+	return 0;
 78+}
 79+
 80 int
 81 issuf(const char *s, char **from, char **to)
 82 {
 83@@ -123,9 +185,11 @@ expandauto(const char *s, const struct Target *t, const char *stem)
 84 			if (s[i + 1] == '@') {
 85 				val = t->name;
 86 			} else if (s[i + 1] == '<') {
 87-				val = t->prereqs.n > 0 ? t->prereqs.v[0] : "";
 88+				val = firstprereq(t);
 89+				if (!val)
 90+					val = "";
 91 			} else if (s[i + 1] == '^' || s[i + 1] == '+' || s[i + 1] == '?') {
 92-				val = joinstrs(&t->prereqs, " ");
 93+				val = joinallprereqs(t, " ");
 94 				mustfree = 1;
 95 			} else if (s[i + 1] == '*') {
 96 				val = stem ? stem : "";
 97@@ -315,36 +379,50 @@ clearsufs(struct SuffixList *list)
 98 int
 99 collectsufrule(struct SufRules *rules, const struct RuleNode *rule)
100 {
101-	char *from, *to;
102+	size_t i;
103 
104-	if (rule->targets.n != 1 || rule->prereqs.n != 0 || rule->order_only.n != 0)
105+	if (rule->targets.n == 0 || rule->prereqs.n != 0 || rule->order_only.n != 0)
106 		return 0;
107-	if (issinglesuf(rule->targets.v[0], &from)) {
108-		size_t k;
109-		struct SingleSufRule *sr;
110+	for (i = 0; i < rule->targets.n; i++) {
111+		char *from, *to;
112 
113-		for (k = 0; k < rules->nsinglesuf; k++) {
114-			if (strcmp(rules->singlesuf[k].from, from) == 0) {
115-				free(from);
116-				freerecipes(&rules->singlesuf[k].recipes);
117-				memset(&rules->singlesuf[k].recipes, 0, sizeof(rules->singlesuf[k].recipes));
118-				addrecipes(&rules->singlesuf[k].recipes, &rule->recipes);
119-				return 1;
120-			}
121+		if (issinglesuf(rule->targets.v[i], &from)) {
122+			free(from);
123+			continue;
124+		}
125+		if (issuf(rule->targets.v[i], &from, &to)) {
126+			free(from);
127+			free(to);
128+			continue;
129 		}
130-		rules->singlesuf = xrealloc(rules->singlesuf,
131-		                            (rules->nsinglesuf + 1) * sizeof(rules->singlesuf[0]));
132-		sr = &rules->singlesuf[rules->nsinglesuf++];
133-		memset(sr, 0, sizeof(*sr));
134-		sr->from = from;
135-		addrecipes(&sr->recipes, &rule->recipes);
136-		return 1;
137-	}
138-	if (!issuf(rule->targets.v[0], &from, &to))
139 		return 0;
140-	{
141+	}
142+	for (i = 0; i < rule->targets.n; i++) {
143+		char *from, *to;
144 		size_t k;
145 
146+		if (issinglesuf(rule->targets.v[i], &from)) {
147+			struct SingleSufRule *sr;
148+
149+			for (k = 0; k < rules->nsinglesuf; k++) {
150+				if (strcmp(rules->singlesuf[k].from, from) == 0) {
151+					free(from);
152+					freerecipes(&rules->singlesuf[k].recipes);
153+					memset(&rules->singlesuf[k].recipes, 0, sizeof(rules->singlesuf[k].recipes));
154+					addrecipes(&rules->singlesuf[k].recipes, &rule->recipes);
155+					goto next_target;
156+				}
157+			}
158+			rules->singlesuf = xrealloc(rules->singlesuf,
159+			                            (rules->nsinglesuf + 1) * sizeof(rules->singlesuf[0]));
160+			sr = &rules->singlesuf[rules->nsinglesuf++];
161+			memset(sr, 0, sizeof(*sr));
162+			sr->from = from;
163+			addrecipes(&sr->recipes, &rule->recipes);
164+			continue;
165+		}
166+		issuf(rule->targets.v[i], &from, &to);
167+
168 		for (k = 0; k < rules->nsufs; k++) {
169 			if (strcmp(rules->sufs[k].from, from) == 0 &&
170 			    strcmp(rules->sufs[k].to, to) == 0) {
171@@ -353,16 +431,18 @@ collectsufrule(struct SufRules *rules, const struct RuleNode *rule)
172 				freerecipes(&rules->sufs[k].recipes);
173 				memset(&rules->sufs[k].recipes, 0, sizeof(rules->sufs[k].recipes));
174 				addrecipes(&rules->sufs[k].recipes, &rule->recipes);
175-				return 1;
176+				goto next_target;
177 			}
178 		}
179+		rules->sufs = xrealloc(rules->sufs, (rules->nsufs + 1) * sizeof(rules->sufs[0]));
180+		memset(&rules->sufs[rules->nsufs], 0, sizeof(rules->sufs[rules->nsufs]));
181+		rules->sufs[rules->nsufs].from = from;
182+		rules->sufs[rules->nsufs].to = to;
183+		addrecipes(&rules->sufs[rules->nsufs].recipes, &rule->recipes);
184+		rules->nsufs++;
185+	next_target:
186+		;
187 	}
188-	rules->sufs = xrealloc(rules->sufs, (rules->nsufs + 1) * sizeof(rules->sufs[0]));
189-	memset(&rules->sufs[rules->nsufs], 0, sizeof(rules->sufs[rules->nsufs]));
190-	rules->sufs[rules->nsufs].from = from;
191-	rules->sufs[rules->nsufs].to = to;
192-	addrecipes(&rules->sufs[rules->nsufs].recipes, &rule->recipes);
193-	rules->nsufs++;
194 	return 1;
195 }
196 
197@@ -380,6 +460,7 @@ instsufrule(const struct SufRules *rules,
198 		for (j = 0; j < rules->active.n; j++) {
199 			for (r = 0; r < rules->nsufs; r++) {
200 				char *stem, *src;
201+				int exists;
202 
203 				if (!sufeq(rules->sufs[r].to, rules->active.v[i]))
204 					continue;
205@@ -388,15 +469,23 @@ instsufrule(const struct SufRules *rules,
206 				if (!matchsuf(rules->sufs[r].to, t->name, &stem))
207 					continue;
208 				src = cat3(stem, "", rules->sufs[r].from);
209-				if (!pathorargetexists(graph, src)) {
210+				exists = pathorargetexists(graph, src);
211+				if (!exists && !sufbuildable(rules, graph, src, 0)) {
212 					free(stem);
213 					free(src);
214 					continue;
215 				}
216-				t->prereqs.v = xrealloc(t->prereqs.v, (t->prereqs.n + 1) * sizeof(t->prereqs.v[0]));
217-				memmove(t->prereqs.v + 1, t->prereqs.v, t->prereqs.n * sizeof(t->prereqs.v[0]));
218-				t->prereqs.v[0] = src;
219-				t->prereqs.n++;
220+				if (!hasword(&t->impprereqs, src)) {
221+					t->impprereqs.v = xrealloc(t->impprereqs.v,
222+					                            (t->impprereqs.n + 1) * sizeof(t->impprereqs.v[0]));
223+					memmove(t->impprereqs.v + 1, t->impprereqs.v,
224+					        t->impprereqs.n * sizeof(t->impprereqs.v[0]));
225+					t->impprereqs.v[0] = src;
226+					t->impprereqs.n++;
227+				} else {
228+					free(src);
229+				}
230+				(void)exists;
231 				for (k = 0; k < rules->sufs[r].recipes.n; k++) {
232 					char *exp;
233 
234@@ -422,25 +511,32 @@ instsufrule(const struct SufRules *rules,
235 		}
236 	}
237 
238-	if (strchr(t->name, '.'))
239-		return 0;
240 	for (i = 0; i < rules->active.n; i++) {
241 		size_t j;
242 
243 		for (j = 0; j < rules->nsinglesuf; j++) {
244 			char *src;
245+			int exists;
246 
247 			if (!sufeq(rules->singlesuf[j].from, rules->active.v[i]))
248 				continue;
249 			src = cat3(t->name, "", rules->singlesuf[j].from);
250-			if (!pathorargetexists(graph, src)) {
251+			exists = pathorargetexists(graph, src);
252+			if (!exists && !sufbuildable(rules, graph, src, 0)) {
253 				free(src);
254 				continue;
255 			}
256-			t->prereqs.v = xrealloc(t->prereqs.v, (t->prereqs.n + 1) * sizeof(t->prereqs.v[0]));
257-			memmove(t->prereqs.v + 1, t->prereqs.v, t->prereqs.n * sizeof(t->prereqs.v[0]));
258-			t->prereqs.v[0] = src;
259-			t->prereqs.n++;
260+			if (!hasword(&t->impprereqs, src)) {
261+				t->impprereqs.v = xrealloc(t->impprereqs.v,
262+				                            (t->impprereqs.n + 1) * sizeof(t->impprereqs.v[0]));
263+				memmove(t->impprereqs.v + 1, t->impprereqs.v,
264+				        t->impprereqs.n * sizeof(t->impprereqs.v[0]));
265+				t->impprereqs.v[0] = src;
266+				t->impprereqs.n++;
267+			} else {
268+				free(src);
269+			}
270+			(void)exists;
271 			for (k = 0; k < rules->singlesuf[j].recipes.n; k++) {
272 				char *exp;
273 
+1, -0
1@@ -205,6 +205,7 @@ struct Target {
2 	const char *name; /* interned */
3 	char *owner;
4 	struct StrList prereqs;
5+	struct StrList impprereqs;
6 	struct StrList order_only;
7 	struct RecipeList recipes;
8 	struct Env env;
+44, -1
 1@@ -487,7 +487,8 @@ defaulttarget(const struct Graph *graph, const char *owner)
 2 			continue;
 3 		if (graph->v[i].name == intern("clean") || graph->v[i].name == intern("fmt"))
 4 			continue;
 5-		if (graph->v[i].recipes.n > 0 || graph->v[i].prereqs.n > 0 || graph->v[i].order_only.n > 0)
 6+		if (graph->v[i].recipes.n > 0 || graph->v[i].prereqs.n > 0 ||
 7+		    graph->v[i].impprereqs.n > 0 || graph->v[i].order_only.n > 0)
 8 			return &graph->v[i];
 9 	}
10 	return 0;
11@@ -533,6 +534,18 @@ addwords(struct StrList *dest, const struct StrList *src)
12 		dest->v[dest->n++] = xstrdup(src->v[i]);
13 }
14 
15+void
16+adduniqwords(struct StrList *dest, const struct StrList *src)
17+{
18+	size_t i;
19+
20+	for (i = 0; i < src->n; i++) {
21+		if (hasword(dest, src->v[i]))
22+			continue;
23+		addstr(dest, src->v[i]);
24+	}
25+}
26+
27 void
28 addrecipe(struct RecipeList *dest, const char *raw)
29 {
30@@ -613,6 +626,36 @@ findctarget(const struct Graph *graph, const char *name)
31 	return findtarget0(graph, name);
32 }
33 
34+const char *
35+firstprereq(const struct Target *t)
36+{
37+	if (t->impprereqs.n > 0)
38+		return t->impprereqs.v[0];
39+	if (t->prereqs.n > 0)
40+		return t->prereqs.v[0];
41+	return 0;
42+}
43+
44+char *
45+joinallprereqs(const struct Target *t, const char *sep)
46+{
47+	struct StrList list;
48+	char *s;
49+
50+	memset(&list, 0, sizeof(list));
51+	addwords(&list, &t->impprereqs);
52+	addwords(&list, &t->prereqs);
53+	s = joinstrs(&list, sep);
54+	freestrs(&list);
55+	return s;
56+}
57+
58+size_t
59+totalprereqs(const struct Target *t)
60+{
61+	return t->prereqs.n + t->impprereqs.n;
62+}
63+
64 void
65 freestrs(struct StrList *list)
66 {