Menu

Tree [6dbf75] default tip /
 History

Read Only access


File Date Author Commit
 assembler 2023-03-29 Oddwarg Oddwarg [448b27] Initial import
 pybris 2024-07-08 Oddwarg Oddwarg [6dbf75] Fixed a bug where auto arrays were not initiali...
 risvmc 2023-03-29 Oddwarg Oddwarg [448b27] Initial import
 samples 2024-07-08 Oddwarg Oddwarg [6dbf75] Fixed a bug where auto arrays were not initiali...
 B Language Reference Manual.html 2023-03-29 Oddwarg Oddwarg [448b27] Initial import
 Pybris.png 2023-03-29 Oddwarg Oddwarg [c5c34c] Added logo
 README.txt 2023-03-29 Oddwarg Oddwarg [d1eaa6] Fixed typo in readme
 errors.h 2023-03-29 Oddwarg Oddwarg [448b27] Initial import
 register allocation.txt 2023-03-29 Oddwarg Oddwarg [448b27] Initial import
 risvm-assembler.jar 2023-03-29 Oddwarg Oddwarg [448b27] Initial import

Read Me

Pybris is a compiler written in Python using Pyparsing for the B Programming Language. 
The compiler emits a variant of Bitmario RISVM assembly.

The practical goal of the project is to provide a way to develop digital signal processing (DSP) effects for the 
Competent Audio library that is a friendlier alternative to writing RISVM assembly by hand.

Pybris is written for Python 2.7, but has also been tested to run with Python 3.8.10. 

---- 0. Basic Usage ----

'pybris.py' compiles console RISVM applications (see section 4), while 'pybris_dsp.py' compiles DSP effects for use 
with the Competent Audio Library (section 5).

To compile an run the hello world sample:
  python pybris/pybris.py samples/helloworld/hello.b
  python assembler/assembler.py samples/helloworld/hello.asm
  risvmc/risvmc samples/helloworld/hello.bin

Alternatively, use the Java assembler (Requires Java 8 or later):
  java -jar risvm-assembler.jar samples/helloworld/hello.asm -o

---- 1. Known issues ----

Error reporting is not very good.

---- 2. Deviation from the specification ----

The compiler implements the B Programming Language as specified in the 1998 edition of the B Language Reference Manual 
from Thinkage Ltd., with the following known deviations:

 - Manifest constants are not implemented. (Instead, you are encouraged to use a C preprocessor such as mcpp.)
 - The standard library is not implemented. (The DSP RISVM comes with a different set of built-in library routines.)
 - Identifiers and keywords are always case sensitive.
 - The compiler uses every character of an identifier, not just the first 6.
 - 32-bit words are used instead of 36-bit words.
 - Octal integers are not supported.
 - Hexadecimal integers are supported (in the familiar 0x notation).
 - Nested ivals in global variable initialisers are not supported.
 - BCD constants are not available. 
 - ASCII constants use 8 bits per character rather than 9.
 - Only *t, *n, and *e character escapes are available.
 - No source code escapes are available.
 - Byte addresses are used, with the following consequences:
    The identity a[i] == i[a] does not hold (those have the addresses a+4*i and i+4*a, respectively).
    The identity a[i] == *(a+i) does not hold (those have the addresses a+4*i and a+i, respectively).
 - Auto-variables can have initialisers, such as 'auto i = 0;'
 - The first expression in a 'for' statement can be an auto declaration, e.g. 'for(auto i = 0; i<8; i++)'
 - Declaring functions using extrn is optional unless the function has not been defined yet (can be disabled).
 - The empty statement '{}' is legal.
 - The empty statement ';' is a syntax error (however 'for(;;)' is still legal).

---- 3. RISVM modifications ----

Pybris compiles to a variant of RISVM assembly that is different from Bitmario's original specification in the 
following ways:
1. The additional instructions icom, ucom, and fcom are required. 
    These compare ints, unsigned ints and floating point numbers, respectively.
    They take 3 register operands a, b, c, such that:
        a = b > c ? 1 : (b < c ? -1 : 0);
2. The following enhancements to the assembly language are required:
     - Support for floating point literals.
     - Labels can be used as data, e.g. '$someData word $someLabel', inserts the address of $someLabel.
     - Data can be sized, e.g.  '$someData dword[64] 1', pads $someData with 63 dword zeroes.

---- 4. Standard functionality ----

The 'pybris.py' compiles regular RISVM programs, which can be run using the standalone RISVM executable after 
assembling. A B program must define the 'main' function to be used in this configuration.

The following built-in functions are available to regular RISVM programs. 
They are not addressable, but otherwise behave as normal functions:

 - printu(uint)     Prints the given unsigned integer and returns it
 - printi(int)      Prints the given signed integer and returns it
 - printf(float)    Prints the given floating point number and returns it
 - printc(char)     Prints the given character object:
                     Returns the last printed character, 0 if the argument ended with a null terminator.
 - print(vec)       Prints the given string (vector of character objects), 
                     and returns the address of the last printed character object.
 - println()        Print a newline.
 - exit(int)        Halt the RISVM and place the given value in the return value register.
 - nargs()          Returns the number of arguments passed to the current function.
 - arg(int)         Returns the argument at the given index (even if no such argument was declared).

---- 5. DSP functionality ----

The 'pybris_dsp.py' script emits RISVM DSP assembly suitable for use with the Competent Audio library. 
A B program must define the following functions to be used in this configuration:

 - init(int, int, int)
    Called when the DSP is initialized (or re-initialized). 
    The arguments are: dspVersion (e.g. 0), sampleRate (e.g. 48000), channelCount (e.g. 2)
 - initMP() (optional)
    Always called immediately after init, with a variable number of arguments, which may be 0. 
    The application and DSP program must agree on a protocol for the arguments that are used.
    If omitted, mainMP will be called instead.
 - mainMP()
    Called before main if the application has sent a message, following the same protocol as initMP.
 - main(int, float*)
    Called to process the frame data in the array of floats given by the second argument. 
    The number of sample frames in the array is given by the first argument.
    The number of samples in each sample frame is given by the channel count passed to init.

In addition to the regular built-in functions, the following additional built-in functions are available to 
DSP programs:

 - cos(float)           Returns the cosine of the argument
 - sin(float)           Returns the sine of the argument
 - tan(float)           Returns the tangent of the argument

 - acos(float)          Returns the inverse cosine of the argument
 - asin(float)          Returns the inverse sine of the argument
 - atan(float)          Returns the inverse tangent of the argument
 - atan2(float, float)  Returns the unique arc tangent for the two arguments

 - cosh(float)          Returns the hyperbolic cosine of the argument
 - sinh(float)          Returns the hyperbolic sine of the argument
 - tanh(float)          Returns the hyperbolic tangent of the argument

 - exp(float)           Returns e^x, where x is the argument

 - log(float)           Returns the natural logarithm of the argument
 - log10(float)         Returns the base 10 logarithm of the argument

 - pow(float, float)    Returns x^y, for the two respective arguments
 - sqrt(float)          Returns the square root of the argument
 - fmod(float, float)   Returns the floating point modulo of the arguments

 - floor(float)         Returns the argument rounded down to an integer, as a float.
 - ceil(float)          Returns the argument rounded up to an integer, as a float.
 - round(float)         Returns the argument rounded to an integer, as a float.

 - fabs(float)          Returns the absolute value of the argument as a float.
 - abs(int)             Returns the absolute value of the argument as an int.
 - fsign(float)         Returns the sign of the argument as a float.
 - sign(int)            Returns the sign of the argument as an int.

 - fmin(float, float)   Returns the smallest of the two floating point values.
 - umin(uint, uint)     Returns the smallest of the two unsigned int values.
 - min(int, int)        Returns the smallest of the two int values.
 - fmax(float, float)   Returns the biggest of the two floating point values.
 - umax(uint, uint)     Returns the biggest of the two unsigned int values.
 - max(int, int)        Returns the biggest of the two int values.

 - bqNew
   Creates an empty biquad filter object for a single channel.
   Note: The filter is automatically deleted when the DSP is disposed, or before it is re-initialized.
   The DSP must record parameter changes so it can re-apply the updated values on reinitialization.
   Arguments: 1
    uint  - sample rate
   Returns: uint - filter handle
   
 - bqProc
   Filters a single audio data channel using a biquad filter.
   Arguments: 4
    uint  - filter handle
    uint  - RISVM address of audio data
    uint  - number of samples to process
    uint  - number of elements between each sample (typically the channel count)
   Returns: void

 - bqReset
   Resets the sample buffer for the given filter. This may be useful when changing the filter type.
   Arguments: 1
    uint  - filter handle
   Returns: void

 - bqLowpass
   Configures the given filter as a lowpass filter
   Arguments: 3
    uint  - filter handle
    float - cutoff frequency (hz)
    float - resonance
   Returns: void

 - bqHighpass
   Configures the given filter as a highpass filter
   Arguments: 3
    uint  - filter handle
    float - cutoff frequency (hz)
    float - resonance
   Returns: void

 - bqBandpass
   Configures the given filter as a bandpass filter
   Arguments: 3
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
   Returns: void

 - bqNotch
   Configures the given filter as a notch filter
   Arguments: 3
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
   Returns: void

 - bqPeaking
   Configures the given filter as a peaking filter
   Arguments: 4
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
    float - gain factor
   Returns: void

 - bqAllpass
   Configures the given filter as an allpass filter
   Arguments: 3
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
   Returns: void

 - bqLowshelf
   Configures the given filter as a low shelf filter
   Arguments: 4
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
    float - gain factor
   Returns: void

 - bqHighshelf
   Configures the given filter as a high shelf filter
   Arguments: 4
    uint  - filter handle
    float - frequency (hz, location of the peak in the frequency spectrum)
    float - Q-factor (related to inverse of the peak width, use 1.0 as a starting point)
    float - gain factor
   Returns: void

 - fvNew
   Creates a Jezar Freeverb reverberator object with the default configuration.
   Note: The reverberator is automatically deleted when the DSP is disposed, or before it is re-initialized.
   The DSP must record parameter changes so it can re-apply the updated values on reinitialization.
   Arguments: 8
    uint  - sample rate
    uint  - number of channels per sample frame
    float - roomsize
    float - damp
    float - wet
    float - dry
    float - width
    float - freezemode
   Returns: uint - reverberator handle

 - fvProc
   Adds reverberation to audio data using a Jezar Freeverb reverberator.
   Arguments: 4
    uint  - reverberator handle
    uint  - RISVM address of audio data
    uint  - number of sample frames to process
    uint  - number of samples between each frame (typically same as channel count)
   Returns: void

 - fvMute
   Resets the given reverberator so it stops being noisy
   Arguments: 1
    uint  - reverberator handle
   Returns: void

 - fvRoomSize
   Sets the room size for the given reverberator
   Arguments: 2
    uint  - reverberator handle
    float - room size factor
   Returns: void

 - fvDamp
   Sets the damping for the given reverberator
   Arguments: 2
    uint  - reverberator handle
    float - damping factor
   Returns: void

 - fvWet
   Sets the wet signal multiplier for the given reverberator
   Arguments: 2
    uint  - reverberator handle
    float - wet factor
   Returns: void

 - fvDry
   Sets the dry signal multiplier for the given reverberator
   Arguments: 2
    uint  - reverberator handle
    float - dry factor
   Returns: void
   
 - fvWidth
   Sets the stereo width for the given reverberator
   Arguments: 2
    uint  - reverberator handle
    float - width factor
   Returns: void
   
 - fvFreezeMode
   Enables or disables freeze mode for the given reverberator.
   In freeze mode the the current wet reverberation signal is sustained forever,
   and further inputs will not affect it.
   Arguments: 2
    uint  - reverberator handle
    float - freeze mode (enabled if value >= 0.5)
   Returns: void

---- 6. Compiler Optimisations ----

The following compiler optimisations are implemented:

 - A very primitive form of local register allocation is used, described in 'register allocation.txt'.
 - Jump tables are used for dense switch statements.
 - The basic form of constant folding is implemented:
    Arithmetic using only numeric literals can be assumed to have no cost.
 - Some instruction selection optimisations are present.

That aside, it should be assumed that the compiler does not optimise for you.

---- 7. Miscellaneous notes ----

Pybris supports the #line directives emitted by C preprocessors: Most errors should display the correct file and line 
number even when using a preprocessor.

Character objects are emitted as 32 bit integers with the first character in the least significant byte. This results 
in the assembly being more difficult to read, but it was the easiest way to ensure strings and character objects have 
the same structure seen from the B program, regardless of the byte order of the target machine, since the B language 
does not support handling individual characters.

In theory, RISVM assembly is byte order-agnostic, but the bytecode form is not. The Java RISVM Assembler can emit 
big-endian bytecode, which should, in theory, allow the program run on big-endian machines. This is currently untested.

Stack traces are made possible (e.g. when triggering a memory error or using the exit function) by following the 
Competent Audio DSP calling convention. The Java RISVM Assembler can be used to emit a line table, which allows the 
RISVM to display assembly line numbers and contents in the stack trace.

Using gotos and labels, and creating pointers to variables using the & operator will degrade or prevent some of the 
register allocation optimizations.

Using goto to jump to any location that is not a label in the same function will result in undefined behaviour.

Attempting to access a missing argument in a function may result in a memory error (halting the VM).