]> git.cworth.org Git - mnemon/blob - mnemon.c
Further separation of mnemon main program from mnemon library.
[mnemon] / mnemon.c
1 /* mnemon - A memory training library
2  *
3  * Copyright © 2006,2011 Carl Worth
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3, or (at your option)
8  * any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software Foundation,
17  * Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA."
18  */
19
20 #include "mnemon.h"
21
22 /* for asprintf */
23 #define _GNU_SOURCE
24 #include <stdio.h>
25 #include <stdlib.h>
26 #include <stdarg.h>
27 #include <stdint.h>
28 #include <math.h>
29
30 #include <sys/types.h>
31 #include <sys/time.h>
32 #include <sys/stat.h>
33 #include <unistd.h>
34 #include <dirent.h>
35 #include <errno.h>
36 #include <string.h>
37 #include <assert.h>
38
39 #include <readline/readline.h>
40 #include <readline/history.h>
41
42 #define ASSERT_NOT_REACHED              \
43 do {                                    \
44     static const int NOT_REACHED = 0;   \
45     assert (NOT_REACHED);               \
46 } while (0)
47
48 static void *
49 xmalloc (size_t size)
50 {
51     void *ret;
52
53     ret = malloc (size);
54     if (ret == NULL) {
55         fprintf (stderr, "Error: out of memory\n");
56         exit (1);
57     }
58
59     return ret;
60 }
61
62 static void *
63 xrealloc (void *ptr, size_t size)
64 {
65     void *ret;
66
67     ret = realloc (ptr, size);
68     if (ret == NULL) {
69         fprintf (stderr, "Error: out of memory\n");
70         exit (1);
71     }
72
73     return ret;
74 }
75
76 static char *
77 xstrdup (const char *s)
78 {
79     char *ret;
80
81     ret = strdup (s);
82     if (ret == NULL) {
83         fprintf (stderr, "Error: out of memory\n");
84         exit (1);
85     }
86
87     return ret;
88 }
89
90 static void
91 xasprintf (char **strp, const char *fmt, ...)
92 {
93     va_list ap;
94     int ret;
95
96     va_start (ap, fmt);
97     ret = vasprintf (strp, fmt, ap);
98     va_end (ap);
99
100     if (ret < 0) {
101         fprintf (stderr, "Error: out of memory\n");
102         exit (1);
103     }
104 }
105
106 static void
107 item_init (item_t       *item,
108            int           score,
109            const char   *challenge,
110            const char   *response)
111 {
112     item->score = score;
113
114     item->challenge = xmalloc (strlen (challenge) + 1 +
115                                strlen (response) + 1);
116     item->response = item->challenge + strlen (challenge) + 1;
117
118     strcpy (item->challenge, challenge);
119     strcpy (item->response, response);
120 }
121
122 static void
123 item_fini (item_t *item)
124 {
125     /* item->response shares allocation with item->challenge, so
126      * doesn't require a separate call to free */
127     free (item->challenge);
128 }
129
130 static void
131 category_init (category_t *category,
132                const char *name)
133 {
134     category->name = xstrdup (name);
135     
136     category->items_size = 0;
137     category->num_items = 0;
138     category->items = NULL;
139     category->order = CATEGORY_ORDER_RANDOM;
140     category->time_limit = 0.0;
141     category->bin_zero_head = 0;
142     category->challenge_type = CHALLENGE_TYPE_TEXT;
143     category->repeat = 0;
144 }
145
146 static void
147 category_fini (category_t *category)
148 {
149     int i;
150
151     for (i = 0; i < category->num_items; i++)
152         item_fini (&category->items[i]);
153
154     free (category->items);
155
156     free (category->name);
157 }
158
159 static void
160 category_grow (category_t *category)
161 {
162     if (category->items_size)
163         category->items_size *= 2;
164     else
165         category->items_size = 1;
166
167     category->items = xrealloc (category->items,
168                                 category->items_size * sizeof (item_t));
169 }
170
171 static item_t *
172 category_add_item (category_t   *category,
173                    int           score,
174                    const char   *challenge,
175                    const char   *response)
176 {
177     item_t *item;
178
179     if (category->num_items == category->items_size)
180         category_grow (category);
181
182     item = &category->items[category->num_items++];
183
184     item_init (item, score, challenge, response);
185
186     return item;
187 }
188
189 static item_t *
190 category_next_bin_zero_item (category_t *category)
191 {
192     int *i = &category->bin_zero_head;
193
194     for ( ; *i < category->num_items; *i = *i + 1)
195         if (category->items[*i].score == 0)
196             return &category->items[*i];
197
198     return NULL;
199 }
200
201 static void
202 category_print (category_t      *category,
203                 FILE            *file)
204 {
205     int i;
206     item_t *item;
207
208     fprintf (file, "order = %s\n\n",
209             category->order == CATEGORY_ORDER_RANDOM ? "random" : "sequential");
210     fprintf (file, "time = %f\n\n",
211              category->time_limit);
212
213     fprintf (file, "challenge = ");
214     switch (category->challenge_type) {
215     case CHALLENGE_TYPE_TEXT:
216         fprintf (file, "text");
217         break;
218     case CHALLENGE_TYPE_IMAGE:
219         fprintf (file, "image");
220         break;
221     case CHALLENGE_TYPE_AUDIO:
222         fprintf (file, "audio");
223         break;
224     case CHALLENGE_TYPE_MIDI:
225         fprintf (file, "midi");
226         break;
227     case CHALLENGE_TYPE_TEXT_TO_SPEECH:
228         fprintf (file, "text-to-speech");
229         break;
230     }
231     fprintf (file, "\n\n");
232
233     fprintf (file, "repeat = %d\n\n", category->repeat);
234
235     for (i = 0; i < category->num_items; i++) {
236         item = &category->items[i];
237         if (i != 0)
238             fprintf (file, "\n");
239         fprintf (file, "%d\n%s\n%s\n",
240                  item->score,
241                  item->challenge,
242                  item->response);
243     }
244 }
245
246 static void
247 bin_init (bin_t *bin,
248           int    score)
249 {
250     bin->score = score;
251
252     bin->items_size = 0;
253     bin->num_items = 0;
254     bin->items = NULL;
255 }
256
257 static void
258 bin_fini (bin_t *bin)
259 {
260     free (bin->items);
261 }
262
263 static void
264 bin_grow (bin_t *bin)
265 {
266     if (bin->items_size)
267         bin->items_size *= 2;
268     else
269         bin->items_size = 1;
270
271     bin->items = xrealloc (bin->items,
272                            bin->items_size * sizeof (item_t*));
273 }
274
275 static void
276 bin_add_item (bin_t     *bin,
277               item_t    *item)
278 {
279     assert (item->score == bin->score);
280
281     if (bin->num_items == bin->items_size)
282         bin_grow (bin);
283
284     bin->items[bin->num_items++] = item;
285 }
286
287 static void
288 bin_remove_item (bin_t  *bin,
289                  int     item_index)
290 {
291     /* Replace the current item with the last item, (no need to shift
292      * any more than that since we don't care about the order of the
293      * items within a bin). */
294     bin->num_items--;
295     if (bin->num_items)
296         bin->items[item_index] = bin->items[bin->num_items];
297 }
298
299 /* Find the index for an item within a bin.
300  *
301  * XXX: This is currently a linear search, so is a potential
302  * performance problem.
303  */
304 static int
305 bin_item_index (bin_t   *bin,
306                 item_t  *item)
307 {
308     int i;
309
310     for (i = 0; i < bin->num_items; i++)
311         if (bin->items[i] == item)
312             return i;
313
314     assert (0);
315 }
316
317 void
318 mnemon_init (mnemon_t *mnemon)
319 {
320     char *home;
321
322     home = getenv ("HOME");
323     if (home == NULL)
324         home = "";
325
326     xasprintf (&mnemon->dir_name, "%s/.mnemon", getenv ("HOME"));
327
328     mnemon->categories_size = 0;
329     mnemon->num_categories = 0;
330     mnemon->categories = NULL;
331
332     mnemon->bins_size = 0;
333     mnemon->num_bins = 0;
334     mnemon->bins = NULL;
335 }
336
337 void
338 mnemon_fini (mnemon_t *mnemon)
339 {
340     int i;
341
342     for (i = 0; i < mnemon->num_bins; i++)
343         bin_fini (&mnemon->bins[i]);
344     free (mnemon->bins);
345
346     for (i = 0; i < mnemon->num_categories; i++)
347         category_fini (&mnemon->categories[i]);
348     free (mnemon->categories);
349
350     free (mnemon->dir_name);
351 }
352
353 static void
354 mnemon_categories_grow (mnemon_t *mnemon)
355 {
356     if (mnemon->categories_size)
357         mnemon->categories_size *= 2;
358     else
359         mnemon->categories_size = 1;
360
361     mnemon->categories = xrealloc (mnemon->categories,
362                                    mnemon->categories_size * sizeof (category_t));
363 }
364
365 /* Get a category by name if it exists */
366 category_t *
367 mnemon_get_category_if_exists (mnemon_t     *mnemon,
368                                const char   *name)
369 {
370     int i;
371
372     for (i = 0; i < mnemon->num_categories; i++)
373         if (strcmp (mnemon->categories[i].name, name) == 0)
374             return &mnemon->categories[i];
375
376     return NULL;
377 }
378
379 /* Get a category by name, creating new one if necessary. */
380 static category_t *
381 mnemon_get_category (mnemon_t   *mnemon,
382                      const char *name)
383 {
384     category_t *category;
385
386     category = mnemon_get_category_if_exists (mnemon, name);
387     if (category)
388         return category;
389
390     mnemon_categories_grow (mnemon);
391
392     category = &mnemon->categories[mnemon->num_categories++];
393
394     category_init (category, name);
395
396     return category;
397 }
398
399 static void
400 mnemon_bins_grow (mnemon_t *mnemon)
401 {
402     if (mnemon->bins_size)
403         mnemon->bins_size *= 2;
404     else
405         mnemon->bins_size = 1;
406
407     mnemon->bins = xrealloc (mnemon->bins,
408                              mnemon->bins_size * sizeof (bin_t));
409 }
410
411 static bin_t *
412 mnemon_get_bin (mnemon_t        *mnemon,
413                 int              score)
414 {
415     int i;
416     bin_t *bin;
417
418     for (i = 0; i < mnemon->num_bins; i++)
419         if (mnemon->bins[i].score == score)
420             return &mnemon->bins[i];
421         else if (mnemon->bins[i].score > score)
422             break;
423
424     if (mnemon->num_bins == mnemon->bins_size)
425         mnemon_bins_grow (mnemon);
426
427     bin = &mnemon->bins[i];
428
429     /* Make room to insert new bin at its sorted location. */
430     if (i < mnemon->num_bins)
431         memmove (bin + 1, bin, (mnemon->num_bins - i) * sizeof (bin_t));
432     mnemon->num_bins++;
433
434     bin_init (bin, score);
435
436     return bin;
437 }
438
439 void
440 mnemon_remove_bin (mnemon_t *mnemon, int bin_number)
441 {
442     bin_t *bin = mnemon_get_bin (mnemon, bin_number);
443     int i;
444
445     if (bin == NULL)
446         return;
447
448     i = bin - mnemon->bins;
449
450     bin_fini (bin);
451
452     memmove (bin, bin + 1, (mnemon->num_bins - i) * sizeof (bin_t));
453     mnemon->num_bins--;
454 }
455
456 static void
457 chomp (char *s)
458 {
459     int len = strlen (s);
460     if (len == 0)
461         return;
462     if (s[len - 1] == '\n')
463         s[len - 1] = '\0';
464 }
465
466 static char *
467 trim_space (char *string)
468 {
469     char *s;
470
471     s = string;
472     while (*s && isspace (*s))
473         s++;
474
475     string = s;
476
477     s = string + strlen (string) - 1;
478     while (s > string && isspace (*s)) {
479         *s = '\0';
480         s--;
481     }
482
483     return string;
484 }
485
486 void
487 mnemon_load_category (mnemon_t          *mnemon,
488                       const char        *name)
489 {
490     FILE *file;
491     char *line = NULL, *end;
492     size_t line_size = 0;
493     ssize_t bytes_read;
494     int line_count = 0;
495     char *path;
496     category_t *category;
497     int i;
498     struct stat st;
499
500     path = xmalloc (strlen (mnemon->dir_name) + 1 + strlen (name) + 1);
501     sprintf (path, "%s/%s", mnemon->dir_name, name);
502
503     file = fopen (path, "r");
504     if (file == NULL) {
505         fprintf (stderr, "Error: Failed to open %s: %s\n",
506                  path, strerror (errno));
507         exit (1);
508     }
509
510     fstat (fileno(file), &st);
511     if (! S_ISREG(st.st_mode)) {
512         fprintf (stderr, "Error: File %s is not a regular file.\n", path);
513         exit (1);
514     }
515
516     category = mnemon_get_category (mnemon, name);
517
518 #define READ_LINE do {                                  \
519     bytes_read = getline (&line, &line_size, file);     \
520     if (bytes_read == -1)                               \
521         goto END_OF_FILE;                               \
522     line_count++;                                       \
523     chomp (line);                                       \
524 } while (0)
525
526     /* Parse options */
527     while (1) {
528         char *name, *equal, *value;
529
530         /* Ignore blank lines */
531         READ_LINE;
532         if (*line == '\0')
533             continue;
534
535         /* An initial digit means we hit an item. Trigger the
536          * spaghetti machine. */
537         if ((*line >= '0' && *line <= '9') || *line == '-')
538             goto PARSE_BIN;
539
540         equal = strchr (line, '=');
541         if (equal == NULL) {
542             fprintf (stderr, "Malformed option, (expected name=value): \"%s\" at %s:%d\n",
543                      line, path, line_count);
544             exit (1);
545         }
546
547         value = equal + 1;
548         name = line;
549         *equal = '\0';
550
551         name = trim_space (name);
552         value = trim_space (value);
553
554         if (strcmp (name, "order") == 0) {
555             if (strcmp (value, "sequential") == 0) {
556                 category->order = CATEGORY_ORDER_SEQUENTIAL;
557             } else if (strcmp (value, "random") == 0) {
558                 category->order = CATEGORY_ORDER_RANDOM;
559             } else {
560                 fprintf (stderr, "Unknown value for \"order\" option \"%s\" at %s:%d\n",
561                          value, path, line_count);
562                 exit (1);
563             }
564         } else if (strcmp (name, "time") == 0) {
565             double limit;
566             char *end;
567             limit = strtod (value, &end);
568             while (isspace (*end))
569                 end++;
570             if (*end == '\0') {
571                 category->time_limit = limit;
572             } else {
573                 fprintf (stderr, "Failed to parse time value: %s at %s:%d\n",
574                          value, path, line_count);
575                 exit (1);
576             }
577         } else if (strcmp (name, "challenge") == 0) {
578             if (strcmp (value, "text") == 0) {
579                 category->challenge_type = CHALLENGE_TYPE_TEXT;
580             } else if (strcmp (value, "image") == 0) {
581                 category->challenge_type = CHALLENGE_TYPE_IMAGE;
582             } else if (strcmp (value, "audio") == 0) {
583                 category->challenge_type = CHALLENGE_TYPE_AUDIO;
584             } else if (strcmp (value, "midi") == 0) {
585                 category->challenge_type = CHALLENGE_TYPE_MIDI;
586             } else if (strcmp (value, "text-to-speech") == 0) {
587                 category->challenge_type = CHALLENGE_TYPE_TEXT_TO_SPEECH;
588             } else {
589                 fprintf (stderr, "Unknown value for \"challenge\" option \"%s\" at %s:%d\n",
590                          value, path, line_count);
591                 exit (1);
592             }
593         } else if (strcmp (name, "repeat") == 0) {
594             if (strcmp (value, "0") == 0) 
595                 category->repeat = 0;
596             else
597                 category->repeat = 1;
598         } else {
599             fprintf (stderr, "Unknown option %s at %s:%d\n",
600                      name, path, line_count);
601             exit (1);
602         }
603     }
604
605     /* Parse items */
606     while (1) {
607         int score;
608         char *challenge, *response;
609
610         /* Ignore blank lines */
611         READ_LINE;
612         if (*line == '\0')
613             continue;
614
615         /* Read bin number */
616       PARSE_BIN:
617         score = strtol (line, &end, 10);
618         if (*end != '\0') {
619             fprintf (stderr, "Failed to parse bin number from \"%s\" at %s:%d\n",
620                      line, path, line_count);
621             exit (1);
622         }
623
624         /* Read challenge */
625         READ_LINE;
626         challenge = strdup (line);
627
628         /* Read response */
629         READ_LINE;
630         response = line;
631
632         category_add_item (category, score, challenge, response);
633
634         free (challenge);
635     }
636   END_OF_FILE:
637
638     free (line);
639     fclose (file);
640     free (path);
641
642     /* Resize category items to fit exactly. */
643     category->items_size = category->num_items;
644     category->items = xrealloc (category->items, category->items_size * sizeof (item_t));
645
646     /* Now that the category is completely loaded, with stable
647      * pointers to every item, we can add each item to its appropriate
648      * bin. */
649     for (i = 0; i < category->num_items; i++) {
650         item_t *item = &category->items[i];
651         bin_t *bin = mnemon_get_bin (mnemon, item->score);
652
653         bin_add_item (bin, item);
654     }
655 }
656
657 void
658 mnemon_load (mnemon_t *mnemon)
659 {
660     DIR *dir;
661     struct dirent *dirent;
662
663     dir = opendir (mnemon->dir_name);
664     if (dir == NULL) {
665         fprintf (stderr, "Error: Failed to open directory %s: %s\n",
666                  mnemon->dir_name, strerror (errno));
667         exit (1);
668     }
669
670     while (1) {
671         dirent = readdir (dir);
672         if (dirent == NULL)
673             break;
674
675         if (dirent->d_type == DT_REG) {
676             /* Ignore files matching *~, (yes, this shouldn't be
677              * hard-coded in such an ad-hoc way, but there you go. */
678             if (dirent->d_name[strlen(dirent->d_name)-1] != '~')
679                 mnemon_load_category (mnemon, dirent->d_name);
680         }
681     }
682
683     closedir (dir);
684 }
685
686 void
687 mnemon_save (mnemon_t *mnemon)
688 {
689     int i, err;
690     char *filename, *lock_filename;
691     FILE *file;
692     category_t *category;
693
694     for (i = 0; i < mnemon->num_categories; i++) {
695         category = &mnemon->categories[i];
696
697         xasprintf (&filename, "%s/%s",
698                    mnemon->dir_name, category->name);
699         xasprintf (&lock_filename, "%s/.#%s",
700                    mnemon->dir_name, category->name);
701
702         file = fopen (lock_filename, "w");
703         if (file == NULL) {
704             fprintf (stderr, "Error: Failed to open %s for writing: %s\n",
705                      lock_filename, strerror (errno));
706             continue;
707         }
708
709         category_print (category, file);
710
711         fsync (fileno (file));
712         fclose (file);
713
714         err = rename (lock_filename, filename);
715         if (err < 0) {
716             fprintf (stderr, "Error: Failed to rename %s to %s: %s\n",
717                      lock_filename, filename, strerror (errno));
718             continue;
719         }
720
721         free (filename);
722         free (lock_filename);
723     }
724 }
725
726 /* Return a uniformly-distributed pseudo-random integer within the
727  * range:
728  *
729  *      0 <= result < num_values
730  */
731 static int
732 rand_within (int num_values)
733 {
734     return (int) (num_values * (rand() / (RAND_MAX + 1.0)));
735 }
736
737 /* Return an exponentially-distributed pseudo-random integer within
738  * the range:
739  *
740  *      0 <= result < num_values
741  *
742  * The distribution is such that each successively larger value will
743  * occur with a probability of half of the previous value.
744  */
745 static int
746 rand_within_exponential (int num_values)
747 {
748     static int r;
749     static uint32_t mask = 0;
750     int ones;
751     int bit;
752
753     /* Optimize the constant case. */
754     if (num_values == 1)
755         return 0;
756
757     ones = 0;
758
759     do {
760         if (mask == 0) {
761             r = rand ();
762             mask = 1 << 31;
763             while (mask > RAND_MAX)
764                 mask >>= 1;
765         }
766         bit = r & mask;
767         mask >>= 1;
768         if (bit) {
769             ones++;
770             if (ones == num_values)
771                 ones = 0;
772         }
773     } while (bit);
774
775     return ones;
776 }
777
778 category_t *
779 mnemon_item_category (mnemon_t  *mnemon,
780                       item_t    *item)
781 {
782     category_t *category;
783     int i, item_index;
784
785     for (i = 0; i < mnemon->num_categories; i++) {
786         category = &mnemon->categories[i];
787         item_index = item - category->items;
788         if (item_index >= 0 && item_index < category->num_items)
789             return category;
790     }
791
792     assert (0);
793 }
794
795 void
796 mnemon_select_item (mnemon_t     *mnemon,
797                     bin_t       **bin_ret,
798                     int          *item_index_ret,
799                     category_t  **category_ret,
800                     int          *introduced_ret)
801 {
802     int bin_index, item_index;
803     bin_t *bin;
804     item_t *item;
805     category_t *category;
806
807     bin_index = rand_within_exponential (mnemon->num_bins);
808     bin = &mnemon->bins[bin_index];
809
810     /* The most intuitive understanding of the introduced flag that
811      * it's tracking never-before-learned items as they are pulled
812      * from the bin with score 0. But that bin can become empty. So
813      * the refined rule is that we also set introduced whenever we
814      * pull from the lowest-indexed bin with a non-negative score. */
815     if (bin->score >=0 &&
816         (bin_index == 0 || mnemon->bins[bin_index-1].score < 0))
817     {
818         *introduced_ret = 1;
819     }
820     else
821     {
822         *introduced_ret = 0;
823     }
824
825     item_index = rand_within (bin->num_items);
826
827     item = bin->items[item_index];
828     category = mnemon_item_category (mnemon, item);
829
830     if (bin->score == 0) {
831         if (category->order == CATEGORY_ORDER_SEQUENTIAL) {
832             item = category_next_bin_zero_item (category);
833             if (item)
834                 item_index = bin_item_index (bin, item);
835         }
836     }
837
838     *bin_ret = bin;
839     *item_index_ret = item_index;
840     *category_ret = category;
841 }
842
843 void
844 mnemon_score_item (mnemon_t *mnemon,
845                    bin_t *bin,
846                    unsigned int item_index,
847                    bool_t correct)
848 {
849     item_t *item;
850
851     if (item_index >= bin->num_items)
852         return;
853
854     item = bin->items[item_index];
855     bin_remove_item (bin, item_index);
856
857     /* If the bin is now empty, we must remove it. */
858     if (bin->num_items == 0)
859     {
860         mnemon_remove_bin (mnemon, bin->score);
861     }
862
863     if (correct)
864     {
865         item->score++;
866         /* We reserve an item score of 0 for an item that has
867          * never been asked. */
868         if (item->score == 0)
869             item->score = 1;
870     }
871     else
872     {
873         /* Penalize an incorrect response by forcing the score
874          * negative. */
875         if (item->score >= 0) {
876             /* We go to -2 to force a little extra reinforcement
877              * when re-learning an item, (otherwise, it will often
878              * get asked again immediately where it is easy to get
879              * a correct response without any learning). */
880             item->score = -2;
881         } else {
882             item->score--;
883         }
884     }
885
886     bin = mnemon_get_bin (mnemon, item->score);
887
888     bin_add_item (bin, item);
889 }