home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
OS/2 Shareware BBS: 10 Tools
/
10-Tools.zip
/
nqc_10b1.zip
/
nqcc.txt
< prev
next >
Wrap
Text File
|
1998-11-15
|
16KB
|
484 lines
Not Quite C Compiler
version 1.0 b1
Introduction
NQC is a simple language for programming the LEGO RCX. The preprocessor and
control structures of NQC are very similar to C. NQC is not a general purpose language -
there are many restrictions that originate from actual limitations and lack of detailed
information about the RCX itself.
A NQC program consists of tasks and subroutines. The first task is started automatically
when the program is run, the remaining tasks can be started by the program. Subroutines
can be shared across all of the tasks.
The compiler itself understands very little of the RCX. Most of the RCX functionality
(such as playing a sound, reading an input sensor, etc) is added by the rcx.nqh include.
This file is included automatically by the compiler (unless -s is specified on the command
line). The file itself defines many high level macros (such as PlaySound()) that are
implemented in terms of low level primitives within the compiler itself. This approach
means that new "system" features can be easily added to rcx.nqh without modifying the
compiler or introducing new keywords.
Usage
The nqcc command line useage looks like this:
nqcc [-s] [-d] [-l] [-e] [-o output_file] [-Dsym=[def]]
[-Usym] source_file
The default behavior is to compile the specified source_file to an output file that is named
by replacing the ".nqc" extension of the source filename with ".rcx". If the source
filename does not end with ".nqc", the default output filename is the source filename with
".rcx" appended to it. The "-o" option may be used to specify an alternate name for the
output file.
The source file name "-" causes nqcc to read stdin as the source file. In this case, the -o
option must be used to explicitly name an output file if one is desired.
The "-s" option prevents the compiler for automatically including "rcx.nqh" before
compiling the source file.
The "-d" option causes nqcc to atttempt downloading the program to an RCX after
compiling it. If an output file was not explicitly named ("-o" option), then no output file
is generated.
The "-l" option writes a bytecode listing to stdout.
The "-e" options sends compiler error messages to stdout instead of stderr. This option is
only needed when running nqcc under Win95, which does not support redirection of
stderr from the standard shell (command.com). The following examples show how to
redirect stderr (to a file called errors) under other platforms:
MPW: nqcc test.nqc │errors
WinNT: nqcc test.nqc 2>errors
The -Dsym[=def] option defines preprocessor symbol sym to the value def.
The -U option undefines preprocessor symbol def.
Two environment variables are used by nqcc:
NQCC_INCLUDE - this environment variable can be used to specifiy a directory to
search for include files in addition to the current working directory. When building
filenames, nqcc simply concatentates the value of NQCC_INCLUDE with the target
filename, so NQCC_INCLUDE must end in the appropriate directory separator ('\' for
PC, ':' for Mac, '/' for Unix).
RCX_PORT - this environment variable can be used to specify a serial port other than the
default (COM1 for PC, Modem for Mac). The value of RCX_PORT should be the name
of the port (for example "COM2" for the PC, or "b" for the Mac's printer port).
Preprocessor
The preprocessor implements the following directives: #include, #define, #ifdef, #ifndef,
#if, #elif, #else, #endif, #undef.
The #include command works as expected, with the caveat that the filename must be
enclosed in double quotes:
#include "foo.nqh"
The #define command is used for simple macro substitution. Redefinition of a macro is
an error (unlike in C where it is a warning). Macros are normally terminated by the end
of the line, but the newline may be escaped to allow multi-line macros:
#define foo(x) do { bar(x); \
baz(x); } while(false)
Conditional compilation works similar to the C preprocessor. Expressions in #if
directives use the same operators and precedence as in C.
C and C++ style comments are supported
/* this is
a comment */
// another comment
Tasks and Subroutines
Each program is made up of tasks and subroutines, each of which is composed of
statements. Each program must have at least one task (named "main"), which serves as
the entry point of the program.
A task looks like this:
task task_name
{
statements
}
Tasks can be started or stopped with the following statements
start task_name;
stop task_name;
Subroutines look very similar to tasks, but use the sub keyword. Note that there are no
parameters or return values for subroutines. The only variables available are global
variables.
sub sub_name
{
statements
}
Subroutines are called just like in C, with the caveat that there are not parameters or
return value:
sub_name();
Due to a limitation in the RCX firmware, subroutines are not allowed to make subroutine
calls (either to themselves or another subroutine).
Statements are contained within tasks and subroutines. Like C, the semicolon (';') is used
as a statement terminator. Groups of statements can be combined into a a block using
braces ('{' and '}').
Values
Many statements (such as variable assignment, or conditional testing) make use of
"values". A value can be many things: a constant, the contents of a global variable, a
random number, the reading from an input sensor, etc. Each of these is covered in more
detail below. In addition, some of the high level commands may take constants and/or
values as arguments. In the description of a command its arguments will be noted as
either "const" or "value". "const" arguments must be a numeric constant. "value"
arguments may be either a numeric constant or another sort of value (such as a variable).
Note that not all value types are legal for all commands that use values.
Numeric constants can be written as either decimal (e.g. 123) or hexidecimal (e.g.
0xABC). Presently, there is very little range checking on constants, so using a value
larger than expected can have unpredictable results. Constants can be combined using the
standard arithmetic operators (+, -, *, /, %, <<, >>, &, |, ^, ~). Precedence and
associativity are the same as C. Parentheses can be used to group lower precedence
operations together.
The RCX provides 32 global variables. In order to use variables in your program, you
must first declare them:
int foo;
All of the variables are global (even across multiple programs), hence they must be
decalred outside the scope of any tasks and subroutines. Variables can be assigned values
using the following assigment operators:
var = value;
var += value;
var -= value;
var *= value;
var /= value;
Note that general expressions (e.g. a = b + c) are only supported for compile time
constants and not for runtime evaluation:
int foo,bar;
task main
{
foo = 2 + 3; // legal
foo = bar + 2; // illegal
}
Other value types inclue:
Input(n) - current value of sensor 'n', where n=0-2
Timer(n) - current value of timer 'n', where n=0-2
Random(n) - random number between 0 and n-1? (not sure about the range)
Message() - value of the last received IR message
Watch() - value of the system clock
Note: the rxc.nqh file defines macros IN_1, IN_2, and IN_3 as Input(0), Input(1), and
Input(2), respectively.
Control Structures
Conditions can be either a comparison beteen two values, or one of the predefined
constants "true" and "false". There are six relations (<, >, <=, >=, ==, !=). Conditions
may be combined using && and ||, or negated with '!".
Here are the control structures:
if (condition) statement
if (condition) statement else statement
while(condition) statement
do statement while(condition)
repeat(value) statement
wait (condition) statement
The first four structures are identical to their counterparts in C.
The "repeat" structure causes a statement to be executed a number of times. This number
does not have to be a constant, for example it could be the value of a global variable,
however the number is only evaluated prior to starting the loop (as opposed to the "for"
structure in C which evaluates the condition before each iteration). The RCX allows
"repeat" loops to be nested (maintaining its own "stack" of look counters). The "break"
statement is also legal within repeat loops, although it has a rather unexpected result
when used within a nested loop. Although the control flow will be broken, there is no
way to pop the old counter off the internal stack, so the enclosing loop will be using the
wrong counter when it starts repeating. This cannot be reliably checked at compile time
since the internal repeat stack is shared across subroutine calls.
The "wait" structure executes the body until the condition becomes true. It is actually
implemented as a macro (so be careful of side effects). It it handy when waiting for
something to happen:
wait(IN_3 == 1); // wait for sensor 3 to have value 1
Several other control flow statements are supported:
break;
continue;
return;
"break" and "continue" can be used within "do" and "while" loops.
"return" can be used to exit a subroutine before getting to its end.
Sensors
The constants IN_1, IN_2, and IN_3 can be used to either read an input sensor, or to
specify a sensor. Sensor types are defined by IN_SWITCH, IN_LIGHT, IN_ANGLE,
IN_PULSE, and IN_EDGE. The Sensor() function is used to initialize the sensor:
Sensor(value sensor, const type);
Some sensor types (such as the IN_ANGLE type) allow you to "reset" the sensors
internal counter with the following command:
ClearSensor(value sensor);
The only supported value types for "sensor" in the above two examples are numeric
constants and Input(n) - all other types have undefined behavior.
Here's a brief example that configures sensor 1 as a switch, then waits for it to be pressed
before playing a sound.
Sensor(IN_1, IN_SWITCH);
wait (IN_1 == 1);
PlaySound(0);
The macros IN_1, IN_2, and IN_3 are equivalent to Input(0), Input(1), and Input(2) and
can be used anywhere that a runtime value is legal (such as setting a variable, checking a
condition, etc).
Motors
The constants OUT_A, OUT_B, and OUT_C refer to the three outputs. They can be
combined (using + or |) in order to specify multiple outputs. OUT_FULL is a constants
that represents full speed for a motor. Here's how to turn on all three motors for 1
second:
Fwd(OUT_A + OUT_B + OUT_C, OUT_FULL);
Sleep(100);
Off(OUT_A + OUT_B + OUT_C);
These commands control motors:
Fwd(const outs, value speed) // turn motor(s) on forwards
Rev(const outs, value speed) // turn motor(s) on backwards
Off(const outs) // stop motor(s)
Other Commands
PlaySound(const n); // play system sound #n
PlayNote(const freq, const duration); // play a note
Sleep(value v) // sleep for v ticks (100 ticks per second)
Display(value v) // set the dispaly mode as follows:
Note: display modes for "Display" are as follows: 0 = system clock, 1-3 = input channel,
4-6 = output channel. To show the value of the third input, use "Display(3)", not
"Display(IN_3)". The later command will read the value of sensor 3, and use that value
as the argument to the display command (e.g. if the sensor's value is 0, then the system
clock will be displayed). Yeah, this is pretty stupid, but its how the RCX works.
SendMessage(value m); // send low 8 bits of m as an IR message
ClearMessage(); // clear the last received IR message
ClearTimer(const n); // clear timer #n
Data Logging
The RCX supports a Datalog feature. Using this feature requires three steps:
1) Set the datalog size
2) Add data points to the datalog
3) Upload the datalog to a host
You can set the datalog size with the following command
SetDatalog(const n); // create datalog for n points
Point can be added with
Datalog(value v); // add v to the datalog
Only certain types of values may be added (variables, timers, sensor readings, the system
clock).
Some (or all) of the datalog may be uploaded at any time as follows:
UploadDatalog(const index, count count);
"index" is the first data point to upload, and "count" is the number of points to upload.
Each entry in the datalog uses three bytes - the first byte is the type of entry,
the next two are a 16 bit value in little endian format. If you set the size of
the datalog to N, then there are actually N+1 entries - the first one (at index 0)
is the number of data points collected so far (type = 0xff), while the remaining
ones (index 1 - N) are the points themselves. Here are the type codes for normal
data points:
0x00 - value of a variable
0x20 - value of a timer
0x40 - value of a sensor
0x80 - value of the clock
Keywords
The following keywords are used in nqcc:
asm if stop
break int sub
continue repeat task
do return true
else start while
false
Grammar
program : unit_list
;
unit_list: unit_list unit
|
;
unit : int id_list ;
| task id block
| sub id block
;
id_list : id_list , id
| id
;
block : { stmt_list }
;
stmt_list : stmt_list stmt
|
;
stmt : ;
| block
| asm { bytes } ;
| while ( condition ) stmt
| do stmt while ( condition ) ;
| repeat ( value ) stmt
| break ;
| continue ;
| if ( condition ) stmt
| if ( condition) stmt else stmt
| id assign value ;
| start id ;
| stop id ;
| id ( ) ;
| return ;
bytes : bytes , expr
| expr
;
value : expr
| id
| @ expr
;
assign : = | += | -= | *= | /=
;
expr : number
| expr binop expr
| - expr
| ~ expr
| ( expr )
| [ value ]
;
binop : + | - | * | / | % | & | | | ^ | << | >>
;
condition : value relop value
| ! condition
| condition && condition
| condition || condition
| ( condition )
| true
| false
;
relop : == | != | < | > | <= | >=
;
For Hackers Only
NQC contains very little in the way of RCX specific functions. Support for controlling
motors, reading sensors, etc. is provided in the rcx.nqh file. This section explains some
of the low level constructs used within these definitions.
Internally, many of the RCX operations use "values", which are essentially a form of
typed data. Each value has a "type" and a "index", which together specify a sort of
effective address for the runtime value. Of course, one of the types is "constant", in
which case the effective value is equal to the index itself. Other types include reading an
input sensor or generating a random number.
The compiler can perform arithmetic operations only on compile time constants. Certain
statement (such as conditional clauses) require values, and constants are automatically
promoted to values in these situations.
However, sometimes it is desireable to specify the type/index pair directly. This can be
done by using the @ operator, which converts a 24 bit number directly to a value. The
lower 16 bits are the index, the next 8 are the type code. For example, since the type code
for "constants" is 2, the following value is equal to 0x123:
@0x20123
The type code for variables is 0, so the following refers to variable 7:
@0x00007
This is very different from 0x00007, which equals the number 7. It would've been much
nicer if LEGO had made the type code for constants equal to 0, but we have to live with
this hack.
Sometimes it is necessary to convert a value back into its "effective address". This is
done by enclosing the value in brackets:
[ Random(7) ]
The effective address is constant, thus can be manipulated with the compiler's arithmetic
operators.
Finally, the "asm" statement allows most primitives to be implemented:
asm { data };
"data" is one or more numbers, separated by commas. The asm statement emits the literal
data contained within its braces. The data bytes for an asm statement must be compile
time contsatnts, not runtime values.