Skip to content

Commit c2045dd

Browse files
generics: sum-type Variants + unified Match/When/Otherwise
One Match/When/Otherwise now drives both static type dispatch and by-value sum types, with fail-fast on a non-exhaustive match. - Include/Misra/Generics/Variant.h (new): `Variant(N, T...)` generates a per-variant tag enum (namespaced `N_<T>` -- no global registry, two variants sharing a payload type never collide), the tagged union, and a typed `N_from_<T>` constructor each. Reuses Types.h's `APPLY_MACRO_FOREACH` plus a context-threading sibling `APPLY_MACRO_FOREACH_C`; the `__VA_OPT__` engine caps payloads at ~256 (a loud compile error past that, not truncation). - Include/Misra/Generics/TypeMatch.h: `When` is `OVERLOAD`-based -- `When(T)` keeps the static type arm (it = the value), `When(N, T)` adds a variant arm (it = the typed payload). `Match` binds the scrutinee as `MisraSubject`. A non-exhaustive match (no arm, no Otherwise) aborts via `ASSERT_OR_FATAL` instead of falling through -- the runtime-fail-fast stance. The static API is unchanged and backward compatible. Dispatch stays a flat if-chain, which the optimiser lowers to an O(1) jump table for dense variant tags. Naming follows the no-MISRA-prefix convention (internal macros are VARIANT_* / APPLY_MACRO_*). Tests: Generics/TypeMatch.c (7 + deadend) and a new Generics/Variant.c (7 + deadend asserting the non-exhaustive abort), wired into Tests/meson.build.
1 parent 93e4e4d commit c2045dd

6 files changed

Lines changed: 296 additions & 17 deletions

File tree

Include/Misra/Generics.h

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33
/// This is free and unencumbered software released into the public domain.
44
///
55
/// Generics subsystem umbrella. Header-only metaprogramming built on
6-
/// `_Generic` -- currently a compile-time type match (`Match` / `When` /
7-
/// `Otherwise`).
6+
/// `_Generic` and the `__VA_OPT__` foreach engine: a compile-time type match
7+
/// and by-value sum types, sharing one `Match` / `When` / `Otherwise`
8+
/// (`When(T)` on a static type, `When(N, T)` on a `Variant(N, ...)` value).
89
///
910
/// Deliberately NOT pulled through the top-level `Misra.h` umbrella: the
1011
/// public macros here use short, ergonomic names (`Match`, `When`, `it`,
@@ -17,5 +18,6 @@
1718
#define MISRA_GENERICS_H
1819

1920
#include <Misra/Generics/TypeMatch.h>
21+
#include <Misra/Generics/Variant.h>
2022

2123
#endif // MISRA_GENERICS_H

Include/Misra/Generics/TypeMatch.h

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
/// arms.
1111
///
1212
/// This is type DISPATCH, not value destructuring: arms select on a value's
13-
/// type only -- there is no taking-apart of structs, no literal or nested
14-
/// matching, and no exhaustiveness check. Arms are mutually exclusive; an
15-
/// optional trailing `Otherwise` catches the rest.
13+
/// type only -- there is no taking-apart of structs, and no literal or nested
14+
/// matching. Arms are mutually exclusive; an optional trailing `Otherwise`
15+
/// catches the rest. A non-exhaustive match -- no `When` hit and no
16+
/// `Otherwise` -- does NOT fall through silently: it aborts via `LOG_FATAL` at
17+
/// runtime, matching the project's fail-fast-for-correctness stance.
1618

1719
#ifndef MISRA_GENERICS_TYPEMATCH_H
1820
#define MISRA_GENERICS_TYPEMATCH_H
1921

22+
#include <Misra/Std/Log.h>
2023
#include <Misra/Types.h>
2124

2225
#ifdef __cplusplus
@@ -53,8 +56,10 @@ extern "C" {
5356
/// SUCCESS : Runs the body of the single arm whose type matches `x` (or
5457
/// `Otherwise` if none), with `it` bound to `x`. Block exits after
5558
/// one arm.
56-
/// FAILURE : Macro cannot fail. A `When` body that misuses `it`'s static type
57-
/// is a compile error (every arm is type-checked).
59+
/// FAILURE : If no `When` matches and there is no `Otherwise`, the match is
60+
/// non-exhaustive and aborts via `LOG_FATAL` -- it never silently
61+
/// falls through. A `When` body that misuses `it`'s static type is a
62+
/// compile error (every arm is type-checked).
5863
///
5964
/// USAGE:
6065
/// Match(value) {
@@ -66,19 +71,37 @@ extern "C" {
6671
/// TAGS: Generics, Match, Type, Dispatch, Compile-Time
6772
///
6873
#define Match(x) \
69-
for (TYPE_OF(x) it = (x), *UNPL(tm_loop) = &it; UNPL(tm_loop); UNPL(tm_loop) = NULL) \
70-
for (bool MisraMatched = false; !MisraMatched; MisraMatched = true)
74+
for (bool MisraMatched = false, UNPL(tm_once) = true; UNPL(tm_once); UNPL(tm_once) = false, \
75+
ASSERT_OR_FATAL(MisraMatched, "Match: no arm matched and no Otherwise (non-exhaustive)")) \
76+
for (TYPE_OF(x) MisraSubject = (x), *UNPL(tm_loop) = &MisraSubject; UNPL(tm_loop); UNPL(tm_loop) = NULL)
7177

7278
///
73-
/// An arm of a `Match`. Runs its body iff no earlier arm matched and `it`'s
74-
/// static type is `T`.
79+
/// An arm of a `Match`, in one of two forms (selected by argument count):
7580
///
76-
/// SUCCESS : Body runs at most once, only for the matching type.
77-
/// FAILURE : Macro cannot fail. Usable only inside a `Match`.
81+
/// * `When(T)` -- static: runs iff no earlier arm matched and the
82+
/// subject's static type is `T`; binds `it` to the value.
83+
/// * `When(N, T)` -- variant: runs iff no earlier arm matched and the
84+
/// subject's runtime tag is `Variant(N, ...)`'s `T`; binds
85+
/// `it` to the T-typed payload.
86+
///
87+
/// SUCCESS : Body runs at most once, only on a match, with `it` bound.
88+
/// FAILURE : Macro cannot fail. Usable only inside a `Match`. `When(N, T)` for
89+
/// a `T` the variant cannot hold is a compile error (no `as_T`
90+
/// member).
7891
///
79-
/// TAGS: Generics, Match, Type, Arm
92+
/// TAGS: Generics, Match, Type, Variant, Arm
8093
///
81-
#define When(T) if (!MisraMatched && Is(it, T) && (MisraMatched = true))
94+
#define When(...) OVERLOAD(When, __VA_ARGS__)
95+
96+
/* static arm: it = the value (its static type). */
97+
#define When_1(T) \
98+
if (!MisraMatched && Is(MisraSubject, T) && (MisraMatched = true)) \
99+
for (TYPE_OF(MisraSubject) it = MisraSubject, *UNPL(tm_bind) = &it; UNPL(tm_bind); UNPL(tm_bind) = NULL)
100+
101+
/* variant arm: it = the T-typed payload of a Variant(N, ...) subject. */
102+
#define When_2(N, T) \
103+
if (!MisraMatched && MisraSubject.tag == N##_##T && (MisraMatched = true)) \
104+
for (T it = MisraSubject.u.as_##T, *UNPL(tm_bind) = &it; UNPL(tm_bind); UNPL(tm_bind) = NULL)
82105

83106
///
84107
/// The fallback arm of a `Match`. Runs iff no earlier arm matched.

Include/Misra/Generics/Variant.h

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/// file : generics/variant.h
2+
/// author : Siddharth Mishra (admin@brightprogrammer.in)
3+
/// This is free and unencumbered software released into the public domain.
4+
///
5+
/// By-value tagged-union sum types. `Variant(Name, T...)` generates a
6+
/// per-variant tag enum, the tagged union, and a typed constructor per
7+
/// payload. Pair with `Match` / `When` from `Generics/TypeMatch.h`:
8+
/// `When(Name, T)` dispatches on the runtime tag and binds the T-typed
9+
/// payload as `it`.
10+
///
11+
/// Self-contained -- no global registration. Two variants sharing a payload
12+
/// type never collide (each tag enum is namespaced by the variant name). Up
13+
/// to 256 payload types (the foreach-expansion ceiling); exceeding it is a
14+
/// loud compile error, not silent truncation. Payload type names must each be
15+
/// a single token -- `typedef` compound types (`unsigned int`, `char *`)
16+
/// before listing them.
17+
18+
#ifndef MISRA_GENERICS_VARIANT_H
19+
#define MISRA_GENERICS_VARIANT_H
20+
21+
#include <Misra/Types.h>
22+
23+
#ifdef __cplusplus
24+
extern "C" {
25+
#endif
26+
27+
// A generated constructor is rarely called for every payload type, so mark
28+
// them maybe-unused. clang's `-Wunused-function` fires for `static inline`
29+
// definitions emitted into a source file; MSVC has no such attribute.
30+
#if defined(__GNUC__) || defined(__clang__)
31+
# define VARIANT_UNUSED __attribute__((unused))
32+
#else
33+
# define VARIANT_UNUSED
34+
#endif
35+
36+
// Context-threading sibling of `APPLY_MACRO_FOREACH` (Types.h): applies
37+
// `gen(ctx, elem)` to each element, reusing the same `__VA_OPT__` trampoline
38+
// (`TRICK_EXPAND` / `TRICK_PARENS`) and therefore the same ~256-element
39+
// ceiling. `ctx` is the variant name, threaded so the generators can
40+
// namespace the tag constants and constructors.
41+
#define APPLY_MACRO_FOREACH_C(gen, ctx, ...) \
42+
__VA_OPT__(TRICK_EXPAND(APPLY_MACRO_FOREACH_C_HELPER(gen, ctx, __VA_ARGS__)))
43+
#define APPLY_MACRO_FOREACH_C_HELPER(gen, ctx, a1, ...) \
44+
gen(ctx, a1) __VA_OPT__(APPLY_MACRO_FOREACH_C_AGAIN TRICK_PARENS(gen, ctx, __VA_ARGS__))
45+
#define APPLY_MACRO_FOREACH_C_AGAIN() APPLY_MACRO_FOREACH_C_HELPER
46+
47+
// Per-payload code generators (internal).
48+
#define VARIANT_ENUMERATOR(N, T) N##_##T,
49+
#define VARIANT_FIELD(T) T as_##T;
50+
#define VARIANT_CTOR(N, T) \
51+
static inline VARIANT_UNUSED N N##_from_##T(T value) { \
52+
N out; \
53+
out.tag = N##_##T; \
54+
out.u.as_##T = value; \
55+
return out; \
56+
}
57+
58+
///
59+
/// Declare a by-value tagged-union sum type `N` over the listed payload types.
60+
/// Emits a per-variant tag enum `enum N##_Tag` with one namespaced constant
61+
/// `N##_<T>` per type, the tagged union, and a `static inline N N##_from_<T>`
62+
/// constructor per type. No global registration -- matchable directly with
63+
/// `Match` / `When(N, T)`.
64+
///
65+
/// SUCCESS : Defines `enum N##_Tag { N_<T>, ... }`, `typedef struct { enum
66+
/// N##_Tag tag; union { T as_<T>; ... } u; } N;`, and a typed
67+
/// constructor per payload. A trailing `;` after the macro is
68+
/// required and consumed.
69+
/// FAILURE : Macro cannot fail. More than 256 payloads is a compile error
70+
/// (the foreach ceiling). Payload type names must each be a single
71+
/// token; compound types must be `typedef`-d first.
72+
///
73+
/// USAGE:
74+
/// Variant(Number, int, float, double);
75+
/// Number n = Number_from_int(7);
76+
/// // match with: Match(n) { When(Number, int) { ... it ... } ... }
77+
///
78+
/// TAGS: Generics, Variant, SumType, Constructor
79+
///
80+
#define Variant(N, ...) \
81+
enum N##_Tag {APPLY_MACRO_FOREACH_C(VARIANT_ENUMERATOR, N, __VA_ARGS__)}; \
82+
typedef struct { \
83+
enum N##_Tag tag; \
84+
union { \
85+
APPLY_MACRO_FOREACH(VARIANT_FIELD, __VA_ARGS__) \
86+
} u; \
87+
} N; \
88+
APPLY_MACRO_FOREACH_C(VARIANT_CTOR, N, __VA_ARGS__) \
89+
struct N##_variant_force_semicolon /* swallow the trailing `;` */
90+
91+
#ifdef __cplusplus
92+
}
93+
#endif
94+
95+
#endif // MISRA_GENERICS_VARIANT_H

Tests/Generics/TypeMatch.c

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,16 @@ bool test_is_predicate(void) {
115115
return Is(i, int) == 1 && Is(i, double) == 0 && Is(d, double) == 1 && Is(d, int) == 0;
116116
}
117117

118+
// Deadend: a non-exhaustive match (no arm matches, no Otherwise) must abort
119+
// rather than fall through silently.
120+
bool deadend_match_nonexhaustive(void) {
121+
Position2D p = {1.0f, 2.0f};
122+
Match(p) {
123+
When(int)(void) it; // never matches Position2D, and there is no Otherwise
124+
}
125+
return true; // unreachable -- the match aborts first
126+
}
127+
118128
int main(void) {
119129
WriteFmt("[INFO] Starting Generics.TypeMatch tests\n\n");
120130
TestFunction tests[] = {
@@ -126,6 +136,10 @@ int main(void) {
126136
test_nested_match_shadows_it,
127137
test_is_predicate,
128138
};
129-
int total = sizeof(tests) / sizeof(tests[0]);
130-
return run_test_suite(tests, total, NULL, 0, "Generics.TypeMatch");
139+
TestFunction deadend_tests[] = {
140+
deadend_match_nonexhaustive,
141+
};
142+
int total = sizeof(tests) / sizeof(tests[0]);
143+
int deadend = sizeof(deadend_tests) / sizeof(deadend_tests[0]);
144+
return run_test_suite(tests, total, deadend_tests, deadend, "Generics.TypeMatch");
131145
}

Tests/Generics/Variant.c

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#include <Misra/Generics.h>
2+
#include <Misra/Std/Io.h>
3+
#include <Misra/Types.h>
4+
5+
#include "../Util/TestRunner.h"
6+
7+
// Self-contained variants -- no global registration; two variants sharing a
8+
// payload type do not collide.
9+
Variant(Number, int, double);
10+
Variant(Cell, int, char);
11+
Variant(Tri, int, float, char);
12+
13+
// ---------------------------------------------------------------------------
14+
// Variant construction + Match / When (variant arms) / Otherwise
15+
// ---------------------------------------------------------------------------
16+
17+
bool test_variant_construct_and_match(void) {
18+
Number n = Number_from_int(42);
19+
bool hit = false;
20+
Match(n) {
21+
When(Number, int) hit = (it == 42);
22+
When(Number, double) hit = false;
23+
}
24+
25+
Number m = Number_from_double(2.5);
26+
bool hd = false;
27+
Match(m) {
28+
When(Number, int) hd = false;
29+
When(Number, double) hd = (it == 2.5);
30+
}
31+
return hit && hd;
32+
}
33+
34+
bool test_variant_it_is_typed_payload(void) {
35+
Number a = Number_from_int(10);
36+
Number b = Number_from_double(1.5);
37+
int ai = 0;
38+
double bd = 0.0;
39+
Match(a) {
40+
When(Number, int) ai = it * 2; // it : int
41+
When(Number, double) bd = it;
42+
}
43+
Match(b) {
44+
When(Number, int) ai = it;
45+
When(Number, double) bd = it * 2.0; // it : double
46+
}
47+
return ai == 20 && bd == 3.0;
48+
}
49+
50+
// value in, value out -- no pointers
51+
static Number twice(Number n) {
52+
Match(n) {
53+
When(Number, int) return Number_from_int(it * 2);
54+
When(Number, double) return Number_from_double(it * 2.0);
55+
}
56+
return n;
57+
}
58+
59+
bool test_variant_by_value_flow(void) {
60+
Number r = twice(Number_from_int(21));
61+
bool ok = false;
62+
Match(r) {
63+
When(Number, int) ok = (it == 42);
64+
When(Number, double) ok = false;
65+
}
66+
return ok;
67+
}
68+
69+
bool test_variant_rvalue_and_otherwise(void) {
70+
bool ok = false;
71+
// Match directly on a function-return rvalue; float-less Number -> Otherwise unused.
72+
Match(twice(Number_from_double(1.5))) {
73+
When(Number, int) ok = false;
74+
When(Number, double) ok = (it == 3.0);
75+
Otherwise ok = false;
76+
}
77+
return ok;
78+
}
79+
80+
bool test_two_variants_independent(void) {
81+
Cell c = Cell_from_char('Z');
82+
bool ok = false;
83+
Match(c) {
84+
When(Cell, int) ok = false;
85+
When(Cell, char) ok = (it == 'Z');
86+
}
87+
return ok;
88+
}
89+
90+
bool test_variant_three_payloads(void) {
91+
Tri vals[] = {Tri_from_int(7), Tri_from_float(1.5f), Tri_from_char('A')};
92+
int seen = 0;
93+
bool ok = true;
94+
for (int k = 0; k < 3; k++) {
95+
Match(vals[k]) {
96+
When(Tri, int) ok = ok && (it == 7), seen |= 1;
97+
When(Tri, float) ok = ok && (it == 1.5f), seen |= 2;
98+
When(Tri, char) ok = ok && (it == 'A'), seen |= 4;
99+
}
100+
}
101+
return ok && seen == 7;
102+
}
103+
104+
bool test_variant_arms_are_exclusive(void) {
105+
Number n = Number_from_int(7);
106+
int count = 0;
107+
Match(n) {
108+
When(Number, int) count++;
109+
When(Number, double) count++;
110+
Otherwise count++;
111+
}
112+
return count == 1;
113+
}
114+
115+
// ---------------------------------------------------------------------------
116+
// Deadend: a non-exhaustive variant match aborts rather than fall through.
117+
// ---------------------------------------------------------------------------
118+
119+
bool deadend_variant_nonexhaustive(void) {
120+
Number n = Number_from_double(9.0); // holds double, only int handled, no Otherwise
121+
Match(n) {
122+
When(Number, int)(void) it;
123+
}
124+
return true; // unreachable -- the match aborts first
125+
}
126+
127+
int main(void) {
128+
WriteFmt("[INFO] Starting Generics.Variant tests\n\n");
129+
TestFunction tests[] = {
130+
test_variant_construct_and_match,
131+
test_variant_it_is_typed_payload,
132+
test_variant_by_value_flow,
133+
test_variant_rvalue_and_otherwise,
134+
test_two_variants_independent,
135+
test_variant_three_payloads,
136+
test_variant_arms_are_exclusive,
137+
};
138+
TestFunction deadend_tests[] = {
139+
deadend_variant_nonexhaustive,
140+
};
141+
int total = sizeof(tests) / sizeof(tests[0]);
142+
int deadend = sizeof(deadend_tests) / sizeof(deadend_tests[0]);
143+
return run_test_suite(tests, total, deadend_tests, deadend, "Generics.Variant");
144+
}

Tests/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ endforeach
796796

797797
generics_tests = [
798798
['Generics.TypeMatch', 'Generics/TypeMatch.c'],
799+
['Generics.Variant', 'Generics/Variant.c'],
799800
]
800801

801802
foreach test_info : generics_tests

0 commit comments

Comments
 (0)