Uma PoC de REPL para Dart

Hackeando com Dart

Um beija-flor de origami no esquema de cores original do Dart.

O modo interativo do Python é ótimo. O Dart não tem um modo interativo no momento, mas é muito bom para prototipar ideias rapidamente, então vamos ver se conseguimos improvisar alguma coisa!

Aviso: eu trabalho no Google, mas este post é sobre um projeto pessoal. Não faço parte do time do Dart nem de nada relacionado. Esta é só a minha humilde opinião e a minha história. Vem comigo.

O modo interativo de linguagens como Python ou Ruby ajudou a torná-las bem acessíveis para iniciantes. Mas elas não são as únicas que usam o conceito de REPL. Um REPL é um loop de leitura, avaliação e impressão (read-eval-print loop). Shells como BASH ou zsh também usam REPLs quando você interage com eles em um terminal. É também o que você tem ao trabalhar com notebooks no Jupyter (IPython Notebooks), Matlab, Mathematica ou Maple. Essa forma de computação interativa é muito popular entre pesquisadores.

É assim que o modo interativo do Python se parece:

Um usuário interage com o REPL do Python.
O bom e velho REPL do Python

O Dart não permite avaliar instruções em um escopo global como o Python, então não existe um jeito óbvio de fazer isso corretamente. Em Dart, as instruções precisam estar dentro de funções, e a função main é o ponto de entrada de um programa. Igualzinho a C++ ou C#. Pessoalmente, eu prefiro assim, porque fica mais fácil entender o que está acontecendo quando um programa executa. Mas... eu ainda queria muito um modo interativo. Ele me permitiria brincar com ideias e testar coisas ainda mais rápido. Então vamos ver se conseguimos criar um REPL como prova de conceito!

Como podemos criar um REPL em dart?

#

O Dart é muito bom para prototipar. Então vamos fazer exatamente isso e não ficar atolados em questões de design da linguagem :)

Acontece que não existe uma função eval como em JavaScript ou Python, e eu não quero escrever um interpretador completo para implementar uma. Ele não seria muito rápido, e eu também não tenho muito tempo. Porém, quando você depura código Dart no Intellij, pode avaliar expressões enquanto percorre o código passo a passo. Avaliar expressões é exatamente o que queremos fazer, não é? Será que dá para usar esse recurso?

Os recursos de depuração do Dart são expostos pelo seu VM service. Ele é um serviço JSON-RPC fornecido pela Dart VM ao qual você pode se conectar para depurar sua aplicação. A Natalie Weizenbaum publicou um artigo sobre o pacote vm_service_client, que oferece uma API muito boa para interagir com o VM service: http://news.dartlang.org/2016/05/unboxing-packages-vmserviceclient.html

Então o que podemos fazer é o seguinte: nosso REPL pode se conectar ao seu próprio VM service para avaliar as expressões que lê do terminal! Parece loucura...

Pessoa sorrindo segurando uma banana, com o texto 'You are so crazy lol' sobreposto.

... mas funciona! Escrevi um spike rápido e funciona mesmo. O Dart tem suporte a programação assíncrona, o que vem bem a calhar, porque impede que o programa bloqueie a si mesmo ao conversar com o próprio VM service.

Spikes são um conceito do desenvolvimento guiado por testes: são experimentos rápidos e improvisados que a gente escreve para responder algumas questões técnicas.

Com a questão da viabilidade resolvida, fica fácil escrever uma prova de conceito decente. Para avaliar expressões que vão além de 1 + 1, precisamos ter suporte a variáveis. Só que variáveis não podem ser criadas com facilidade, porque uma declaração de variável não é uma expressão em Dart. Em Python, você pode simplesmente declarar uma variável na hora atribuindo um valor a ela. Em Dart, podemos simular campos dinâmicos sobrescrevendo n[oSuchMethod](https://www.dartlang.org/articles/language/emulating-functions#interactions-with-mirrors-and-nosuchmethod) para criar elementos em um dicionário na hora. Chamo essa classe de Scope, e quando avaliamos expressões dentro de uma instância dela, os campos podem ser acessados como variáveis globais.

Pessoa confusa segurando a cabeça, com o texto 'What do these words mean' sobreposto.

Na verdade, o código é mais direto:

dart
// Copyright (c) 2016, Andreas 'blackhc' Kirsch. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
import 'dart:mirrors';
​
Scope currentScope;
​
@proxy
class Scope {
  final _scope = <Symbol, dynamic>{};
​
  Scope();
​
  Scope.predefined(Map<Symbol, dynamic> symbols) {
    _scope.addAll(symbols);
  }
​
  factory Scope.clone(Scope other) => new Scope.predefined(other._scope);
​
  @override
  dynamic noSuchMethod(Invocation invocation) {
    if (invocation.isGetter) {
      if (_scope.containsKey(invocation.memberName)) {
        return _scope[invocation.memberName];
      } else {
        return super.noSuchMethod(invocation);
      }
    } else if (invocation.isSetter) {
      final variable = MirrorSystem.getSymbol(
          MirrorSystem.getName(invocation.memberName).split('=').first);
      _scope[variable] = invocation.positionalArguments.first;
      return null;
    } else if (invocation.isMethod) {
      return Function.apply(_scope[invocation.memberName] as Function,
          invocation.positionalArguments, invocation.namedArguments);
    } else {
      throw new UnsupportedError('Neither setter, nor getter, nor method!');
    }
  }
​
  @override
  String toString() => _scope.toString();
}

Com isso, já dá para fazer coisas como a = 3e b = a*3:

Um usuário avalia expressões simples no REPL do Dart.
Expressões simples: FEITO

Uma limitação em que esbarramos rapidinho é que só conseguimos acessar símbolos que foram importados no arquivo que declara a classe Scope. import ‘…’; não pode ser avaliado pelo VM service. Então nada de dart:io se não o importarmos explicitamente, e nada de bibliotecas próprias :(

Opa, peraí! O Dart consegue criar novos isolates (workers independentes) a partir de uma URI com Isolate.spawnUri. Os usuários poderiam especificar imports adicionais na linha de comando, e o REPL poderia gerar código-fonte incluindo esses imports e então criar um novo isolate com o código gerado, que deixa os imports disponíveis para o usuário.

Pessoa inclinada para a frente numa conversa, com o texto 'That's not enough. We have to go deeper' sobreposto.

E funciona \o/

Um usuário importa bibliotecas próprias com sucesso no REPL do Dart.
Imports personalizados: FEITO

Suportando mais Dart

#

Outro problema é que só conseguimos avaliar expressões. Instruções de controle como blocos if/else ou loops while não são expressões. No caso de instruções, poderíamos envolvê-las em uma closure e executar a closure, o que é uma expressão de chamada de função. Assim, if (a == 1) print('a is 1!!'); viraria

() { if (a == 1) print('a is 1!!'); }();

Só precisamos descobrir se a entrada é uma expressão ou uma instrução. Isso é difícil, porque teríamos de escrever um parser de Dart para isso. Mas o Dart é escrito em Dart, e o pacote analyzer fornece de graça um parser capaz de analisar qualquer código Dart!

dart
// Copyright (c) 2016, Andreas 'blackhc' Kirsch. All rights reserved. Use of
// this source code is governed by a BSD-style license that can be found in the
// LICENSE file.
import 'dart:io';
​
import 'package:analyzer/dart/ast/ast.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:analyzer/error/listener.dart';
import 'package:analyzer/src/dart/scanner/reader.dart';
import 'package:analyzer/src/dart/scanner/scanner.dart';
import 'package:analyzer/src/generated/parser.dart';
​
bool _tryParse(String code, Function parse) {
  final reader = new CharSequenceReader(code);
  final errorListener = new BooleanErrorListener();
  final scanner = new Scanner(null, reader, errorListener);
  final token = scanner.tokenize();
  final parser = new Parser(null, errorListener);
  final node = parse(parser, token) as AstNode;
​
  return !errorListener.errorReported &&
      node != null &&
      node.endToken.next.type == TokenType.EOF;
}
​
bool isExpression(String code) => _tryParse(
    code, (Parser parser, Token token) => parser.parseExpression(token));
​
bool isStatements(String code) => _tryParse(code, (Parser parser, Token token) {
      final statements = parser.parseStatements(token);
      if (statements.isEmpty) {
        return null;
      }
      return statements.last;
    });

E assim, isso também está resolvido.

Um usuário avalia tanto instruções quanto expressões no REPL do Dart.
Instruções e expressões: FEITO

Mais imports

#

O último detalhe que nos impede de importar qualquer biblioteca é que, por padrão, um novo Isolate só enxerga os pacotes mencionados no seu pubspec.yaml. Mas queremos poder importar qualquer biblioteca. Isolate.spawnUri tem um parâmetro packageConfig que nos permite especificar um mapa de nome de pacote para caminho do pacote. Basta usar outro parâmetro de linha de comando para apontar para outro pacote e usar a configuração de pacotes dele no nosso Isolate. Uhuu!

Logo esbarramos no problema de que nosso Isolate precisa acessar o pacote analyzer (e outros), que talvez não sejam carregados por qualquer que seja o pacote com que você queira brincar na sua sessão do REPL. O pacote package_resolver vem ao resgate! Com ele, conseguimos manipular configurações de pacotes com facilidade.

dart
import 'package:package_resolver/package_resolver.dart';
​
Future<PackageResolver> mergePackageConfigs(String otherPackageRoot) async {
  final currentConfig = await PackageResolver.current.packageConfigMap;
  final otherConfig =
      (await SyncPackageResolver.loadConfig(otherPackageRoot + '/.packages'))
          .packageConfigMap;
  // We do something horrible here: We simply merge the current config with
  // the other. Hoping it will still work..
  final config = <String, Uri>{};
  config.addAll(currentConfig);
  config.addAll(otherConfig);
  return new PackageResolver.config(config);
}

Com tudo isso, temos um fluxo de trabalho completo implementado:

Um usuário importa uma biblioteca de outro pacote no REPL do Dart.
Importar qualquer biblioteca de outro pacote: FEITO

O que vem a seguir?

#

Este post ficou longo... A prova de conceito inteira tem 451 linhas de código, ou seja, quase tão longa quanto este post.

Meme do Grumpy Cat com o texto 'THE END IS NEAR - GOOD'.

O código está em https://github.com/BlackHC/dart_repl. Se você tem o Dart instalado, dá para testar facilmente:

pub global activate dart_repl
pub global run dart_repl

Curti muito criar essa prova de conceito no meu tempo livre. Todas as peças se encaixaram em poucas horas. O suporte de IDE para Dart no IntelliJ é excelente, e hoje existe uma tonelada de documentação e artigos por aí. Confira, por exemplo, a série Unboxing Packages da Natalie Weizenbaum: http://news.dartlang.org/2016/04/unboxing-packages-async-part-3.html e outros. Hackear em baixo nível com Dart é divertido, e há ótimas bibliotecas para soltar a criatividade. O code_builder parece muito promissor, e o built_collection oferece coleções imutáveis. O David Morgan também vem publicando artigos sobre coleções imutáveis em Dart.

Para o dart_repl, seria legal importar bibliotecas adicionais em tempo de execução sem precisar reiniciar. O time do Dart adicionou recentemente suporte a hot reload na VM. Isso traz principalmente uma experiência melhor no Flutter para desenvolvedores de apps mobile. Talvez isso também pudesse ser usado para imports sob demanda e para definir funções e classes no REPL.

De modo geral, o Dart poderia ser ótimo para pesquisa e para pesquisadores, e eu adoraria ver suporte do Jupyter ao Dart. Bastaria implementar a interface de kernel dele para tornar um REPL de Dart como esse compatível... :) Deve ser fácil, né?

Meme de encerramento sobre a prova de conceito do REPL do Dart, com o texto 'You Deserve A Cookie'.

Mais do Dart