Language::Mumps - Perl module to translate Mumps programs to perl scripts


Language-Mumps documentation Contained in the Language-Mumps distribution.

Index


Code Index:

NAME

Top

Language::Mumps - Perl module to translate Mumps programs to perl scripts

SYNOPSIS

Top

  use Language::Mumps;

  $pcode = Language::Mumps::compile(qq{\tw "Hello world!",!\n\th});
  eval $pcode;

  Language::Mumps::evaluate(qq{\ts x=1 w x});

  Language::Mumps::interprete("example.mps");

  Mumps:translate("example.mps", "example.pl");

prompt % perl example.pl

DESCRIPTION

Top

This module compiles Mumps code to Perl code. The API is simillar to MumpsVM.

ENVIRONMENT

Top

Edit ~/.pmumps or /etc/pmumps to set up persistent arrays.

FILES

Top

$BINDIR/pmumps Interpreter
~/.pmumps User configuration
/etc/pmumps.cf Site configuration

AUTHOR

Top

Ariel Brosh.

COPYRIGHT AND LICENSE

Top

SEE ALSO

Top

pmumps, DB_File.


Language-Mumps documentation Contained in the Language-Mumps distribution.

# PerlMUMPS by Ariel Brosh
# Usage is free, including commercial use, enterprise and legacy use
# However, any modifications should be notified to the maintainer
# Email: smueller@cpan.org

# Note:
# This compiler parses and generates in the same phase, therefore is not
# very maintainable

package Language::Mumps;
$VERSION = '1.08';
use Fcntl;
use strict;
use vars qw($FETCH $STORE $DB $SER $IMPORT @TYING $xpos $ypos
  %symbols $selected_io $flag @handlers @xreg @yreg
  $curses_inside $varstack %RES $RESKEYS %COMMANDS $scope_do
  %FUNCTIONS %FUNS @tmpvars $tmphash $infun $scopes @stack
  @program %bookmarks $lnum $forgiveful $forscope %dbs
  $VERSION);

# Map short form to long form commands

%COMMANDS = qw(B BREAK C CLOSE D DO E ELSE F FOR G GOTO HALT HALT
               H HANG I IF J JOB K KILL L LOCK O OPEN Q QUIT
               R READ S SET U USE V VIEW W WRITE X XECUTE
               ZE HALT ZP ZP ZFUNCTION ZFUNCTION
               ZRETURN ZRETURN ZD ZD);

# Map short form to long form functions

%FUNCTIONS = qw(I IO T TEST P PIECE H HOROLOG J JOB 
                 X X Y Y ZDATE ZD ZA ZN);

# Function schema
# array of: funcname => array of | lval => 1/0, prot => prototype
# If lval is 1, function can be use as lvalue.
# prototype has one char per function parameter.
# I = input O = output L = list T = tuple

%FUNS = (
         'ASCII' => [{'lval' => 0, 'prot' => 'II'},
                     {'lval' => 0, 'prot' => 'I'}],
         'CHAR' => [{'lval' => 0, 'prot' => 'L'}],
         'DATA' => [{'lval' => 0, 'prot' => 'O'}],
         'EXTRACT' => [{'lval' => 0, 'prot' => 'I'},
                       {'lval' => 0, 'prot' => 'II'},
                       {'lval' => 0, 'prot' => 'III'}],
         'FIND' => [{'lval' => 0, 'prot' => 'II'},
                     {'lval' => 0, 'prot' => 'III'}],
         'JOB' => [{'lval' => 0, 'prot' => ''}],
         'JUSTIFY' => [{'lval' => 0, 'prot' => 'II'},
                     {'lval' => 0, 'prot' => 'III'}],
         'HOROLOG' => [{'lval' => 0, 'prot' => ''}],
         'IO' => [{'lval' => 1, 'prot' => ''}],
         'LEN' => [{'lval' => 0, 'prot' => 'II'},
                     {'lval' => 0, 'prot' => 'I'}],
         'NEXT' => [{'lval' => 0, 'prot' => 'O'}],
         'ORDER' => [{'lval' => 0, 'prot' => 'O'}],
         'PIECE' => [{'lval' => 1, 'prot' => 'OII'},
                 {'lval' => 0, 'prot' => 'III'},
                 {'lval' => 0, 'prot' => 'IIII'}],
         'RANDOM' => [{'lval' => 0, 'prot' => 'I'}],
         'SELECT' => [{'lval' => 0, 'prot' => 'T'}],
         'TEST' => [{'lval' => 1, 'prot' => ''}],
         'X' => [{'lval' => 0, 'prot' => ''}],
         'Y' => [{'lval' => 0, 'prot' => ''}],
         'ZAB' => [{'lval' => 0, 'prot' => 'I'}],
         'ZB' => [{'lval' => 0, 'prot' => 'I'}],
         'ZCD' => [{'lval' => 0, 'prot' => ''},
                   {'lval' => 0, 'prot' => 'I'}],
         'ZCL' => [{'lval' => 0, 'prot' => ''},
                   {'lval' => 0, 'prot' => 'I'}],
         'ZD' => [{'lval' => 0, 'prot' => ''}],
         'ZD1' => [{'lval' => 0, 'prot' => ''}],
         'ZD2' => [{'lval' => 0, 'prot' => 'I'}],
         'ZD3' => [{'lval' => 0, 'prot' => 'III'}],
         'ZD4' => [{'lval' => 0, 'prot' => 'III'}],
         'ZD5' => [{'lval' => 0, 'prot' => 'III'}],
         'ZD6' => [{'lval' => 0, 'prot' => 'I'},
                  {'lval' => 0, 'prot' => ''}],
         'ZD7' => [{'lval' => 0, 'prot' => 'I'},
                  {'lval' => 0, 'prot' => ''}],
         'ZD8' => [{'lval' => 0, 'prot' => 'I'},
                  {'lval' => 0, 'prot' => ''}],
         'ZD9' => [{'lval' => 0, 'prot' => 'I'},
                  {'lval' => 0, 'prot' => ''}],
         'ZDBI' => [{'lval' => 0, 'prot' => 'IIIIO'}],
         'ZF' => [{'lval' => 0, 'prot' => 'I'}],
         'ZH' => [{'lval' => 0, 'prot' => 'I'}],
         'ZL' => [{'lval' => 0, 'prot' => 'II'},
                  {'lval' => 0, 'prot' => 'I'}],
         'ZN' => [{'lval' => 0, 'prot' => 'I'}],
         'ZR' => [{'lval' => 0, 'prot' => 'I'}],
         'ZS' => [{'lval' => 0, 'prot' => 'I'}],
         'ZSQR' => [{'lval' => 0, 'prot' => 'I'}],
         'ZT' => [{'lval' => 0, 'prot' => 'I'}],
         'ZVARIABLE' => [{'lval' => 0, 'prot' => 'I'}],
         );

####
## M line to Perl line

sub m2pl {
    my $line = shift;

# Convert 8 spaces to a tab if -f used
# M requires lines to begin with tabs

    $line =~ s/^(\w+) {8}/$1\t/ if ($forgiveful);

# Embedded perl code

    if ($line =~ s/^\%//) {
        return "$line\n";
    }

# Comment

    if ($line =~ s/^\#//) {
        return "";
    }

# Does not begin with a tab - plain text

    unless ($line =~ /\t/) {
        return "Language::Mumps::write('$line');\n";
    }

# Reset variable factory

    &resetvars;

    my ($label, $llin) = split(/\s*\t\s*/, $line, 2);
    $line = $llin;

# Labels must begin with a letter

    die "Illegal label $label" unless (!$label || $label =~ /^[a-z]\w*/i);

# Bookmarks are for source listing. Available only if M program was
# compiled and executed inside the same Perl script

    $bookmarks{$label} = $lnum;
    $label = "__lbl_Mumps_$label\: " if ($label);

# Do the actual work
    $label . &ml2pl($line);
}

sub ml2pl {
    my $line = shift;
    my ($res, $tmp, $code);

# M commands may be several in a line

    while ($line) {
        my ($token, $cond, $pre, $post);

# "Eat" one token, cancelling spaces.

        if ($line =~ s/^\s*(\S*?)\s+//) {
            $token = $1;
        } else {
            $token = $line;
            $line = '';
        }

# Close block

        if ($token eq '}') {
            die "Unexpected right bracket" unless ($scopes--);
            $code .= "}\n";
            next;
        }

# Command:Condition - Run the command conditionally

        if ($token =~ /^([a-z]\w*):(.*)$/i) {
            $token = $1;
            $cond = $2;
        }

        if ($cond) {
            ($pre, $tmp) = &makecond($cond);
            $pre .= "if ($tmp) {\n";
            $post = "\n}";
        }

        $token = uc($token);

#        my ($k, $v);
        foreach (keys %COMMANDS) {
# If $token is either short or long form of command, call function

            if ($_ eq $token || $COMMANDS{$_} eq $token) {
# $line is passed *by reference*

                $res = &{$COMMANDS{$_}}($line);

# Kill spaces

                $line =~ s/^\s*//;
                goto success;
            }
        }
        die "Unrecognized command $token";
success:
        $code .= "$pre$res$post\n";
    }
    $code;
}

####
## Convert a block of M code to a block of Perl code

sub compile {
    my $text = shift;
    my @lines = split(/\r?\n/, $text);
    %bookmarks =();
    @program = @lines;
# Stack based scope for $scopes - push the scope counter to the stack
# until the end of the function

    local($scopes);
    $lnum = 0;
# Iterate over code

    my @code = map {++$lnum; "# $lnum) $_\n" . &m2pl($_);} @lines;
# Ensure we close all blocks

    die "Unclosed brackets" if ($scopes);

# Add essential code
# mumps.cfg will be read only by the compiler, not by programs
    join("", "use Language::Mumps qw(Runtime $IMPORT);\nno strict;\n",  @code,
              "### end\n", &m2pl("\tQUIT"));
}

####
## Compile an M program and evaluate immediately

sub evaluate {
    my $prog = shift;
    my $code = &compile($prog);
    local (@stack);
    $@ = undef;
    eval $code;
    die $@ if ($@);
}

####
## Read an M program from a file, compile and run

sub interprete {
    my $fn = shift;
    open(I, $fn);
    my $prog = join("", <I>);
    close(I);
    evaluate($prog);
}

####
## Translate an M file to a Perl file

sub translate {
    my ($i, $o) = @_;
    open(I, $i);
    my $prog = join("", <I>);
    close(I);
    my $code = &compile($prog);
    open(O, ">$o");
    print O <<EOM;
#############################################################################
# This Perl script was created by the MUMPS to Perl compiler by Ariel Brosh #
#############################################################################

$code

1;
EOM
    close(O);
}

####
## Return a line of the program
## Not thread safe - supports only one M program per Perl script

sub list {
    my ($line, $off);
    my $lnum = ($line > 0) ? ($line - 1) : $bookmarks{$line} || die "Unknown label";
    $program[$lnum - 1 + $off];
}

######################################################################
## COMMANDS                                                         ##
######################################################################
## Each function receives a line of code *by reference*, removes    ##
## input tokens as they are "eaten" and returns Perl code to add to ##
## the output.                                                      ##
######################################################################


####
## BREAK - Stop the program

sub BREAK {
    return "exit;";
}

####
## CLOSE
## Add code to create a list of parameters
## Add code to iterate through them and close file objects

sub CLOSE ($) {
    my ($code, $var) = &makelist($_[0]);
    return $code . <<EOM;
foreach ($var) {
    die "Can't CLOSE unit 5" if (\$_ == 5);
    close($Language::Mumps::handlers[\$_]);
}
EOM
}

####
## DO
## DO label - jump to the label. Create a label for returning.
##    Add code to push this label to the stack.
## DO "program" or DO @var - Interprete another program
##    Add code to invoke the interprete method.
##    (Will make program listing useless)
## DO $$<expr> - Call a perl function. Test flag is set to the
##    non zeroeness of the return.

sub DO ($) {
    if ($_[0] =~ s/^\s*([a-z]\w*)\b//i) {
        my $dest = $1;
        ++$scope_do;
        my $lbl = &nextvar("d$scope_do");
        return <<EOM;
push(\@Language::Mumps::stack, '$lbl');
goto __lbl_Mumps_$dest;
$lbl:
EOM
    }
    if ($_[0] =~ /^[\@"]/) {
        $_[0] =~ s/^\@//;
        my ($code, $var) = &makeexp($_[0]);
        return $code . "Language::Mumps::interprete($var);";
    }
    if ($_[0] =~ /^\$\$/) {
        my ($code, $var) = &makeexp($_[0]);
        return $code . "\$Language::Mumps::flag = $var ? 1 : undef;";
    }
    $_[0] =~ s/\s.*$//;
    die "Illegal argument for DO $_[0]";
}

####
## ELSE - Things to do if the test flags is false.
##    Usually but not necessarily after IF.
## Add code to check test flag and -
## If called with { - Increase the scope counter, leave Perl code
## in a block
## If called with a list of commands - call the interpreter recursively
## to interprete the rest of the line, put it inside the conditional
## block.

sub ELSE ($) {
    my $code = "unless (\$Language::Mumps::flag) {";
    if ($_[0] =~ s/^\{\s*//) {
        $scopes++;
        return $code;
    }
    my $block = &ml2pl($_[0]);
    "$code\n$block}";
}

####
## FOR var=token,token,token
##  Make a Foreach over the list.
##  Token can be: start:step:last

sub FOR ($) {
    unless ($_[0]) {
        die "Iterator expected in FOR";
    }

## Construct the iteration variable
    my ($itercode, $lvar) = &makevar($_[0]);

## Get Perl code to represent lvalue
    my $var = $lvar->lval;

## Allocate an iteration var
    my $itervar = &nextvar();

## Allocate a var to hold the list
    my $eachlist = &nextvar('@');
# Allocate vars to hold from, to, step
    my $f = &nextvar('$');
    my $t = &nextvar('$');
    my $s = &nextvar('$');

# Code to attach the Perl iteration var to a symbol table entry of the
# selected LValue. (Needed to support complex access)

    $itercode .= "*$itervar = \\$var;\n";

# From now on, $var is the soft reference to $var

    $var = "\$$itervar";
    die "= expected in FOR" unless ($_[0] =~ s/^\=//);

# Code inside the loop will be stored in a subroutine
# This way we can forward-rely on it
# All procedures will have a unique identifier

    my $procname = "__tmpfor" . ++$forscope;
    my ($flag, $listflag);
    my $first = 1;

# "Eat" the remainder of the parameter
    while (1) {
# Set $flag to true if we are in the end of the input
        $flag = 1 unless ($_[0] && $_[0] !~ /^\s/);
# Unless it is the first token, or input has ended, we must skip a comma
        die "Comma expected in FOR" unless ($first || $_[0] =~ s/^,// || $flag);
# No more first token
        $first = undef;
# "Eat" value
        my ($code, $val) = &makeexp($_[0]);
        if ($flag || $_[0] =~ s/^\://) {
# If we are in the end of input, or we have a compund token,
# we have to flush the simple tokens.
            $itercode .= "foreach \$var ($eachlist) " .
                 "{&$procname;}\n\$eachlist = ();\n" if ($listflag);
            last if ($flag);
# If we got here, it is a compound token
            $listflag = undef;
# Add the code to evaluate the loop start, and to assign it to the
# loop start variable
            $itercode .= $code;
            $itercode .= "$f = $val;\n";

# Get the step value. Note: we have already skipped the colon.
            ($code, $val) = &makeexp($_[0]);
            $itercode .= $code;
            $itercode .= "$s = $val;\n";

# If we have got more input, it must be delimited with a colon

            if ($_[0] && $_[0] !~ /^[,\s]/) {
                die "Upper bound expected in FOR" unless ($_[0] =~ s/^://);
# "Eat" the to value.
                my ($code, $val) = &makeexp($_[0]);
                $itercode .= $code;
                $itercode .= "$t = $val;\n";
            } else {
# Infinite loop requested. (M dictates this syntax)
# If To is two Steps below From, we probably will never
# Reach To.
                $itercode .= "$t = $f - $s * 2;\n";
            }
# Obsolete sick code
#            my $sign = (qw(< == >))[($f <=> $t) + 1];
#            my $step = (qw(+ + -))[($f <=> $t) + 1];
#            my $cond = ($t ? "$var $sign $t" : 1);
#            my $incr = (abs($s) == 1) ? ($var . ($step x 2))
#                    : "$var $step= " . abs($s);

# Generate for(;;) code.
# Make To run away one step. This way the original To value is still
# inside the loop.
# We check if the iterator is still different from To, and if it is
# in the same direction as From was for.
            my $for = "($var = $f, $t += $s; " .
              "$var != $t && ($var <=> $t) == ($f <=> $t); " .
              "$var += $s)";
            $itercode .= "for $for {\&$procname;}\n";
        } else {
# Simple token - add to list
            $itercode .= $code . "push($eachlist, $val);\n";
            $listflag = 1;
        }
    }
# Dismiss soft reference

    $itercode .= "*$itervar = \\\$sysundef;\n";
    $_[0] =~ s/^\s*//;
    die "Code expected in FOR" unless ($_[0]);

# Define the subroutine we "owe"
# Either open a block, or call the interpreter recursively to
# translate the rest of the line into the subroutine

    $itercode .= "sub $procname {\n";
    if ($_[0] =~ s/^\{\s*//) {
        $scopes++;
        return $itercode;
    }
    my $code = &ml2pl($_[0]);
    $_[0] = '';
# Dixi et salvavi, Anima meam!
    return "$itercode$code\n}";
}

####
## GOTO label
## Translate to perl gotos, do not check label existence

sub GOTO ($) {
    if ($_[0] =~ s/^([a-z]\w*)\b//i) {
        return "goto __lbl_Mumps_$1;";
    }
    $_[0] =~ s/\s.*$//;
    die "Illegal label in GOTO: $_[0]";
}

####
## HALT - exit the program

sub HALT {
    return "exit;";
}

####
## HANG - Exit if no parameter, Sleep if parameter attached
sub HANG ($) {
    return "exit;" unless ($_[0]);
    my ($code, $var) = &makeexp($_[0]);
    return $code . "sleep($var);";
}

####
## IF
## Load the test flag, then make a block conditional to the test flag
## Block creation like in FOR or ELSE

sub IF ($) {
    die "Condition expected in IF" unless ($_[0]);
    my ($code, $val) = &makeexp($_[0]);
    my $condcode = $code . "\$Language::Mumps::flag = $val ? 1 : undef;\nif (\$Language::Mumps::flag) {\n";
    $_[0] =~ s/^\s*//;
    die "Code expected in IF" unless ($_[0]);
    if ($_[0] =~ s/^\{//) {
        $scopes++;
        return $condcode;
    }
    $code = &ml2pl($_[0]);
    $_[0] = '';
    return "$condcode$code\n}";
}

####
## JOB - unsopported

sub JOB {
    die "Not implemented: JOB";
}

####
## KILL - kill the whole symbol table
## KILL var - kill one symbol or array
## KILL (var) - kill everything besides one symbol

sub KILL ($) {
## No parameter - kill everything
    unless ($_[0]) {
        return "%Language::Mumps::symbols = ()";
    }
    my $rev;
    my $thecode;
#    my $cond = "if";
# Allocate a var name to hold a copy of the symbol table
    my $tmptbl = &nextvar();

# Check if we have paranthesis
    if ($_[0] =~ s/^\(//) {
        $rev = 1;
    }
# Prepare a hash to store the copied symbol table

    $thecode = "{ my \%$tmptbl;\n";
    my $n;
    while ($_[0] && $_[0] !~ /^\s/) {
        $n++;
        last if ($n == 2 && $rev && $_[0] =~ s/^\)>//);
        die "Variable expected in KILL" unless ($_[0] =~ /^\^?\w/);
        my ($code, $var) = &makevar($_[0]);
        die "Can unkill only regular arrays" if ($rev && ref($var) !~ /var/i);
        my $addr = $var->addr;
# Either extract the variable purge code, or call runtime function
# to deep copy the chosen var into the new symbol table
# entry
        $thecode .= $code . (!$rev
                ?  $var->purge . "\n"
                : "&Language::Mumps::moveimage(\\\%Language::Mumps::symbol, \\\%$tmptbl, " .
                        "$addr);\n"
           );
    }
# If unkilling, deep copy the symbol table back
    if ($rev) {
        $thecode .= <<EOM;
\%Language::Mumps::symbol = ();
foreach (keys \%$tmptbl) {
    \$Language::Mumps::symbol{\$_} = \$$tmptbl\{\$_};
}
EOM
    }
    chomp $thecode;
    $thecode;
}

####
## LOCK ^array - lock an array database. Implemented only for disk mapped
## arrays
## LOCK - With no parameters, remove any previous locks.

sub LOCK ($) {
    unless ($_[0]) {
    return <<EOM;
foreach (\@Language::Mumps::locks) {
    flock(\$_, 8);
}
\@Language::Mumps::locks = ();
EOM
    }
# Get the var
    my ($code, $var) = &makevar($_[0]);
    die "Only one array can be LOCKed" if ($_[0] && $_[0] !~ /^\s/);
# Get the dereferencing to the database
    my $ext = $var->getdb;
    my $tdb = &nextvar('$');
    my $fd = &nextvar('$');
return <<EOM;
$tdb = $ext;
$fd = $tdb->fd;
die "LOCK: flock: $!" unless flock($fd, 6);
push(\@Language::Mumps::locks, $fd);
EOM
}

####
## OPEN file-number:open-string
## open-string = filename/method
## method = NEW|OLD|APPEND

sub OPEN ($) {
# Allocate a variable to hold the stream number
    my $opennum = &nextvar('$');
# Allocate a variable to hold the parse tokens of the open string
    my $tokens = &nextvar('@');
# Allocate two vars to hold the actual tokens
    my $ofn = &nextvar('$');
    my $omet = &nextvar('$');
# "Eat" the expression for the file number
    my ($code, $var) = &makeexp($_[0]);
    die ": expected in OPEN" unless ($_[0] =~ s/^\://);
    $code .= "$opennum = $var;\n";
# "Eat" the open string
    my ($code2, $var2) = &makeexp($_[0]);
# Generate code
    $code . $code2 . <<EOM;
die "Can't reOPEN unit 5" if ($opennum == 5);
($ofn, $omet) = $tokens = split(/\\//, $var2);
die "Illegal OPEN string" unless (scalar($tokens) == 2 &&
    grep /^$omet\$/i, qw(NEW OLD APPEND));
\$Language::Mumps::handlers[$opennum] = "F" . $opennum;
open(\$Language::Mumps::handlers[$opennum],
    {NEW => '>', APPEND => '>>', OLD=> '<'}->{uc($omet)} . $ofn);
\$Language::Mumps::handlers[$opennum] = \*{\$Language::Mumps::handlers[$opennum]};
EOM
}

####
## QUIT
## End a subroutine or the whole program

sub QUIT {
    return <<EOM;
if (\@Language::Mumps::stack) {
    goto &{pop \@Language::Mumps::stack};
}
exit;
EOM
}

####
## READ var,var.... Read variables
## READ *var - Read one keypress, return ASCII code (with Curses)
##    Test flag will be false if we read nothing
## READ ?seconds,var - Read with timeout
## READ "prompt",var

sub READ ($) {
    my ($result, $timeout, $done);
    while ($_[0] && $_[0] !~ /^\s/) {
# Iterate over arguments
        die "Comma expected in READ" unless (!$done++ || $_[0] =~ s/^,//);
# If we have a varname
        if ($_[0] =~ /^\*?[a-z^]/i) {
            my $icode = "&Language::Mumps::read";
# Skip asterik if any, and decide we read one char
            if ($_[0] =~ s/^\*//) {
                $icode = "ord(&Language::Mumps::readkey)";
            }
# In both cases, reading uses a runtime function
            my ($code, $lvar) = &makevar($_[0]);

# Extract lvalue dereferencing code
            my $var = $lvar->lval;
# If we have a timeout, run the code inside an eval() which will be
# interrupted by SIGALARM

            $result .= "\$SIG{ALRM} = sub {die 1;}; \$\@ = undef; alarm $timeout;\n"
                . "eval {\n" if ($timeout);
            $result .= "$var = $icode;\n";
            $result .= "};\n\$SIG{ALRM} = undef; alarm 0;\n\$Language::Mumps::flag = (\$\@ ? undef : 1);\n" if ($timeout);
            $timeout = undef;
        } elsif ($_[0] =~ s/^\?//) {
            my $snip;
            ($snip, $timeout) = &makeexp($_[0]);
            $result .= $snip;
        } else {
# Constants - inteprete as prompts to be written
            my ($code, $var) = &makeexp($_[0]);
            $result .= $code . "&Language::Mumps::write($var);\n";
        }
    }
    chomp $result;
    $result;
}

####
## SET var=value,var=value

sub SET ($) {
    my ($result, $done);
    while ($_[0] && $_[0] !~ /^\s/) {
        die ", expected in SET" unless ($_[0] =~ s/^,// || !$done++);
# "Eat" var
        my ($code, $lvar) = &makevar($_[0]);
# Extract code to dereference lvalue
        my $var = $lvar->lval;
# Enforce equal sign and skip it
        die "= expected in SET" unless ($_[0] =~ s/^\=//);
# "Eat" value
        my ($code2, $val) = &makeexp($_[0]);
        my $lval = &nextvar("");
# Generate code to:
# Make a temporary variable with soft reference, make assignment,
#     dismiss soft referrence
        $result .= $code . "*$lval = \\$var;\n" .
                $code2 . "\$$lval = $val;\n*$lval = \\\$sysundef;\n";
    }
    $result;
}

####
## USE file-number
##    Generate code to save the xpos and ypos values
sub USE ($) {
    my ($code, $val) = &makeexp($_[0]);
    return $code . <<EOM;
\$Language::Mumps::xreg[\$Language::Mumps::selected_io] = \$Language::Mumps::xpos;
\$Language::Mumps::yreg[\$Language::Mumps::selected_io] = \$Language::Mumps::ypos;
\$Language::Mumps::selected_io = $val;
\$Language::Mumps::xpos = \$Language::Mumps::xreg[\$Language::Mumps::selected_io];";
\$Language::Mumps::ypos = \$Language::Mumps::yreg[\$Language::Mumps::selected_io];";
EOM
}

####
## VIEW - Not implemented

sub VIEW {
    die "Not implemented: VIEW";
}

####
## WRITE val,val.....

sub WRITE {
    my ($code, $val) = &makelist($_[0]);
    return $code . <<EOM;
foreach ($val) {
    &Language::Mumps::write(\$_);
}
EOM
}

####
## XECUTE value,value,value
## Evaluate the M code expressed in the parameters
sub XECUTE {
    my ($code, $val) = &makelist($_[0]);
    return $code . <<EOM;
foreach ($val) {
    eval &ml2pl($_);
    die "XECUTE: \$\@" if \$\@;
}
EOM
}

####
## ZP -- Evaluate Perl code until end of the line
## Test flag represents the non zeroeness of the result

sub ZP ($) {
    my $line = $_[0];
    $_[0] = '';
    return "\$Language::Mumps::flag = ($line) ? 1 : undef;";
}

####
## ZD - Evaluate perl code until the end of the line

sub ZD ($) {
    my $line = $_[0];
    $_[0] = '';
    return $line;
}

####
## ZFUNCTION - Incompatible with MumpsVM!
## ZFUNCTION function(var1,var2,var3...)
## ZFUNCTION function
## Functions are called as var calls to perl functions - with DO $$

sub ZFUNCTION ($) {
      my @tokens = ($_[0] =~ s/^\s*([a-z]\w*)(?:\(?:(?:([a-z]\w*)(\,[a-z]\w*)*)?\))?\s*$//i);
      die "Incorrect function header in ZFUNCTION" unless (@tokens);
      die "Cannot nest functions in ZFUNCTION" if ($infun++ > 1);
      my $fun = shift @tokens;
      $tmphash = &nextvar("");
      @tmpvars = @tokens;
      my $code .= "sub $fun {\nmy \%$tmphash;\n";
# Save out of scope variables
      foreach (@tokens) {
          my $obj = new Language::Mumps::var;
          $obj->name($_);
          my $var = $obj->lval;
          $code .= "\$$tmphash\{'$_'} = $var;\n$var = shift;\n";
      }
      $code;
}

####
## ZRETURN - End a function *once*

sub ZRETURN ($) {
    die "Not in a function in ZRETURN" unless ($infun--);
    my ($code, $var) = &makeexp($_[0]);
# Pull out of scope vars from the stack

    foreach (@tmpvars) {
          my $obj = new Language::Mumps::var;
          $obj->name($_);
          my $var = $obj->lval;
          $code .= "$var =\$$tmphash\{'$_'}\n";
    }
    $code . "return $var;\n}";
}

################################################################
## Utility functions - parsing                                ##
################################################################
## Three parameters by reference -                            ##
## 0 - Line of code - parsed tokens are removed               ##
## 1 - depth of parsing - arrays have indexes, functions have ##
##    parameters, etc. Used for scoping.                      ##
## 2 - Number of right paranthesis expected                   ##
################################################################

####
## makevar - "Eat" a reference to a variable
## This can be a variable identifier, a function identifier,
## or a reference to a disk stored array

sub makevar ($) {
    my ($a, $b) = (0, 0);
    makevar2($_[0], $a, $b);
}

sub makevar2 ($$) {
    my ($code, $obj, $val, $var, $isfun, $extra);
## Advance scope
    ++$_[1];
## Variables beginning with '$' are functions

    if ($_[0] =~ s/^\$//) {
# Function - skip the $
        $obj = new Language::Mumps::Func;
        $isfun = 1;
# Tolerate double $ - Perl function calls
        $extra = '$';
    } elsif ($_[0] =~ s/^\^//) {
# Arrays beginning with ^ are actually stored on disk
        $obj = new Language::Mumps::Database;
    } elsif ($_[0] =~ s/^\&//) {
# Variables preceded by & are simply perl vars with the
# corresponding name
        $obj = new Language::Mumps::Freevar;
    } else {
# Regular variables. % is a valid leading char and not skipped
        $extra = '%';
        $_[0] =~ s/^\@//;
        $obj = new Language::Mumps::Var;
    }
    die "Illegal array name" unless ($_[0] =~ /^[a-z$extra]/i);
# Remove alphanumeric token
    $_[0] =~ s/^([a-z$extra]\w*)//i;
    my $alias = $1;
# Resolve function aliases
    $alias = $FUNCTIONS{uc($alias)} || $alias if ($isfun);
    my $this;
# If we have opening paranthesis - awaiting array indices or function
# parameters
    if ($_[0] =~ s/^\(//) {
        unless ($isfun) {
# Array indices arriving
# Call makelist2 - scope to be increased - paranthesis counter
# increased
# Add the code to produce the list.
              ($code, $var) = &makelist2($_[0], $_[1], $_[2] + 1);
              die "No closing brackets" unless ($_[0] =~ /^\)/);
              goto regular;
        }
# This must be a function
        if ($alias =~ s/^(\$)//) {
# If it is a Perl function call, convert the Function
# object to a  Primitive object "partisanically"
# Construct the parameter list
              ($code, $var) = &makelist2($_[0], $_[1], $_[2] + 1);
              bless $obj, 'Language::Mumps::Primitive';
              goto regular;
        }
# This is an M function, therefore case insensitive
        $alias =~ tr/a-z/A-Z/;
# Lookup the function
        my $opt = $FUNS{$alias};
        die "Illegal function $alias" unless (@$opt);
        my $line;
# Check all the calling conventions of the function, to find
#    if any of the prototypes match
        foreach (@$opt) {
# Extract the prototype, copy the code line
            $line = $_[0];
            $@ = undef;
            $obj->prot($_->{'prot'});
# Call makelist2 with the extra parameter defining the prototype
# $line is passed by reference
            eval {
                ($code, $var) = &makelist2($line, $_[1], $_[2] + 1,
                   $obj->prot);
# makelist2 might raise an exception. This die can as well
                die "No closing brackets" unless ($line =~ /^\)/);
            };
# If there were no exceptions, this prototype match
            goto success unless ($@);
        }
# No prototype matched
        die "Unmatched function prototype for $alias: $@";
success:
# Commit the changes to the code line
        $_[0] = $line;
regular:
# If we were handling a regular variable, we are here
# Set the parameter list
        $obj->list($var);
        die "No closing brackets" unless ($_[0] =~ s/^\)//);
    } elsif ($isfun) {
# If there were no paranthesis
        $alias =~ tr/a-z/A-Z/;
        my $opt = $FUNS{$alias};
        die "Illegal function $alias" unless (@$opt);
        my $line;
# Check if any of the candidate functions except empty prototypes
        foreach (@$opt) {
            goto day unless ($_->{'prot'});
        }
        die "Function $alias requires parameters";
day:
    }
    $obj->name($alias);
# Return the code and the variable object reference
# Call ->lval to get a Perl code to dereference it
    ($code, $obj);
}

# Parse an expression
# White spaces are forbidden inside expressions
# As M is defined - parsing is DUMB - left to right.

sub makeexp ($) {
    my ($a, $b) = (0, 0);
    makeexp2($_[0], $a, $b);
}

sub makeexp2 ($$) {
    my ($step);
    my $scope = ++$_[1];
    my ($result, $sum);
# Allocate a Perl variable to hold the result
    my $var = &nextvar('$');
    my $negation;
# Iterate over the code line
# Known delimiters are colons, commas and spaces
    while ($_[0] && $_[0] !~ /^(\,|\s|\:)/) {
        my ($val, $code);
# "Eat" one character from the code line
        $_[0] =~ s/^(.)//;
        my $ch = $1;

# If we found right paranthesis
        if ($ch eq ')') {
# Unget the closing paranthesis - somebody needs it
            $_[0] = $ch . $_[0];
# Ensure we had a pending scope
            last if ($_[2]);
            die "Unexpected right bracket";
        }

# Double quotes start strings
        if ($ch eq '"') {
            my $flag;
# Iterate over the rest of the string
            while (1) {
# "Eat one character
                $_[0] =~ s/^(.)//;
                my $ch = $1;
# If this is a double quote sign, and not escaped, we've done it
                last if ($ch eq '"' && !$flag);
# If it is a backslash and not escapes - we are escaping
                if ($ch eq '\\' && !$flag) {
                    $flag = 1;
                    next;
                }
# If we are escaping - add a backslash. Otherwise add the character,
# taking care of dollar signs and other things that might confuse Perl
                $ch = ($flag ? "\\$ch" : quotemeta($ch));
# We are not escaping anymore
                $flag = undef;
# Add to the token
                $val .= $ch;
# We require closing double quotes
                die "Unterminated string" unless ($_[0]);
            }
# The Perl code to emit is the string in double quotes
            $val = qq!"$val"!;


        } elsif ($ch eq '!') {
# Line feed
            $val = qq!"\\n"!;
        } elsif ($ch eq '#' && !$result) {

# Emit a clear screen instruction, understood by the write() function
            $val = qq!['cls']!;
        } elsif ($ch eq '?' && $result) {

# ? in M can be either a binary operator or a prefix unary operator
# Depending on context

# Parse an M style regexp
            die "Regexp expected" unless ($_[0] =~ s/^(\S+)//);
# Convert to Perl regexp
            $val = &makeregexp($1);
# Compare the whole string to the regexp - 1 or undef
            $result .= "$var = ($var =~ /^$val\$/);\n";
            $sum = undef;
            next;
        } elsif ($ch eq '?') {

# Tab instruction 
            my $var;
            ($code, $var) = makeexp($_[0]);
            $val = qq!['tab', $var]!;
        } elsif ($ch =~ /[0-9\.]/) {

# A number
            my ($exp, $dot);
            $val = $ch;
# Iterate over rest of string, while finding numeric chars
            while ($_[0] =~ s/^(\d+|\.|E)//i) {
                my $ch = $1;
                if ($ch eq '.') {
# Dot only once
                    $dot++;
                    die "Illegal number" if ($dot > 1 || $exp);
                }
                if (uc($ch) eq 'E') {
# Exp only once
                    $exp++;
                    die "Illegal number" if ($exp > 1);
                }
# Add chars
                $val .= $ch;
            }
# Must end in a digit
            die "Illegal number" unless ($val =~ /\d$/);
        } elsif ($ch =~ /[a-z\$\^\@\%\&]/i) {

# Seems like a variable
# Unget the char
            $_[0] = $ch . $_[0];
# Get the var using makevar
            ($code, $val) = &makevar2($_[0], $_[1], $_[2]);
# Get the code to dereference the value of the var
            $val = $val->rval;

        } elsif ($ch =~ /['-]/ && ($sum || !$result)) {
# Unary negation operator
# Save the negation for later use
            $ch =~ s/'/!/;
            $negation = $ch;
            next;
        }
# End of char switch

        if ($ch eq '(') {
            ($code, $val) = &makeexp2($_[0], $_[1], $_[2] + 1);
            die "No closing brackets" unless ($_[0] =~ /^\)/);
        }

# We just passed an operand, not a binary operator
        if (defined($val)) {
# Generate assignment
            $result .= $code;
            $result .= "$var = $negation$val;\n";
# Include prepared computation, if any (See below)
            $result .= "$sum\n" if ($sum);
# Clear computation and negation registers
            $sum = undef;
            $negation = undef;
            next;
        }

# If we had a binary operator but found no right operand
        die "Right operand expected" if ($sum);

# We are expecting an operator now

# Allocate a new variable
        my $oldvar = $var;
        $var = &nextvar('$');
        my $qch = quotemeta($ch);
# Handle basic operators
        if ("+-*/!&_#" =~ /$qch/) {
            $ch =~ s/\!/||/; # ! means OR in M
            $ch =~ s/\&/&&/; 
            $ch =~ s/_/./;   # _ is string concatenation
            $ch =~ s/#/%/;   # '#' is modulu
            $sum = "$var $ch= $oldvar;"; # Prepare implied increment
        }
        if ($ch eq "'") {
# This is a negation
            if ($_[0] =~ /^\=\<\>/) {
                $_[0] =~ s/^(.)//;
                $ch = "* -1 $1"; # $qch does not change
            }
        }
        if ("=<>" =~ /$qch/) {
            $ch =~ s/\=/==/;
            $sum = "$var = ($oldvar <=> $var) || ($var cmp $oldvar);\n" .
                   "$var = ($var $ch 0);";
        }
        if ($ch =~ /\[\]/) {
# $oldvar contains $var
            my ($s1, $s2) = ($var, $oldvar);
            ($s2, $s1) = ($var, $oldvar) if ($ch eq '[');
            $sum = "$s2 = quotemeta($s2);\n$var = (($s1 =~ /$s2/) ? 1 : undef);";
        }
        die "Parse error on $ch" unless ($sum)
    }
    die "Right operand expected" if ($sum);
    die "Right bracket expacted $_[2] $_[0]" if ($_[2] && $_[0] =~ /^\s/);
    ("$result", $var);
}

####
## Parse a list, with optional prototype

sub makelist ($) {
    my ($a, $b) = (0, 0);
    makelist2($_[0], $a, $b, $_[1]);
}

sub makelist2 ($$) {
    my ($step);
    my $scope = ++$_[1];
    my ($result, $sum);
# Allocate a variable to store the list

    my $var = &nextvar('@');

# Allocate a label, used for tuple parsing

    my $lbl = "__lbl_$var";

    my $i;
    my $first = 1;

# Generate code to create empty list

    $result = "$var = ();\n";

# Optional prototype parameter

    my $proto = $_[3];
    while ($_[0] && $_[0] !~ /^\s/) {
# Iterate on code line

# Force comme unless first

        die "Comma expected" unless ($first || $_[0] =~ s/^,//);

# If we had a prorotype, used it up, but there still is input - it's
#    a mismatch

        die "Parameter mismatch" if ($_[3] && !$proto);
        my $typ;

# Fetch one prototype char

        $typ = $1 if ($proto =~ s/^(.)//);
        $proto = 'L' if ($typ eq 'L'); # Nothing to validate in a plain
                                       # list, but must keep $proto
                                       # unepmty
        $proto = 'T' if ($typ eq 'T'); # Tuples are length unlimited
        $typ =~ s/[IL]//; # Nothing to validate in a plain input field

# Define handlers to prototypes

        my %procs = (
## Unprototyped field - call makeexp2 to fetch data
                   "", sub($$) {&makeexp2($_[0], $_[1], $_[2])},
## Output field - get variable signature as a second parameter
                  "O", sub ($$) {
                      my ($code, $var) = &makevar2($_[0], $_[1], $_[2]);
                      ($code, $var->sig);},
## Tuples - add a finish condition every candidate
                  "T", sub ($$) {my ($code, $var2) = &maketuple2($_[0],
                             $_[1], $_[2], 2, ":"); 
                     my ($cond, $res) = @$var2;
                     ("$code $var = ($res);\ngoto $lbl if ($cond);", "undef");
                  },
## Source anchor - Line number, Label, or Label + Line number
                  "S", sub ($$) {
   die "Source anchor expected" unless 
            ($_[0] =~ s/^(\d+|(?:[a-z]\w*)?\+\d+|[a-z]\w*)//i);
                    my ($lbl, $off) = split(/\+/, $1);
                    $off *= 1;
                    my $var = &nextvar('$');
                    ("$var = &Language::Mumps::list('$lbl', $off);\n", $var);}
                  );

# Call the corresponding function

        my ($code, $val) = &{$procs{$typ}}($_[0], $_[1], $_[2]);

# Generate code to add to list
        $result .= $code . "push($var, $val);\n";
        ++$i;
        $first = undef;
        if ($_[0] =~ /^\)/) {
            last if ($_[2]);
            die "Unexpected right bracket";
        }
    }
# Add finish label for tuples

    $result .= "$lbl: " if ($proto eq 'T');
    die "Expected right operand" if ($sum);
    ($result, $var, $i);
}

####
## Make a tuple - a series of values and conditions to choose each
## Arguements: Code line, scopes, paranthesis, number of tokens,
## delimiter

sub maketuple ($) {
    my ($a, $b) = (0, 0);
    maketuple2($_[0], $a, $b, $_[1], $_[2]);
}

sub maketuple2 ($$) {
    my ($done, $result);
    ++$_[1];
    my @ary;
    my $first = 1;
    my $delim = quotemeta($_[4]);
    foreach (1 .. $_[3]) {
# Count times, expect delmiters
        die "$_[4] expected" unless ($first || $_[0] =~ s/^$delim//);
        $first = undef;
# Get expression
        my ($code, $var) = &makeexp2($_[0], $_[1], $_[2]);
        my $save = &nextvar('$');
        $result .= $code . "$save = $var;\n";
        push(@ary, $var);
    }
# Return a compile time list of referrences to tuple members
    ($result, \@ary);
}

#####
## Make regexp

# Map M meta chars to perl regexps

%RES = qw(A [a-zA-Z]
          C [\x0-\x1F0xFF]
          E [\x0-\x7F]
          H [\xE0-\xFA]
          L [a-z]
          N \d
          U [A-Z]);
# Prepare an ascii string of all non alphanumeric characters
# in between a white space and lower case 'a'
# Which is M's definition for P

my $s = pack("C*", (ord(' ') + 1 .. ord('a') - 1));
$s =~ tr/a-z0-9A-Z//;
$RES{'P'} = '[' . quotemeta($s) . ']';
$RESKEYS = join("", keys %RES);

sub makeregexp {
    my $result;
    my $src = shift;
    while ($src) {
# Iterate over string
        if ($src =~ s/^([$RESKEYS])//) {
# Is it a meta char?
            $result .= $RES{$1};
        } elsif ($src =~ s/^".*?"//) {
# Did we just find a literal?
            $result .= quotemeta($1);
        } else {
# Unrecognized
            die "Invalid REGEXP char: " . substr($src, 0, 1);
        }

# These are only after recognized tokens

# Dot - 1 to many
        if ($src =~ s/\.//) {
            $result .= '+';
        }
# Number - times
        if ($src =~ s/^(\d+)//) {
            $result .= "{$1}";
        }
    }
    $result;
}

####
## Manufacture a temporary var (register)

sub nextvar {
    my $pre = shift;
    $varstack++;
    my $sc = "_" x $scopes;
    "$pre$sc\__tmp$varstack";
}

####
## Reuse varnames after each statement, in order not to overpopulate
## symbol table

sub resetvars {
    $varstack = 0;
}

#####################################################################
## Runtime utilities                                               ##
#####################################################################

####
## Load Curses module *once* upon request

sub curse {
    require Curses;
    return undef unless (*Curses::new{CODE});
    Curses::initscr() unless ($curses_inside++);
    1;
}

####
## Clear screen or send form feed

sub cls {
    if ($Language::Mumps::selected_io == 5) {
        &curse;
        Curses::clear();
    } else {
        &write("\l");
    }
    ($xpos, $ypos) = (0, 0);
}

####
## Read a char from the keyboard

sub readkey {
    &curse;
    Curses::getch();
}

####
## Buffered input

sub read {
# Choose file number - 5 is STDIO
    my $file = ($selected_io == 5) ? \*STDIN : $handlers[$selected_io];
    my $s = scalar(<$file>);
    chomp $s;
    $xpos = 0;
    $ypos++;
    $s;
}

####
## Output

sub write {
# Choose file number - 5 is STDIO
    my $file = ($selected_io == 5) ? \*STDOUT : $handlers[$selected_io];
    my $item = shift;
# Do nothing for an empty string
    return unless (defined($item));
    if (UNIVERSAL::isa($item, 'ARRAY')) {
        if ($item->[0] eq 'cls') {
            &cls;
            next;
        }
        if ($item->[0] eq 'tab') {
            &tab($item->[1]);
            next;
        }
    }
# Split to lines
    my @frags = ($item eq "\n" ? ('', '') : split(/\n/, $item));
    my $i;
# Iterate over lines
    foreach (@frags) {
# Print line
        print $file $_;
# Increase xpos
        $xpos = ($xpos + length($_));
# Advance line counter
        if (++$i < @frags) {
            print $file "\n";
            $xpos = 0;
            $ypos++;
        } 
    }
}

####
## Tab the basic style

sub tab {
    my $to = shift;
# Are we past the tab point?
    &write("\n") if ($xpos > $to);
    my $dist = $to - $xpos;
    &write(' ' x $dist);
}

##################################################################
## Class loader                                                 ##
##################################################################
## Users of the class should import both the serializer and the ##
## flat file database engine in order to use disk stored arrays ##
## Compiled programs should import Runtime to initialize        ##
##################################################################

sub import {
    my $class = shift;
    my $state;

    foreach $state (@_) {
        if ($state eq "Runtime") {
# Runtime initialize
            tie %symbols, 'Language::Mumps::Tree';
            tie %dbs, 'Language::Mumps::Forest';
            $selected_io = 5;
        } elsif ($state =~ /^[SNG]?DBM?_File$/) {
# Prepare values to tie a DBM engine
            $@ = undef;
            eval "require $state; import $state;";
            die $@ if ($@);
            @TYING = (O_RDWR|O_CREAT, 0644,
                 ($state eq 'DB_File') ? ($DB_File::DB_HASH) : ());
            $DB = $state;

# Choose a serializer
        } elsif ($state eq 'Data::Dumper') {
            $@ = undef;
            eval "require $state; import $state;";
            die $@ if ($@);
            $FETCH = sub {no strict; eval $_[0];};
            $STORE = \&Data::Dumper::Dumper;
            $SER = $state;
        } elsif ($state eq 'Data::Dump') {
            $@ = undef;
            eval "require $state; import $state;";
            die $@ if ($@);
            $STORE = \&Data::Dump::dump;
            $FETCH = sub {no strict; eval $_[0];};
            $SER = $state;
        } elsif ($state eq 'FreezeThaw' || $state eq 'Storable') {
            $@ = undef;
            eval "require $state; import $state;";
            die $@ if ($@);
            $FETCH = \&{"$SER\::thaw"};
            $STORE = \&{"$SER\::freeze"};
            $SER = $state;
        } elsif ($state eq 'XML::Dumper') {
            $@ = undef;
            eval "require XML::Parser; import XML::Parser;";
            eval "require XML::Dumper; import XML::Dumper;";
            die $@ if ($@);
            $Language::Mumps::Pool::XML = new XML::Dumper;
            $FETCH = sub { 
                my $xml = shift;
                return undef unless ($xml);
                my $parser = new XML::Parser(Style => 'Tree');
                my $tree = $parser->parse($xml);
                $Language::Mumps::Pool::XML->xml2pl($tree); };
            $STORE = sub { $Language::Mumps::Pool::XML->pl2xml(shift); };
            $SER = $state;
        } elsif ($state eq 'Data::DumpXML') {
            $@ = undef;
            eval "require Data::DumpXML; import Data::DumpXML;";
            eval "require Data::DumpXML::Parser; import Data::DumpXML::Parser;";
            $Language::Mumps::Pool::XML = Data::DumpXML::Parser->new();
            die $@ if ($@);
            $STORE = \&Data::DumpXML::dump_xml;
            $FETCH = sub { $Language::Mumps::Pool::XML->parse(@_); };
            $SER = $state;
# Read configuration file
        } elsif ($state eq 'Config') {
            require "/etc/pmumps.cf" if (-f "/etc/pmumps.cf");
            require "~/.pmumps" if (-f "~/.pmumps");
# Variables received from configuration file, call import again
            import Language::Mumps ($DB, $SER);
        } else {
# Error
            die "Unrecognized option $state";
        }
    }
# Save DBM and serializer choice
    $IMPORT = join(" ", grep /./, grep {defined}($DB, $SER));
}

####
## Return a tied hash to a named disk stored array

sub dbs {
    my $db = shift;
# Qualified database name
    my $dbt = "Language::Mumps::DB::_$db";
# Qualified tree name
    my $dbf = "Language::Mumps::DB::Back::_$db";;
# Create database directory
    unless (-d "global") { 
        mkdir "global", 0755 || die "Can't create global/: $!";
    }
# Ensure DBM engine was selected
    die "You must configure database storage" unless ($DB);
# Tie the database flat hash
    tie(%$dbf, $DB, "global/$db.db", @TYING) || die "DB: $!";
# Tie the tree hash
    my $t = tie %$dbt, 'Language::Mumps::Tree', \%$dbf, $FETCH,
        $STORE;
# Returned the tied hash
    \%$dbt;
}

####
## Deep copy a tree/subtree to another tree/subtree

sub moveimage {
    my ($src, $dst, $key) = @_;
    $dst->{$key} = $src->{$key};
    my $t = tied(%$src);
    my @children = $t->query($key);
    foreach (@children) {
        &moveimage($src, $dst, "$key\0$_");
    }
}

######################################################################

package Language::Mumps::Tree;

#######################################################
## Tied hash holding a tree.                         ##
#######################################################
## Possible storing and fetching in a flat hash tied ##
## to a database.                                    ##
#######################################################
## The list of access keys is joined with char #0 to ##
## form the relevant key in the flat hash.           ##
## Each node has its children list attached.         ##
#######################################################


####
## Destroy the tree

sub CLEAR {
    my $self = shift;
    my $hash = $self->{'hash'};
    %$hash = ();
}

####
## Store a value in the tree

sub STORE {
    my ($self, $key, $val) = @_;
    my $hash = $self->{'hash'};
    my $store = $self->{'store'};
    my $fetch = $self->{'fetch'};
# Split the access keys
    my @tokens = split(/\0/, $key);
    my @addr;
    my $addr; # Pointer points to root
# Verify the path exists
    do {
# Fetch one token
        my $this = shift @tokens;
        my $flag; # Flag is non zero only if something new
                  # needed to be created
# Release the structure stored in the current pointer
# If none there, $flags increases, and an empty hash returned
        my $base = &$fetch($hash->{$addr}) || ++$flag && {};
# Ensure the existence of the metadata hash
        $base->{'metadata'} ||= ++$flag && {};
# Ensure the next node is marked used
        $base->{'metadata'}->{$this} ||= ++$flag;
        $hash->{$addr} = &$store($base) if ($flag);
# Advance the pointer
        push(@addr, $this);
        $addr = join("\0", @addr);
    } while (@tokens);
# Iterate until all path ensured

    my $flag;
# Fetch the data
    my $base = &$fetch($hash->{$addr}) || ++$flag && {};
    ($base->{'data'} eq $val) || ++$flag && ($base->{'data'} = $val);
# Do not update storage unless value changed, to save time with
# DBM implemented storage
    $hash->{$addr} = &$store($base) if ($flag);
}

####
## Fetch a value from the tree

sub FETCH {
    my ($self, $key) = @_;
    my $hash = $self->{'hash'};
    my $fetch = $self->{'fetch'};
## Fetch the structure
    return undef unless ($hash->{$key});
    my $base = &$fetch($hash->{$key}) || {};
## Extract the data element
    $base->{'data'};
}

####
## Does a node exist?

sub EXISTS {
    my ($self, $key) = @_;
    my $hash = $self->{'hash'};
    my $fetch = $self->{'fetch'};
    return undef unless ($hash->{$key});
    my $base = &$fetch($hash->{$key}) || {};
    (exists $base->{'data'});
}

####
## Return the children list for a node

sub query {
    my ($self, $key) = @_;
    my $hash = $self->{'hash'};
    my $store = $self->{'store'};
    my $fetch = $self->{'fetch'};
    my $base = &$fetch($hash->{$key}) || {};
    keys %{$base->{'metadata'}};
}

####
## Delete a node

sub DELETE {
    my ($self, $key) = @_;
    my $hash = $self->{'hash'};
    my $store = $self->{'store'};
    my $fetch = $self->{'fetch'};
    my $base = &$fetch($hash->{$key}) || {};
    foreach (keys %{$base->{'metadata'}}) {
        $self->DELETE("$key\0$_");
    }
    delete $hash->{$key};
    unless ($key =~ s/\0([^\0]*)$//) {
        $key =~ s/^(.*)$//;
    }
    delete $hash->{$key}->{'metadata'}->{$1};
}

####
## Return a flat hash with all the structures deserialized
## This is needed to implement keys and values functions

sub extrapolate {
    my ($self, $key) = @_;
    my @sons = $self->query($key);
    my %recur = map {$self->extrapolate($_);} @sons;
    $recur{$key} = $self->FETCH($key) if ($self->EXISTS($key));
    %recur;
}

####
## Return the first pair of the tree

sub FIRSTKEY {
    my $self = shift;
    $self->{'keys'} = {$self->extrapolate("")};
    $self->NEXTKEY;
}

####
## Return the next one

sub NEXTKEY {
    my ($self, $lastkey) = @_;
    each %{$self->{'keys'}};
}

####
## Tie a hash to the class
## Default serializing and deserializing functions are equality
## functions, that do not change the values, for memory arrays
## A hash is tied with the storage hash, fetch function and
## stroage function.

sub TIEHASH {
    my ($class, $hash, $fetch, $store) = @_;
    $fetch ||= sub {$_[0];};
    $store ||= sub {$_[0];};
    $hash ||= {};
    my $self = {'hash' => $hash, 'store' => $store, 'fetch' => $fetch};
    bless $self, $class;
}
##################################################################

package Language::Mumps::Entity;

##################################################
## Base class for variable and function classes ##
##################################################

####
## Trivial constructor

sub new {
    bless {}, shift;
}

####
## Return whether names in the class of an object are case sensitive

sub case {
    my $class = ref(shift);
    ${$class . "::CASE"};
}

####
## Set or get the entity name

sub name {
    my $self = shift;
    $self->{'name'} = shift if (@_);
    $self->case ? $self->{'name'} : uc($self->{'name'});
}

####
## Set or get the list of parameters or indices for an entity

sub list {
    my $self = shift;
    $self->{'list'} = shift if (@_);
    $self->{'list'} || '()';
}

####
## Check if the entity has parameters or indices

sub isatom {
    my $self = shift;
    $self->{'list'} ? undef : 1;
}

####
## Return the rvalue representing an entity
## Does not equal to rval in derived classes

sub rval {
    my $self = shift;
    $self->lval;
}

####
## Return the lvalue for a variable
## By default, points to a element in a hash, holding a variable

sub lval {
    my $self = shift;
    '${' . $self->hash . '}{' . $self->addr . '}';
}

####
## Code to erase an entity

sub purge {
     die "Abstract";
}

####
## Return the hash associated with the entity

sub hash {
     die "Abstract";
}

####
## Return the key in the hash the entity is stored in

sub addr {
     die "Abstract";
}

####
## Return a tuple of the hash name and hash key
## Used mainly for providing functions runtime definitions
## of variables

sub sig {
    my $self = shift;
    "(bless [" . $self->hash . ", " . $self->addr . "], 'varsig')";
}

#####################################################################

package Language::Mumps::Var;
use vars qw(@ISA);
@ISA = qw(Language::Mumps::Entity);

#####################################################
## An object to represent an M var in compile time ##
#####################################################

####
## Erase a variable by erasing it from the symbol table

sub purge {
    my $self = shift;
    my $list = $self->list;
    my $name = $self->name;
    "delete \$Language::Mumps::symbols{'$name', $list};";
}

####
## Hash holding regular arrays

sub hash {
    "Language::Mumps::symbols";
}

####
## Address is either symbol name, or symbol name joined with the value of
## the intermediate variable containing the indices

sub addr {
    my $self = shift;
    my $list = $self->list;
    my $name = $self->name;
    $self->isatom ? "'$name'" : qq!join("\\0", '$name', $list)!;
}

#####################################################################

package Language::Mumps::Primitive;
use vars qw(@ISA $CASE);
@ISA = qw(Language::Mumps::Entity);
$CASE = 1;

####################################################
## Object to represent a perl function            ##
####################################################

####
## Lvalue impossible

sub lval {
    die "Can't use functions as Lvalue";
}

####
## Rvalue is calling the function

sub rval {
    my $self = shift;
    my $name = $self->name;
    my $list = $self->list;
    "$name($list);";
}

#######################################################################

package Language::Mumps::Database;
use vars qw(@ISA);
@ISA = qw(Language::Mumps::Entity);

###########################################################
## Object to represent a variable in a disk stored array ##
###########################################################


####
## Deleteion will be realized using DELETE in the tied hash

sub purge {
    my $self = shift;
    my $list = $self->list;
    my $name = $self->name;
    "delete \$Language::Mumps::dbs{'$name'}->{$list}";
}

####
## Local method to return code to dereference the DBM object (not tied
## hash) tied to the array
## Used in the LOCK function

sub getdb {
    my $self = shift;
    my $name = $self->name;
    "tied(\%{tied(\$Language::Mumps::dbs{'$name'})->{'hash'}})";
}

####
## Return the tree attached to the var

sub hash {
    my $self = shift;
    my $name = $self->name;
    "\$Language::Mumps::dbs{'$name'}";
}

####
## Return the access key to the Tree hash

sub addr {
    my $self = shift;
    my $list = $self->list;
    qq!join("\\0", $list)!;
}

####################################################################

package Language::Mumps::Freevar;
use vars qw(@ISA $CASE);
@ISA = qw(Language::Mumps::Entity);
$CASE = 1;

####################################################
## Object representing a raw perl var             ##
####################################################


####
## Lvalue is a scalar if no keys, otherwise hash with a key

sub lval {
    my $self = shift;
    my $name = $self->name;
    $self->isatom ? "\$$name" : $self->SUPER::lval;
}

####
## Hash name is raw

sub hash {
    my $self = shift;
    $self->name;
}

####
## Joined keys, supported in perl notation as well

sub addr {
    my $self = shift;
    my $list = $self->list;
    qq!join("\\0", $list)!;
}

####################################################################

package Language::Mumps::Func;
use vars qw(@ISA @zwi_tokens);
@ISA = qw(Language::Mumps::Entity);

############################################
## Object to represent an M function call ##
############################################

####
## Set or get the prototype

sub prot {
    my $self = shift;
    $self->{'prot'} = shift if (@_);
    $self->{'prot'};
}

####
## Return Lvalue if applicable

sub lval {
    my $self = shift;
    my $name = $self->name;
    my $prot = $self->prot;
    my $opt = $Language::Mumps::FUNS{$name};
    my $rec;
# Search for the metadata entry fitting the choosed prototype

    foreach $rec (@$opt) {
        last if ($rec->{'prot'} eq $prot);
    }
    die "Lvalue unavailable for function $name" unless ($rec->{'lval'});
# Call the local function to return the Lvalue for this function
    &{"l_$name"}($self);
}

####
## Rvalue - generate code to call the runtime function

sub rval {
    my $self = shift;
    my $name = $self->name;
    my $list = $self->list;
    "&Language::Mumps::Func::$name($list)";
}

####
## $ASCII(string, position = 1) - return ASCII of one char (1 based)

sub ASCII {
    my ($str, $pos) = @_;
    $pos -= ($pos && 1);
    my $ch = substr($str, $pos, 1);
    $ch ? -1 : ord($ch);
}

####
## $CHAR(list) Convert ASCII codes to string

sub CHAR {
    pack("C*", @_);
}

####
## $DATA(array(index,index...))
## Left digit - does it have children? Right digit - does it exist?

sub DATA {
    my ($hash, $addr) = @{$_[0]};
    my $d0 = defined($hash->{$addr});
    my $d1 = scalar(tied(%$hash)->query($addr));
    $d1 * 10 + $d0;
}

####
## $EXTRACT(string, from, to = from) - substring, 1 based locations

sub EXTRACT {
    my ($str, $from, $to) = @_;
    $to ||= $from;
    substr($str, $from - 1, $to - $from + 1);
}

####
## $FIND(long string, short string, start = 1) - find substring

sub FIND {
    my ($str, $sub, $pos) = @_;
    $pos -= ($pos && 1);
    index($str, $sub, $pos);
}

####
## $HOROLOG - Sailor time function (Works for Y2K, will not work after
## 2100)

sub HOROLOG {
    my $years = 1970 - 1841;
    my $leaps = int($years / 4) - 1;
    my $distance = 1 + 365 * $years + $leaps;
    my $now = time;
    my @here = localtime($now);
    my @gmt = gmtime($now);
    my $here = $here[1] + 60 * $here[2];
    my $gmt = $gmt[1] + 60 * $gmt[2];
    my $offset = 60 * ($here - $gmt);
    my $there = $now + $offset;
    my $n1 = int($there / 3600 / 24) + $distance;
    my $n2  = $gmt * 60 + $gmt[0];
    "$n1,$n2";
}

####
## $IO - Currently selected IO channel

sub IO {
    $Language::Mumps::selected_io;
}

sub l_IO {
    '$Language::Mumps::selected_io';
}

####
## $JOB - process id

sub JOB {
    $$;
}

####
## $JUSTIFY(string, length, decimal fraction length) - Right justify.
##     If third parameter is non zero, trailing zeroes are added
##      for numbers.
##

sub JUSTIFY {
    my ($str, $ln, $dec) = @_;
    $str = sprintf("%.${dec}d", $str) if ($dec);
    my $l = $ln - length($str);
    ($l > 0 ? (" " x $ln) : "") . $str;
}

####
## $LEN(string) - Length
## $LEN(string, substring) - How many times substring exists in string

sub LEN {
    my ($str, $token) = @_;
    $token = quotemeta($token) || ".";
    scalar($str =~ s/($token)//g);
}

####
## $NEXT(array(indices...,rightmost index))
## Returns the rightmost index of the array element
## whose rightmost index comes right after the parameter.
## Use array(indices...,-1) to find the first element
## Returns -1 on failure.
## M design bug: -1 is a valid key for an array

sub NEXT {
    my ($hash, $addr) = @{$_[0]};
    my @tokens = split(/\0/, $addr);
    my $right = pop @tokens;
    my @sons = sort (tied(%$hash)->query(join("\0", @tokens)));
    return -1 unless (@sons);
    return $sons[0] if ($right == -1);
    foreach (@sons) {
        return $_ if ($_ gt $right);
    }
    return -1;
}

####
## $ORDER - simillar to NEXT, but with numeric and not lexicographic
##   order

sub ORDER {
    my ($hash, $addr) = @{$_[0]};
    my @tokens = split(/\0/, $addr);
    my $right = pop @tokens;
    my @sons = sort {$a <=> $b} @{tied(%$hash)->query(join("\0", @tokens))};
    foreach (@sons) {
        return $_ if ($_ >= $right || $right == -1);
    }
    return -1;
}

####
## $PIECE(string, delimiter, $from, $to) - Points to to a specific
##   token in a delimited list, or to a range of tokens, including
##   the dleimiters.

sub PIECE {
    my ($str, $delim, $from, $to) = @_;
    if (ref($str) eq 'varsig') {
        my ($hash, $addr) = @$str;
        $str = $hash->{$addr};
    }
    my $qdelim = quotemeta($delim);
    my @tokens = split(/$qdelim/, $str);
    $to ||= $from;
    join($delim, @tokens[($from - 1) .. ($to - 1)]);
}

sub l_PIECE {
    my $list = shift;
    "\${&Language::Mumps::Func::tiePIECE($list)}";
}

sub tiePIECE {
    my $scalar;
    tie $scalar, 'Language::Mumps::Piece', @_;
    \$scalar;
}

####
## $RANDOM(max) - integer random

sub RANDOM {
    my $max = shift;
    int(rand($max));
}

####
## $SELECT(val1:cond1,val2:cond2...)
## Receives pairs of value:condition. Returns the first value for which
## the condition is true
##>> Actual work is done by the tokenizer in makelist2

sub SELECT {
    $_[0];
}

####
## $TEST - The test flag

sub TEST {
    $Language::Mumps::flag;
}

sub l_TEST {
    '$Language::Mumps::flag';
}

## No idea what this is doing here

sub TEXT {
    $_[0];
}

####
## $X - The x position register

sub X {
    \$Language::Mumps::xreg[\$Language::Mumps::selected_io]
}

####
## $Y - The Y position register

sub Y {
    \$Language::Mumps::yreg[\$Language::Mumps::selected_io]
}

########################################################
## Z* Functions are not part of the M specification   ##
##    and are mostly copied from MumpsVM              ##
########################################################

####
## $ZAB(number) - Absolute value

sub ZAB {
    abs(shift);
}

####
## $ZB(string) - Trims spaces

sub ZB {
    $_ = shift;
    s/^\s*//;
    s/\s*$//;
    s/\s+/ /;
    $_;
}

####
## $ZCD(filename)
## Data dumper, for backup and database garbage collection
## Weird API taken from MumpsVM
## If filename is omitted, 8 leftmost digits of UCT time are taken
## with the suffix .dmp
## Dumps all the databases to a text file using the serializer
## Returns the filename

sub ZCD {
    my $fn = shift || substr(time, 0, 8) . ".dmp";
    my $forest = {};
    $! = undef;
# Iterate over the database directory
# Have the hash $forest have references to all the databases

    foreach ((glob "global/*.db"), (glob "global/*.db.*")) {
        s|^global/||;
        s/\.db(\..*)?$//;
#        next if ($forest->{$_});
        eval {
            $forest->{$_} = {%{$Language::Mumps::dbs{$_}}};
        };
    }
    open(DUMP, ">$fn");
    print DUMP &$Language::Mumps::STORE($forest);
    close(DUMP);
# Remove links to unused databases, to free memory
    foreach (values %$forest) {
        my $hash = tied(%$_)->{'hash'};
        untie %$hash;
        undef %$hash;
        untie %$_;
        undef %$_;
    }
    %Language::Mumps::dbs = ();
    $fn;
}

####
## $ZCL - Weird API from MumpsVM
## Restore databases from a dumped file

sub ZCL {
    my $fn = shift || "dump";
    %Language::Mumps::dbs = ();
    open(LOAD, $fn);
    binmode LOAD;
    my $buffer;
    while (read(LOAD, $buffer, 8192, length($buffer))) {}
    close(LOAD);
    my $forest = &$Language::Mumps::FETCH($buffer);
    undef $buffer;
    foreach (keys %$forest) {
        unlink "global/$_.db";
        %{$Language::Mumps::dbs{$_}} = %{$forest->{$_}};
    }
# Remove links to unused databases, to free memory
    foreach (values %$forest) {
        my $hash = tied(%$_)->{'hash'};
        untie %$hash;
        undef %$hash;
        untie %$_;
        undef %$_;
    }
    %Language::Mumps::dbs = ();
}

############################
## Date functions         ##
## API taken from MumpsVM ##
############################

####
## $ZD - Readable local time

sub ZD {
    scalar(localtime);
}

####
## $ZD1 - UTC

sub ZD1 {
    time;
}

####
## $ZD2(utc) - Convert to readable string

sub ZD2 {
    scalar(localtime(shift));
}

####
## $ZD3(year, month, day) - Return day of the year

sub ZD3 {
    my ($y, $m, $d) = @_;
    require Time::Local;
    my $t = Time::Local::timelocal(0, 0, 0, $d, $m - 1, $y - 1900);
    my @t = localtime($t);
    $t[7] + 1;
}

####
## $ZD(year, day of the year) - Returns y + " " + m + " " + d string
## Hint: use $PIECE

sub ZD4 {
    my ($y, $dy) = @_;
    my @mon = qw(31 28 31 30 31 30 31 31 30 31 30 31);
    my $m;
    while ($dy > $mon[$m]) {$dy -= $mon[$m++];}
    join(" ", $y, $m + 1, $dy);
}

####
## $ZD5(year, month, day) - Returns year + "," + year day + "," +
##                (week day - 1)

sub ZD5 {
    my ($y, $m, $d) = @_;
    require Time::Local;
    my $t = Time::Local::timelocal(0, 0, 0, $d, $m - 1, $y - 1900);
    my @t = localtime($t);
    join(",", $y, $t[7] + 1, $t[6]);
}

####
## $ZD6(utc = now) - returns Ho:Mi clock time

sub ZD6 {
    my $t = (shift) || time;
    my @t = localtime($t);
    sprintf("%2d:%02d", $t[2], $t[1]);
}

####
## $ZD7(utc = now) Returns y-m-d

sub ZD7 {
    my $t = (shift) || time;
    my @t = localtime($t);
    join("-", $t[5] + 1900, $t[4] + 1, $t[3]);
}

####
## $ZD8(utc) returns y-m-d,Ho:Mi

sub ZD8 {
    my $t = shift;
    &ZD7($t) . "," . &ZD6($t);
}

####
## $ZD9(utc = now) returns y-m-d,week day - 1,Ho:Mi

sub ZD9 {
    my $t = (shift) || time;
    my @t = localtime($t);
    join(",", &ZD7($t), $t[6], &ZD6($t));
}

########################################
## $DBI - Perl oriented data access   ##
########################################
## $ZDBI(dsn, user, pass, select query, array)
## Performs query. Result API taken from MumpsVM's ZODBC
## Array in 5th parameter get the record number (1 based)
## per any key combination representing the ordered fields.
## This allows you to navigate using the function $NEXT
## Array %tpl gets the keys joined by a backslash in each index
## which is equal to the row number.

sub ZDBI {
    my ($dsn, $u, $p, $query, $ary) = @_;
    require DBI;
    import DBI;
    my $dbh = DBI->connect($dsn, $u, $p);
    my $sth = $dbh->prepare($query) || die $DBI::errstr;
    $sth->execute || die $DBI::errstr;
    my ($i, $rec, $glb);
    $glb = $Language::Mumps::dbs{$1} if ($ary =~ /^\^(.*)$/);
    
    while ($rec = $sth->fetchrow_array) {
        $Language::Mumps::symbol{"%tpl", ++$i} = join("\\", @$rec);
        unless ($glb) {
            $Language::Mumps::symbol{$ary, @$rec} = $i;
        } else {
            $glb->{@$rec} = $i;
        }
    }
    $sth->finish;
    $i;
}

####
## $ZF(filename) - true if file exists

sub ZF {
    (-f shift);
}

####
## $ZH(string) - HTTP encodes

sub ZH {
    my $s = shift;
    $s =~ s/([^ a-zA-Z0-9])/sprintf("%%%02x", $1)/ge;
    $s =~ s/ /+/g;
    $s;
}

####
## $ZL(num) = ln(num)
## $ZL(string, len) = Left justify

sub ZL {
    my ($a1, $a2) = @_;
    return ln($a1) unless (defined($a2));
    substr($a1 . (" " x $a2), 0, $a2);
}

####
## $ZN(string) - Qualify as a database name
## All letters converted uppercase, all non alphanumeric
## characters removed

sub ZN {
    my $s = uc(shift);
    $s =~ s/\W//g;
    $s;
}

####
## $ZP(string, len) - Left justify

sub ZP {
    my ($a1, $a2) = @_;
    substr($a1 . (" " x $a2), 0, $a2);
}

####
## $ZR(x) - Square root

sub ZR {
     sqrt(shift);
}

####
## $ZS(Shell command) - Executes a command sending the output

sub ZS {
    &Language::Mumps::write(`$_[0]`);
}

####
## $ZSQR(num) - Power of two

sub ZSQR {
     my $x = shift;
     $x * $x;
}

####
## $ZT(file nadler) - The position of the cursor

sub ZT {
    my $file = ($Language::Mumps::selected_io == 5) ? \*STDIN : $Language::Mumps::handlers[$Language::Mumps::selected_io];
    tell($file);
}

####
## $ZVARIABLE(name) - Returns a Perl scalar with that name

sub ZVARIABLE {
    ${scalar(caller) . '::' . $_[0]};
}

####
## $ZV1(name) - Checks if the name is an apropriate identifier

sub ZV1 {
    $_[0] =~ /^[a-z]\w*$/;
}

####
## $ZWI(string) loads the token stack with space delimited tokens
## from a string

sub ZWI {
    @zwi_tokens = split(/\s+/, shift);
}

####
## $ZWN Pulls a token from the token stack

sub ZWN {
    shift @zwi_tokens;
}

##################################################################

package Language::Mumps::Piece;

##################################################
## Class to implement the Lvalue $PIECE binding ##
##################################################

# Tie the parameters

sub TIESCALAR {
    my $class = shift;
    bless [@_], $class;
}

# Fetch the $PIECE

sub FETCH {
    my $self = shift;
    &Language::Mumps::Func::PIECE(@$self);
}

# Store

sub STORE {
    my ($self, $val) = @_;
    my ($var, $delim, $from, $to) = @$self;
    $to ||= $from;
    my ($hash, $addr) = @$var;
    my $str = $hash->{$addr};
    $delim = quotemeta($delim);
    my @tokens = split(/$delim/, $str);
    splice(@tokens, $from - 1 , $to - $from - 1, $val);
    $str = join($delim, @tokens);
    $hash->{$addr} = $str;
}

###############################################################

package Language::Mumps::Forest;

#############################################
## Class to implement a grove (aka forest) ##
#############################################


sub TIEHASH {
    bless {'dbs' => {}}, shift;
}

sub FETCH {
    my ($self, $key) = @_;
    my $dbs = $self->{'dbs'};
    $dbs->{$key} ||= &Language::Mumps::dbs($key);
    $dbs->{$key};
}

sub DELETE {
    my ($self, $key) = @_;
    my $dbs = $self->{'dbs'};
    my $hash = $dbs->{$key};
    untie %$hash;
}

sub CLEAR {
    my ($self, $key) = @_;
    my $dbs = $self->{'dbs'};
    my $hash;
    foreach $hash (keys %$dbs) {
        untie %$hash;
    }
    delete $self->{'dbs'};
}
__END__

__END__
# Documentation