Skip to content

Instantly share code, notes, and snippets.

@tluyben
Last active April 7, 2024 22:00
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save tluyben/16ee2645c4c8aed813005d51488d5c6a to your computer and use it in GitHub Desktop.
Save tluyben/16ee2645c4c8aed813005d51488d5c6a to your computer and use it in GitHub Desktop.
A minimal Forth implementation in C#
/*
* Minimal .NET Forth implementation. Just an experiment. Do not use for anything serious.
* by Tycho Luyben (https://github.com/tluyben)
*
* The only 'primitive' (built-in) is an foreign function interface word which allows you to define
* whatever is needed, for example:
*
* hello System.String System.Console.WriteLine ffi
*
* will print hello.
*
* ffi references must include the complete package name and function, aka
*
* hello string Console.WriteLine ffi
*
* will not work.
*
* Words can be excaped to prevent execution:
*
* 5 4 `+
*
* Stack: 5 4 +
*
* Note the + on the stack is not executed.
*
* To execute it ; run the exec word.
*
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
namespace TinyForths
{
// This is the runtime, this is all that's needed for the language
public class Forth1
{
// base ENVironment
public static Stack<string> St = new Stack<string>();
public static Dictionary<string, string> Ws = new Dictionary<string, string>();
// foreign function interface, only supports static methods for ease
public static void FFI()
{
var tf = St.Pop(); // total package.class.function name
var i = tf.LastIndexOf('.');
if (i < 0) throw new EntryPointNotFoundException($"FFI with {tf} has no package");
// find the Type
var cls = tf.Substring(0, i);
Type t = AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetType(cls) != null).GetType(cls);
if (t == null) throw new EntryPointNotFoundException($"Cannot resolve type {cls}");
// find the method
var ts = St.Pop().Split(new char[] { ',' }).Where(tt=>tt!="void").Select(tt => Type.GetType(tt)).ToArray(); // parameter types
var f = t.GetMethod(tf.Substring(i + 1), ts); // get the static method
var o = ts.Select(p => (object)(p == typeof(string) ? St.Pop() : Convert.ChangeType(St.Pop(), p))).ToArray(); // set the input params
// run the (static) method
f.Invoke(null, o);
}
// base execution
public static void EC(string s)
{
//Console.WriteLine($"executing {s} with stack {string.Join(" ",St.ToArray())} ");
s.Replace("\n", " ")
.Split(null).Select(s1 => s1.Trim()).Where(s1 => s1 != "") // tokenizer
.ToList().ForEach(s1 =>
{
// the only built-in word
if (s1[0] != '`' && s1 == "ffi") FFI();
else // this is a defined word
if (s1[0] != '`' && Ws.ContainsKey(s1)) EC(Ws[s1]);
else // the rest we add on the stack
{
if (s1[0] == '`') s1 = s1.Substring(1);
St.Push(s1);
}
}); // execution
}
}
// Create some internals to make the language usable
public static class ForthBasicInternals
{
static string PopOff(int arity)
{
var ar = new string[arity];
while (arity-- > 0) ar[arity] = Forth1.St.Pop();
return String.Join(" ", ar);
}
// define words
public static void Define(string name, int arity)
{
Forth1.Ws[name] = PopOff(arity);
}
public static void Add(int a, int b)
{
Forth1.St.Push((a + b).ToString());
}
public static void Mult(int a, int b)
{
Forth1.St.Push((a * b).ToString());
}
public static void Dup(string w)
{
Forth1.St.Push(w);
Forth1.St.Push(w);
}
public static void Drop(string w)
{
}
public static void Swap(string w1, string w2)
{
Forth1.St.Push(w1);
Forth1.St.Push(w2);
}
public static void Print(string w)
{
if (Forth1.Ws.ContainsKey(w))
{
Console.WriteLine(Forth1.Ws[w]);
}
else
{
Console.WriteLine(w);
}
}
public static void PrintStack()
{
Console.WriteLine(string.Join(" ", Forth1.St.Reverse().ToArray()));
}
public static void Exec(string w)
{
Forth1.EC(w);
}
public static void If(int condArity, int thenArity, int elseArity)
{
var _cond = PopOff(condArity);
var _then = PopOff(thenArity);
var _else = PopOff(elseArity);
Forth1.EC(_cond);
if (Forth1.St.Pop() == "true")
Forth1.EC(_then);
else
Forth1.EC(_else);
}
public static void Eq(int a1, int a2)
{
Forth1.St.Push(a1 == a2 ? "true" : "false");
}
}
public class Forth1Console
{
public static void Main(string[] args)
{
// define some library calls
Forth1.EC("System.String,System.Int32 TinyForths.ForthBasicInternals.Define `ffi 3 def System.String,System.Int32 TinyForths.ForthBasicInternals.Define ffi");
Forth1.EC("System.String System.Console.WriteLine `ffi 3 . def");
Forth1.EC("void TinyForths.ForthBasicInternals.PrintStack `ffi 3 .s def");
Forth1.EC("System.String TinyForths.ForthBasicInternals.Exec `ffi 3 exec def");
Forth1.EC("System.String TinyForths.ForthBasicInternals.Dup `ffi 3 dup def");
Forth1.EC("System.String TinyForths.ForthBasicInternals.Drop `ffi 3 drop def");
Forth1.EC("System.Int32,System.Int32 TinyForths.ForthBasicInternals.Eq `ffi 3 = def");
Forth1.EC("System.String,System.String TinyForths.ForthBasicInternals.Swap `ffi 3 swap def");
Forth1.EC("System.Int32,System.Int32 TinyForths.ForthBasicInternals.Add `ffi 3 + def");
Forth1.EC("System.Int32,System.Int32 TinyForths.ForthBasicInternals.Mult `ffi 3 * def");
Forth1.EC("System.Int32,System.Int32,System.Int32 TinyForths.ForthBasicInternals.If `ffi 3 if def");
// redefine print to allow to print words
Forth1.EC("System.String TinyForths.ForthBasicInternals.Print `ffi 3 `. def");
// have some fun
Forth1.EC("hello . world .");
Forth1.EC("2 3 + . 9 11 + . 6 66 * . x y z .s 7 3 * .s dup .s drop swap .s");
Forth1.EC("swap 3 `* .s exec .s");
Forth1.EC("hello world 2 helloworld def `helloworld .");
Forth1.EC("5 5 = 5 6 = .s");
Forth1.EC("5 cheese meat 5 `= 1 1 2 if .s");
Forth1.EC("5 cheese `. meat `. 6 `= 2 2 2 if");
// repl
while (true)
{
var s = Console.ReadLine().Trim();
if (s == "") continue;
if (s == "\\e") break;
Forth1.EC(s);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment