Compilador y máquina virtual
Esta sección cubre la compilación de programas y operaciones en el lenguaje Needle en la máquina virtual.
Almacenamiento y compilación de código fuente
Los contratos inteligentes y las funciones están escritos en lenguaje Go y se almacenan en la tabla de contratos inteligentes del ecosistema.
Al ejecutar un contrato inteligente, se lee su código fuente de la base de datos y se compila en bytecode.
Cuando se realiza un cambio en el contrato inteligente, su código fuente se actualiza y se guarda en la base de datos. Luego, se compila el código fuente, lo que resulta en un cambio en el bytecode correspondiente.
El bytecode no se almacena físicamente en ningún lugar, por lo que cuando se ejecuta el programa nuevamente, se vuelve a compilar el código fuente.
Todo el código fuente descrito en la tabla de contratos inteligentes del ecosistema se compila estrictamente en orden en una máquina virtual, cuyo estado es el mismo en todos los nodos.
Cuando se llama a un contrato inteligente, la máquina virtual no cambia su estado de ninguna manera. La ejecución de cualquier contrato inteligente o llamada a función ocurre en una pila de ejecución separada creada en cada llamada externa.
Cada ecosistema puede tener un llamado "ecosistema virtual", que se puede utilizar junto con una tabla de datos fuera de la cadena en un solo nodo y no puede afectar directamente a la cadena de bloques o a otros ecosistemas virtuales. En este caso, el nodo que aloja este ecosistema virtual compilará sus contratos inteligentes y creará su propia máquina virtual.
Estructuras de la máquina virtual
Estructura VM
La máquina virtual se define en la memoria de acuerdo con la siguiente estructura.
La estructura VM tiene los siguientes elementos:
- Block - Contiene una estructura de bloque;
- ExtCost - Una función que devuelve el costo de ejecución de una función externa de Golang;
- FuncCallsDB - Una colección de nombres de funciones de Golang que devuelven el costo de procesamiento de la base de datos como primer parámetro. Estas funciones utilizan EXPLAIN para calcular el costo;
- Extern - Una identificación booleana que indica si el contrato inteligente es externo. Cuando se crea la VM, se establece en verdadero y no es necesario llamar explícitamente al contrato inteligente compilado. Esto permite llamar al código del contrato inteligente que se determinará en el futuro;
- ShiftContract - El ID del primer contrato inteligente en la VM;
- logger - La salida de registro de errores de la VM.
Estructura de bloque
La máquina virtual está compuesta por un árbol de objetos del tipo Block type.
Un bloque es una unidad independiente que contiene algunos bytes de código. En pocas palabras, todo lo que se coloca dentro de los corchetes ({}
) de un lenguaje es un bloque.
Por ejemplo, el siguiente código crea un bloque con una función. Este bloque contiene otro bloque con una declaración if, que a su vez contiene otro bloque con una declaración while.
El bloque se define en la memoria con la siguiente estructura:
La estructura de bloque tiene los siguientes elementos:
- Objects - Un mapeo de objetos internos de tipo puntero ObjInfo. Por ejemplo, si hay una variable en el bloque, se puede obtener información sobre ella a través de su nombre;
- Typeo - El tipo de bloque. Cuando el bloque es una función, el tipo es ObjFunc. Cuando el bloque es un contrato inteligente, el tipo es ObjContract;
- Owner - Una estructura de tipo puntero OwnerInfo. Esta estructura contiene información sobre el propietario del contrato inteligente compilado. Se especifica durante la compilación del contrato inteligente o se obtiene de la tabla contracts;
- Info - Contiene información sobre el objeto, que depende del tipo de bloque;
- Parent - Un puntero que apunta al bloque padre;
- Vars - Un array que contiene el tipo de variables del bloque actual;
- Code - El bytecode del bloque en sí mismo, que se ejecutará cuando el control se transfiera a este bloque, por ejemplo, en una llamada de función o en un bucle;
- Children* - Un array que contiene bloques secundarios, como funciones anidadas, bucles y operadores condicionales.
Estructura ObjInfo
La estructura ObjInfo contiene información sobre objetos internos.
La estructura ObjInfo tiene los siguientes elementos:
- Type es el tipo de objeto. Puede ser uno de los siguientes valores:
- ObjContract -- Contrato inteligente;
- ObjFunc -- función;
- ObjExtFunc -- función externa de golang;
- ObjVar -- variable;
- ObjExtend -- variable $name.
- Value -- contiene la estructura de cada tipo.
Estructura ContractInfo
Apunta al tipo ObjContract, el campo Value contiene la estructura ContractInfo.
Estructura FieldInfo
La estructura FieldInfo se usa en la estructura ContractInfo y describe los elementos del contrato inteligente sección de datos.
La estructura FieldInfo tiene los siguientes elementos:
- Name - Nombre del campo;
- Type - Tipo de campo;
- Original - Campo opcional;
- Tags -- Etiquetas adicionales para el campo.
Estructura FuncInfo
La estructura FuncInfo apunta al tipo ObjFunc, el campo Value contiene la estructura FuncInfo.
La estructura FuncInfo tiene los siguientes elementos:
- Params -- un array de tipos de parámetros;
- Results -- un array de tipos de resultados;
- Names -- un mapeo de datos de la función de cola, por ejemplo,
DBFind().Columns()
; - Variadic -- si la función puede tener un número variable de parámetros, es verdadero;
- ID -- el ID de la función.
Estructura FuncName
La estructura FuncName se utiliza en FuncInfo y describe los datos de la función de cola.
La estructura FuncName tiene los siguientes elementos:
- Params -- un array de tipos de parámetros;
- Offset -- un array de desplazamientos de estas variables. De hecho, todos los parámetros pueden inicializarse en la función utilizando el punto
.
; - Variadic -- si la función de la cola puede tener un número variable de parámetros, es verdadero.
Estructura ExtFuncInfo
ObjExtFunc es un tipo que apunta a una función externa y el campo Value contiene la estructura ExtFuncInfo. Se utiliza para describir una función de Golang.
La estructura ExtFuncInfo tiene los siguientes elementos:
- Name, Params, Results son los mismos parámetros que la estructura FuncInfo;
- Auto -- un arreglo de variables que se pasan como parámetros adicionales a la función, por ejemplo, la variable de tipo SmartContract llamada sc;
- Func -- una función de golang.
Estructura VarInfo
La estructura VarInfo apunta al tipo ObjVar, y el campo Value contiene una estructura VarInfo.
La estructura VarInfo tiene los siguientes elementos:
- Obj -- Información sobre el tipo de variable y su valor;
- Owner -- Puntero al bloque al que pertenece.
Valor ObjExtend
Apunta al tipo ObjExtend, el campo Value contiene una cadena que incluye el nombre de una variable o función.
Instrucciones de la máquina virtual
Estructura ByteCode
Un bytecode es una secuencia de estructuras de tipo ByteCode.
La estructura tiene los siguientes campos:
- Cmd - Almacena el identificador de la instrucción;
- Value - Contiene el operando (valor).
En general, la instrucción opera en el elemento superior de la pila y, si es necesario, escribe el resultado en ella.
Identificadores de comando
El archivo vm/cmds_list.go describe los identificadores de las instrucciones de la máquina virtual.
- cmdPush -- Coloca el valor del campo Value en la pila. Por ejemplo, coloca números y líneas en la pila;
- cmdVar -- Coloca el valor de la variable en la pila. Value contiene un puntero a la estructura VarInfo y la información sobre la variable;
- cmdExtend -- Coloca el valor de la variable externa en la pila. Value contiene una cadena con el nombre de la variable (comenzando con
$
); - cmdCallExtend -- Llama a una función externa (cuyo nombre comienza con
$
). Los argumentos de la función se obtienen de la pila y el resultado se coloca en la pila. Value contiene el nombre de la función (comenzando con $
); - cmdPushStr -- Coloca la cadena del campo Value en la pila;
- cmdCall -- Llama a una función de la máquina virtual. Value contiene la estructura ObjInfo. Esta instrucción se utiliza para las funciones ObjExtFunc de Golang y las funciones ObjFunc de Needle. Cuando se llama a la función, se obtienen sus argumentos de la pila y se coloca el valor del resultado en la pila;
- cmdCallVari -- Similar a la instrucción cmdCall, llama a una función de la máquina virtual. Esta instrucción se utiliza para llamar a funciones con un número variable de argumentos;
- cmdReturn -- Se utiliza para salir de una función. El valor de retorno se coloca en la pila y no se utiliza el campo Value;
- cmdIf -- Transfiere el control al bytecode en la estructura bloque que se pasa en el campo Value. El control se transfiere a la pila solo si la función valueToBool devuelve
true
para el elemento superior de la pila. De lo contrario, el control se transfiere a la siguiente instrucción; - cmdElse -- Funciona de la misma manera que la instrucción cmdIf, pero el control se transfiere a la estructura especificada solo si la función valueToBool devuelve
false
para el elemento superior de la pila; - cmdAssignVar -- Obtiene una lista de variables de tipo VarInfo del campo Value. Estas variables obtienen sus valores mediante la instrucción cmdAssign;
- cmdAssign -- Asigna los valores de la pila a las variables obtenidas mediante la instrucción cmdAssignVar;
- cmdLabel -- Define una etiqueta que se utiliza para devolver el control durante un ciclo while;
- cmdContinue -- Transfiere el control a la etiqueta cmdLabel. No se utiliza el campo Value al ejecutar una nueva iteración del ciclo;
- cmdWhile -- Utiliza la función valueToBool para comprobar el elemento superior de la pila. Si este valor es
true
, se llama a la estructura bloque del campo value; - cmdBreak -- Sale del ciclo;
- cmdIndex -- Coloca el valor del map o array en la pila mediante un índice. No se utiliza el campo Value. Por ejemplo:
(map | array) (index value) => (map | array [index value])
; - cmdSetIndex -- Asigna el valor del elemento superior de la pila a un elemento del map o array. No se utiliza el campo Value. Por ejemplo:
(map | array) (index value) (value) => (map | array)
; - cmdFuncName -- Describe los parámetros agregados mediante puntos
.
. Por ejemplo: func name => Func (...) .Name (...)
; - cmdUnwrapArr -- Define una bandera booleana si el elemento superior de la pila es un array;
- cmdMapInit -- Inicializa los valores del map;
- cmdArrayInit -- Inicializa los valores del array;
- cmdError -- Se crea cuando el contrato inteligente o la función se detienen debido a un error especificado (
error, warning, info
).
Instrucciones de operación de pila
Aquí están las instrucciones que manejan directamente la pila. Estas instrucciones no utilizan el campo Value.
- cmdNot -- Negación lógica.
(val) => (!ValueToBool(val))
; - cmdSign -- Cambio de signo.
(val) => (-val)
; - cmdAdd -- Suma.
(val1)(val2) => (val1 + val2)
; - cmdSub -- Resta.
(val1)(val2) => (val1 - val2)
; - cmdMul -- Multiplicación.
(val1)(val2) => (val1 * val2)
; - cmdDiv -- División.
(val1)(val2) => (val1 / val2)
; - cmdAnd -- Operación lógica AND.
(val1)(val2) => (valueToBool(val1) && valueToBool(val2))
; - cmdOr -- Operación lógica OR.
(val1)(val2) => (valueToBool(val1) || valueToBool(val2))
; - cmdEqual -- Comparación de igualdad, devuelve bool.
(val1)(val2) => (val1 == val2)
; - cmdNotEq -- Comparación de desigualdad, devuelve bool.
(val1)(val2) => (val1 != val2)
; - cmdLess -- Comparación de menor que, devuelve bool.
(val1)(val2) => (val1 < val2)
; - cmdNotLess -- Comparación de mayor o igual que, devuelve bool.
(val1)(val2) => (val1 >= val2)
; - cmdGreat -- Comparación de mayor que, devuelve bool.
(val1)(val2) => (val1 > val2)
; - cmdNotGreat -- Comparación de menor o igual que, devuelve bool.
(val1)(val2) => (val1 <= val2)
.
Estructura Runtime
La ejecución del bytecode no afecta a la máquina virtual. Por ejemplo, permite que varias funciones y contratos inteligentes se ejecuten simultáneamente en una sola máquina virtual.
La estructura Runtime se utiliza para ejecutar funciones y contratos inteligentes, así como cualquier expresión y bytecode.
- stack -- La pila que ejecuta el bytecode;
- blocks -- La pila de llamadas de bloques;
- vars -- La pila de variables. Cuando se llama al bytecode dentro de un bloque, sus variables se agregarán a esta pila de variables. Después de salir del bloque, el tamaño de la pila de variables volverá a su valor anterior;
- extend -- Puntero de mapeo de valores de variables externas (
$name
); - vm -- Puntero de la máquina virtual;
- cost -- Unidad de combustible para el resultado de la ejecución;
- err -- Error durante la ejecución.
Estructura blockStack
La estructura blockStack se utiliza en la estructura Runtime.
- Block -- Puntero al bloque en ejecución;
- Offset -- Desplazamiento de la última instrucción ejecutada en el bytecode del bloque especificado.
Función RunCode
El bytecode se ejecuta en la función RunCode. Contiene un bucle que ejecuta la operación correspondiente para cada instrucción del bytecode. Antes de procesar el bytecode, es necesario inicializar los datos necesarios.
Aquí se añade un nuevo bloque a otros bloques.
A continuación, se obtiene la información de los parámetros relevantes de la función de cola. Estos parámetros están contenidos en el último elemento de la pila.
Entonces, es necesario inicializar todas las variables definidas en el bloque actual con sus valores iniciales.
Debido a que las variables en una función también son variables, necesitamos extraerlas del último elemento de la pila en el orden descrito por la función en sí.
Inicializar la variable local con el valor inicial aquí.
Siguiente, actualice los valores de los parámetros de variables que se pasan en la función de cola.
Si los parámetros de variable pasados son un número variable de parámetros, entonces combinarlos en un arreglo variable.
Después, lo que tenemos que hacer es eliminar los valores que se pasaron como parámetros de función desde la parte superior de la pila, para así mover la pila. Ya hemos copiado sus valores en un arreglo de variables.
Después de que se complete la ejecución del ciclo de instrucciones de bytecode, es necesario limpiar correctamente la pila.
Eliminar el bloque actual de la pila de bloques.
Si salimos con éxito de una función que ha sido ejecutada, agregaremos el valor de retorno al final de la pila anterior.
Como puede ver, si no ejecutamos la función, no restauraremos el estado de la pila y saldremos de la función tal como está. La razón es que los bucles y estructuras condicionales que ya se han ejecutado en la función también son bloques de código de bytes.
Otras funciones de la VM
Utilice la función NewVM para crear una máquina virtual. Cada máquina virtual tiene la función Extend que agrega cuatro funciones: ExecContract, MemoryUsage, CallContract y Settings.
Nosotros iteramos a través de todos los objetos pasados y solo miramos las funciones.
De acuerdo con la información relevante recibida sobre la función, llene la estructura ExtFuncInfo y agréguela al mapa de Objects de nivel superior por nombre.
La estructura ExtFuncInfo tiene un array de parámetros llamado Auto. Normalmente, el primer parámetro es sc *SmartContract
o rt *Runtime
.
No podemos pasarlos desde el lenguaje Needle, ya que son necesarios para ejecutar algunas funciones de Golang. Por lo tanto, especificamos que estos variables se utilizarán automáticamente al llamar a la función.
En este caso, el primer parámetro de las cuatro funciones mencionadas es rt *Runtime
.
Asignar información sobre el parámetro.
y el tipo del valor devuelto.
Agregar una función a la raíz Objects, de esta manera el compilador podrá encontrarlas más tarde cuando se utilice el contrato inteligente.
Compilador
El archivo compile.go contiene funciones encargadas de compilar el array de tokens obtenido del analizador léxico. La compilación puede dividirse en dos niveles condicionalmente, en el nivel superior, se manejan funciones, contratos inteligentes, bloques de código, declaraciones condicionales y de bucle, definiciones de variables, entre otros. En el nivel inferior, compilamos bloques de código en bucles y declaraciones condicionales o expresiones dentro de condiciones.
En primer lugar, describamos el nivel inferior de manera simple. La función compileEval puede completar la conversión de una expresión en bytecode. Dado que estamos utilizando una máquina virtual de pila, es necesario convertir una expresión de registro de infijo normal a notación de sufijo o notación polaca inversa. Por ejemplo, 1+2
se convierte en 12+
, luego colocamos 1
y 2
en la pila, luego aplicamos la operación de suma a los dos últimos elementos de la pila y escribimos el resultado en la pila. Este algoritmo de conversión (opens new window) se puede encontrar en Internet.
La variable global opers = map [uint32] operPrior
contiene la prioridad de las operaciones necesarias para convertir a notación polaca inversa.
Las siguientes variables se definen al comienzo de la función compileEval:
- buffer -- un búfer temporal para las instrucciones de bytecode;
- bytecode -- un búfer final para las instrucciones de bytecode;
- parcount -- un búfer temporal para calcular los parámetros al llamar a una función;
- setIndex -- una variable que se establece en [true]{.title-ref} durante el proceso de trabajo al asignar elementos a un map o array. Por ejemplo, en el caso de
a["my"] = 10
, necesitamos usar la instrucción cmdSetIndex especificada.
Obtenemos una marca en un bucle y realizamos la acción correspondiente, por ejemplo, si encontramos una llave, detenemos el análisis de la expresión. Al mover una cadena, verificamos si la sentencia anterior es un operador y si está dentro de paréntesis, de lo contrario, salimos y analizamos la expresión.
En circunstancias normales, este algoritmo corresponde a un algoritmo de conversión a notación polaca inversa. Teniendo en cuenta algunas llamadas necesarias a contratos inteligentes, funciones e índices, así como otras cosas que no se encuentran durante el análisis del marcador de tipo lexIdent, comprobaremos si hay una variable, función o contrato inteligente con este nombre. Si no se encuentra ningún contenido relevante y esto no es una llamada a una función o contrato inteligente, se indicará un error.
Podríamos encontrarnos en una situación en la que describiremos una llamada de contrato inteligente más adelante. En este ejemplo, si no se encuentra una función o variable con el mismo nombre, consideramos que se llamará al contrato inteligente.
En este lenguaje de programación, no hay diferencia entre la llamada de contrato inteligente y la llamada de función. Pero necesitamos llamar al contrato inteligente utilizando la función ExecContract utilizada en el bytecode.
En este código, estamos registrando la cantidad de variables hasta el momento en la variable count
, la cual también se escribe en la pila junto con la cantidad de argumentos de la función. Cada vez que se verifica un argumento posteriormente, simplemente necesitamos aumentar la cantidad en una unidad en el último elemento de la pila.
Tenemos un parámetro de lista Used que indica los contratos inteligentes que han sido llamados, por lo que necesitamos marcar cuando se llama a un contrato inteligente. Si se llama al contrato inteligente sin ningún parámetro, debemos agregar dos parámetros vacíos para llamar a ExecContract y obtener al menos dos parámetros.
Si vemos que hay un corchete cuadrado al lado, entonces agregamos el comando cmdIndex para obtener el valor por el índice.
La función CompileBlock puede generar un árbol de objetos y bytecode independiente de expresiones. El proceso de compilación se basa en una máquina de estados finitos, al igual que un analizador léxico, pero con las siguientes diferencias.
En primer lugar, no usamos símbolos sino etiquetas.
En segundo lugar, describimos inmediatamente todas las variables de estado y transición en states.
Esto representa un arreglo de objetos indexados por tipo de etiqueta, cada etiqueta tiene una estructura de compileState y se especifica un nuevo estado en NewState.
Si ya hemos entendido esta estructura, podemos especificar la función de manejador en el campo Func.
Tomemos como ejemplo el estado principal.
Si encontramos un salto de línea o un comentario, mantendremos el mismo estado. Si encontramos la palabra clave contract, cambiaremos el estado a stateContract y comenzaremos a analizar esa estructura.
Si encontramos la palabra clave func, cambiaremos el estado a stateFunc. Si recibimos cualquier otra etiqueta, se llamará a una función que genere un error.
Supongamos que nos encontramos con la palabra clave func y hemos cambiado el estado a stateFunc. Debido a que el nombre de la función debe seguir la palabra clave func, al cambiar el nombre de la función mantendremos el mismo estado. Para todos los demás tokens, generaremos un error correspondiente.
Si obtenemos el nombre de la función en el token de identificador, pasamos al estado stateFParams, donde podemos obtener los parámetros de la función.
Durante la operación anterior, llamamos a la función fNameBlock. Es importante tener en cuenta que la estructura Block se crea utilizando la marca statePush, y aquí la obtenemos del búfer y llenamos los datos que necesitamos. La función fNameBlock se utiliza para contratos inteligentes y funciones (incluyendo funciones y contratos inteligentes anidados).
Utiliza la estructura correspondiente para llenar el campo Info y se escribe a sí misma en los Objects del bloque padre. De esta manera, podemos llamar a esta función o contrato inteligente mediante el nombre especificado.
Del mismo modo, creamos funciones correspondientes para todos los estados y variables. Estas funciones suelen ser muy pequeñas y realizan algunas tareas al construir el árbol de la máquina virtual.
Para la función CompileBlock, simplemente recorre todas las etiquetas y cambia el estado según lo descrito en states. Casi todas las etiquetas adicionales corresponden a código de programa adicional.
- statePush -- Agrega el objeto Block al árbol de objetos;
- statePop -- Se utiliza cuando el bloque termina con una llave de cierre;
- stateStay -- Cuando se cambia a un nuevo estado, es necesario mantener la etiqueta actual;
- stateToBlock -- Cambia al estado stateBlock, utilizado para procesar while y if. Después de procesar la expresión, es necesario procesar el bloque dentro de las llaves;
- stateToBody -- Cambia al estado stateBody;
- stateFork -- Guarda la posición de la etiqueta. Se utiliza cuando la expresión comienza con un identificador o un nombre que comienza con
$
, lo que indica una llamada a función o una asignación; - stateToFork -- Se utiliza para obtener la etiqueta almacenada en stateFork. Esta etiqueta se pasará a la función de procesamiento;
- stateLabel -- Se utiliza para insertar la instrucción cmdLabel. La estructura while necesita esta etiqueta;
- stateMustEval -- Verifica la disponibilidad de la expresión condicional al comienzo de las estructuras if y while.
Aparte de la función CompileBlock, también se debe mencionar la función FlushBlock.
Pero el problema es que el árbol de bloques es independiente de la construcción actual de la máquina virtual, es decir, obtenemos información sobre las funciones y contratos inteligentes existentes en la máquina virtual, pero recopilamos los bloques ya compilados en un árbol separado.
De lo contrario, si ocurre un error durante la compilación, debemos revertir el estado de la máquina virtual a un estado anterior. Por lo tanto, compilamos el árbol por separado, pero después de una compilación exitosa, debemos llamar a la función FlushContract.
Esta función agregará el árbol de bloques completado a la máquina virtual actual. En este punto, la fase de compilación se ha completado.
Analizador léxico
El analizador léxico procesa la cadena de entrada y forma una secuencia de tokens de los siguientes tipos:
- lexSys - Token de sistema, como:
{}
, []
, ()
, ,
, .
, etc.; - lexOper - Token de operación, como:
+
, -
, /
, \
, *
; - lexNumber - Número;
- lexident - Identificador;
- lexNewline - Salto de línea;
- lexString - Cadena de caracteres;
- lexComment - Comentario;
- lexKeyword - Palabra clave;
- lexType - Tipo;
- lexExtend - Referencia a variables o funciones externas, como:
$myname
.
En la versión actual, se construyó una tabla de conversión (máquina de estados finitos) con la ayuda del archivo script/lextable/lextable.go para analizar tokens y escribirlos en el archivo lex_table.go. En general, se puede separar de la tabla de conversión inicial generada por ese archivo y crear una tabla de conversión en memoria (init()
) inmediatamente al inicio.
El análisis léxico en sí ocurre en la función lexParser en el archivo lex.go.
lextable/lextable.go
Aquí definimos nuestro alfabeto de operación y describimos cómo una máquina de estados finitos cambia de un estado a otro según el siguiente símbolo recibido.
states contiene un objeto JSON con una lista de estados.
Excepto por símbolos específicos, d
se utiliza para representar todos los símbolos no especificados en el estado.
n
representa 0x0a, s
representa un espacio, q
representa una comilla simple, Q
representa una comilla doble, r
representa caracteres >= 128, a
representa AZ y az, 1
representa 1-9.
El nombre del estado es la clave y el objeto de valor enumera los posibles valores. Luego, para cada conjunto, hay un nuevo estado al que se debe cambiar. Luego viene el nombre de la etiqueta, si necesitamos volver al estado inicial, el tercer parámetro es una marca de servicio que indica cómo procesar el símbolo actual.
Por ejemplo, tenemos un estado principal y un carácter de entrada "/", "/": ["solidus", "", "push next"],
- push - hace que la instrucción recuerde que está en una pila separada;
- next - pasa al siguiente carácter, mientras cambiamos el estado a solidus, luego obtenemos el siguiente carácter y miramos el estado de solidus.
Si el siguiente carácter es "/" o "/", entonces pasamos al estado de comentario comment, ya que comienzan con "//" o "/".
Obviamente, cada comentario tiene un estado posterior diferente, ya que terminan con diferentes símbolos.
Si el siguiente carácter no es "/" ni "*", entonces registramos todo el contenido de la pila como una marca de tipo lexOper, limpiamos la pila y volvemos al estado principal.
El siguiente módulo convierte el árbol de estados en una matriz numérica y lo escribe en el archivo lex_table.go.
En el primer bucle:
Formamos un alfabeto de símbolos válidos.
Además, en state2int, proporcionamos un identificador de secuencia único para cada estado.
Cuando recorremos todos los estados y cada conjunto en el estado y cada símbolo en el conjunto, escribimos un número de tres bytes [identificador de nuevo estado (0=principal)] + [tipo de marca (0-sin marca)] + [marca].
La bidimensionalidad del array table se debe a que se divide en estados y 34 símbolos de entrada del array alphabet, que se ordenan en el mismo orden.
Estamos en el estado main en la fila cero de la tabla table. Tomamos el primer carácter, buscamos su índice en el array alphabet y obtenemos el valor de la columna correspondiente al índice dado.
A partir del valor recibido, recibimos la marca en el byte de menor orden. Si se completa el análisis, el segundo byte indica el tipo de marca recibida. En el tercer byte, recibimos el índice del nuevo estado siguiente.
Todo esto se describe con más detalle en la función lexParser en lex.go.
Si desea agregar nuevos caracteres, debe agregarlos al array alphabet y aumentar la constante AlphaSize.
Si desea agregar nuevas combinaciones de símbolos, debe describirlas en el estado, similar a las opciones existentes. Después de esto, ejecute el archivo lextable.go para actualizar el archivo lex_table.go.
lex.go
lexParser
La función genera directamente el análisis léxico y devuelve una matriz de tokens recibidos según la cadena de entrada proporcionada. Analicemos la estructura de los tokens.
- Type -- Tipo de marcador. Tiene uno de los siguientes valores:
lexSys, lexOper, lexNumber, lexIdent, lexString, lexComment, lexKeyword, lexType, lexExtend
; - Value -- Valor del marcador. El tipo de valor depende del tipo de marcador, analicemos más detalladamente:
- lexSys -- Incluye paréntesis, comas, etc. En este caso,
Type = ch << 8 | lexSys
, consulte las constantes isLPar ... isRBrack
, este valor es uint32; - lexOper -- El valor se representa como una secuencia de caracteres equivalente en forma de uint32. Consulte las constantes
isNot ... isOr
; - lexNumber -- El número se almacena como int64 o float64. Si el número tiene un punto decimal, es float64;
- lexIdent -- El identificador se almacena como string;
- lexNewLine -- Salto de línea. También se utiliza para calcular la línea y la posición del marcador;
- lexString -- La cadena se almacena como string;
- lexComment -- El comentario se almacena como string;
- lexKeyword -- La palabra clave solo almacena el índice correspondiente, consulte las constantes
keyContract ... keyTail
. En este caso, Type = KeyID << 8 | lexKeyword
.
Además, debe tenerse en cuenta que las palabras clave true, false, nil
se convierten inmediatamente en un marcador de tipo lexNumber y se utilizan los tipos correspondientes bool
e intreface {}
; - lexType -- Este valor contiene el valor de tipo
reflect.Type
correspondiente; - lexExtend -- Identificador que comienza con el símbolo
$
. Estas variables y funciones se pasan desde el exterior, por lo que se asignan a un tipo de marcador especial. Este valor contiene el nombre en forma de cadena, sin el símbolo $
al principio.
- Líne -- Línea en la que se encuentra el marcador;
- Column -- Posición del marcador dentro de la línea.
Vamos a analizar en detalle la función lexParser. La función todo busca el índice del símbolo en el alfabeto y recupera un nuevo estado, un identificador de token (si lo hay) y otras banderas de la tabla de transición en función del estado actual y el símbolo entrante.
El análisis en sí implica llamar a la función todo para cada carácter siguiente y cambiar a un nuevo estado. Una vez que se recibe un token, creamos el token correspondiente en el criterio de salida y continuamos analizando.
Cabe señalar que durante el análisis, no acumulamos tokens de símbolos en una pila o matriz separada, ya que solo guardamos el desplazamiento donde comienza el token. Después de obtener el token, movemos el desplazamiento del siguiente token a la posición actual de análisis.
La parte restante es verificar las banderas de estado léxico utilizadas en el análisis:
- lexfPush -- Esta bandera significa que comenzamos a acumular símbolos en un nuevo token;
- lexfNext -- Este carácter debe agregarse al token actual;
- lexfPop -- Se recibe el token y, por lo general, tenemos el tipo de identificador de token para el análisis;
- lexfSkip -- Esta bandera se utiliza para excluir caracteres del análisis, como barras de control en cadenas
\n \r \"
. Se reemplazan automáticamente durante la fase de análisis léxico.
Lenguaje Needle
Lexemas
El código fuente del programa debe estar codificado en UTF-8.
Los siguientes tipos léxicos:
- Palabras clave -
action
, break
, conditions
, continue
,
contract
, data
, else
, error
, false
, func
, if
,
info
, nil
, return
, settings
, true
, var
, warning
,
while
; - Números - Solo se aceptan números decimales. Hay dos tipos básicos: int y float. Si un número tiene un punto decimal, se convierte en un número de punto flotante float. El tipo int es equivalente a int64 en golang. El tipo float es equivalente a float64 en golang.
- Cadenas - Las cadenas pueden estar entre comillas dobles (
"una cadena"
) o acentos graves (\
una cadena`). Ambos tipos de cadenas pueden contener saltos de línea. Las cadenas entre comillas dobles pueden contener comillas dobles, saltos de línea y retornos de carro escapados con una barra invertida. Por ejemplo,
"Esto es una "primera cadena".rnEsto es una segunda cadena."`. - Comentarios - Hay dos tipos de comentarios. Los comentarios de una sola línea usan dos barras inclinadas hacia adelante (
//
). Por ejemplo, // Este es un comentario de una sola línea
. Los comentarios de varias líneas usan una barra inclinada hacia adelante y un asterisco (/* */
) y pueden abarcar varias líneas. Por ejemplo, /* Este es un comentario de varias líneas */
. - Identificadores - Nombres de variables y funciones que consisten en letras a-z y A-Z, símbolos UTF-8, números y guiones bajos. Los nombres pueden comenzar con una letra, guión bajo,
@
o $
. Los nombres que comienzan con $
son nombres de variables definidos en la sección de datos. Los nombres que comienzan con $
también se pueden usar para definir variables globales dentro de la sección de condiciones y la sección de acciones. Los contratos inteligentes del ecosistema pueden usar el símbolo @
para la invocación. Por ejemplo: @1NewTable(...)
.
Tipos
En la clase Needle, se especifica el tipo correspondiente de golang junto a ella.
- bool - bool, con un valor predeterminado de false;
- bytes - []byte{}, con un valor predeterminado de una matriz de bytes vacía;
- int - int64, con un valor predeterminado de 0;
- address - uint64, con un valor predeterminado de 0;
- array - []interface{}, con un valor predeterminado de una matriz vacía;
- map - map[string]interface{}, con un valor predeterminado de un objeto de matriz vacío;
- money - decimal.Decimal, con un valor predeterminado de 0;
- float - float64, con un valor predeterminado de 0;
- string - string, con un valor predeterminado de una cadena vacía;
- file - map[string]interface{}, con un valor predeterminado de un objeto de matriz vacío.
Estos tipos de variables se definen utilizando la palabra clave var
. Por ejemplo, var var1, var2 int
. Cuando se define una variable de esta manera, recibirá el valor predeterminado de su tipo.
Todos los valores de variables tienen el tipo interface{}, y luego se asignan al tipo de golang requerido. Por lo tanto, por ejemplo, los tipos array y map son tipos de golang []interface{} y map[string]interface{}.
Ambos tipos de matrices pueden contener elementos de cualquier tipo.
Expresiones
Las expresiones pueden contener operaciones aritméticas, operaciones lógicas y llamadas a funciones. Todas las expresiones se evalúan de izquierda a derecha según su prioridad de operación. Si la prioridad de la operación es la misma, la evaluación también es de izquierda a derecha.
La lista de operaciones de mayor a menor prioridad:
- Llamadas a funciones y paréntesis - Al llamar a una función, los parámetros pasados se evalúan de izquierda a derecha;
- Operaciones unarias - Negación lógica
!
y cambio de signo aritmético -
; - Multiplicación y división - Multiplicación aritmética
*
y división /
; - Suma y resta - Suma aritmética
+
y resta -
; - Comparación lógica -
>= > > >=
; - Igualdad y desigualdad lógica -
== !=
; - AND lógico -
&&
; - OR lógico -
||
.
Al evaluar el AND lógico y el OR lógico, siempre se evalúan ambos lados de la expresión.
Needle no tiene comprobación de tipos en tiempo de compilación. Al evaluar operandos, intenta convertir el tipo a un tipo más complejo. El orden de complejidad para los tipos puede ser el siguiente: string, int, float, money
, y solo se implementan conversiones de tipo parcial. El tipo de cadena admite operaciones de suma, lo que resulta en concatenación de cadenas. Por ejemplo, string + string = string, money - int = money, int * float = float
.
Para las funciones, se realiza una comprobación de tipos en los tipos string
e int
durante la ejecución.
El tipo array y el tipo map pueden ser direccionados mediante índices.
Para el tipo array, se debe especificar un valor int como índice. Para el tipo map, se debe especificar una variable o un valor string como índice.
Si se asigna un valor a un elemento de un array con un índice mayor al índice máximo actual, se añadirán elementos vacíos al array. Estos elementos tendrán un valor de inicialización de nil. Por ejemplo: .. código:
En las expresiones de lógica condicional (por ejemplo, if
, while
, &&
, ||
, !
), el tipo se convertirá automáticamente en un valor lógico, si el tipo no es el valor predeterminado, entonces será verdadero.
Ámbito
Las llaves especifican un bloque que puede contener variables de ámbito local. Por defecto, el ámbito de la variable se extiende a su propio bloque y a todos los bloques anidados. En un bloque, se puede definir una nueva variable con el nombre de una variable existente. En este caso, la variable externa con el mismo nombre no está disponible.
Ejecución de contratos
Al llamar a un contrato inteligente, los parámetros definidos en la sección data deben ser pasados a él. Antes de ejecutar el contrato inteligente, la máquina virtual recibe estos parámetros y los asigna a las variables correspondientes ($Param).
Luego se llaman la función predefinida conditions y la función action.
Los errores que ocurren durante la ejecución de un contrato inteligente se pueden dividir en dos tipos: errores formales y errores ambientales. Los errores formales se generan utilizando comandos especiales: error, warning, info
y cuando la función incorporada devuelve err
no igual a nil.
El lenguaje Needle no maneja excepciones. Cualquier error terminará la ejecución del contrato inteligente. Dado que se crea una pila separada y una estructura para almacenar valores de variables al ejecutar un contrato inteligente, el mecanismo de recolección de basura de golang eliminará automáticamente estos datos cuando se complete la ejecución del contrato inteligente.
En informática, BNF es una técnica de símbolos utilizada para gramáticas libres de contexto, generalmente utilizada para describir la sintaxis de los lenguajes utilizados en informática.
<decimal digit> :
<decimal number> :
<symbol code> :
<real number> :
<integer number> :
<number> :
<letter> :
<space> :
<tabulation> :
<newline> :
<special symbol> :
<symbol> :
<name> :
<function name> :
<variable name> :
<type name> :
<string symbol> :
<string element> :
<string> :
<assignment operator> :
<unary operator> :
<binary operator> :
<operator> :
<parameters> :
<contract call> :
<function call> :
<block contents> :
<block> :
<block command> :
<if> :
<while> :
<contract> :
<data section> :
<data parameter> :
<tag> :
<conditions> :
<action> :
<function> :
<variable description> :
<tail> :
<variables definition> :