FennerScript
Um motor de scripts embutível, estaticamente tipado, que roda dentro do seu aplicativo e só enxerga o que você registrar.
O FennerScript é um motor de scripts para aplicativos Fenner, no papel que o PascalScript tem no Delphi: o usuário do aplicativo escreve um script, o aplicativo o compila e executa dentro do próprio processo, expondo ao script as funções, classes e objetos que quiser.
O script é estaticamente tipado, compilado para bytecode e executado por uma VM escrita em Fenner. Nada do que o script faz escapa do que o host registrou: sem acesso a arquivos, memória ou API do Windows, a menos que o host exponha.
fonte do script ──► Lexer ──► Parser ──► Compiler ──► bytecode ──► VM
▲ │
host: RegisterFunction/Class ◄───┘ (nativos)O mínimo#
uses
System.Remainder,
FennerScript.Engine; // + Config SourcePath C:\Fenner\Common\FennerScript\src
var
LEngine: TFsEngine;
begin
LEngine := TFsEngine.Create;
if not LEngine.Compile('program P; begin WriteLn(6 * 7); end.', 'P') then
WriteLn(LEngine.ErrorText) // P(1,40): mensagem
else if not LEngine.Execute then
WriteLn(LEngine.ErrorText);
end.Para rodar um arquivo sem escrever um programa: bin\fsrun.exe script.fs (-d mostra o bytecode, -s conta os passos, -n N limita os passos). Compile o programa com Config All SourcePath apontando para a pasta src do FennerScript (os módulos são FennerScript.*).
A linguagem do script#
É um subconjunto do Fenner (família Delphi, sensível a caixa, palavras reservadas em minúsculas, fechos qualificados como end if; aceitos). Um script pode ser program, module ou unit, ou nem ter cabeçalho.
| Família | Tipos |
|---|---|
| inteiros | Int8 Int16 Int32 Int64, UInt8 UInt16 UInt32 UInt64 (apelidos: Integer, Byte, Word, Cardinal, ShortInt, SmallInt, LongInt, NativeInt…) |
| reais | Float64 (apelidos Double, Single, Float32, Extended) |
| lógico | Bool8 (apelidos Boolean, Bool16…): True/False |
| texto | UString (apelidos String, LString, U8String, WString…), índice base 1 |
| caractere | WChar (apelidos Char, AChar) |
| enumerado | type TDia = (Seg, Ter); com valores explícitos (A = 1, B = 5) |
| conjunto | set of TDia (até 64 elementos), set of 0..9 |
| faixa | type T = 0..255; |
| registro | record … end (valor: atribuir copia) |
| vetor | array[0..9] of T, array[1..3, 1..3], array of T (dinâmico, referência), array of const |
| classe | class(TPai) … end (campos, métodos virtuais, property, class function) |
| procedural | function(A: Int32): Int32, procedure(…) of object |
Comandos: if/else, while, repeat/until, for … to/downto, for X in vetor/texto, case (inteiros, enumerados, caracteres e textos; faixas 1..5), break/continue/exit(valor), try/except [on E: Classe do …]/finally, raise Classe.Create('m'), raise 'texto', raise;, inherited, e if c then a else b como valor. var X := valor; e for var I := 1 to N valem no meio de um bloco.
Ainda não existem no script: ponteiros, genéricos, interfaces, variant, rotinas aninhadas, métodos anônimos, with, goto, string[n].
Rotinas embutidas: Length SetLength High Low Inc Dec Ord Chr Abs Sqr Succ Pred Odd Assigned Trunc Round Floor Ceil Frac Min Max Include Exclude Copy Pos Delete Insert Halt Write WriteLn FreeAndNil; em texto, IntToStr StrToInt StrToIntDef FloatToStr StrToFloat BoolToStr UpperCase LowerCase Trim SameText CompareStr StringOfChar ReplaceStr Format; matemática Sqrt Sin Cos Tan ArcTan Exp Ln Log10 Power Random Pi; e GetTickCount. Format aceita %d %i %u %x %X %c %s %f %g %% com largura, -, 0 e precisão. WriteLn vai para Engine.OnWrite (por omissão, o console).
Aritmética igual à do Fenner#
A promoção de tipos inteiros segue o fc: cada operando sobe para um tipo com sinal de largura suficiente (UInt8/UInt16 → Int32, UInt32 → Int64; o mínimo é Int32), o resultado tem a maior largura, e o resultado de + - * div shl é estreitado (dá a volta) à largura do tipo. A conferência foi feita comparando milhares de expressões aleatórias (inteiros de 8 a 64 bits, reais, e programas inteiros com laços, rotinas, classes e exceções) executadas no fc e no fsrun.
Exceções#
Exception, EDivByZero, ERangeError, EAccessViolation, EConvertError, EInvalidCast e EStackOverflow existem na prelude. Erros da VM (divisão por zero, índice fora da faixa, objeto nil, cast inválido, pilha) viram exceções de script que o try/except captura; sem tratador, a execução termina e Execute devolve False com a mensagem e a linha em ErrorText.
O lado do aplicativo (host)#
Registrar uma função#
procedure NativeTriple(const ACall: TFsCall);
begin
ACall.ResultInt(ACall.ArgInt(0) * 3);
end procedure;
LEngine.RegisterFunction('function Triple(A: Int32): Int32', @NativeTriple);TFsCall lê os argumentos na ordem declarada (ArgInt ArgFloat ArgStr ArgObj ArgHost), escreve parâmetros var/out com SetInt SetFloat SetStr SetObj e o resultado com ResultInt ResultFloat ResultStr ResultObj ResultHost. ACall.Fail('msg') levanta uma exceção de script, e qualquer exceção Fenner que escape do nativo também vira exceção de script.
Registrar uma classe#
LCls := LEngine.RegisterClass('TMeuObj', 'TObject', 'TMeuObj'); // nome no script, pai, ClassName do host
LCls.AddMethod('constructor Create', @NCreate);
LCls.AddMethod('procedure Faz(N: Int32)', @NFaz);
LCls.AddMember('property Nome: UString read GetNome write SetNome');O objeto do host mora na "caixa" do Self. Uma classe do script pode herdar de uma classe do host. FsRegisterStandardClasses(Engine) liga TStringList e é o exemplo completo.
Objetos, constantes e declarações soltas#
LEngine.RegisterObject('Aplicacao', 'TMeuObj', LObjeto); // variável global já ligada
LEngine.RegisterDeclaration('const Versao = ''1.0'';');
LEngine.RegisterDeclaration('type TModo = (Rapido, Lento);');Compilar, executar, chamar#
| Método | O que faz |
|---|---|
Compile(fonte, nome) | acrescenta um módulo; False → ErrorText/ErrorUnit/ErrorLine/ErrorCol |
Execute | cria uma VM nova (globais zeradas) e roda o corpo principal dos módulos |
CallFunction(nome, args) | chama uma rotina do script (FsInt, FsStr, FsFloat, FsObj, FsBool montam os argumentos) |
CallProc(proc, args) | chama um valor procedural que o script passou ao host |
Get/SetGlobalInt/Str/Float | variáveis globais do script |
OnWrite | para onde vai o Write/WriteLn do script |
OnNeedUnit | resolve uses Outro carregando o fonte do módulo |
MaxSteps, Terminate | limite de passos e parada, de outra thread também |
Disassemble | o bytecode, para depurar |
DebugLines + OnLine | com DebugLines := True antes de Compile, a VM chama OnLine(unidade, linha): depurador, passo a passo, realce da linha corrente |
MaxArrayLen, MaxStrLen | limites de memória do script (16 milhões de elementos; 32 milhões de caracteres) |
Segurança e limites#
O script só chega ao que foi registrado. Para código de terceiros: defina MaxSteps, deixe MaxCallDepth/MaxStackSlots da VM nos valores padrão (20 000 chamadas; 4 milhões de slots por banco) e use Terminate a partir de outra thread (um botão "parar"). Nativos que tocam o sistema (arquivos, rede, processos) são decisão do host.
Demonstração e desempenho#
O demo\FsIde.fxp é uma pequena IDE em WCL (editor, saída, Rodar, Desmontar) que mostra o motor dentro de um aplicativo de janelas: expõe ShowMessage, InputBox, Sleep, a classe de host TFsMemo e o objeto global Memo.
Numa máquina de 2010: fib(30) em cerca de 1 s; um laço de 20 milhões de iterações com mod em cerca de 1 s. A VM executa algo como 100 a 150 milhões de instruções por segundo.
Arquitetura#
- O bytecode tem 4 bancos tipados:
I(Int64),F(Float64),S(UString) eO(TFsObject); cada instrução tem 4Int32. FennerScript.Compiler*.fxié a mesma classeTFsCompiler; a prelude (TObject,Exception, externos) é escrita em FennerScript e embutida.FennerScript.Disasmé o desmontador, e os nomes dos opcodes vêm deTFsOp.- Há testes de ponta a ponta (scripts com saída esperada), da API do host e da VM isolada, montando bytecode à mão.