Come implementare una 'funzione' con 'return' senza usare la parola chiave 'function'

0

Sulla falsariga di Come simulare Controllo-Flusso senza utilizzare i Primitivi del flusso di controllo , mi chiedo come simulare il ritorno da una funzione.

Dato un setup di esempio come questo:

console.log(a(10))

function a(a) {
  var x = b(a)
  var y = c(a)
  return x + y
}

function b(a) {
  return a * 2
}

function c(a) {
  return a + 1
}

Come reimplementare queste funzioni senza utilizzare function o return . Qualcosa sembra usare una pila in qualche modo come sotto. Sto cercando di imparare a pensare all'assemblaggio. Non sono sicuro di come funzioni il return della funzione, gli aspetti nidificati delle funzioni o le funzioni, quando viene implementato con uno stack iterativo in un ciclo while come sotto.

var stack = []
var pointer = 0
var a = 10

while (true && pointer < 3) {
  switch (pointer) {
    case 0:
      // var x = b(a)
      // var y = c(a)
      // stack.push(x + y)
      pointer++
      break
    case 1:
      stack.push(a * 2)
      pointer++
      break
    case 2:
      stack.push(a + 1)
      pointer++
      break
  }
}

console.log(stack.pop())
    
posta Lance Pollard 19.07.2018 - 19:32
fonte

2 risposte

4

Se si sta scrivendo un interprete per un'architettura di istruzioni istruzione codice macchina, si forniscono e si mantengono (1) i registri della CPU, che includono il Contatore programmi (alias Puntatore istruzioni) e (2) parte della memoria principale.

Quindi il loop dell'interprete recupera l'istruzione a cui fa riferimento il contatore del programma. Praticamente tutte le istruzioni modificheranno alcuni registri della CPU, in particolare: il contatore del programma.

Le istruzioni aritmetiche modificheranno la memoria, i registri e / o il registro dei flag; mentre si aumenta anche esplicitamente il Contatore del Programma per fare riferimento all'istruzione sequenzialmente successiva - quindi, l'iterazione successiva del ciclo dell'interprete eseguirà naturalmente in sequenza le istruzioni del programma interpretato.

Le istruzioni di ramo incondizionate modificheranno il contatore del programma in modo più diretto. Le istruzioni di derivazione condizionale testano alcune condizioni e quindi incrementano il PC come con le istruzioni aritmetiche (in falsa condizione: fall thru) o modificano il PC direttamente come con rami incondizionati (su true).

Il ciclo dell'interprete principale procede semplicemente all'esecuzione dell'istruzione successiva come indicato dal contatore del programma. Pertanto, se il programma (interpretato) ha un ciclo, il ciclo dell'interprete rivisiterà le istruzioni.

Chiama e amp; il ritorno viene simulato in un interprete semplicemente eseguendo gli effetti secondari della chiamata di codice macchina & istruzioni per la restituzione. Ad esempio, un'istruzione di chiamata potrebbe (1) spingere l'indirizzo di ritorno nello stack (una scrittura in memoria al puntatore dello stack insieme a una modifica del puntatore dello stack), (2) il controllo di trasferimento come con un ramo incondizionato (ad es. Contatore di programma). L'istruzione di ritorno fa il contrario: fai uscire l'indirizzo di ritorno dallo stack (leggi la memoria in cima allo stack mentre aggiusti anche il puntatore dello stack), seguito da inserire quel valore nel contatore del programma.

Nonostante le chiamate e / o i ritorni nel programma interpretato, il loop dell'interprete continua Ad-nauseam, essendo relativamente inconsapevole di queste variazioni del flusso di controllo, eseguendo semplicemente fedelmente l'istruzione successiva come indicato dal suo contatore di programma. Per spingere e amp; spuntando il chiamante & il callee esegue con una diversa area della pila e quindi si coordinano in un certo senso, non per cestinare l'altro, mentre richiedono poco dell'hardware (o dell'interprete).

Come per l'hardware reale, un interprete deve solo mantenere una singola copia dei registri della CPU. Il chiamante e il chiamato sono scritti (da un compilatore, forse) per seguire la convenzione di chiamata dell'architettura della macchina. Questa convenzione consente al chiamante e al destinatario di condividere i registri della CPU e di tenersi lontani gli uni dagli altri.

ok, postuliamo un codice macchina molto semplice in cui tutte le istruzioni sono 16-bit e il primo byte l'opcode, il secondo è un operando:

// CPU State
int pc;
int regs[8];

// Main memory
unsigned char *mem;

// Interpreter inf. loop
while (true) {
    int opcode = mem[pc++];
    int operand = mem[pc++];
    // note: at this point, pc points to sequentially next instruction

    // decode the various opcodes
    switch (opcode) {
    case 0:
    ...
    case 100: // let's say this is an unconditional branch instruction
        pc = operand;
        break;
    case 101: // let's say this is a conditional branch (here testing reg#0)
        int condition = regs[0] == 0;
        if (condition)
           pc = operand;
        // recall that the pc has already been incremented for fall thru
        break;
    ...
    case 200: // let's say this is a machine code call instruction
        regs[7]--;     // let's say reg#7 is the stack pointer
        mem[regs[7]] = pc; // note this pc is 1 instruction further along than the call itself, hence this is the return address
        pc = operand;
        break;
    case 201: // let's say this is a machine code return instruction
        pc = mem[++regs[7]];
        break;            
    case 255:
    ...
    }
}

Potresti dare un'occhiata all'assemblaggio per una definizione di funzione e anche per una chiamata a una funzione.

La funzione avrà un prologo (e un po 'di epilogo) quando imposta (strappa) lo stato macchina per la funzione da eseguire dato che sia il chiamante che il chiamante condividono la CPU con le sue risorse di registro fisse (questo è fatto in base alla convenzione di chiamata).

Il codice dell'epilogue conterrà la versione della lingua della macchina di "ritorno" come ultima istruzione. L'operazione "return" del codice macchina trasferisce il flusso di controllo da chiamata a chiamata semplicemente alterando il Contatore Programma. Non restituisce un valore in modo che return x+y; in un linguaggio di livello superiore richieda più codice: codice per calcolare x + y e quindi restituirlo al chiamante, oltre all'epilogo (per la convenzione di chiamata) e al codice macchina " return "che esegue la modifica finale nel flusso di controllo.

La chiamata consisterà anche in diverse istruzioni, la prima delle quali imposta gli argomenti per il chiamato, e l'ultima istruzione della sequenza di chiamata (gli slot di ritardo del ramo modulo su alcune architetture) è l'istruzione "chiamata" del codice macchina, che cattura l'indirizzo di ritorno per un uso successivo (ad esempio, spingendolo sullo stack di runtime) e realizza altrimenti un ramo incondizionato. Quindi, una chiamata, dato che in f(a+1,b-1) calcolerà prima un + 1 e un b-1, quindi li passerà come parametri in base alla convenzione di chiamata e infine trasferirà il flusso del controllo a f , tramite un codice macchina " chiama "istruzione.

    
risposta data 19.07.2018 - 20:44
fonte
0

Sembra, algebricamente, che tu possa sostituire.

A partire da ...

function a(a) {
 var x = b(a)
 var y = c(a)
 return x + y
}

Se b(a) = a * 2 , allora possiamo sostituire a * 2 ovunque vediamo b(a) :

function a(a) {
 var x = a * 2
 var y = c(a)
 return x + y
}

E se c(a) = a + 1 possiamo sostituire di nuovo:

function a(a) {
 var x = a * 2
 var y = a + 1
 return x + y
}

Che è uguale a

function a(a) {
 return (a * 2) + (a + 1)
}

La semplificazione:

function a(a) {
 return (a * 3) + 1
}

O semplicemente:

whatever = (a * 3) + 1
    
risposta data 19.07.2018 - 19:41
fonte

Leggi altre domande sui tag