1. Introduction

  1. Introduction
    1. The Product
    2. Performance and Debugging Insights
    3. The compiled code
    4. Why not use PHP or JavaScript
    5. Example of Compiled Code
    6. Why Not Use Processor Core Instructions Directly?
    7. Compare to a FileMaker Script Doing the Same
    8. Interaction with the FileMaker Environment
    9. Standalone Compiler or Plugin Compiler
    10. Conclusion

A Brief History of the ACF Language

With over 30 years of experience in the computer industry, and having worked extensively with various programming languages, I have developed the ACF language by drawing inspiration from the syntax of multiple programming languages. The primary objective was to create a language that is both easy to read and write, while also being efficient in performing tasks that I found lacking in conventional scripting or standard calculations. In ACF, you will find elements inspired by PHP, C, old Fortran, Basic, ADA, and other proprietary programming languages.

To enhance code readability, I chose not to implement the curly-bracket block syntax seen in PHP. Instead, in ACF, code blocks are concluded with keywords such as "end if," "end for," or "end while." This approach aligns with the syntax used in FileMaker scripting, making the code more structured and comprehensible.

In regular FileMaker calculations, although they excel at handling simple calculations, nesting "if" constructs can often result in code that is challenging to decipher. While indentation and breaking code into multiple lines can improve readability to some extent, it becomes problematic when calculations end with a multitude of closing parentheses. Let's examine an example from a typical FileMaker calculation:


WashCharacters ( If(DeliveryAddress::l_Company_Name ≠ "" and DeliveryAddress::l_Country ≠ "" ;  LeftWords ( DeliveryAddress::l_Country ; 1) ;     If( Order::p_Country ≠ "" ; LeftWords( Order::p_Country  ;1) ; "NO"))) 

Is it immediately clear whether this example achieves its intended purpose? Or does it lack clarity and logic? A quick analysis reveals that there might be a logic issue in this calculation. Deciphering its purpose can take several minutes. In contrast, ACF's block structure syntax would have made this issue apparent at a glance, potentially preventing such bugs during development.

Consider the following ACF equivalent of the same calculation. While it may be somewhat longer, its clarity is evident:

function delivery_country ()
    string country;
    If (DeliveryAddress::l_Company_Name ≠ "" && DeliveryAddress::l_Country ≠ "") then
        country = @LeftWords ( DeliveryAddress::l_Country ; 1)@;
    elseif ( Order::p_Country ≠ "" ) then
        country = @LeftWords( Order::p_Country  ;1)@;
    else
        country = "NO"; 
    end if
    return country; 
end     

The ACF language aims to elevate code quality, expedite development, enhance code portability, promote code reuse, and ultimately save valuable development time. It prioritizes clear and structured code, which aids developers in writing robust and maintainable software.

1.1. The Product

The ACF compiler serves as a compiler designed for use with the ACF FileMaker Plugin, aimed at facilitating the creation of advanced custom functions for FileMaker development. Traditionally, FileMaker primarily relies on scripts and calculations as its core language components. While calculations do include some structural elements such as "if" or "case" structures, they tend to be concise one-liners. However, as complexity increases, the code can become challenging to read and understand. Additionally, FileMaker calculations do not inherently support looping.

The concept of Advanced Custom Functions (ACFs) introduces programming language structures into the realm of FileMaker, creating a proprietary language definition within this project. The syntax of this language draws inspiration from other programming languages while adopting a scripting language-like syntax to expedite FileMaker developers' familiarity with it.

Here's an example of a custom function that calculates the annual interest rate based on the loan value, payment size, payment frequency, and loan duration. This calculation is not achievable through a simple formula and necessitates an iterative "test and fail" approach to converge on the result. When attempting to create such a function within the standard FileMaker custom function framework, recursive methods are typically required starting from FileMaker 17.

To begin, let's define a function that calculates the Payment Value based on the Present Value, interest rate, and the number of payments.


    /*
       AnnuityLoanPayment: 
       Calculate the Payment amounth for an annuity loan : 
       PV = Present Value
       r  = Interest rate
       n  = number of payments
    */ 

function AnnuityLoanPayment ( float PV, float r, int n)
    float P = r*PV/(1-(1+r)^(-n));  
    return P; 
end

Next, we'll create another function that utilizes this calculation for simulation purposes, and we'll include print statements to aid in debugging and testing.


    /*
       CalcAnnuityInterestRate: 
       Calculate the Interest rate for an annuity loan by simulation : 
       LoanSum = Present Value
       P  = Payment amounth
       Y  = number of years
       nY  = number of payments pr year. 
    */ 

function CalcAnnuityInterestRate ( float LoanSum, float P, int Y, int nY)

float r; 
float res; 
// We start with High Interest rate. 
float rY = 100.0;  
float step = rY/2; 
float usedrY; 
if (P*Y*nY < LoanSum) then
        throw "\nNot enough payment - Payment starts at : " + LoanSum / (nY*Y); 
else
    repeat
        usedrY = rY; 
        r = rY/100/nY; 
        res = AnnuityLoanPayment ( LoanSum, r, Y*nY); 
        print "\nInterest: " + rY + "% - Payment: " + res;
        if ((res-P)>0.0) then
            print " diff(+) " + (res-P); 
            rY = rY - step; 
        else
            print " diff(-) " + (res-P); 
            rY = rY + step; 
        end if
        step = step / 2; 
  until ((abs(res-P)<0.0001) || (step < 0.000001));
end if
return usedrY; 
end

1.2. Performance and Debugging Insights

During the execution of approximately 26 iterations within the loop, we successfully obtained the desired result. For example, consider the following function call:


    CalcAnnuityInterestRate(100000.0, 2000.0, 5, 12); 
The result was computed in a mere 0.000217 seconds or 217 microseconds on my developer Mac Mini, featuring a 2.8 GHz Intel Core i5 processor. It's important to note that the print statements were commented out during this test. However, when the print statements were included, the execution time increased slightly to 483 microseconds. Here's an example of the output with the print statements active:


    Interest: 100.0000% - Payment: 8402.305216 diff(+) 6402.305216
    Interest: 50.00000% - Payment: 4560.474166 diff(+) 2560.474166
    Interest: 25.00000% - Payment: 2935.132338 diff(+) 935.132338
    Interest: 12.50000% - Payment: 2249.793823 diff(+) 249.793823
    Interest: 6.250000% - Payment: 1944.926168 diff(-) -55.073832
    Interest: 9.375000% - Payment: 2094.082735 diff(+) 94.082735
    Interest: 7.812500% - Payment: 2018.677871 diff(+) 18.677871
    Interest: 7.031250% - Payment: 1981.594566 diff(-) -18.405434
    Interest: 7.421875% - Payment: 2000.084452 diff(+) 0.084452
    Interest: 7.226562% - Payment: 1990.826555 diff(-) -9.173445
    Interest: 7.324219% - Payment: 1995.452267 diff(-) -4.547733
    Interest: 7.373047% - Payment: 1997.767550 diff(-) -2.232450
    Interest: 7.397461% - Payment: 1998.925799 diff(-) -1.074201
    Interest: 7.409668% - Payment: 1999.505075 diff(-) -0.494925
    Interest: 7.415771% - Payment: 1999.794751 diff(-) -0.205249
    Interest: 7.418823% - Payment: 1999.939598 diff(-) -0.060402
    Interest: 7.420349% - Payment: 2000.012024 diff(+) 0.012024
    Interest: 7.419586% - Payment: 1999.975811 diff(-) -0.024189
    Interest: 7.419968% - Payment: 1999.993918 diff(-) -0.006082
    Interest: 7.420158% - Payment: 2000.002971 diff(+) 0.002971
    Interest: 7.420063% - Payment: 1999.998444 diff(-) -0.001556
    Interest: 7.420111% - Payment: 2000.000708 diff(+) 0.000708
    Interest: 7.420087% - Payment: 1999.999576 diff(-) -0.000424
    Interest: 7.420099% - Payment: 2000.000142 diff(+) 0.000142
    Interest: 7.420093% - Payment: 1999.999859 diff(-) -0.000141
    Interest: 7.420096% - Payment: 2000.000000 diff(+) 0.000000
    Execution completed in 0.000483 secs: result: 7.4201

1.3. The compiled code

It's important to note that the compiled code is not machine code executed directly by the processor core. Instead, it comprises a series of instructions interpreted by a runtime library. These instructions can include both processor-like instructions and library functions.

1.4. Why not use PHP or JavaScript

While languages like PHP and JavaScript are undoubtedly powerful, our aim with ACF is to provide tight integration with the FileMaker environment. ACF allows you to reference FileMaker calculations and variables directly within your source code. For example:

string a = @let([v = 22; b=23]; v*b*$$ConstantValue)@;

or

$$OurPartResult = sqrt ( pi*r^2 ) + $$FileMakerVar1; 

This approach streamlines development by seamlessly blending code with the FileMaker environment, making it highly efficient.

Another reason for not using PHP or JavaScript is that they are typically interpreted languages, which are not as fast as compiled languages. ACF, being a compiled language, offers performance benefits. Additionally, ACF's compiler checks the source code for syntax errors, ensuring that your code is free from runtime errors due to syntax issues. It also reduces dependency on external libraries, which can be OS-dependent or subject to compatibility issues. Furthermore, ACF's compiled product can be easily installed in FileMaker using a script step, providing a secure way to deploy your solution without exposing the source code.

1.5. Example of Compiled Code

This example delves into technical details. If you are not particularly interested in the code's construction, feel free to skip to the next section.

First, let's examine the compiled sequence of the small function. The following mnemonics are presented to represent the assembly-like instructions, which are, in reality, stored as integer representations. Within the runtime environment, a stack is employed to handle arguments, and a variable stack is used for local variables. These variables are assigned a numerical identifier relative to the beginning of the local variable block for the function. The instructions themselves occupy either 1, 2, or 3 memory locations, depending on their parameters.

// 168: function AnnuityLoanPayment ( float PV, float r, int n) 
 841:         ENTER 38 3          // AnnuityLoanPayment
 844:         DECL 0 5          // Declare variable PV: DOUBLE
 847:         LDPARX 0          // PV
 849:         DECL 1 5          // Declare variable r: DOUBLE
 852:         LDPARX 1          // r
 854:         DECL 2 1          // Declare variable n: INTEGER
 857:         LDPARX 2          // n

// 169:     float P = r*PV/(1-(1+r)^(-n));  
 859:         DECL 3 5          // Declare variable P: DOUBLE
 862:         LDVARL 1          // r
 864:         LDVARL 0          // PV
 866:         MUL_FF            // Multiply 2 doubles
 867:         LDNUM 6           // 1
 869:         LDNUM 6           // 1
 871:         LDVARL 1          // r
 873:         ADD_IF            // Add int and double
 874:         LDVARL 2          // n
 876:         LDNUM 21          // -1
 878:         MUL_II            // Multiply 2 int's
 879:         XupY              // Power
 880:         SUB_IF            // Subtract int and double
 881:         DIV_FF            // Div 2 doubles
 882:         STOREL 3          // P

// 170:     return P; 
 884:         LDVARL 3          // P
 886:         RETURN 1 
// 171: end

The compiler establishes a table containing all the literals, which can be either strings or numbers utilized in the calculations. Instructions within the code only reference the index associated with these literals in the table. This approach allows literals to be shared among all the functions within the same file, optimizing memory usage and enhancing efficiency.

1.6. Why Not Use Processor Core Instructions Directly?

There are several compelling reasons for avoiding direct utilization of processor core instructions:

  1. Hardware Independence: If we were to directly employ processor core instructions, the runtime would become highly dependent on specific hardware configurations. By using the compiled code approach, we achieve hardware independence. The same code can seamlessly run on different types of computer hardware without requiring any alterations. This simplifies the compiler's task, as it only needs to generate a single target code. Additionally, it eliminates the need for maintaining multiple versions of the executable to accommodate diverse Mac and Windows user environments.

  2. Control and Containment: Another significant advantage lies in the degree of control we exert over executions. By encapsulating execution within the program space for the functions we create, we maintain a level of isolation and control that isn't achievable through direct processor core instruction execution.

  3. Enhanced Library Concept: The use of a higher-level language like ACF allows us to employ a more straightforward library concept. Many of the instructions within ACF are, in fact, library functions that perform complex operations. These functions often encompass more functionality than what processor core instructions alone could achieve.

While it's conceivable to further optimize performance by using processor core instructions directly, the example illustrated above demonstrates that the current approach yields remarkably fast execution speeds—beyond initial expectations.

1.7. Compare to a FileMaker Script Doing the Same

I created a regular custom function to replicate the first function, followed by a script to execute the simulation. With the inclusion of print statements (using a variable to collect text), the average execution time was approximately seven milliseconds. When I removed the text logging, the execution time occasionally improved to 4, 5, or 6 milliseconds. However, this remains significantly slower than our compiled code, which runs at approximately 500 microseconds with print statements and only 217 microseconds without them. In other words, our compiled code is approximately 23 times faster without print statements and 14 times faster with them. Despite these variations, both approaches ultimately yield the same result.

You can explore this example in the download area under the name "acf-annuity-loan speed demo."

Here is the FileMaker script I used for test

Set Variable [ $ts ; Value: Get(CurrentTimeUTCMilliseconds) ] 
Set Variable [ $LoanSum ; Value: 100000 ] 
Set Variable [ $Payment ; Value: 2000 ] 
Set Variable [ $Y ; Value: 5 ] 
Set Variable [ $nY ; Value: 12 ] 
Set Variable [ $rY ; Value: 100 ] 
Set Variable [ $step ; Value: $ry/2 ] 
Set Variable [ $$debLog ; Value: "" ] 
If [ $Payment * $Y * $nY < $LoanSum ] 
    Show Custom Dialog [ "Not enough payment - Payment starts at : " & ($LoanSum / ($nY*$Y)) ] 
Else
    Loop
        Set Variable [ $usedrY ; Value: $rY ] 
        Set Variable [ $r ; Value: $rY / 100 / $nY ] 
        Set Variable [ $res ; Value: AnnuityLoanPayment ( $LoanSum ; $r ; $Y * $nY ) ] 
        // Set Variable [ $$deblog ; Value: $$deblog & "¶" & "Interest: " & $rY & "% - gives Payment: " + $res ] 
        If [ ($res-$Payment)>0 ] 
            // Set Variable [ $$deblog ; Value: $$deblog & " diff(+) " & ($res-$Payment) ] 
            Set Variable [ $rY ; Value: $rY - $step ] 
        Else
            // Set Variable [ $$deblog ; Value: $$deblog & " diff(-) " & ($res-$Payment) ] 
            Set Variable [ $rY ; Value: $rY + $step ] 
        End If
        Set Variable [ $step ; Value: $step / 2 ] 
    Exit Loop If [ ((Abs($res-$Payment)<,0001) or ($step < ,000001)) ] 
    End Loop
End If
Set Variable [ $te ; Value: Get(CurrentTimeUTCMilliseconds) ] 
Show Custom Dialog [ "Finished" ; "We finished this in " &  ( $te - $ts ) & " millisecs, result: " & $usedrY ] 

1.8. Interaction with the FileMaker Environment

What sets ACF apart from many other high-level language implementations is its seamless interaction with the FileMaker environment, a key aspect that makes ACF functions custom functions.

  1. You can utilize and assign values to standard FileMaker variables by simply referencing their names, such as $FileName or $$extraresult. While accessing these variables may not be as fast as using internal local variables, it provides an additional interface to the script executing the functions.

  2. ACF allows you to perform FileMaker calculations directly from within the ACF script. Enclose single-line calculations with "@" characters or use double "@@" for multi-line calculations.

  3. You can access field content by employing the "::" notation for field names, such as table::field, directly within internal calculations. However, it's important to note that you cannot assign values to fields directly. Instead, you can return a value from the function and use the "set field" command to update field values.

1.9. Standalone Compiler or Plugin Compiler

Initially, the compiler was developed as a standalone compiler (command-line tool) with the intention of eventually incorporating it into a FileMaker plugin, along with the runtime. Currently, the plugin includes both the runtime and the compiler, and many functions are tightly integrated into the FileMaker plugin environment. Consequently, the standalone compiler is no longer actively maintained. However, if there is a specific need, we can provide a standalone syntax checker for use in integrated development environments (IDEs) to help you syntax-check your code directly from the editor. This would be a separate project.

1.10. Conclusion

This package empowers developers to work more efficiently in a language that shares many similarities with traditional programming languages like PHP, C, 4D, BASIC, and FORTRAN. Those with experience in any of these languages should find it easy to adapt to ACF. The product promises faster development, eliminating the need to navigate through multiple modal dialogues when writing scripts in FileMaker's script editor. ACF's source code can be edited using a plain text editor such as TextMate or even Xcode, enabling smooth copy and paste operations without the interruption of modal dialogues.

Furthermore, the compiled code runs swiftly, enhancing the end user's experience with the solution. Additionally, the SQL functions in ACF allow for data retrieval, updates, or inserts without the need to reference the current layout's table occurrence.