+/* @eq{S1}{S2}...@
+ *
+ * Expands to "true" if all the arguments are identical, otherwise to "false"
+ * (i.e. if any pair of arguments differs).
+ *
+ * If there are no arguments then expands to "true". Evaluates all arguments
+ * (with their side effects) even if that's not strictly necessary to discover
+ * the result.
+ */
+static int exp_eq(int nargs,
+ char **args,
+ struct sink *output,
+ void attribute((unused)) *u) {
+ int n, result = 1;
+
+ for(n = 1; n < nargs; ++n) {
+ if(strcmp(args[n], args[0])) {
+ result = 0;
+ break;
+ }
+ }
+ return mx_bool_result(output, result);
+}
+
+/* @ne{S1}{S2}...@
+ *
+ * Expands to "true" if all of the arguments differ from one another, otherwise
+ * to "false" (i.e. if any value appears more than once).
+ *
+ * If there are no arguments then expands to "true". Evaluates all arguments
+ * (with their side effects) even if that's not strictly necessary to discover
+ * the result.
+ */
+static int exp_ne(int nargs,
+ char **args,
+ struct sink *output,
+ void attribute((unused))*u) {
+ hash *h = hash_new(sizeof (char *));
+ int n, result = 1;
+
+ for(n = 0; n < nargs; ++n)
+ if(hash_add(h, args[n], "", HASH_INSERT)) {
+ result = 0;
+ break;
+ }
+ return mx_bool_result(output, result);
+}
+
+/* @discard{...}@
+ *
+ * Expands to nothing. Unlike the comment expansion @#{...}, side effects of
+ * arguments are not suppressed. So this can be used to surround a collection
+ * of macro definitions with whitespace, free text commentary, etc.
+ */
+static int exp_discard(int attribute((unused)) nargs,
+ char attribute((unused)) **args,
+ struct sink attribute((unused)) *output,
+ void attribute((unused)) *u) {
+ return 0;
+}
+
+/* @define{NAME}{ARG1 ARG2...}{DEFINITION}@
+ *
+ * Define a macro. The macro will be called NAME and will act like an
+ * expansion. When it is expanded, the expansion is replaced by DEFINITION,
+ * with each occurence of @ARG1@ etc replaced by the parameters to the
+ * expansion.
+ */
+static int exp_define(int attribute((unused)) nargs,
+ const struct mx_node **args,
+ struct sink attribute((unused)) *output,
+ void attribute((unused)) *u) {
+ char **as, *name, *argnames;
+ int rc, nas;
+
+ if((rc = mx_expandstr(args[0], &name, u, "argument #0 (NAME)")))
+ return rc;
+ if((rc = mx_expandstr(args[1], &argnames, u, "argument #1 (ARGS)")))
+ return rc;
+ as = split(argnames, &nas, 0, 0, 0);
+ mx_register_macro(name, nas, as, args[2]);
+ return 0;
+}
+
+/* @basename{PATH}
+ *
+ * Expands to the UNQUOTED basename of PATH.
+ */
+static int exp_basename(int attribute((unused)) nargs,
+ char **args,
+ struct sink attribute((unused)) *output,
+ void attribute((unused)) *u) {
+ return sink_writes(output, d_basename(args[0])) < 0 ? -1 : 0;
+}
+
+/* @dirname{PATH}
+ *
+ * Expands to the UNQUOTED directory name of PATH.
+ */
+static int exp_dirname(int attribute((unused)) nargs,
+ char **args,
+ struct sink attribute((unused)) *output,
+ void attribute((unused)) *u) {
+ return sink_writes(output, d_dirname(args[0])) < 0 ? -1 : 0;
+}
+
+/* @q{STRING}
+ *
+ * Expands to STRING.
+ */
+static int exp_q(int attribute((unused)) nargs,
+ char **args,
+ struct sink attribute((unused)) *output,
+ void attribute((unused)) *u) {
+ return sink_writes(output, args[0]) < 0 ? -1 : 0;
+}
+
+/** @brief Register built-in expansions */