Uma PoC de REPL para Dart
Hackeando com 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:
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...
... 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.
Na verdade, o código é mais direto:
// 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:
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.
E funciona \o/
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!
// 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.
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.
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:
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.
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é?
Mais do Dart
Trazendo construtores primários para o Dart
Um olhar por dentro das decisões de design, dos trade-offs e do açúcar sintático por trás da adição de construtores primários no Dart 3.13.
A história da interop com JS no Dart
O suporte ao Wasm acaba de chegar ao Flutter beta atual, graças a um marco empolgante de interop com JavaScript alcançado no Dart 3.3. Para celebrar…