PureDartLua

REPL in action

This is a custom a custom Lua 5.5 interpreter and utilities written from scratch in pure Dart. I wrote this as a part of a series of learning exercises on how to write my own compilers and programming languages.

See this section for remaining issues.

Tutorial

This package exports a full fledged library and a simple executable for running lua scripts. Runnable examples below can be found under ./bin/examples/.

Run The Driver

To get started, run bin/input.lua. Everything after the file path is passed into the run() starter as input arguments table arg.

dart bin/main.dart -e bin/input.lua hello world!

Use In Your Own Code

It's easy. Just include the utilities file that includes default runtime behaviors and the Std library.

import 'package:puredartlua/runner.dart';

void main() {
  run(parse("print('hello, world!')")!);
}

This will execute and print hello, world! to the console.

Custom Data

But you probably want to define your own tables and functions in lua. And probably read those values back in dart too.

For this, you need a custom runtime via the Evaluator class and use the specific (bool, LuaObject) runner(AST, constructor, onErrors?) utility function.

import 'package:puredartlua/runner.dart';

/// This example driver shows the user how to define custom methods in dart,
/// run lua programs calling custom methods,
/// and find the result from lua back to dart.
void main() {
  final evaluator = Evaluator();

  final addOne = LuaFuncBuilder.create('add_one')
      .arg('n')
      .exec(
        call: () {
          final n = evaluator.findVar('n')?.valueAsInt() ?? 0;
          return n + 1;
        },
      );

  evaluator.defGlobal(addOne);

  runner(parse("x = add_one(6)")!, constructor: () => evaluator);

  int? result = evaluator.findVar('x')?.valueAsInt();

  // Prints: 7
  print(result);
}

See the implementation for the ready-to-use Evaluator class in lib/lua/lua.dart.

As An Interpreter

To make a LUA interpreter, simply feed runner(...) the result of the user's parsed input. The runner returns a tuple of type (bool ok, LuaObject out). You can print the result to the user. Quit the loop when the user types a signal. For example exit.

import 'dart:convert';
import 'package:puredartlua/runner.dart';

void handleErrors(List<String> errs) => errs.forEach(print);

void main() {
  final evaluator = Evaluator();
  print('Press enter to continue on next line.');
  print('Blank lines terminates input.');
  print('Type "exit" to quit.');
  print('Type "dump_scope" to write the global scope to console.');
  print('-------------------');
  String content = '';
  bool loop = true;
  while (loop) {
    stdout.add('\$ '.codeUnits);
    final String input = stdin.readLineSync(encoding: utf8)?.trim() ?? '';

    if (input.isNotEmpty) {
      if(content.isEmpty) {
        switch (input.toLowerCase()) {
          case 'exit':
            loop = false;
            continue;
          case 'dump_scope':
            evaluator.impl.scope.dump();
            content = '';
            continue;
        }
      }
      content += '$input\n';
      continue;
    } else {
      content += input;
    }

    final ast = parse(content, onErrors: handleErrors);
    content = '';

    if (ast == null) continue;

    final (_, out) = runner(
      ast,
      constructor: () => evaluator..clearResults(),
      onErrors: handleErrors,
    );

    print(out);
  }
}

DOT File Visualizer

Run

dart bin/main.dart -v my_script.lua

The DOT file will be embedded in an HTML page my_script.lua.html.

All Features

  • MIT Licensed.
  • No FFI or extra dependencies.
  • Out-Of-The-Box AST Transformers:
    1. DOT file visualizer for input scripts.
    2. A simple obfuscator for scrambling lua scripts that still run with same behavior.
  • Includes powerful examples:
    1. Command Line Interface
    2. Interpreter
    3. Autodoc generation and programmable API so your own libs can generate docs to share with your consumers.
    4. Custom runtime integration.
  • Create your own custom runtime and define LuaObjects in dart.
    • Truthy and Native2Lua Dart class extensions for convenient bridge between userdata and lua types.
    • Parser, Evaluator, and StdRuntime classes extensible and modifiable.
  • Ready-To-Use run() and runner() utility functions for immediate execution.
  • LuaFunctionBuilder class to conveniently build complex lua functions.
  • You can emit, collect, and act on your own custom warnings, diagnostic info, or errors.
  • Globals provided by _ENV and legacy _G upvalues.
  • Metatables supported via setmetatable(t, mt) and getmetatable(t) as you'd expect.
  • Metamethods: __call, __index, __newindex, __add, __sub, __mul, __div, __mod, __pow, __unm, __idiv, __band, __bor, __bxor, __bnot, __shl, __shr, __concat, __len, __eq, __lt, __le, __tostring.
  • Programmable variable attributes.
    • Standard <const> support built-in.
  • Standard lua runtime libs (partial implementation).
    • strings
    • include
    • ipairs
    • pairs
    • table
    • print
    • math
  • pcall() implementation.
    • xpcall() is just another call to pcall() in this runtime.
  • Functions taking one argument of either string literal or table literal can omit parenthesis.
  • Math operations on numeric-coercible strings returns numbers.
  • For-loop iterators and stateless iterators.

Because this is a pure dart lua interpreter, it is not expected to be as fast as the C ffi alternative libs for Dart. However, it is much more programmer friendly!

Tests

All tests are under the /test/ directory. The driver is test/lualib_test.dart. The test scripts are under /test/assets/.

The driver uses a custom preflight setup that extracts every comment of the form -- Expected: ... and compares the output in Evaluator.impl.stdOut. If the output does not match the expected string, then it is an error. This allows the scripts to contain the data needed for the checks without requiring additional setup.

For example:

-- This is why we're here.
local str = 'Hello, world'

-- Expect: Hello, world
print(str)

-- Testing variadic args in print.

-- Expect: 1 2 3 nil true
print(1, 2, 3, nil, true)

-- Expect: \n
print()

-- Expect: 1
print(1)

Running this test will result:

Hello, world
1 2 3 nil true
1
--- Test Results ---
OK

Note that your terminal (VSCode) may supress dart's print(...) newlines \n and empty string tokens ''. The standard lua print(...) method will capture these even if they do not show up in the terminal.

Work In Progress!

I am using this in my own projects and as such I have not created tutorials or get started guides. I will get around to that when I can!

Missing Lua Lang Support

Here's what's left to be compliant with most Lua 5.5 programs:

  • Missing modules:
    • The rest of the table utility methods. To be covered in time.
    • The string module seems reasonable to add.
    • The io module could be added, but not a priority.
    • The debug related modules are not included and are not a priority for me atm.
  • Bytecode generation. Which is necessary because...
    • We need to jump on goto and resume on ::label:: statements.
    • Coroutines library is added but the runtime needs bytecode to make use of it.
    • No tail call support. This optimization also requires bytecode to be effective.

Extra Goals

  • Semantics: code path type unification.
    • return statements could have the function's final type identified.
    • Nondeterministic functions should be identified as such.
      • This would allow invariant code paths to be protomoted to constant value generation.

Unplanned

This interpreter makes use of Dart's execution stack and memory model and it does not explicitly cleanup any memory. Therefore, anything related to Lua's garbage collector is not supported and not planned to be supported in the forseeable future.

This also means the metamethods related to GC such as __mode, __close, and __gc are not implemented either. If you use them your scripts will run fine, but any side effects these metamethods have will do nothing in this runtime.