File: ./benchmarks_test.go
   1 package fmscripts
   2 
   3 import (
   4     "math"
   5     "testing"
   6 )
   7 
   8 const (
   9     interferingRipples = "" +
  10         "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
  11         "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
  12 
  13     smilingGhost = "" +
  14         "log1p(((x - 1)**2 + y*y - 4)*((x + 1)**2 + y*y - 4)*" +
  15         "(x*x + (y - sqrt(3))**2 - 4) - 5)"
  16 )
  17 
  18 var benchmarks = []struct {
  19     Name   string
  20     Script string
  21     Native func(float64, float64) float64
  22 }{
  23     {"Load", "x", func(x, y float64) float64 { return x }},
  24     {"Constant", "4.25", func(x, y float64) float64 { return 4.25 }},
  25     {"Constant Addition", "1+2", func(x, y float64) float64 { return 1 + 2 }},
  26     {"0 Additions", "x", func(x, y float64) float64 { return x }},
  27     {"1 Additions", "x+x", func(x, y float64) float64 { return x + x }},
  28     {"2 Additions", "x+x+x", func(x, y float64) float64 { return x + x + x }},
  29     {"0 Multiplications", "x", func(x, y float64) float64 { return x }},
  30     {"1 Multiplications", "x*x", func(x, y float64) float64 { return x * x }},
  31     {"2 Multiplications", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  32     {"0 Divisions", "x", func(x, y float64) float64 { return x }},
  33     {"1 Divisions", "x/x", func(x, y float64) float64 { return x / x }},
  34     {"2 Divisions", "x/x/x", func(x, y float64) float64 { return x / x / x }},
  35     {"e+pi", "e+pi", func(x, y float64) float64 { return math.E + math.Pi }},
  36     {"1-2", "1-2", func(x, y float64) float64 { return 1 - 2 }},
  37     {"e-pi", "e-pi", func(x, y float64) float64 { return math.E - math.Pi }},
  38     {"Add 0", "x+0", func(x, y float64) float64 { return x + 0 }},
  39     {"x*1", "x*1", func(x, y float64) float64 { return x * 1 }},
  40     {"Abs Func", "abs(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  41     {"Abs Syntax", "&(-x)", func(x, y float64) float64 { return math.Abs(-x) }},
  42     {
  43         "Square (pow)",
  44         "pow(x, 2)",
  45         func(x, y float64) float64 { return math.Pow(x, 2) },
  46     },
  47     {"Square", "square(x)", func(x, y float64) float64 { return x * x }},
  48     {"Square Syntax", "*x", func(x, y float64) float64 { return x * x }},
  49     {
  50         "Linear",
  51         "3.4 * x - 0.2",
  52         func(x, y float64) float64 { return 3.4*x - 0.2 },
  53     },
  54     {"Direct Square", "x*x", func(x, y float64) float64 { return x * x }},
  55     {
  56         "Cube (pow)",
  57         "pow(x, 3)",
  58         func(x, y float64) float64 { return math.Pow(x, 3) },
  59     },
  60     {"Cube", "cube(x)", func(x, y float64) float64 { return x * x * x }},
  61     {"Direct Cube", "x*x*x", func(x, y float64) float64 { return x * x * x }},
  62     {
  63         "Reciprocal (pow)",
  64         "pow(x, -1)",
  65         func(x, y float64) float64 { return math.Pow(x, -1) },
  66     },
  67     {
  68         "Reciprocal Func",
  69         "reciprocal(x)",
  70         func(x, y float64) float64 { return 1 / x },
  71     },
  72     {"Direct Reciprocal", "1/x", func(x, y float64) float64 { return 1 / x }},
  73     {"Variable Negation", "-x", func(x, y float64) float64 { return -x }},
  74     {"Constant Multip.", "1*2", func(x, y float64) float64 { return 1 * 2 }},
  75     {"e*pi", "e*pi", func(x, y float64) float64 { return math.E * math.Pi }},
  76     {
  77         "Variable y = mx + k",
  78         "2.1*x + 3.4",
  79         func(x, y float64) float64 { return 2.1*x + 3.4 },
  80     },
  81     {"sin(x)", "sin(x)", func(x, y float64) float64 { return math.Sin(x) }},
  82     {"cos(x)", "cos(x)", func(x, y float64) float64 { return math.Cos(x) }},
  83     {"exp(x)", "exp(x)", func(x, y float64) float64 { return math.Exp(x) }},
  84     {"expm1(x)", "expm1(x)", func(x, y float64) float64 { return math.Expm1(x) }},
  85     {"ln(x)", "ln(x)", func(x, y float64) float64 { return math.Log(x) }},
  86     {"log2(x)", "log2(x)", func(x, y float64) float64 { return math.Log2(x) }},
  87     {"log10(x)", "log10(x)", func(x, y float64) float64 { return math.Log10(x) }},
  88     {"1/2", "1/2", func(x, y float64) float64 { return 1.0 / 2.0 }},
  89     {"e/pi", "e/pi", func(x, y float64) float64 { return math.E / math.Pi }},
  90     {"exp(2)", "exp(2)", func(x, y float64) float64 { return math.Exp(2) }},
  91     {
  92         "Club Beat Pulse",
  93         "sin(10*tau * exp(-20*x)) * exp(-2*x)",
  94         func(x, y float64) float64 {
  95             return math.Sin(10*2*math.Pi*math.Exp(-20*x)) * math.Exp(-2*x)
  96         },
  97     },
  98     {
  99         "Interfering Ripples",
 100         interferingRipples,
 101         func(x, y float64) float64 {
 102             return math.Exp(-0.5*math.Sin(2*math.Hypot(x-2, y+1))) +
 103                 math.Exp(-0.5*math.Sin(10*math.Hypot(x+2, y-3.4)))
 104         },
 105     },
 106     {
 107         "Floor Lights",
 108         "abs(sin(x)) / y**1.4",
 109         func(x, y float64) float64 {
 110             return math.Abs(math.Sin(x)) / math.Pow(y, 1.4)
 111         },
 112     },
 113     {
 114         "Domain Hole",
 115         "log1p(sin(x + y) + (x - y)**2 - 1.5*x + 2.5*y + 1)",
 116         func(x, y float64) float64 {
 117             v := x - y
 118             return math.Log1p(math.Sin(x+y) + v*v - 1.5*x + 2.5*y + 1)
 119         },
 120     },
 121     {
 122         "Beta Gradient",
 123         "lbeta(x + 5.1, y + 5.1)",
 124         func(x, y float64) float64 { return lnBeta(x+5.1, y+5.1) },
 125     },
 126     {
 127         "Hot Bars",
 128         "abs(x) + sqrt(abs(sin(2*y)))",
 129         func(x, y float64) float64 {
 130             return math.Abs(x) + math.Sqrt(math.Abs(math.Sin(2*y)))
 131         },
 132     },
 133     {
 134         "Grid Pattern",
 135         "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(y**2))",
 136         func(x, y float64) float64 {
 137             return math.Sin(math.Sin(x)+math.Cos(y)) +
 138                 math.Cos(math.Sin(x*y)+math.Cos(y*y))
 139         },
 140     },
 141     {
 142         "Smiling Ghost",
 143         smilingGhost,
 144         func(x, y float64) float64 {
 145             xm1 := x - 1
 146             xp1 := x + 1
 147             v := y - math.Sqrt(3)
 148             w := (xm1*xm1 + y*y - 4) * (xp1*xp1 + y*y - 4) * (x*x + v*v - 4)
 149             return math.Log1p(w - 5)
 150         },
 151     },
 152     {
 153         "Forgot its Name",
 154         "1 / (1 + x.abs/y*y)",
 155         func(x, y float64) float64 {
 156             return 1 / (1 + math.Abs(x)/y*y)
 157         },
 158     },
 159 }
 160 
 161 func BenchmarkEmpty(b *testing.B) {
 162     b.Run("empty program", func(b *testing.B) {
 163         var p Program
 164         b.ReportAllocs()
 165         b.ResetTimer()
 166 
 167         for i := 0; i < b.N; i++ {
 168             _ = p.Run()
 169         }
 170     })
 171 
 172     f := func(float64, float64) float64 {
 173         return math.NaN()
 174     }
 175 
 176     b.Run("empty function", func(b *testing.B) {
 177         b.ReportAllocs()
 178         b.ResetTimer()
 179 
 180         for i := 0; i < b.N; i++ {
 181             _ = f(0, 0)
 182         }
 183     })
 184 }
 185 
 186 func BenchmarkBasicProgram(b *testing.B) {
 187     for _, tc := range benchmarks {
 188         var c Compiler
 189         defs := map[string]any{
 190             "x": 0.05,
 191             "y": 0,
 192             "z": 0,
 193         }
 194 
 195         b.Run(tc.Name, func(b *testing.B) {
 196             p, err := c.Compile(tc.Script, defs)
 197             if err != nil {
 198                 const fs = "while compiling %q, got error %q"
 199                 b.Fatalf(fs, tc.Script, err.Error())
 200                 return
 201             }
 202 
 203             x, _ := p.Get("x")
 204             _, _ = p.Get("y")
 205             _, _ = p.Get("z")
 206 
 207             b.ResetTimer()
 208             for i := 0; i < b.N; i++ {
 209                 _ = p.Run()
 210                 *x++
 211             }
 212         })
 213     }
 214 }
 215 
 216 func BenchmarkBasicFunc(b *testing.B) {
 217     for _, tc := range benchmarks {
 218         b.Run(tc.Name, func(b *testing.B) {
 219             x := 0.05
 220             y := 0.0
 221             fn := tc.Native
 222             b.ResetTimer()
 223 
 224             for i := 0; i < b.N; i++ {
 225                 fn(x, y)
 226                 x++
 227                 y++
 228             }
 229         })
 230     }
 231 }
 232 
 233 func BenchmarkSoundProgram(b *testing.B) {
 234     var soundBenchmarkTests = []struct {
 235         Name   string
 236         Script string
 237     }{
 238         {
 239             "Sine Wave",
 240             "sin(1000 * tau * x)",
 241         },
 242         {
 243             "Laser Pulse",
 244             "sin(100 * tau * exp(-40 * u))",
 245         },
 246         {
 247             "Club Beat Pulse",
 248             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 249         },
 250     }
 251 
 252     for _, tc := range soundBenchmarkTests {
 253         var c Compiler
 254         defs := map[string]any{
 255             "t": 0.05,
 256             "u": 0,
 257         }
 258 
 259         const seconds = 2
 260         const sampleRate = 48_000
 261         dt := 1.0 / float64(sampleRate)
 262         // buf is the destination buffer for all calculated samples
 263         buf := make([]float64, 0, seconds*sampleRate)
 264 
 265         b.Run(tc.Name, func(b *testing.B) {
 266             p, err := c.Compile(tc.Script, defs)
 267             if err != nil {
 268                 const fs = "while compiling %q, got error %q"
 269                 b.Fatalf(fs, tc.Script, err.Error())
 270                 return
 271             }
 272 
 273             // input parameters for the program
 274             t, _ := p.Get("t")
 275             u, _ := p.Get("u")
 276 
 277             b.ResetTimer()
 278             for i := 0; i < b.N; i++ {
 279                 // avoid buffer expansions after the first run
 280                 buf = buf[:0]
 281 
 282                 // benchmark 1 second of generated sound
 283                 for j := 0; j < seconds*sampleRate; j++ {
 284                     // calculate time in seconds from current sample index
 285                     v := float64(j) * dt
 286                     *t = v
 287                     _, *u = math.Modf(v)
 288 
 289                     // calculate a mono sample
 290                     buf = append(buf, p.Run())
 291                 }
 292             }
 293         })
 294     }
 295 }
 296 
 297 func BenchmarkNativeSoundProgram(b *testing.B) {
 298     const tau = 2 * math.Pi
 299 
 300     var soundBenchmarkTests = []struct {
 301         Name string
 302         Fun  func(float64, float64) float64
 303     }{
 304         {
 305             "Sine Wave",
 306             func(t, u float64) float64 {
 307                 return math.Sin(1000 * tau * t)
 308             },
 309         },
 310         {
 311             "Laser Pulse",
 312             func(t, u float64) float64 {
 313                 return math.Sin(100 * tau * math.Exp(-40*u))
 314             },
 315         },
 316         {
 317             "Club Beat Pulse",
 318             func(t, u float64) float64 {
 319                 return math.Sin(10*tau*math.Exp(-20*t)) * math.Exp(-2*t)
 320             },
 321         },
 322     }
 323 
 324     for _, tc := range soundBenchmarkTests {
 325         const seconds = 2
 326         const sampleRate = 48_000
 327         dt := 1.0 / float64(sampleRate)
 328         // buf is the destination buffer for all calculated samples
 329         buf := make([]float64, 0, seconds*sampleRate)
 330 
 331         b.Run(tc.Name, func(b *testing.B) {
 332             // input parameters for the program
 333             t := 0.05
 334             u := 0.00
 335             fn := tc.Fun
 336             b.ResetTimer()
 337 
 338             for i := 0; i < b.N; i++ {
 339                 // avoid buffer expansions after the first run
 340                 buf = buf[:0]
 341 
 342                 // benchmark 1 second of generated sound
 343                 for j := 0; j < seconds*sampleRate; j++ {
 344                     // calculate time in seconds from current sample index
 345                     v := float64(j) * dt
 346                     t = v
 347                     _, u = math.Modf(v)
 348 
 349                     // calculate a mono sample
 350                     buf = append(buf, fn(t, u))
 351                 }
 352             }
 353         })
 354     }
 355 }
 356 
 357 func BenchmarkImageProgram(b *testing.B) {
 358     const (
 359         // use part of a full HD pic to give more runs for each test, giving
 360         // greater statistical-stability/comparability across benchmark runs
 361         w = 1920 / 4
 362         h = 1080 / 4
 363 
 364         intRipples = "" +
 365             "exp(-0.5 * sin(2 * hypot(x - 2, y + 1))) + " +
 366             "exp(-0.5 * sin(10 * hypot(x + 2, y - 3.4)))"
 367         domainHole = "" +
 368             "log1p(sin(x + y) + pow(x - y, 2) - 1.5*x + 2.5*y + 1)"
 369         gridPatPow = "" +
 370             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(pow(y, 2)))"
 371         gridPatSquare = "" +
 372             "sin(sin(x)+cos(y)) + cos(sin(x*y)+cos(square(y)))"
 373         smilingGhostFunc = "" +
 374             "log1p((pow(x - 1, 2) + y*y - 4)*" +
 375             "(pow(x + 1, 2) + y*y - 4)*" +
 376             "(x*x + pow(y - sqrt(3), 2) - 4) - 5)"
 377         smilingGhostSyntax = "" +
 378             "log1p((*(x - 1) + *y - 4)*" +
 379             "(*(x + 1) + *y - 4)*" +
 380             "(*x + *(y - sqrt(3)) - 4) - 5)"
 381     )
 382 
 383     var imageBenchmarkTests = []struct {
 384         Name   string
 385         Width  int
 386         Height int
 387         Script string
 388     }{
 389         {"Horizontal Linear", w, h, "x"},
 390         {"Multiplication", w, h, "x*y"},
 391         {"Star", w, h, "exp(-0.5 * hypot(x, y))"},
 392         {"Interfering Ripples", w, h, intRipples},
 393         {"Floor Lights", w, h, "abs(sin(x)) / pow(y, 1.4)"},
 394         {"Domain Hole", w, h, domainHole},
 395         {"Beta Gradient", w, h, "lbeta(x + 5.1, y + 5.1)"},
 396         {"Hot Bars", w, h, "abs(x) + sqrt(abs(sin(2*y)))"},
 397         {"Grid Pattern (pow)", w, h, gridPatPow},
 398         {"Grid Pattern (square)", w, h, gridPatSquare},
 399         {"Smiling Ghost (pow)", w, h, smilingGhostFunc},
 400         {"Smiling Ghost (square syntax)", w, h, smilingGhostSyntax},
 401     }
 402 
 403     for _, tc := range imageBenchmarkTests {
 404         var c Compiler
 405         defs := map[string]any{
 406             "x": 0,
 407             "y": 0,
 408         }
 409 
 410         // 4k-resolution results buffer
 411         buf := make([]float64, 1920*1080*4)
 412 
 413         b.Run(tc.Name, func(b *testing.B) {
 414             p, err := c.Compile(tc.Script, defs)
 415             if err != nil {
 416                 const fs = "while compiling %q, got error %q"
 417                 b.Fatalf(fs, tc.Script, err.Error())
 418                 return
 419             }
 420 
 421             // input parameters for the program
 422             x, _ := p.Get("x")
 423             y, _ := p.Get("y")
 424 
 425             w := tc.Width
 426             h := tc.Height
 427             dx := 1.0 / float64(w)
 428             dy := 1.0 / float64(h)
 429             xmin := -5.2
 430             ymax := 23.91
 431             b.ResetTimer()
 432             for i := 0; i < b.N; i++ {
 433                 // avoid buffer expansions after the first run
 434                 buf = buf[:0]
 435 
 436                 for j := 0; j < h; j++ {
 437                     *y = ymax - float64(j)*dy
 438                     for i := 0; i < w; i++ {
 439                         *x = float64(i)*dx + xmin
 440                         buf = append(buf, p.Run())
 441                     }
 442                 }
 443             }
 444         })
 445     }
 446 }
 447 
 448 func BenchmarkCompiler(b *testing.B) {
 449     f := func(name string, src string) {
 450         b.Run(name, func(b *testing.B) {
 451             for i := 0; i < b.N; i++ {
 452                 var c Compiler
 453                 _, err := c.Compile(src, map[string]any{
 454                     "x": 0.05,
 455                     "y": 0,
 456                     "z": 0,
 457                 })
 458 
 459                 if err != nil {
 460                     const fs = "while compiling %q, got error %q"
 461                     b.Fatalf(fs, src, err.Error())
 462                 }
 463             }
 464         })
 465     }
 466 
 467     for _, tc := range mathCorrectnessTests {
 468         f(tc.Name, tc.Script)
 469     }
 470 
 471     for _, tc := range benchmarks {
 472         f(tc.Name, tc.Script)
 473     }
 474 }

     File: ./compilers.go
   1 package fmscripts
   2 
   3 import (
   4     "fmt"
   5     "math"
   6     "strconv"
   7 )
   8 
   9 // unary2op turns unary operators into their corresponding basic operations;
  10 // some entries are only for the optimizer, and aren't accessible directly
  11 // from valid source code
  12 var unary2op = map[string]opcode{
  13     "-": neg,
  14     "!": not,
  15     "&": abs,
  16     "*": square,
  17     "^": square,
  18     "/": rec,
  19     "%": mod1,
  20 }
  21 
  22 // binary2op turns binary operators into their corresponding basic operations
  23 var binary2op = map[string]opcode{
  24     "+":  add,
  25     "-":  sub,
  26     "*":  mul,
  27     "/":  div,
  28     "%":  mod,
  29     "&&": and,
  30     "&":  and,
  31     "||": or,
  32     "|":  or,
  33     "==": equal,
  34     "!=": notequal,
  35     "<>": notequal,
  36     "<":  less,
  37     "<=": lessoreq,
  38     ">":  more,
  39     ">=": moreoreq,
  40     "**": pow,
  41     "^":  pow,
  42 }
  43 
  44 // Compiler lets you create Program objects, which you can then run. The whole
  45 // point of this is to create quicker-to-run numeric scripts. You can even add
  46 // variables with initial values, as well as functions for the script to use.
  47 //
  48 // Common math funcs and constants are automatically detected, and constant
  49 // results are optimized away, unless builtins are redefined. In other words,
  50 // the optimizer is effectively disabled for all (sub)expressions containing
  51 // redefined built-ins, as there's no way to be sure those values won't change
  52 // from one run to the next.
  53 //
  54 // See the comment for type Program for more details.
  55 //
  56 // # Example
  57 //
  58 // var c fmscripts.Compiler
  59 //
  60 //  defs := map[string]any{
  61 //      "x": 0,    // define `x`, and initialize it to 0
  62 //      "k": 4.25, // define `k`, and initialize it to 4.25
  63 //      "b": true, // define `b`, and initialize it to 1.0
  64 //      "n": -23,  // define `n`, and initialize it to -23.0
  65 //      "pi": 3,   // define `pi`, overriding the default constant named `pi`
  66 //
  67 //      "f": numericKernel // type is func ([]float64) float64
  68 //      "g": otherFunc     // type is func (float64) float64
  69 //  }
  70 //
  71 // prog, err := c.Compile("log10(k) + f(sqrt(k) * exp(-x), 45, -0.23)", defs)
  72 // // ...
  73 //
  74 // x, _ := prog.Get("x") // Get returns (*float64, bool)
  75 // y, _ := prog.Get("y") // a useless pointer, since program doesn't use `y`
  76 // // ...
  77 //
  78 //  for i := 0; i < n; i++ {
  79 //      *x = float64(i)*dx + minx // you update inputs in place using pointers
  80 //      f := prog.Run()           // method Run gives you a float64 back
  81 //      // ...
  82 //  }
  83 type Compiler struct {
  84     maxStack int     // the exact stack size the resulting program needs
  85     ops      []numOp // the program's operations
  86 
  87     vaddr map[string]int // address lookup for values
  88     faddr map[string]int // address lookup for functions
  89     ftype map[string]any // keeps track of func types during compilation
  90 
  91     values []float64 // variables and constants available to programs
  92     funcs  []any     // funcs available to programs
  93 }
  94 
  95 // Compile parses the script given and generates a fast float64-only Program
  96 // made only of sequential steps: any custom funcs you provide it can use
  97 // their own internal looping and/or conditional logic, of course.
  98 func (c *Compiler) Compile(src string, defs map[string]any) (Program, error) {
  99     // turn source code into an abstract syntax-tree
 100     root, err := parse(src)
 101     if err != nil {
 102         return Program{}, err
 103     }
 104 
 105     // generate operations
 106     if err := c.reset(defs); err != nil {
 107         return Program{}, err
 108     }
 109     if err = c.compile(root, 0); err != nil {
 110         return Program{}, err
 111     }
 112 
 113     // create the resulting program
 114     var p Program
 115     p.stack = make([]float64, c.maxStack)
 116     p.values = make([]float64, len(c.values))
 117     copy(p.values, c.values)
 118     p.ops = make([]numOp, len(c.ops))
 119     copy(p.ops, c.ops)
 120     p.funcs = make([]any, len(c.ftype))
 121     copy(p.funcs, c.funcs)
 122     p.names = make(map[string]int, len(c.vaddr))
 123 
 124     // give the program's Get method access only to all allocated variables
 125     for k, v := range c.vaddr {
 126         // avoid exposing numeric constants on the program's name-lookup
 127         // table, which would allow users to change literals across runs
 128         if _, err := strconv.ParseFloat(k, 64); err == nil {
 129             continue
 130         }
 131         // expose only actual variable names
 132         p.names[k] = v
 133     }
 134 
 135     return p, nil
 136 }
 137 
 138 // reset prepares a compiler by satisfying internal preconditions func
 139 // compileExpr relies on, when given an abstract syntax-tree
 140 func (c *Compiler) reset(defs map[string]any) error {
 141     // reset the compiler's internal state
 142     c.maxStack = 0
 143     c.ops = c.ops[:0]
 144     c.vaddr = make(map[string]int)
 145     c.faddr = make(map[string]int)
 146     c.ftype = make(map[string]any)
 147     c.values = c.values[:0]
 148     c.funcs = c.funcs[:0]
 149 
 150     // allocate vars and funcs
 151     for k, v := range defs {
 152         if err := c.allocEntry(k, v); err != nil {
 153             return err
 154         }
 155     }
 156     return nil
 157 }
 158 
 159 // allocEntry simplifies the control-flow of func reset
 160 func (c *Compiler) allocEntry(k string, v any) error {
 161     const (
 162         maxExactRound = 2 << 52
 163         rangeErrFmt   = "%d is outside range of exact float64 integers"
 164         typeErrFmt    = "got value of unsupported type %T"
 165     )
 166 
 167     switch v := v.(type) {
 168     case float64:
 169         _, err := c.allocValue(k, v)
 170         return err
 171 
 172     case int:
 173         if math.Abs(float64(v)) > maxExactRound {
 174             return fmt.Errorf(rangeErrFmt, v)
 175         }
 176         _, err := c.allocValue(k, float64(v))
 177         return err
 178 
 179     case bool:
 180         _, err := c.allocValue(k, debool(v))
 181         return err
 182 
 183     default:
 184         if isSupportedFunc(v) {
 185             c.ftype[k] = v
 186             _, err := c.allocFunc(k, v)
 187             return err
 188         }
 189         return fmt.Errorf(typeErrFmt, v)
 190     }
 191 }
 192 
 193 // allocValue ensures there's a place to match the name given, returning its
 194 // index when successful; only programs using unreasonably many values will
 195 // cause this func to fail
 196 func (c *Compiler) allocValue(s string, f float64) (int, error) {
 197     if len(c.values) >= maxOpIndex {
 198         const fs = "programs can only use up to %d distinct float64 values"
 199         return -1, fmt.Errorf(fs, maxOpIndex+1)
 200     }
 201 
 202     i := len(c.values)
 203     c.vaddr[s] = i
 204     c.values = append(c.values, f)
 205     return i, nil
 206 }
 207 
 208 // valueIndex returns the index reserved for an allocated value/variable:
 209 // all unallocated values/variables are allocated here on first access
 210 func (c *Compiler) valueIndex(s string, f float64) (int, error) {
 211     // name is found: return the index of the already-allocated var
 212     if i, ok := c.vaddr[s]; ok {
 213         return i, nil
 214     }
 215 
 216     // name not found, but it's a known math constant
 217     if f, ok := mathConst[s]; ok {
 218         return c.allocValue(s, f)
 219     }
 220     // name not found, and it's not of a known math constant
 221     return c.allocValue(s, f)
 222 }
 223 
 224 // constIndex allocates a constant as a variable named as its own string
 225 // representation, which avoids multiple entries for repeated uses of the
 226 // same constant value
 227 func (c *Compiler) constIndex(f float64) (int, error) {
 228     // constants have no name, so use a canonical string representation
 229     return c.valueIndex(strconv.FormatFloat(f, 'f', 16, 64), f)
 230 }
 231 
 232 // funcIndex returns the index reserved for an allocated function: all
 233 // unallocated functions are allocated here on first access
 234 func (c *Compiler) funcIndex(name string) (int, error) {
 235     // check if func was already allocated
 236     if i, ok := c.faddr[name]; ok {
 237         return i, nil
 238     }
 239 
 240     // if name is reserved allocate an index for its matching func
 241     if fn, ok := c.ftype[name]; ok {
 242         return c.allocFunc(name, fn)
 243     }
 244 
 245     // if name wasn't reserved, see if it's a standard math func name
 246     if fn := c.autoFuncLookup(name); fn != nil {
 247         return c.allocFunc(name, fn)
 248     }
 249 
 250     // name isn't even a standard math func's
 251     return -1, fmt.Errorf("function not found")
 252 }
 253 
 254 // allocFunc ensures there's a place for the function name given, returning its
 255 // index when successful; only funcs of unsupported types will cause failure
 256 func (c *Compiler) allocFunc(name string, fn any) (int, error) {
 257     if isSupportedFunc(fn) {
 258         i := len(c.funcs)
 259         c.faddr[name] = i
 260         c.ftype[name] = fn
 261         c.funcs = append(c.funcs, fn)
 262         return i, nil
 263     }
 264     return -1, fmt.Errorf("can't use a %T as a number-crunching function", fn)
 265 }
 266 
 267 // autoFuncLookup checks built-in deterministic funcs for the name given: its
 268 // result is nil only if there's no match
 269 func (c *Compiler) autoFuncLookup(name string) any {
 270     if fn, ok := determFuncs[name]; ok {
 271         return fn
 272     }
 273     return nil
 274 }
 275 
 276 // genOp generates/adds a basic operation to the program, while keeping track
 277 // of the maximum depth the stack can reach
 278 func (c *Compiler) genOp(op numOp, depth int) {
 279     // add 2 defensively to ensure stack space for the inputs of binary ops
 280     n := depth + 2
 281     if c.maxStack < n {
 282         c.maxStack = n
 283     }
 284     c.ops = append(c.ops, op)
 285 }
 286 
 287 // compile is a recursive expression evaluator which does the actual compiling
 288 // as it goes along
 289 func (c *Compiler) compile(expr any, depth int) error {
 290     expr = c.optimize(expr)
 291 
 292     switch expr := expr.(type) {
 293     case float64:
 294         return c.compileLiteral(expr, depth)
 295     case string:
 296         return c.compileVariable(expr, depth)
 297     case []any:
 298         return c.compileCombo(expr, depth)
 299     case unaryExpr:
 300         return c.compileUnary(expr.op, expr.x, depth)
 301     case binaryExpr:
 302         return c.compileBinary(expr.op, expr.x, expr.y, depth)
 303     case callExpr:
 304         return c.compileCall(expr, depth)
 305     case assignExpr:
 306         return c.compileAssign(expr, depth)
 307     default:
 308         return fmt.Errorf("unsupported expression type %T", expr)
 309     }
 310 }
 311 
 312 // compileLiteral generates a load operation for the constant value given
 313 func (c *Compiler) compileLiteral(f float64, depth int) error {
 314     i, err := c.constIndex(f)
 315     if err != nil {
 316         return err
 317     }
 318     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 319     return nil
 320 }
 321 
 322 // compileVariable generates a load operation for the variable name given
 323 func (c *Compiler) compileVariable(name string, depth int) error {
 324     // handle names which aren't defined, but are known math constants
 325     if _, ok := c.vaddr[name]; !ok {
 326         if f, ok := mathConst[name]; ok {
 327             return c.compileLiteral(f, depth)
 328         }
 329     }
 330 
 331     // handle actual variables
 332     i, err := c.valueIndex(name, 0)
 333     if err != nil {
 334         return err
 335     }
 336     c.genOp(numOp{What: load, Index: opindex(i)}, depth)
 337     return nil
 338 }
 339 
 340 // compileCombo handles a sequence of expressions
 341 func (c *Compiler) compileCombo(exprs []any, depth int) error {
 342     for _, v := range exprs {
 343         err := c.compile(v, depth)
 344         if err != nil {
 345             return err
 346         }
 347     }
 348     return nil
 349 }
 350 
 351 // compileUnary handles unary expressions
 352 func (c *Compiler) compileUnary(op string, x any, depth int) error {
 353     err := c.compile(x, depth+1)
 354     if err != nil {
 355         return err
 356     }
 357 
 358     if op, ok := unary2op[op]; ok {
 359         c.genOp(numOp{What: op}, depth)
 360         return nil
 361     }
 362     return fmt.Errorf("unary operation %q is unsupported", op)
 363 }
 364 
 365 // compileBinary handles binary expressions
 366 func (c *Compiler) compileBinary(op string, x, y any, depth int) error {
 367     switch op {
 368     case "===", "!==":
 369         // handle binary expressions with no matching basic operation, by
 370         // treating them as aliases for function calls
 371         return c.compileCall(callExpr{name: op, args: []any{x, y}}, depth)
 372     }
 373 
 374     err := c.compile(x, depth+1)
 375     if err != nil {
 376         return err
 377     }
 378     err = c.compile(y, depth+2)
 379     if err != nil {
 380         return err
 381     }
 382 
 383     if op, ok := binary2op[op]; ok {
 384         c.genOp(numOp{What: op}, depth)
 385         return nil
 386     }
 387     return fmt.Errorf("binary operation %q is unsupported", op)
 388 }
 389 
 390 // compileCall handles function-call expressions
 391 func (c *Compiler) compileCall(expr callExpr, depth int) error {
 392     // lookup function name
 393     index, err := c.funcIndex(expr.name)
 394     if err != nil {
 395         return fmt.Errorf("%s%s", expr.name, err.Error())
 396     }
 397     // get the func value, as its type determines the calling op to emit
 398     v, ok := c.ftype[expr.name]
 399     if !ok {
 400         return fmt.Errorf("%s: function is undefined", expr.name)
 401     }
 402 
 403     // figure which type of function operation to use
 404     op, ok := func2op(v)
 405     if !ok {
 406         return fmt.Errorf("%s: unsupported function type %T", expr.name, v)
 407     }
 408 
 409     // ensure number of args given to the func makes sense for the func type
 410     err = checkArgCount(func2info[op], expr.name, len(expr.args))
 411     if err != nil {
 412         return err
 413     }
 414 
 415     // generate operations to evaluate all args
 416     for i, v := range expr.args {
 417         err := c.compile(v, depth+i+1)
 418         if err != nil {
 419             return err
 420         }
 421     }
 422 
 423     // generate func-call operation
 424     given := len(expr.args)
 425     next := numOp{What: op, NumArgs: opargs(given), Index: opindex(index)}
 426     c.genOp(next, depth)
 427     return nil
 428 }
 429 
 430 // checkArgCount does what it says, returning informative errors when arg
 431 // counts are wrong
 432 func checkArgCount(info funcTypeInfo, name string, nargs int) error {
 433     if info.AtLeast < 0 && info.AtMost < 0 {
 434         return nil
 435     }
 436 
 437     if info.AtLeast == info.AtMost && nargs != info.AtMost {
 438         const fs = "%s: expected %d args, got %d instead"
 439         return fmt.Errorf(fs, name, info.AtMost, nargs)
 440     }
 441 
 442     if info.AtLeast >= 0 && info.AtMost >= 0 {
 443         const fs = "%s: expected between %d and %d args, got %d instead"
 444         if nargs < info.AtLeast || nargs > info.AtMost {
 445             return fmt.Errorf(fs, name, info.AtLeast, info.AtMost, nargs)
 446         }
 447     }
 448 
 449     if info.AtLeast >= 0 && nargs < info.AtLeast {
 450         const fs = "%s: expected at least %d args, got %d instead"
 451         return fmt.Errorf(fs, name, info.AtLeast, nargs)
 452     }
 453 
 454     if info.AtMost >= 0 && nargs > info.AtMost {
 455         const fs = "%s: expected at most %d args, got %d instead"
 456         return fmt.Errorf(fs, name, info.AtMost, nargs)
 457     }
 458 
 459     // all is good
 460     return nil
 461 }
 462 
 463 // compileAssign generates a store operation for the expression given
 464 func (c *Compiler) compileAssign(expr assignExpr, depth int) error {
 465     err := c.compile(expr.expr, depth)
 466     if err != nil {
 467         return err
 468     }
 469 
 470     i, err := c.allocValue(expr.name, 0)
 471     if err != nil {
 472         return err
 473     }
 474 
 475     c.genOp(numOp{What: store, Index: opindex(i)}, depth)
 476     return nil
 477 }
 478 
 479 // func sameFunc(x, y any) bool {
 480 //  if x == nil || y == nil {
 481 //      return false
 482 //  }
 483 //  return reflect.ValueOf(x).Pointer() == reflect.ValueOf(y).Pointer()
 484 // }
 485 
 486 // isSupportedFunc checks if a value's type is a supported func type
 487 func isSupportedFunc(fn any) bool {
 488     _, ok := func2op(fn)
 489     return ok
 490 }
 491 
 492 // funcTypeInfo is an entry in the func2info lookup table
 493 type funcTypeInfo struct {
 494     // AtLeast is the minimum number of inputs the func requires: negative
 495     // values are meant to be ignored
 496     AtLeast int
 497 
 498     // AtMost is the maximum number of inputs the func requires: negative
 499     // values are meant to be ignored
 500     AtMost int
 501 }
 502 
 503 // func2info is a lookup table to check the number of inputs funcs are given
 504 var func2info = map[opcode]funcTypeInfo{
 505     call0:  {AtLeast: +0, AtMost: +0},
 506     call1:  {AtLeast: +1, AtMost: +1},
 507     call2:  {AtLeast: +2, AtMost: +2},
 508     call3:  {AtLeast: +3, AtMost: +3},
 509     call4:  {AtLeast: +4, AtMost: +4},
 510     call5:  {AtLeast: +5, AtMost: +5},
 511     callv:  {AtLeast: -1, AtMost: -1},
 512     call1v: {AtLeast: +1, AtMost: -1},
 513 }
 514 
 515 // func2op tries to match a func type into a corresponding opcode
 516 func func2op(v any) (op opcode, ok bool) {
 517     switch v.(type) {
 518     case func() float64:
 519         return call0, true
 520     case func(float64) float64:
 521         return call1, true
 522     case func(float64, float64) float64:
 523         return call2, true
 524     case func(float64, float64, float64) float64:
 525         return call3, true
 526     case func(float64, float64, float64, float64) float64:
 527         return call4, true
 528     case func(float64, float64, float64, float64, float64) float64:
 529         return call5, true
 530     case func(...float64) float64:
 531         return callv, true
 532     case func(float64, ...float64) float64:
 533         return call1v, true
 534     default:
 535         return 0, false
 536     }
 537 }

     File: ./compilers_test.go
   1 package fmscripts
   2 
   3 import (
   4     "testing"
   5 )
   6 
   7 func TestBuiltinFuncs(t *testing.T) {
   8     list := []map[string]any{
   9         determFuncs,
  10     }
  11 
  12     for _, kv := range list {
  13         for k, v := range kv {
  14             t.Run(k, func(t *testing.T) {
  15                 if !isSupportedFunc(v) {
  16                     t.Fatalf("%s: invalid function type %T", k, v)
  17                 }
  18             })
  19         }
  20     }
  21 }
  22 
  23 var mathCorrectnessTests = []struct {
  24     Name     string
  25     Script   string
  26     Expected float64
  27     Error    string
  28 }{
  29     {
  30         Name:     "value",
  31         Script:   "-45.2",
  32         Expected: -45.2,
  33         Error:    "",
  34     },
  35     {
  36         Name:     "simple",
  37         Script:   "1 + 3",
  38         Expected: 4,
  39         Error:    "",
  40     },
  41     {
  42         Name:     "fancier",
  43         Script:   "1*8 + 3*2**3",
  44         Expected: 32,
  45         Error:    "",
  46     },
  47     {
  48         Name:     "function call",
  49         Script:   "log2(1024)",
  50         Expected: 10,
  51         Error:    "",
  52     },
  53     {
  54         Name:     "function call (2 args)",
  55         Script:   "pow(3, 3)",
  56         Expected: 27,
  57         Error:    "",
  58     },
  59     {
  60         Name:     "function call (3 args)",
  61         Script:   "fma(3.5, 56, -1.52)",
  62         Expected: 194.48,
  63         Error:    "",
  64     },
  65     {
  66         Name:     "vararg-function call",
  67         Script:   "min(log10(10_000), log10(1_000_000), log2(4_096), 100 - 130)",
  68         Expected: -30,
  69         Error:    "",
  70     },
  71     {
  72         Name:     "square shortcut",
  73         Script:   "*-3",
  74         Expected: 9,
  75         Error:    "",
  76     },
  77     {
  78         Name:     "abs shortcut",
  79         Script:   "&-3",
  80         Expected: 3,
  81         Error:    "",
  82     },
  83     {
  84         Name:     "negative constant",
  85         Script:   "(-3)",
  86         Expected: -3,
  87         Error:    "",
  88     },
  89     {
  90         Name:     "power syntax",
  91         Script:   "2**4",
  92         Expected: 16,
  93         Error:    "",
  94     },
  95     {
  96         Name:     "power syntax order",
  97         Script:   "3*2**4",
  98         Expected: 48,
  99         Error:    "",
 100     },
 101     {
 102         Script:   "2*3**4",
 103         Expected: 162,
 104         Error:    "",
 105     },
 106     {
 107         Script:   "2**3*4",
 108         Expected: 32,
 109         Error:    "",
 110     },
 111     {
 112         Script:   "(2*3)**4",
 113         Expected: 1_296,
 114         Error:    "",
 115     },
 116     {
 117         Script:   "*2*2",
 118         Expected: 8,
 119         Error:    "",
 120     },
 121     {
 122         Script:   "2*2*2",
 123         Expected: 8,
 124         Error:    "",
 125     },
 126     {
 127         Script:   "3 == 3 ? 10 : -1",
 128         Expected: 10,
 129         Error:    "",
 130     },
 131     {
 132         Script:   "3 == 4 ? 10 : -1",
 133         Expected: -1,
 134         Error:    "",
 135     },
 136     {
 137         Script:   "log10(-1) ?? 4",
 138         Expected: 4,
 139         Error:    "",
 140     },
 141     {
 142         Script:   "log10(10) ?? 4",
 143         Expected: 1,
 144         Error:    "",
 145     },
 146     {
 147         Script:   "abc = 123; abc",
 148         Expected: 123,
 149         Error:    "",
 150     },
 151     {
 152         Name:     "calling func(float64, ...float64) float64",
 153         Script:   "horner(2.5, 1, 2, 3)",
 154         Expected: 14.25,
 155         Error:    "",
 156     },
 157 }
 158 
 159 func TestCompiler(t *testing.T) {
 160     for _, tc := range mathCorrectnessTests {
 161         name := tc.Name
 162         if len(name) == 0 {
 163             name = tc.Script
 164         }
 165 
 166         t.Run(name, func(t *testing.T) {
 167             var c Compiler
 168             p, err := c.Compile(tc.Script, map[string]any{"x": 1})
 169             if err != nil {
 170                 t.Fatalf("got compiler error %q", err.Error())
 171             }
 172 
 173             f := p.Run()
 174             if (err != nil || tc.Error != "") && err.Error() != tc.Error {
 175                 const fs = "expected error %q, got error %q instead"
 176                 t.Fatalf(fs, err.Error(), tc.Error)
 177             }
 178 
 179             if f != tc.Expected {
 180                 const fs = "expected result to be %f, got %f instead"
 181                 t.Fatalf(fs, tc.Expected, f)
 182             }
 183         })
 184     }
 185 }

     File: ./doc.go
   1 /*
   2 # Floating-point Math Scripts
   3 
   4 This self-contained package gives you a compiler/interpreter combo to run
   5 number-crunching scripts. These are limited to float64 values, but they run
   6 surprisingly quickly: various simple benchmarks suggest it's between 1/2 and
   7 1/4 the speed of native funcs.
   8 
   9 There are several built-in numeric functions, and the compiler makes it easy
  10 to add your custom Go functions to further speed-up any operations specific to
  11 your app/domain.
  12 
  13 Finally, notice how float64 data don't really limit you as much as you might
  14 think at first, since they can act as booleans (treating 0 as false, and non-0
  15 as true), or as exact integers in the extremely-wide range [-2**53, +2**53].
  16 */
  17 package fmscripts

     File: ./go.mod
   1 module fmscripts
   2 
   3 go 1.18

     File: ./math.go
   1 package fmscripts
   2 
   3 import (
   4     "math"
   5 )
   6 
   7 const (
   8     // the maximum integer a float64 can represent exactly; -maxflint is the
   9     // minimum such integer, since floating-point values allow such symmetries
  10     maxflint = 2 << 52
  11 
  12     // base-2 versions of size multipliers
  13     kilobyte = 1024 * 1.0
  14     megabyte = 1024 * kilobyte
  15     gigabyte = 1024 * megabyte
  16     terabyte = 1024 * gigabyte
  17     petabyte = 1024 * terabyte
  18 
  19     // unit-conversion multipliers
  20     mi2kmMult   = 1 / 0.6213712
  21     nm2kmMult   = 1.852
  22     nmi2kmMult  = 1.852
  23     yd2mtMult   = 1 / 1.093613
  24     ft2mtMult   = 1 / 3.28084
  25     in2cmMult   = 2.54
  26     lb2kgMult   = 0.4535924
  27     ga2ltMult   = 1 / 0.2199692
  28     gal2lMult   = 1 / 0.2199692
  29     oz2mlMult   = 29.5735295625
  30     cup2lMult   = 0.2365882365
  31     mpg2kplMult = 0.2199692 / 0.6213712
  32     ton2kgMult  = 1 / 907.18474
  33     psi2paMult  = 1 / 0.00014503773800722
  34     deg2radMult = math.Pi / 180
  35     rad2degMult = 180 / math.Pi
  36 )
  37 
  38 // default math constants
  39 var mathConst = map[string]float64{
  40     "e":   math.E,
  41     "pi":  math.Pi,
  42     "tau": 2 * math.Pi,
  43     "phi": math.Phi,
  44     "nan": math.NaN(),
  45     "inf": math.Inf(+1),
  46 
  47     "minint":     -float64(maxflint - 1),
  48     "maxint":     +float64(maxflint - 1),
  49     "minsafeint": -float64(maxflint - 1),
  50     "maxsafeint": +float64(maxflint - 1),
  51 
  52     "false": 0.0,
  53     "true":  1.0,
  54     "f":     0.0,
  55     "t":     1.0,
  56 
  57     // conveniently-named multipliers
  58     "femto": 1e-15,
  59     "pico":  1e-12,
  60     "nano":  1e-09,
  61     "micro": 1e-06,
  62     "milli": 1e-03,
  63     "kilo":  1e+03,
  64     "mega":  1e+06,
  65     "giga":  1e+09,
  66     "tera":  1e+12,
  67     "peta":  1e+15,
  68 
  69     // unit-conversion multipliers
  70     "mi2km":   mi2kmMult,
  71     "nm2km":   nm2kmMult,
  72     "nmi2km":  nmi2kmMult,
  73     "yd2mt":   yd2mtMult,
  74     "ft2mt":   ft2mtMult,
  75     "in2cm":   in2cmMult,
  76     "lb2kg":   lb2kgMult,
  77     "ga2lt":   ga2ltMult,
  78     "gal2l":   gal2lMult,
  79     "oz2ml":   oz2mlMult,
  80     "cup2l":   cup2lMult,
  81     "mpg2kpl": mpg2kplMult,
  82     "ton2kg":  ton2kgMult,
  83     "psi2pa":  psi2paMult,
  84     "deg2rad": deg2radMult,
  85     "rad2deg": rad2degMult,
  86 
  87     // base-2 versions of size multipliers
  88     "kb":      kilobyte,
  89     "mb":      megabyte,
  90     "gb":      gigabyte,
  91     "tb":      terabyte,
  92     "pb":      petabyte,
  93     "binkilo": kilobyte,
  94     "binmega": megabyte,
  95     "bingiga": gigabyte,
  96     "bintera": terabyte,
  97     "binpeta": petabyte,
  98 
  99     // physical constants
 100     "c":   299_792_458,       // speed of light in m/s
 101     "g":   6.67430e-11,       // gravitational constant in N m2/kg2
 102     "h":   6.62607015e-34,    // planck constant in J s
 103     "ec":  1.602176634e-19,   // elementary charge in C
 104     "e0":  8.8541878128e-12,  // vacuum permittivity in C2/(Nm2)
 105     "mu0": 1.25663706212e-6,  // vacuum permeability in T m/A
 106     "k":   1.380649e-23,      // boltzmann constant in J/K
 107     "mu":  1.66053906660e-27, // atomic mass constant in kg
 108     "me":  9.1093837015e-31,  // electron mass in kg
 109     "mp":  1.67262192369e-27, // proton mass in kg
 110     "mn":  1.67492749804e-27, // neutron mass in kg
 111 
 112     // float64s can only vaguely approx. avogadro's mole (6.02214076e23)
 113 }
 114 
 115 // deterministic math functions lookup-table generated using the command
 116 //
 117 // go doc math | awk '/func/ { gsub(/func |\(.*/, ""); printf("\"%s\": math.%s,\n", tolower($0), $0) }'
 118 //
 119 // then hand-edited to remove funcs, or to use adapter funcs when needed: removed
 120 // funcs either had multiple returns (like SinCos) or dealt with float32 values
 121 var determFuncs = map[string]any{
 122     "abs":         math.Abs,
 123     "acos":        math.Acos,
 124     "acosh":       math.Acosh,
 125     "asin":        math.Asin,
 126     "asinh":       math.Asinh,
 127     "atan":        math.Atan,
 128     "atan2":       math.Atan2,
 129     "atanh":       math.Atanh,
 130     "cbrt":        math.Cbrt,
 131     "ceil":        math.Ceil,
 132     "copysign":    math.Copysign,
 133     "cos":         math.Cos,
 134     "cosh":        math.Cosh,
 135     "dim":         math.Dim,
 136     "erf":         math.Erf,
 137     "erfc":        math.Erfc,
 138     "erfcinv":     math.Erfcinv,
 139     "erfinv":      math.Erfinv,
 140     "exp":         math.Exp,
 141     "exp2":        math.Exp2,
 142     "expm1":       math.Expm1,
 143     "fma":         math.FMA,
 144     "floor":       math.Floor,
 145     "gamma":       math.Gamma,
 146     "inf":         inf,
 147     "isinf":       isInf,
 148     "isnan":       isNaN,
 149     "j0":          math.J0,
 150     "j1":          math.J1,
 151     "jn":          jn,
 152     "ldexp":       ldexp,
 153     "lgamma":      lgamma,
 154     "log10":       math.Log10,
 155     "log1p":       math.Log1p,
 156     "log2":        math.Log2,
 157     "logb":        math.Logb,
 158     "mod":         math.Mod,
 159     "nan":         math.NaN,
 160     "nextafter":   math.Nextafter,
 161     "pow":         math.Pow,
 162     "pow10":       pow10,
 163     "remainder":   math.Remainder,
 164     "round":       math.Round,
 165     "roundtoeven": math.RoundToEven,
 166     "signbit":     signbit,
 167     "sin":         math.Sin,
 168     "sinh":        math.Sinh,
 169     "sqrt":        math.Sqrt,
 170     "tan":         math.Tan,
 171     "tanh":        math.Tanh,
 172     "trunc":       math.Trunc,
 173     "y0":          math.Y0,
 174     "y1":          math.Y1,
 175     "yn":          yn,
 176 
 177     // a few aliases for the standard math funcs: some of the single-letter
 178     // aliases are named after the ones in `bc`, the basic calculator tool
 179     "a":          math.Abs,
 180     "c":          math.Cos,
 181     "ceiling":    math.Ceil,
 182     "cosine":     math.Cos,
 183     "e":          math.Exp,
 184     "isinf0":     isAnyInf,
 185     "isinfinite": isAnyInf,
 186     "l":          math.Log,
 187     "ln":         math.Log,
 188     "lg":         math.Log2,
 189     "modulus":    math.Mod,
 190     "power":      math.Pow,
 191     "rem":        math.Remainder,
 192     "s":          math.Sin,
 193     "sine":       math.Sin,
 194     "t":          math.Tan,
 195     "tangent":    math.Tan,
 196     "truncate":   math.Trunc,
 197     "truncated":  math.Trunc,
 198 
 199     // not from standard math: these custom funcs were added manually
 200     "beta":       beta,
 201     "bool":       num2bool,
 202     "clamp":      clamp,
 203     "cond":       cond, // vector-arg if-else chain
 204     "cube":       cube,
 205     "cubed":      cube,
 206     "degrees":    degrees,
 207     "deinf":      deInf,
 208     "denan":      deNaN,
 209     "factorial":  factorial,
 210     "fract":      fract,
 211     "horner":     polyval,
 212     "hypot":      hypot,
 213     "if":         ifElse,
 214     "ifelse":     ifElse,
 215     "inv":        reciprocal,
 216     "isanyinf":   isAnyInf,
 217     "isbad":      isBad,
 218     "isfin":      isFinite,
 219     "isfinite":   isFinite,
 220     "isgood":     isGood,
 221     "isinteger":  isInteger,
 222     "lbeta":      lnBeta,
 223     "len":        length,
 224     "length":     length,
 225     "lnbeta":     lnBeta,
 226     "log":        math.Log,
 227     "logistic":   logistic,
 228     "mag":        length,
 229     "max":        max,
 230     "min":        min,
 231     "neg":        negate,
 232     "negate":     negate,
 233     "not":        notBool,
 234     "polyval":    polyval,
 235     "radians":    radians,
 236     "range":      rangef, // vector-arg max - min
 237     "reciprocal": reciprocal,
 238     "rev":        revalue,
 239     "revalue":    revalue,
 240     "scale":      scale,
 241     "sgn":        sign,
 242     "sign":       sign,
 243     "sinc":       sinc,
 244     "sq":         sqr,
 245     "sqmin":      solveQuadMin,
 246     "sqmax":      solveQuadMax,
 247     "square":     sqr,
 248     "squared":    sqr,
 249     "unwrap":     unwrap,
 250     "wrap":       wrap,
 251 
 252     // a few aliases for the custom funcs
 253     "deg":   degrees,
 254     "isint": isInteger,
 255     "rad":   radians,
 256 
 257     // a few quicker 2-value versions of vararg funcs: the optimizer depends
 258     // on these to rewrite 2-input uses of their vararg counterparts
 259     "hypot2": math.Hypot,
 260     "max2":   math.Max,
 261     "min2":   math.Min,
 262 
 263     // a few entries to enable custom syntax: the parser depends on these to
 264     // rewrite binary expressions into func calls
 265     "??":  deNaN,
 266     "?:":  ifElse,
 267     "===": same,
 268     "!==": notSame,
 269 }
 270 
 271 // DefineDetFuncs adds more deterministic funcs to the default set. Such funcs
 272 // are considered optimizable, since calling them with the same constant inputs
 273 // is supposed to return the same constant outputs, as the name `deterministic`
 274 // suggests.
 275 //
 276 // Only call this before compiling any scripts, and ensure all funcs given are
 277 // supported and are deterministic. Random-output funcs certainly won't fit
 278 // the bill here.
 279 func DefineDetFuncs(funcs map[string]any) {
 280     for k, v := range funcs {
 281         determFuncs[k] = v
 282     }
 283 }
 284 
 285 func sqr(x float64) float64 {
 286     return x * x
 287 }
 288 
 289 func cube(x float64) float64 {
 290     return x * x * x
 291 }
 292 
 293 func num2bool(x float64) float64 {
 294     if x == 0 {
 295         return 0
 296     }
 297     return 1
 298 }
 299 
 300 func logistic(x float64) float64 {
 301     return 1 / (1 + math.Exp(-x))
 302 }
 303 
 304 func sign(x float64) float64 {
 305     if math.IsNaN(x) {
 306         return x
 307     }
 308     if x > 0 {
 309         return +1
 310     }
 311     if x < 0 {
 312         return -1
 313     }
 314     return 0
 315 }
 316 
 317 func sinc(x float64) float64 {
 318     if x == 0 {
 319         return 1
 320     }
 321     return math.Sin(x) / x
 322 }
 323 
 324 func isInteger(x float64) float64 {
 325     _, frac := math.Modf(x)
 326     if frac == 0 {
 327         return 1
 328     }
 329     return 0
 330 }
 331 
 332 func inf(sign float64) float64 {
 333     return math.Inf(int(sign))
 334 }
 335 
 336 func isInf(x float64, sign float64) float64 {
 337     if math.IsInf(x, int(sign)) {
 338         return 1
 339     }
 340     return 0
 341 }
 342 
 343 func isAnyInf(x float64) float64 {
 344     if math.IsInf(x, 0) {
 345         return 1
 346     }
 347     return 0
 348 }
 349 
 350 func isFinite(x float64) float64 {
 351     if math.IsInf(x, 0) {
 352         return 0
 353     }
 354     return 1
 355 }
 356 
 357 func isNaN(x float64) float64 {
 358     if math.IsNaN(x) {
 359         return 1
 360     }
 361     return 0
 362 }
 363 
 364 func isGood(x float64) float64 {
 365     if math.IsNaN(x) || math.IsInf(x, 0) {
 366         return 0
 367     }
 368     return 1
 369 }
 370 
 371 func isBad(x float64) float64 {
 372     if math.IsNaN(x) || math.IsInf(x, 0) {
 373         return 1
 374     }
 375     return 0
 376 }
 377 
 378 func same(x, y float64) float64 {
 379     if math.IsNaN(x) && math.IsNaN(y) {
 380         return 1
 381     }
 382     return debool(x == y)
 383 }
 384 
 385 func notSame(x, y float64) float64 {
 386     if math.IsNaN(x) && math.IsNaN(y) {
 387         return 0
 388     }
 389     return debool(x != y)
 390 }
 391 
 392 func deNaN(x float64, instead float64) float64 {
 393     if !math.IsNaN(x) {
 394         return x
 395     }
 396     return instead
 397 }
 398 
 399 func deInf(x float64, instead float64) float64 {
 400     if !math.IsInf(x, 0) {
 401         return x
 402     }
 403     return instead
 404 }
 405 
 406 func revalue(x float64, instead float64) float64 {
 407     if !math.IsNaN(x) && !math.IsInf(x, 0) {
 408         return x
 409     }
 410     return instead
 411 }
 412 
 413 func pow10(n float64) float64 {
 414     return math.Pow10(int(n))
 415 }
 416 
 417 func jn(n, x float64) float64 {
 418     return math.Jn(int(n), x)
 419 }
 420 
 421 func ldexp(frac, exp float64) float64 {
 422     return math.Ldexp(frac, int(exp))
 423 }
 424 
 425 func lgamma(x float64) float64 {
 426     y, s := math.Lgamma(x)
 427     if s < 0 {
 428         return math.NaN()
 429     }
 430     return y
 431 }
 432 
 433 func signbit(x float64) float64 {
 434     if math.Signbit(x) {
 435         return 1
 436     }
 437     return 0
 438 }
 439 
 440 func yn(n, x float64) float64 {
 441     return math.Yn(int(n), x)
 442 }
 443 
 444 func negate(x float64) float64 {
 445     return -x
 446 }
 447 
 448 func reciprocal(x float64) float64 {
 449     return 1 / x
 450 }
 451 
 452 func rangef(v ...float64) float64 {
 453     min := math.Inf(+1)
 454     max := math.Inf(-1)
 455     for _, f := range v {
 456         min = math.Min(min, f)
 457         max = math.Max(max, f)
 458     }
 459     return max - min
 460 }
 461 
 462 func cond(v ...float64) float64 {
 463     for {
 464         switch len(v) {
 465         case 0:
 466             // either no values are left, or no values were given at all
 467             return math.NaN()
 468 
 469         case 1:
 470             // either all previous conditions failed, and this last value is
 471             // automatically chosen, or only 1 value was given to begin with
 472             return v[0]
 473 
 474         default:
 475             // check condition: if true (non-0), return the value after it
 476             if v[0] != 0 {
 477                 return v[1]
 478             }
 479             // condition was false, so skip the leading pair of values
 480             v = v[2:]
 481         }
 482     }
 483 }
 484 
 485 func notBool(x float64) float64 {
 486     if x == 0 {
 487         return 1
 488     }
 489     return 0
 490 }
 491 
 492 func ifElse(cond float64, yes, no float64) float64 {
 493     if cond != 0 {
 494         return yes
 495     }
 496     return no
 497 }
 498 
 499 func lnGamma(x float64) float64 {
 500     y, s := math.Lgamma(x)
 501     if s < 0 {
 502         return math.NaN()
 503     }
 504     return y
 505 }
 506 
 507 func lnBeta(x float64, y float64) float64 {
 508     return lnGamma(x) + lnGamma(y) - lnGamma(x+y)
 509 }
 510 
 511 func beta(x float64, y float64) float64 {
 512     return math.Exp(lnBeta(x, y))
 513 }
 514 
 515 func factorial(n float64) float64 {
 516     return math.Round(math.Gamma(n + 1))
 517 }
 518 
 519 func degrees(rad float64) float64 {
 520     return rad2degMult * rad
 521 }
 522 
 523 func radians(deg float64) float64 {
 524     return deg2radMult * deg
 525 }
 526 
 527 func fract(x float64) float64 {
 528     return x - math.Floor(x)
 529 }
 530 
 531 func min(v ...float64) float64 {
 532     min := +math.Inf(+1)
 533     for _, f := range v {
 534         min = math.Min(min, f)
 535     }
 536     return min
 537 }
 538 
 539 func max(v ...float64) float64 {
 540     max := +math.Inf(-1)
 541     for _, f := range v {
 542         max = math.Max(max, f)
 543     }
 544     return max
 545 }
 546 
 547 func hypot(v ...float64) float64 {
 548     sumsq := 0.0
 549     for _, f := range v {
 550         sumsq += f * f
 551     }
 552     return math.Sqrt(sumsq)
 553 }
 554 
 555 func length(v ...float64) float64 {
 556     ss := 0.0
 557     for _, f := range v {
 558         ss += f * f
 559     }
 560     return math.Sqrt(ss)
 561 }
 562 
 563 // solveQuadMin finds the lowest solution of a 2nd-degree polynomial, using
 564 // a formula which is more accurate than the textbook one
 565 func solveQuadMin(a, b, c float64) float64 {
 566     disc := math.Sqrt(b*b - 4*a*c)
 567     r1 := 2 * c / (-b - disc)
 568     return r1
 569 }
 570 
 571 // solveQuadMax finds the highest solution of a 2nd-degree polynomial, using
 572 // a formula which is more accurate than the textbook one
 573 func solveQuadMax(a, b, c float64) float64 {
 574     disc := math.Sqrt(b*b - 4*a*c)
 575     r2 := 2 * c / (-b + disc)
 576     return r2
 577 }
 578 
 579 func wrap(x float64, min, max float64) float64 {
 580     return (x - min) / (max - min)
 581 }
 582 
 583 func unwrap(x float64, min, max float64) float64 {
 584     return (max-min)*x + min
 585 }
 586 
 587 func clamp(x float64, min, max float64) float64 {
 588     return math.Min(math.Max(x, min), max)
 589 }
 590 
 591 func scale(x float64, xmin, xmax, ymin, ymax float64) float64 {
 592     k := (x - xmin) / (xmax - xmin)
 593     return (ymax-ymin)*k + ymin
 594 }
 595 
 596 // polyval runs horner's algorithm on a value, with the polynomial coefficients
 597 // given after it, higher-order first
 598 func polyval(x float64, v ...float64) float64 {
 599     if len(v) == 0 {
 600         return 0
 601     }
 602 
 603     x0 := x
 604     x = 1.0
 605     y := 0.0
 606     for i := len(v) - 1; i >= 0; i-- {
 607         y += v[i] * x
 608         x *= x0
 609     }
 610     return y
 611 }

     File: ./math_test.go
   1 package fmscripts
   2 
   3 import "testing"
   4 
   5 func TestTables(t *testing.T) {
   6     for k, v := range determFuncs {
   7         t.Run(k, func(t *testing.T) {
   8             if !isSupportedFunc(v) {
   9                 t.Fatalf("unsupported func of type %T", v)
  10             }
  11         })
  12     }
  13 }

     File: ./optimizers.go
   1 package fmscripts
   2 
   3 import (
   4     "math"
   5 )
   6 
   7 // unary2func matches unary operators to funcs the optimizer can use to eval
   8 // constant-input unary expressions into their results
   9 var unary2func = map[string]func(x float64) float64{
  10     // avoid unary +, since it's a no-op and thus there's no instruction for
  11     // it, which in turn makes unit-tests for these optimization tables fail
  12     // unnecessarily
  13     // "+": func(x float64) float64 { return +x },
  14 
  15     "-": func(x float64) float64 { return -x },
  16     "!": func(x float64) float64 { return debool(x == 0) },
  17     "&": func(x float64) float64 { return math.Abs(x) },
  18     "*": func(x float64) float64 { return x * x },
  19     "^": func(x float64) float64 { return x * x },
  20     "/": func(x float64) float64 { return 1 / x },
  21 }
  22 
  23 // binary2func matches binary operators to funcs the optimizer can use to eval
  24 // constant-input binary expressions into their results
  25 var binary2func = map[string]func(x, y float64) float64{
  26     "+":  func(x, y float64) float64 { return x + y },
  27     "-":  func(x, y float64) float64 { return x - y },
  28     "*":  func(x, y float64) float64 { return x * y },
  29     "/":  func(x, y float64) float64 { return x / y },
  30     "%":  func(x, y float64) float64 { return math.Mod(x, y) },
  31     "&":  func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  32     "&&": func(x, y float64) float64 { return debool(x != 0 && y != 0) },
  33     "|":  func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  34     "||": func(x, y float64) float64 { return debool(x != 0 || y != 0) },
  35     "==": func(x, y float64) float64 { return debool(x == y) },
  36     "!=": func(x, y float64) float64 { return debool(x != y) },
  37     "<>": func(x, y float64) float64 { return debool(x != y) },
  38     "<":  func(x, y float64) float64 { return debool(x < y) },
  39     "<=": func(x, y float64) float64 { return debool(x <= y) },
  40     ">":  func(x, y float64) float64 { return debool(x > y) },
  41     ">=": func(x, y float64) float64 { return debool(x >= y) },
  42     "**": func(x, y float64) float64 { return math.Pow(x, y) },
  43     "^":  func(x, y float64) float64 { return math.Pow(x, y) },
  44 }
  45 
  46 // func2unary turns built-in func names into built-in unary operators
  47 var func2unary = map[string]string{
  48     "a":          "&",
  49     "abs":        "&",
  50     "inv":        "/",
  51     "neg":        "-",
  52     "negate":     "-",
  53     "reciprocal": "/",
  54     "sq":         "*",
  55     "square":     "*",
  56     "squared":    "*",
  57 }
  58 
  59 // func2binary turns built-in func names into built-in binary operators
  60 var func2binary = map[string]string{
  61     "mod":     "%",
  62     "modulus": "%",
  63     "pow":     "**",
  64     "power":   "**",
  65 }
  66 
  67 // vararg2func2 matches variable-argument funcs to their 2-argument versions
  68 var vararg2func2 = map[string]string{
  69     "hypot": "hypot2",
  70     "max":   "max2",
  71     "min":   "min2",
  72 }
  73 
  74 // optimize tries to simplify the expression given as much as possible, by
  75 // simplifying constants whenever possible, and exploiting known built-in
  76 // funcs which are known to behave deterministically
  77 func (c *Compiler) optimize(expr any) any {
  78     switch expr := expr.(type) {
  79     case []any:
  80         return c.optimizeCombo(expr)
  81 
  82     case unaryExpr:
  83         return c.optimizeUnaryExpr(expr)
  84 
  85     case binaryExpr:
  86         return c.optimizeBinaryExpr(expr)
  87 
  88     case callExpr:
  89         return c.optimizeCallExpr(expr)
  90 
  91     case assignExpr:
  92         expr.expr = c.optimize(expr.expr)
  93         return expr
  94 
  95     default:
  96         f, ok := c.tryConstant(expr)
  97         if ok {
  98             return f
  99         }
 100         return expr
 101     }
 102 }
 103 
 104 // optimizeCombo handles a sequence of expressions for the optimizer
 105 func (c *Compiler) optimizeCombo(exprs []any) any {
 106     if len(exprs) == 1 {
 107         return c.optimize(exprs[0])
 108     }
 109 
 110     // count how many expressions are considered useful: these are
 111     // assignments as well as the last expression, since that's what
 112     // determines the script's final result
 113     useful := 0
 114     for i, v := range exprs {
 115         _, ok := v.(assignExpr)
 116         if ok || i == len(exprs)-1 {
 117             useful++
 118         }
 119     }
 120 
 121     // ignore all expressions which are a waste of time, and optimize
 122     // all other expressions
 123     res := make([]any, 0, useful)
 124     for i, v := range exprs {
 125         _, ok := v.(assignExpr)
 126         if ok || i == len(exprs)-1 {
 127             res = append(res, c.optimize(v))
 128         }
 129     }
 130     return res
 131 }
 132 
 133 // optimizeUnaryExpr handles unary expressions for the optimizer
 134 func (c *Compiler) optimizeUnaryExpr(expr unaryExpr) any {
 135     // recursively optimize input
 136     expr.x = c.optimize(expr.x)
 137 
 138     // optimize unary ops on a constant into concrete values
 139     if x, ok := expr.x.(float64); ok {
 140         if fn, ok := unary2func[expr.op]; ok {
 141             return fn(x)
 142         }
 143     }
 144 
 145     switch expr.op {
 146     case "+":
 147         // unary plus is an identity operation
 148         return expr.x
 149 
 150     default:
 151         return expr
 152     }
 153 }
 154 
 155 // optimizeBinaryExpr handles binary expressions for the optimizer
 156 func (c *Compiler) optimizeBinaryExpr(expr binaryExpr) any {
 157     // recursively optimize inputs
 158     expr.x = c.optimize(expr.x)
 159     expr.y = c.optimize(expr.y)
 160 
 161     // optimize binary ops on 2 constants into concrete values
 162     if x, ok := expr.x.(float64); ok {
 163         if y, ok := expr.y.(float64); ok {
 164             if fn, ok := binary2func[expr.op]; ok {
 165                 return fn(x, y)
 166             }
 167         }
 168     }
 169 
 170     switch expr.op {
 171     case "+":
 172         if expr.x == 0.0 {
 173             // 0+y -> y
 174             return expr.y
 175         }
 176         if expr.y == 0.0 {
 177             // x+0 -> x
 178             return expr.x
 179         }
 180 
 181     case "-":
 182         if expr.x == 0.0 {
 183             // 0-y -> -y
 184             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 185         }
 186         if expr.y == 0.0 {
 187             // x-0 -> x
 188             return expr.x
 189         }
 190 
 191     case "*":
 192         if expr.x == 0.0 || expr.y == 0.0 {
 193             // 0*y -> 0
 194             // x*0 -> 0
 195             return 0.0
 196         }
 197         if expr.x == 1.0 {
 198             // 1*y -> y
 199             return expr.y
 200         }
 201         if expr.y == 1.0 {
 202             // x*1 -> x
 203             return expr.x
 204         }
 205         if expr.x == -1.0 {
 206             // -1*y -> -y
 207             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.y})
 208         }
 209         if expr.y == -1.0 {
 210             // x*-1 -> -x
 211             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 212         }
 213 
 214     case "/":
 215         if expr.x == 1.0 {
 216             // 1/y -> reciprocal of y
 217             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.y})
 218         }
 219         if expr.y == 1.0 {
 220             // x/1 -> x
 221             return expr.x
 222         }
 223         if expr.y == -1.0 {
 224             // x/-1 -> -x
 225             return c.optimizeUnaryExpr(unaryExpr{op: "-", x: expr.x})
 226         }
 227 
 228     case "**":
 229         switch expr.y {
 230         case -1.0:
 231             // x**-1 -> 1/x, reciprocal of x
 232             return c.optimizeUnaryExpr(unaryExpr{op: "/", x: expr.x})
 233         case 0.0:
 234             // x**0 -> 1
 235             return 1.0
 236         case 1.0:
 237             // x**1 -> x
 238             return expr.x
 239         case 2.0:
 240             // x**2 -> *x
 241             return c.optimizeUnaryExpr(unaryExpr{op: "*", x: expr.x})
 242         case 3.0:
 243             // x**3 -> *x*x
 244             sq := unaryExpr{op: "*", x: expr.x}
 245             return c.optimizeBinaryExpr(binaryExpr{op: "*", x: sq, y: expr.x})
 246         }
 247 
 248     case "&", "&&":
 249         if expr.x == 0.0 || expr.y == 0.0 {
 250             // 0 && y -> 0
 251             // x && 0 -> 0
 252             return 0.0
 253         }
 254     }
 255 
 256     // no simplifiable patterns were detected
 257     return expr
 258 }
 259 
 260 // optimizeCallExpr optimizes special cases of built-in func calls
 261 func (c *Compiler) optimizeCallExpr(call callExpr) any {
 262     // recursively optimize all inputs, and keep track if they're all
 263     // constants in the end
 264     numlit := 0
 265     for i, v := range call.args {
 266         v = c.optimize(v)
 267         call.args[i] = v
 268         if _, ok := v.(float64); ok {
 269             numlit++
 270         }
 271     }
 272 
 273     // if func is overridden, there's no guarantee the new func works the same
 274     if _, ok := determFuncs[call.name]; c.ftype[call.name] != nil || !ok {
 275         return call
 276     }
 277 
 278     // from this point on, func is guaranteed to be built-in and deterministic
 279 
 280     // handle all-const inputs, by calling func and using its result
 281     if numlit == len(call.args) {
 282         in := make([]float64, 0, len(call.args))
 283         for _, v := range call.args {
 284             f, _ := v.(float64)
 285             in = append(in, f)
 286         }
 287 
 288         if f, ok := tryCall(determFuncs[call.name], in); ok {
 289             return f
 290         }
 291     }
 292 
 293     switch len(call.args) {
 294     case 1:
 295         if op, ok := func2unary[call.name]; ok {
 296             expr := unaryExpr{op: op, x: call.args[0]}
 297             return c.optimizeUnaryExpr(expr)
 298         }
 299         return call
 300 
 301     case 2:
 302         if op, ok := func2binary[call.name]; ok {
 303             expr := binaryExpr{op: op, x: call.args[0], y: call.args[1]}
 304             return c.optimizeBinaryExpr(expr)
 305         }
 306         if name, ok := vararg2func2[call.name]; ok {
 307             call.name = name
 308             return call
 309         }
 310         return call
 311 
 312     default:
 313         return call
 314     }
 315 }
 316 
 317 // tryConstant tries to optimize the expression given into a constant
 318 func (c *Compiler) tryConstant(expr any) (value float64, ok bool) {
 319     switch expr := expr.(type) {
 320     case float64:
 321         return expr, true
 322 
 323     case string:
 324         if _, ok := c.vaddr[expr]; !ok {
 325             // name isn't explicitly defined
 326             if f, ok := mathConst[expr]; ok {
 327                 // and is a known math constant
 328                 return f, true
 329             }
 330         }
 331         return 0, false
 332 
 333     default:
 334         return 0, false
 335     }
 336 }
 337 
 338 // tryCall tries to simplify the function expression given into a constant
 339 func tryCall(fn any, in []float64) (value float64, ok bool) {
 340     switch fn := fn.(type) {
 341     case func(float64) float64:
 342         if len(in) == 1 {
 343             return fn(in[0]), true
 344         }
 345         return 0, false
 346 
 347     case func(float64, float64) float64:
 348         if len(in) == 2 {
 349             return fn(in[0], in[1]), true
 350         }
 351         return 0, false
 352 
 353     case func(float64, float64, float64) float64:
 354         if len(in) == 3 {
 355             return fn(in[0], in[1], in[2]), true
 356         }
 357         return 0, false
 358 
 359     case func(float64, float64, float64, float64) float64:
 360         if len(in) == 4 {
 361             return fn(in[0], in[1], in[2], in[3]), true
 362         }
 363         return 0, false
 364 
 365     case func(float64, float64, float64, float64, float64) float64:
 366         if len(in) == 5 {
 367             return fn(in[0], in[1], in[2], in[3], in[4]), true
 368         }
 369         return 0, false
 370 
 371     case func(...float64) float64:
 372         return fn(in...), true
 373 
 374     case func(float64, ...float64) float64:
 375         if len(in) >= 1 {
 376             return fn(in[0], in[1:]...), true
 377         }
 378         return 0, false
 379 
 380     default:
 381         // type isn't a supported func
 382         return 0, false
 383     }
 384 }

     File: ./optimizers_test.go
   1 package fmscripts
   2 
   3 import (
   4     "math"
   5     "reflect"
   6     "testing"
   7 )
   8 
   9 func TestOptimizerTables(t *testing.T) {
  10     for k := range unary2func {
  11         t.Run(k, func(t *testing.T) {
  12             if _, ok := unary2op[k]; !ok {
  13                 t.Fatalf("missing unary constant optimizer for %q", k)
  14             }
  15         })
  16     }
  17 
  18     for k := range binary2func {
  19         t.Run(k, func(t *testing.T) {
  20             if _, ok := binary2op[k]; !ok {
  21                 t.Fatalf("missing binary constant optimizer for %q", k)
  22             }
  23         })
  24     }
  25 
  26     for k, name := range func2unary {
  27         t.Run(k, func(t *testing.T) {
  28             if _, ok := determFuncs[k]; !ok {
  29                 const fs = "func(x) optimizer %q has no matching built-in func"
  30                 t.Fatalf(fs, k)
  31             }
  32 
  33             if _, ok := unary2op[name]; !ok {
  34                 t.Fatalf("missing unary func optimizer for %q", k)
  35             }
  36         })
  37     }
  38 
  39     for k, name := range func2binary {
  40         t.Run(k, func(t *testing.T) {
  41             if _, ok := determFuncs[k]; !ok {
  42                 const fs = "func(x, y) optimizer %q has no matching built-in func"
  43                 t.Fatalf(fs, k)
  44             }
  45 
  46             if _, ok := binary2op[name]; !ok {
  47                 t.Fatalf("missing binary func optimizer for %q", k)
  48             }
  49         })
  50     }
  51 
  52     for k := range vararg2func2 {
  53         t.Run(k, func(t *testing.T) {
  54             if _, ok := determFuncs[k]; !ok {
  55                 const fs = "vararg optimizer %q has no matching built-in func"
  56                 t.Fatalf(fs, k)
  57             }
  58         })
  59     }
  60 }
  61 
  62 func TestOptimizer(t *testing.T) {
  63     var tests = []struct {
  64         Source   string
  65         Expected any
  66     }{
  67         {"1", 1.0},
  68         {"3+4*5", 23.0},
  69 
  70         {"e", math.E},
  71         {"pi", math.Pi},
  72         {"phi", math.Phi},
  73         {"2*pi", 2 * math.Pi},
  74         {"4.51*phi-14.23564", 4.51*math.Phi - 14.23564},
  75         {"-e", -math.E},
  76 
  77         {"exp(2*pi)", math.Exp(2 * math.Pi)},
  78         {"log(2342.55) / log(43.21)", math.Log(2342.55) / math.Log(43.21)},
  79         {"f(3)", callExpr{name: "f", args: []any{3.0}}},
  80         {"min(3, 2, -1.5)", -1.5},
  81 
  82         {"hypot(x, 4)", callExpr{name: "hypot2", args: []any{"x", 4.0}}},
  83         {"max(x, 4)", callExpr{name: "max2", args: []any{"x", 4.0}}},
  84         {"min(x, 4)", callExpr{name: "min2", args: []any{"x", 4.0}}},
  85 
  86         {"rand()", callExpr{name: "rand"}},
  87 
  88         {
  89             "sin(2_000 * x * tau * x)",
  90             callExpr{
  91                 name: "sin",
  92                 args: []any{
  93                     binaryExpr{
  94                         "*",
  95                         binaryExpr{
  96                             "*",
  97                             binaryExpr{"*", 2_000.0, "x"},
  98                             2 * math.Pi,
  99                         },
 100                         "x",
 101                     },
 102                 },
 103             },
 104         },
 105 
 106         {
 107             "sin(10 * tau * exp(-20 * x)) * exp(-2 * x)",
 108             binaryExpr{
 109                 "*",
 110                 // sin(...)
 111                 callExpr{
 112                     name: "sin",
 113                     args: []any{
 114                         // 10 * tau * exp(...)
 115                         binaryExpr{
 116                             "*",
 117                             10 * 2 * math.Pi,
 118                             // exp(-20 * x)
 119                             callExpr{
 120                                 name: "exp",
 121                                 args: []any{
 122                                     binaryExpr{"*", -20.0, "x"},
 123                                 },
 124                             },
 125                         },
 126                     },
 127                 },
 128                 // exp(-2 * x)
 129                 callExpr{
 130                     name: "exp",
 131                     args: []any{binaryExpr{"*", -2.0, "x"}},
 132                 },
 133             },
 134         },
 135     }
 136 
 137     defs := map[string]any{
 138         "x": 3.5,
 139         "f": math.Exp,
 140     }
 141 
 142     for _, tc := range tests {
 143         t.Run(tc.Source, func(t *testing.T) {
 144             var c Compiler
 145             root, err := parse(tc.Source)
 146             if err != nil {
 147                 t.Fatal(err)
 148                 return
 149             }
 150 
 151             if err := c.reset(defs); err != nil {
 152                 t.Fatal(err)
 153                 return
 154             }
 155 
 156             got := c.optimize(root)
 157             if !reflect.DeepEqual(got, tc.Expected) {
 158                 const fs = "expected result to be\n%#v\ninstead of\n%#v"
 159                 t.Fatalf(fs, tc.Expected, got)
 160                 return
 161             }
 162         })
 163     }
 164 }

     File: ./parsing.go
   1 package fmscripts
   2 
   3 import (
   4     "errors"
   5     "fmt"
   6     "strconv"
   7     "strings"
   8 )
   9 
  10 // parse turns source code into an expression type interpreters can use.
  11 func parse(src string) (any, error) {
  12     tok := newTokenizer(src)
  13     par, err := newParser(&tok)
  14     if err != nil {
  15         return nil, err
  16     }
  17 
  18     v, err := par.parse()
  19     err = par.improveError(err, src)
  20     return v, err
  21 }
  22 
  23 // pickLine slices a string, picking one of its lines via a 1-based index:
  24 // func improveError uses it to isolate the line of code an error came from
  25 func pickLine(src string, linenum int) string {
  26     // skip the lines before the target one
  27     for i := 0; i < linenum && len(src) > 0; i++ {
  28         j := strings.IndexByte(src, '\n')
  29         if j < 0 {
  30             break
  31         }
  32         src = src[j+1:]
  33     }
  34 
  35     // limit leftover to a single line
  36     i := strings.IndexByte(src, '\n')
  37     if i >= 0 {
  38         return src[:i]
  39     }
  40     return src
  41 }
  42 
  43 // unaryExpr is a unary expression
  44 type unaryExpr struct {
  45     op string
  46     x  any
  47 }
  48 
  49 // binaryExpr is a binary expression
  50 type binaryExpr struct {
  51     op string
  52     x  any
  53     y  any
  54 }
  55 
  56 // callExpr is a function call and its arguments
  57 type callExpr struct {
  58     name string
  59     args []any
  60 }
  61 
  62 // assignExpr is a value/variable assignment
  63 type assignExpr struct {
  64     name string
  65     expr any
  66 }
  67 
  68 // parser is a parser for JavaScript-like syntax, limited to operations on
  69 // 64-bit floating-point numbers
  70 type parser struct {
  71     tokens []token
  72     line   int
  73     pos    int
  74     toklen int
  75 }
  76 
  77 // newParser is the constructor for type parser
  78 func newParser(t *tokenizer) (parser, error) {
  79     var p parser
  80 
  81     // get all tokens from the source code
  82     for {
  83         v, err := t.next()
  84         if v.kind != unknownToken {
  85             p.tokens = append(p.tokens, v)
  86         }
  87 
  88         if err == errEOS {
  89             // done scanning/tokenizing
  90             return p, nil
  91         }
  92 
  93         if err != nil {
  94             // handle actual errors
  95             return p, err
  96         }
  97     }
  98 }
  99 
 100 // improveError adds more info about where exactly in the source code an error
 101 // came from, thus making error messages much more useful
 102 func (p *parser) improveError(err error, src string) error {
 103     if err == nil {
 104         return nil
 105     }
 106 
 107     line := pickLine(src, p.line)
 108     if len(line) == 0 || p.pos < 1 {
 109         const fs = "(line %d: pos %d): %w"
 110         return fmt.Errorf(fs, p.line, p.pos, err)
 111     }
 112 
 113     ptr := strings.Repeat(" ", p.pos) + "^"
 114     const fs = "(line %d: pos %d): %w\n%s\n%s"
 115     return fmt.Errorf(fs, p.line, p.pos, err, line, ptr)
 116 }
 117 
 118 // parse tries to turn tokens into a compilable abstract syntax tree, and is
 119 // the parser's entry point
 120 func (p *parser) parse() (any, error) {
 121     // source codes with no tokens are always errors
 122     if len(p.tokens) == 0 {
 123         const msg = "source code is empty, or has no useful expressions"
 124         return nil, errors.New(msg)
 125     }
 126 
 127     // handle optional excel-like leading equal sign
 128     p.acceptSyntax(`=`)
 129 
 130     // ignore trailing semicolons
 131     for len(p.tokens) > 0 {
 132         t := p