[PATCH] Add arithmetic builtin functions

Pete Dietl <[email protected]>
Newsgroups gmane.comp.gnu.make.bugs
Message-ID <CACB-c0HBEPCT8=Y88wq7T2EzjVVDk4yB3vJ0Vf5vouVO9FORJw@mail.gmail.com>
Here I submit to you a patch that adds the following builtin functions
to make: `add` (addition), `sub` (subtraction), `mul`
(multiplication), `div` (division), `mod` (modulus), `max` (maximum),
`min` (minimum), and `abs` (absolute value). The implementation I
provide supports both integers and floats with promotions from integer
to float when appropriate (an operation where at least one argument is
a float causes the other argument to be promoted to a float and the
result to be a float).

I attempted to keep the operations behavior similar to how they behave
in Scheme.
Here are the function details:

Calling `add` with a single argument simply returns that argument;
`add` accepts 1+ arguments.
Calling `sub` with a single argument returns the negation of that
argument; `sub` accepts 1+ arguments.
Calling `mul` with a single argument simply returns that argument;
`mul` accepts 1+ arguments.
Calling `div` with a single argument returns the inverse of that
argument (i.e., `1/arg`); `div` accepts 1+ arguments. Furthermore, all
divisors are checked against being zero.
Calling `mod` requires that exactly two arguments are provided and
that both arguments are integers. Furthermore, the divisor is checked
against being zero.
Calling `max` with a single argument simply returns that argument;
`max` accepts 1+ arguments.
Calling `min` with a single argument simply returns that argument;
`max` accepts 1+ arguments.
Calling `abs` requires exactly one argument.

Please let me know what you all think.

From 76db129404896c15440b405949b8c830e7ebb98f Mon Sep 17 00:00:00 2001
From: Pete Dietl <[email protected]>
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(-)

diff --git a/src/function.c b/src/function.c
index b4c38052..61cb7d5b 100644
--- a/src/function.c
+++ b/src/function.c
@@ -14,6 +14,10 @@ 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, see <https://www.gnu.org/licenses/>.  */

+#include <assert.h>
+#include <math.h>
+#include <stdlib.h>
+
 #include "makeint.h"
 #include "filedef.h"
 #include "variable.h"
@@ -23,7 +27,6 @@ this program.  If not, see
<https://www.gnu.org/licenses/>.  */
 #include "commands.h"
 #include "debug.h"

-
 struct function_table_entry
   {
     union {
@@ -952,6 +955,394 @@ func_let (char *o, char **argv, const char
*funcname UNUSED)
   return o + strlen (o);
 }

+enum number_type
+{
+  t_integer,
+  t_floating
+};
+
+struct number
+{
+  enum number_type type;
+  union
+  {
+    double floating;
+    long long integer;
+  };
+};
+
+enum math_operation_init_type
+{
+  t_constant,
+  t_first_value,
+};
+
+struct math_operation_init
+{
+  enum math_operation_init_type init_type;
+  long long constant;
+};
+
+enum math_operation
+{
+  op_add,
+  op_subtract,
+  op_multiply,
+  op_max,
+  op_min,
+  op_abs,
+};
+
+/* Generic function for double */
+static inline double
+generic_math_op_double (double a, double b, enum math_operation op)
+{
+  switch (op)
+    {
+    case op_add:
+      return a + b;
+    case op_subtract:
+      return a - b;
+    case op_multiply:
+      return a * b;
+    case op_max:
+      return (a > b) ? a : b;
+    case op_min:
+      return (a < b) ? a : b;
+    case op_abs:
+      return fabs (a);
+    default:
+      /* Handle invalid operation */
+      return 0.0;
+    }
+}
+
+/* Generic function for long long int */
+static inline long long int
+generic_math_op_ll (long long int a, long long int b, enum math_operation op)
+{
+  switch (op)
+    {
+    case op_add:
+      return a + b;
+    case op_subtract:
+      return a - b;
+    case op_multiply:
+      return a * b;
+    case op_max:
+      return (a > b) ? a : b;
+    case op_min:
+      return (a < b) ? a : b;
+    case op_abs:
+      return llabs (a);
+    default:
+      /* Handle invalid operation */
+      return 0;
+    }
+}
+
+/* Generic macro to select the correct function based on type and
+   assert that the types of a and b are the same */
+#define generic_math_op(a, b, op)                                             \
+  ((void)sizeof (                                                             \
+       char[__builtin_types_compatible_p (typeof (a), typeof (b)) ? 1 : -1]), \
+   _Generic ((a),                                                             \
+       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++;
+
+  // 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')
+    OSS (fatal, *expanding_var,
+         _ ("Invalid argument to %s function: '%s' not a number"), op_name, s);
+
+  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)
+{
+  char buffer[64];
+  if (n.type == t_integer)
+    snprintf (buffer, sizeof buffer, "%lld", n.integer);
+  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';
+        }
+    }
+  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);
+
+      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.
+  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.
+  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)
+        {
+          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);
+
+  // 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.
+  total = parse_number (argv[0], funcname);
+
+  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);
+
+  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);
+
+  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),
+  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),
-- 
2.43.0
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.