left-icon

Implementing a Custom Language Succinctly®
by Vassili Kaplan

Previous
Chapter

of
A
A
A

CHAPTER 5

Exceptions and Custom Functions

Exceptions and Custom Functions


“The primary duty of an exception handler is to get the error out of the lap of the programmer and into the surprised face of the user.”

        Verity Stob

In this chapter we are going to see how to implement custom functions and methods in CSCS. We’ll also see how to throw and catch an exception, since when implementing exception stack, we need to use the information about functions and methods being called (“on the stack”).

Related functions and methods are usually implemented together, in the same file, so it would be a nice feature to include code from different files. We’ll see how to do that next.

Including files

To implement including file functionality, we use the same algorithm we already used with other functions—making the implementation in a class deriving from the ParserFunction class.

The IncludeFile derives from the ParserFunction class (see also the UML diagram in Figure 4). Check out its implementation in Code Listing 40.

Code Listing 40: The implementation of the IncludeFile class

class IncludeFile : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    string filename = Utils.GetItem(script).AsString();
    string[] lines = File.ReadAllLines(filename);

    string includeFile = string.Join(Environment.NewLine, lines);
    Dictionary<intint> char2Line;
    string includeScript = Utils.ConvertToScript(includeFile,

                                                 out char2Line);
    ParsingScript tempScript = new ParsingScript(includeScript, 0,

                                                 char2Line);
    tempScript.Filename = filename;
    tempScript.OriginalScript = string.Join(                                      

                            Constants.END_LINE.ToString(), lines);

    while (tempScript.Pointer < includeScript.Length) {
      tempScript.ExecuteTo();
      tempScript.GoToNextStatement();
    }
    return Variable.EmptyInstance;
  }
}

The Utils.GetItem method is, on one hand, just a wrapper over the Parser.SplitAndMerge method. Additionally, it takes care of the string expressions between quotes. On the other hand, it also converts an expression between curly braces to an array.

Code Listing 41: The implementation of the GetItem method

public static Variable GetItem(ParsingScript script)
{
  script.MoveForwardIf(Constants.NEXT_ARG, Constants.SPACE);
  Utils.CheckNotEnd(script);


  Variable value = new Variable();
  bool inQuotes = script.Current == Constants.QUOTE;

  if (script.Current == Constants.START_GROUP)  {
    // We are extracting a list between curly braces.
    script.Forward()// Skip the first brace.
    bool isList = true;
    value.Tuple = GetArgs(script, Constants.START_GROUP,

                          Constants.END_GROUP, out isList);
    return value;
  }  

  else {
    // A variable, a function, or a number.
    Variable var = script.Execute(Constants.NEXT_OR_END_ARRAY);
    value.Copy(var);
  }

  if (inQuotes) {
    script.MoveForwardIf(Constants.QUOTE);
  }


  script.MoveForwardIf(Constants.SPACE);
  return value;
}

In addition, the IncludeFile.Evaluate method invokes the Utils.ConvertToScript method, shown in Code Listing 42.

It’s actually one of the key methods in understanding how the CSCS language works. It shows the first, preprocessing step of the script to be parsed. Basically, the method translates the passed string to another string, which our parser can understand. Among other things, this method removes all the text in comments and all unnecessary blanks, like spaces, tabs, or new lines.

Code Listing 42: The implementation of the Utils.ConvertToScript method

public static string ConvertToScript(string source,

                                     out Dictionary<intint> char2Line) 

{
  StringBuilder sb = new StringBuilder(source.Length);
  char2Line = new Dictionary<intint>();

  bool inQuotes        = false;
  bool spaceOK         = false;
  bool inComments      = false;
  bool simpleComments  = false;
  char previous        = Constants.EMPTY;
  int parentheses      = 0;
  int groups           = 0;
  int lineNumber       = 0;
  int lastScriptLength = 0;

  for (int i = 0; i < source.Length; i++)  {
    char ch = source[i];
    char next = i + 1 < source.Length ? source[i + 1] : Constants.EMPTY;
   if (ch == '\n') {
      if (sb.Length > lastScriptLength) {
        char2Line[sb.Length - 1] = lineNumber;
        lastScriptLength = sb.Length;
      }
      lineNumber++;
    }
    if (inComments && ((simpleComments && ch != '\n') ||
                      (!simpleComments && ch != '*'))) {
      continue;
    }

    switch (ch) {
      case '/':
        if (inComments || next == '/' || next == '*')  {
          inComments = true;
          simpleComments = simpleComments || next == '/';
          continue;
        }
        break;
      case '*':
        if (inComments && next == '/') {
          i++; // skip next character
          inComments = false;
          continue;
        }
        break;
      case '“':
      case '”':
      case '"':
        ch = '"';
        if (!inComments && previous != '\\') {

          inQuotes = !inQuotes;
        }
        break;
      case ' ':
        if (inQuotes) {
          sb.Append (ch);
        } else {
          spaceOK = KeepSpace(sb, next)|| (previous != Constants.EMPTY &&

                             previous != Constants.NEXT_ARG && spaceOK);
          bool spaceOKonce = KeepSpaceOnce(sb, next);
          if (spaceOK || spaceOKonce) sb.Append(ch);
        }
        continue;
      case '\t':
      case '\r':
        if (inQuotes) sb.Append(ch);
        continue;
      case '\n':
        if (simpleComments) {
          inComments = simpleComments = false;
        }
        spaceOK    = false;
        continue;
      case Constants.END_ARG:
        if (!inQuotes) {
          parentheses--;
          spaceOK = false;
        }
        break;
      case Constants.START_ARG:
        if (!inQuotes) parentheses++;
        break;
      case Constants.END_GROUP:
        if (!inQuotes) {
          groups--;
          spaceOK = false;
        }
        break;
      case Constants.START_GROUP:
        if (!inQuotes) groups++;
        break;
      case Constants.END_STATEMENT:
        if (!inQuotes) spaceOK = false;
        break;
      defaultbreak;
    }
    if (!inComments) {
      sb.Append(ch);
    }
    previous = ch;
  }
  // Here we can throw an exception if the “parentheses” is not 0.

  // Same for “groups”. Nonzero means there are some unmatched

  // parentheses or curly braces.
  return sb.ToString();
}

The Utils.CovertToScript method uses an auxiliary char2Line dictionary. It’s needed to have a reference to the original line numbers in the parsing script in case an exception is thrown (or the code is just wrong), so that the user knows in which line the problem occurred. We’ll see it in more detail in the "Getting line numbers where errors occur” section in Chapter 7: Localization.

If you want to define a new style for comments, it will be in this method. This method also allows the user to have different characters for a quote character: “ and ” characters are both replaced by the " character, which is the only one that our parser understands.

How do we know which spaces must be removed and which not? First of all, if we are inside of quotes, we leave everything as is, because the expression is just a string value.

In other cases, we leave at most one space between tokens. For some functions we need spaces to separate tokens, but for others, spaces aren’t needed and tokens are separated by other characters, for example, by an opening parenthesis.

An example of a function that requires a space as a separation token is a change directory function: cd C:\Windows. A function that doesn’t require a space to separate tokens is any mathematical function, for instance, in sin(2*10). A space between the sine function and the opening parenthesis is never needed.

Code Listing 43 contains auxiliary functions used in the CovertToScript method that determine whether we need to keep a space or not.

Code Listing 43: The implementation of the keeping space functions

public static bool EndsWithFunction(string buffer, List<string> functions)
{
  foreach (string key in functions) {
    if (buffer.EndsWith(key, StringComparison.OrdinalIgnoreCase)) {
      char prev = key.Length >= buffer.Length ?
        Constants.END_STATEMENT :
        buffer [buffer.Length - key.Length - 1];
      if (Constants.TOKEN_SEPARATION.Contains(prev)) {
        return true;
      }
    }
  }
  return false;
}

public static bool SpaceNotNeeded(char next)
{
  return (next == Constants.SPACE || next == Constants.START_ARG ||
    next == Constants.START_GROUP || next == Constants.START_ARRAY ||
    next == Constants.EMPTY);
}

public static bool KeepSpace(StringBuilder sb, char next)
{
  if (SpaceNotNeeded(next)) {
    return false;
  }

  return EndsWithFunction(sb.ToString()Constants.FUNCT_WITH_SPACE);
}

public static bool KeepSpaceOnce(StringBuilder sb, char next)
{
  if (SpaceNotNeeded(next)) {
    return false;
  }
  return EndsWithFunction(sb.ToString()Constants.FUNCT_WITH_SPACE_ONCE);
}

As you can see, we distinguish between two types of functions that require a space as a separation token. See Code Listing 44.

Code Listing 44: Functions allowing spaces as separation tokens

// Functions that allow a space separator after them, on top of the

// parentheses. The function arguments may have spaces as well,

// e.g. “copy a.txt b.txt”

public static List<string> FUNCT_WITH_SPACE = new List<string> {
  APPENDLINE, CD, CONNECTSRV, COPY, DELETE, DIR, EXISTS,

  FINDFILES, FINDSTR, FUNCTION, MKDIR, MORE, MOVE, PRINT, READFILE, RUN,

  SHOW, STARTSRV, TAIL, TRANSLATE, WRITE, WRITELINE, WRITENL
};

// Functions that allow a space separator after them, on top of the

// parentheses, but only once, i.e. function arguments are not allowed

// to have spaces between them e.g. return a*b; throw exc;
public static List<string> FUNCT_WITH_SPACE_ONCE = new List<string> {
  RETURN, THROW
};

Basically, we want to maintain two modes of operation for our language—a command-line language (or a “shell” language, in Unix terms), which uses mostly spaces as a separation criterion between tokens, and a regular, scripting language, which uses parentheses as a separation criterion.

Let’s return to the implementation of the IncludeFile class in Code Listing 40. Once we’ve converted the string containing the script to a format that we understand (in Code Listing 42), we go over each statement of the script and apply the whole Split-and-Merge algorithm to each statement.

To move to the next statement in the script, we use the ParsingScript.GoToNextStatement auxiliary method (as seen in Code Listing 18). In particular, it deals with cases when the last processed statement is also the last one in a group of statements (between the curly braces), or when we need to get rid of the character separating different statements (defined as END_STATEMENT = ';' in the Constants class).

Throwing an exception

To throw an exception, we use the same approach we already used with other control flow functions: we implement the ThrowFunction class as a ParserFunction class. The ThrowFunction class is included in Figure 4, and its implementation is in Code Listing 45.

Code Listing 45: The implementation of the ThrowFunction class

class ThrowFunction : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    // 1. Extract what to throw.
    Variable arg = Utils.GetItem(script);

    // 2. Convert it to a string.
    string result = arg.AsString();

    // 3. Throw it!
    throw new ArgumentException(result);
  }
}

This class is registered with the parser as follows:

public const string THROW = "throw"; ParserFunction.RegisterFunction(Constants.THROW, new ThrowFunction());

This means that as soon as our parser sees something like:

throw "Critical exception!";

In CSCS code, our C# code will throw an exception. How can we catch it?

Catching exceptions

To catch an exception, we must have a try block. The processing of the catch module will follow the processing of the try block. The class TryBlock is also derived from the ParserFunction class; see Figure 4. Its implementation is in Code Listing 46. The main functionality is in the Interpreter class, where we can reuse the already implemented ProcessBlock (Code Listing 17), SkipBlock (Code Listing 19), and SkipRestBlocks (Code Listing 20) methods.

Code Listing 46: The implementation of the try and catch functionality

class TryBlock : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    return Interpreter.Instance.ProcessTry(script);
  }
}

internal Variable ProcessTry(ParsingScript script)
{
  int startTryCondition = script.Pointer - 1;
  int currentStackLevel = ParserFunction.GetCurrentStackLevel();

  Exception exception   = null;
  Variable result = null;
  try {
    result = ProcessBlock(script);
  } catch(ArgumentException exc) {
    exception = exc;
  }

  if (exception != null ||
      result.Type == Variable.VarType.BREAK ||
      result.Type == Variable.VarType.CONTINUE) {
    // We are here from the middle of the try-block either because
    // an exception was thrown or because of a Break/Continue. Skip it.
    script.Pointer = startTryCondition;
    SkipBlock(script);
  }

  string catchToken = Utils.GetNextToken(script);
  script.Forward()// skip opening parenthesis
  // The next token after the try block must be a catch.
  if (!Constants.CATCH_LIST.Contains(catchToken))  {
    throw new ArgumentException("Expecting a 'catch()' but got [" +
                                catchToken + "]");
  }

  string exceptionName = Utils.GetNextToken(script);
  script.Forward()// skip closing parenthesis

  if (exception != null) {
    string excStack = CreateExceptionStack(exceptionName,

                                           currentStackLevel);
    ParserFunction.InvalidateStacksAfterLevel(currentStackLevel);
    GetVarFunction excFunc = new GetVarFunction(

                             new Variable(exception.Message + excStack));
    ParserFunction.AddGlobalOrLocalVariable(exceptionName, excFunc);

    result = ProcessBlock(script);
    ParserFunction.PopLocalVariable(exceptionName);
  } else {
    SkipBlock (script);
  }
        
  SkipRestBlocks(script);
  return result;
}

The TryBlock class is registered with the parser as follows:

public const string TRY = "try";

ParserFunction.RegisterFunction(Constants.TRY, new TryBlock());

When we catch an exception, we then also create an exception stack; see Code Listing 47.

Code Listing 47: The implementation of the Interpreter.CreateExceptionStack method

static string CreateExceptionStack(string exceptionName,

                                   int lowestStackLevel) 

{
  string result = "";
  Stack<ParserFunction.StackLevel> stack = ParserFunction.ExecutionStack;
  int level = stack.Count;


  foreach (ParserFunction.StackLevel stackLevel in stack) {
    if (level-- < lowestStackLevel) {
      break;
    }
    if (string.IsNullOrWhiteSpace(stackLevel.Name)) {
      continue;
    }
    result += Environment.NewLine + "  " + stackLevel.Name + "()";
  }

  if (!string.IsNullOrWhiteSpace (result)) {
    result = " --> " + exceptionName + result;
  }

  return result;
}

In order to use the exception data in CSCS, we add a variable containing exception information. This variable is the GetVarFunction class, which we add to the parser:

  ParserFunction.AddGlobalOrLocalVariable(exceptionName, excFunc);

The excFunc variable is of type GetVarFunction; see its implementation in Code Listing 48. The GetVarFunction class is just a wrapper over the exception being thrown. We register it with the parser using the exception name, so that as soon as the CSCS code accesses the exception by its name, it gets the exception information that we supplied. You can easily add more fancy things to the exception information there, like having separate fields for the exception name and the exception stack. We’ll see some examples of exceptions at the end of this chapter.

Code Listing 48: The implementation of the GetVarFunction class

class GetVarFunction : ParserFunction
{
  internal GetVarFunction(Variable value)
  {
    m_value = value;
  }


  protected override Variable Evaluate(ParsingScript script)
  {

    return m_value;
  }

  private Variable m_value;

}

To make everything work, we have defined a few new data structures to the ParserFunction class. In particular, the StackLevel class contains all the local variables used inside of a CSCS function; see Code Listing 49.

The Stack<StackLevel> s_locals member holds a stack having the local variables for each function being called on the stack. The AddLocalVariable and AddStackLevel methods add a new local variable and, correspondingly, a new StackLevel.

Note: This is the place where you want to disallow local names if there is already a global variable or function with the same name.

The Dictionary<string, ParserFunction> s_functions member holds all the global variables and functions (in CSCS all variables and functions are the same; both derive from the ParserFunction class). The keys to the dictionary are the function or variable names. The RegisterFunction and AddGlobal methods both add a new variable or a function. There is also the isNative Boolean flag, which indicates whether the function is implemented natively in C# or is a custom function implemented in CSCS.

When trying to associate a function or a variable name to the actual function or variable, the GetFunction is called. Note that it first searches the local names—they have a precedence over the global names.

Code Listing 49: The implementation of the global and local variables in the ParserFunction class

public class ParserFunction
{

  public class StackLevel {
    public StackLevel(string name = null) {
      Name = name;
      Variables = new Dictionary<stringParserFunction>();
    }
    public string Name { getset}
    public Dictionary<stringParserFunction> Variables { getset}
  }

  // Global functions and variables:
  private static Dictionary<stringParserFunction> s_functions =

             new Dictionary<stringParserFunction>();

  // Local variables:
  // Stack of the functions being executed:
  private static Stack<StackLevel> s_locals = new Stack<StackLevel>();
  public  static Stack<StackLevel> ExecutionStack {

                                   get { return s_locals; } }

  public static ParserFunction GetFunction(string item)
  {
    ParserFunction impl;
    // First search among local variables.
    if (s_locals.Count > 0) {
     Dictionary<stringParserFunction> local = s_locals.Peek().Variables;
      if (local.TryGetValue(item, out impl)) {
        // Local function exists (a local variable).
        return impl;
      }
    }
    if (s_functions.TryGetValue(item, out impl)) {
      // A global function exists and is registered

      // (e.g. pi, exp, or a variable).
      return impl.NewInstance();
    }

    return null;
  }

  public static bool FunctionExists(string item)
  {
    bool exists = false;
    // First check if the local function stack has this variable defined.
    if (s_locals.Count > 0) {
      Dictionary<stringParserFunction> local = s_locals.Peek().Variables;
      exists = local.ContainsKey(item);
    }

    // If it is not defined locally, then check globally:
    return exists || s_functions.ContainsKey(item);
  }


  public static void AddGlobalOrLocalVariable(string name,

                                              ParserFunction function) {
    function.Name = name;
    if (s_locals.Count > 0) {
      AddLocalVariable(function);
    } else {
      AddGlobal(name, function, false /* not native */);
    }
  }


  public static void RegisterFunction(string name, ParserFunction function,
                                      bool isNative = true) {
    AddGlobal(name, function, isNative);
  }


  static void AddGlobal(string name, ParserFunction function,
                        bool isNative = true) {
    function.isNative = isNative;
    s_functions[name] = function;
    if (string.IsNullOrWhiteSpace(function.Name)) {
      function.Name = name;
    }
    if (!isNative) {
      Translation.AddTempKeyword(name);
    }

  }

  public static void AddLocalVariables(StackLevel locals)
  {
    s_locals.Push(locals);
  }


  public static void AddStackLevel(string name)
  {
    s_locals.Push(new StackLevel(name));
  }
  public static void AddLocalVariable(ParserFunction local)
  {
    local.m_isGlobal = false;
    StackLevel locals = null;
    if (s_locals.Count == 0) {
      locals = new StackLevel();
      s_locals.Push(locals);
    } else {
      locals = s_locals.Peek();
    }
    locals.Variables[local.Name] = local;
    Translation.AddTempKeyword(local.Name);
  }


  public static void PopLocalVariables()
  {
    s_locals.Pop();
  }


  public static int GetCurrentStackLevel()
  {
    return s_locals.Count;
  }


  public static void InvalidateStacksAfterLevel(int level)
  {
    while (s_locals.Count > level) {
      s_locals.Pop();
    }
  }


  public static void PopLocalVariable(string name)
  {
    if (s_locals.Count == 0) {
      return;
    }
    Dictionary<stringParserFunction> locals = s_locals.Peek().Variables;
    locals.Remove(name);
  }

}

Implementing custom methods and functions

To implement custom methods and functions in CSCS, we need two classes, FunctionCreator and CustomFunction, both deriving from the ParserFunction class; see Figure 4. The FunctionCreator class is shown in Code Listing 50. We register it with the parser as follows:

public const string FUNCTION = "function";

ParserFunction.RegisterFunction(Constants.FUNCTION, new FunctionCreator());

Note: No worries, we’ll see very soon how to redefine a function name in the configuration file.

So a typical function in our language looks like:

function functionName(param1, param2, ..., paramN) {

    // Function Body;

}

Code Listing 50: The implementation of the FunctionCreator class

class FunctionCreator : ParserFunction
{
  protected override Variable Evaluate(ParsingScript script)
  {
    string funcName = Utils.GetToken(script, Constants.TOKEN_SEPARATION);

    string[] args = Utils.GetFunctionSignature(script);
    if (args.Length == 1 && string.IsNullOrWhiteSpace(args[0])) {
      args = new string[0];
    }

    script.MoveForwardIf(Constants.START_GROUP, Constants.SPACE);
    int parentOffset = script.Pointer;

    string body = Utils.GetBodyBetween(script, Constants.START_GROUP,

                                               Constants.END_GROUP);

    CustomFunction customFunc = new CustomFunction(funcName, body, args);
    customFunc.ParentScript = script;
    customFunc.ParentOffset = parentOffset;

    ParserFunction.RegisterFunction(funcName, customFunc,

                                    false /* not native */);

    return new Variable(funcName);
  }
}

First, the FunctionCreator.Evaluate method calls an auxiliary Utils.GetToken method, which extracts the functionName in the function definition. Then, the Utils.GetFunctionSignature auxiliary function gets all the function arguments, see Code Listing 51.

Note that we do not have explicit types in our language: the types are deduced on the fly from the context. Therefore, the result of the Utils.GetFunctionSignature function is an array of strings, like arg1, arg2, …, argN. An example of a function signature is: function power(a, n).

Code Listing 51: The implementation of the GetFunctionSignature method

public static string[] GetFunctionSignature(ParsingScript script)
{
  script.MoveForwardIf(Constants.START_ARG, Constants.SPACE);

  int endArgs = script.FindFirstOf(Constants.END_ARG.ToString());
  if (endArgs < 0) {
    throw new ArgumentException("Couldn't extract function signature");
  }

  string argStr = script.Substr(script.Pointer, endArgs - script.Pointer);
  string[] args = argStr.Split(Constants.NEXT_ARG_ARRAY);

  script.Pointer = endArgs + 1;
  return args;
}

The auxiliary Utils.GetBodyBetween method extracts the actual body of the function in Code Listing 52. As method arguments, we pass the open character as Constants.START_GROUP (which I defined as { ), and the close character as Constants.END_GROUP (which I defined as }).

Code Listing 52: The implementation of the GetBodyBetween method

public static string GetBodyBetween(ParsingScript script,

                                    char open, char close) 

{
  // We are supposed to be one char after the beginning of the string,
  // so we must not have the opening char as the first character.
  StringBuilder sb = new StringBuilder(script.Size());
  int braces = 0;

  for (; script.StillValid(); script.Forward())  {
    char ch = script.Current;

    if (string.IsNullOrWhiteSpace(ch.ToString()) && sb.Length == 0) {
      continue;
    } else if (ch == open) {
      braces++;
    } else if (ch == close) {
      braces--;
    }

    sb.Append(ch);
    if (braces == -1)  {
      if (ch == close) {
        sb.Remove(sb.Length - 11);
      }
      break;
    }
  }
  return sb.ToString();
}

Basically, the FunctionCreator class creates a new instance of the CustomFunction class (see its implementation in Code Listing 53) and registers it with the parser.

Code Listing 53: The implementation of the CustomFunction class

class CustomFunction : ParserFunction
{
  internal CustomFunction(string funcName,
                          string body, string[] args) {
    m_name = funcName;
    m_body = body;
    m_args = args;
  }

  protected override Variable Evaluate(ParsingScript script)
  {
    bool isList;
    string parsing = script.Rest;
    List<Variable> functionArgs = Utils.GetArgs(script,
        Constants.START_ARG, Constants.END_ARG, out isList);

    script.MoveBackIf(Constants.START_GROUP);
    if (functionArgs.Count != m_args.Length) {   
      throw new ArgumentException("Function [" + m_name +

         "] arguments mismatch: " + m_args.Length + " declared, " +

         functionArgs.Count + " supplied");
    }

    // 1. Add passed arguments as local variables to the Parser.
    StackLevel stackLevel = new StackLevel(m_name);
    for (int i = 0; i < m_args.Length; i++) {
      stackLevel.Variables[m_args[i]] =

          new GetVarFunction(functionArgs[i]);
    }

    ParserFunction.AddLocalVariables(stackLevel);

    // 2. Execute the body of the function.
    Variable result = null;
    ParsingScript tempScript = new ParsingScript(m_body);
    tempScript.ScriptOffset = m_parentOffset;
    if (m_parentScript != null) {
      tempScript.Char2Line      = m_parentScript.Char2Line;
      tempScript.Filename       = m_parentScript.Filename;
      tempScript.OriginalScript = m_parentScript.OriginalScript;
    }

    while (tempScript.Pointer < m_body.Length - 1 && 
          (result == null || !result.IsReturn)) {
      string rest = tempScript.Rest;
      result = tempScript.ExecuteTo();
      tempScript.GoToNextStatement();
    }

    ParserFunction.PopLocalVariables();
    result.IsReturn = false;
    return result;
  }

  public ParsingScript ParentScript { set { m_parentScript = value} }
  public int           ParentOffset { set { m_parentOffset = value} }
  public string        Body         { get { return m_body; } }
  public string        Header       { get {  

    return Constants.FUNCTION + " " + Name + " " +
           Constants.START_ARG + string.Join (", ", m_args) +
           Constants.END_ARG + " " + Constants.START_GROUP;
    }

  }

  private string        m_body;
  private string[]      m_args;
  private ParsingScript m_parentScript = null;
  private int           m_parentOffset = 0;
}

We call the Utils.GetArgs auxiliary function to extract the arguments (defined in Code Listing 25).

We’ll see the usage of the m_parentOffset, m_parentScript (and its properties: Char2Line, Filename, and OriginalScript) in the "Getting line numbers where errors ” section in Chapter 7  Localization.

An example of a CSCS custom function: factorial

Let’s now see custom functions and exceptions in action. As an example, we’ll create a factorial. The factorial function, denoted as n!, is defined as follows:

n! = 1 * 2 * 3 * … * (n - 1) * n.

There is a special definition when n = 0: 0! = 1. The factorial is not defined for negative numbers. Note that we can also define the factorial recursively: n! = (n - 1)! * n.

Code Listing 54: The CSCS code for the factorial recursive implementation

function factorial(n)
{
  if (!isInteger(n) || n < 0) {
    exc = "Factorial is for nonnegative integers only (n=" + n + ")";
    throw (exc);
  }
  if (<1) {
    return 1;
  }

  return n * factorial(n - 1);
}

function isInteger(candidate) {

  return candidate == round(candidate);

}

function factorialHelper(n)
{
  try {
    f = factorial(n);
    print("factorial(", n, ")=", f);
  } catch (exc) {
    print("Caught exception: ", exc);
  }
}

factorialHelper(0);
factorialHelper(10);
factorialHelper("blah");

The implementation of the recursive version of the factorial in CSCS is shown in Code Listing 54. The factorial function uses the isInteger CSCS function to check if the passed parameter is an integer. This function is implemented in Code Listing 54 as well. It calls the round function, which was already implemented in C# (see Code Listing 34).

These are the results of running the CSCS script of Code Listing 54:

factorial(0)=1

factorial(10)=3628800

Caught exception: Factorial is for nonnegative integers only (n=blah) --> exc

  factorial()

  factorialHelper()

In the Caught exception clause you can see the exception stack, which was produced by the CreateExceptionStack method (see Code Listing 47). One can also add the line numbers where the exception occurred. We’ll see more about that in Chapter 7  Localization.

Conclusion

In this chapter we continued adding functionality to the CSCS language: we saw how to include files, to throw and to catch an exception, how to add local and global variables, and how to implement custom functions in CSCS. We also saw some examples of writing custom functions.

In the next chapter we are going to see how to develop some data structures to be used in CSCS.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.