// Core functions -- the main scm-to-C interface // // This file is part of Beastie // SPDX-FileCopyrightText: 2023 Norman Gray // SPDX-License-Identifier: BSD-2-Clause #if __GNUC__ #undef _XOPEN_SOURCE #ifndef _GNU_SOURCE #define _GNU_SOURCE #endif #endif #ifndef ALL_FUNCTIONS // define as zero to exclude functions which depend on other modules // (allows us to build beastie0) #define ALL_FUNCTIONS 1 #endif /* ALL_FUNCTIONS */ #include #include #include #include #include #include #include // for MAXPATHLEN #include #include #include #if HAVE_ALLOCA_H #include #endif #include "config.h" #include "core.h" #include "util.h" #include "unicode-scm.h" #if ALL_FUNCTIONS #include "util-extra.h" #include "uniprops.h" #if HAVE_ICU #include "unicode/uvernum.h" #endif #include "parse-authors.h" #include "parse-fmtstring.h" #include "parse-bst.h" #include "parse-aux.h" #include "parse-markdown.h" #include "parse-mdinline.h" #include "parse-mdblock.h" #include "parse-json.h" #include "lex-bib2.h" #if HAVE_SYS_UTSNAME_H #include #endif static s7_pointer parse_bst_source_proc(s7_scheme* sc, s7_pointer args) { // (parse-bst-source** #t (or/c string? #f)) : parse from file // (parse-bst-source** #f string?) : parse string contents char is_file_p = s7_boolean(sc, s7_car(args)); s7_pointer input = s7_cadr(args); // presume that the caller has checked types OK: // if IS_FILE_P is true, then INPUT is string or #f; // if false, then INPUT is a string const char* input_string; if (is_file_p && !s7_boolean(sc, input)) { input_string = NULL; // means stdin } else { input_string = s7_string(input); } //fprintf(stderr, "parse_bst_source_proc: %s %s...\n", (is_file_p ? "file" : "string"), input_string); yyscan_t scanner; struct bst_extra_s S; if (is_file_p) { scanner = parse_bst_setup_file(&S, input_string); } else { scanner = parse_bst_setup_string(&S, input_string); } if (scanner == NULL) { // warning or error? #f or '()? scheme_eval("print-warning", s7_make_string(sc, "parse-bst-input*: can't open source ~a to read"), input, NULL); return s7_nil(sc); // JUMP OUT } s7_pointer result; int status = bstparse(&S, &result, scanner); // fprintf(stderr, "parse_bst_input_proc: %s <%s> -> status=%d\n", // (is_file_p ? "file" : "string"), input_string, status); parse_bst_finish(&S, scanner); return status == 0 ? result : s7_f(sc); } static s7_pointer parse_aux_source_proc(s7_scheme* sc, s7_pointer args) { // (parse-aux-source** #t (or/c string? #f)) : parse from file // (parse-aux-source** #f string?) : parse string contents char is_file_p = s7_boolean(sc, s7_car(args)); s7_pointer input = s7_cadr(args); // presume that the caller has checked types OK: // if IS_FILE_P is true, then INPUT is string or #f; // if false, then INPUT is a string int error_status = 1; // error unless reset const char* input_string = NULL; FILE *infile = NULL; if (is_file_p) { if (s7_boolean(sc, input)) { infile = fopen(s7_string(input), "r"); if (infile == NULL) { // warning or error? #f or '()? scheme_eval("print-warning", s7_make_string(sc, "parse-aux-file: can't open file '~a' to read"), input, NULL); goto tidyup; // JUMP OUT } } else { infile = NULL; // means stdin } } else { input_string = s7_string(input); } yyscan_t scanner; struct aux_extra_s S; if (is_file_p) { scanner = parse_aux_setup_file(&S, infile); } else { scanner = parse_aux_setup_string(&S, input_string); } if (scanner == NULL) { // warning or error? #f or '()? scheme_eval("print-warning", s7_make_string(sc, "parse-aux-input*: can't open source ~a to read"), input, NULL); goto tidyup; // JUMP OUT } s7_pointer result; error_status = auxparse(&S, &result, scanner); // returns 1 on error // fprintf(stderr, "parse_aux_input_proc: %s <%s> -> status=%d\n", // (is_file_p ? "file" : "string"), s7_string(input), error_status); parse_aux_finish(&S, scanner); tidyup: if (is_file_p && infile) fclose(infile); return error_status ? s7_f(sc) : result; } static s7_pointer parse_markdown_source_proc(s7_scheme* sc, s7_pointer args) { // (parse-markdown-input* #t accumulator? (or/c string? #f)) : parse from file // (parse-markdown-input* #f accumulator? string?) : parse string contents char is_file_p = s7_boolean(sc, s7_car(args)); s7_pointer accumulator = s7_cadr(args); s7_pointer input = s7_caddr(args); // presume that the caller has checked types OK: // if IS_FILE_P is true, then INPUT is string or #f; // if false, then INPUT is a string int error_status = 1; // error unless reset s7_pointer source_label; const char* input_source; if (is_file_p) { if (s7_boolean(sc, input)) { input_source = s7_string(input); // filename source_label = input; } else { input_source = NULL; // means stdin source_label = s7_make_string(sc, ""); } } else { input_source = s7_string(input); const size_t slen = 20; const char fmt[] = ""; const size_t buflen = slen + sizeof(fmt); char* buf = alloca(buflen+1); int eol = 0; // end of first line or slen, whichever comes first while (eol -> status=%d\n", // (is_file_p ? "file" : "string"), input_string, status); parse_json_finish(&S, scanner); if (status == 0) { return result; } else if (result == NULL) { // not sure what happened here return s7_cons(sc, s7_f(sc), s7_make_string(sc, "(unknown JSON parsing error!)")); } else { // the 'result' should be an error message return s7_cons(sc, s7_f(sc), result); } } // Hash a string into an unsigned 32-bit integer. // This is an _unsophisticated_ hash function, // merely using the K&R/Java hash function // // This isn't used in the main program, but it is used in doc/assemble.docs.scm static s7_pointer string_to_hash_proc(s7_scheme* sc, s7_pointer args) { s7_pointer str = s7_car(args); if (! s7_is_string(str)) { return s7_wrong_type_arg_error(sc, "string->hash", 1, str, "a string"); } const unsigned char* s = (const unsigned char*)s7_string(str); const unsigned char* s_end = s + s7_string_length(str); uint32_t h = 0; for (const unsigned char* p = s; p < s_end; p++) { h = h * 31 + *p; } return s7_make_integer(sc, h); } #endif /* ALL_FUNCTIONS */ // implement subprocess // I don't much like the way that (system ...) is implemented in s7, // so here's a better way. // // (subprocess cmd arg ...) // // cmd and args must be string? // // Normally, this returns the stdout of the command as a string. // // If the command fails (returns a non-zero exit status), then return // #f, possibly also calling print-warning. // // If we fail to set up the subprocess (pipe fails, or the command // isn't found), then raise an error tagged 'subprocess, with extra // information containing the errno, or errno+128 if the exec fails. // // There is currently no implemented way of redirecting stdin. static s7_pointer subprocess_proc(s7_scheme* sc, s7_pointer args) { s7_pointer cmd = s7_car(args); if (! s7_is_string(cmd)) { // JUMP OUT return s7_wrong_type_arg_error(sc, "subprocess", 0, cmd, "a string"); } const char* command_name = s7_string(cmd); s7_int nargs = s7_list_length(sc, args); const char** cargs = alloca((nargs+1) * sizeof(char*)); for (int i=0; i 0) { s7_pointer s = s7_make_string_with_length(sc, buf, nread); rvals = s7_cons(sc, s, rvals); } close(filedes[0]); int status; wait(&status); if (WIFEXITED(status)) { if (WEXITSTATUS(status) == 0) { s7_int llen = s7_list_length(sc, rvals); if (llen == 0) { result = s7_make_string(sc, ""); } else if (llen == 1) { result = s7_car(rvals); } else { s7_pointer string_append = scheme_name_to_value("string-append"); result = s7_call(sc, string_append, s7_reverse(sc, rvals)); } } else if (WEXITSTATUS(status) < 128) { scheme_eval("print-trace", s7_make_string(sc, "subprocess: command ~a failed (exit status ~a)"), args, s7_make_integer(sc, WEXITSTATUS(status)), NULL); // not useful to show rvals, too? result = s7_f(sc); } else { s7_error(sc, s7_make_symbol(sc, "subprocess"), scheme_make_list(s7_make_string(sc, "unable to exec for command ~a (errno ~s)"), cmd, s7_make_integer(sc, WEXITSTATUS(status)-128), NULL)); assert(0); } } else { s7_error(sc, s7_make_symbol(sc, "subprocess"), scheme_make_list(s7_make_string(sc, "process for command ~a exited abnormally (status ~x)"), cmd, s7_make_integer(sc, status), NULL)); assert(0); } } return result; } static s7_pointer command_line_getopt_proc(s7_scheme* sc, s7_pointer scheme_args) { opterr = 0; // suppress stderr warning of unexpected options #if HAVE_DECL_OPTRESET // BSD getopt optind = 1; // reset (in case we're being called a second time) optreset = 1; // (ditto) #else // GNU getopt optind = 0; #endif s7_pointer optspec = s7_car(scheme_args); s7_pointer command_line = s7_cadr(scheme_args); char silent_opterr = s7_boolean(sc, s7_caddr(scheme_args)); if (! s7_is_string(optspec)) { return s7_wrong_type_arg_error(sc, "getopt*", 1, optspec, "a string"); } if (! s7_is_list(sc, command_line)) { return s7_wrong_type_arg_error(sc, "getopt*", 2, command_line, "a list of strings"); } s7_int argc = s7_list_length(sc, command_line); char** argv = malloc(argc * sizeof(char*)); if (argv == NULL) { error_exit("Can't allocate " S7INT_PRINTF " bytes for command-line in command_line_getopt", argc * sizeof(char*)); } // Assemble an argv list. // Note that getopt() wants an array of pointers to char*, not // 'const char*'; I'm not sure what it plans to do with the // strings, but it seems safest, therefore, to pass it an array of // pointers to copies. { s7_pointer cl = command_line; for (int i=0; ipattern)); if (p->m != NULL) { free(p->m); p->m = NULL; } // p->re will always be a compiled RE, // since the object won't be successfully created otherwise regfree(&p->re); free(p); return NULL; } static s7_pointer mark_re(s7_scheme *sc, s7_pointer obj) { re_t p = (re_t)s7_c_object_value(obj); s7_mark(p->pattern); return NULL; } static s7_pointer re_to_string(s7_scheme *sc, s7_pointer args) { re_t p = (re_t)s7_c_object_value(s7_car(args)); const char* fmt = ""; const size_t slen = s7_string_length(p->pattern) + strlen(fmt) + 1; char* s = alloca(slen); snprintf(s, slen, fmt, s7_string(p->pattern)); return s7_make_string(sc, s); } #define H_make_re \ "`(regexp \"pattern\" [flag ...])` : compile the pattern (extended RE syntax).\n" \ "\n" \ "Flags:\n" \ " * `'ignore-case` : the pattern is compiled\n" \ " so that it ignores case when matching.\n" \ " * `'no-substitute` : the pattern\n" \ " is compiled so that the matching functions will report only success/fail,\n" \ " and not record subexpressions (a mild optimisation, where appropriate).\n" \ " * `'newline` : use alternate newline processing, as described for\n" \ " the `REG_NEWLINE` flag in regcomp(3)." static s7_pointer make_re_proc(s7_scheme *sc, s7_pointer args) { // args is of length at least 1 s7_pointer pattern = s7_car(args); // flags is a list of symbols adjusting how the pattern is compiled; // the only one currently defined is 'ignore-case s7_pointer rest = s7_cdr(args); if (! s7_is_string(pattern)) { return s7_wrong_type_arg_error(sc, "regexp", 1, pattern, "a string (pattern)"); } re_t p = malloc(sizeof(struct re_s)); if (p == NULL) { error_exit("Can't allocate memory for regexp!"); } if (s7_string_length(pattern) == 0) { // Special case. // // This is the 'empty regexp', "". The regcomp function doesn't regard // this as a valid regexp, and returns an error in this case. But we // want to handle this, so we can split on empty regexps. The // function will happily compile "()", so supply that instead. // // This means that we do end up with the compiled regexp // wanting to save a pattern. I was tempted to hack this away // by adjusting re_nsub post-compilation, but that seems // fragile. Instead, we flag this case, and special-case it // in re_match_positions, below. pattern = s7_make_string(sc, "()"); p->empty_pattern_p = 1; } else { p->empty_pattern_p = 0; } int compile_flags = REG_EXTENDED; for (int flagn = 2; !s7_is_null(sc, rest); flagn++, rest=s7_cdr(rest)) { // The various regcomp() flags defined are: // // REG_EXTENDED // Use Extended Regular Expressions. // REG_ICASE // Ignore case in match (see XBD Regular Expressions). // REG_NOSUB // Report only success/fail in regexec(). // REG_NEWLINE // Change the handling of characters s7_pointer flag1 = s7_car(rest); if (! s7_is_symbol(flag1)) { return s7_wrong_type_arg_error(sc, "regexp", flagn, flag1, "a symbol"); } const char* flag1_string = s7_symbol_name(flag1); if (strcmp(flag1_string, "ignore-case") == 0) { compile_flags |= REG_ICASE; } else if (strcmp(flag1_string, "no-substitute") == 0) { compile_flags |= REG_NOSUB; } else if (strcmp(flag1_string, "newline") == 0) { compile_flags |= REG_NEWLINE; } else { scheme_eval("print-warning", s7_make_string(sc, "regexp: compiling \"~a\": unrecognised flag symbol '~s (ignored)"), pattern, flag1, NULL); } } p->pattern = pattern; s7_pointer result; int regcomp_status; if ((regcomp_status = regcomp(&p->re, s7_string(pattern), compile_flags)) == 0) { if (compile_flags & REG_NOSUB) { p->m = NULL; } else { //fprintf(stderr, "pattern \"%s\" has %zu submatches\n", s7_string(pattern), p->re.re_nsub); size_t nmatch = p->re.re_nsub + 1; p->m = malloc(nmatch * sizeof(regmatch_t)); if (p->m == NULL) { error_exit("Can't allocate memory for %zu matches for re \"%s\"", nmatch, s7_string(pattern)); } } result = s7_make_c_object(sc, re_type_tag, (void*)p); } else { char buf[BUFSIZ]; regerror(regcomp_status, &p->re, buf, BUFSIZ); scheme_eval("print-warning", s7_make_string(sc, "Error compiling regexp pattern \"~a\": ~a"), pattern, s7_make_string(sc, buf), NULL); free(p); p = NULL; result = s7_f(sc); } if (regcomp_status == 0) { assert(p->pattern != NULL); assert((compile_flags & REG_NOSUB) ? (p->m == NULL) : (p->m != NULL)); assert(p->empty_pattern_p == 0 || p->empty_pattern_p == 1); assert(s7_boolean(sc, result)); } else { assert(p == NULL); assert(! s7_boolean(sc, result)); } return result; } static s7_pointer re_is_equal_proc(s7_scheme* sc, s7_pointer args) { s7_pointer o1 = s7_car(args); s7_pointer o2 = s7_cadr(args); if (o1 == o2) return s7_t(sc); if (!s7_is_c_object(o2) || (s7_c_object_type(o2) != re_type_tag)) return s7_f(sc); re_t p1 = (re_t)s7_c_object_value(o1); re_t p2 = (re_t)s7_c_object_value(o2); return s7_make_boolean(sc, s7_is_equivalent(sc, p1->pattern, p2->pattern)); } // Until revision 07d99b2a9127, this function supported an // end-of-range parameter, to limit the part of the string scanned by // the regexp (this was added only a few revisions before then). // However that was done using a function regnexec, which extends // POSIX and is standard on macOS, but seems not to be present in GNU // libc. I could work around that, but it would create configuration // and code-path complications for a feature I actually never use. #define H_re_match_positions \ "`(regexp-match** re s start flags)` : " \ "to be called only internally. " \ "Arguments\n" \ "\n" \ " re : a compiled re?\n" \ " string : a string? to be matched\n" \ " start : character number to start the match\n" \ " flags : flags adjusting the way the search is done\n" \ " (the only flags at present are 1 (ie, bit 1 set)\n" \ " which avoids storing the matches,\n" \ " and 2, which returns strings rather than a\n" \ " position pair)\n" \ "\n" \ "Returns a list of cons pairs indicating the start and end\n" \ "characters of a match, where the first is the whole string\n" \ "which matches the regexp, and the successive ones correspond to\n" \ "parenthesised matches in the regexp.\n" \ "\n" \ "If the regexp doesn't match, then return #f.\n" static s7_pointer re_match_positions_proc(s7_scheme* sc, s7_pointer args) { s7_pointer re_arg = s7_car(args); s7_pointer s_arg = s7_cadr(args); s7_pointer si_start = s7_caddr(args); s7_pointer flags_arg = s7_cadddr(args); re_t re; const char* s; s7_int i_start = 0; s7_int flags = 0; if (is_re_p(re_arg)) { re = (re_t) s7_c_object_value(re_arg); } else { return s7_wrong_type_arg_error(sc, "regexp-match**", 1, re_arg, "a re? object"); } if (s7_is_string(s_arg)) { s = s7_string(s_arg); } else { return s7_wrong_type_arg_error(sc, "regexp-match**", 2, s_arg, "a string?"); } if (s7_is_integer(si_start)) { i_start = s7_integer(si_start); if (i_start < 0) { return return_beastie_error(sc, "regexp-match** \"%s\": start-index %lld is negative", s, i_start); } else if (i_start > s7_string_length(s_arg)) { //?or return_beastie_error(sc, "regexp-match** \"%s\": start-index %lld is beyond end of string", s, i_start); return s7_f(sc); } } else { return s7_wrong_type_arg_error(sc, "regexp-match**", 3, si_start, "a non-negative integer"); } if (s7_is_integer(flags_arg)) { flags = s7_integer(flags_arg); } else { return s7_wrong_type_arg_error(sc, "regexp-match**", 4, flags_arg, "a non-negative integer"); } char match_only_p = flags & 1; char return_strings_p = flags & 2; assert(i_start >= 0); s7_pointer result; if (i_start > s7_string_length(s_arg)) { // starting past the end of the string // (starting exactly at the end of the string is OK if the re // matches a zero length string) result = s7_f(sc); } else if (re->empty_pattern_p) { // special case this if (return_strings_p) { result = s7_list(sc, 1, s7_make_string(sc, "")); } else { result = s7_list(sc, 1, s7_cons(sc, si_start, si_start)); } } else if (match_only_p || re->m == NULL) { // re->m is NULL if the regexp was compiled with REG_NOSUB if (regexec(&re->re, &s[i_start], 0, NULL, 0) == 0) { result = s7_t(sc); } else { result = s7_f(sc); } } else { assert(re->m != NULL); if (regexec(&re->re, &s[i_start], re->re.re_nsub + 1, re->m, 0) == 0) { result = s7_nil(sc); // re->m is re->re.re_nsub+1 items long for (int mi=re->re.re_nsub; mi>=0; mi--) { // Fields .rm_so and rm_eo are offsets from s[i_start], // so add that back in _unless_ there was no match, // in which case we must leave these as -1 int so = re->m[mi].rm_so; if (so >= 0) so += i_start; int eo = re->m[mi].rm_eo; if (eo >= 0) eo += i_start; // fprintf(stderr, " (" S7INT_PRINTF "," S7INT_PRINTF ") -> (%d,%d)\n", // re->m[mi].rm_so, re->m[mi].rm_eo, so, eo); if (so < 0) { // a subexpression which didn't participate in the match result = s7_cons(sc, s7_f(sc), result); } else if (return_strings_p) { result = s7_cons(sc, s7_make_string_with_length(sc, &s[so], eo-so), result); } else { result = s7_cons(sc, s7_cons(sc, s7_make_integer(sc, so), s7_make_integer(sc, eo)), result); } } } else { result = s7_f(sc); } } return result; } void define_re_functions(s7_scheme* sc) { re_type_tag = s7_make_c_type(sc, "re"); s7_c_type_set_gc_free(sc, re_type_tag, free_re_object); s7_c_type_set_gc_mark(sc, re_type_tag, mark_re); s7_c_type_set_is_equal(sc, re_type_tag, re_is_equal_proc); s7_c_type_set_to_string(sc, re_type_tag, re_to_string); s7_define_function(sc, "regexp?", is_re_proc, 1, 0, false, "`(regexp? x)` : return `#t` if X is a regular expression"); s7_define_function(sc, "regexp", make_re_proc, 1, 0, true, H_make_re); s7_define_function(sc, "regexp=?", re_is_equal_proc, 2, 0, false, "(regexp=? re1 re2) : return #t if the two REs match the same pattern."); s7_define_function(sc, "regexp-match**", re_match_positions_proc, 4, 0, false, H_re_match_positions); } // various filesystem things static s7_pointer get_current_directory_proc(s7_scheme* sc, s7_pointer args) { char* buf = alloca(MAXPATHLEN); return s7_make_string(sc, getcwd(buf, MAXPATHLEN)); } static s7_pointer setenv_proc(s7_scheme* sc, s7_pointer args) { s7_pointer env_name = s7_car(args); s7_pointer value = s7_cadr(args); if (! s7_is_string(env_name)) { return s7_wrong_type_arg_error(sc, "setenv", 1, env_name, "a string"); } if (s7_is_string(value)) { setenv(s7_string(env_name), s7_string(value), 1); } else if (! s7_boolean(sc, value)) { // value is #f unsetenv(s7_string(env_name)); } else { return s7_wrong_type_arg_error(sc, "setenv", 2, value, "a string or #f"); } return s7_f(sc); } static s7_pointer symbol_lt_proc(s7_scheme* sc, s7_pointer args) { // (symbol= 0) return s7_f(sc); a1 = a2; } return s7_t(sc); } // ctype functions // The list of ctype functions is those included in ISO-9899-2018, Sect 7.4.1 #define UNIPROP_PROC(c_func) uniprop_ ## c_func ## _proc #define MAKE_UNIPROP_FUNCTION(c_func, scm_func) \ MAKE_CHAR_FUNCTION(UNIPROP_PROC(c_func), UNIPROP_FUNC(c_func), scm_func) #define MAKE_CHAR_FUNCTION(c_proc, c_func, scm_func) \ static s7_pointer c_proc(s7_scheme* sc, s7_pointer args) \ { \ s7_pointer c = s7_car(args); \ s7_int ci; \ if (s7_is_character(c)) { \ ci = s7_character(c); \ } else if (s7_is_integer(c)) { \ ci = s7_integer(c); \ } else { \ return s7_f(sc); \ } \ return s7_make_boolean(sc, c_func(ci)); \ } #if HAVE_ICU #define WRAPDOC(doc) doc #else #define WRAPDOC(doc) doc " (BMP only)" #endif // this is used in the list of delayed functions below #define DEFINE_UNIPROP_FUNCTION(c_func, scm_func, doc) \ DEFINE_CHAR_FUNCTION(UNIPROP_PROC(c_func), scm_func, WRAPDOC(doc)) #define DEFINE_CHAR_FUNCTION(c_func, scm_func, doc) \ { scm_func, \ c_func, \ 1, 0, false, \ WRAPDOC("Return #t if the argument is character or integer, and is " doc ", and #f otherwise") } #if ALL_FUNCTIONS MAKE_UNIPROP_FUNCTION(alnum_p, "char-alnum?"); MAKE_UNIPROP_FUNCTION(letter_p, "char-alpha?"); MAKE_UNIPROP_FUNCTION(uppercase_letter_p, "char-upper?"); MAKE_UNIPROP_FUNCTION(lowercase_letter_p, "char-lower?"); // MAKE_UNIPROP_FUNCTION(other_letter_p, "char-other-letter?"); MAKE_UNIPROP_FUNCTION(number_p, "char-digit?"); MAKE_UNIPROP_FUNCTION(space_p, "char-space?"); MAKE_UNIPROP_FUNCTION(whitespace_p, "char-wordbreak?"); MAKE_UNIPROP_FUNCTION(nonbreakingspace_p, "char-nbsp?"); MAKE_UNIPROP_FUNCTION(punctuation_p, "char-punct?"); MAKE_UNIPROP_FUNCTION(cntrl_p, "char-space?"); // the following have no ctype analogues MAKE_UNIPROP_FUNCTION(symbol_p, "char-symbol?"); MAKE_UNIPROP_FUNCTION(mark_p, "char-mark?"); MAKE_UNIPROP_FUNCTION(alphabetic_p, "uchar-alphabetic?"); MAKE_UNIPROP_FUNCTION(wordcharacter_p, "uchar-word-character?"); // the following don't have Unicode cases MAKE_CHAR_FUNCTION(isblank_proc, isblank, "char-blank?"); MAKE_CHAR_FUNCTION(isgraph_proc, isgraph, "char-graph?"); MAKE_CHAR_FUNCTION(isprint_proc, isprint, "char-print?"); MAKE_CHAR_FUNCTION(isxdigit_proc, isxdigit, "char-xdigit?"); // The mycu_*case_character functions return 0 if the character has no // corresponding mapping. Here, return the original character in that case. #define MAKE_CASECHANGE_FUNCTION(c_func) \ static s7_pointer UNIPROP_PROC(c_func)(s7_scheme* sc, s7_pointer args) \ { \ s7_pointer c = s7_car(args); \ s7_int ci; \ if (s7_is_character(c)) { \ ci = s7_character(c); \ } else if (s7_is_integer(c)) { \ ci = s7_integer(c); \ } else { \ return c; \ } \ s7_int newc = UNIPROP_FUNC(c_func)(ci); \ return s7_make_integer(sc, (newc == 0 ? ci : newc)); \ } MAKE_CASECHANGE_FUNCTION(uppercase_character); MAKE_CASECHANGE_FUNCTION(lowercase_character); MAKE_CASECHANGE_FUNCTION(titlecase_character); #define DEFINE_CASECHANGE_FUNCTION(c_func, scm_func, doc) \ { scm_func, \ UNIPROP_PROC(c_func), \ 1, 0, false, \ WRAPDOC(doc) } static s7_pointer load_builtin_module_by_name(s7_scheme* sc, s7_pointer args) { s7_pointer module_name = s7_car(args); s7_pointer target_env = s7_cadr(args); // this is an internal-only function if (! s7_is_symbol(module_name)) { return s7_wrong_type_arg_error(sc, "load-builtin-module/name*", 1, module_name, "a symbol"); } if (! s7_is_let(target_env)) { return s7_wrong_type_arg_error(sc, "load-builtin-module/name*", 2, target_env, "a let"); } const char* module_name_c = s7_symbol_name(module_name); size_t module_content_len; const char* module_content = get_module_content(module_name_c, &module_content_len); // fprintf(stderr, "module_content[%lu] = <%.40s...%d>\n", module_content_len, module_content, // module_content[module_content_len]); if (module_content) { s7_load_c_string_with_environment(sc, module_content, module_content_len, target_env); return target_env; } else { return s7_f(sc); } }; #endif /* ALL_FUNCTIONS */ struct delayed_functions_s { const char *name; s7_function fnc; s7_int required_args; s7_int optional_args; bool rest_arg; const char *doc; } delayed_functions[] = { // These aren't in any particular order -- we work through them with a linear search. #if ALL_FUNCTIONS { "parse-author-list**", parse_authorlist_proc, 2, 0, false, "(parse-author-list** author-string #f) : " "The AUTHOR-STRING is the BibTeX-style author list to be parsed. " "The LOCATION is intended to be a string with an indication of location, " "for error messages, but this is currently unimplmented, " "and must be passed as #f." }, { "parse-aux-source**", parse_aux_source_proc, 2, 0, false, "(parse-aux-source** file? source) : " "if FILE?, then SOURCE is a filename or #f (indicating stdin); " "if (not FILE?) then SOURCE is a string to be parsed." }, { "parse-bst-source**", parse_bst_source_proc, 2, 0, false, "(parse-bst-source** file? source) : " "if FILE?, then SOURCE is a filename or #f (indicating stdin); " "if (not FILE?) then SOURCE is a string to be parsed." }, { "parse-fmtstring**", parse_fmtstring_proc, 1, 0, false, "(parse-fmtstring* str) : Parse the BibTeX format string, for format-names" }, { "parse-markdown-source/metadata**", parse_markdown_source_proc, 3, 0, false, "(parse-markdown-source/metadata** file? accumulator source) : " "if FILE?, then SOURCE is a filename or #f (indicating stdin); " "if (not FILE?) then it is a string to be parsed. " "In both cases, parse the input, returning the parse tree, " "and putting metadata into the accumulator." }, { "parse-mdinline**", parse_mdinline_string_proc, 4, 0, false, "(parse-mdinline** str metadata source lineno) : " "Parse the given inline .md content; return an xexpr." }, { "parse-json-source**", parse_json_source_proc, 2, 0, false, "(parse-json-source** file? source) : " "if FILE?, then SOURCE is a filename or #f (indicating stdin); " "if (not FILE?) then SOURCE is a string to be parsed." }, #endif /* ALL_FUNCTIONS */ // The Unicode functions are defined in unicode-scm.c // the first lot should be available in the ALL_FUNCTIONS=0 case // (ie, within beastie0, because they're used in readermacros.scm) { "make-ustring", make_ustring_proc, 0, 0, true, "`(make-ustring [codepoint/string/ustring ...])` :\n" "make a ustring, optionally adding content.\n\n" "The list of arguments is the same as that for ustring-append, qv." }, { "ustring?", is_ustring_proc, 1, 0, false, "`(ustring? x)` : return `#t` if x is a ustring" }, { "unicode-decode1/port/utf8", unicode_decode1_port_utf8_proc, 0, 1, false, "`(unicode-decode1/port/utf8 [p])` : " "decode a single unicode character from the input port,\n" "which should be UTF-8 encoded.\n" "If no argument is supplied, it defaults to `(current-input-port)`.\n" "This function isn't (currently) particularly robust against malformed UTF-8 input.\n" "It shouldn't collapse, but may skip characters when recovering.\n\n" "Returns the next codepoint from the input, or `#`;\n" "returns Unicode replacement character on malformed input." }, { "ustring-append!", ustring_append_inplace_proc, 1, 0, true, "`(ustring-append! ustring char|codepoint|... ...)` :\n" "append the arguments to the ustring.\n" "The replacement happens in place – the initial `ustring` argument is modified\n" "(cf ustring-append).\n" "The list of possible argument types is the same as for `ustring-append`."}, #if ALL_FUNCTIONS { "unicode-reader?", is_unicode_reader_proc, 1, 0, false, "`(unicode-reader? x)` : return `#t` if x is a unicode-reader" }, { "make-unicode-reader/file*", make_unicode_reader_file_proc, 2, 0, false, "`(make-unicode-reader/file* fn ascii-characters?)` :\n" "make a unicode reader from a file FN.\n" "If ASCII-CHARACTERS? is true, then the reader will return codepoints\n" "below 0x80 as characters rather than integers\n" "(this is sometimes convenient for debugging, but is unnecessary\n" "in general use)." }, { "make-unicode-reader/string*", make_unicode_reader_string_proc, 2, 0, false, "`(make-unicode-reader/string* str ascii-characters?)` :\n" "make a unicode reader from a string, STR.\n" "If ASCII-CHARACTERS? is true, then the reader will return codepoints\n" "below 0x80 as characters rather than integers." }, { "unicode-reader-read", unicode_reader_read_proc, 1, 0, false, "`(unicode-reader-read unicode-reader)` : " "return a single codepoint from the stream.\n" "Returns `#` at the end of the stream.\n\n" "The unicode-reader object can also be called directly,\n" "as a no-argument function, to return the codepoint,\n" "and it can be called with argument `'location` to give the current\n" "location, as an alternative to `unicode-reader-location`." }, { "unicode-decode/utf8", unicode_decode_utf8_proc, 1, 0, false, "`(unicode-decode/utf8 s)` : " "decode the UTF8 string\n" "(an s7 string, regarded as a sequence of bytes encoded in UTF-8)\n" "into a ustring" }, { "unicode-encode/utf8", unicode_encode_utf8_proc, 1, 0, false, "`(unicode-encode/utf8 l)` : " "given a list or iterator of Unicode codepoints, convert it to a UTF8 string\n" "(an s7 string, regarded as a sequence of bytes)"}, { "unicode-encode1/utf8", unicode_encode1_utf8_proc, 1, 0, false, "`(unicode-encode1/utf8 i)` : " "given a single character or integer, convert it to a UTF8 string\n" "(an s7 string, regarded as a sequence of bytes)" }, { "unicode-reader-source", unicode_reader_source_proc, 1, 0, false, "`(unicode-reader-source rdr)` :\n" "show the source from which the reader draws its content,\n" "either as a filename, or as a \"string\", enclosed in quotes.\n" "The output is intended to be printable."}, { "unicode-reader-location", unicode_reader_location_proc, 1, 0, false, "`(unicode-reader-location rdr)` :\n" "show the current location within the reader's source." }, { "ustring-length", ustring_length_proc, 1, 0, false, "`(ustring-length us)` : return the number of characters (ie, codepoints) in the ustring" }, { "ustring=?", ustring_is_equal_proc, 2, 0, true, "`(ustring=? s1 s2 [flags...])` : " "return true if the two arguments are ustrings or strings, and equal.\n" "Arguments of type `string?` are converted to `ustring?` before comparison.\n\n" #if HAVE_ICU "There are no flags defined at present." #else "If the flag `:collapse-replacements` is present, then sequences of\n" "unicode replacement characters, U+fffd, will compare equal even if\n" "they contain different numbers of these characters." #endif }, { "ustring->hash", ustring_hash_proc, 1, 0, false, "`(ustring->hash u)` : return a hash integer for the ustring" }, { "ustring-cache-object-get*", ustring_cache_object_get_proc, 1, 0, false, "`(ustring-cache-object-get* us)` : retrieve the cache-object for the ustring.\n" "The procedure evaluates to the stored object, or `#f` if nothing has been stored." }, { "ustring-cache-object-set!*", ustring_cache_object_set_proc, 2, 0, false, "`(ustring-cache-object-set!* us obj)` : store the cache-object for the ustring.\n" "The procedure will store any object as the cache-object:\n" "it is up to the caller to add key-value semantics.\n" "The procedure evaluates to the newly-stored cache-object."}, { "string->hash", string_to_hash_proc, 1, 0, false, "`string->hash : string? -> integer?` : Given a string, return an integer hash for it. " "This is an _unsophisticated_ hash function, " "merely using the K&R/Java hash function" }, { "ustringstring", ustring_to_string_proc, 1, 1, false, "`(ustring->string ustr [:display/:write/:readable/#t/#f])` \n" ": returns the ustring `str` converted to a (normal) UTF-8 string.\n" "If the second argument is `:display` or `#f`, then the string is suitable for display\n" "rather than for subsequent reading; otherwise, it is in a form which can be\n" "re-read to produce an equivalent object."}, { "ustring->symbol", ustring_to_symbol_proc, 1, 0, false, "`(ustring->symbol ustr)` : returns the ustring `str` converted to a symbol." }, { "symbol->ustring", symbol_to_ustring_proc, 1, 0, false, "`(symbol->ustring sym)` : returns the symbol `sym` converted to a new ustring" }, { "ustring-map-internal*", ustring_map_internal_proc, 2, 0, false, "`(ustring-map-internal* ustring sym)` : map a ustring with an internal function (internal use only)."}, // { "make-ustring-iterator*", // make_ustring_iterator_proc, // 1, 0, false, // "`(make-ustring-iterator* ustring?)` : make a ustring iterator (internal only)" }, { "unicode-load-hook*", unicode_load_hook, 3, 0, false, "Defines unicode C-implemented functions (internal only!)" }, // see mdblock-parse.lex { "mdblock-load-hook*", mdblock_load_hook, 3, 0, false, "Define mdblock C-implemented functions (internal only!)" }, // see lex-bib2.c { "make-biblex*", make_biblex_proc, 1, 0, false, "`(make-biblex* unicode-reader?)` :create a lexer for .bib files, reading from the given reader" }, { "biblex?", is_biblex_proc, 1, 0, false, "`(biblex? x)` : return `#t` if `x` is a .bib file lexer"}, { "biblex-load-hook*", // this also defines make-biblex/file* and make-biblex/string* biblex_load_hook, 3, 0, false, "Define biblex C-implemented functions (internal only!)" }, #endif /* ALL_FUNCTIONS */ { "getopt*", command_line_getopt_proc, 3, 0, false, // don't document the command-line keyword argument -- used for testing "(getopt* optspec command-line silent-opterr) : " "a helper function for the (getopt ... ) macro.\n" "Parse the list of strings in the command-line using the\n" "given option-specification (see getopt(3)). " "The result is a list (unspecified order) where the car is an alist of\n" "`(#\\optchar . argument)`, and the cdr is the non-option arguments;\n" "if an option takes no argument, the 'argument' is unspecified.\n" "Unexpected options are reported on stderr, unless `silent-opterr` non-#f,\n" "in which case they are returned as `(#\\? . #\\optchar)`" }, { "subprocess", subprocess_proc, 1, 0, true, "`subprocess : string? ... -> (or string? #f)`" "(subprocess \"program\" ...) : " "Call the first argument, as a command, " "passing the others as arguments. Returns stdout as a string, " "or #f if the command returns non-zero, or raise 'subprocess if the command can't be run. " "\n\n" "If the PROGRAM doesn't start with a slash, '/', it is looked up in the path." }, { "current-directory", get_current_directory_proc, 0, 0, false, "`(current-directory)` : return the current working directory, as a string." }, { "setenv", setenv_proc, 2, 0, false, "`(setenv envvar (or/c string? #f))` :\n" "set the environment variable `envvar` to the given value.\n" "If the value is `#f`, the environment variable is deleted."}, { "symbol boolean?` : " "true if the symbols `s1`... are strictly ordered. " "The comparison is equivalent to that which would arise from using `symbol->string`." }, #if ALL_FUNCTIONS // for this macro, see above DEFINE_UNIPROP_FUNCTION(alnum_p, "char-alnum?", "alphanumeric (Unicode L or N)"), DEFINE_UNIPROP_FUNCTION(letter_p, "char-alpha?", "alphabetic (Unicode L)"), DEFINE_UNIPROP_FUNCTION(uppercase_letter_p, "char-upper?", "upper-case alphabetic (Unicode Lu or Lt)"), DEFINE_UNIPROP_FUNCTION(lowercase_letter_p, "char-lower?", "lower-case alphabetic (Unicode Ll)"), // DEFINE_UNIPROP_FUNCTION(other_letter_p, // "char-other-letter?", // "other alphabetic letter (Unicode L other than Lu, Lt or Ll; BMP only)"), DEFINE_UNIPROP_FUNCTION(number_p, "char-digit?", "a digit (Unicode N)"), DEFINE_UNIPROP_FUNCTION(space_p, "char-space?", "a space (Unicode Z, but including the ASCII whitespace\n" "characters below U+20)." #if HAVE_ICU "\n\nThere is more than one reasonable definition of whitespace.\n" "The whitespace characters below U+20, such as newline and tab,\n" "are _not_ whitespace in Unicode terms, but we include them here\n" "in order to match POSIX/ctype isspace; this also includes\n" "non-breaking spaces as whitespace\n" "(though, eg, Java's whitespace definition doesn't)." #endif ), DEFINE_UNIPROP_FUNCTION(whitespace_p, "char-wordbreak?", "true if the specified code point is a whitespace character\n" "according to Java/ICU.\n\n" "This is similar to `char-space?`, except that it does\n" "_not_ include the non-breaking space codepoints\n" "(U+00A0 NBSP, or U+2007 Figure Space or U+202F Narrow NBSP).\n" "It is therefore defined to match the Java\n" "`Character.isWhitespace` function;\n" "See [Unicode ICU docs u_isWhitespace function](https://unicode-org.github.io/icu-docs/apidoc/dev/icu4c/uchar_8h.html)\n" "for further discussion."), DEFINE_UNIPROP_FUNCTION(nonbreakingspace_p, "char-nbsp?", "true if the specified codepoint is a non-breaking space,\n" "and specifically whether it is\n" "U+00A0 NBSP, or U+2007 Figure Space or U+202F Narrow NBSP.\n" "Compare `char-wordbreak?`."), DEFINE_UNIPROP_FUNCTION(punctuation_p, "char-punct?", "punctuation (Unicode P)"), DEFINE_UNIPROP_FUNCTION(cntrl_p, "char-cntrl?", "control (ctype iscntrl, plus Unicode Cc & Cf)"), // the following two have no ctype analogues DEFINE_UNIPROP_FUNCTION(symbol_p, "char-symbol?", "symbol (Unicode S)"), DEFINE_UNIPROP_FUNCTION(mark_p, "char-mark?", "mark (Unicode M)"), { "uchar-alphabetic?", UNIPROP_PROC(alphabetic_p), 1, 0, false, "Returns `#t` if the character has the Unicode ‘Alphabetic’ property.\n" "That is, it is lowercase, uppercase, of classes Lt, Lm, Lo, or Nl,\n" "or is ‘other alphabetic’. See the Unicode Character Database for details.\n" "\n" "The induced set of characters is a superset of that induced by `char-letter?`.\n" "This function is also distinct from the s7 `char-alphabetic?` function,\n" "which is defined only for ASCII characters\n" "(but it overlaps with that function in that range).\n" }, { "uchar-word-character?", // There is a general discussion of the question of what should and // shouldn't be included as a 'word character' in a 2018 // thread on the Unicode mailing list. See before and after the // message at https://www.unicode.org/mail-arch/unicode-ml/y2018-m05/0104.html // and specifically // https://www.unicode.org/mail-arch/unicode-ml/y2018-m05/0117.html UNIPROP_PROC(wordcharacter_p), 1, 0, false, "Returns `#t` if the character argument should be regarded as a part of a word.\n" "The induced character set includes `uchar-alphabetic?` (and thus `char-alpha?`),\n" "but includes (the Unicode categories)\n" "diacritics, extender characters and join-control characters.\n" "\n" "There isn't a formal definition of this set within Unicode,\n" "but this set has been\n" "[described](https://www.unicode.org/mail-arch/unicode-ml/y2018-m05/0117.html)\n" "as ‘a decent approximation of what is (naively) expected to fall\n" "within an “alphabetic” string for most scripts’." }, DEFINE_CHAR_FUNCTION(isblank_proc, "char-blank?", "blank (in terms of ctype.h)"), DEFINE_CHAR_FUNCTION(isgraph_proc, "char-graph?", "graphic (in terms of ctype.h)"), DEFINE_CHAR_FUNCTION(isprint_proc, "char-print?", "printing (in terms of ctype.h)"), DEFINE_CHAR_FUNCTION(isxdigit_proc, "char-xdigit?", "a hex-digit (in terms of ctype.h)"), DEFINE_CASECHANGE_FUNCTION(uppercase_character, "uchar-upcase", "`(uchar-upcase c)` : \n" "change the given character to uppercase.\n\n" #if HAVE_ICU "This matches `char-upcase` for ASCII characters,\n" "but it also works correctly for other Unicode characters.\n" #else "This matches `char-upcase` for ASCII characters,\n" "but it also works correctly for the majority of cases\n" "within the Unicode BMP (there are number of Unicode\n" "edge-cases which we evade at present).\n" "\n" #endif "The function accepts either\n" "a character or an integer argument,\n" "and returns an integer.\n" "If given an argument of another type,\n" "including string types,\n" "it returns the argument unchanged."), DEFINE_CASECHANGE_FUNCTION(lowercase_character, "uchar-downcase", "Change the given character to lowercase.\n" "Analogous to `uchar-upcase`, qv."), DEFINE_CASECHANGE_FUNCTION(titlecase_character, "uchar-titlecase", "Change the given character to titlecase.\n" "Analogous to `uchar-upcase`, qv."), #endif /* ALL_FUNCTIONS */ }; size_t delayed_functions_len = sizeof(delayed_functions)/sizeof(delayed_functions[0]); static s7_pointer delayed_define_function_into_env(s7_scheme* sc, s7_pointer args) { s7_pointer function_name = s7_car(args); s7_pointer env = s7_cadr(args); if (! s7_is_symbol(function_name)) { s7_wrong_type_arg_error(sc, "define-function/delayed/env*", 1, function_name, "a symbol"); } if (! s7_is_let(env)) { s7_wrong_type_arg_error(sc, "define-function/delayed/env*", 2, env, "a let"); } const char* required_name = s7_symbol_name(function_name); for (int i=0; i