automaton_generator 2.0.9 copy "automaton_generator: ^2.0.9" to clipboard
automaton_generator: ^2.0.9 copied to clipboard

An automaton generator is a code generator (codegen) for use in generators of converters, scanners, parsers, state machines, etc.

automaton_generator #

An automaton generator is a code generator (codegen) for use in generators of converters, scanners, parsers, state machines, etc.

Version: 2.0.9

Pub Package GitHub Issues GitHub Forks GitHub Stars GitHub License

What is an automaton generator? #

An automaton generator is a code generator (codegen) for use in generators of converters, scanners, parsers, state machines, etc.
It is a template-based branch code generator.
More precisely, a generator of an automaton consisting of states.
Each state is a computational unit and must necessarily define one of two (or usually both) template placeholders.
These are the acceptance and rejection placeholders.
They are specified in templates using special markers:

  • {{@accept}}
  • {{@reject}}

There are three kinds of states available for generating states:

  • Choice
  • Sequence
  • Operation

The Choice state generates selective branches, where each branch is an alternative.
These branches are ordered, meaning that the computations do not happen simultaneously, but they are performed in the order specified.
The Sequence state generates sequential branches. The sequential branch is an indivisible alternative. If any element causes a failure, the entire sequence of computation will be rejected.

The most important kind of state is Operation.
The Operation state acts as a transition and as an action.
That is, not a generator, but the source code in the template determines the transition conditions.
With this approach to implementation, the generator does not create any restrictions on the implementation of the automaton logic.

What is the difference between a state and an automaton? #

The State after code generation will contain placeholders ({{@accept}}, and/or {{@reject}}).
In fact, at this point in time, the State is in an intermediate condition.
It is ready for further use but it is not yet an automaton.
The State is not yet an automaton, since an automaton implies the presence of acceptors.
Acceptor is a State that transfers control to another computation process.
In this condition, two actions can be performed on the State.

  • Subsequent state code injection
  • Finalization code injection (transformation into an acceptor)

Subsequent state code injection is the process of placing the source code of the subsequent State into the appropriate placeholder.
That is, it is part of the process of building a code base based on templates with placeholders for nested code.
In fact, the templates may seem strange because it is not entirely clear how such code can work.

Example:

if (condition) {
  {{@accept}}
}
{{@reject}}

Another, more complex, example, with a failure registration code and a recovery code.

final {{pos}} = scanner.position;
if (condition) {
  {{@accept}}
} else {
  // failure registration
}
// Recovery
scanner.position = {{pos}};
{{@reject}}

Why will the rejection code never execute the code after the acceptance code has finished executing?
The answer is very simple, the acceptance code will never return control to the rejection code if acceptance branch of computation completes successfully.
Otherwise (if the acceptance branch rejects computation), code execution will continue down until it reaches the lowest rejection point.
During code generation, all intermediate rejection points will be removed, allowing alternative code to be executed.
Thus, either a successful exit (acceptance) with transfer of control will occur anywhere or at the very bottom point of the computation the control will be transferred forcibly (without any result) to external (or outer) computation.

Even a single State can be an automaton but to transform this State into an automaton it is necessary to close it. That is, transforming it into an acceptor.
It is possible to use return, continue or break statements as acceptors. Or shared variable assignment statement if it is necessary for the computation to descend to a lower point and make a branches based on the analysis of the variable value (an example can be found in the mux function in the extra library.).

It is not very convenient to close all ending states manually correctly, and therefore there is a special generator (AutomatonGenerator) and an auxiliary helper function automaton for this purpose. In fact, this is a wrapper for the generator.

How to use this software? #

For convenient code generation, it is not enough to use only states.
For this purpose (and as an example of usage) the extra library has helper functions to simplify code generation.
These are the most commonly used general-purpose computations.
Below is a list of these functions:

  • automaton
  • block
  • functionBody
  • many
  • many1
  • map
  • mux
  • optional
  • procedureBody
  • recognize
  • skipMany
  • skipMany1

All of them, except for the recognize function, are context-free generators.
The recognize function requires context parameters (position and substring), but can be used in most cases.

Example of a command machine #

In order to reduce duplicate code, a helper library will be used in the examples. It is used exclusively to simplify code generation.
Also, the need for such helper libraries arises because the automaton states are primitive and very limited objects. In fact, they should be considered as instructions (and data structures) of an intermediate language.

Below is the source code for the helper library.

import 'package:automaton_generator/state.dart';

class Action extends Operation {
  final String source;

  Action(this.source);

  @override
  State toState() {
    final template = '''
$source
{{@accept}}
''';
    final state = OperationState('void', template);
    return state;
  }
}

class Choice extends Operation {
  final List<Operation> operations;

  Choice(this.operations);

  @override
  State toState() {
    return ChoiceState('void', operations.map((e) => e.toState()).toList());
  }
}

class Fatal extends Operation {
  final String source;

  Fatal(this.source);

  @override
  State toState() {
    final template = '''
$source
{{@reject}}
''';
    final state = OperationState('void', template);
    return state;
  }
}

abstract class Operation {
  State toState();
}

class Sequence extends Operation {
  final List<Operation> operations;

  Sequence(this.operations);

  @override
  State toState() {
    return SequenceState(operations.map((e) => e.toState()).toList());
  }
}

class Test extends Operation {
  final String condition;

  Test(this.condition);

  @override
  State toState() {
    final template = '''
if ($condition) {
  {{@accept}}
}
{{@reject}}''';
    final state = OperationState('void', template);
    return state;
  }
}

extension OperationExt on Operation {
  Sequence operator +(Operation operation) {
    if (this case final Sequence sequence) {
      sequence.operations.add(operation);
      return sequence;
    } else {
      return Sequence([this, operation]);
    }
  }

  Choice operator |(Operation operation) {
    if (this case final Choice choice) {
      choice.operations.add(operation);
      return choice;
    } else {
      return Choice([this, operation]);
    }
  }
}

And the source code for a simple command machine generator.
This is a free-form generator in its implementation approach.

import 'dart:io';

import 'package:automaton_generator/allocator.dart';
import 'package:automaton_generator/extra.dart';
import 'package:automaton_generator/helper.dart';
import 'package:automaton_generator/state.dart';

import 'example_helper.dart';

void main(List<String> args) {
  final definitions = [
    ('turn_on', ['!power'], 'power = true; volume = 2;'),
    ('turn_off', ['power'], 'power = false; volume = 0 ;'),
    ('volume_up', ['volume < 5', 'power'], 'volume++;'),
    ('volume_down', ['volume > 0', 'power'], 'volume--;'),
  ];

  final states = <Operation>[];
  const printState = 'print(\'power: \$power, volume: \$volume\');';
  for (final definition in definitions) {
    final commandName = escapeString(definition.$1, '"');
    final commandCondition = definition.$2.join(' && ');
    final commandAction = definition.$3;
    final testCommand = Test('command == $commandName');
    final testCondition = Test(commandCondition);
    final work = Action(commandAction);
    final showState = Action(printState);
    final notifyRejected = Action('print(\'Command $commandName rejected\');');
    final onRejected = showState + notifyRejected;
    final state =
        testCommand + ((testCondition + work + showState) | onRejected);
    states.add(state);
  }

  const unknownCommand = 'print(\'Unknown command: \$command\');';

  bool getMode() => true;
  final ignoreFailures = getMode();
  String? reject;
  if (!ignoreFailures) {
    reject = 'break;';
    states.add(Fatal(unknownCommand));
  } else {
    states.add(Action(unknownCommand));
  }

  final start = Choice(states);
  final startState = start.toState();
  final s0 = automaton(
    'void',
    startState,
    '{{@state}}',
    accept: 'continue;',
    reject: reject,
  );
  s0.generate(Allocator().allocate);
  final source = s0.source;

  final library = '''
import 'dart:collection';

void main(List<String> args) {
  final audio = Audio();
  final commands = [
    'turn_off',
    'turn_on',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_down',
  ];
  audio.commands.addAll(commands);
  audio.execute();
  commands.clear();
  commands.addAll([
    'good_buy',
    'turn_off',
  ]);
  audio.commands.addAll(commands);
  audio.execute();
}

class Audio {
  final Queue<String> commands = Queue();

  var power = false;

  int volume = 2;

  void execute() {
    while (commands.isNotEmpty) {
      final command = commands.removeFirst();
      print('-' * 40);
      print(command);
      $source
    }
  }
}''';
  const outputFile = 'example/example.dart';
  File(outputFile).writeAsStringSync(library);
  Process.runSync(Platform.executable, ['format', outputFile]);
}

class Command extends Operation {
  final String name;

  Command(this.name);

  @override
  State toState() {
    final escapedName = escapeString(name);
    final template = '''
if (command == $escapedName) {
  {{@accept}}
}
{{@reject}}''';
    final state = OperationState('void', template);
    return state;
  }
}

This free-form generator generates the following source code:

import 'dart:collection';

void main(List<String> args) {
  final audio = Audio();
  final commands = [
    'turn_off',
    'turn_on',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_down',
  ];
  audio.commands.addAll(commands);
  audio.execute();
  commands.clear();
  commands.addAll([
    'good_buy',
    'turn_off',
  ]);
  audio.commands.addAll(commands);
  audio.execute();
}

class Audio {
  final Queue<String> commands = Queue();

  var power = false;

  int volume = 2;

  void execute() {
    while (commands.isNotEmpty) {
      final command = commands.removeFirst();
      print('-' * 40);
      print(command);
      if (command == "turn_on") {
        if (!power) {
          power = true;
          volume = 2;
          print('power: $power, volume: $volume');
          continue;
        }
        print('power: $power, volume: $volume');
        print('Command "turn_on" rejected');
        continue;
      }
      if (command == "turn_off") {
        if (power) {
          power = false;
          volume = 0;
          print('power: $power, volume: $volume');
          continue;
        }
        print('power: $power, volume: $volume');
        print('Command "turn_off" rejected');
        continue;
      }
      if (command == "volume_up") {
        if (volume < 5 && power) {
          volume++;
          print('power: $power, volume: $volume');
          continue;
        }
        print('power: $power, volume: $volume');
        print('Command "volume_up" rejected');
        continue;
      }
      if (command == "volume_down") {
        if (volume > 0 && power) {
          volume--;
          print('power: $power, volume: $volume');
          continue;
        }
        print('power: $power, volume: $volume');
        print('Command "volume_down" rejected');
        continue;
      }
      print('Unknown command: $command');
      continue;
    }
  }
}

That is, without much effort, a simple command machine code generator was created.

An example of a command machine in operation.

----------------------------------------
turn_off
power: false, volume: 2
Command "turn_off" rejected
----------------------------------------
turn_on
power: true, volume: 2
----------------------------------------
volume_up
power: true, volume: 3
----------------------------------------
volume_up
power: true, volume: 4
----------------------------------------
volume_up
power: true, volume: 5
----------------------------------------
volume_up
power: true, volume: 5
Command "volume_up" rejected
----------------------------------------
volume_down
power: true, volume: 4
----------------------------------------
good_buy
Unknown command: good_buy
----------------------------------------
turn_off
power: false, volume: 0

Another way to implement such a generator.

  • Using TOML
  • Using Stream
import 'dart:io';

import 'package:automaton_generator/allocator.dart';
import 'package:automaton_generator/extra.dart';
import 'package:automaton_generator/helper.dart';
import 'package:automaton_generator/state.dart';
import 'package:toml/toml.dart';

import 'example_helper.dart';

void main(List<String> args) {
  final document = TomlDocument.parse(_definition);
  final map = document.toMap();
  final states = <Operation>[];
  const printState = 'print(\'power: \$power, volume: \$volume\');';
  for (final entry in (map['state'] as Map).entries) {
    final command = entry.key as String;
    final value = entry.value as Map;
    final commandCondition = value['condition'] as String;
    final accept = value['accept'] as String;
    final commandName = escapeString(command, '"');
    final commandAction = accept;
    final testCommand = Test('command == $commandName');
    final testCondition = Test(commandCondition);
    final work = Action(commandAction);
    final showState = Action(printState);
    final notifyRejected = Action('print(\'Command $commandName rejected\');');
    final onRejected = notifyRejected + showState;
    final state =
        testCommand + ((testCondition + work + showState) | onRejected);
    states.add(state);
  }

  const unknownCommand = 'print(\'Unknown command: \$command\');';

  bool getMode() => true;
  final ignoreFailures = getMode();
  String? reject;
  if (!ignoreFailures) {
    reject = 'break;';
    states.add(Fatal(unknownCommand));
  } else {
    states.add(Action(unknownCommand));
  }

  final start = Choice(states);
  final startState = start.toState();
  final s0 = automaton(
    'void',
    startState,
    '{{@state}}',
    accept: 'return;',
    reject: reject,
  );
  s0.generate(Allocator().allocate);
  final source = s0.source;

  final library = '''
import 'dart:async';

void main(List<String> args) async {
  final commands = StreamController<String>();
  Audio(commands.stream);
  final commandList = [
    'turn_off',
    'turn_on',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_down',
    'good_buy',
    'turn_off',
  ];

  await for (final command in Stream.fromIterable(commandList)) {
    commands.add(command);
  }
}

class Audio {
  final Stream<String> commands;

  var power = false;

  int volume = 2;

  Audio(this.commands) {
    commands.listen(_onCommand);
  }

  void _onCommand(String command) {
    print('-' * 40);
    print(command);
    $source
  }
}
''';
  const outputFile = 'example/example_async_command_machine.dart';
  File(outputFile).writeAsStringSync(library);
  Process.runSync(Platform.executable, ['format', outputFile]);
}

const _definition = """
[state.turn_on]
condition = '!power'
accept = '''
power = true; volume = 2;
'''

[state.turn_off]
condition = 'power'
accept = '''
power = false; volume = 0;
'''

[state.volume_up]
condition = 'volume < 5 && power'
accept = '''
volume++;;
'''

[state.volume_down]
condition = 'volume > 0 && power'
accept = '''
volume--;
'''

""";

class Command extends Operation {
  final String name;

  Command(this.name);

  @override
  State toState() {
    final escapedName = escapeString(name);
    final template = '''
if (command == $escapedName) {
  {{@accept}}
}
{{@reject}}''';
    final state = OperationState('void', template);
    return state;
  }
}

This generator generates the following source code:

import 'dart:async';

void main(List<String> args) async {
  final commands = StreamController<String>();
  Audio(commands.stream);
  final commandList = [
    'turn_off',
    'turn_on',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_up',
    'volume_down',
    'good_buy',
    'turn_off',
  ];

  await for (final command in Stream.fromIterable(commandList)) {
    commands.add(command);
  }
}

class Audio {
  final Stream<String> commands;

  var power = false;

  int volume = 2;

  Audio(this.commands) {
    commands.listen(_onCommand);
  }

  void _onCommand(String command) {
    print('-' * 40);
    print(command);
    if (command == "turn_on") {
      if (!power) {
        power = true;
        volume = 2;
        print('power: $power, volume: $volume');
        return;
      }
      print('Command "turn_on" rejected');
      print('power: $power, volume: $volume');
      return;
    }
    if (command == "turn_off") {
      if (power) {
        power = false;
        volume = 0;
        print('power: $power, volume: $volume');
        return;
      }
      print('Command "turn_off" rejected');
      print('power: $power, volume: $volume');
      return;
    }
    if (command == "volume_up") {
      if (volume < 5 && power) {
        volume++;
        ;
        print('power: $power, volume: $volume');
        return;
      }
      print('Command "volume_up" rejected');
      print('power: $power, volume: $volume');
      return;
    }
    if (command == "volume_down") {
      if (volume > 0 && power) {
        volume--;
        print('power: $power, volume: $volume');
        return;
      }
      print('Command "volume_down" rejected');
      print('power: $power, volume: $volume');
      return;
    }
    print('Unknown command: $command');
    return;
  }
}

Example of a state machine #

import 'dart:io';

import 'package:automaton_generator/allocator.dart';
import 'package:automaton_generator/extra.dart';

import 'example_helper.dart';

void main(List<String> args) {
  final sm = StateMachine('Door');
  final closed = sm.add('closed');
  final open = sm.add('open');
  final locked = sm.add('locked');
  closed.add('open', open);
  open.add('close', closed);
  closed.add('lock', locked);
  locked.add('unlock', closed);
  sm.start = closed;
  final source = sm.generate();
  const outputFile = 'example/example_state_machine.dart';
  File(outputFile).writeAsStringSync(source);
  Process.runSync(Platform.executable, ['format', outputFile]);
}

class S {
  final StateMachine stateMachine;

  final String name;

  final Map<String, S> commands = {};

  S(this.name, this.stateMachine);

  @override
  int get hashCode => name.hashCode;

  @override
  bool operator ==(other) {
    if (other is S) {
      return other.name == name;
    }

    return false;
  }

  void add(String name, S state) {
    if (commands.containsKey(name)) {
      final state = commands[name];
      throw StateError('Transition \'$name\' already exists: $state');
    }

    commands[name] = state;
  }

  @override
  String toString() {
    return name;
  }
}

class StateMachine {
  static const _template = r"""
void main(List<String> args) {
  final door = {{name}}Machine((sm, cmd) {
    throw StateError('''
Unable to move to next state.
State machine: $sm
State: ${sm.state}
Command: $cmd
''');
  });

  door.addListener((command,  previous, current) {
    if (command == {{name}}Command.open) {
      final now = DateTime.now();
      print('Hello, I am a door watcher, the door was open at $now');
    }
  });

  door.addListener((command,  previous, current) {
    print("Move from '${previous.name}' state to '${current.name}' state using '${command.name}' command");
  });

  door.addListener((command,  previous, current) {
    if (command == {{name}}Command.close) {
      print('Good bye!');
    }
  });

  door.moveNext({{name}}Command.open);
  door.moveNext({{name}}Command.close);
  door.moveNext({{name}}Command.lock);
  door.moveNext({{name}}Command.unlock);
  door.moveNext({{name}}Command.open);
}

class {{name}}Machine {
  void Function({{name}}Machine machine, {{name}}Command command) onError;

  final _listeners = <void Function(
    {{name}}Command command,
    {{name}}State previous,
    {{name}}State current,
  )>[];

  {{name}}State _state = {{name}}State.closed;

  {{name}}Machine(this.onError);

  {{name}}State get state => _state;

  void addListener(
      void Function(
        {{name}}Command command,
        {{name}}State previous,
        {{name}}State current,
      )  listener) {
    if (_listeners.contains(listener)) {
      _listeners.remove(listener);
    }

    _listeners.add(listener);
  }

  void moveNext({{name}}Command command) {
    {{@state}}
  }

  void removeListener(void Function(
        {{name}}Command command,
        {{name}}State previous,
        {{name}}State current,
      ) listener) {
    _listeners.remove(listener);
  }

  void _setState({{name}}Command command, {{name}}State previous, {{name}}State current) {
    _state = current;
    for (final listener in _listeners.toList()) {
      listener(command, previous, current);
    }
  }
}

enum {{name}}Command { {{commands}} }

enum {{name}}State { {{states}} }

""";

  final String name;

  S? start;

  final Map<String, S> states = {};

  StateMachine(this.name);

  S add(String name) {
    if (states.containsKey(name)) {
      throw StateError('The state \'$name\' already exists');
    }

    final state = S(name, this);
    states[name] = state;
    return state;
  }

  String generate() {
    String stateExpr(S s) {
      return '${name}State.$s';
    }

    final stateAlternatives = <Operation>[];
    final stateSet = <String>{};
    final commandSet = <String>{};
    String setState(S s) => '_setState(command, _state, ${stateExpr(s)});';
    for (final s in states.values) {
      final testState = Test('_state == ${stateExpr(s)}');
      final commands = <Operation>[];
      for (final entry in s.commands.entries) {
        final commandName = entry.key;
        final nextState = entry.value;
        final testTransition = Test('command == ${name}Command.$commandName');
        final action = testTransition + Action(setState(nextState));
        commands.add(action);
        commandSet.add(commandName);
      }

      final stateAlternative = testState + Choice(commands);
      stateAlternatives.add(stateAlternative);
      stateSet.add(s.name);
    }

    final start = Choice(stateAlternatives);
    final startState = start.toState();
    final s0 = automaton(
      'void',
      startState,
      '{{@state}}',
      accept: 'return;',
      reject: 'onError(this, command);',
    );
    s0.generate(Allocator().allocate);
    final stateList = stateSet.toList();
    final commandList = commandSet.toList();
    stateList.sort();
    commandList.sort();
    var template = _template;
    template = template.replaceAll('{{name}}', name);
    template = template.replaceAll('{{@state}}', s0.source);
    template = template.replaceAll('{{commands}}', commandList.join(', '));
    template = template.replaceAll('{{states}}', stateList.join(', '));
    return template;
  }
}

Source code of the generated state machine.

void main(List<String> args) {
  final door = DoorMachine((sm, cmd) {
    throw StateError('''
Unable to move to next state.
State machine: $sm
State: ${sm.state}
Command: $cmd
''');
  });

  door.addListener((command, previous, current) {
    if (command == DoorCommand.open) {
      final now = DateTime.now();
      print('Hello, I am a door watcher, the door was open at $now');
    }
  });

  door.addListener((command, previous, current) {
    print(
        "Move from '${previous.name}' state to '${current.name}' state using '${command.name}' command");
  });

  door.addListener((command, previous, current) {
    if (command == DoorCommand.close) {
      print('Good bye!');
    }
  });

  door.moveNext(DoorCommand.close);
  door.moveNext(DoorCommand.open);
  door.moveNext(DoorCommand.close);
  door.moveNext(DoorCommand.lock);
  door.moveNext(DoorCommand.unlock);
  door.moveNext(DoorCommand.open);
}

class DoorMachine {
  void Function(DoorMachine machine, DoorCommand command) onError;

  final _listeners = <void Function(
    DoorCommand command,
    DoorState previous,
    DoorState current,
  )>[];

  DoorState _state = DoorState.closed;

  DoorMachine(this.onError);

  DoorState get state => _state;

  void addListener(
      void Function(
        DoorCommand command,
        DoorState previous,
        DoorState current,
      ) listener) {
    if (_listeners.contains(listener)) {
      _listeners.remove(listener);
    }

    _listeners.add(listener);
  }

  void moveNext(DoorCommand command) {
    if (_state == DoorState.closed) {
      if (command == DoorCommand.open) {
        _setState(command, _state, DoorState.open);
        return;
      }
      if (command == DoorCommand.lock) {
        _setState(command, _state, DoorState.locked);
        return;
      }
    }
    if (_state == DoorState.open) {
      if (command == DoorCommand.close) {
        _setState(command, _state, DoorState.closed);
        return;
      }
    }
    if (_state == DoorState.locked) {
      if (command == DoorCommand.unlock) {
        _setState(command, _state, DoorState.closed);
        return;
      }
    }
    onError(this, command);
  }

  void removeListener(
      void Function(
        DoorCommand command,
        DoorState previous,
        DoorState current,
      ) listener) {
    _listeners.remove(listener);
  }

  void _setState(DoorCommand command, DoorState previous, DoorState current) {
    _state = current;
    for (final listener in _listeners.toList()) {
      listener(command, previous, current);
    }
  }
}

enum DoorCommand { close, lock, open, unlock }

enum DoorState { closed, locked, open }

An example of how this example works.

Hello, I am a door watcher, the door was open at 2025-03-09 23:46:29.464238
Move from 'closed' state to 'open' state using 'open' command
Move from 'open' state to 'closed' state using 'close' command
Good bye!
Move from 'closed' state to 'locked' state using 'lock' command
Move from 'locked' state to 'closed' state using 'unlock' command
Hello, I am a door watcher, the door was open at 2025-03-09 23:46:29.469235
Move from 'closed' state to 'open' state using 'open' command

More complex examples #

More complex application examples will be provided later.

0
likes
160
points
34
downloads

Publisher

unverified uploader

Weekly Downloads

An automaton generator is a code generator (codegen) for use in generators of converters, scanners, parsers, state machines, etc.

Repository (GitHub)
View/report issues

Topics

#codegen #code-builder #generator #state-machine

Documentation

API reference

License

BSD-3-Clause (license)

Dependencies

simple_sparse_list

More

Packages that depend on automaton_generator