jjrscott

Flores

Just over a year ago I wrote the post entitled Yet Another Programming Language. That particular project hasn’t quite been finished yet. However, I have written a very small execution engine in Java named Flores.

Flores has the following features:

  1. One data type - String. Well, two if you count the singleton null.
  2. The while operator, complete with continue and break.
  3. The if operator, complete with else.
  4. The = operator.
  5. All functions are defined externally in Java as static methods. These have to be added to a Flores engine object at Java runtime by overriding the abstract static method callFunction.
  6. No functions by default. The developer needs to add them.

This may look like an odd features list, but there is method in the madness. I have often needed a small scripting lanaguage with one or more of the following features:

  • Security. I want total control over a script’s access to the outside world. With Flores, if I don’t explicitly add a function, it’s not there.
  • Maintainability. At 639 lines, including comments, there can only be so many bugs.
  • Simplicity. I don’t want tail recursion or co-routines. When I say simple, I really mean it.
  • Licensing. I need a license that’s business friendly, and for that, I’ve always found the zlib/libpng License reliable.

Flores is available below. If you have some of the same criteria that I do, hopefully you’ll 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;

public abstract class Flores {
	private static class Cookie {
		public String program_;
		public int index_;

		public Cookie(String program) {
			program_ = program;
			index_ = 0;
		}
	}

	public static class Argument {
		private String key_;
		private String value_;

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

		public String getKey() {
			return key_;
		}

		public String getValue() {
			return value_;
		}
	}

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

		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, String value) {
			variables_.put(variable, value);
		}

		public String getVariable(String variable) {
			return (String) 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 {
		public RuntimeException(Flores engine, String message) {
			super(message);
		}

		public RuntimeException(Flores engine, String message, Throwable cause) {
			super(message, cause);
		}
	}

	private static class ReturnJump extends RuntimeException {
		public ReturnJump(Flores engine, String result) {
			super(engine, result);
		}
	}

	private static class ContinueJump extends RuntimeException {
		public ContinueJump(Flores engine) {
			super(engine, null);
		}
	}

	private static class BreakJump extends RuntimeException {
		BreakJump(Flores engine) {
			super(engine, null);
		}
	}

	private static interface Unit {
		public String execute(Flores engine, Variables variables) throws Throwable;
	}

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

		public String execute(Flores engine, Variables variables) throws Throwable {
			if (Flores.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 implements Unit {
		public Unit expression_;

		public String execute(Flores engine, Variables variables) throws Throwable {
			throw new ReturnJump(engine, expression_.execute(engine, variables));
		}
	}

	private static class Break implements Unit {
		public String execute(Flores engine, Variables variables) throws Throwable {
			throw new BreakJump(engine);
		}
	}

	private static class Continue implements Unit {
		public String execute(Flores engine, Variables variables) throws Throwable {
			throw new ContinueJump(engine);
		}
	}

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

		public String execute(Flores engine, Variables variables) throws Throwable {
			while (Flores.isTrue(expression_.execute(engine, variables)))
				try {
					yes_.execute(engine, variables);
				}
			catch (BreakJump e) {
				break;
			} catch (ContinueJump e) {
				continue;
			}
			return null;
		}
	}

	private static class Block implements Unit {
		ArrayList block_;

		public Block() {
			block_ = new ArrayList();
		}

		public String execute(Flores engine, Variables variables) throws Throwable {
			for (int index = 0; index < block_.size(); index++)
				((Unit) block_.get(index)).execute(engine, variables);
			return null;
		}
	}

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

		public Call() {
			expressions_ = new ArrayList();
		}

		public String execute(Flores engine, Variables variables) throws Throwable {
			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));
			}

			return engine.callFunction(function_, results);
		}
	}

	private static class Quote implements Unit {
		private String string_;

		public Quote(String string) {
			string_ = string;
		}

		public String execute(Flores engine, Variables variables) throws Throwable {
			return string_;
		}
	}

	private static class Null implements Unit {
		public Null() {}

		public String execute(Flores engine, Variables variables) throws Throwable {
			return null;
		}
	}

	private static class Variable implements Unit {
		private String variable_;

		public Variable(String variable) {
			variable_ = variable;
		}

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

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

		public String execute(Flores engine, Variables variables) throws Throwable {
			variables.setVariable(variable_, expression_.execute(engine, variables));
			return null;
		}
	}

	public Flores() {}

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

	public String execute(String program) throws ParseException, RuntimeException, Throwable {
		Cookie cookie = new Cookie(program);
		Unit unit = parse(cookie);
		if (unit == null)
			throw new RuntimeException(this, "No program to run");

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

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

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

		return unit;
	}

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

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

	private static 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 static Unit parse_if(Cookie cookie) throws ParseException {
		If unit = new If();

		if (!bnf_alpha_sw(cookie, "if"))
			return null;

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

		unit.expression_ = parse_expression(cookie);

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

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

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

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

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

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

		unit.no_ = parse_if(cookie);

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

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

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

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

		return unit;
	}

	private static Unit parse_while(Cookie cookie) throws ParseException {
		While unit = new While();

		if (!bnf_alpha_sw(cookie, "while"))
			return null;

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

		unit.expression_ = parse_expression(cookie);

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

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

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

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

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

		return unit;
	}

	private static Unit parse_return(Cookie cookie) throws ParseException {
		Return unit = new Return();

		if (!bnf_alpha_sw(cookie, "return"))
			return null;

		unit.expression_ = parse_expression(cookie);

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

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

		return unit;
	}

	private static Unit parse_break(Cookie cookie) throws ParseException {
		Break unit = new Break();

		if (!bnf_alpha_sw(cookie, "break"))
			return null;

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

		return unit;
	}

	private static Unit parse_continue(Cookie cookie) throws ParseException {
		Continue unit = new Continue();

		if (!bnf_alpha_sw(cookie, "continue"))
			return null;

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

		return unit;
	}

	private static Unit parse_quote(Cookie cookie) throws ParseException {
		if (!bnf_alpha_sw(cookie, "\""))
			return null;

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

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

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

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

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

		String name = bnf_name_sw(cookie);

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

		Unit call = parse_call(cookie, name);

		if (call != null)
			return call;

		return new Variable(name);
	}

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

		Call call = new Call();

		call.function_ = name;

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

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

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

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

				if (expression == null)
					expression = new Variable(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 (bnf_alpha_sw(cookie, ","));

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

		return call;
	}

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

		String name = bnf_name_sw(cookie);

		if (name == null)
			return null;

		Unit call = parse_call(cookie, name);

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

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

			assign.expression_ = parse_expression(cookie);

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

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

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

	private static String bnf_name_sw(Cookie cookie) {
		skipWhitespace(cookie);

		int start = cookie.index_;

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

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

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

	private static boolean skipWhitespace(Cookie cookie) {
		while (cookie.index_ < cookie.program_.length() && Character.isWhitespace(cookie.program_.charAt(cookie.index_)))
			cookie.index_++;

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

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

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

	private static boolean bnf_alpha_sw(Cookie cookie, String constant) {
		skipWhitespace(cookie);

		return bnf_alpha(cookie, constant);
	}

	private static boolean bnf_alpha(Cookie cookie, String constant) {
		if (cookie.index_ + constant.length() > cookie.program_.length())
			return false;

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

	private static boolean isTrue(String string) {
		return string != null;
	}
}