jjrscott

Georgia

Last year I published a small programming language named Flores. At the time it seamed that no-one could be productive with a language that limited. Well, one can indeed get a long way; Flores existed a good 4 months before the post and makes it 19 months old.

Inevitably one does need new features, a programmer cannot live by Strings alone. So lets create a version 2 of Flores, one with more features. The problem with that is that there will be some inevitable questions:

  • Will version 1 code work with a version 2 scripting engine?
  • Do I have to upgrade to get keep getting bug fixes?

These are valid questions, but they shouldn’t even need to be asked. There’s nothing wrong with Flores, it’s complete in its own right. That’s what fulfilling the requirements means. Really what we mean here is: build a new (and different) language on top of the Flores source code and add fundamentally new features.

So, without further ado, meet Georgia. It has the following features:

  1. Everything Flores had at release 6 (unless otherwise stated)
  2. Natively works with Objects instead of Strings
  3. Allowes integers to be parsed without quotes. Eg 1 instead of “1”
  4. Digits are not allowed as part of function names.

Georgia is available below and as with Flores I hope you find it useful.

/*
 * Copyright (c) 2007 John Scott
 * 
 * This software is provided 'as-is', without any express or implied warranty.
 * In no event will the authors be held liable for any damages arising from
 * the use of this software.
 * 
 * Permission is granted to anyone to use this software for any purpose,
 * including commercial applications, and to alter it and redistribute it
 * freely, subject to the following restrictions:
 * 
 * 1. The origin of this software must not be misrepresented; you must not
 *    claim that you wrote the original software. If you use this software in
 *    a product, an acknowledgment in the product documentation would be
 *    appreciated but is not required.
 * 
 * 2. Altered source versions must be plainly marked as such, and must not be
 *    misrepresented as being the original software.
 * 
 * 3. This notice may not be removed or altered from any source distribution.
 */

package com.jjrscott.lang;

import java.util.ArrayList;
import java.util.HashMap;
import java.lang.Exception;
import java.lang.StackTraceElement;
import java.util.Arrays;

import java.io.StringReader;
import java.io.LineNumberReader;
import java.io.IOException;

public abstract class Georgia {
	public static class Argument {
		private String key_;
		private Object value_;

		public Argument(String key, Object value) {
			key_ = key;
			value_ = value;
		}

		public String getKey() {
			return key_;
		}

		public Object getValue() {
			return value_;
		}
	}

	public static class Parameter {
		public String key_;
		public Unit unit_;

		public Parameter(String key, Unit unit) {
			key_ = key;
			unit_ = unit;
		}

		public String getKey() {
			return key_;
		}

		public Unit getUnit() {
			return unit_;
		}
	}

	private static class Variables {
		private HashMap variables_;

		public Variables() {
			variables_ = new HashMap();
		}

		public void setVariable(String variable, Object value) {
			variables_.put(variable, value);
		}

		public Object getVariable(String variable) {
			return variables_.get(variable);
		}

		private boolean hasVariable(String variable) {
			return variables_.containsKey(variable);
		}
	}

	public static class ParseException extends Exception {
		Cookie cookie_;

		public ParseException(Cookie cookie, String message) {
			super(message);
			cookie_ = cookie;
		}

		public String getMessage() {
			return super.getMessage() + ": " + cookie_.program_.substring(cookie_.index_) + " (" + cookie_.index_ + ")";
		}
	}

	public static class RuntimeException extends Exception {
		Unit unit_;

		public RuntimeException(Georgia engine, String message) {
			super(message);
		}

		public RuntimeException(Georgia engine, Unit unit, String message) {
			super(message);
			unit_ = unit;
		}

		public RuntimeException(Georgia engine, Unit unit, String message, Throwable cause) {
			super(message, cause);
			unit_ = unit;
		}

		public RuntimeException(Georgia engine, Unit unit, Throwable cause) {
			super(cause);
			unit_ = unit;
		}

		public String getMessage() {
			if (unit_ != null)
				return super.getMessage() + " at line " + unit_.line_number_;
			else
				return super.getMessage();
		}
	}

	private static class Jump extends Exception {
		private Object result_ = null;

		public Jump(Georgia engine, Object result) {
			super("foo");
			result_ = result;
		}

		public Jump(Georgia engine) {
			super("foo");
			result_ = null;
		}

		public Object getResult() {
			return result_;
		}
	}

	private static class ReturnJump extends Jump {
		public ReturnJump(Georgia engine, Object result) {
			super(engine, result);
		}
	}

	private static class ContinueJump extends Jump {
		public ContinueJump(Georgia engine) {
			super(engine, null);
		}
	}

	private static class BreakJump extends Jump {
		public BreakJump(Georgia engine) {
			super(engine, null);
		}
	}

	private static abstract class Unit {
		public int index_;
		public int line_number_;

		public Unit(Cookie cookie) {
			index_ = cookie.index_;
			line_number_ = cookie.line_numbers_[cookie.index_];
		}

		public abstract Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump;
	}

	private static class If extends Unit {
		public Unit expression_;
		public Unit yes_;
		public Unit no_;

		public If(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			if (Georgia.isTrue(expression_.execute(engine, variables)))
				return yes_.execute(engine, variables);
			else if (no_ != null)
				return no_.execute(engine, variables);
			else
				return null;
		}
	}

	private static class Return extends Unit {
		public Unit expression_;

		public Return(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			throw new ReturnJump(engine, expression_.execute(engine, variables));
		}
	}

	private static class Break extends Unit {
		public Break(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			throw new BreakJump(engine);
		}
	}

	private static class Continue extends Unit {
		public Continue(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			throw new ContinueJump(engine);
		}
	}

	private static class While extends Unit {
		public Unit expression_;
		public Unit yes_;

		public While(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			while (Georgia.isTrue(expression_.execute(engine, variables)))
				try {
					yes_.execute(engine, variables);
				}
			catch (BreakJump e) {
				break;
			} catch (ContinueJump e) {
				continue;
			}
			return null;
		}
	}

	private static class Block extends Unit {
		ArrayList block_;

		public Block(Cookie cookie) {
			super(cookie);
			block_ = new ArrayList();
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			for (int index = 0; index < block_.size(); index++)
				((Unit) block_.get(index)).execute(engine, variables);
			return null;
		}
	}

	private static class Call extends Unit {
		public ArrayList expressions_;
		public String function_;

		public Call(Cookie cookie) {
			super(cookie);
			expressions_ = new ArrayList();
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			Argument[] results = new Argument[expressions_.size()];
			for (int index = 0; index < expressions_.size(); index++) {
				Parameter parameter = (Parameter) expressions_.get(index);
				results[index] = new Argument(parameter.getKey(), parameter.getUnit().execute(engine, variables));
			}

			try {
				return engine.callFunction(function_, results);
			} catch (Throwable exception) {
				throw new RuntimeException(engine, this, exception);
			}
		}
	}

	private static class Quote extends Unit {
		private String string_;

		public Quote(Cookie cookie, String string) {
			super(cookie);
			string_ = string;
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			return string_;
		}
	}

	private static class Null extends Unit {
		public Null(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			return null;
		}
	}

	private static class Variable extends Unit {
		private String variable_;

		public Variable(Cookie cookie, String variable) {
			super(cookie);
			variable_ = variable;
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			if (variables.hasVariable(variable_))
				return variables.getVariable(variable_);
			else
				throw new RuntimeException(engine, this, "Variable " + variable_ + " does not exist");
		}
	}

	private static class Assign extends Unit {
		public String variable_;
		public Unit expression_;

		public Assign(Cookie cookie) {
			super(cookie);
		}

		public Object execute(Georgia engine, Variables variables) throws RuntimeException, Jump {
			variables.setVariable(variable_, expression_.execute(engine, variables));
			return null;
		}
	}

	public abstract Object callFunction(String name, Argument[] arguments) throws Throwable;

	public Object execute(String program) throws ParseException, RuntimeException {

		Cookie cookie = new Cookie(program);
		Unit unit = parse(cookie);
		if (unit == null)
			throw new RuntimeException(this, "No valid program");

		Variables variables = new Variables();
		try {
			return unit.execute(this, variables);
		} catch (ReturnJump e) {
			return e.getResult();
		} catch (Jump e) {
			return null;
		}
	}

	private Unit parse(Cookie cookie) throws ParseException {
		Unit unit = parse_block(cookie);

		if (cookie.skipWhitespace())
			throw new ParseException(cookie, "Program was not fully parsed");

		return unit;
	}

	private Unit parse_block(Cookie cookie) throws ParseException {
		Block block = new Block(cookie);
		Unit found = null;
		while ((found = parse_statement(cookie)) != null)
			block.block_.add(found);

		if (block.block_.size() > 0)
			return block;
		else
			return null;
	}

	private Unit parse_statement(Cookie cookie) throws ParseException {
		Unit found;

		found = parse_if(cookie);
		if (found != null)
			return found;

		found = parse_while(cookie);
		if (found != null)
			return found;

		found = parse_return(cookie);
		if (found != null)
			return found;

		found = parse_break(cookie);
		if (found != null)
			return found;

		found = parse_continue(cookie);
		if (found != null)
			return found;

		found = parse_assign(cookie);

		return found;
	}

	private Unit parse_if(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("if"))
			return null;

		If unit = new If(cookie);

		if (!cookie.bnf_alpha_sw("("))
			throw new ParseException(cookie, "Expected (");

		unit.expression_ = parse_expression(cookie);

		if (unit.expression_ == null)
			throw new ParseException(cookie, "We wanted a expression here");

		if (!cookie.bnf_alpha_sw(")"))
			throw new ParseException(cookie, "Expected )");

		if (!cookie.bnf_alpha_sw("{"))
			throw new ParseException(cookie, "Expected {");

		unit.yes_ = parse_block(cookie);
		if (unit.yes_ == null)
			throw new ParseException(cookie, "We wanted a block here");

		if (!cookie.bnf_alpha_sw("}"))
			throw new ParseException(cookie, "Expected }");

		if (!cookie.bnf_alpha_sw("else"))
			return unit;

		unit.no_ = parse_if(cookie);

		if (unit.no_ != null)
			return unit;

		if (!cookie.bnf_alpha_sw("{"))
			throw new ParseException(cookie, "Expected {");

		unit.no_ = parse_block(cookie);
		if (unit.no_ == null)
			throw new ParseException(cookie, "We wanted a block here");

		if (!cookie.bnf_alpha_sw("}"))
			throw new ParseException(cookie, "Expected }");

		return unit;
	}

	private Unit parse_while(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("while"))
			return null;

		While unit = new While(cookie);

		if (!cookie.bnf_alpha_sw("("))
			throw new ParseException(cookie, "Expected (");

		unit.expression_ = parse_expression(cookie);

		if (unit.expression_ == null)
			throw new ParseException(cookie, "We wanted a expression here");

		if (!cookie.bnf_alpha_sw(")"))
			throw new ParseException(cookie, "Expected )");

		if (!cookie.bnf_alpha_sw("{"))
			throw new ParseException(cookie, "Expected {");

		unit.yes_ = parse_block(cookie);
		if (unit.yes_ == null)
			throw new ParseException(cookie, "We wanted a block here");

		if (!cookie.bnf_alpha_sw("}"))
			throw new ParseException(cookie, "Expected }");

		return unit;
	}

	private Unit parse_return(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("return"))
			return null;

		Return unit = new Return(cookie);

		unit.expression_ = parse_expression(cookie);

		if (unit.expression_ == null)
			throw new ParseException(cookie, "We wanted a expression here");

		if (!cookie.bnf_alpha_sw(";"))
			throw new ParseException(cookie, "Expected ;");

		return unit;
	}

	private Unit parse_break(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("break"))
			return null;

		Break unit = new Break(cookie);

		if (!cookie.bnf_alpha_sw(";"))
			throw new ParseException(cookie, "Expected ;");

		return unit;
	}

	private Unit parse_continue(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("continue"))
			return null;

		Continue unit = new Continue(cookie);

		if (!cookie.bnf_alpha_sw(";"))
			throw new ParseException(cookie, "Expected ;");

		return unit;
	}

	private Unit parse_quote(Cookie cookie) throws ParseException {
		if (!cookie.bnf_alpha_sw("\"")) {

			int start = cookie.index_;

			while (cookie.index_ < cookie.program_.length() && Character.isDigit(cookie.program_.charAt(cookie.index_)))
				cookie.index_++;

			if (start == cookie.index_)
				return null;

			return new Quote(cookie, cookie.program_.substring(start, cookie.index_));
		}

		StringBuffer string = new StringBuffer();
		int start = cookie.index_;

		while (cookie.index_ < cookie.program_.length() && cookie.program_.charAt(cookie.index_) != '"') {
			if (cookie.bnf_alpha("\\n")) {
				string.append("\n");
			} else if (cookie.bnf_alpha("\\\\")) {
				string.append("\\");
			} else if (cookie.bnf_alpha("\\\"")) {
				string.append("\"");
			} else {
				string.append(cookie.program_.substring(cookie.index_, cookie.index_ + 1));
				cookie.index_ += 1;
			}
		}

		if (!cookie.bnf_alpha_sw("\""))
			throw new ParseException(cookie, "Expected \"");
		else
			return new Quote(cookie, string.toString());
	}

	private Unit parse_expression(Cookie cookie) throws ParseException {
		if (!cookie.skipWhitespace())
			return null;

		String name = cookie.bnf_name_sw();

		if ("null".equals(name))
			return new Null(cookie);

		Unit call = parse_call(cookie, name);

		if (call != null)
			return call;

		Unit quote = parse_quote(cookie);
		if (quote != null)
			return quote;

		if (name == null)
			return null;

		return new Variable(cookie, name);
	}

	private Unit parse_call(Cookie cookie, String name) throws ParseException {
		if (!cookie.bnf_alpha_sw("("))
			return null;

		Call call = new Call(cookie);

		call.function_ = name;

		if (cookie.bnf_alpha_sw(")"))
			return call;

		do {
			String key = cookie.bnf_name_sw();
			Unit expression = null;

			if (key != null && !cookie.bnf_alpha_sw(":")) {
				if (key.equals("null"))
					expression = new Null(cookie);

				if (expression == null)
					expression = parse_call(cookie, key);

				if (expression == null)
					expression = new Variable(cookie, key);

				key = null;
			}

			if (expression == null)
				expression = parse_expression(cookie);

			if (expression != null)
				call.expressions_.add(new Parameter(key, expression));
			else
				throw new ParseException(cookie, "Expected an expression");
		} while (cookie.bnf_alpha_sw(","));

		if (!cookie.bnf_alpha_sw(")"))
			throw new ParseException(cookie, "Expected )");

		return call;
	}

	private Unit parse_assign(Cookie cookie) throws ParseException {
		if (!cookie.skipWhitespace())
			return null;

		String name = cookie.bnf_name_sw();

		if (name == null)
			return null;

		Unit call = parse_call(cookie, name);

		if (call != null) {
			if (!cookie.bnf_alpha_sw(";"))
				throw new ParseException(cookie, "Expected ;");

			return call;
		} else if (cookie.bnf_alpha_sw("=")) {
			Assign assign = new Assign(cookie);
			assign.variable_ = name;

			assign.expression_ = parse_expression(cookie);

			if (assign.expression_ == null)
				throw new ParseException(cookie, "Expexted an expression");

			if (!cookie.bnf_alpha_sw(";"))
				throw new ParseException(cookie, "Expected ;");

			return assign;
		} else {
			throw new ParseException(cookie, "Expected = or a (");
		}
	}

	private static class Cookie {
		public String program_;
		public int index_;
		public LineNumberReader lnr_;
		public int[] line_numbers_;

		public Cookie(String program) {
			line_numbers_ = new int[program.length()];
			lnr_ = new LineNumberReader(new StringReader(program));
			System.out.println("bits : " + line_numbers_.length);
			try {
				for (int i = 0; i < program.length(); i++) {
					line_numbers_[i] = lnr_.getLineNumber() + 1;
					lnr_.read();
				}
			} catch (Throwable e) {}
			program_ = program;
			index_ = 0;
		}

		public String bnf_name_sw() {
			skipWhitespace();

			int start = index_;

			while (index_ < program_.length() && Character.isLetter(program_.charAt(index_)))
				index_++;

			if (start == index_)
				return null;

			return program_.substring(start, index_);
		}

		public boolean skipWhitespace() {
			while (index_ < program_.length() && Character.isWhitespace(program_.charAt(index_)))
				index_++;

			if (bnf_alpha("/*")) {
				while (!bnf_alpha("*/") && index_ < program_.length())
					index_++;

				while (index_ < program_.length() && Character.isWhitespace(program_.charAt(index_)))
					index_++;
			}

			if (index_ < program_.length())
				return true;
			else
				return false;
		}

		public boolean bnf_alpha_sw(String constant) {
			skipWhitespace();

			return bnf_alpha(constant);
		}

		public boolean bnf_alpha(String constant) {
			if (index_ + constant.length() > program_.length())
				return false;

			if (program_.startsWith(constant, index_)) {
				index_ += constant.length();
				return true;
			} else {
				return false;
			}
		}
	}

	private static boolean isTrue(Object value) {
		return value != null;
	}
}