diff --git a/klang/Cargo.toml b/klang/Cargo.toml index 710213f..48eddbb 100644 --- a/klang/Cargo.toml +++ b/klang/Cargo.toml @@ -26,8 +26,8 @@ prost-build = "^0.13.3" [[bin]] -name = "kcompile" -path = "src/bin/kcompile.rs" +name = "kompile" +path = "src/bin/kompile.rs" [[test]] name = "integration_test" diff --git a/klang/src/bin/kcompile.rs b/klang/src/bin/kompile.rs similarity index 100% rename from klang/src/bin/kcompile.rs rename to klang/src/bin/kompile.rs diff --git a/klang/src/parser/errors.rs b/klang/src/parser/errors.rs index c524c7c..0599c8c 100644 --- a/klang/src/parser/errors.rs +++ b/klang/src/parser/errors.rs @@ -1,42 +1,49 @@ use crate::parser::Rule; use pest::iterators::Pair; +use prost::{DecodeError, EncodeError}; use std::error::Error; use std::fmt; #[derive(Debug)] pub struct ParseError { pub message: String, - pub line: usize, - pub column: usize, } impl ParseError { pub fn new(message: String) -> ParseError { - ParseError { - message, - line: 0, - column: 0, - } + ParseError { message } } pub fn from_pair(message: String, pair: Pair) -> ParseError { let (line, column) = pair.as_span().start_pos().line_col(); ParseError { - message, - line, - column, + message: format!("{} (line: {}, column: {})", message, line, column), } } } impl fmt::Display for ParseError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!( - f, - "{} (line: {}, column: {})", - self.message, self.line, self.column - ) + write!(f, "{}", self.message) } } impl Error for ParseError {} + +impl From for ParseError { + fn from(error: std::io::Error) -> Self { + ParseError::new(format!("{}", error)) + } +} + +impl From for ParseError { + fn from(error: EncodeError) -> Self { + ParseError::new(format!("{}", error)) + } +} + +impl From for ParseError { + fn from(error: DecodeError) -> Self { + ParseError::new(format!("{}", error)) + } +} diff --git a/klang/src/parser/lang.rs b/klang/src/parser/lang.rs index 18d8144..33a7301 100644 --- a/klang/src/parser/lang.rs +++ b/klang/src/parser/lang.rs @@ -30,10 +30,7 @@ pub fn parse_program(pair: pest::iterators::Pair) -> Result) -> Result, ParseError> { diff --git a/klang/src/parser/mod.rs b/klang/src/parser/mod.rs index 000b8ee..df8f878 100644 --- a/klang/src/parser/mod.rs +++ b/klang/src/parser/mod.rs @@ -43,51 +43,8 @@ pub fn write_program_to_file( binary: bool, ) -> Result<(), ParseError> { if binary { - let mut buf = Vec::new(); - prost::Message::encode(&program.ast_program, &mut buf).map_err(|e| { - ParseError::new(format!( - "Error encoding program to file '{}': {}", - file_path.display(), - e - )) - })?; - - fs::write(file_path, &buf).map_err(|e| { - ParseError::new(format!( - "Error writing program to file '{}': {}", - file_path.display(), - e - )) - })?; + program.save_binary(file_path) } else { - let mut output = String::new(); - - fn render_command(cmd: &ast::Command, indent: usize) -> String { - let mut result = format!("{:indent$}{}", "", cmd.text, indent = indent); - if !cmd.children.is_empty() { - result.push_str(" {\n"); - for child in &cmd.children { - result.push_str(&render_command(child, indent + 2)); - } - result.push_str(&format!("{:indent$}}}\n", "", indent = indent)); - } else { - result.push('\n'); - } - result - } - - for command in &program.ast_program.commands { - output.push_str(&render_command(command, 0)); - } - - fs::write(file_path, &output).map_err(|e| { - ParseError::new(format!( - "Error writing program to file '{}': {}", - file_path.display(), - e - )) - })?; + program.save_text(file_path) } - - Ok(()) } diff --git a/klang/src/parser/structs.rs b/klang/src/parser/structs.rs index f5f01ff..b96a50d 100644 --- a/klang/src/parser/structs.rs +++ b/klang/src/parser/structs.rs @@ -1,12 +1,102 @@ -use super::ast::Program as AstProgram; -use super::ir::Program as IrProgram; +use super::ast::{Command as AstCommand, Program as AstProgram}; +use super::errors::ParseError; use pest_derive::Parser; +use std::fs; +use std::path::Path; #[derive(Parser)] #[grammar = "pest/klang.pest"] pub struct PestParser; +pub struct Node { + pub text: String, + pub children: Vec, +} + +impl Node { + pub fn to_ast(&self) -> AstCommand { + AstCommand { + text: self.text.clone(), + children: self.children.iter().map(|child| child.to_ast()).collect(), + } + } + + pub fn from_ast(ast: &AstCommand) -> Self { + Node { + text: ast.text.clone(), + children: ast + .children + .iter() + .map(|child| Node::from_ast(child)) + .collect(), + } + } + + pub fn to_string(&self, indent: usize) -> String { + let mut result = format!("{:indent$}{}", " ", self.text, indent = indent); + if !self.children.is_empty() { + result.push_str(" {\n"); + for child in &self.children { + result.push_str(&child.to_string(indent + 2)); + } + result.push_str(&format!("{:indent$}}}", " ", indent = indent)); + } + result.push('\n'); + result + } +} + pub struct KlangProgram { - pub ast_program: AstProgram, - pub ir_program: IrProgram, + pub program: Vec, +} + +impl KlangProgram { + pub fn to_ast(&self) -> AstProgram { + AstProgram { + commands: self.program.iter().map(|node| node.to_ast()).collect(), + } + } + + pub fn from_ast(ast: &AstProgram) -> Self { + KlangProgram { + program: ast + .commands + .iter() + .map(|command| Node::from_ast(command)) + .collect(), + } + } + + pub fn save_binary(&self, path: &Path) -> Result<(), ParseError> { + let mut buf = Vec::new(); + prost::Message::encode(&self.to_ast(), &mut buf)?; + fs::write(path, &buf)?; + Ok(()) + } + + pub fn load_binary(path: &Path) -> Result { + let buf = fs::read(path)?; + let program = prost::Message::decode(&*buf)?; + Ok(KlangProgram::from_ast(&program)) + } + + pub fn save_text(&self, path: &Path) -> Result<(), ParseError> { + let output = self.to_text(); + fs::write(path, &output)?; + Ok(()) + } + + pub fn to_text(&self) -> String { + self.program + .iter() + .map(|node| node.to_string(0)) + .collect::>() + .join("\n") + } +} + +impl std::fmt::Display for KlangProgram { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_text()) + } } diff --git a/pyklang/bindings.pyi b/pyklang/bindings.pyi index 38126d6..2e072c6 100644 --- a/pyklang/bindings.pyi +++ b/pyklang/bindings.pyi @@ -2,6 +2,27 @@ # ruff: noqa: E501, F401 +class PyKlangProgram: + def save_binary(self, path:str) -> None: + ... + + def save_text(self, path:str) -> None: + ... + + def __repr__(self) -> str: + ... + + @staticmethod + def load_binary(path:str) -> PyKlangProgram: + ... + + def get_version() -> str: ... +def parse_file(path:str) -> PyKlangProgram: + ... + +def parse_string(input:str) -> PyKlangProgram: + ... + diff --git a/pyklang/kompile.py b/pyklang/kompile.py new file mode 100644 index 0000000..74fed95 --- /dev/null +++ b/pyklang/kompile.py @@ -0,0 +1,29 @@ +"""Defines the PyKlang CLI.""" + +import argparse +from pathlib import Path + +from pyklang.bindings import parse_file + + +def main() -> None: + parser = argparse.ArgumentParser(description="Kompile a Klang program.") + parser.add_argument("input", help="The input file to compile.") + parser.add_argument("-o", "--output", help="The output file to compile.") + parser.add_argument("-t", "--text", action="store_true", help="Output the text representation of the program.") + args = parser.parse_args() + + program = parse_file(args.input) + + if args.output is None: + print(program) + else: + Path(args.output).parent.mkdir(parents=True, exist_ok=True) + if args.text: + program.save_text(args.output) + else: + program.save_binary(args.output) + + +if __name__ == "__main__": + main() diff --git a/pyklang/src/lib.rs b/pyklang/src/lib.rs index b9441a1..d04a3ee 100644 --- a/pyklang/src/lib.rs +++ b/pyklang/src/lib.rs @@ -1,6 +1,11 @@ +use klang::parser::errors::ParseError; +use klang::parser::structs::KlangProgram; +use klang::parser::{parse_file as klang_parse_file, parse_string as klang_parse_string}; +use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3_stub_gen::define_stub_info_gatherer; use pyo3_stub_gen::derive::*; +use std::path::Path; #[pyfunction] #[gen_stub_pyfunction] @@ -8,9 +13,85 @@ fn get_version() -> String { env!("CARGO_PKG_VERSION").to_string() } +#[pyfunction] +#[gen_stub_pyfunction] +fn parse_file(path: &str) -> PyResult { + let program = klang_parse_file(&Path::new(path)) + .map_err(|e| PyValueError::new_err(PyParseError { inner: e }.to_string()))?; + Ok(PyKlangProgram { inner: program }) +} + +#[pyfunction] +#[gen_stub_pyfunction] +fn parse_string(input: &str) -> PyResult { + let program = klang_parse_string(input) + .map_err(|e| PyValueError::new_err(PyParseError { inner: e }.to_string()))?; + Ok(PyKlangProgram { inner: program }) +} + +#[gen_stub_pyclass] +#[pyclass] +struct PyKlangProgram { + inner: KlangProgram, +} + +impl From for PyKlangProgram { + fn from(program: KlangProgram) -> Self { + PyKlangProgram { inner: program } + } +} + +#[gen_stub_pymethods] +#[pymethods] +impl PyKlangProgram { + fn save_binary(&self, path: &str) -> PyResult<()> { + self.inner + .save_binary(&Path::new(path)) + .map_err(|e| PyValueError::new_err(e.to_string())) + } + + fn save_text(&self, path: &str) -> PyResult<()> { + self.inner + .save_text(&Path::new(path)) + .map_err(|e| PyValueError::new_err(e.to_string())) + } + + fn __repr__(&self) -> PyResult { + Ok(self.inner.to_string()) + } + + #[staticmethod] + fn load_binary(path: &str) -> PyResult { + let program = KlangProgram::load_binary(&Path::new(path)) + .map_err(|e| PyValueError::new_err(e.to_string()))?; + Ok(PyKlangProgram { inner: program }) + } +} + +#[pyclass] +struct PyParseError { + inner: ParseError, +} + +impl std::fmt::Display for PyParseError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +#[pymethods] +impl PyParseError { + fn __str__(&self) -> PyResult { + Ok(self.inner.to_string()) + } +} + #[pymodule] fn bindings(m: &Bound) -> PyResult<()> { m.add_function(wrap_pyfunction!(get_version, m)?)?; + m.add_function(wrap_pyfunction!(parse_file, m)?)?; + m.add_function(wrap_pyfunction!(parse_string, m)?)?; + m.add_class::()?; Ok(()) } diff --git a/setup.py b/setup.py index b44b73b..800e269 100644 --- a/setup.py +++ b/setup.py @@ -57,4 +57,5 @@ def run(self) -> None: extras_require={"dev": requirements_dev}, packages=find_packages(include=["pyklang"]), cmdclass={"build_ext": RustBuildExt}, + entry_points={"console_scripts": ["kompile=pyklang.kompile:main"]}, )