添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

Introduction

It is very useful to have a way of quickly testing small pieces of code. This is particularly useful if you are exploring a new system or library for the first time. Interactive interpreters allow for conversational programming and it's a popular feature of languages such as Python. Visual Basic will allow you to execute code in the Immediate Window, but there are limits to what can be done in Visual Studio. For instance, you cannot declare and create new objects.

Traditionally, compiled languages such as C# are not usually used in this interactive way, but the .NET framework makes it straightforward to compile code a line at a time, using the System.CodeDom.Compiler and Microsoft.CSharp namespaces. There have been several programs for using C# as a scripting language that avoid a separate compile step. The argument there is that since C# compilation is so fast for small programs, one doesn't need the full machinery of Visual Studio to build and manage them. CSI (Simple C# Interpreter) is a reimplementation of C#Shell which was designed for the Mono framework. CSI is a lot faster, because it does not actually have to spawn the full compiler, and uses a technique for creating statically typed session variables . An interactive example will make this clearer:

CSI Simple C# Interpreter
# $s = "Hello, World!"
# Print($s.Substring(0,5),$s.GetType())
Hello  System.String
# $l = $s.Split(null)
# foreach(string ss in $l) Print(ss)
Hello,
World!

In the first line, we assign a string to a session variable $s ; note that a semicolon is automatically appended to each line. The function Print is available, which takes a variable number of arguments. It's easier to type than Console.WriteLine and will also work in a GUI console session. Statements such as foreach and any other legal C# code can be evaluated.

Implementation

CSI relies on .NET's own code compilation libraries, so it doesn't actually need to parse and interpret C# code (there is at least one true C# interpreter .) So the technical problem is how to keep a common environment active between each separately compiled line. For each line typed, CSI generates and compiles a new assembly that looks like this:

C#
(using namespaces)
class CsiChunk : CodeChunk {
    public override void Go (Hashtable V) {
        (code goes here)

There are some commands to control the compilation context. For instance, /n System.IO will insert a using System.IO; in the assembly code; /r System.Drawing.dll will add a reference to that assembly. Any CSI commands can be put in a session include file (csigui.csi, csi.csi for the GUI and console versions respectively; I've included some examples of these with the source and binaries).

Session variables are replaced with global lookup table references. The line $s = "Hello, World! becomes V["s"] = "Hello, World!";. In the same way (but with a key difference) $s.Substring(0,5) becomes ((System.String)V["s"]).Substring(0,5). Any references other than assignments are cast to the actual type of the variable. I'm relying on a very cool C# feature called autoboxing where any value type is automatically converted into an object on heap. This allows any value (such as numbers) to be boxed as an object and put into an object container such as a HashTable, and later casting to the correct type will unbox the value.

The actual compilation is the most straightforward part of CSI, and is quite standard. The compiled assembly is loaded by dynamically instantiating the CsiChunk class. CSI doesn't have such a class, but both CSI and the assembly know about CodeChunk. So I can cast the object to CodeChunk and call the overridden Go method, passing it the global lookup table.

C#
public static void Instantiate(Assembly a, Hashtable table) {
     try {
         CodeChunk chunk = (CodeChunk)a.CreateInstance("CsiChunk");
         chunk.Go(table);
     }  catch(Exception ex) {
         Print(ex.GetType() + " was thrown: " + ex.Message);

Session variables would be fairly useless unless they cast to their correct type. CSI has to massage code so that $var is replaced by V["var"]. If it is an assignment then the value is not cast. Otherwise, find the type of the object V["var"] and use that. It's important to find a publicly accessible type, because the actual runtime type may be an implementation class which isn't available to us. For instance, Type.GetMethods returns an array of RuntimeMethodInfo, which is derived from MethodInfo. So if the type is a class, we look for a public base class.

C#
Type GetPublicRuntimeType(object symVal) {
    Type symType = null;
    if (symVal != null) {
        symType = symVal.GetType();
        while (! symType.IsPublic)
            symType = symType.BaseType;
    return symType;

It's important to understand this scheme, because then certain basic limitations of CSI become clear. First, there must be actual assignment, so the object can't be created by a reference parameter. Multiple assignments on a line is allowable, but session variables don't have a definite type until the next line:

# $x = 1.0; $y = 2.0; $z = 3.0
# Print($x+$y*$z)
# $a = 10; $b = 20; Print($a + $b);
Compiling string: 'V["a"] = 10; V["b"] = 20; Print(V["a"] + V["b"]);'
Operator '+' cannot be applied to operands of type 'object' and 'object'

You can extend a 'line' over several lines of input, if it uses braces. CSI isn't psychic, so it's necessary to put the brace on the first line so braces can be counted properly. Here semicolons are essential and $sum must have been previously declared. Any declarations inside such a block must be explicit.

# foreach(Item item in $items) {
      $sum += item.Size;
      Print($sum,item.Size);

In short, session variables can only be used in subsequent lines, and multiline blocks really only count as single lines of compilation.

Macros and Functions

One thing I learnt from the UnderC Project is that a C-style macro preprocessor is very useful in interactive work. The one I use here was described previously on the Code Project. Its obvious use is to make long identifiers and tricky constructs easier to type:

# #def FOR(i,n) for(int i =0; i < (n); i++)
# FOR(k,5) Print(k,k*k)
# #def wl Console.WriteLine
# wl("{0} == {1}",10,10)
10 == 10

I'm not suggesting that this is a good style for normal C#! Experience has shown that macro processors lead to trouble in production code, since people tend to construct a private language and it makes debugging much harder. But in interactive work, you can write informal code, just as with English. I would hesitate to use "ain't" in written articles, but it's fine in conversation.

Macros may be used to define new commands (which begin with '/').

# #def P(x) Print
#/P 2*6 + 1

The main reason that CSI has a preprocessor is that it makes defining and using functions more convenient. You can define functions and use them thereafter as if they were part of CSI.

# double sqr(double x) { return x*x; }
# Print(sqr(10))
# Print(Math.Sqrt(sqr(10)))
# void dump(int[] arr) {
#  FOR(i,arr.Length)
#   Print(i,arr[i]);
# dump(new int[] { 1, 7, 3, 4 })

CSI knows that a function is being defined by merely looking for a pattern where two identifiers start the line followed by an argument list and an open brace. The definition for sqr above results in this assembly being compiled as Csi1.dll.

C#
public class Csi1 : CsiFunctionContext {
  public double _sqr(double x) { return x*x; }

A macro 'sqr' is then defined to be 'Csi1._sqr', which makes it possible to use the function without knowing which assembly it lives in. Subsequent functions will be in Csi2.dll, and so on.

Graphical Console

CSI can be built as a console program, or as a Windows Forms application. Not only does this provide a nicer environment, but it allows GUI code to be tested. For example, although you can create and show a window from the console version, it cannot do anything interesting because there is no event loop. There are some interesting issues about building graphical consoles in .NET which were not clearly documented, so I'll describe how to do it here.

A RichTextBox is the obvious control, but there is some necessary work to intercept the ENTER key. The solution is to derive a custom text box and override IsInputKey:

C#
 protected override bool IsInputKey(Keys keyData) {
     if (keyData == Keys.Enter) {
         int lineNo = GetLineFromCharIndex(SelectionStart);
         if (lineNo < Lines.Length) {
             string line = Lines[lineNo];
             parent.DelayedExecute(line);
    return base.IsInputKey(keyData);

Text boxes have a useful property called Lines which acts like an indexable collection of all the lines in the control. It is straightforward to get the line which the user has just entered. I have found, however, that WordWrap must be switched off for this scheme to work properly, and it is important not to try modifying the control from within this function. So the current line is passed to a function which starts a timer, and a short while later, the prompt can be written and the line evaluated by the interpreter:

C#
public void DelayedExecute(string line) {
    currentLine = line.Substring(prompt.Length);
    timer.Start();
void Execute(object sender,EventArgs e) {
    timer.Stop();
    stringHandler(currentLine);
    Write(prompt);

Some ideas for using CSI

It's possible to create useful GUI extensions to CSI just using its facilities. For example, the default session file csigui.csi contains the following code which creates a form and fills it with a PropertyGrid control. A macro I is defined which sets the SelectedObject property of the grid and makes the form accessible. For efficiency reasons, I've packed everything into two lines.

$pf=new Form();$pg=new PropertyGrid()
$pg.SelectedObject = 
  $pf;$pg.Dock=DockStyle.Fill;$pf.Controls.Add($pg);$pf.Text="Properties";$pf.Show();
#def I(x) $pg.SelectedObject=x; $pf.BringToFront()

The session variables $form and $text are always available in the GUI build. To inspect the properties of the text box, simply say /I $text.

CSI exports a function called MInfo. This uses introspection to either list the methods of a class or detailed information about a particular method. I've defined two macros which make this easier to use, which are defined in the sample session include files. This is useful when exploring an assembly for the first time.

# #def M(klass) MInfo(typeof(klass),null)
# #def MI(method) MInfo(null, #method)
# /M string
ToString GetTypeCode Clone CompareTo GetHashCode
Equals ToString Join Equals CopyTo
ToCharArray Split Substring Trim TrimStart
TrimEnd Compare CompareTo CompareOrdinal EndsWith
IndexOf IndexOfAny IndexOf LastIndexOf LastIndexOfAny
LastIndexOf PadLeft PadRight StartsWith ToLower
ToUpper Trim Insert Replace Remove
Format Copy Concat Intern IsInterned
GetEnumerator
# /MI Split
String[] Split(Char[])
String[] Split(Char[], Int32)
# /MI Remove
String Remove(Int32, Int32)
# /P "hello dolly".Remove(2,3)
he dolly

You can of course load your own assemblies with /r. For instance, say I had a robot.dll which controlled a robot (what else?). By clever use of macros, it becomes possible to test your robot interactively. I can create a RobotController object, and mosey the device around using simple commands.

/r robot.dll
# $robot = new RobotController()
# /P $robot
robbie
# $robot.TurnLeft()
# $robot.Move(10)
# #def TL $robot.TurnLeft()
# #def MV(x) $robot.Move(x)
# MV(10)
# /MV 10

Unit testing is very much part of the software buzz these days, and interactive programming can help in initial exploration. This style has proved very productive working with hardware (consider the history of the FORTH programming language.)

Another interesting application of CSI is as an embedded console that exposes the innards of your application to interactive testing. An embedded CSI prompt allows you to 'crawl inside' your program and test components in their working environment. This applies to CSI itself - the main Interpreter class is available from within an interactive session, and the session variable $interpreter has already been set.

> /P Assembly.GetAssembly(typeof(Interpreter))
csigui, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null 
> /M Interpreter
ReadIncludeFile SetValue ProcessLine AddNamespace AddReference
> /P $interpreter
Interpreter 
> $interpreter.SetValue("alice","here we go")
> Print($alice,'*',$alice.Remove(0,2))
here we go * re we go

CSI can be linked in as a small 15 K DLL referenced by your program, which can access all your public classes, provided the assemblies are referenced. If nothing else, it can be used as an intelligent trace monitor window, if you output your traces through Debug.Trace, which is defined below. In the console window, you can now type Debug.Tracing = true and so switch on tracing selectively.

C#
public delegate void ObjectTracer(object o);
public class Debug {
    static public bool Tracing = false;
    static public ObjectTracer TraceHook = null;
    public static void Trace(params object[] objs) {
        if (Tracing)
            Utils.Printl(objs);
    public static void TraceObject(object o) {
        if (TraceHook != null)
            TraceHook(o);

Calls to Debug.TraceObject can be customized (by default, they do nothing). Assuming I've liberally put such calls throughout my code, it's now possible to execute your own arbitrary code dynamically. Here I'm only interested in looking at trace calls for any LineAdaptor object. This technique would be useful in displaying objects that only satisfy some arbitrary criteria, rather than having to wade through thousands of trace output lines.

> void dump(object o) { if (o.GetType() == typeof(LineAdaptor)) Print(o); }
> Debug.TraceHook = new ObjectTracer(dump)
.... exercise your program, looking at all LineAdaptor objects...
> Debug.TraceHook = null

Criticisms and Future Possibilities

The approach used in CSI is potentially wasteful of system resources, because every little assembly created remains loaded. In a long session, this might eventually be a problem, but the leak would be quite slow due to the small size of typical compiled lines.

A good question (which I'm expecting from the .NET wizards) is 'why not use Application Domains?'. Surely, running code in a separate AppDomain provides advantages? This is true, but not so much in this case. Since we keep references to all objects generated, it isn't possible to close a separate AppDomain without resetting the whole session and losing all created variables. So it seems easier to use the default include file (csi.csi or csigui.csi) sensibly and just restart CSI. Of course, I may have missed something subtle here. The main reason is that it would make CSI a harder program to understand. (The actual core is currently less than three hundred lines.)

There are several minor issues which occurred to me. It would be cool if the graphical console used different colours for input and output, as I did for the UnderC project. People may like an inspectable list of currently created session variables, although it would be more interesting to supply the necessary general hooks in CSI so that a variable list could be generated using CSI scripts (like the Property Inspector window). Currently there can be only one instance of Interpreter in an application, and it may be useful to relax that restriction.

I've found CSI to be an exciting program to play with, and it's become an invaluable part of my programming tool chest. Embedded .NET-aware interpreters make new debugging tactics possible, and allow you to test your code interactively.

It looks like a free custom license: The source code of CSI version 0.8 at Steve's website says "Use freely, but please acknowledge!" -- rather start with that code.
Sign In·View Thread  Hi to everyone.
I've found some problems with the part of the code that recognize variables inside string (MassageInput).
I think it will be better to use this line of code :
// exclude matches found inside strings
if (s[m.Index - 1] == '"' && s[m.Index+m.Value.Length] == '"')
continue;
What do you think?
I have solved my problems, I'll test it better soon.
Best regards.
It doesn't work, but it's cool!

Sign In·View Thread 
Oops, maybe it's more correct to check array bounds!
// exclude matches found inside strings
if ( m.Index > 0 && m.Index+m.Value.Length <= s.Length )
if (s[m.Index - 1] == '"' && s[m.Index + m.Value.Length] == '"')
continue;
Pardon...
It doesn't work, but it's cool!

Sign In·View Thread  Compiling string: 'if ( ((System.Int32)V["a"])==1 ){((System.Int32)V["a"])=2;}'
The left-hand side of an assignment must be a variable, property or indexer
If I create a new variable inside brackets, it works.
It doesn't work, but it's cool!

Sign In·View Thread  In case someone is interested in getting this C# Interpreter to work with the .NET Framework 3.5, it can easily be done with a few simple changes detailed below. That way you can also interactively work with the nice C# 3.0 language features such as LINQ, extension methods and Lambda expressions.
For the update, I started with the source code of CSI version 0.8 from Steve's site at http://home.mweb.co.za/sd/sdonovan/csi-src.zip and then applied the changes suggested by CreProDes on 14 Dec '05 for the .NET Framework 2.0 (i.e., update the three lines of code involving the field named "compiler" to get rid of the warning about the obsolete CreateCompiler method). Then, also in the file named "interpreter.cs", replace "new CSharpCodeProvider()" with
var x = new string[] {"a","bb","ccc"}; Print(x.Skip(2).First()) $3 = new string[] {"A1","B2","C3"}; Print((from s in $3 where s.StartsWith("B") select s).Single()) Print($3.Where(s => s.EndsWith("3")).First() == $3.Last())
that would display "ccc", "B2" and True, as expected.
Sign In·View Thread  Wow, I was looking exatcly for this, a nice tool indeed. Good Work!
However I'm wondering, can you declare classes, I would have thought
yes, but if I type:
Compiling string '{public class Test{public int i;}}'
} expected
Type or namespace definition, or end-of-file expected
help will be appreciated
Martin
Sign In·View Thread  I was trying to get this thing running under mono. Unfortunately it complains it cannot find System.dll . I have tried either with compiling the source or direct executable
Sign In·View Thread  Overall, this is a pretty slick app. Thanks for putting it out there.
I do have one bug to report, however (hopefully, this hasn't already been raised):
When defining a function and making an assignment to a session variable (to mimic a class-level variable, for example, or to define shared state across threads, etc.), the CSI interpreter will cast the left side of the assignment.
For example, assume $myVariable is a string:
$myVariable = "test";
...will be interpretted as...
(System.String)V["myVariable"] = "test";
Sign In·View Thread 
Hello. I am not familiar with this interpreter, but maybe there is a reasoned explanation behind the above code. Unless you have explicit problems with running code caused by what you noticed, you may find the cause of the casting in my humble guessing below:
I could assume that v[...] is a dictionary of objects, keyed by a string. The code
C#
will have the value "test" (a string) but it will be boxed as object. That would explain the need for a type cast.
Sign In·View Thread  How do I declare the type of a session variable?
Given a Regex $rx I would like to declare something like
MatchCollection $matches = $rx.Matches("someinputbeeingsearched")
I also have trouble with
/n System.Text.RegularExpressions
As it thinks that System.Text does not have RegularExpressions yet I am able to do this:
$rx=new System.Text.RegularExpressions.Regex(@"\{\d\}",System.Text.RegularExpressions.RegexOptions.Compiled | System.Text.RegularExpressions.RegexOptions.IgnoreCase)
without having to declare the namespace.
Thannks
Fritz Schenk alias
intrader
Sign In·View Thread  GeneralSuppressing KeyPress to avoid eating command prompt and for Framework v2 Pin
CreProDes14-Dec-05 0:06
CreProDes14-Dec-05 0:06  Hi Steve, Thanks for the tool, it sure helps me a lot in my approach design for the problem. I have made the following changes in order to make it work with framework v2 and avoid user from deleting the command prompt.

// File: console.cs
// Adding a property command prompt here, as this is our
// customised display screen it will be more opt to place
// it in this class
string _commandPrompt = ">";
public
string CommandPrompt {
      get { return _commandPrompt; }
      set { _commandPrompt = value; }
}

then overriding the KeyDown event handler as

protected
override void OnKeyDown(KeyEventArgs e) {
    int
lineNo = GetLineFromCharIndex(SelectionStart);
    if
(e.KeyData == Keys.Back) {

        if
(Lines[lineNo] == CommandPrompt || lineNo < (Lines.Length-1)) {
            e.SuppressKeyPress =
true;
       
    base
.OnKeyDown(e);

}

and thus changing the line

public class GuiConsoleForm : Form...
.
.
.

public
GuiConsoleForm(string caption, string cmdPrompt, StringHandler h) {
        Text = caption;
        prompt = cmdPrompt;
        stringHandler = h;
        textBox = new ConsoleTextBox(this, cmdPrompt);
.
.
.

for making the app to work with framework 2, all we need to do is comment "ICodeCompiler compiler; " and "compiler = prov.CreateCompiler();" from the file interpreter.cs and replace "compiler.CompileAssemblyFromSource(cp, finalSource);" with "prov.CompileAssemblyFromSource(cp, finalSource);". Have a great day. CodeCanvas
Sign In·View Thread  GeneralRe: Suppressing KeyPress to avoid eating command prompt and for Framework v2 Pin
Steve Donovan15-Dec-05 2:22
Steve Donovan15-Dec-05 2:22 
Thanks, man. Glad to hear that net 2 was a two-line change.
I'm going to do a revised version incorporating all the excellent suggestions and fixes. Given the level of interest, perhaps we should open up a SourceForge page.
steve d.

Sign In·View Thread  GeneralRe: Suppressing KeyPress to avoid eating command prompt and for Framework v2 Pin
CreProDes15-Dec-05 3:28
CreProDes15-Dec-05 3:28  public static void PrintVTable()
IDictionaryEnumerator en = interp.VarTable.GetEnumerator();
while (en.MoveNext())
console.Write(en.Key + " : " + en.Value + "\n");
public static void Remove(String s)
interp.VarTable.Remove(s);
// Macros
#def ls RunCsi.PrintVTable
#def rm(x) RunCsi.Remove(x)
//Examples
> $a = 1
a : 1
> rm("a")
Sign In·View Thread  Hi Steve,
Would you please explain why you make semicolon autocomplete?
Would it be neat if you keep strict typing of semicolon, such that
multi-line compiling will be more smooth?
For example, if I want to print an array.
The following won't work as you mentioned:
for (int i=0; i<10; i++)
Print($a[i]);
That is because semicolon is automatically added after for (int ... )
So, what if a semicolon should be strictlly typed?
Sign In·View Thread 
That's a fair point, and I think it would be a useful option to allow explicit semicolon. I always say:
for(int i = 0; i < 10; i++) {
Print($a[i]);
so CSI can count braces. But this is not everyone's style.
I would still need to count braces....
steve d.
Sign In·View Thread  Now look at the following line, notice that it has the same pattern as the 'else if'
clause above:
void doStuff(object o ){
CSI thinks that 'else if(1 >0){' is a function definition, and gets screwed up.
So in order to fix it, do the following in 'ExecuteLine':
bool hasName = funMatch != Match.Empty; if(hasName && funMatch.ToString().Trim().StartsWith("else")) hasName = false; Sign In·View Thread  The 'ExecuteLine' method of the interpreter class will try to substitute inside this string.
So to fix it, we add the following logic to 'ExecuteLine' (new stuff is in bold).
string className ...; Match funMatch = funDef.Match(codeStr); string test = funMatch.ToString(); bool hasName = funMatch != Match.Empty; if(MacroSubsitiuter.insideQuote(codeStr,test,funMatch.Index)) hasName = false; This assumes that you have added the 'insideQuote' method to MacroSubsitiuter (I showed this in a previous thread).
Sign In·View Thread  Web02 2.8:2023-05-13:1