Mini-flak Quine più veloce


26

Mini-Flak è un sottoinsieme del Brain-Flak lingua, dove i <>, <...>e []non sono consentite le operazioni. A rigor di termini, non deve corrispondere alla seguente regex:

.*(<|>|\[])

Mini-Flak è il sottoinsieme completo di Turing-Flak più piccolo conosciuto.


Qualche tempo fa sono stato in grado di realizzare un Quine in Mini-Flak , ma era troppo lento per correre nella vita dell'universo.

Quindi la mia sfida per te è fare un Quine più veloce.


punteggio

Per assegnare un punteggio al tuo codice, inserisci un @cyflag alla fine del codice ed eseguilo nell'interprete Ruby ( Provalo online usa l'interprete ruby) usando il -dflag. Il tuo punteggio dovrebbe essere stampato su STDERR come segue:

@cy <score>

Questo è il numero di cicli necessari al programma prima di terminare ed è lo stesso tra le esecuzioni. Poiché ogni ciclo richiede circa lo stesso tempo per l'esecuzione, il punteggio deve essere direttamente correlato al tempo impiegato per eseguire il programma.

Se il tuo Quine è troppo lungo per essere ragionevolmente eseguito sul tuo computer, puoi calcolare il numero di cicli a mano.

Il calcolo del numero di cicli non è molto difficile. Il numero di cicli equivale a 2 volte il numero di monadi eseguiti più il numero di niladi eseguiti. Ciò equivale a sostituire ogni nilad con un singolo carattere e contare il numero di caratteri eseguiti in totale.

Esempio di punteggio

  • (()()()) segna 5 perché ha 1 monade e 3 nilad.

  • (()()()){({}[()])} segna 29 perché la prima parte è la stessa di prima e segna 5 mentre il ciclo contiene 6 monadi e 2 punteggi nilad 8. Il ciclo viene eseguito 3 volte, quindi contiamo il suo punteggio 3 volte. 1*5 + 3*8 = 29


Requisiti

Il tuo programma deve ...

  • Essere almeno 2 byte

  • Stampa il suo codice sorgente quando eseguito in Brain-Flak usando la -Abandiera

  • Non corrisponde alla regex .*(<|>|\[])


Suggerimenti

  • L' interprete Crane-Flak è categoricamente più veloce dell'interprete ruby ​​ma manca di alcune funzionalità. Consiglierei di provare prima il tuo codice usando Crane-Flak e poi di segnarlo nell'interprete ruby ​​quando sai che funziona. Consiglio vivamente di non eseguire il programma in TIO. TIO non è solo più lento dell'interprete desktop, ma si interrompe anche in circa un minuto. Sarebbe estremamente impressionante se qualcuno riuscisse a ottenere un punteggio abbastanza basso da eseguire il programma prima che TIO scadesse.

  • [(...)]{}e (...)[{}]funzionano allo stesso modo <...>ma non infrangono il requisito di fonte limitata

  • Puoi dare un'occhiata a Brain-Flak e Mini-Flak Quines se vuoi avere un'idea di come affrontare questa sfida.


1
"current best" -> "current only"
HyperNeutrino,

Risposte:


33

Mini-Flak, 6851113 cicli

Il programma (letteralmente)

So che molte persone probabilmente non si aspettano che un quine Mini-Flak utilizzi caratteri non stampabili e persino caratteri multi-byte (rendendo rilevante la codifica). Tuttavia, questo quine fa e gli non stampabili, combinati con la dimensione del quine (93919 caratteri codificati come 102646 byte di UTF-8), rendono abbastanza difficile inserire il programma in questo post.

Tuttavia, il programma è molto ripetitivo e, come tale, si comprime molto bene. In modo che l'intero programma sia disponibile letteralmente da Stack Exchange, c'è un xxdhexdump reversibile di una gzipversione compressa del quine completo nascosto dietro il pieghevole di seguito:

(Sì, è così ripetitivo che puoi persino vedere le ripetizioni dopo che è stato compresso).

La domanda dice "Consiglio vivamente anche di non eseguire il programma in TIO. Non solo TIO è più lento dell'interprete desktop, ma scadrà anche in circa un minuto. Sarebbe estremamente impressionante se qualcuno riuscisse a ottenere un punteggio abbastanza basso per essere eseguito il loro programma prima che TIO scadesse. " Posso farlo! Ci vogliono circa 20 secondi per funzionare su TIO, usando l'interprete Ruby: provalo online!

Il programma (leggibile)

Ora ho dato una versione del programma che i computer possono leggere, proviamo una versione che gli umani possono leggere. Ho convertito i byte che compongono il quine nella tabella codici 437 (se hanno il bit alto impostato) o nelle immagini di controllo Unicode (se sono codici di controllo ASCII), ho aggiunto spazi bianchi (qualsiasi spazio bianco preesistente è stato convertito in immagini di controllo ), codificato per la lunghezza di esecuzione utilizzando la sintassi «string×length»e alcuni bit pesanti di dati sono stati eliminati:

␠
(((()()()()){}))
{{}
    (({})[(()()()())])
    (({})(
        {{}{}((()[()]))}{}
        (((((((({})){}){}{})){}{}){}){}())
        {
            ({}(
                (␀␀!S␠su! … many more comment characters … oq␝qoqoq)
                («()×35» («()×44» («()×44» («()×44» («()×44» («()×45»
                … much more data encoded the same way …
                («()×117»(«()×115»(«()×117»
                «000010101011┬â┬ … many more comment characters … ┬â0┬â┬à00␈␈
                )[({})(
                    ([({})]({}{}))
                    {
                        ((()[()]))
                    }{}
                    {
                        {
                            ({}(((({}())[()])))[{}()])
                        }{}
                        (({}))
                        ((()[()]))
                    }{}
                )]{}
                %Wwy$%Y%ywywy$wy$%%%WwyY%$$wy%$$%$%$%$%%wy%ywywy'×almost 241»
                ,444454545455┬ç┬ … many more comment characters … -a--┬ü␡┬ü-a␡┬ü
            )[{}()])
        }{}
        {}({}())
    )[{}])
    (({})(()()()()){})
}{}{}␊

(Il "quasi 241" è perché alla 241a copia manca il finale ', ma è identico agli altri 240.)

Spiegazione

A proposito dei commenti

La prima cosa da spiegare è: che succede con i personaggi non stampabili e altre cianfrusaglie che non sono comandi Mini-Flak? Potresti pensare che aggiungere commenti al quine renda le cose più difficili, ma questa è una competizione di velocità (non una competizione di dimensioni), il che significa che i commenti non danneggiano la velocità del programma. Nel frattempo, Brain-Flak, e quindi Mini-Flak, scarica il contenuto dello stack sull'output standard; se fosse necessario assicurarsi che lo stack contenesse soloi personaggi che compongono i comandi del tuo programma, dovresti spendere dei cicli per pulire lo stack. Così com'è, Brain-Flak ignora la maggior parte dei personaggi, quindi fintanto che ci assicuriamo che gli elementi della pila spazzatura non siano comandi Brain-Flak validi (rendendolo un poliglotta Brain-Flak / Mini-Flak) e non siano negativi o esterni nell'intervallo Unicode, possiamo semplicemente lasciarli in pila, consentire loro di essere emessi e mettere lo stesso carattere nel nostro programma nello stesso posto per conservare la proprietà quine.

C'è un modo particolarmente importante in cui possiamo trarne vantaggio. Il quine funziona usando una lunga stringa di dati e praticamente tutto l'output del quine viene prodotto formattando la stringa di dati in vari modi. C'è solo una stringa di dati, nonostante il programma abbia più pezzi; quindi dobbiamo essere in grado di utilizzare la stessa stringa di dati per stampare parti diverse del programma. Il trucco "dati inutili non importa" ci permette di farlo in un modo molto semplice; memorizziamo i caratteri che compongono il programma nella stringa di dati aggiungendo o sottraendo un valore al o dal loro codice ASCII. In particolare, i caratteri che compongono l'inizio del programma vengono memorizzati come loro codice ASCII + 4, i caratteri che compongono la sezione che viene ripetuta quasi 241 volte come il loro codice ASCII - 4,ogni carattere della stringa di dati con un offset; se, ad esempio, lo stampiamo con 4 aggiunti ad ogni codice carattere, otteniamo una ripetizione della sezione ripetuta, con alcuni commenti prima e dopo. (Questi commenti sono semplicemente le altre sezioni del programma, con i codici carattere spostati in modo che non formino alcun comando Brain-Flak valido, perché è stato aggiunto un offset errato. Dobbiamo schivare i comandi Brain-Flak, non solo Mini- Flak comanda, per evitare di violare la parte a della domanda; la scelta degli offset è stata progettata per garantire ciò.)

A causa di questo trucco per i commenti, in realtà dobbiamo solo essere in grado di produrre la stringa di dati formattata in due modi diversi: a) codificata nello stesso modo della sorgente, b) come codici di carattere con un offset specificato aggiunto ad ogni codice. Questa è un'enorme semplificazione che rende totalmente utile la lunghezza aggiunta.

Struttura del programma

Questo programma è composto da quattro parti: l'introduzione, la stringa di dati, il formattatore della stringa di dati e l'outro. L'introduzione e l'outro sono fondamentalmente responsabili dell'esecuzione della stringa di dati e del suo formattatore in un ciclo, specificando ogni volta il formato appropriato (ovvero se codificare o compensare e quale offset utilizzare). La stringa di dati è solo un dato, ed è l'unica parte della quina per la quale i caratteri che lo compongono non sono specificati letteralmente nella stringa di dati (farlo sarebbe ovviamente impossibile, in quanto dovrebbe essere più lungo di se stesso); è quindi scritto in un modo particolarmente facile da rigenerare da se stesso. Il formattatore della stringa di dati è composto da 241 parti quasi identiche, ognuna delle quali formatta un dato specifico dal 241 nella stringa di dati.

Ogni parte del programma può essere prodotta tramite la stringa di dati e il suo formattatore come segue:

  • Per produrre l'outro, formattare la stringa di dati con offset +8
  • Per produrre il formattatore della stringa di dati, formattare la stringa di dati con offset +4, 241 volte
  • Per produrre la stringa di dati, formatta la stringa di dati codificandola nel formato di origine
  • Per produrre l'introduzione, formattare la stringa di dati con offset -4

Quindi tutto ciò che dobbiamo fare è esaminare come funzionano queste parti del programma.

La stringa di dati

(«()×35» («()×44» («()×44» («()×44» («()×44» («()×45» …

Abbiamo bisogno di una semplice codifica per la stringa di dati in quanto dobbiamo essere in grado di invertire la codifica nel codice Mini-Flak. Non puoi essere molto più semplice di così!

L'idea chiave alla base di questo quine (a parte il trucco dei commenti) è notare che in pratica esiste solo un posto in cui è possibile memorizzare grandi quantità di dati: le "somme dei valori di ritorno del comando" all'interno dei vari livelli di annidamento della sorgente del programma. (Questo è comunemente noto come terzo stack, anche se Mini-Flak non ha un secondo stack, quindi "stack di lavoro" è probabilmente un nome migliore nel contesto di Mini-Flak.) Le altre possibilità per l'archiviazione dei dati sarebbero lo stack principale / primo (che non funziona perché è lì che deve arrivare il nostro output e non possiamo spostare l'output oltre l'archiviazione in un modo remotamente efficiente) e codificato in un bignum in un singolo elemento dello stack (che non è adatto a questo problema perché ci vuole tempo esponenziale per estrarre dati da esso); quando li elimini, lo stack di lavoro è l'unica posizione rimanente.

Per "archiviare" i dati su questo stack, utilizziamo comandi non bilanciati (in questo caso, la prima metà di un (…)comando), che verranno successivamente bilanciati nel formattatore della stringa di dati. Ogni volta che chiudiamo uno di questi comandi nel formatter, esso spingerà la somma di un dato preso dalla stringa di dati e i valori di ritorno di tutti i comandi a quel livello di annidamento nel formatter; possiamo assicurarci che quest'ultimo si aggiunga a zero, quindi il formatter vede semplicemente i singoli valori presi dalla stringa di dati.

Il formato è molto semplice:, (seguito da n copie di (), dove n è il numero che vogliamo memorizzare. (Si noti che ciò significa che è possibile memorizzare solo numeri non negativi e l'ultimo elemento della stringa di dati deve essere positivo.)

Un punto leggermente non intuitivo sulla stringa di dati è l'ordine in cui si trova. Il "start" della stringa di dati è la fine più vicina all'inizio del programma, ovvero il livello di annidamento più esterno; questa parte viene formattata per ultima (poiché il formatter va dai livelli di annidamento più interni a quelli più esterni). Tuttavia, nonostante sia stato formattato per ultimo, viene stampato per primo, poiché i valori inseriti per primi nello stack vengono stampati per ultimi dall'interprete Mini-Flak. Lo stesso principio si applica al programma nel suo insieme; dobbiamo prima formattare l'outro, quindi il formattatore della stringa di dati, quindi la stringa di dati, quindi l'intro, ovvero il contrario dell'ordine in cui sono memorizzati nel programma.

Il formattatore della stringa di dati

)[({})(
    ([({})]({}{}))
    {
        ((()[()]))
    }{}
    {
        {
            ({}(((({}())[()])))[{}()])
        }{}
        (({}))
        ((()[()]))
    }{}
)]{}

Il formattatore della stringa di dati è composto da 241 sezioni ognuna con un codice identico (una sezione ha un commento leggermente diverso), ognuna delle quali formatta un carattere specifico della stringa di dati. (Non potremmo usare un ciclo qui: abbiamo bisogno di uno sbilanciato )per leggere la stringa di dati tramite l'abbinamento del suo sbilanciato (, e non possiamo mettere uno di quelli all'interno di un {…}ciclo, l'unica forma di ciclo che esiste. Quindi invece, " srotolare "il formattatore e ottenere semplicemente l'intro / outro per generare la stringa di dati con l'offset del formatter 241 volte.)

)[({})( … )]{}

La parte più esterna di un elemento formatter legge un elemento della stringa di dati; la semplicità della codifica della stringa di dati porta a una piccola complessità nella lettura. Iniziamo chiudendo l'ineguagliato (…)nella stringa di dati, quindi negate ( […]) due valori: il dato che abbiamo appena letto dalla stringa di dati ( ({})) e il valore restituito del resto del programma. Copiamo il valore restituito del resto dell'elemento formatter con (…)e aggiungiamo la copia alla versione negata con {}. Il risultato finale è che il valore di ritorno dell'elemento stringa di dati e dell'elemento formatter insieme è l'origine meno l'origine meno il valore di ritorno più il valore di ritorno, o 0; ciò è necessario per fare in modo che l'elemento stringa di dati successivo produca il valore corretto.

([({})]({}{}))

Il formattatore utilizza l'elemento stack superiore per sapere in quale modalità è (0 = formato nella formattazione della stringa di dati, qualsiasi altro valore = offset con cui generare). Tuttavia, dopo aver letto la stringa di dati, l'origine è in cima al formato nello stack e li vogliamo viceversa. Questo codice è una variante più breve del codice di scambio Brain-Flak, prendendo da sopra b a b sopra a  +  b ; non solo è più breve, ma è anche (in questo caso specifico) più utile, perché l'effetto collaterale dell'aggiunta di b a a non è problematico quando b è 0 e quando b non è 0, esegue il calcolo dell'offset per noi.

{
    ((()[()]))
}{}
{
    …
    ((()[()]))
}{}

Brain-Flak ha solo una struttura di flusso di controllo, quindi se vogliamo qualcosa di diverso da un whileloop, ci vorrà un po 'di lavoro. Questa è una struttura "negata"; se c'è uno 0 in cima allo stack, lo rimuove, altrimenti posiziona uno 0 in cima allo stack. (Funziona abbastanza semplicemente: fintanto che non c'è uno 0 in cima allo stack, spingi 1 - 1 nello stack due volte; quando hai finito, fai apparire l'elemento dello stack in alto.)

È possibile inserire il codice all'interno di una struttura negata, come mostrato qui. Il codice verrà eseguito solo se la parte superiore dello stack era diversa da zero; quindi se abbiamo due strutture negate, supponendo che i primi due elementi dello stack non siano entrambi zero, si annulleranno a vicenda, ma qualsiasi codice all'interno della prima struttura verrà eseguito solo se l'elemento dello stack superiore era diverso da zero e il codice all'interno la seconda struttura verrà eseguita solo se l'elemento dello stack superiore era zero. In altre parole, questo è l'equivalente di un'istruzione if-then-else.

Nella clausola "then", che viene eseguita se il formato è diverso da zero, in realtà non abbiamo nulla da fare; ciò che vogliamo è spingere i dati + offset nello stack principale (in modo che possa essere emesso alla fine del programma), ma è già lì. Quindi dobbiamo solo affrontare il caso della codifica dell'elemento stringa di dati in formato sorgente.

{
    ({}(((({}())[()])))[{}()])
}{}
(({}))

Ecco come lo facciamo. La {({}( … )[{}()])}{}struttura dovrebbe essere familiare come un ciclo con un numero specifico di iterazioni (che funziona spostando il contatore del ciclo sullo stack di lavoro e tenendolo lì; sarà al sicuro da qualsiasi altro codice, perché l'accesso allo stack di lavoro è legato a il livello di annidamento del programma). Il corpo del loop è ((({}())[()])), che crea tre copie dell'elemento stack superiore e aggiunge 1 al valore più basso. In altre parole, trasforma un 40 in cima allo stack in 40 sopra 40 sopra 41, o visto come ASCII, (in ((); esegue ripetutamente farà (in (()dentro (()()in (()()()e così via, e quindi è un modo semplice per generare nostra stringa dati (supponendo che c'è una (in cima alla pila già).

Una volta terminato il ciclo, (({}))duplica la parte superiore dello stack (in modo che ora inizi ((()…piuttosto che (()…. Il lead (verrà utilizzato dalla copia successiva del formattatore della stringa di dati per formattare il carattere successivo (lo espanderà in (()(()…quindi (()()(()…e così via, quindi questo genera la separazione (nella stringa di dati).

%Wwy$%Y%ywywy$wy$%%%WwyY%$$wy%$$%$%$%$%%wy%ywywy'

C'è un ultimo bit di interesse nel formattatore di stringhe di dati. OK, quindi per lo più questo è solo l'outro spostato verso il basso di 4 punti di codice; tuttavia, quell'apostrofo alla fine potrebbe sembrare fuori posto. '(codepoint 39) si sposterebbe in +(codepoint 43), che non è un comando Brain-Flak, quindi potresti aver indovinato che è lì per qualche altro scopo.

Il motivo è che il formattatore di stringhe di dati si aspetta che ci sia già uno (stack (non contiene un 40 letterale da nessuna parte). Il'è in realtà all'inizio del blocco che si ripete per creare il formattatore della stringa di dati, non la fine, quindi dopo che i caratteri del formattatore della stringa di dati sono stati inseriti nello stack (e il codice sta per passare alla stampa della stringa di dati stesso), l'outro regola i 39 in cima alla pila in un 40, pronto per il formatter (il formatter in esecuzione stesso questa volta, non la sua rappresentazione nella fonte) per usarlo. Ecco perché abbiamo "quasi 241" copie del formatter; nella prima copia manca il suo primo carattere. E quel personaggio, l'apostrofo, è uno dei soli tre caratteri nella stringa di dati che non corrispondono al codice Mini-Flak da qualche parte nel programma; è puramente un metodo per fornire una costante.

L'intro e l'outro

(((()()()()){}))
{{}
    (({})[(()()()())])
    (({})(
        {{}{}((()[()]))}{}
        (((((((({})){}){}{})){}{}){}){}())
        {
            ({}(
                (␀␀!S␠su! … many more comment characters … oq␝qoqoq)
                …
            )[{}()])
        }{}
        {}({}())
    )[{}])
    (({})(()()()()){})
}{}{}␊

L'introduzione e l'outro sono concettualmente la stessa parte del programma; l'unica ragione per cui facciamo una distinzione è che l'outro deve essere emesso prima della stringa di dati e del suo formattatore (in modo che stampi dopo di loro), mentre l'intro deve essere prodotto dopo di loro (stampando prima di loro).

(((()()()()){}))

Iniziamo posizionando due copie di 8 in pila. Questo è l'offset per la prima iterazione. La seconda copia è perché il ciclo principale prevede che ci sia un elemento spazzatura in cima allo stack sopra l'offset, lasciato indietro dal test che decide se esiste il ciclo principale, e quindi dobbiamo inserire un elemento spazzatura in modo che non getta via l'elemento che realmente vogliamo; una copia è il modo più rapido (quindi il più veloce da produrre) per farlo.

Esistono altre rappresentazioni del numero 8 che non sono più lunghe di questa. Tuttavia, quando si va per il codice più veloce, questa è sicuramente l'opzione migliore. Per prima cosa, l'uso ()()()()è più veloce di, diciamo, (()()){}perché nonostante entrambi siano lunghi 8 caratteri, il primo è un ciclo più veloce, perché (…)viene conteggiato come 2 cicli, ma ()solo come uno. Il salvataggio di un ciclo è trascurabile rispetto a una considerazione molto più grande per un , tuttavia: (e )ha punti di codice molto più bassi di {e }, quindi generare il frammento di dati per loro sarà molto più veloce (e il frammento di dati occuperà meno spazio nel codice, pure).

{{} … }{}{}

Il ciclo principale. Questo non conta le iterazioni (è un whileciclo, non un forciclo e utilizza un test per scoppiare). Una volta uscito, scartiamo i primi due elementi dello stack; l'elemento in alto è uno 0 innocuo, ma l'elemento in basso sarà il "formato da utilizzare nella prossima iterazione", che (essendo un offset negativo) è un numero negativo e se ci sono numeri negativi nello stack quando il Mini -Flak esce dal programma, l'interprete si arresta in modo anomalo nel tentativo di emetterli.

Poiché questo ciclo utilizza un test esplicito per essere superato, il risultato di quel test verrà lasciato nello stack, quindi lo scartiamo come prima cosa che facciamo (il suo valore non è utile).

(({})[(()()()())])

Questo codice spinge 4 e f  - 4 sopra un elemento stack f , lasciando tale elemento in posizione. Stiamo calcolando il formato per la prossima iterazione in anticipo (mentre abbiamo le 4 costanti a portata di mano) e contemporaneamente mettiamo lo stack nell'ordine corretto per le prossime parti del programma: useremo f come formato per questa iterazione e prima è necessario il 4.

(({})( … )[{}])

Questo salva una copia di f  - 4 nello stack di lavoro, in modo che possiamo usarlo per la prossima iterazione. (Il valore di f sarà ancora presente a quel punto, ma sarà in un posto scomodo sullo stack, e anche se potessimo spostarlo nel posto corretto, dovremmo passare dei cicli sottraendo 4 da esso, e stampa ciclicamente il codice per fare quella sottrazione. Molto più semplice semplicemente memorizzarlo ora.)

{{}{}((()[()]))}{}

Un test per vedere se l'offset è 4 (cioè f  - 4 è 0). In tal caso, stiamo stampando il formattatore della stringa di dati, quindi dobbiamo eseguire la stringa di dati e il suo formattatore 241 volte anziché una sola volta su questo offset. Il codice è abbastanza semplice: se f  - 4 è diverso da zero, sostituire f  - 4 e il 4 stesso con una coppia di zeri; quindi in entrambi i casi, pop l'elemento stack superiore. Ora abbiamo un numero sopra f in pila, o 4 (se vogliamo stampare questa iterazione 241 volte) o 0 (se vogliamo stamparlo solo una volta).

(
    ((((((({})){}){}{})){}{}){}){}
    ()
)

Questa è una specie interessante di costante Brain-Flak / Mini-Flak; la lunga fila qui rappresenta il numero 60. Potresti essere confuso dalla mancanza di (), che normalmente si trovano ovunque nelle costanti Brain-Flak; questo non è un numero normale, ma un numero della Chiesa, che interpreta i numeri come un'operazione di duplicazione. Ad esempio, il numero della Chiesa per 60, visto qui, crea 60 copie del suo input e le combina tutte insieme in un unico valore; in Brain-Flak, le uniche cose che possiamo combinare sono i numeri regolari, in aggiunta, quindi finiamo per aggiungere 60 copie della parte superiore della pila e moltiplicando così la parte superiore della pila per 60.

Come nota a margine, puoi usare un cercatore di numeri Underload , che genera numeri Church nella sintassi Underload, per trovare il numero appropriato anche in Mini-Flak. I numeri di sottocarico (diversi da zero) utilizzano le operazioni "elemento stack superiore duplicato" :e "combina due elementi stack superiori" *; sia quelle operazioni esistono in Brain-Flak, quindi basta tradurre :a ), *a {}, anteporre una {}, e aggiungere abbastanza (all'inizio per equilibrio (questo sta usando uno strano mix della pila principale e la pila di lavoro, ma funziona).

Questo particolare frammento di codice utilizza il numero di chiesa 60 (in effetti uno snippet "moltiplica per 60"), insieme a un incremento, per generare l'espressione 60 x  + 1. Quindi, se avessimo un 4 dal passaggio precedente, questo ci dà un valore di 241, o se avessimo uno 0, otteniamo solo un valore di 1, ovvero questo calcola correttamente il numero di iterazioni di cui abbiamo bisogno.

La scelta di 241 non è casuale; era un valore scelto per essere a) approssimativamente la lunghezza alla quale il programma sarebbe comunque finito eb) 1 più di 4 volte un numero circolare. I numeri rotondi, 60 in questo caso, tendono ad avere rappresentazioni più brevi come numeri della Chiesa perché hai più flessibilità nei fattori da copiare. Il programma contiene in seguito un'imbottitura per portare esattamente la lunghezza a 241.

{
    ({}(
        …
    )[{}()])
}{}

Questo è un ciclo for, come quello visto in precedenza, che esegue semplicemente il codice al suo interno un numero di volte uguale alla parte superiore dello stack principale (che consuma; il contatore del loop stesso è memorizzato sullo stack di lavoro, ma la visibilità di che è legato al livello di annidamento del programma e quindi è impossibile per tutto tranne che per il ciclo stesso interagire con esso). Questo in realtà esegue la stringa di dati e il suo formattatore 1 o 241 volte, e poiché ora abbiamo estratto tutti i valori che stavamo usando per il nostro calcolo del flusso di controllo dallo stack principale, abbiamo il formato da usare sopra di esso, pronto per il formatter da usare.

(␀␀!S␠su! … many more comment characters … oq␝qoqoq)

Il commento qui non è del tutto privo di interesse. Per prima cosa, ci sono un paio di comandi Brain-Flak; il )fine è naturalmente generato come effetto collaterale del modo le transizioni tra i vari segmenti del programma di lavoro, in modo che (l'all'inizio è stato aggiunto manualmente per bilanciare (e nonostante la lunghezza del commento all'interno, mettendo un commento all'interno un ()comando è ancora un ()comando, quindi tutto ciò che fa è aggiungere 1 al valore restituito della stringa di dati e del suo formattatore, qualcosa che il ciclo for ignora del tutto).

In particolare, quei caratteri NUL all'inizio del commento chiaramente non sono compensati da nulla (anche la differenza tra +8 e -4 non è sufficiente per trasformare a (in NUL). Si tratta di una semplice imbottitura per portare la stringa di dati a 239 elementi fino a 241 elementi (che si pagano facilmente da soli: occorrerebbero molto più di due byte per generare 1 vs. 239 anziché 1 vs. 241 quando si calcola il numero di iterazioni richieste ). NUL è stato usato come carattere di riempimento perché ha il punto di codice più basso possibile (rendendo il codice sorgente per la stringa di dati più breve e quindi più veloce per l'output).

{}({}())

Rilascia l'elemento stack superiore (il formato che stiamo utilizzando), aggiungi 1 al successivo (l'ultimo carattere da generare, ovvero il primo carattere da stampare, della sezione del programma che abbiamo appena formattato). Non abbiamo più bisogno del vecchio formato (il nuovo formato si nasconde nello stack di lavoro); e l'incremento è innocuo nella maggior parte dei casi, e modifica il carattere 'di formattazione della stringa di dati a un'estremità della sorgente in un ((che è richiesto nello stack per la prossima volta che eseguiamo il formattatore, per formattare la stringa di dati). Abbiamo bisogno di una trasformazione come quella nell'outro o nell'intro, perché forzare ogni elemento di formattatore di stringhe di dati con cui iniziare (lo renderebbe un po 'più complesso (poiché dovremmo chiudere il (e quindi annullarne l'effetto in seguito), esaremmo in qualche modo necessario generare un extra (da qualche parte perché abbiamo solo quasi 241 copie del formattatore, non tutti i 241 (quindi è meglio che un personaggio innocuo come 'è quello che manca).

(({})(()()()()){})

Infine, il test di uscita del loop. L'attuale top dello stack principale è il formato necessario per la prossima iterazione (che è appena tornata dallo stack di lavoro). Questo lo copia e aggiunge 8 alla copia; il valore risultante verrà scartato la prossima volta attorno al loop. Tuttavia, se abbiamo appena stampato l'introduzione, l'offset era -4, quindi l'offset per la "prossima iterazione" sarà -8; -8 + 8 è 0, quindi il ciclo uscirà anziché continuare sull'iterazione in seguito.


16

128.673.515 cicli

Provalo online

Spiegazione

La ragione per cui i quint di Miniflak sono destinati a essere lenti è la mancanza di accesso casuale di Miniflak. Per ovviare a questo, creo un blocco di codice che accetta un numero e restituisce un dato. Ogni dato rappresenta un singolo carattere come prima e il codice principale richiede semplicemente questo blocco per ognuno alla volta. Questo funziona essenzialmente come un blocco di memoria ad accesso casuale.


Questo blocco di codice ha due requisiti.

  • Deve prendere un numero e produrre solo il codice carattere per quel carattere

  • Deve essere facile riprodurre un po 'alla volta la tabella di ricerca in Brain-Flak

Per costruire questo blocco ho effettivamente riutilizzato un metodo dalla mia prova che Miniflak è Turing completo. Per ogni dato esiste un blocco di codice simile al seguente:

(({}[()])[(())]()){(([({}{})]{}))}{}{(([({}{}(%s))]{}))}{}

Questo sottrae uno dal numero in cima allo stack e se zero spinge %sil dato al di sotto di esso. Dato che ogni pezzo diminuisce di una dimensione di uno se inizi con n in pila, otterrai l'ennesimo dato.

Questo è bello e modulare, quindi può essere scritto facilmente da un programma.


Successivamente dobbiamo impostare la macchina che traduce effettivamente questa memoria nella fonte. È composto da 3 parti in quanto tali:

(([()]())())
{({}[(
  -Look up table-
 )]{})
 1. (({}[()])[(())]()){(([({}{})]{}))}{}{([({}{}(([{}]))(()()()()()))]{})}{}

 2. (({}[()])[(())]()){(([({}{})]{}))}{}{([({}{}
      (({}[(
      ({}[()(((((()()()()()){}){}){}))]{}){({}[()(({}()))]{}){({}[()(({}((((()()()){}){}){}()){}))]{}){({}[()(({}()()))]{}){({}[()(({}(((()()()()())){}{}){}))]{}){([(({}{}()))]{})}}}}}{}
      (({}({}))[({}[{}])])
     )]{}({})[()]))
      ({[()]([({}({}[({})]))]{})}{}()()()()()[(({}({})))]{})
    )]{})}{}

 3. (({}[()])[(())]()){(([({}{})]{}))}{}{([({}{}
     (({}(({}({}))[({}[{}])][(
     ({}[()(
      ([()](((()()[(((((((()()()){})())){}{}){}){})]((((()()()()())){}{}){})([{}]([()()](({})(([{}](()()([()()](((((({}){}){}())){}){}{}))))))))))))
     )]{})
     {({}[()(((({})())[()]))]{})}{}
     (([(((((()()()()){}){}()))){}{}([({})]((({})){}{}))]()()([()()]({}(({})([()]([({}())](({})([({}[()])]()(({})(([()](([({}()())]()({}([()](([((((((()()()())()){}){}){}()){})]({}()(([(((((({})){}){}())){}{})]({}([((((({}())){}){}){}()){}()](([()()])(()()({}(((((({}())())){}{}){}){}([((((({}))){}()){}){}]([((({}[()])){}{}){}]([()()](((((({}())){}{}){}){})(([{}](()()([()()](()()(((((()()()()()){}){}){}()){}()(([((((((()()()())){}){}())){}{})]({}([((((({})()){}){}){}()){}()](([()()])(()()({}(((((({}){}){}())){}){}{}(({})))))))))))))))))))))))))))))))))))))))))))))))
     )]{})[()]))({()()()([({})]{})}{}())
    )]{})}{}

   ({}[()])
}{}{}{}
(([(((((()()()()){}){}())){}{})]((({}))([()]([({}())]({}()([()]((()([()]((()([({})((((()()()()){}){}()){})]()())([({})]({}([()()]({}({}((((()()()()()){}){}){}))))))))))))))))))

La macchina è composta da quattro parti che vengono eseguite nell'ordine che inizia con 1 e termina con 3. Le ho etichettate nel codice sopra. Ogni sezione utilizza anche lo stesso formato di tabella di ricerca che utilizzo per la codifica. Questo perché l'intero programma è contenuto in un ciclo e non vogliamo eseguire ogni sezione ogni volta che eseguiamo il ciclo, quindi inseriamo la stessa struttura RA e interroghiamo la sezione che desideriamo ogni volta.

1

La sezione 1 è una semplice sezione di installazione.

Il programma indica le prime query sezione 1 e dato 0. Il dato 0 non esiste, quindi invece di restituire quel valore semplicemente diminuisce la query una volta per ogni dato. Questo è utile perché possiamo usare il risultato per determinare il numero di dati, che diventeranno importanti nelle sezioni future. La Sezione 1 registra il numero di dati negativizzando il risultato e interroga la Sezione 2 e l'ultimo dato. L'unico problema è che non possiamo interrogare direttamente la sezione 2. Poiché è rimasto un altro decremento, è necessario eseguire una query su una sezione inesistente 5. In effetti, ciò avverrà ogni volta che si esegue una query su una sezione all'interno di un'altra sezione. Lo ignorerò nella mia spiegazione, tuttavia, se stai cercando un codice, ricorda solo 5 significa tornare indietro di una sezione e 4 significa eseguire nuovamente la stessa sezione.

2

La sezione 2 decodifica i dati in caratteri che compongono il codice dopo il blocco dati. Ogni volta che si aspetta che lo stack appaia così:

Previous query
Result of query
Number of data
Junk we shouldn't touch...

Associa ogni possibile risultato (un numero da 1 a 6) a uno dei sei caratteri Miniflak validi ( (){}[]) e lo posiziona sotto il numero di dati con "Junk che non dovremmo toccare". Questo ci rende uno stack come:

Previous query
Number of data
Junk we shouldn't touch...

Da qui è necessario eseguire una query sul dato successivo o, se sono stati eseguiti query, passare alla sezione 3. La query precedente non è in realtà la query esatta inviata, ma piuttosto la query meno il numero di dati nel blocco. Questo perché ogni dato decrementa la query di uno in modo che la query risulti piuttosto distorta. Per generare la query successiva, aggiungere una copia del numero di dati e sottrarre uno. Ora il nostro stack è simile a:

Next query
Number of data
Junk we shouldn't touch...

Se la nostra query successiva è zero, abbiamo letto tutta la memoria necessaria nella sezione 3, quindi aggiungiamo nuovamente il numero di dati alla query e schiaffeggiamo un 4 in cima allo stack per passare alla sezione 3. Se la query successiva non è zero, abbiamo metti un 5 nello stack per eseguire nuovamente la sezione 2.

3

La sezione 3 rende il blocco dei dati interrogando la nostra RAM proprio come la sezione 3.

Per brevità ometterò la maggior parte dei dettagli di come funziona la sezione 3. È quasi identico alla sezione 2, tranne che invece di tradurre ogni dato in un carattere, li traduce in un lungo pezzo di codice che rappresenta la sua voce nella RAM. Al termine della sezione 3, dice al programma di uscire dal loop.


Dopo che il ciclo è stato eseguito, il programma deve solo premere il primo bit del quine ([()]())(()()()()){({}[(. Lo faccio con il seguente codice che implementa le tecniche standard di complessità di Kolmogorov.

(([(((((()()()()){}){}())){}{})]((({}))([()]([({}())]({}()([()]((()([()]((()([({})((((()()()()){}){}()){})]()())([({})]({}([()()]({}({}((((()()()()()){}){}){}))))))))))))))))))

Spero fosse chiaro. Si prega di commentare se si è confusi su qualcosa.


Quanto tempo ci vuole per farlo? Tempi su TIO.
Pavel,

@Pavel Non lo eseguo su TIO perché sarebbe incredibilmente lento, utilizzo lo stesso interprete che utilizza TIO (quello rubino ). Sono necessari circa 20 minuti per essere eseguiti su un vecchio server rack a cui ho accesso. Ci vogliono circa 15 minuti in Crain-Flak, ma Crain-Flak non ha flag di debug, quindi non posso segnarlo senza eseguirlo nell'interprete Ruby.
Wheat Wizard

@Pavel L'ho eseguito di nuovo e programmato. Ci è voluto 30m45.284sper completare su un server piuttosto di fascia bassa (all'incirca l'equivalente di un desktop moderno medio) usando l'interprete ruby.
Wheat Wizard
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.