Como o zig funciona?

De um tradutor: esta postagem foi publicada no blog do autor em 15 de março de 2018. À medida que uma linguagem evolui, sua sintaxe pode ser diferente no momento. Tudo descrito está relacionado ao Zig 0.2.0, a versão atual do idioma é Zig 0.3.0.

Entrei em contato com o autor da postagem e ele gentilmente forneceu um link para o repositório com a versão atual das fontes do projeto no Zig 0.3.0

Olá Vamos escrever um intérprete de Brainfuck! "Por quê?" "Você pode perguntar, mas não encontrará a resposta aqui."

Eu vou fazer isso no Zig .

Zig é ...


... uma nova linguagem de programação. Ainda está em beta e está se desenvolvendo rapidamente. Se você já viu o código Zig antes, o código nesta postagem pode parecer um pouco diferente para você. Ele é realmente diferente! O Zig 0.2.0 acaba de ser lançado, coincidindo com o lançamento do LLVM 6 há algumas semanas, e inclui muitas alterações de sintaxe e melhorias gerais de linguagem. Principalmente, muitos "feitiços" foram substituídos por palavras-chave. Veja aqui uma explicação mais profunda de todas as alterações!

O Zig foi projetado para ser legível e relativamente intuitivo para aqueles familiarizados com linguagens compiladas e digitadas, como C, C ++ e, em alguns momentos, Rust.

O código foi compilado e testado com o Zig 0.2.0, que está disponível no momento através de vários canais , incluindo homebrew, se você estiver no OSX: brew install zig.

Vamos começar


Para saber como o Brainfuck funciona, veja aqui . Não há quase nada para aprender lá, mas é uma linguagem completa de Turing , o que significa que você pode escrever qualquer coisa nela.

Publiquei o código aqui , caso você queira ver o produto final ou as confirmações antecipadas.

Zig é uma linguagem compilada. Quando você compila um programa, o binário resultante (se você estiver compilando um binário executável, não uma biblioteca) deve ter uma função principal que marca o ponto de entrada.

Então ...

// main.zig fn main() void { } 

... e começar ...

 $ zig build-exe main.zig 

... dá ...

 /zig/std/special/bootstrap.zig:70:33: error: 'main' is private /zigfuck/main.zig:2:1: note: declared here 

main deve ser declarado como público para ser visível fora do módulo ...

 // main.zig pub fn main() void { } 

Deixe o programa brainfuck usar uma matriz de 30.000 bytes como memória, eu farei essa matriz.

 // main.zig pub fn main() void { const mem: [30000]u8; } 

Posso declarar uma constante (const) ou uma variável (var). Aqui, declarei mem como uma matriz de 30.000 bytes não assinados (u) (8 bits).

Isso não compila.

 /main.zig:3:5: error: variables must be initialized 

Um programa C equivalente seria compilado normalmente: eu posso declarar uma variável sem inicialização, mas o Zig me obriga a tomar uma decisão agora, no momento em que a variável é declarada. Posso não me importar com o que está escrito nele, mas devo indicar isso explicitamente. Farei isso inicializando a variável com um valor indefinido (indefinido).

 // main.zig pub fn main() void { const mem: [30000]u8 = undefined; } 

A inicialização de uma variável com um valor indefinido não oferece nenhuma garantia sobre o valor da variável na memória. É o mesmo que uma declaração de variável não inicializada em C, exceto que você precisa indicar isso explicitamente.

Mas talvez eu não me importe com como inicializar essa memória. Talvez eu queira ter uma garantia de que os zeros ou algum valor arbitrário sejam escritos lá. Nesse caso, também devo declarar explicitamente isso:

 // main.zig pub fn main() void { const mem = []u8{0} ** 30000; } 

Pode parecer estranho, mas ** é o operador usado para expandir matrizes. Declaro uma matriz de 0 bytes e, em seguida, expanda-a para 30.000 e obtenho o valor final de inicialização de 30.000 zero bytes. Esta operação ocorre uma vez, no momento da compilação . comptime é uma das grandes idéias de Zig, e voltarei a ela em uma das seguintes postagens.

Agora vamos escrever um programa no brainfuck que não faz nada além de incrementar o primeiro slot de memória cinco vezes!

 pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; } 

No Zig, cadeias de caracteres são matrizes de bytes. Não devo declarar src como uma matriz de bytes, porque o compilador implica isso. Isso é opcional, mas se você quiser, é possível:

 const src: [5]u8 = "+++++"; 

Isso irá compilar bem. No entanto, isto:

 const src: [6]u8= "+++++"; 

não será.

 main.zig:5:22: error: expected type '[6]u8', found '[5]u8' 

Mais uma observação: como as strings são apenas matrizes, elas não terminam com zero. No entanto, você pode declarar uma cadeia C. com terminação nula. Como literal, ela terá a seguinte aparência:

 c"Hello I am a null terminated string"; 

Para o bem comum ...


Eu quero fazer algo com cada caractere em uma string. Eu consigo! No início do main.zig, importo algumas funções da biblioteca padrão:

 const warn = @import("std").debug.warn; 

A importação , como praticamente tudo o que começa com o sinal @, é uma função interna do compilador . Esses recursos estão sempre disponíveis globalmente. Importar aqui funciona de maneira semelhante ao javascript - você pode importar qualquer coisa cavando o espaço para nome e extrair dele quaisquer funções ou variáveis ​​publicamente disponíveis. No exemplo acima, importo diretamente a função warn e a atribuo, de repente, à constante warn. Agora ela pode ser chamada. Este é um padrão comum: importamos diretamente do namespace std e, em seguida, chamamos std.debug.warn () ou o atribuímos à variável warn. É assim:

 const std = @import("std"); const warn = std.debug.warn; 

 const warn = @import("std").debug.warn; // main.zig pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { warn("{}", c); } } 

Durante a depuração e o desenvolvimento e teste inicial, eu apenas quero imprimir algo na tela. O zig é propenso a erros e o stdout também é propenso a erros. Não quero fazer isso agora e posso imprimir diretamente no stderr usando o warn, que importamos da biblioteca padrão.

warn usa uma string formatada, como printf em C! O código acima será impresso:

 4343434343 

43 é o código de caractere ascii +. Eu também posso escrever:

 warn("{c}", c); 

e obtenha:

 +++++ 

Então, inicializamos o espaço da memória e escrevemos o programa. Agora estamos percebendo a própria linguagem. Começarei com + e substituirei o corpo do loop for pelo switch:

 for (src) |c| { switch(c) { '+' => mem[0] += 1 } } 

Eu recebo dois erros:

 /main.zig:10:7: error: switch must handle all possibilities switch(c) { ^ /main.zig:11:25: error: cannot assign to constant '+' => mem[0] += 1 ^ 

Obviamente, não posso atribuir um novo valor a uma variável, que é uma constante! mem precisa ser transformado em uma variável ...

 var mem = []u8{0} ** 30000; 

como em outros erros, minha construção de switch deve saber o que fazer se o personagem não for +, mesmo que nada precise ser feito. No meu caso, é exatamente isso que eu quero. Encho este caso com um bloco vazio:

 for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } } 

Agora eu posso compilar o programa. Ligue para avisar no final e execute:

 const warn = @import("std").debug.warn; pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } } warn("{}", mem[0]); } 

Recebo o número 5 impresso em stderr , como eu esperava.

Vamos seguir em frente ...


Da mesma forma, apoiamos.

 switch(c) { '+' => mem[0] += 1, '-' => mem[0] -= 1, else => {} } 

Para usar> e <, você precisa usar uma variável adicional, que serve como um "ponteiro" na memória que eu aloquei para o programa de foda cerebral do usuário.

 var memptr: u16 = 0; 

Como um 16-bit não assinado pode ter no máximo 65535, é mais do que suficiente indexar 30.000 bytes de espaço de endereço.

de fato, 15 bits seriam suficientes para nós, o que nos permite endereçar 32767 bytes. O Zig permite tipos com larguras diferentes , mas ainda não o u15.

você pode realmente fazer o u15 desta maneira:

 const u15 = @IntType(false, 15): 

É proposto que qualquer tipo [iu] \ d + seja válido como um tipo inteiro.

Agora, em vez de usar mem [0], eu posso usar essa variável.

 '+' => mem[memptr] += 1, '-' => mem[memptr] -= 1, 

<e> simplesmente incremente e diminua esse ponteiro.

 '>' => memptr += 1, '<' => memptr -= 1, 

Ótimo Podemos escrever um programa real agora!

Cheque 1,2,3


O Zig possui um mecanismo de teste embutido. Em qualquer lugar em qualquer arquivo, posso escrever um bloco de teste:

 test "Name of Test" { // test code } 

e execute o teste na linha de comando: zig test $ FILENAME. O restante dos blocos de teste é igual ao código regular.

Vamos olhar para isso:

 // test.zig test "testing tests" {} zig test test.zig Test 1/1 testing tests...OK 

Obviamente, um teste vazio é inútil. Eu posso usar assert para realmente confirmar a execução dos testes.

 const assert = @import("std").debug.assert; test "test true" { assert(true); } test "test false" { assert(false); } 

 zig test test.zig "thing.zig" 10L, 127C written :!zig test thing.zig Test 1/2 test true...OK Test 2/2 test false...assertion failure [37;1m_panic.7 [0m: [2m0x0000000105260f34 in ??? (???) [0m [37;1m_panic [0m: [2m0x0000000105260d6b in ??? (???) [0m [37;1m_assert [0m: [2m0x0000000105260619 in ??? (???) [0m [37;1m_test false [0m: [2m0x0000000105260cfb in ??? (???) [0m [37;1m_main.0 [0m: [2m0x00000001052695ea in ??? (???) [0m [37;1m_callMain [0m: [2m0x0000000105269379 in ??? (???) [0m [37;1m_callMainWithArgs [0m: [2m0x00000001052692f9 in ??? (???) [0m [37;1m_main [0m: [2m0x0000000105269184 in ??? (???) [0m [37;1m??? [0m: [2m0x00007fff5c75c115 in ??? (???) [0m [37;1m??? [0m: [2m0x0000000000000001 in ??? (???) [0m 

O teste caiu. Use o seguinte comando para reproduzir o erro:

 ./zig-cache/test 

O rastreamento de pilha na papoula ainda está em desenvolvimento.

Para testar isso com eficiência, preciso dividi-lo em pedaços. Vamos começar com isso:

 fn bf(src: []const u8, mem: [30000]u8) void { var memptr: u16 = 0; for (src) |c| { switch(c) { '+' => mem[memptr] += 1, '-' => mem[memptr] -= 1, '>' => memptr += 1, '<' => memptr -= 1, else => {} } } } pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; bf(src, mem); } 

Parece funcionar, certo?

Mas ...

 /main.zig:1:29: error: type '[30000]u8' is not copyable; cannot pass by value 

isso é descrito em https://github.com/zig-lang/zig/issues/733 .

Zig é rigoroso sobre isso. Tipos complexos e todos os objetos que podem ser redimensionados não podem ser transmitidos por valor. Isso torna a alocação de pilha previsível e lógica, além de evitar cópias desnecessárias. Se você deseja usar a semântica da transferência por valor em seu programa, você pode implementá-lo usando sua estratégia de alocação, mas o próprio idioma não suporta isso em circunstâncias comuns.

A maneira natural de contornar essa limitação é passar um ponteiro em vez de um valor (passar por referência). O Zig usa uma estratégia diferente, fatias. Uma fatia é um ponteiro com um comprimento anexado a ela e com uma verificação de queda nas bordas. A sintaxe na assinatura da função é semelhante a esta:

 fn bf(src: []const u8, mem: []u8) void { ... } 

e ao chamar a função, fica assim:

 bf(src, mem[0..mem.len]); 

Observe que eu defini o limite superior simplesmente referindo-se ao comprimento da matriz. Existe uma forma abreviada de notação para esses casos:

 bf(src, mem[0..]); 

Agora posso começar a escrever testes que testam diretamente a função bf (). Vou adicionar funções de teste ao final do arquivo por enquanto ...

 test "+" { var mem = []u8{0}; const src = "+++"; bf(src, mem[0..]); assert(mem[0] == 3); } 

Pego a matriz mem de um byte e depois verifico o que deve acontecer (o byte é incrementado três vezes). Isso funciona!

 Test 1/1 +...OK 

"-" é verificado da mesma maneira:

 test "-" { var mem = []u8{0}; const src = "---"; bf(src, mem[0..]); assert(mem[0] == 253); } 

Não funciona! Quando tento subtrair 1 de 0, recebo ...

 Test 2/2 -...integer overflow 

mem é uma matriz de bytes não assinados e subtrair 1 de 0 causa um estouro. Mais uma vez, Zig me faz declarar o que quero explicitamente. Nesse caso, não preciso me preocupar com estouro, de fato, quero que isso aconteça, já que estamos lidando com aritmética modular , de acordo com a especificação do cérebro . Isso significa que decrementar uma célula com o número 0 me dará 255 e um incremento de 255 me dará 0.

O Zig possui várias operações aritméticas auxiliares que oferecem a semântica do "empacotamento" garantido .

 '+' => mem[memptr] +%= 1, '-' => mem[memptr] -%= 1, 

Isso resolve todo o problema do estouro e faz o que eu esperava.

Para testar <e>, navego por uma pequena matriz e verifico o valor da célula incrementada:

 test ">" { var mem = []u8{0} ** 5; const src = ">>>+++"; bf(src, mem[0..]); assert(mem[3] == 3); } 

e ...

 test "<" { var mem = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, mem[0..]); assert(mem[3] == 3); assert(mem[2] == 2); assert(mem[1] == 1); } 

Neste último caso, posso comparar diretamente o resultado com uma matriz estática usando ...

 const mem = std.mem; 

Lembre-se de que eu já importei std. No exemplo abaixo, eu uso o mem.eql neste espaço para nome:

 test "<" { var storage = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, storage[0..]); assert(mem.eql(u8, storage, []u8{ 0, 1, 2, 3, 0 })); } 

... e lembre-se, literais de string, são apenas matrizes u8 em zig, e eu posso colocar literais hexadecimais neles, ou seja, O código a seguir funcionará da mesma maneira!

 assert(mem.eql(u8, storage, "\x00\x01\x02\x03\x00")); 

Adicione o "."! Simplesmente imprime como caractere o valor do byte na célula que o ponteiro aponta. Estou usando o aviso agora, mas mais tarde vou substituí-lo por stdout. Isso é fácil de fazer conceitualmente, mas um pouco confuso na implementação. Farei isso mais tarde!

 '.' => warn("{c}", storage[memptr]), 

Ciclos
[e] - a mágica começa aqui ....

[- se o valor da célula atual for zero, pule as etapas para o colchete de fechamento sem executar o código.
] - se o valor da célula atual não for zero, retorne ao colchete de abertura e execute o código novamente.

Desta vez, vou começar com um teste, vou testá-los juntos (obviamente, não faz sentido testá-los separadamente). O primeiro caso de teste - a célula de armazenamento [2] deve estar vazia, embora o loop deva incrementá-lo se for iniciado:

 test "[] skips execution and exits" { var storage = []u8{0} ** 3; const src = "+++++>[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 5); assert(storage[1] == 0); assert(storage[2] == 0); } 

e vou criar espaços em branco para a instrução switch:

 '[' => if (storage[memptr] == 0) { }, ']' => if (storage[memptr] == 0) { }, 

O que fazer agora? Você pode usar uma abordagem ingênua. Eu apenas incremento o ponteiro src até encontrá-lo]. Mas não posso usar o loop for em zig para isso, ele foi criado apenas para iterar pelas coleções, sem perder seus elementos. Uma construção adequada aqui é enquanto:

foi:

 var memptr: u16 = 0; for (src) |c| { switch(c) { ... } } 

tornou-se ...

 var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { ... } srcptr += 1; } 

Agora posso reatribuir o ponteiro srcptr no meio do bloco, farei isso:

 '[' => if (storage[memptr] == 0) { while (src[srcptr] != ']') srcptr += 1; }, 

Isso satisfaz o teste "[] ignora a execução do código e sai"
Isso satisfaz o teste "[] pula a execução e sai", embora não seja totalmente confiável, como veremos.

E quanto a fechar colchetes? Eu acredito que pode ser escrito simplesmente por analogia:

 test "[] executes and exits" { var storage = []u8{0} ** 2; const src = "+++++[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 25); } ']' => if (storage[memptr] != 0) { while (src[srcptr] != '[') srcptr -= 1; }, 

Você pode ver o que acontece ... Uma solução ingênua com dois colchetes tem uma falha fatal e quebra completamente em loops aninhados. Considere o seguinte:

 ++>[>++[-]++<-] 

O resultado deve ser {2, 0}, mas o primeiro colchete aberto simplesmente se move estupidamente para o primeiro colchete, e tudo fica confuso. Você precisa pular para o próximo colchete de fechamento no mesmo nível de aninhamento. É fácil adicionar um contador de profundidade e rastreá-lo à medida que avança pela linha. Fazemos isso nas duas direções:

 '[' => if (storage[memptr] == 0) { var depth:u16 = 1; srcptr += 1; while (depth > 0) { srcptr += 1; switch(src[srcptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } }, ']' => if (storage[memptr] != 0) { var depth:u16 = 1; srcptr -= 1; while (depth > 0) { srcptr -= 1; switch(src[srcptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } }, 

e testes relacionados: observe que src nos dois testes inclui um loop interno.

 test "[] skips execution with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++>[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 2); assert(storage[1] == 0); } test "[] executes with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 2); } 

Separadamente, observe [-] - o idioma do cérebro, significando "zere esta célula". Você pode ver que não importa qual o valor da célula no início, ela será decrementada até atingir 0 e a execução continuará.

Caminho azarado


Não contei com a possibilidade de o programa em bf ser interrompido. O que acontece se eu enviar um programa de entrada incorreto ao meu intérprete? Por exemplo, simplesmente [sem um colchete de fechamento, ou <, que imediatamente vai além da matriz de memória? (Posso quebrar o ponteiro da memória, mas é melhor considerar isso como um erro).

Vou olhar um pouco à frente e explicar todas as diferenças no código. Vou colocar a função intérprete bf em um arquivo separado e também a funcionalidade seekBack e seekForward em minhas próprias funções.

 const warn = @import("std").debug.warn; const sub = @import("std").math.sub; fn seekBack(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr = sub(u16, ptr, 1) catch return error.OutOfBounds; switch(src[ptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } return ptr; } fn seekForward(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr += 1; if (ptr >= src.len) return error.OutOfBounds; switch(src[ptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } return ptr; } pub fn bf(src: []const u8, storage: []u8) !void { var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { '+' => storage[memptr] +%= 1, '-' => storage[memptr] -%= 1, '>' => memptr += 1, '<' => memptr -= 1, '[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr), '.' => warn("{c}", storage[memptr]), else => {} } srcptr += 1; } } 

Isso facilita muito a leitura, na minha opinião, do askForward e do askBack e parece muito semelhante, e fiquei tentado a refatorá-los para algo mais inteligente e compacto, mas no final eles fazem coisas diferentes e lidam com erros também de maneiras diferentes. Mais fácil de copiar e ajustar, e assim ficará mais claro. Também ajustarei o askForward mais tarde, em algum momento, possivelmente em uma postagem subsequente.

Eu adicionei algumas coisas importantes! Observe que todas as três funções agora estão retornando um tipo! .. Esta é a nova sintaxe para o que costumava ser o tipo% T (união de erro). Isso significa que a função pode retornar um determinado tipo ou um erro. Quando tento chamar essa função, devo usar try antes de chamar a função, que lança o erro na pilha de chamadas se o erro ocorrer ou usar catch:

 const x = functionCall() catch {} 

Onde eu manejo erros em um bloco catch. Como está escrito, o catch pode engolir quaisquer erros. Essa é uma prática ruim, mas aqui o Zig nos faz fazê-lo explicitamente. Se eu pegar um erro em um bloco vazio, declaro que não acho que possa ocorrer um erro ou não preciso lidar com isso. Na prática, pode ser algo como TODO e, de fato, é muito fácil torná-lo explícito!

 const x = functionCall() catch { @panic("TODO") } 

Lembre-se de que esse caso nunca acontecerá no código de produção. Estou notificando o compilador que sei o que estou fazendo. Se um erro pudesse ocorrer, eu teria que adicionar o tratamento de erros.

Então, quais erros devo retornar do seekBack ou seekForward?

Em seekBack:

 ptr = sub(u16, ptr, 1) catch return error.OutOfBounds; 

Substituí o ponteiro de decremento para usar a sub-função std lib, que gera um erro de estouro se ocorrer um estouro. Quero pegar esse erro e retornar o erro OutOfBounds, que eu criei aqui apenas usando-o.

Erros O Zig é basicamente uma matriz de códigos de erro gerados pelo compilador quando você usa o erro. Eles são garantidos como únicos e podem ser usados ​​como valores em um bloco de chave.

Quero usar OutOfBounds aqui porque, semanticamente, se o ponteiro da memória se tornar menor que zero, peço que o tempo de execução vá além do espaço de memória que aloquei.

da mesma forma na função seekForward:

 if (ptr >= src.len) return error.OutOfBounds; 

Nesse caso, se o ponteiro for maior que src.len, eu pego o erro aqui e retorno o mesmo erro.

ao ligar para:

 '[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr), 

Eu tento chamar essas funções. Se eles forem chamados com êxito, serão executados corretamente e tente retorna srcptr. Se não tiverem êxito, tente encerra a função e retorna um erro no local da chamada para toda a função bf.

A chamada pode ser da principal!

 const bf = @import("./bf.zig").bf; // yes, hello const hello_world = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; pub fn main() void { storage = []u8{0} ** 30000; bf(hello_world, storage[0..]) catch {}; } 

Eu engulo esse erro aqui, e isso não deve ser feito, mas observaremos um ponto importante sobre a facilidade com que o zig pode transmitir erros na pilha de chamadas. Não é responsabilidade da função de chamada verificar todos os casos de erro, mas o compilador força a chamada de todas as funções que podem falhar com uma tentativa. Isso sempre deve ser feito, mesmo que os erros sejam ignorados!

A nova sintaxe try / catch elimina muitos feitiços como %% e% que as pessoas não gostam tanto.

Agora, eu implementei 7 dos 8 caracteres do cérebro, e isso é suficiente para executar um programa "significativo".

Um programa significativo


Aqui está o programa:

 //   ,   const fib = "++++++++++++++++++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++++>++++++++++++++++>>+<<[>>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[<+>-]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<<<++++++++++++++++++++++++++++++++++++++++++++++++.[-]<<<<<<<.>.>>[>>+<<-]>[>+<<+>-]>[<+>-]<<<-]<<++..."; 

Vamos correr ...

 pub fn main() void { storage = []u8{0} ** 30000; bf(fib, storage[0..]) catch {}; } 

voila!

 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219, 

Uma lembrança volta a mim toda vez que penso em uma série de Fibonacci ... eu a descobri no programa PBS (Public Broadcasting Service, um serviço de transmissão de televisão não comercial americano) nos anos 80, e sempre me lembro disso. Eu pensei que seria esquecido, mas o Youtube é uma grande coisa .

Como posso melhorar isso?


Eu já sugeri alguns TODOs. Eu não deveria ter usado stderr para saída. Eu quero usar stdout.

Cada vez que abro o intérprete, abro o fluxo no stdout e imprimo nele:

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ... 

O que está acontecendo aqui? io.getStdOut(), ( catch unreachable — , !). , , , print. print , warn, . print , .

, stdout, stdout. Zig , , .

, , ? , , ? , ? , Zig !

, !

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch unreachable; } 


, bf , !void. , main. , :

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; } 

!

 /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch shell returned 1 

, bf ! , , stdout, bf. , , , try. , , , catch, try, , .

, :

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ... 

:

 const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(try io.getStdOut())).stream); ... '.' => try stdout.print("{c}", storage[memptr]), ... 

:

 const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; } 

, , !

 /Users/jfo/code/zigfuck/main.zig:7:46: error: error.SystemResources not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OperationAborted not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.IoPending not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.BrokenPipe not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.Unexpected not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.WouldBlock not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileClosed not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DestinationAddressRequired not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DiskQuota not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileTooBig not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.InputOutput not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoSpaceLeft not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.AccessDenied not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoStdHandles not handled in switch shell returned 1 

Zig , ! switch , , , , .

 pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { error.OutOfBounds => @panic("Out Of Bounds!"), else => @panic("IO error") }; } 

Ainda não está correto o tratamento de erros, a rigor, mas só quero demonstrar como o Zig é inteligente, relatando todos os tipos de casos de erro à função de chamada! E quando ocorre um erro, você obtém um rastreamento de erro em vez de um rastreamento de pilha! Coisa legal!

Todo


Existem muitas melhorias diferentes que você pode fazer com o intérprete! Na verdade, você precisa lidar corretamente com todos os erros e implementar o operador ",", que no brainfuck atua como a função getc, permitindo inserir dados no programa quando ele é executado. Você também precisa tornar possível ler o arquivo de origem no buffer e interpretá-lo, em vez de usar o código-fonte bf codificado. Existem também algumas melhorias que não são estritamente necessárias, mas podem ilustrar alguns dos recursos do Zig. Em vez de despejá-los todos no final do post, vou dividi-los em partes e publicá-los em posts futuros, que serão menores e mais fáceis de digerir.

Conclusão


, , Zig . Zig , , , , , ++. , , . , , . Zig , , , .

Zig, , 0.2.0 ! , , debug-, , ! --release-fast --release-safe, . .

Zig. , 1.0.0, Zig, , , !

, #zig freenode , .

Source: https://habr.com/ru/post/pt435574/


All Articles