The SRP PreCompiler is unlike any other stored procedure in the SRP Utilities collection. It is not a utility you explicitly call. It is, rather, a total enhancement to the BASIC+ language. Indeed, we've come to call the many syntax improvements it provides Enhanced BASIC+.

You do not need the SRP Editor to invoke the SRP PreCompiler. You simply need to include the following line of code at the top of your program:

#pragma precomp SRP_PreCompiler

This one line instructs the OpenInsight compiler to pass your program to SRP_PreCompiler before passing it on the standard BASIC+ compiler, given the SRP PreCompiler to ability to trans-compile new language features into fully supported ones. These new features are:

Extended BASIC+ Syntax


For Each Loops

For each loops offer a convenient way to walk through a dynamic array from beginning to end. If you have an @FM delimited array, you can do this:

For Each Value in MyValues
    Total += Value
Next Value

You no longer have to keep track of nesting iterators, extract the current element, and worry about nasty inefficiencies. This represents a massive increase in productivity and readability.

Custom delimiters are supported via the using keyword.

For Each Value in MyValues using ","
    Total += Value
Next Value

You can safely nest For Each loops as well.

For Each Field in Array using @FM
    For Each Value in Field using @VM
        Total += Value
    Next Value
Next Field

Sometimes, we still need to know the position of the current element. In that case, you can use the setting keyword.

For Each Value in MyValues using @STM setting Pos
    NewValues<Pos> = Value
Next Value

Conditional Return Statement

The SRP PreCompiler extends the Return statement to make it easy to perform a sanity check on the return value. Most people are familiar with the following syntax:

If Assigned(Result) else Result = ""
Return Result

This code ensures we never accidentally return an unassigned variable. Now, with the help of SRP PreCompiler, we can do this in a single line:

Return Result or ""

Note that this syntax should only be used for the stored procedure's final Return statement, not for returning from a gosub.


Service Modules

SRP has been a big advocate for organizing code into Service Modules: stored procedures in which the first parameter identifies a service to execute. Normally, this involves branching to gosubs, mapping the input variables, and making sure the stored procedure has enough parameters to cover all the services' needs. Here is a traditional example:

Compile function Invoice_Services(Service, Param1, Param2, Param3, Param4, Param5)

Begin Case
    Case Service _EQC "GetCustomerAddress" ; GoSub GetCustomerAddress
    Case Service _EQC "AddSalesTax"        ; GoSub AddSalesTax
    Case 1
        // Service not handled
End Case

If Assigned(Response) else Response = ""
Return Response

GetCustomerAddress:
    AddressType = Param1
    Begin Case
        Case AddressType _EQC "Mailing"  ; Response = CustRec< MAILING_ADDRESS$ >
        Case AddressType _EQC "Shipping" ; Response = CustRec< SHIPPING_ADDRESS$ >
        Case AddressType _EQC "Plant"    ; Response = CustRec< PLANT_ADDRESS$ >
    End Case
return

AddSalesTax:
    Amount  = Param1
    Amount += (Amount * CustRec<TAX_RATE$>)
    Param1  = Amount
return

The SRP PreCompiler reduces much of the ceremony involved in this architecture.

First, we save time using parameter placeholders, like so:

Compile function Invoice_Services(@SERVICE, @PARAMS)

The @SERVICE parameter is required by the SRP PreCompiler to identify it as a service module. The @PARAMS placeholder negates the need to update the parameter list every time you add a new service. The SRP PreCompiler will take care of it for you.

Now, we can simply our branching logic from a long Case statement into a single one:

GoToService else
    // Service not handled
end

The GoToService keyword uses the @SERVICE parameter to determine what service to jump to. If the service does not exist, it gives you the chance to handle that via the else clause. If you don't want to handle it, you can certainly do it in one line.

GoToService

Now, we write our services. In the past, we did this using standard gosub labels. Now, we use the Service keyword and we list the parameters the service expects:

Service GetCustomerAddress(AddressType)
    Begin Case
        Case AddressType _EQC "Mailing"  ; Response = CustRec< MAILING_ADDRESS$ >
        Case AddressType _EQC "Shipping" ; Response = CustRec< SHIPPING_ADDRESS$ >
        Case AddressType _EQC "Plant"    ; Response = CustRec< PLANT_ADDRESS$ >
    End Case
End Service

Service AddSalesTax(Ref Amount)
    Amount += (Amount * CustRec<TAX_RATE$>)
End Service

Our services now look like tiny functions within the same stored procedure, complete with their own pass-by-value parameters. When the service is called, we can manipulate the parameters as we see fit without altering the original values passed into the service in the first place. This is good design as most callers expect their parameters to remain unchanged. In the rare case you need to pass back changes to a service parameter, like in the AddSalesTax example above, you can precede the parameter with the Ref keyword.

You will also notice the Options keyword. This is purely for documentation purposes and will prove 

Here is the final code of our service module. It is arguably much cleaner.

Compile function Invoice_Services(@SERVICE, @PARAMS)
#pragma precomp SRP_PreCompiler

GoToService

Return Response or ""

Service GetCustomerAddress(AddressType)
    Begin Case
        Case AddressType _EQC "Mailing"  ; Response = CustRec< MAILING_ADDRESS$ >
        Case AddressType _EQC "Shipping" ; Response = CustRec< SHIPPING_ADDRESS$ >
        Case AddressType _EQC "Plant"    ; Response = CustRec< PLANT_ADDRESS$ >
    End Case
End Service

Service AddSalesTax(Ref Amount)
    Amount  = Param1
    Amount += (Amount * CustRec<TAX_RATE$>)
    Param1  = Amount
End Service

The SRP PreCompiler also produces meta data about the service when you compile. This metadata can be used by tools such as the SRP Editor to give you coding hints. This makes you much more productive as you can get the information you need about each service as you program. 

One such feature, which only the SRP Editor takes advantage of, is options. Options allow you to document a limited set of values for a parameter. 

Options ADDRESSTYPES = "Mailing", "Shipping", "Plant"
Service GetCustomerAddress(AddressType=ADDRESSTYPE)
    Begin Case
        Case AddressType _EQC "Mailing"  ; Response = CustRec< MAILING_ADDRESS$ >
        Case AddressType _EQC "Shipping" ; Response = CustRec< SHIPPING_ADDRESS$ >
        Case AddressType _EQC "Plant"    ; Response = CustRec< PLANT_ADDRESS$ >
    End Case
End Service

The Options keyword lets you name a set of options, ADDRESSTYPES in this case, and then assign that name a comma delimited list of quoted or unquoted values. Then, you assign the options, by name, to any parameter that uses those options. In the above example, we assign ADDRESSTYPE options to the AddressType parameter.

Again, this has no effect on how your code functions, but if you are in the SRP Editor, those options will appear in a drop down as you type.

Event Modules

Commuter modules have been around for some time, and they are structured very similarly to services. Typically, gosubs are created for each event, and a large case statement uses the current control and event to branch to the correct handler. Here is a typical example:

Compile function MY_WINDOW_EVENTS(CtrlEntId, Event, Param1, Param2, Param3, Param4, Param5)

Window = @Window[1, "F*"]
Control = Field(CtrlEntId, ",", 2, 3)

// Branch to event handler
Begin Case

    Case Control EQ Window
        Begin Case
            Case Event EQ "CREATE"          ; GoSub WINDOW.CREATE
        End Case

    Case Control EQ "MY_EDITFIELD"
        Begin Case
            Case Event EQ "CHAR"            ; GoSub MY_EDITFIELD.CHAR
        End Case

    Case Event EQ "CLICK"
        Begin Case
            Case Control EQ "OK_BUTTON"     ; GoSub PUB_OK.CLICK
            Case Control EQ "CANCEL_BUTTON" ; GoSub PUB_CANCEL.CLICK
        End Case

    Case 1
        // Event not handled

End Case

// Return 1 by default so the event chain continues
If Assigned(EventFlow) else EventFlow = 1
Return EventFlow

WINDOW.CREATE:
    CreateParam = Param1
return

MY_EDITFIELD.CHAR:
    VirtCode = Param1
    ScanCode = Param2
    CtrlKey = Param3
    ShiftKey = Param4
    AltKey = Param5
return

OK_BUTTON.CLICK:
    EventFlow = 0
    End_Dialog(@Window, Data)
return

CANCEL_BUTTON.CLICK:
    Post_Event(@Window, "CLOSE")
return

The SRP PreCompiler can clean this up quite nicely. First, we use parameter placeholders in at the top.

Compile function MY_WINDOW_EVENTS(CtrlEntId, Event, @PARAMS)

This placeholder will allow us to add new event handlers at will without having to remember how many parameters are needed at the top.

Next, we replace the large case statement with a simple branching statement.

// Branch to event handler
GoToEvent Event for CtrlEntId else
    // Event not handled
end

The GoToEvent statement takes an event name and a control ID, and uses them to branch to the correct event handler. If the handler doesn't exist, then it jumps to the optional else clause. Omitting the else close is permitted.

// Branch to event handler
GoToEvent Event for CtrlEntId

Now, we can rewrite our event gosubs using the Event keyword and supplying named parameters.

Event WINDOW.CREATE(CreateParam)
End Event

Event MY_EDITFIELD.CHAR(VirtCode, ScanCode, CtrlKey, ShiftKey, AltKey)
End Event

Event OK_BUTTON.CLICK()
    EventFlow = 0
    End_Dialog(@Window, Data)
End Event

Event CANCEL_BUTTON.CLICK()
    Post_Event(@Window, "CLOSE")
End Event

With the SRP PreCompiler, we can rewrite the entire example as so:

Compile function MY_WINDOW_EVENTS(CtrlEntId, Event, Param1, Param2, Param3, Param4, Param5)
#pragma precomp SRP_PreCompiler

// Branch to event handler
GoToEvent Event for CtrlEntId else
    // Event not handled
end

// Return 1 by default so the event chain continues
Return EventFlow or 1

Event WINDOW.CREATE(CreateParam)
End Event

Event MY_EDITFIELD.CHAR(VirtCode, ScanCode, CtrlKey, ShiftKey, AltKey)
End Event

Event OK_BUTTON.CLICK()
    EventFlow = 0
    End_Dialog(@Window, Data)
End Event

Event CANCEL_BUTTON.CLICK()
    Post_Event(@Window, "CLOSE")
End Event

Once again, we've simplified the usually tedious task of keeping our commuter module up to date. When a new event handler is need, just add the handler and the SRP PreCompiler will take care of the rest.

If you use the SRP Editor, you can take advantage of another productivity feature: event autofill. To use it, add the following line at the top of your code:

#window MY_WINDOW_NAME

Replace MY_WINDOW_NAME with the window you want attached to your commuter module. Once you do this, you will get drop downs when writing event handlers.

eventsdropdown

Moreover, the SRP Editor will autofill the event's parameters, saving you valuable time.

eventautofill

Unit Test Modules

Unit Test Modules are stored procedures containing multiple related tests.

NOTE: Unit Test Modules are only really useful within the SRP Editor. When a Unit Test Module is compiled, metadata for tests are stored in the system and the tests appear within the SRP Editor UI. This allows you to run any number of unit tests and examine their results.

They have a similar syntax as Service Modules and Event Modules. Here's a sample:

Compile function Test_Costumers(@Test)
#pragma precomp SRP_PreCompiler

GotoTest

Return TestResult or 1

Test CalculateAge

	SampleCustomerId = "0001"
    Assert Customer_Services("GetAge", SampleCustomerId) equals 32

End Test

The main feature of the Unit Test Module is the Assert statement. It has the following syntax:

Assert <actual> equals <expected>

This statement is key to making unit tests work. When the evaluation fails, the SRP Editor will display the two values side by side for easy comparison.


Unpacking

IMPORTANT: Unpacking was added in 2.1.1 and is not supported in prior versions.

Unpacking lets you assign elements of a dynamic array into variables all on a single line! For example, if you use the SRP_Date Decode service, you'll get an @FM delimited array of information. If you wanted to pull the year, month, and day, you would traditionally need to do this:

Info = SRP_Date("Decode", SRP_Date("Today", 1))
Year = Info<1>
Month = Info<2>
Day = Info<3>

With unpacking, you can do this:

(Year, Month, Day) = SRP_Date("Decode", SRP_Date("Today", 1))

The unpacking syntax is activated when you surround your variables in parenthesis. Order matters. The first variable gets item <1>, the second gets item <2>, and so on. You can skip a position using the NULL keyword, an underscore, or just omitting. For example, if you wanted year and day, but not the month, any of these three formats will work:

(Year, NULL, Day) = SRP_Date("Decode", SRP_Date("Today", 1))
(Year, _, Day) = SRP_Date("Decode", SRP_Date("Today", 1))
(Year,,Day) = SRP_Date("Decode", SRP_Date("Today", 1))

If you need to unpack an array with a delimiter that is not @FM, then you add the USING keyword after the parenthesis:

(First, _, Last) using ',' = "John,Seymour,Doe"

This syntax is useful for smaller arrays when readability is important to you. This is not useful for parsing large records.

NOTE: You can only unpack into individual variables, not into other arrays or matrices.

Super Strings

IMPORTANT: Super strings were added in 2.2.11 and are not supported in prior versions.

Super strings offer some powerful string manipulation options. Super strings use accent marks, which means you can use single and double quotations in your string without concatentation:

Var = `"I'd be delighted," she said cheerfully.`

Super strings also support interpolation. Interpolating a variable is more compact form of concatenation: it's like weaving your variables into the string. If, for example, you need to use variables or system variables in a string, you can do this:

Var = `Hello, $Name. Your login is @USERNAME.`

In super strings, the $ and @ characters are reserved. The dollar sign means you are weaving in a variable or expression. In the above example, there is a variable call Name, and the value of that variable will be concatenated into the string at runtime. The at-symbol means you are weaving in a system variable. If you need these characters in your super string, just double them up, e.g., `$$5.00`.

Expressions are supported. To use them, you need use curly braces after the dollar sign, like so:

Var = `Today is ${SRP_Date("DayOfWeekName", SRP_DateTime("DayOfWeek", DateTime))}, it's ${SRP_DateTime("Format", DateTime, "HH:mmaa")} now.`

Important: Using angle brackets on a variable is an expression, so you must use curly braces:

Incorrect = `The book "$BookInfo<1>" was written by $BookInfo<2>.`
Correct = `The book "${BookInfo<1>}" was written by ${BookInfo<2>}.`

Supers strings are optimized to produce the fewest concatenations possible. For example, when you interpolate delimiters into your strings, concatenation is avoided:

// 8 concatenations
OldWay = "Monday":@FM:"Tuesday":@FM:"Wednesday":@FM:"Thursday":@FM:"Friday"

// No concatenations
NewWay = `Monday@FMTuesday@FMWednesday@FMThursday@FMFriday`

In the SRP Editor, interpolated variables are syntax highlight inside the strings to make readability much easier. But we're not done! Super strings also allow you to work with raw, multilined strings:

Var = ```
	<breakfast_menu>
		<food>
			<name>Belgian Waffles</name>
			<price>$5.95</price>
			<description>Two of our famous Belgian Waffles with plenty of real maple syrup</description>
			<calories>650</calories>
		</food>
		<food>
			<name>Strawberry Belgian Waffles</name>
			<price>$7.95</price>
			<description>Light Belgian waffles covered with strawberries and whipped cream</description>
			<calories>900</calories>
		</food>
	</breakfast_menu>
    ```

The above super string will compile into a single string, with line breaks and indentations. There are a couple rules to follow for the precompiler to properly understand your super string:

  1. Opener and closer must be at least 3 accent marks, and they must match. If you open with five accents, you must close with 5 accents.
  2. Opener and closer must be on their own lines. The string begins on the line after your opener and ends on the line just before your closer.
  3. Blank lines and line breaks are included in the string.
  4. Any whitespace that appears in front of your closer is ignore on all other lines. In the above example, there is one tab in front of the closer, so one tab will be ignore at the beginning of each line.
  5. You can comment out lines, but you must use the asterisk and it must be the first character of the line. (Using Ctrl+/ in the SRP Editor does this for you).

This can be useful for storing large amounts of data that you don't want stored in a record for someone to mess with. For example, the setup XML for the SRP Ribbon Control. The precompiler will account for string size limits and perform concatenation as needed. In the debugger, the whole string will behave like a single operation. Note, however, that object code in OpenInsight does have a size limit for literal data. In almost all circumstances, you don't have to worry about it, but if you include pages and pages of strings in a stored procedure that is very large, you could run into issues.

Multilined super strings also support interpolation, great for setting up some JSON:

Var = ```
    {
        "name" : "$Name",
        "day-of-week" : "${SRP_Date("DayOfWeekName", SRP_DateTime("DayOfWeek", DateTime))}",
        "time" : "${SRP_DateTime("Format", DateTime, "HH:mmaa")}"
    }
    ```

Lastly, you might want to see your string nicely formatted in your code, but you don't want all that whitespace at runtime. In that case, you can use the "-compress" argument:

New = ``` -compress
	{
		"CloseButtons" : true,
		"Tabs" : [
			{
				"TabCaption" : "${Title<1>}",
				"TabImage" : "${Icon<1>}"
			},
			{
				"TabCaption" : "${Title<2>}",
				"TabImage" : "${Icon<2>}"
			}
		]
	}
	```

The -compress argument will trim each line and strip all line breaks at compile time. In the above example, that's okay, because JSON doesn't need all that whitespace anyway.

Caveats

The SRP PreCompiler will work in any BASIC+ editor, but there is an important caveat to using Enhanced BASIC+ in anything other than the SRP Editor. The SRP Editor recognizes the new syntax and highlights code accordingly whereas the builtin OI Editor will not. Everything will still compile and run, but it will a little harder to read. Moreover, some features of the PreCompiler, such as the metadata, won't impede your code, but you also won't get the most out of your development outside of the SRP Editor.

Another issue with the SRP PreCompiler is that precompiling doesn't produce errors. Thus, if you make an error in the new syntax, the resulting error can be confusing.

Lastly, if you are using SRP PreCompiler, it's best to avoid using variable names that match the keywords Service, Test, and Event. The SRP Precompiler will do it's best to avoid confusing variables with keywords, but it's not bulletproof. It's better to avoid using them at all if you can help it.


  • No labels