De un traductor: esta publicación se publicó en el blog del autor el 15 de marzo de 2018. A medida que evoluciona un lenguaje, su sintaxis puede ser diferente en la actualidad. Todo lo descrito se relaciona con Zig 0.2.0, la versión actual del lenguaje es Zig 0.3.0.
Me puse en contacto con el autor de la publicación, y amablemente proporcionó un enlace al repositorio con la versión actual de las fuentes del proyecto en Zig 0.3.0
Hola ¡Escribamos un intérprete Brainfuck! "¿Por qué?" "Puede preguntar, pero no encontrará la respuesta aquí".
Lo haré en
Zig .

Zig es ...
... un nuevo lenguaje de programación. Todavía está en beta y se está desarrollando rápidamente. Si has visto el código Zig antes, el código en esta publicación puede parecerte un poco diferente. ¡Él es realmente diferente! Zig 0.2.0 acaba de ser lanzado, coincidiendo con el lanzamiento de
LLVM 6 hace unas semanas, e incluye muchos cambios de sintaxis y mejoras generales del lenguaje. En su mayoría, muchos "hechizos" han sido reemplazados por palabras clave. ¡Vea
aquí para una explicación más profunda de todos los cambios!
Zig está diseñado
para ser legible y relativamente intuitivo para aquellos familiarizados con lenguajes compilados y mecanografiados como C, C ++ y, en algunos puntos, Rust.
El código fue compilado y probado con Zig 0.2.0, que está disponible ahora a
través de varios canales , incluido homebrew, si está en OSX: brew install zig.
Empecemos
Para aprender cómo funciona Brainfuck, mira
aquí . No hay casi nada que aprender allí, pero es un lenguaje
completo de Turing , lo que significa que puede escribir
cualquier cosa en él.
Publiqué el código
aquí , en caso de que quiera ver el producto final o las confirmaciones iniciales.
Zig es un lenguaje compilado. Cuando compila un programa, el binario resultante (si está compilando un binario ejecutable, no una biblioteca) debe tener una función principal que marque el punto de entrada.
Entonces ...
// main.zig fn main() void { }
... y empezar ...
$ zig build-exe main.zig
... se da por vencido ...
/zig/std/special/bootstrap.zig:70:33: error: 'main' is private /zigfuck/main.zig:2:1: note: declared here
main debe declararse como público para ser visible fuera del módulo ...
// main.zig pub fn main() void { }
Deje que el programa Brainfuck use una matriz de 30,000 bytes como memoria, haré una matriz de este tipo.
// main.zig pub fn main() void { const mem: [30000]u8; }
Puedo declarar una constante (const) o una variable (var). Aquí, declare mem como una matriz de 30,000 bytes sin signo (u) (8 bits).
Esto no se compila.
/main.zig:3:5: error: variables must be initialized
Un programa C equivalente se compilaría normalmente: puedo declarar una variable sin inicialización, pero Zig me obliga a tomar una decisión ahora, en el momento en que se declara la variable. Puede que no me importe lo que se escriba en él, pero debo indicarlo explícitamente. Haré esto inicializando la variable con un valor indefinido (indefinido).
// main.zig pub fn main() void { const mem: [30000]u8 = undefined; }
La inicialización de una variable con un valor indefinido no ofrece ninguna garantía sobre el valor de la variable en la memoria. Esto es lo mismo que una declaración de variable no inicializada en C, excepto que debe indicarlo explícitamente.
Pero tal vez no me importa cómo inicializar este recuerdo. Quizás quiero tener una garantía de que los ceros o algún valor arbitrario están escritos allí. En este caso, también debería decir explícitamente esto:
// main.zig pub fn main() void { const mem = []u8{0} ** 30000; }
Puede parecer extraño, pero ** es el operador utilizado para expandir las matrices. Declaro una matriz de 0 bytes y luego la amplío a 30,000 y obtengo el valor de inicialización final de 30,000 bytes cero. Esta operación ocurre una vez,
en tiempo de compilación . comptime es una de las grandes ideas de Zig, y volveré sobre ella en una de las siguientes publicaciones.
¡Ahora escribamos un programa sobre brainfuck que no hace más que incrementar la primera ranura de memoria cinco veces!
pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; }
En Zig, las cadenas son conjuntos de bytes. No debería declarar src como una matriz de bytes, porque el compilador implica esto. Esto es opcional, pero si lo desea, es posible:
const src: [5]u8 = "+++++";
Esto compilará bien. Sin embargo, esto:
const src: [6]u8= "+++++";
No será.
main.zig:5:22: error: expected type '[6]u8', found '[5]u8'
Una nota más: dado que las cadenas son solo matrices, no terminan en cero. Sin embargo, puede declarar una cadena C terminada en nulo. Como literal, se verá así:
c"Hello I am a null terminated string";
Por el bien común ...
Quiero hacer
algo con cada personaje de una cadena. Puedo hacerlo! Al comienzo de main.zig, importo algunas funciones de la biblioteca estándar:
const warn = @import("std").debug.warn;
importar , como prácticamente todo lo que comienza con el signo @, es una
función de compilación incorporada . Dichas características siempre están disponibles a nivel mundial. Importar aquí funciona de manera similar a JavaScript: puede importar cualquier cosa al cavar en el espacio de nombres y extraer de él cualquier función o variable disponible públicamente. En el ejemplo anterior, importo directamente la función de advertencia y la asigno, de repente, a la constante de advertencia. Ahora ella puede ser llamada. Este es un patrón común: importamos directamente desde el espacio de nombres estándar y luego llamamos a std.debug.warn () o lo asignamos a la variable de advertencia. Se ve así:
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 la depuración y el desarrollo inicial y las pruebas, solo quiero imprimir algo en la pantalla. Zig es
propenso a errores , y stdout también es propenso a errores. No quiero hacer esto ahora, y puedo imprimir directamente en stderr usando warn, que importamos de la biblioteca estándar.
warn toma una cadena formateada, como printf en C! El código anterior imprimirá:
4343434343
43 es el código de carácter ascii +. También puedo escribir:
warn("{c}", c);
y obtener:
+++++
Entonces, inicializamos el espacio de memoria y escribimos el programa. Ahora nos estamos dando cuenta del lenguaje mismo. Comenzaré con + y reemplazaré el cuerpo del bucle for con el interruptor:
for (src) |c| { switch(c) { '+' => mem[0] += 1 } }
Me dan dos errores:
/main.zig:10:7: error: switch must handle all possibilities switch(c) { ^ /main.zig:11:25: error: cannot assign to constant '+' => mem[0] += 1 ^
¡Por supuesto, no puedo asignar un nuevo valor a una variable, que es una constante! mem necesita hacerse una variable ...
var mem = []u8{0} ** 30000;
Al igual que con otros errores, mi construcción de
interruptor debería saber qué hacer si el carácter no es +, incluso si no se necesita hacer nada. En mi caso, esto es exactamente lo que quiero. Lleno este caso con un bloque vacío:
for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } }
Ahora puedo compilar el programa. Llama al avisar al final y ejecuta:
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]); }
Me sale el número 5 impreso en
stderr , como esperaba.
Sigamos adelante ...
Del mismo modo, apoyamos.
switch(c) { '+' => mem[0] += 1, '-' => mem[0] -= 1, else => {} }
Para usar> y <, necesita usar una variable adicional, que sirve como un "puntero" en la memoria que asigné para el programa brainfuck del usuario.
var memptr: u16 = 0;
Dado que un 16 bits sin signo puede ser un máximo de 65535, es más que suficiente para indexar 30,000 bytes de espacio de direcciones.
de hecho, 15 bits serían suficientes para nosotros, lo que nos permite direccionar 32767 bytes. Zig permite tipos con diferentes anchos , pero aún no u15.
en realidad puedes hacer u15 de esta manera:
const u15 = @IntType(false, 15):
Se propone que cualquier tipo [iu] \ d + sea válido como un tipo entero.
Ahora, en lugar de usar mem [0], puedo usar esta variable.
'+' => mem[memptr] += 1, '-' => mem[memptr] -= 1,
<y> simplemente incremente y disminuya este puntero.
'>' => memptr += 1, '<' => memptr -= 1,
Genial ¡Podemos escribir un programa real ahora!
Comprobar 1,2,3
Zig tiene un motor de prueba incorporado. En cualquier parte de cualquier archivo puedo escribir un bloque de prueba:
test "Name of Test" { // test code }
y ejecute la prueba desde la línea de comando: zig test $ FILENAME. El resto de los bloques de prueba son los mismos que los del código regular.
Veamos esto:
// test.zig test "testing tests" {} zig test test.zig Test 1/1 testing tests...OK
Por supuesto, una prueba vacía es inútil. Puedo usar afirmar para confirmar realmente la ejecución de las pruebas.
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
La prueba cayó. Use el siguiente comando para reproducir el error:
./zig-cache/test
El seguimiento de la pila en la amapola aún está en desarrollo.Para probar esto de manera eficiente, necesito romperlo en pedazos. Comencemos con esto:
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 que funciona, ¿verdad?
Pero ...
/main.zig:1:29: error: type '[30000]u8' is not copyable; cannot pass by value
Esto se describe en https://github.com/zig-lang/zig/issues/733 .
Zig es estricto sobre esto. Los tipos complejos y todos los objetos que pueden cambiar de tamaño no se pueden pasar por valor. Esto hace que la asignación de la pila sea predecible y lógica, y evita copias innecesarias. Si desea utilizar la semántica de transferencia por valor en su programa, puede implementarla usted mismo utilizando su estrategia de asignación, pero el lenguaje en sí mismo no lo admite en circunstancias normales.
La forma natural de evitar esta limitación es pasar un puntero en lugar de un valor (pasar por referencia). Zig usa una estrategia diferente, cortes. Una rebanada es un puntero con una longitud adjunta y con un cheque para caer en los bordes. La sintaxis en la firma de la función se ve así:
fn bf(src: []const u8, mem: []u8) void { ... }
y al llamar a la función se ve así:
bf(src, mem[0..mem.len]);
Tenga en cuenta que definí el límite superior simplemente haciendo referencia a la longitud de la matriz. Hay una forma abreviada de notación para tales casos:
bf(src, mem[0..]);
Ahora puedo comenzar a escribir pruebas que prueben la función bf () directamente. Agregaré funciones de prueba al final del archivo por ahora ...
test "+" { var mem = []u8{0}; const src = "+++"; bf(src, mem[0..]); assert(mem[0] == 3); }
Tomo la matriz de memoria de un byte y luego verifico lo que debería suceder (el byte se incrementa tres veces). Funciona!
Test 1/1 +...OK
"-" se marca de la misma manera:
test "-" { var mem = []u8{0}; const src = "---"; bf(src, mem[0..]); assert(mem[0] == 253); }
No funciona! Cuando trato de restar 1 de 0, obtengo ...
Test 2/2 -...integer overflow
mem es una matriz de bytes sin signo, y restar 1 de 0 provoca un desbordamiento. Nuevamente, Zig me hace declarar lo que quiero explícitamente. En este caso, no tengo que preocuparme por el desbordamiento, de hecho, quiero que suceda, ya que estamos tratando con
aritmética modular , de acuerdo con la
especificación de brainfuck . Esto significa que decrementar una celda con el número 0 me dará 255, y un incremento de 255 me dará 0.
Zig tiene varias operaciones aritméticas auxiliares que ofrecen la
semántica de la "envoltura" garantizada .
'+' => mem[memptr] +%= 1, '-' => mem[memptr] -%= 1,
Esto resuelve todo el problema de desbordamiento y hace lo que esperaba.
Para probar <and>, navego a través de una pequeña matriz y verifico el valor de la celda incrementada:
test ">" { var mem = []u8{0} ** 5; const src = ">>>+++"; bf(src, mem[0..]); assert(mem[3] == 3); }
y ...
test "<" { var mem = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, mem[0..]); assert(mem[3] == 3); assert(mem[2] == 2); assert(mem[1] == 1); }
En el último caso, puedo comparar directamente el resultado con una matriz estática usando ...
const mem = std.mem;
Recordemos que ya he importado std. En el siguiente ejemplo, uso mem.eql en este espacio de nombres:
test "<" { var storage = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, storage[0..]); assert(mem.eql(u8, storage, []u8{ 0, 1, 2, 3, 0 })); }
... y recuerden, literales de cadena, estos son solo matrices u8 en zig, y puedo poner literales hexadecimales en ellos, es decir ¡El siguiente código funcionará de la misma manera!
assert(mem.eql(u8, storage, "\x00\x01\x02\x03\x00"));
Añadir el "." Simplemente imprime como carácter el valor de byte en la celda a la que apunta el puntero. Estoy usando warn ahora, pero más tarde lo reemplazaré con stdout. Esto es fácil de hacer conceptualmente, pero algo confuso en la implementación. ¡Lo haré luego!
'.' => warn("{c}", storage[memptr]),
Ciclos
[y] - la magia comienza aquí ...
[- si el valor de la celda actual es cero, omita los pasos al corchete de cierre sin ejecutar el código.
] - si el valor de la celda actual no es cero, regrese al paréntesis de apertura y ejecute el código nuevamente.
Esta vez comenzaré con una prueba, los probaré juntos (obviamente, no tiene sentido probarlos por separado). El primer caso de prueba: la celda de almacenamiento [2] debe estar vacía, aunque el ciclo debe aumentarla si comienza:
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); }
y crearé espacios en blanco para la declaración de cambio:
'[' => if (storage[memptr] == 0) { }, ']' => if (storage[memptr] == 0) { },
Que hacer ahora Puedes usar un enfoque ingenuo. Simplemente incremente el puntero src hasta que lo encuentre]. Pero no puedo usar el bucle for en zig para esto, se creó solo para iterar a través de colecciones, sin perder sus elementos. Una construcción adecuada aquí es mientras:
fue:
var memptr: u16 = 0; for (src) |c| { switch(c) { ... } }
se convirtió en ...
var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { ... } srcptr += 1; }
Ahora puedo reasignar el puntero srcptr en el medio del bloque, haré esto:
'[' => if (storage[memptr] == 0) { while (src[srcptr] != ']') srcptr += 1; },
Esto satisface la prueba "[] omite la ejecución del código y sale"
Esto satisface la prueba "[] omite la ejecución y sale", aunque no es del todo confiable, como veremos.
¿Qué hay de cerrar los corchetes? Creo que se puede escribir simplemente por analogía:
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; },
Puedes ver lo que sucede ... Una solución ingenua con dos paréntesis tiene un defecto fatal y se rompe por completo en los bucles anidados. Considere lo siguiente:
++>[>++[-]++<-]
El resultado debería ser {2, 0}, pero el primer paréntesis abierto simplemente se mueve estúpidamente al primer paréntesis de cierre, y todo se vuelve desordenado. Debe saltar al siguiente corchete de cierre en el mismo nivel de anidamiento. Es fácil agregar un contador de profundidad y rastrearlo a medida que avanza a lo largo de la línea. Lo hacemos en ambas direcciones:
'[' => 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 => {} } } },
y pruebas relacionadas: tenga en cuenta que src en ambas pruebas incluye un bucle 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); }
Por separado, tenga en cuenta [-] - el idioma de brainfuck, que significa "cero esta celda". Puede ver que no importa qué valor tenía la celda al principio, se reducirá hasta llegar a 0 y luego la ejecución continuará.
Camino desafortunado
No conté con la posibilidad de que el programa en bf se rompiera. ¿Qué sucede si envío un programa de entrada incorrecto a mi intérprete? Por ejemplo, simplemente [sin un corchete de cierre, o <, que inmediatamente va más allá de la matriz de memoria? (Puedo ajustar el puntero de memoria, pero es mejor considerar esto como un error).
Voy a mirar un poco más adelante y explicar todas las diferencias en el código. Pondré la función de intérprete bf en un archivo separado y también pondré la funcionalidad seekBack y seekForward en mis pequeñas funciones.
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; } }
Esto hace que el cambio sea mucho más fácil de leer, en mi opinión, seekForward y seekBack funcionan y se ven muy similares, y tuve la tentación de refactorizarlos en algo más inteligente y compacto, pero al final hacen cosas diferentes y manejan los errores También de diferentes maneras. Más fácil de copiar y ajustar, por lo que será más claro. También ajustaré seekForward más tarde, en algún momento, posiblemente en una publicación posterior.
¡Agregué algunas cosas importantes! ¡Tenga en cuenta que las tres funciones ahora devuelven un tipo! .. Esta es la nueva sintaxis de lo que solía ser el tipo% T (unión de error). Esto significa que la función puede devolver cierto tipo o un error. Cuando intento llamar a tal función, debo usar try antes de llamar a la función, que arroja el error en la pila de llamadas si ocurre el error, o usar catch:
const x = functionCall() catch {}
Donde manejo errores en un bloque catch. Tal como está escrito, catch puede tragarse cualquier error. Esta es una mala práctica, pero aquí Zig nos obliga a hacerlo explícitamente. Si encuentro un error en un bloque vacío, declaro que no creo que pueda ocurrir un error o que no necesito manejarlo. En la práctica, puede ser algo como TODO, y de hecho es muy fácil hacerlo explícito.
const x = functionCall() catch { @panic("TODO") }
Recuerde que tal caso nunca sucederá en el código de producción. Estoy notificando al compilador que sé lo que estoy haciendo. Si pudiera ocurrir un error, tendría que agregar el manejo de errores.
Entonces, ¿qué errores debo devolver de seekBack o seekForward?
En seekBack:
ptr = sub(u16, ptr, 1) catch return error.OutOfBounds;
Reemplacé el puntero de disminución para usar la subfunción de std lib, que arroja un error de desbordamiento si ocurre un desbordamiento. Quiero detectar este error y devolver el error OutOfBounds en su lugar, que creo aquí solo con usarlo.
Errores Zig es básicamente una matriz de códigos de error que genera el compilador cuando usa el error. Algún tipo de error. Se garantiza que son únicos y se pueden usar como valores en un bloque de interruptores.
Quiero usar OutOfBounds aquí porque, semánticamente, si el puntero de memoria se vuelve menor que cero, le pido al tiempo de ejecución que vaya más allá del espacio de memoria que asigné.
de manera similar en la función seekForward:
if (ptr >= src.len) return error.OutOfBounds;
En este caso, si el puntero es más grande que src.len, capto el error aquí y devuelvo el mismo error.
al llamar:
'[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr),
Intento llamar a estas funciones. Si se llaman con éxito, se ejecutan correctamente e intenta devolver srcptr. Si no tienen éxito, intente termina la función y devuelve un error al lugar de la llamada a toda la función bf.
¡La llamada puede ser de main!
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 {}; }
Me trago este error aquí, y no debería hacerse, pero notaremos un punto importante sobre la facilidad con la que zig puede pasar errores por la pila de llamadas. No es responsabilidad de la función de llamada verificar cada caso de error, pero el compilador fuerza la llamada de cada función que puede fallar con un intento. ¡Esto siempre debe hacerse, incluso si se ignoran los errores!
La nueva sintaxis try / catch elimina los muchos hechizos como %% y% que a la gente no le gusta tanto.
Ahora he implementado 7 de 8 personajes de brainfuck, y esto es suficiente para ejecutar un programa "significativo".
Un programa significativo
Aquí está el programa:
// , const fib = "++++++++++++++++++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++++>++++++++++++++++>>+<<[>>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[<+>-]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<<<++++++++++++++++++++++++++++++++++++++++++++++++.[-]<<<<<<<.>.>>[>>+<<-]>[>+<<+>-]>[<+>-]<<<-]<<++...";
Corramos ...
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,
Cada vez que pienso en una serie de Fibonacci recuerdo un recuerdo ... Lo descubrí en el programa PBS (Public Broadcasting Service, un servicio de transmisión de televisión no comercial estadounidense) en los años 80, y siempre recuerdo eso. Pensé que sería olvidado, pero YouTube es una gran cosa .
¿Cómo puedo mejorar esto?
Ya he insinuado algunos TODOS. No debería haber usado stderr para la salida. Quiero usar stdout.
Cada vez que abro el intérprete, abro la secuencia en stdout e imprimo en ella:
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, ...
¿Qué está pasando aquí?
Llamo a io.getStdOut (), que puede generar errores (y de nuevo, explícitamente me trago un posible error con catch inalcanzable: si esta función devuelve un error, ¡el programa se bloqueará!). Inicializo la secuencia, tomo un puntero e inicializo como una secuencia de salida en la que puedo escribir llamando a print. print acepta una cadena formateada, al igual que warn, por lo que el reemplazo se realiza directamente. print también puede generar un error, y también me trago estos errores.En un programa escrito correctamente, debo tener en cuenta los posibles errores de abrir stdout, así como los posibles errores de intentos de escribir en stdout. Zig hace que sea muy fácil ignorar estos errores siempre que sepa que los está ignorando.¿Qué sucede si decido que quiero convertir mi prototipo en un lanzamiento? ¿Me sentaré con una taza de café y haré un trabajo desagradecido de manejo de errores, confiando en décadas de experiencia y conocimiento para enumerar cada posible caso de error, y cómo puedo manejarlo? Pero, ¿qué pasa si no tengo décadas de experiencia y conocimiento? ¡Está bien, Zig lo hará!Quiero demostrar algo poderoso, ¡salida de error! 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; }
Sé que bf puede generar errores porque devuelve! Void. Me trago este error en el lado de la llamada, en la función principal. Cuando estoy listo para aceptar mi destino y hacer lo correcto, puedo detectar posibles errores como este: 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) { }; }
¡El compilador ahora es mi amigo! /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch shell returned 1
¡Este error debería serle familiar, ya que se generó desde bf y funciones auxiliares! Pero imaginemos que miro los errores generados por stdout que tragué en bf. En lugar de tragarlos, debería empujarlos hacia arriba de la cadena usando try. Recuerde que usando una llamada a la función que genera errores sin captura, usamos try, que termina la función cuando ocurre un error, proporcionando a la función de llamada el manejo de cualquier error potencial.Entonces, en lugar de: 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, ...
Nosotros hacemos: 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]), ...
Compilamos: 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) { }; }
¡y obtenga una lista de todos los posibles errores que puedo obtener llamando a la función! /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 me da la oportunidad de manejar estos errores con cuidado si quiero o puedo hacerlo! Realizo un cambio en función de los valores de error, manejo de casos si lo deseo y omitir si quiero omitirlos. 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") }; }
Esto todavía no es un manejo correcto de los errores, estrictamente hablando, ¡pero solo quiero demostrar cuán inteligente es Zig informando todo tipo de casos de error a la función de llamada! Y cuando se produce un error, se obtiene un seguimiento de error en lugar de un seguimiento de pila. Cosa genial!Todo
¡Hay muchas mejoras diferentes que puede hacer con el intérprete! En realidad, necesita manejar correctamente todos los errores, y debe implementar el operador "," que en brainfuck actúa como la función getc, permitiéndole ingresar datos en el programa cuando se ejecuta. También debe hacer posible leer el archivo fuente en el búfer e interpretarlo, en lugar de usar el código fuente bf codificado. También hay algunas mejoras que no son estrictamente necesarias, pero que pueden ilustrar algunas de las características de Zig. En lugar de tirarlos todos al final de la publicación, los dividiré en partes y los publicaré en publicaciones futuras, que serán más pequeñas y fáciles de digerir.Conclusión
Espero que este proyecto en miniatura a medio terminar te dé una idea de cómo se ve el código Zig y para qué se puede usar. Zig no es un cuchillo suizo, no es una herramienta perfecta para todo, se enfoca en ciertas cosas, en ser un lenguaje de sistema pragmático que se puede usar en conjunto o en lugar de C y C ++. Esto me hizo abordar cuidadosamente el uso de la memoria, la administración de la memoria y el manejo de errores. En un entorno con recursos limitados, esta es una característica útil, no un error. Zig es determinista, no tiene ambigüedades e intenta facilitar la escritura de código confiable en un entorno en el que tradicionalmente es difícil de hacer.Describí solo una pequeña parte de la sintaxis y las características de Zig, ¡hay muchos cambios interesantes que han llegado al lenguaje en la versión 0.2.0 y superior! ¡Todo el código que escribí está compilado en modo de depuración, lo cual es óptimo para las comprobaciones de seguridad y para reducir el tiempo de compilación para acelerar las iteraciones! Hay modos de liberación rápida y segura de liberación, y habrá más en el futuro . Puede leer más sobre sus diferencias y explicaciones sobre estos modos aquí .Constantemente me sorprende la velocidad y dirección del desarrollo de Zig. Todavía hay muchas cosas en movimiento, y lo será hasta el lanzamiento de la versión 1.0.0, y si decides probar Zig, solo recuerda que hay muchas buenas ideas, ¡y espero implementarlas!Pruébalo y únete a #zig en freenode en cualquier momento si tienes preguntas.