Una alternativa infinitamente más portátil al lenguaje de programación C.

Para aquellos de ustedes que recuerdan "gratis", el roble es esencialmente una versión más robusta y de alto nivel de ese proyecto. El objetivo del roble es ser el nivel más alto posible en la interfaz, pero lo más pequeño y bajo de nivel posible en el backend.
Soy un graduado y estudiante de primer año de secundaria recién acuñados en la universidad en busca de trabajo. Si disfrutas de mis proyectos, ¡considera apoyarme al comprarme un café!
La clave para la portabilidad loca de Oak es su implementación de backend increíblemente compacta. El código para el backend de Oak se puede expresar en menos de 100 líneas de C. Una implementación tan pequeña solo es posible debido al pequeño conjunto de instrucciones de la representación intermedia. Oak's IR solo está compuesto por 17 instrucciones diferentes . ¡Eso está a la par con Brainfuck!
El backend de Oak funciona de manera muy simple. Cada instrucción funciona en una cinta de memoria . Esta cinta es esencialmente una matriz estática de flotadores de doble precisión.
let x : num = 5.25 ; ... let p : & num = & x ; `beginning of heap`
| | |
v v v
[ 0 , 0 , 0 , 5.25 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 , 3 , 0 , 0 , 0 , 0 , 0 , 0 , ... ]
^
|
`current location of the stack pointer`Cuando una variable se define en una función, se le da una posición estática en relación con el puntero base actual de la máquina virtual. Entonces, cuando se llama una función, el espacio para las variables de la función se asigna en la pila, y el puntero base se incrementa para usar este nuevo espacio. Luego, el compilador simplemente reemplaza la variable con su dirección agregada al desplazamiento del puntero base en el resto del código.
Además, la cinta de memoria funciona como una pila y un montón . Después de que se asigne el espacio para todas las variables del programa, comienza la memoria utilizada para la pila. La pila crece y se encoge con datos en todo el programa: cuando se suman dos números, por ejemplo, se salen de la pila y se reemplazan con el resultado. Del mismo modo, el montón crece y se reduce a lo largo del programa. Sin embargo, el montón se usa para datos asignados dinámicamente : información con una huella de memoria desconocida en el momento de la compilación .
Ahora que comprende cómo funciona fundamentalmente el backend de Oak, ¡aquí está el conjunto de instrucciones completas!
| Instrucción | Efecto secundario |
|---|---|
push(n: f64); | Empuje un número a la pila. |
add(); | Explique dos números de la pila y empuje su suma. |
subtract(); | Avance dos números de la pila. Resta el primero del segundo y empuja el resultado. |
multiply(); | Explique dos números de la pila y empuje su producto. |
divide(); | Avance dos números de la pila. Divida el segundo por el primero y presione el resultado. |
sign(); | Exaque un número de la pila. Si es mayor o igual a cero, presione 1 , de lo contrario, presione -1 . |
allocate(); | Explique un número de la pila y devuelva un puntero a ese número de celdas libres en el montón. |
free(); | Explique un número de la pila y ve a donde apunta este número en la memoria. Saca otro número de la pila y libera tantas celdas en esta ubicación en la memoria. |
store(size: i32); | Explique un número de la pila y ve a donde apunta este número en la memoria. Luego, los números size pop fuera de la pila. Almacene estos números en orden inverso en esta ubicación en la memoria. |
load(size: i32); | Explique un número de la pila y ve a donde apunta este número en la memoria. Luego, presione el número size de celdas de memoria consecutivas en la pila. |
call(fn: i32); | Llame a una función definida por el usuario por su ID de compilador asignado. |
call_foreign_fn(name: String); | Llame a una función extranjera por su nombre en la fuente. |
begin_while(); | Comience un bucle de tiempo. Para cada iteración, descubra un número de la pila. Si el número no es cero, continúe el bucle. |
end_while(); | Marque el final de un bucle de tiempo. |
load_base_ptr(); | Cargue el puntero base del marco de pila establecido, que siempre es menor o igual al puntero de la pila. Las variables se almacenan en relación con el puntero base para cada función. Entonces, una función que define x: num e y: num , x podría almacenarse en base_ptr + 1 , e y podría almacenarse en base_ptr + 2 . Esto permite que las funciones almacenen variables en la memoria dinámicamente y según sea necesario, en lugar de usar ubicaciones de memoria estática. |
establish_stack_frame(arg_size: i32, local_scope_size: i32); | Aparente el número de celdas arg_size de la pila y guárdelas. Luego, llame a load_base_ptr para reanudar la trama de la pila principal cuando termine esta función. Empuje local_scope_size número de ceros en la pila para dejar espacio para las variables de la función. Finalmente, empuje las celdas de argumento almacenado de nuevo a la pila, ya que originalmente se ordenaron. |
end_stack_frame(return_size: i32, local_scope_size: i32); | PARA return_size Número de celdas de la pila y guárdelas. Luego, establezca el número de celdas local_scope_size de la pila para descartar la memoria del marco de la pila. Explique un valor de la pila y guárdelo en el puntero base para reanudar el marco de la pila principal. Finalmente, empuje las celdas de valor de retorno almacenadas en la pila, ya que se ordenaron originalmente. |
¡Usando solo estas instrucciones, Oak puede implementar abstracciones de nivel aún más altos de lo que C puede ofrecer ! Eso puede no parecer mucho, pero es muy poderoso para un idioma tan pequeño.
La sintaxis de roble está fuertemente inspirada en el lenguaje de programación de óxido.
Las funciones se declaran con la palabra clave fn y son sintácticamente idénticas a las funciones de óxido, con la excepción de la semántica return . Además, los tipos y constantes definidos por el usuario se declaran con el type y las palabras clave const respectivamente.
Similar a los atributos exteriores de Rust, Oak presenta muchas banderas de tiempo de compilación. Algunos de estos se demuestran a continuación junto con otras características de roble.

Entonces, ¿cómo funciona exactamente el compilador de roble?
Aplanar estructuras en sus funciones
putnumln(*bday.day) se convierte en putnumln(*Date::day(&bday)) . Este es un proceso bastante simple.Calcule el tamaño del tipo de cada operación
// `3` is the size of the structure on the stack
fn Date :: new ( month : 1 , day : 1 , year : 1 ) -> 3 {
month ; day ; year
}
// self is a pointer to an item of size `3`
fn Date :: day ( self : & 3 ) -> & 1 { self + 1 }
fn main ( ) -> 0 {
let bday : 3 = Date :: new ( 5 , 14 , 2002 ) ;
}Calcule estáticamente la huella de memoria del programa
Convertir expresiones y declaraciones de roble en instrucciones IR equivalentes
Hay muchas circunstancias diferentes en las que una llamada de método es válida. Los métodos siempre toman un puntero a la estructura como argumento . Sin embargo, no se requiere un objeto que llama a un método para ser un puntero . Por ejemplo, el siguiente código es válido: let bday: Date = Date::new(); bday.print(); . El bday variable no es un puntero, pero el método .print() aún se puede usar. He aquí por qué.
Cuando el compilador ve una llamada de método aplanado, necesita encontrar una manera de transformar la "expresión de instancia" en un puntero. Para las variables, esto es fácil: ¡solo agregue una referencia! Por ejemplo, expresiones que ya son consejos, es aún más fácil: ¡no hagas nada! Sin embargo, para cualquier otro tipo de expresión, es un poco más detallado. El compilador se escabulle en una variable oculta para almacenar la expresión y luego compila la llamada del método nuevamente usando la variable como expresión de instancia. Bastante genial, ¿verdad?
Ensamblar las instrucciones IR para un objetivo
Target . Si implementa cada una de las instrucciones de IR para su idioma utilizando el rasgo Target , ¡el roble puede compilarse automáticamente hasta su nuevo lenguaje de programación o ensamblaje! Sí, ¡es tan fácil como parece! Para permitir a los usuarios leer la documentación de bibliotecas y archivos sin acceso a Internet, Oak proporciona el subcomando doc . Esto permite a los autores agregar atributos de documentación a su código para ayudar a otros usuarios a comprender su código o API sin tener que examinar la fuente y leer comentarios.
Aquí hay algún código de ejemplo.
# [ std ]
# [ header ( "This file tests Oak's doc subcommand." ) ]
# [ doc ( "This constant is a constant." ) ]
const CONSTANT = 3 ;
// No doc attribute
const TEST = CONSTANT + 5 ;
# [ doc ( "This structure represents a given date in time.
A Date object has three members:
|Member|Value|
|-|-|
|`month: num` | The month component of the date |
|`day: num` | The day component of the date |
|`year: num` | The year component of the date |" ) ]
struct Date {
let month : num , day : num , year : num ;
# [ doc ( "The constructor used to create a date." ) ]
fn new ( month : num , day : num , year : num ) -> Date {
return [ month , day , year ] ;
}
# [ doc ( "Print the date object to STDOUT" ) ]
fn print ( self : & Date ) {
putnum ( self ->month ) ; putchar ( '/' ) ;
putnum ( self ->day ) ; putchar ( '/' ) ;
putnumln ( self ->year ) ;
}
}
# [ doc ( "This function takes a number `n` and returns `n * n`, or `n` squared." ) ]
fn square ( n : num ) -> num {
return n * n
}
fn main ( ) {
let d = Date :: new ( 5 , 14 , 2002 ) ;
d . print ( ) ;
} Y aquí hay un uso de ejemplo del subcomando doc para imprimir la documentación formateada en el terminal.

Para obtener la compilación del desarrollo actual, clone el repositorio e instálelo.
git clone https://github.com/adam-mcdaniel/oakc
cd oakc
cargo install -f --path . Para obtener la compilación de la versión actual, instale desde cajas.io.
# Also works for updating oakc
cargo install -f oakcLuego, los archivos de roble se pueden compilar con el binario OAKC.
oak c examples/hello_world.ok -c
main.exeC Backend : cualquier compilador GCC que admite C99
Go Backend - Compilador Golang 1.14
Typescript Backend - compilador TypeScript 3.9