Codegen works (only through tests right now though)

This commit is contained in:
Borodinov Ilya 2024-12-04 19:54:32 +03:00
parent caedb752b7
commit 2ecea1e653
Signed by: noth
GPG key ID: 75503B2EF596D1BD
12 changed files with 493 additions and 5 deletions

67
Cargo.lock generated
View file

@ -131,6 +131,7 @@ version = "0.1.0"
dependencies = [
"cogs_ast",
"eyre",
"intern-arc",
]
[[package]]
@ -142,6 +143,10 @@ dependencies = [
"tracing",
]
[[package]]
name = "cogs_runtime"
version = "0.1.0"
[[package]]
name = "color-eyre"
version = "0.6.3"
@ -267,6 +272,17 @@ dependencies = [
"similar",
]
[[package]]
name = "intern-arc"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328e5c9569f7aa3ee795c58e1e7da8875c1eb506fba10341e912d892e6d0a193"
dependencies = [
"loom",
"once_cell",
"parking_lot",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@ -297,6 +313,16 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
@ -390,6 +416,9 @@ name = "once_cell"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
dependencies = [
"parking_lot_core",
]
[[package]]
name = "overload"
@ -403,6 +432,29 @@ version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.15"
@ -501,6 +553,15 @@ dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f"
dependencies = [
"bitflags",
]
[[package]]
name = "regex"
version = "1.11.1"
@ -588,6 +649,12 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sharded-slab"
version = "0.1.7"

View file

@ -1,6 +1,6 @@
[workspace]
members = [
"crates/ast",
"crates/ast", "crates/runtime",
"crates/codegen",
"crates/parser",
"integrations/axum"

View file

@ -6,3 +6,4 @@ edition = "2021"
[dependencies]
cogs_ast = { path = "../ast" }
eyre.workspace = true
intern-arc = "0.6.1"

View file

@ -0,0 +1,177 @@
use super::*;
pub struct Generator {
pub trees: Vec<Tree>,
pub intern_str: StrInterner,
}
macro_rules! push {
(@noquote $cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{
$cx.format.push_str($fmt);
$cx.arguments.extend([$($arg),+]);
}};
($cx:expr, $fmt:expr, $($arg:expr),+$(,)?) => {{
$cx.format.push_str($fmt);
$cx.arguments.extend([$(quoted(&$arg)),+]);
}};
}
struct AppendContext {
pre: String,
format: String,
arguments: Vec<String>,
}
impl AppendContext {
#[inline]
fn push(&mut self, format: &str) {
self.format.push_str(format);
}
#[inline]
fn push_arg(&mut self, value: String) {
self.push("{}");
self.arguments.push(value);
}
}
fn quoted(s: &str) -> String {
format!("r#\"{}\"#", s)
}
impl Expression {
fn append(&self, cx: &mut AppendContext) {
match self {
Expression::Literal(literal) => push!(cx, "\"{}\"", literal),
Expression::Code(code) => cx.push_arg(code.trim().to_string()),
}
}
}
impl HtmlTag {
fn append(&self, cx: &mut AppendContext) {
push!(
cx,
// add space after, if there are attributes
if self.attributes.is_empty() {
"<{}"
} else {
"<{} "
},
self.tag,
);
for attr in &self.attributes {
push!(
cx,
if attr.value.is_some() { "{}=" } else { "{}" },
attr.name
);
if let Some(value) = &attr.value {
value.append(cx);
}
}
if self.content.is_empty() {
cx.push("/>");
return;
}
cx.push(">");
for tree in &self.content {
tree.append(cx);
}
push!(cx, "</{}>", self.tag);
}
}
impl CodeTree {
fn append(&self, cx: &mut AppendContext) {
match self {
CodeTree::Code(code) => {
let trimmed = code.trim();
if trimmed.is_empty() {
return;
}
cx.pre.push_str(code.trim());
}
CodeTree::HtmlTag(html_tag) => html_tag.append(cx),
}
}
}
impl CodeBlock {
fn append(&self, cx: &mut AppendContext) {
let mut my_cx = AppendContext {
pre: String::new(),
format: String::new(),
arguments: Vec::new(),
};
for code in self.content.iter() {
code.append(&mut my_cx);
}
let fmt = if my_cx.format.is_empty() {
my_cx.pre
} else {
format!(
r##"{{{pre}
format!(r#"{format}"#, {arguments})
}}"##,
pre = my_cx.pre,
format = my_cx.format,
arguments = my_cx.arguments.join(","),
)
};
push!(@noquote cx, "{}", fmt);
}
}
impl Tree {
fn append(&self, cx: &mut AppendContext) {
match self {
Tree::HtmlText(text) => {
// this adds {} to the format string and adds "{text}" to the args
cx.push_arg(quoted(text.trim()));
}
Tree::HtmlTag(html_tag) => html_tag.append(cx),
Tree::CodeBlock(code_block) => code_block.append(cx),
}
}
}
impl Generator {
pub fn to_format(&self) -> String {
let mut cx = AppendContext {
pre: String::new(),
format: String::new(),
arguments: Vec::new(),
};
for tree in self.trees.iter() {
if let Tree::CodeBlock(code_block) = tree {
if !code_block.has_html {
for code in code_block.content.iter() {
let CodeTree::Code(code) = code else {
panic!("has_html = false, but got CodeTree::HtmlTag")
};
let trimmed = code.trim();
if trimmed.is_empty() {
continue;
}
cx.pre.push_str(code.trim());
}
}
} else {
tree.append(&mut cx);
}
}
format!(
"{pre}let __rendered = format!(r#\"{format}\"#, {arguments});",
pre = cx.pre,
format = cx.format,
arguments = cx.arguments.join(","),
)
}
}

122
crates/codegen/src/ir.rs Normal file
View file

@ -0,0 +1,122 @@
use super::*;
pub enum Expression {
Literal(InternedStr),
Code(InternedStr),
}
pub struct HtmlAttribute {
pub name: InternedStr,
pub value: Option<Expression>,
}
pub struct HtmlTag {
pub tag: InternedStr,
pub attributes: Vec<HtmlAttribute>,
pub content: Vec<Tree>,
}
pub enum CodeTree {
HtmlTag(HtmlTag),
Code(InternedStr),
}
pub struct CodeBlock {
pub has_html: bool,
pub content: Vec<CodeTree>,
}
pub enum Tree {
HtmlText(InternedStr),
HtmlTag(HtmlTag),
CodeBlock(CodeBlock),
}
impl Tree {
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
match value {
ast::Element::Text(text) => Tree::HtmlText(intern.intern_ref(text)),
ast::Element::Html(html) => Tree::HtmlTag(HtmlTag::from_ast(html, intern)),
ast::Element::Block(block) => Tree::CodeBlock(CodeBlock::from_ast(block, intern)),
}
}
}
impl HtmlTag {
pub fn from_ast(value: &ast::HtmlTag, intern: &StrInterner) -> Self {
let tag = intern.intern_ref(&value.tag);
let attributes = value
.attributes
.iter()
.map(|attr| HtmlAttribute::from_ast(attr, intern))
.collect();
let content = value
.content
.iter()
.map(|elem| Tree::from_ast(elem, intern))
.collect();
HtmlTag {
tag,
attributes,
content,
}
}
}
impl HtmlAttribute {
pub fn from_ast(value: &ast::Attribute, intern: &StrInterner) -> Self {
let ast::Element::Text(name) = &value.name else {
panic!("attribute name should be a string")
};
let name = intern.intern_ref(name);
let value = value
.value
.as_ref()
.map(|v| Expression::from_ast(v, intern));
HtmlAttribute { name, value }
}
}
impl CodeBlock {
pub fn from_ast(value: &ast::CodeBlock, intern: &StrInterner) -> Self {
let mut has_html = false;
let content = value
.content
.iter()
.map(|elem| {
if matches!(elem, ast::Element::Html(_)) {
has_html = true
};
CodeTree::from_ast(elem, intern)
})
.collect();
CodeBlock { content, has_html }
}
}
impl Expression {
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
// TODO expression in parser
match value {
ast::Element::Text(text) => Expression::Literal(intern.intern_ref(text)),
ast::Element::Html(_html) => {
panic!("ast::Element::Html should not be used as attribute value")
} // this is the only case where expression is used so we can mention that in the panic message
ast::Element::Block(_block) => {
todo!("code blocks are not supported as attribute values until Expression is implemented in the parser")
}
}
}
}
impl CodeTree {
pub fn from_ast(value: &ast::Element, intern: &StrInterner) -> Self {
match value {
ast::Element::Text(text) => CodeTree::Code(intern.intern_ref(text)),
ast::Element::Html(html) => CodeTree::HtmlTag(HtmlTag::from_ast(html, intern)),
ast::Element::Block(_) => {
panic!("nested code block detected (how the hell did the parser do this?)")
}
}
}
}

View file

@ -1,5 +1,43 @@
extern crate cogs_ast as ast;
mod generate;
use generate::*;
mod ir;
use ir::*;
// type aliases for:
// 1. convenience
// 2. easy swapping to InternedOrd if i ever find it better
type InternedStr = intern_arc::InternedHash<str>;
type StrInterner = intern_arc::HashInterner<str>;
pub fn generate(ast: &ast::Component) -> eyre::Result<String> {
Ok(format!("not yet implemented"))
let mut generator = Generator {
trees: Vec::new(),
intern_str: StrInterner::new(),
};
let mut elements = ast.elements.iter();
for element in elements {
generator
.trees
.push(Tree::from_ast(element, &generator.intern_str));
}
let render = generator.to_format();
Ok(format!(
r#"
pub struct Cog;
impl cogs_runtime::Component for Cog {{
type Props = ();
type Error = core::convert::Infallible;
fn render(&self, props: Self::Props) -> impl core::future::Future<Output = Result<String, Self::Error>> + core::marker::Send + '_ {{
async move {{
{render}
Ok(__rendered)
}}
}}
}}
"#
))
}

View file

@ -204,7 +204,7 @@ fn parse_inside_code_block(input: &str) -> IResult<&str, Vec<Element>> {
debug!("Attempting inside code block {input}");
let (input, elems) = parse_consecutive_proper_elements(input)?;
dbg!(&elems);
debug!(?elems, "parsed inside code block");
Ok((input, elems))
}

View file

@ -0,0 +1,6 @@
[package]
name = "cogs_runtime"
version = "0.1.0"
edition = "2021"
[dependencies]

43
crates/runtime/src/lib.rs Normal file
View file

@ -0,0 +1,43 @@
use core::fmt;
use std::future::Future;
pub trait Component {
type Props;
type Error;
fn render<'a>(&'a self, props: Self::Props) -> impl Future<Output = Result<String, Self::Error>> + Send + 'a;
}
pub trait Render {
fn render(&self) -> String;
}
impl Render for () {
fn render(&self) -> String {
String::new()
}
}
impl Render for &str {
fn render(&self) -> String {
format!("{}", self)
}
}
impl Render for String {
fn render(&self) -> String {
format!("{}", self)
}
}
impl<T: Render> Render for &T {
fn render(&self) -> String {
(**self).render()
}
}
impl<T: Render> Render for Box<T> {
fn render(&self) -> String {
(**self).render()
}
}

View file

@ -3,4 +3,17 @@ source: src/tests.rs
expression: "cogs_codegen::generate(&ast).unwrap()"
snapshot_kind: text
---
not yet implemented
pub struct Cog;
impl cogs_runtime::Component for Cog {
type Props = ();
type Error = core::convert::Infallible;
fn render(&self, props: Self::Props) -> impl core::future::Future<Output = Result<String, Self::Error>> + core::marker::Send + '_ {
async move {
let x = 1;let __rendered = format!(r#"<{}><{}>{}</{}><{} {}="{}">{}{}</{}>{}{}</{}>{}"#, r#"body"#,r#"h1"#,r#"Yo."#,r#"h1"#,r#"a"#,r#"src"#,r#"https://www.youtube.com/watch?v=dQw4w9WgXcQ"#,r#"Click this"#,x,r#"a"#,{println!("test");
format!(r#"<{}>{}</{}>"#, r#"p"#,r#"More Html"#,r#"p"#)
},r#""#,r#"body"#,r#""#);
Ok(__rendered)
}
}
}

View file

@ -5,6 +5,15 @@ snapshot_kind: text
---
Component {
elements: [
Block(
CodeBlock {
content: [
Text(
"let x = 1;\n",
),
],
},
),
Html(
HtmlTag {
tag: "body",
@ -40,6 +49,15 @@ Component {
Text(
"Click this",
),
Block(
CodeBlock {
content: [
Text(
"x",
),
],
},
),
],
},
),

View file

@ -1,6 +1,9 @@
{
let x = 1;
}
<body>
<h1>Yo.</h1>
<a src="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Click this</a>
<a src="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Click this {x}</a>
{
println!("test");
<p>More Html</p>