» Home

  » Developer Guide


Brass SSE Developer Guide - Tutorials

 

Tutorial 7 - Variable Typing and Shiny Debugging

Now that you're a proficient Shiny coder (wow, that was fast!) it's time to learn a little about the internals of the language. We're going to start with a brand new project so it's easy to see what's going on when we debug, our game has already grown to 200 lines!

Download the Source for this Tutorial Here

 

Variable Typing

At the end of the last tutorial we ran into a bit of a puzzle. Even though we declared a function as returning a string, Shiny allowed us to return an integer. Let's see how that worked.

Shiny was designed to be easy and powerful. C/C++ is very powerful but a bit clumsy, and switching between variable types can be a pain. Visual Basic is not as powerful but it's very easy to use, and allows switching between certain variable types with ease.

Shiny combines the best of both worlds. The compiler will convert between the standard variable types "on the fly", with no additional work from you. This means you can assign basic variables to each other without worrying about type. Here's a simple example:

int Year = 2006;
string YearString = "The Year is " + Year;

 

Just like Visual Basic, Shiny will automatically convert the Year integer variable to a string and concatenate it on to the end of the "real" string. If you displayed the string above, you'd see:

The Year is 2006

 

It's important to know how Shiny will behave when using different types together (recap: the basic variable types were introduced in tutorial 3).

integer to string Converts integer to a string representation
string MyString = 99; MyString equals "99"

boolean to string Converts boolean to a string containing "True" or "False"
string MyString = True; MyString equals "True"

double to string Converts double to a string representation
string MyString = 3.1415; MyString equals "3.1415"

dword to string Converts dword to a string representation
string MyString = 0xABCD1234; MyString equals "0xABCD1234"



string to integer Converts string representation of a number to an integer
int MyInt = "99"; MyInt equals 99

boolean to integer Converts boolean True to integer 1, False to integer 0
int MyInt = True; MyInt equals 1

double to integer Floors the fractional component of a double
int MyInt = 3.1415; MyInt equals 3

dword to integer Converts dword to a signed integer
int MyInt = 0xABCD1234; MyInt equals 2882343476

 

string to bool If the string is "true", "True", or "TRUE" the bool is set to True, otherwise the bool is set to False.
bool MyBool = "True"; MyBool equals True

integer to bool If the integer is greater than zero the bool is set to True, otherwise the bool is set to False.
bool MyBool = 99; MyBool equals True

double to bool If the double is greater than zero the bool is set to True, otherwise the bool is set to False.
bool MyBool = 3.1415; MyBool equals True

dword to bool If the dword is equal to 0x00000000 the bool is set to False, otherwise the bool is set to True.
bool MyBool = 0xABCD1234; MyBool equals True

 

string to dword Converts string representation of a dword to a dword
dword MyDword = "0xABCD1234"; MyDword equals 0xABCD1234

integer to dword Converts integer to a dword (base16/hexadecimal)
dword MyDword = 99; MyDword equals 0x00000063

bool to dword If the bool is True, the dword is set to 0xffffffff, otherwise the dword is set to 0x00000000
dword MyDword = False; MyDword equals 0x00000000

double to dword Floors the double and converts it to a dword (base 16/hex)
dword MyDword = 3.1415; MyDword equals 0x00000003

 

Implicit Conversion In Action

Implicit conversion happens automatically (that's why it's implicit!) wherever you use a basic type variable. For example, passing a string to a function that is expecting an integer will automatically generate a string-to-integer conversion. This is why the function return statement at the end of the last tutorial didn't generate a compiler error - implicit type conversion made sure the integer was converted to a string before being returned.

 

The advanced types cannot be implicitly type-converted to each other or to basic types. This is a logical limitation, as there's no sensible way to convert a file to a dword, or a website to a boolean :). If you try to use implicit conversion with the advanced types the compiler will generate an error. For example:

pen MyPen;
string MyStr = MyPen;

will generate:

(Line 2) =: Assignment to non-similar type

 

SSEdit: Under The Hood

Sometimes it's useful to understand exactly what Shiny is doing under the hood. A nice compiler message is great most of the time, but there's always that one time when it would really help to see what's going on.

SSEdit gives you unprecedented access to the internals of the Shiny language. No other compiler breaks down the code and compilation steps to let you see exactly what's going on.

There are 3 important features of SSEdit you can take advantage of. We'll look at them one by one in the context of this little program:

function Init()
{
    string MyStr;
    int MyInt;

    MyStr = GenerateNumber();
    MyInt = MyStr;

    MyStr = "The number is: " + MyInt;
}

int function GenerateNumber()
{
    return 99;
}

There's a link to download it at the top of the tutorial, or just copy and paste it into a new SSEdit document.

 

The Tokens and Tables Window

The Shiny compiler breaks down your source code into tokens and 3 tables.

  • Tokens are the individual bits of your code recognised by the compiler as either keywords (createimage, drawtext etc) or symbols (variable names, function names etc).

  • The Symbol Table is a list of all the variable names and values ("constants") you use in your code. For example, in the statement int MyInt = 1; both MyInt and 1 would be in the Symbol Table.

  • The Function Table is a list of all the functions (including handlers) in your code

  • The Import Table is a list of all the functions and extensions imported from DLL's. This is something we will cover in a later tutorial.

 

These tables are only filled out when your code is compiled, so open the sample code for this tutorial and compile it as normal. Don't execute it.

On the SSEdit toolbar you'll see 3 icons to the right of the compile and execute icons, circled below:

 

The left icon of these 3 toggles the "Tokens and Tables" window. Click it, or select "View Tokens and Tables" from the Build menu. If you've compiled your code you'll see something like this (if not, the window will be empty):

 

As you can see, the individual components of the sample code have been broken up and displayed in each window. Some of the items visible in this window don't appear to relate to any code (for example, the _ZERO_ symbol) - this is normal, the compiler allocates some reserved symbols in some instances.

This window will be useful to you if you get compiler errors about undeclared variables or undefined functions. It's also a one-stop shop to see what functions your code imports and what extensions it uses. The reason this window is so useful is because it tells you what the compiler sees, not what's in your code. So not only can it help find bugs in your code, it can flag up bugs in the compilation process (if you do find a bug in the compiler please let us know!).

 

The Syntax Tree

As part of the compilation process the Shiny compiler has to understand what you want your code to do. For example, any programmer will know that this statement:

int a = (3 * 2) + 1;

means "multiply 3 by 2, then add 1 to the result, then assign variable a to the overall result". As far as the compiler knows, when it sees this line of code all it can think is, "a-big-long-string-of-symbols-that-does-something". Seriously.

To do anything useful with this code the compiler must break it down into single instructions and order it correctly. The order is very important, because if the code is compiled as "add 1 to 2, then multiply the result by 3, then assign the result to a" you'll get a very different value in a at the end of it!

This is why Shiny is much faster and more efficient than scripting languages such as Javascript. The reason applications that use Javascript are called "interpreters" is because they have to perform this interpretation of the code as the script runs. Because Shiny performs all this work beforehand as part of the compiling process the end result is a faster, more powerful plugin.

When the compiler breaks down your code into single instructions it creates something called a "Syntax Tree". As the name implies, it's a tree containing the entire syntax of your code, with the first instruction at the top of the tree.

To access the syntax tree, click the middle icon of the 3 circled, or select "View Syntax Tree" from the Build menu.

 

It's much easier to understand when you see it, so let's take a look at the syntax tree for the sample code:

There's a lot of it, even for a small program, so don't worry about trying to absorb it all at once. Just take a small section, read through it and try to understand the process. Here's an excerpt of how line 13 breaks down

 

Each node in the syntax tree is associated with a line number in the script (shown in brackets on the left), and there is one node per instruction.

You can use this tree to help you track down bugs. For example, if MyStr wasn't being assigned the value that GenerateNumber() returns, you could look in the tree and make sure that the appropriate implicit conversion was being made (in this case, int2str as shown).

The syntax tree also shows variable scope (here you can see scope2::MyStr, global variables do not have a scopeN:: prefix), which is a good way to make sure the right variable is being used in context. The variables and constants used relate directly to the symbol table in the Tokens and Tables windows, so it's normal to use both windows at the same time to cross check each other.

The path of execution is in a top down system, so the topmost node will be executed, then all it's children, then the next node etc. Here is the path of execution, numbered in order with 1 being the first instruction executed:

 |----(line 13) expression statement
1     |----(line 13) assign
2          |----(line 13) store value in var:
3               |----(line 13) null offset:
4     |----(line 13) int2str conversion
5          |----(line 13) function GenerateNumber

 

Knowing how the syntax tree works is a very useful way of tracking down bugs and making sure your code is being executed in the right order.

 

The Bytecode Viewer

Although it's unlikely you'll need to use this window it's available anyway. Once the compiler has created the syntax tree it then converts it into "virtual assembly". Explaining assembly language is way, way outside the scope of this tutorial so if you're not sure what this is about you can safely skip this section.

To view the bytecode, click the right hand icon of the 3 circled, or select "View Bytecode" from the Build menu.

 

We won't bother looking at the bytecode in detail - it's self-explanatory if you're used to looking at this kind of thing, and if you're not then it won't help you anyway :)

 

Runtime Debugging

The Tokens and Tables, Syntax Tree and Bytecode Viewer were primarily created to help with the development of the Shiny language. They're very useful for debugging but not too intuitive or immediate. If you've got a logic problem in your code it's much easier to debug it while your code is running.

At the moment Brass only supports very basic debugging via a Virtual Machine debug window. A proper run-time debugger is in the works, but for now the debug window will help you a lot.

So far you've seen the SSEdit and Brass integration working when clicking the execute button on the SSEdit toolbar. Your compiled SSE is packed up and sent to Brass, which then automatically loads it for testing. We can go one step further here!

Load Brass, select "Configuration" from the Options menu and click on the "SSE Development" tab.

 

This is where you turn on the Virtual Machine debug window. Select the middle radio button ("Show VM Output only when an SSE is deployed by SSEdit"), tick the "Automatically close the VM output window" checkbox and click OK.

Go back to SSEdit and execute the plugin.

Click the "Compile" icon ( ), then click the "Execute" icon ( )

 

SSEdit minimizes itself, Brass minimizes itself and the blank plugin window appears. Alongside it a new window will pop up:

 

This is the Virtual Machine debug window. Take a look at the 2 lines of text with timestamps. Now switch back to SSEdit.

At the bottom of the Init handler you'll see 2 lines of code:

print MyStr;
print "Init has finished";

 

The print statement allows you to output anything you want to the debug window. If you're trying to track down a bug in a loop, or if you want to find out exactly what data is in a string (for example, you're parsing a website HTML file) you can just use the print statement to output the data you want to analyze. It's legal to use any standard Shiny expression with print, for example:

print "The string is " + MyString ": value " + MyInt;

Using the return value of function calls is also legal.

 

Array Boundary Handling

In tutorial 5 we covered what happens when you try to access an invalid array element (the last element in the array is returned instead). The VM will warn you when this happens via the debug window. You'll see a message like this:

The full message says "Warning: array boundary overflow, last element returned instead".

 

 

A Quick Recap

You've had a complete tour of the way implicit type conversion works, and now you know how to use some of the debugging and analysis features of SSEdit and Brass.

In this tutorial you saw:

  • How implicit type conversion works
  • How to use the Tokens and Tables, Syntax Tree and Bytecode windows
  • How to use the Brass Virtual Machine debug window

The next tutorial will introduce the last advanced concepts of Shiny, including how to access win32api functions directly from the language, and how Shiny Extensions work.