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.
|