DEV Community

Makoto Tajitsu
Makoto Tajitsu

Posted on

Execute Other Language on Salesforce Apex

I tried to execute serverside language, that is not Apex, on Salesforce.

Language specification

specification is following.

// dynamic declaration
a = 123 

// function call
puts "hello" 

// for loop
for i = 0; i < 10; i = i + 1 {
  puts i
}

// while loop
i = 0
while i < 3 {
  puts i
  i = i + 1
}

// if statement
if a == 123 {
  puts "a"
} else {
  puts "b"
}

How to implment

Tokenize(Lexer)

I implemented like the following.

public class Lexer {
  public String str;
  public Integer index;

  public List<String> reserved = new List<String>{
    'if',
    'else',
    'for',
    'while',
    'true',
    'false'
  };

  public Lexer(String str) {
    this.str = str;
    this.index = 0;
  }

  public List<Token> parse() {
    List<Token> tokens = new List<Token>();
    String current = this.current();
    while (true){
      Token token;
      switch on current {
        when '+', '-', '*', '/', '=', '\n', '(', ')', '{', '}', '!', ';', '>', '<' {
          if (current == '=' || current == '!' || current == '<' || current == '>') {
            if (this.peek() == '=') {
              String type = current + this.peek();
              token = new Token(type, type);
              this.next();
            }
          }
          if (token == null) {
            token = new Token(current, current);
          }
        }
        when else {
          if (Pattern.matches('[0-9]', current)) {
            token = this.parseInt();
          } else if (Pattern.matches('[a-zA-Z]', current)) {
            token = this.parseIdent();
          }
        }
      }
      if (token != null) {
        tokens.add(token);
      }
      if (this.index == this.str.length() - 1) {
        break;
      }
      current = this.next();
    }
    return tokens;
  }

  public String peek() {
    return this.str.substring(this.index+1, this.index+2);
  }

  public String current() {
    return this.str.substring(this.index, this.index+1);
  }

  public String next() {
    this.index++;
    return this.current();
  }
  // ...

It checks chars one by one and generate token array.

Parse and Generate AST

Parser generate AST from tokens created by Lexer.
It checks tokens one by one and generate statement node array.

public class Parser {
  public List<Token> tokens;
  public Integer index;

  public Parser(List<Token> tokens) {
    this.tokens = tokens;
    this.index = 0;
  }

  public List<Node> parse() {
    return this.statements();
  }

  public List<Node> statements() {
    List<Node> statements = new List<Node>();
    while (true){
      Node stmt = this.statement();
      if (stmt == null) {
        break;
      }
      statements.add(stmt);
      this.consume('\n');
    }
    return statements;
  }

  public Node statement() {
    if (this.current('if') != null) {
      return this.ifStatement();
    }
    if (this.current('while') != null) {
      return this.whileStatement();
    }
    if (this.current('for') != null) {
      return this.forStatement();
    }
    if (this.peek('=') != null) {
      return this.assign();
    }
    return this.call();
  }

  public Node assign() {
    Token ident = this.consume('Ident');
    if (ident == null) {
      return null;
    }
    Token op = this.consume('=');
    if (op == null) {
      return null;
    }
    Node exp = this.add();
    return new AssignNode(ident.value, exp);
  }

  public Node add() {
    Node exp = this.mul();
    Token op = this.consume('+');
    if (op != null) {
      return new BinaryOperatorNode('+', exp, this.add());
    }
    op = consume('-');
    if (op != null) {
      return new BinaryOperatorNode('-', exp, this.add());
    }
    return exp;
  }

  public Node mul() {
    Node exp = this.term();
    Token op = this.consume('*');
    if (op != null) {
      return new BinaryOperatorNode('*', exp, this.mul());
    }
    op = this.consume('/');
    if (op != null) {
      return new BinaryOperatorNode('/', exp, this.mul());
    }
    return exp;
  }

  public Node term() {
    Token exp = this.consume('Integer');
    if (exp != null) {
      return new IntegerNode(exp.value);
    }
    exp = this.consume('Ident');
    if (exp != null) {
      return new IdentifierNode(exp.value);
    }
    return null;
  }

  public Token consume(String type) {
    Token currentToken = this.current();
    if (currentToken == null) {
      return null;
    }
    if (currentToken.type == type) {
      this.index++;
      return currentToken;
    }
    return null;
  }

  public Token peek() {
    if (this.tokens.size() <= this.index+1) {
      return null;
    }
    return this.tokens[this.index+1];
  }

  public Token current() {
    if (this.tokens.size() == this.index) {
      return null;
    }
    return this.tokens[this.index];
  }
// ...

Generate Code by Visitor pattern

Each node implement accept method.

public class BinaryOperatorNode implements Node {
  public String type;
  public Node left;
  public Node right;

  public BinaryOperatorNode(String type, Node left, Node right) {
    this.type = type;
    this.left = left;
    this.right = right;
  }

  public Value accept(Visitor v) {
    return v.visitBinaryOperator(this);
  }
}

Visitor implement visit{NodeName} method.

public class Visitor {
  public Map<String, Value> variables;
  private String buffer;

  public Visitor() {
    this.variables = new Map<String, Value>();
  }

  public void run(String str) {
    this.buffer = '';
    List<Token> tokens = new Lexer(str).parse();
    List<Node> nodes = new Parser(tokens).parse();
    for (Node node : nodes) {
      this.visit(node);
    }
  }

  public String getBuffer() {
    return this.buffer;
  }

  public void visit(Node node) {
    Value result = node.accept(this);
  }

  public Value visitInteger(IntegerNode node) {
    return new Value(node.value, node);
  }

  public Value visitString(StringNode node) {
    return new Value(node.value, node);
  }
// ...

Finally, it can execute mini language by Apex.

String code = '';
code += 'a = "hello"\n';
code += 'puts a + " apex!"';
Visitor v = new Visitor();
v.run(code);
System.debug(v.getBuffer());

With #getBuffer method, Apex can receive data that puts outputs.

Consideration

If you want to write Salesforce Application with dynamic language, the above approach may be useful.
You can store program code to SObject record and execute it.
So you don't need to deploy any change by mini-language.

And you can mini-language as setting DSL.
For example, custom field or other formatted data(JSON, YAML, XML...) have no logic but only value.
If you write logic to Apex, you need to deploy. Formula DataType is less expressive than Apex.
In that point, mini-language as DSL have more expressive than formula data type, no need to deploy and easy to write.

Top comments (0)