chiark / gitweb /
Copy macro parse into lib/ and make a start on tests for it.
authorRichard Kettlewell <rjk@greenend.org.uk>
Sat, 3 May 2008 19:42:13 +0000 (20:42 +0100)
committerRichard Kettlewell <rjk@greenend.org.uk>
Sat, 3 May 2008 19:42:13 +0000 (20:42 +0100)
.bzrignore
lib/Makefile.am
lib/macros.c [new file with mode: 0644]
lib/macros.h [new file with mode: 0644]
lib/t-macros.c [new file with mode: 0644]

index 420efc6..8b39f2e 100644 (file)
@@ -172,3 +172,4 @@ lib/t-utf8
 lib/t-vector
 lib/t-words
 lib/t-wstat
+lib/t-macros
index 5320060..852852d 100644 (file)
@@ -21,7 +21,8 @@
 TESTS=t-addr t-basen t-bits t-cache t-casefold t-cookies \
                t-filepart t-hash t-heap t-hex t-kvp t-mime t-printf \
                t-regsub t-selection t-signame t-sink t-split t-syscalls \
-               t-trackname t-unicode t-url t-utf8 t-vector t-words t-wstat
+               t-trackname t-unicode t-url t-utf8 t-vector t-words t-wstat \
+               t-macros
 
 noinst_LIBRARIES=libdisorder.a
 include_HEADERS=disorder.h
@@ -59,6 +60,7 @@ libdisorder_a_SOURCES=charset.c charset.h             \
        kvp.c kvp.h                                     \
        log.c log.h log-impl.h                          \
        logfd.c logfd.h                                 \
+       macros.c macros.h                               \
        mem.c mem.h mem-impl.h                          \
        mime.h mime.c                                   \
        mixer.c mixer.h mixer-oss.c mixer-alsa.c        \
@@ -164,6 +166,10 @@ t_kvp_SOURCES=t-kvp.c test.c test.h
 t_kvp_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_kvp_DEPENDENCIES=libdisorder.a
 
+t_macros_SOURCES=t-macros.c test.c test.h
+t_macros_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
+t_macros_DEPENDENCIES=libdisorder.a
+
 t_mime_SOURCES=t-mime.c test.c test.h
 t_mime_LDADD=libdisorder.a $(LIBPCRE) $(LIBICONV) $(LIBGC)
 t_mime_DEPENDENCIES=libdisorder.a
diff --git a/lib/macros.c b/lib/macros.c
new file mode 100644 (file)
index 0000000..7cf7494
--- /dev/null
@@ -0,0 +1,211 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/macros.c
+ * @brief Macro expansion
+ */
+
+#include <config.h>
+#include "types.h"
+
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+
+#include "macros.h"
+#include "mem.h"
+#include "vector.h"
+#include "log.h"
+
+VECTOR_TYPE(mx_node_vector, const struct mx_node *, xrealloc);
+
+/** @brief Parse a template
+ * @param filename Input filename (for diagnostics)
+ * @param line Line number (use 1 on initial call)
+ * @param input Start of text to parse
+ * @param end End of text to parse or NULL
+ * @return Pointer to parse tree root node
+ *
+ * Parses the text in [start, end) and returns an (immutable) parse
+ * tree representing it.
+ *
+ * If @p end is NULL then the whole string is parsed.
+ *
+ * Note that the @p filename value stored in the parse tree is @p filename,
+ * i.e. it is not copied.
+ */
+const struct mx_node *mx_parse(const char *filename,
+                              int line,
+                              const char *input,
+                              const char *end) {
+  int braces, expansion_start_line, argument_start_line;
+  const char *argument_start, *argument_end, *p;
+  struct mx_node_vector v[1];
+  struct dynstr d[1];
+  struct mx_node *head = 0, **tailp = &head, *e;
+  int omitted_terminator;
+
+  if(!end)
+    end = input + strlen(input);
+  while(input < end) {
+    if(*input != '@') {
+      expansion_start_line = line;
+      dynstr_init(d);
+      /* Gather up text without any expansions in. */
+      while(input < end && *input != '@') {
+       if(*input == '\n')
+         ++line;
+       dynstr_append(d, *input++);
+      }
+      dynstr_terminate(d);
+      e = xmalloc(sizeof *e);
+      e->next = 0;
+      e->filename = filename;
+      e->line = expansion_start_line;
+      e->type = MX_TEXT;
+      e->text = d->vec;
+      *tailp = e;
+      tailp = &e->next;
+      continue;
+    }
+    mx_node_vector_init(v);
+    braces = 0;
+    p = input;
+    ++input;
+    expansion_start_line = line;
+    omitted_terminator = 0;
+    while(!omitted_terminator && input < end && *input != '@') {
+      /* Skip whitespace */
+      if(isspace((unsigned char)*input)) {
+       if(*input == '\n')
+         ++line;
+       ++input;
+       continue;
+      }
+      if(*input == '{') {
+       /* This is a bracketed argument.  We'll walk over it counting
+        * braces to figure out where the end is. */
+       ++input;
+       argument_start = input;
+       argument_start_line = line;
+       while(input < end && (*input != '}' || braces > 0)) {
+         switch(*input++) {
+         case '{': ++braces; break;
+         case '}': --braces; break;
+         case '\n': ++line; break;
+         }
+       }
+        /* If we run out of input without seeing a '}' that's an error */
+       if(input >= end)
+         fatal(0, "%s:%d: unterminated expansion '%.*s'",
+               filename, argument_start_line,
+               (int)(input - argument_start), argument_start);
+        /* Consistency check */
+       assert(*input == '}');
+        /* Record the end of the argument */
+        argument_end = input;
+        /* Step over the '}' */
+       ++input;
+       if(input < end && isspace((unsigned char)*input)) {
+         /* There is at least some whitespace after the '}'.  Look
+          * ahead and see what is after all the whitespace. */
+         for(p = input; p < end && isspace((unsigned char)*p); ++p)
+           ;
+         /* Now we are looking after the whitespace.  If it's
+          * anything other than '{', including the end of the input,
+          * then we infer that this expansion finished at the '}' we
+          * just saw.  (NB that we don't move input forward to p -
+          * the whitespace is NOT part of the expansion.) */
+         if(p == end || *p != '{')
+           omitted_terminator = 1;
+       }
+      } else {
+       /* We are looking at an unbracketed argument.  (A common example would
+        * be the expansion or macro name.)  This is terminated by an '@'
+        * (indicating the end of the expansion), a ':' (allowing a subsequent
+        * unbracketed argument) or a '{' (allowing a bracketed argument).  The
+        * end of the input will also do. */
+       argument_start = input;
+       argument_start_line = line;
+       while(input < end
+             && *input != '@' && *input != '{' && *input != ':') {
+         if(*input == '\n') ++line;
+         ++input;
+       }
+        argument_end = input;
+        /* Trailing whitespace is not significant in unquoted arguments (and
+         * leading whitespace is eliminated by the whitespace skip above). */
+        while(argument_end > argument_start
+              && isspace((unsigned char)argument_end[-1]))
+          --argument_end;
+        /* Step over the ':' if that's what we see */
+       if(input < end && *input == ':')
+         ++input;
+      }
+      /* Now we have an argument in [argument_start, argument_end), and we know
+       * its filename and initial line number.  This is sufficient to parse
+       * it. */
+      mx_node_vector_append(v, mx_parse(filename, argument_start_line,
+                                        argument_start, argument_end));
+    }
+    /* We're at the end of an expansion.  We might have hit the end of the
+     * input, we might have hit an '@' or we might have matched the
+     * omitted_terminator criteria. */
+    if(input < end) {
+      if(!omitted_terminator) {
+        assert(*input == '@');
+        ++input;
+      }
+    }
+    /* @@ terminates this file */
+    if(v->nvec == 0)
+      break;
+    /* Currently we require that the first element, the expansion name, is
+     * always plain text.  Removing this restriction would raise some
+     * interesting possibilities but for the time being it is considered an
+     * error. */
+    if(v->vec[0]->type != MX_TEXT)
+      fatal(0, "%s:%d: expansion names may not themselves contain expansions",
+            v->vec[0]->filename, v->vec[0]->line);
+    /* Guarantee a NULL terminator (for the case where there's more than one
+     * argument) */
+    mx_node_vector_terminate(v);
+    e = xmalloc(sizeof *e);
+    e->next = 0;
+    e->filename = filename;
+    e->line = expansion_start_line;
+    e->type = MX_EXPANSION;
+    e->name = v->vec[0]->text;
+    e->nargs = v->nvec - 1;
+    e->args = v->nvec > 1 ? &v->vec[1] : 0;
+    *tailp = e;
+    tailp = &e->next;
+  }
+  return head;
+}
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/macros.h b/lib/macros.h
new file mode 100644 (file)
index 0000000..8996f57
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * This file is part of DisOrder
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+
+/** @file lib/macros.h
+ * @brief Macro expansion
+ */
+
+#ifndef MACROS_H
+#define MACROS_H
+
+/** @brief One node in a macro expansion parse tree */
+struct mx_node {
+  /** @brief Next element or NULL at end of list */
+  struct mx_node *next;
+
+  /** @brief Node type, @ref MX_TEXT or @ref MX_EXPANSION. */
+  int type;
+
+  /** @brief Filename containing this node */
+  const char *filename;
+  
+  /** @brief Line number at start of this node */
+  int line;
+  
+  /** @brief Plain text (if @p type is @ref MX_TEXT) */
+  char *text;
+
+  /** @brief Expansion name (if @p type is @ref MX_EXPANSION) */
+  char *name;
+
+  /** @brief Argument count (if @p type is @ref MX_EXPANSION) */
+  int nargs;
+
+  /** @brief Argument values, parsed recursively (or NULL if @p nargs is 0) */
+  const struct mx_node **args;
+};
+
+/** @brief Text node */
+#define MX_TEXT 0
+
+/** @brief Expansion node */
+#define MX_EXPANSION 1
+
+const struct mx_node *mx_parse(const char *filename,
+                              int line,
+                              const char *input,
+                              const char *end);
+
+#endif /* MACROS_H */
+
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/
diff --git a/lib/t-macros.c b/lib/t-macros.c
new file mode 100644 (file)
index 0000000..77164d8
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * This file is part of DisOrder.
+ * Copyright (C) 2008 Richard Kettlewell
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
+ * USA
+ */
+#include "test.h"
+#include "macros.h"
+
+static void test_macros(void) {
+  const struct mx_node *m;
+#define L1 "this is just some\n"
+#define L2 "plain text\n"
+  static const char plain[] = L1 L2;
+
+  /* As simple as it gets */
+  m = mx_parse("plaintext1", 1, "", NULL);
+  insist(m == 0);
+
+  /* Almost as simple as that */
+  m = mx_parse("plaintext1", 1, plain, NULL);
+  insist(m->type == MX_TEXT);
+  check_string(m->filename, "plaintext1");
+  insist(m->line == 1);
+  check_string(m->text, L1 L2);
+  insist(m->next == 0);
+
+  /* Check that partial parses stop in the right place */
+  m = mx_parse("plaintext2", 5, plain, plain + strlen(L1));
+  insist(m->type == MX_TEXT);
+  check_string(m->filename, "plaintext2");
+  insist(m->line == 5);
+  check_string(m->text, L1);
+  insist(m->next == 0);
+
+  /* The simplest possible expansion */
+  m = mx_parse("macro1", 1, "@macro@", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro1");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 0);
+  insist(m->args == 0);
+  insist(m->next == 0);
+
+  /* Spacing variants of the above */
+  m = mx_parse("macro2", 1, "@    macro@", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro2");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 0);
+  insist(m->args == 0);
+  insist(m->next == 0);
+  m = mx_parse("macro3", 1, "@macro    @", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro3");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 0);
+  insist(m->args == 0);
+  insist(m->next == 0);
+
+  /* Unterminated variants */
+  m = mx_parse("macro4", 1, "@macro", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro4");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 0);
+  insist(m->args == 0);
+  insist(m->next == 0);
+  m = mx_parse("macro5", 1, "@macro   ", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro5");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 0);
+  insist(m->args == 0);
+  insist(m->next == 0);
+
+  /* Macros with a :-separated argument */
+  m = mx_parse("macro5", 1, "@macro:arg@", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro5");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 1);
+  insist(m->next == 0);
+  
+  insist(m->args[0]->type == MX_TEXT);
+  check_string(m->args[0]->filename, "macro5");
+  insist(m->args[0]->line == 1);
+  check_string(m->args[0]->text, "arg");
+  insist(m->args[0]->next == 0);
+
+  /* Multiple :-separated arguments, and spacing, and newlines */
+  m = mx_parse("macro6", 1, "@macro : \n arg1 : \n arg2@", NULL);
+  insist(m->type == MX_EXPANSION);
+  check_string(m->filename, "macro6");
+  insist(m->line == 1);
+  check_string(m->name, "macro");
+  insist(m->nargs == 2);
+  insist(m->next == 0);
+  
+  insist(m->args[0]->type == MX_TEXT);
+  check_string(m->args[0]->filename, "macro6");
+  insist(m->args[0]->line == 2);
+  check_string(m->args[0]->text, "arg1");
+  insist(m->args[0]->next == 0);
+  
+  insist(m->args[1]->type == MX_TEXT);
+  check_string(m->args[1]->filename, "macro6");
+  insist(m->args[1]->line == 3);
+  check_string(m->args[1]->text, "arg2");
+  insist(m->args[1]->next == 0);
+
+  
+
+}
+
+TEST(macros);
+
+/*
+Local Variables:
+c-basic-offset:2
+comment-column:40
+fill-column:79
+indent-tabs-mode:nil
+End:
+*/