ZScript Technical FAQ

From ZCWiki
Jump to navigationJump to search

Q: What do the various compiler errors mean?

A: Here are the errors that might arise during script compilation, along with a more detailed explanation of the problem:

Errors During Parsing or Preprocessing

P00: Can't open of parse input file!

The compiler creates and uses a temporary file during its operation, so you may get this error if ZQ cannot create a file with write permission in its working directory.

More likely, though, you have made a typo in your ZScript source which is tripping up the lexer or parser. The lexing and parsing error handling is more primitive than the semantic checking, as I am using the flex and bison error handling "out of the box", but you should still be provided with a line number and context of the error, with the parser informing you of the kind of token it found, and what it expected instead. This list of expected tokens is however not all-inclusive.

Keep in mind that ZScript is a modified SUBSET of the C language, and some features are not yet (or never will be) implemented.

P01: Failure to parse imported file foo.

As in P00, but the problem is in an imported file, and not the main source.

P02: Recursion limit of x hit while preprocessing. Perhaps you have circular imports?

Suppose you have the following script files


import "b.z"


import "a.z"

and you try to compile a.z. The compiler will open b.z and attempt to recursively compile it. But b.z tries to import a.z, so the compiler enters an infinite loop of opening a.z, then b.z, then a.z, etc. To protect against this compilation halts if import recursion depth reaches a certain threshold.

Future versions of ZQ may have better import handling, which may eliminate this problem. For now, user care is the only solution.

Errors During Symbol Table Construction

S04: Function foo was already declared with that type signature.

You tried to declare two functions in the same scope with the same name and type signature. Note that return type is NOT enough to distinguish otherwise identical function declarations; recall also that int and float are the same type internally, so if two function type signatures are identical except that one has a parameter of type float where the other has the same parameter of type int, you will have a conflict.

There are two additional subtle situations where you might get a conflict:

  1. A function declared at file scope might conflict with a function that's implicitly added to file scope by the preprocessor because of an import statement.
  1. A function might conflict with one already reserved by the ZScript standard library. All current and future ZScript library functions start with a capital letter, and so starting user functions in lowercase might be a prudent convention.

S05: Function parameter foo cannot have void type.

It makes no sense to pass void parameters, and so declaring such is illegal.

S06: Duplicate script with name foo already exists.

Script names must be wholly unique; if there's already an ffc script named FOO you cannot create an item script so named.

Remember also that scripts might be imported by the preprocessor.

S07: Variable foo can't have type void.

Self-explanatory, and common sense.

S08: There is already a variable with name foo defined in this scope.

Since variables cannot be declared at file scope, and the ZScript library does not reserve any variables, this error should rarely cause confusion. Remeber that, as in C, you can use braces ({}) to introduce an extra scope, as in the following snippet:

int x;
  int x; //legal

S09: Variable foo is undeclared.

A variable with the give name could not be found in the current or any enclosing scope. Keep in mind the following subtleties:

  1. To access a different script's global variables, or to access a global variable from within a function delcared at file scope, you must use the dot operator: scriptname.varname.
  1. To access the data members of the ffc or item associated with a script, you must use the this pointer: this->varname.
  1. ZScript uses C++-style for loop scoping rules, so variables declared in the header part of the for loop cannot be accessed outside the loop:
for(int i=0; i<5;i++);
i = 2; //NOT legal

S10: Function foo is undeclared.

As S09, but with functions instead of variables. Points 1 and 2 of S09 apply here as well.

S11: Script foo must implement void run().

Every script must implement a function named run, with some type signature, and return type void. This is the method that's automatically invoked when a script is run by the ZC engine.

'S12: Script foos run() must have return type void.

Every script's run method must have void return type. Self-explanatory.

S26: Pointer types (ffc, etc) cannot be declared as global variables.

It is illegal to declare a global variable (that is, a variable in script scope) or any type other than int, float, and bool. Why? Recall that global variables are permanent; they persist from frame to frame, screen to screen. Pointer types, on the other hand, reference ffcs or items that are transitory; they become stale after a single WAITFRAME, so it makes no sense to try to store them for the long term.

S30: Script foo may have only one run method.

Your run method may have any type signature, but because of this flexibility, the compiler cannot determine which run script it the "real" entry point of the script if multiple run methods are declared.

S32: Script foo is of illegal type.

There is no such thing as an itemclass script or bool script. Don't even try.

Errors During Type-Checking

T16: Cast from foo to bar.

The only "safe" implicit casts are from int to float and vice-versa (since they are the same type), and from int (or float) to bool (0 becomes false, anything else true). The results of any other kind of cast are unspecified.

Explicit casts are unimplemented and unnecessary.

T17: Cannot cast from foo to bar.

As with T16, but in this case the cast is expressly forbidden.

T19: Constant division by zero.

While constant-folding, the compiler has detected a division by zero.

Note that not only division by a CONSTANT zero can be detected at compile time. The following code, for instance, compiles with no errors but will most likely still crash ZC:

int x=0;

T20: Truncation of constant x.

Both floats and ints can only be specified to four places of decimal precision. Any extra digits are simply ignored by the compiler, which then issues this warning.

T21: Could not match type signature foo.

The compiler can determine no way of casting the parameters of a function call so that they match the type signature of a declared function. For instance, in the following snippet

int x = Sin(true);

since true cannot be cast to a float, this function call cannot be matched to the library function Sin, triggering this error.

T22: Two or more functions match type signature foo.

If there is ambiguity as to which function declaration a function call should matched to, the compiler will attempt to determine the "best fit." These measures are however occasionally still not enough. Consider for instance the following snippet:

void foo(int x, bool y) {}
void foo(bool x, int y) {}

matching either of the two foo declarations requires one cast, so no match can be made. In contrast, the following code has no error:

void foo(int x, bool y) {}
void foo(bool x, bool y) {}

(The first foo function is matched.)

T23: This function must return a value.

All return statements must be followed by an expression if the enclosing function returns a value. Note that the compiler does NOT attempt to verify that all executions paths of a function with non-void return type actually returns a value; if you fail to return a value, the return value is undefined. For instace, the following is a legal (though incorrect) ZScript program:

int foo() {}

L24: Too many global variables.

The assembly language to which ZScript is compiled has a built-in maximum of 256 global variables. ZScript can't do anything if you exceed this limit.

T25: Constant bitshift by noninteger amount; truncating to nearest integer.

Self-explanatory; the bitshift operators (<< and >>) require that their second parameters be whole integers.

T27: Left of the arrow (->) operator must be a pointer type (ffc, etc).

It makes no sense to write "x->foo" if x is of type int. You may only use the dereference (->) operator on pointer types.

T28: That pointer type does not have a function foo.

You tried to use the dereference operator (->) to call a function that does not exist for the pointer type being dereferenced. Check the list of member variables and functions and verify you have not mistyped the function you are trying to call.

T29: That pointer type does not have a variable foo.

As above, but the member variable you tried to access does not exist.

Errors During Intermediate Code Generation

G33: Break must lie inside of an enclosing for or while loop.

As in C, the break keyword aborts the innermost for or while loop being executed; as such it makes no sense to break if an enclosing for or while loop does not exist.

G34: Continue must lie inside of an enclosing for or while loop.

As the above error, but with the continue keyword. Continue aborts the current iteration of the loop and skips to the next iteration. Any other errors you receive (in particular anything marked as an "Internal Error" is indicative of a bug in the compiler, and should be reported promptly.

Q: Assembly only had floats. What is the difference between the types "int" and "float"? What about "bool"?

A: There is no difference whatsoever between float and int; both are synonymous and provided as a convenience. Functions that take ints happily accept floats, and vice-versa. Moreover, unlike C, there is no implicit truncation of ints; in the following snippet

int x = 1/2;

the value of x becomes 0.5, not 0.

Bools are represented internally as in C: 0 is false, anything else represents true. However, like in C++, bools and ints are NOT interchangeable; you can't add two bools, and should not assign a bool to a variable of type int. However, ints are safely and implicitly cast as bools.

Q: What should I know about global variables?

A: Global variables are variables declared at script scope; you cannot declare file-scope variables. For instance, int the following snippet, x is a global variable:

ffc script foo {
   int a;
   void run() {
      int b;

Some notes on global variables:

  • Global variables are implicitly stored in the GD registers and are thus persistent: they are saved with the quest.
  • A global variable is visible "as-is" to all methods and other global variable declarations within the same enclosing script; global variables are accessible from other scripts, or from global functions, using the dot operator: in the above example, a can be accessed using "foo.a" by other scripts.
  • Only simple types (int, float, bool) are allowed to be stored in global variables. Pointer types become stale after every frame, and so cannot be safely used globally.
  • Global variables are initialized once, when a player first begins a new quest. Within a script, they are guaranteed to be initialized in declaration order. It is unspecified which of two scripts' global variables are initialized first.

Q: Pointers scare me. How do pointers work in ZScript?

A: Pointers in ZScript are a lot "tamer" than raw C pointers. The ZScript pointer types are ffc, item, itemclass, link, game, and screen; you can only declare new pointers of the first three types.

Each pointer type has various data members and functions that can be accessed using the -> operator, listed elsewhere. Using pointer types is not much more difficult than using simple types, with the exception of two subtleties:

1. Pointer must point to something, or disaster will strike. Consider the following code:

item i;
i->X = 10;

a new item pointer, i, is declared, then immediately dereferenced. The problem with that is that i doesn't refer to anything, so who knows what item will happen to be moved by the above code: the behavior here is unspecified, and may even crash ZC. To avoid problems with these "dangling" pointers, always declare pointers as pointing to something, such as

item i = CreateItem(3);

or even, if the code is enclosed in an item script,

item i = this;
i->X = 10;

2. Pointers become stale after every Waitframe(). Suppose you have the following code:

item i = CreateItem(3);
i->X = 10;

This code works fine most of the time, but occasionally crashes ZC. Why? Well, what happens if Link picks up the item during the frame break? The item ceases to exist, and i becomes "dangling." To avoid this pitfall, NEVER count on pointers persisting between frame breaks. There are two exceptions. The "this" pointer in ffc scripts will stick around safely until the ffc script Quit()s. The other exception are the "global" pointers: Link, Screen, and Game. You can't read or assign to them, and they remain safe throughout the entirety of the quest.

Pop quiz: why is it unsafe to call Waitframe() from within an item scripts? (Hint: the "this" pointer.)

Q: Help! My global variables are not being initialized correctly, or ZC crashes on quest start!

There are two likely causes for this "bug." Both are caused by misunderstanding how global variables are initialized.

When you start a new quest, then, and only then, are the global variables initialized, and the order in which they are initialized is only partially specified, as described here.

Consider the following code:

ffc script foo {
   void run() {}
   int a = 0;
   int b = a+10;
item script bar {
   int c = a+1;

What's wrong here? The foo script is perfectly fine; a is guaranteed to be initialized before b, so b will be correctly initialized to 10. However, the problem occurs in script bar: even though it comes later in the file than foo's declaration, it is NOT guaranteed that c will be intialized after a, so there's no telling what value c will assume.

The other possible problem is illustrated in the following snippet:

item script foo {
   void run() {}
   int a = this->X;

Why is this snippet buggy? Well, recall that a is initialized immediately after quest start, so foo's "this" pointer is not yet pointing to anything meaningful. To correct the code, move the initialization of a inside the run method:

item script foo {
   void run() {a=this->X;}
   int a;

Q: Ack! I'm trying to do something to all items each frame, but ZC acts weird or crashes!

A: Great care must be taken whenever you invoke Waitframe() around pointers. As mentioned [URL="http://www.armageddongames.net/showpost.php?p=1090095&postcount=5"]here[/URL], pointers become stale after each frame, and moreoever there's occasionally even greater subtlety. Consider for instance the following snippet:

int numitems = Screen->NumItems();
   for(int i=0; i<numitems; i++)
       item it = Screen->LoadItem(i);

Can you spot the problem? Even though I'm refreshing the item pointers each frame, I'm NOT refreshing the number of items on the screen, so if the number of items decreases (say Link picks one up), one of the LoadItem() calls will return a dangling pointer. The fix is simple; just be sure to recompute both the pointers and the number of items each frame:

   for(int i=0; i<Screen->NumItems(); i++)
       item it = Screen->LoadItem(i);

Q: Can I mix ASM and ZScript scripts?

A: Yes, if you're very, very careful.

In general adding ASM scripts in slots unused by ZScript is safe. Just realize that if you write to global or screen registers, you may be writing over values used by some of the ZScript scripts.

After every compilation the full ASM output of the compiler is written to allegro.log, so by inspecting this output you can determine what global variables are being used by ZScript, so that you can avoid (or not avoid!) using them in your ASM scripts.

On the other hand, directly modifying the ASM output of a ZScript compilation requires very, very great care, for several reasons:

  1. Almost all of the D registers (currently D0-D6) are reserved as important scratch spaces for things like computing the stack frame offset of local variables, and should not be overwritten by foreign code.
  2. ZScript-compiled code is heavily reliant on the stack for storing everything from local variables to parameters of function calls to this pointers. The state of the stack must thus also be preserved by foreign code.
  3. Changing the line numbers of ZScript code requires a lot of work verifying that references to those line numbers (in things like GOTO) are modified accordingly. Even more tricky is that line numbers are sometimes indirectly stored in registers, pushed onto the stack, then popped off much later and used as return values via GOTOR.
  4. If you ever decide to modify the ZScript code and recompile, you'll of course need to reapply your "patch."

Q: What is the entry point of a ZScript? How do I pass parameters in?

All scripts must implement a method called "run" which return void. This is the script's entry point, and is called automatically when a script it to be executed.

The run method can have any type signature. When assigning a script to an item or FFC in ZQuest, you have the option of specifying starting values for D0, D1, etc; these starting values are passed into any parameters of run(), in order. For instance, in the snippet

ffc script foo {
   void run(int a) {}

a will be set to the intial value specified for D0 when run is called. If you have more parameters, as in, say

ffc script foo {
   void run(int a, int b, int c) {}

then a, b, c will be set to the initial values of D0, D1, and D2, respectively.

The type signature of run() can be ANYTHING, so it is possible to pass in non-integer types like bool or even item and ffc. However, declaring a run() with non-simple parameter types is highly unsafe and discouraged.