Paul, Thanks for the review! 1. I will remove the use of GCC C extensions, the use of the _Generic C11 feature, and the use of compound literals. 2. I do need help getting started on the copyright paperwork though I seem to remember completing this before. At any rate, I will sign it again if necessary. 3. I will add unit tests in my next submission in addition to addressing all of your comments.
On Sun, Dec 8, 2024 at 3:11 PM Paul Smith <psm...@gnu.org> wrote: > Thanks for this work Pete! > > I reviewed the previous (lengthy) email thread before looking at this > patch. > > This is a good first crack. There are a few confusing bits and we will > need tests added for the various corner cases, and documentation. I > can do the docs if you prefer; I usually edit them anyway to keep the > manual in a single "voice" as much as possible. > > For a contribution this size we'll need copyright paperwork to be > signed; let me know if you need my help with getting that started. > > > On Fri, 2024-11-29 at 04:14 -0800, Pete Dietl wrote: > > From 76db129404896c15440b405949b8c830e7ebb98f Mon Sep 17 00:00:00 > > 2001 > > From: Pete Dietl <petedi...@gmail.com> > > Date: Fri, 29 Nov 2024 03:57:06 -0800 > > Subject: [PATCH] Add arithmetic builtin functions > > > > --- > > src/function.c | 401 > > ++++++++++++++++++++++++++++++++++++++++++++++++- > > 1 file changed, 400 insertions(+), 1 deletion(-) > > > + default: > > + /* Handle invalid operation */ > > + return 0.0; > > I prefer to not add a default: to switch statements which are based on > enum values. This way the compiler will warn us if we forget to handle > any of the enumerations. > > Also, returning 0 here is not right anyway and would just lead to > confusing behavior. If you really want to check at runtime that the > operator is valid you can add a call to fatal() about an internal error > just before the end of the function (after the switch) since every case > invokes return anyway. > > > +/* Generic macro to select the correct function based on type and > > + assert that the types of a and b are the same */ > > > + char[__builtin_types_compatible_p (typeof (a), typeof (b)) ? > > 1 : -1]), \ > > + _Generic > > ((a), \ > > As noted by Gisle, we shouldn't use either GCC-specific features > (unless they are just hints) or features introduced after C99. > > Also in GNU Make code we just write "long long" rather than the full > "long long int". > > > + double: > > generic_math_op_double, \ > > + long long int: generic_math_op_ll) (a, b, op)) > > + > > +static struct number > > +parse_number (const char *s, const char *op_name) > > +{ > > + const char *beg = s; > > + const char *end = s + strlen (s) - 1; > > + char *endp; > > + struct number ret = { .integer = 0, .type = t_integer }; > > + > > + while (isspace(*s)) > > + s++; > > Please use the NEXT_TOKEN (s) macro for this (see makint.h) > > > + > > + // We need to check this because even though we can command > > `strtoll` to > > + // parse only base-ten numbers, we can't command `strtod` to only > > parse > > + // base-10 numbers. Therefore, without this check `0xB` would get > > rejected by > > + // `strtoll`, but accepted by `strtod` and treated as `11.0`. > > + if (*s != '\0' && s[1] == 'x') > > Please check for both upper and lowercase "x". > > > + OSS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: '%s' not a number"), > > op_name, s); > > The _() macro has special dispensation to not have a space after the > macro name; all calls should just be _("foo") > > Also I think you should omit the word "function" from all messages and > just use "Invalid argument to %s: ..." etc. > > > + errno = 0; > > + ret.integer = strtoll (beg, &endp, 10); > > + if (errno == ERANGE) > > + OSS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: '%s' out of range"), > > op_name, s); > > + if (endp == beg || endp <= end) > > + { > > + /* Empty or not an integer */ > > + errno = 0; > > + ret.type = t_floating; > > + ret.floating = strtod (beg, &endp); > > + if (errno == ERANGE) > > + OSS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: '%s' out of > > range"), op_name, > > + s); > > + if (endp == beg || endp <= end) > > + OSS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: '%s'"), op_name, > > s); > > + } > > + > > + return ret; > > +} > > + > > +static char * > > +number_to_string (char *o, struct number n) > > This name leads to concerns about where the memory is handled. Maybe > "number_to_variable_buffer()" would be more accurate? > > > +{ > > + char buffer[64]; > > + if (n.type == t_integer) > > + snprintf (buffer, sizeof buffer, "%lld", n.integer); > > We have a function make_lltoa() in misc.c, which is more portable on > some systems. There's also a make_toui() but that's not suitable for > what you want: I have no opinion on whether you should try moving some > of these other methods to misc.c to follow that convention, or leave > them here. > > > + else > > + { > > + char *end; > > + snprintf (buffer, sizeof buffer, "%.14f", n.floating); > > + end = buffer + strlen (buffer) - 1; > > + while (end > buffer && *end == '0') > > + *end-- = '\0'; // Remove trailing zero > > + > > + // If there's a decimal point left, append a '0' to keep a > > single zero > > + if (*end == '.') > > + { > > + end++; > > + *end++ = '0'; > > + *end = '\0'; > > + } > > I think the above can be much simpler since you know for sure that the > string will contain a ".". You can just walk backwards until either > you find a non-0, or else the char before the pointer is '.' (to > preserve a single "0" after the decimal). > > > + } > > + return variable_buffer_output (o, buffer, strlen (buffer)); > > +} > > + > > +struct number > > +perform_math_op (enum math_operation op, const char *op_name, > > + struct math_operation_init init, char **argv) > > +{ > > + struct number parsed; > > + struct number total; > > + > > + if (init.init_type == t_first_value) > > + { > > + assert (*argv != NULL); > > Please don't assert here. We know it can't happen. If this is null > we'll crash in parse_number anyway. > > > + > > + total = parse_number (*argv, op_name); > > + argv++; > > + } > > + else > > + { > > + total.type = t_integer; > > + total.integer = init.constant; > > + } > > + > > + for (; *argv != NULL; argv++) > > + { > > + parsed = parse_number (*argv, op_name); > > + if (total.type == t_integer) > > + { > > + if (parsed.type == t_integer) > > + total.integer > > + = generic_math_op (total.integer, parsed.integer, > > op); > > + else // `parsed` is floating, `total`` is integer. So, > > convert total > > + // to floating > > + { > > + total.type = t_floating; > > + total.floating = generic_math_op > > ((double)total.integer, > > + parsed.floating, > > op); > > + } > > + } > > + else // `total` is floating > > + { > > + if (parsed.type == t_integer) > > + total.floating > > + = generic_math_op (total.floating, > > (double)parsed.integer, op); > > + else // `parsed` is floating, `total`` is floating > > + total.floating > > + = generic_math_op (total.floating, parsed.floating, > > op); > > + } > > + } > > + > > + return total; > > +} > > + > > +static char * > > +func_add (char *o, char **argv, const char *funcname) > > +{ > > + struct number n = perform_math_op ( > > + op_add, funcname, > > + (struct math_operation_init){ .init_type = t_constant, > > .constant = 0 }, > > + argv); > > + > > + return number_to_string (o, n); > > +} > > + > > +static char * > > +func_subtract (char *o, char **argv, const char *funcname) > > +{ > > + struct math_operation_init init; > > + struct number n; > > + > > + // If we received a single argument, negate it. > > I don't see the advantage in creating this special case. If someone > wants the negation of a value, wouldn't they just write "-$V" instead > of "$(sub $V)"? > > > + if (argv[0] != NULL && argv[1] == NULL) > > + { > > + init.init_type = t_constant; > > + init.constant = 0; > > + } > > + else > > + init.init_type = t_first_value; > > + > > + n = perform_math_op (op_subtract, funcname, init, argv); > > + > > + return number_to_string (o, n); > > +} > > + > > +static char * > > +func_multiply (char *o, char **argv, const char *funcname) > > +{ > > + struct number n = perform_math_op ( > > + op_multiply, funcname, > > + (struct math_operation_init){ .init_type = t_constant, > > .constant = 1 }, > > + argv); > > + > > + return number_to_string (o, n); > > +} > > + > > +static char * > > +func_divide (char *o, char **argv, const char *funcname) > > +{ > > + struct number parsed; > > + struct number total; > > + > > + // We either received one argument and we will return its inverse > > or we will use it as > > + // our initial value for `total` and carry on dividing. > > I don't really understand the advantage of this special case for div. > If someone wants the inverse, why wouldn't they just use $(div 1 X)? > What does this (to me kind of obscure) special case of $(div X) buy us? > > > + total = parse_number (*argv, funcname); > > + > > + if (total.type == t_integer ? total.integer == 0 : total.floating > > == 0.0) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument cannot be > > zero"), > > funcname); > > + > > + // If we received a single argument, compute its inverse. > > + if (argv[0] != NULL && argv[1] == NULL) > > + { > > + if (total.type == t_integer) > > + total.integer = 1 / total.integer; > > + else > > + total.floating = 1.0 / total.floating; > > + > > + return number_to_string (o, total); > > + } > > + > > + // We are using argv[0] as our initial value for total, so skip > > past it. > > + argv++; > > + > > + for (; *argv != NULL; argv++) > > + { > > + parsed = parse_number (*argv, funcname); > > + > > + if (parsed.type == t_integer ? parsed.integer == 0 : > > parsed.floating == 0.0) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument cannot be > > zero"), funcname); > > + if (total.type == t_integer) > > It seems like this testing of two "number" values to align them based > on type happens in a few different places. Maybe it would make sense > to create a little function just for that operation, to check the types > of the two values and align them by changing both to double if either > one is double. > > The function could even return the resulting type so the caller can > check the return value to see whether to update the integer or floating > values in the number. > > > + { > > + if (parsed.type == t_integer) > > + total.integer /= parsed.integer; > > + else // `parsed` is floating, `total`` is integer. So, > > convert total > > + // to floating > > + { > > + total.type = t_floating; > > + total.floating = (double)total.integer / > > parsed.floating; > > + } > > + } > > + else // `total` is floating > > + { > > + if (parsed.type == t_integer) > > + total.floating /= (double)parsed.integer; > > + else // `parsed` is floating, `total`` is floating > > + total.floating /= parsed.floating; > > + } > > + } > > + > > + return number_to_string (o, total); > > +} > > + > > +static char * > > +func_modulus (char *o, char **argv, const char *funcname) > > +{ > > + struct number parsed; > > + struct number total; > > + > > + // We require exactly two arguments > > + assert(argv[0] != NULL && argv[1] != NULL && argv[2] == NULL); > > No asserts please. > > > + > > + // We either received one argument and we will return its inverse > > or we will use it as > > + // our initial value for `total` and carry on dividing. > > I think this comment was copy-pasted from somewhere else...? > > > + total = parse_number (argv[0], funcname); > > I find "total" and "parsed" to be confusing names. Names related to > mod would be better. > > > + > > + if (total.type == t_floating) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument must be an > > integer"), funcname); > > + > > + if (total.integer == 0) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument cannot be > > zero"), > > funcname); > > Why can't this be 0? I think this should be allowed. > > > + > > + parsed = parse_number (argv[1], funcname); > > + > > + if (parsed.type == t_floating) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument must be an > > integer"), funcname); > > + > > + if (parsed.integer == 0) > > + OS (fatal, *expanding_var, > > + _ ("Invalid argument to %s function: argument cannot be > > zero"), > > funcname); > > + > > + total.integer %= parsed.integer; > > + > > + return number_to_string (o, total); > > +} > > + > > +static char * > > +func_maximum (char *o, char **argv, const char *funcname) > > +{ > > + struct number n = perform_math_op ( > > + op_max, funcname, > > + (struct math_operation_init){ .init_type = t_first_value}, > > + argv); > > + > > + return number_to_string (o, n); > > +} > > + > > +static char * > > +func_minimum (char *o, char **argv, const char *funcname) > > +{ > > + struct number n = perform_math_op ( > > + op_min, funcname, > > + (struct math_operation_init){ .init_type = t_first_value}, > > + argv); > > + > > + return number_to_string (o, n); > > +} > > + > > +static char * > > +func_absolute_value (char *o, char **argv, const char *funcname) > > +{ > > + struct number n; > > + > > + // We accept exactly one argumnent > > + assert(argv[0] != NULL && argv[1] == NULL); > > Please don't assert. If the issue could happen then check and throw an > error. If not, the program will dump core soon enough for null > pointers. In this case it can't happen if you have the function table > set properly. > > > + > > + n = parse_number (argv[0], funcname); > > + > > + if (n.type == t_integer) > > + n.integer = llabs(n.integer); > > + else > > + n.floating = fabs(n.floating); > > + > > + return number_to_string (o, n); > > +} > > + > > struct a_word > > { > > struct a_word *chain; > > @@ -2394,6 +2785,8 @@ static char *func_call (char *o, char **argv, > > const char *funcname); > > static const struct function_table_entry function_table_init[] = > > { > > /* Name MIN MAX EXP? Function */ > > + FT_ENTRY ("abs", 1, 1, 1, func_absolute_value), > > In general it's best if the function name matches the name of the > function, so "func_abs", "func_max", "func_sub", etc. > > > + FT_ENTRY ("add", 1, 0, 1, func_add), > > FT_ENTRY ("abspath", 0, 1, 1, func_abspath), > > FT_ENTRY ("addprefix", 2, 2, 1, func_addsuffix_addprefix), > > FT_ENTRY ("addsuffix", 2, 2, 1, func_addsuffix_addprefix), > > @@ -2401,6 +2794,7 @@ static const struct function_table_entry > > function_table_init[] = > > FT_ENTRY ("basename", 0, 1, 1, func_basename_dir), > > FT_ENTRY ("call", 1, 0, 1, func_call), > > FT_ENTRY ("dir", 0, 1, 1, func_basename_dir), > > + FT_ENTRY ("div", 1, 0, 1, func_divide), > > FT_ENTRY ("error", 0, 1, 1, func_error), > > FT_ENTRY ("eval", 0, 1, 1, func_eval), > > FT_ENTRY ("file", 1, 2, 1, func_file), > > @@ -2416,6 +2810,10 @@ static const struct function_table_entry > > function_table_init[] = > > FT_ENTRY ("join", 2, 2, 1, func_join), > > FT_ENTRY ("lastword", 0, 1, 1, func_lastword), > > FT_ENTRY ("let", 3, 3, 0, func_let), > > + FT_ENTRY ("max", 1, 0, 1, func_maximum), > > + FT_ENTRY ("min", 1, 0, 1, func_minimum), > > + FT_ENTRY ("mod", 2, 2, 1, func_modulus), > > + FT_ENTRY ("mul", 1, 0, 1, func_multiply), > > FT_ENTRY ("notdir", 0, 1, 1, func_notdir_suffix), > > FT_ENTRY ("or", 1, 0, 0, func_or), > > FT_ENTRY ("origin", 0, 1, 1, func_origin), > > @@ -2424,6 +2822,7 @@ static const struct function_table_entry > > function_table_init[] = > > FT_ENTRY ("shell", 0, 1, 1, func_shell), > > FT_ENTRY ("sort", 0, 1, 1, func_sort), > > FT_ENTRY ("strip", 0, 1, 1, func_strip), > > + FT_ENTRY ("sub", 1, 0, 1, func_subtract), > > FT_ENTRY ("subst", 3, 3, 1, func_subst), > > FT_ENTRY ("suffix", 0, 1, 1, func_notdir_suffix), > > FT_ENTRY ("value", 0, 1, 1, func_value), > > -- > Paul D. Smith <psm...@gnu.org> Find some GNU Make tips at: > https://www.gnu.org http://make.mad-scientist.net > "Please remain calm...I may be mad, but I am a professional." --Mad > Scientist > > > >