Skip to content

Instantly share code, notes, and snippets.

@jaytaph
Created April 30, 2026 07:04
Show Gist options
  • Select an option

  • Save jaytaph/bb0aa4aadfa0ef534fb8b102de735117 to your computer and use it in GitHub Desktop.

Select an option

Save jaytaph/bb0aa4aadfa0ef534fb8b102de735117 to your computer and use it in GitHub Desktop.
proof-of-concept.rs
//! proof-of-concept.rs
//!
//! Three design ideas explored together in a single compilable file:
//!
//! 1. Accessor-based Document trait — the engine holds NodeId values and
//! asks the document questions; no &Node
//! ever escapes the interface boundary.
//! Because Document has no associated
//! types, the *Ext shortcut traits
//! (HasDocumentExt, HasCssSystemExt, …)
//! are no longer needed at all.
//!
//! 2. Has* trait decomposition — code that only parses uses
//! `C: HasDocument`, code that only draws
//! uses `C: HasRenderBackend`; neither is
//! forced to know about the other.
//!
//! 3. config! macro — user writes six choices; all impl
//! blocks are generated.
//!
//! Two Document implementations show that swapping storage is one line:
//!
//! ArenaDocument — nodes as structs in a Vec (AOS)
//! ColumnDocument — data in parallel Vecs (SOA, cache-friendly reads)
//!
//! Run with:
//! rustc --edition 2021 proof-of-concept.rs && ./proof-of-concept
#![allow(dead_code, unused_variables)]
use std::collections::HashMap;
// ── Shared primitives ─────────────────────────────────────────────────────────
pub type NodeId = usize;
const ROOT_ID: NodeId = 0;
#[derive(Debug, Clone, PartialEq)]
pub enum NodeType {
Document,
Element,
Text,
Comment,
}
// ── Swappable component traits ────────────────────────────────────────────────
/// The only document interface the rest of the engine ever sees.
/// Nodes are never materialized outside the document — everything is NodeId.
/// Note: no generic parameter C here; no associated Node type.
/// That's what makes *Ext traits unnecessary.
pub trait Document: 'static {
fn new() -> Self
where
Self: Sized;
fn root(&self) -> NodeId;
fn node_type(&self, id: NodeId) -> NodeType;
fn tag_name(&self, id: NodeId) -> Option<&str>;
fn text_value(&self, id: NodeId) -> Option<&str>;
fn attribute(&self, id: NodeId, name: &str) -> Option<&str>;
fn children(&self, id: NodeId) -> &[NodeId];
fn parent(&self, id: NodeId) -> Option<NodeId>;
// Parser calls these — never constructs a Node struct
fn create_element(&mut self, tag: &str) -> NodeId;
fn create_text(&mut self, value: &str) -> NodeId;
fn attach(&mut self, child: NodeId, parent: NodeId, position: Option<usize>);
}
pub trait CssSystem: 'static {
fn parse_str(css: &str) -> Vec<String>; // stub: returns selector strings
}
pub trait HtmlParser<C: HasDocument>: 'static {
fn parse(html: &str) -> C::Document;
}
pub trait Layouter: 'static {}
pub trait RenderBackend: 'static {}
pub trait ChromeHandle: 'static {}
// ── Has* traits (scoped bounds) ───────────────────────────────────────────────
//
// Generic functions only declare the bounds they actually need:
//
// fn walk<C: HasDocument>(doc: &C::Document, id: NodeId) // no renderer in sight
// fn draw<C: HasRenderBackend>(rb: &C::RenderBackend) // no parser in sight
pub trait HasDocument: Sized {
type Document: Document;
}
pub trait HasCssSystem {
type CssSystem: CssSystem;
}
pub trait HasHtmlParser: HasDocument {
type HtmlParser: HtmlParser<Self>;
}
pub trait HasLayouter: HasDocument + HasCssSystem {
type Layouter: Layouter;
}
pub trait HasRenderBackend {
type RenderBackend: RenderBackend;
}
pub trait HasChrome {
type ChromeHandle: ChromeHandle;
}
// ── ModuleConfig ──────────────────────────────────────────────────────────────
pub trait ModuleConfig:
Sized
+ HasDocument
+ HasCssSystem
+ HasHtmlParser
+ HasLayouter
+ HasRenderBackend
+ HasChrome
{
}
// ── config! macro ─────────────────────────────────────────────────────────────
//
// User writes six choices; all impl blocks are generated.
// This is the entire public API for defining a configuration.
macro_rules! config {
(
$name:ident,
Document = $doc:ty,
HtmlParser = $parser:ty,
CssSystem = $css:ty,
Layouter = $layout:ty,
RenderBackend = $render:ty,
ChromeHandle = $chrome:ty $(,)?
) => {
#[derive(Clone, Debug, PartialEq)]
pub struct $name;
impl HasDocument for $name {
type Document = $doc;
}
impl HasCssSystem for $name {
type CssSystem = $css;
}
impl HasHtmlParser for $name {
type HtmlParser = $parser;
}
impl HasLayouter for $name {
type Layouter = $layout;
}
impl HasRenderBackend for $name {
type RenderBackend = $render;
}
impl HasChrome for $name {
type ChromeHandle = $chrome;
}
impl ModuleConfig for $name {}
};
}
// ── Document implementation 1: Arena (AOS) ───────────────────────────────────
// Each node is a struct; all nodes live in a Vec indexed by NodeId.
struct ArenaNode {
node_type: NodeType,
tag_name: Option<String>,
text_value: Option<String>,
attributes: HashMap<String, String>,
children: Vec<NodeId>,
parent: Option<NodeId>,
}
pub struct ArenaDocument {
nodes: Vec<ArenaNode>,
}
impl ArenaDocument {
fn alloc(&mut self, node: ArenaNode) -> NodeId {
let id = self.nodes.len();
self.nodes.push(node);
id
}
}
impl Document for ArenaDocument {
fn new() -> Self {
Self {
nodes: vec![ArenaNode {
node_type: NodeType::Document,
tag_name: None,
text_value: None,
attributes: HashMap::new(),
children: Vec::new(),
parent: None,
}],
}
}
fn root(&self) -> NodeId { ROOT_ID }
fn node_type(&self, id: NodeId) -> NodeType { self.nodes[id].node_type.clone() }
fn tag_name(&self, id: NodeId) -> Option<&str> { self.nodes[id].tag_name.as_deref() }
fn text_value(&self, id: NodeId) -> Option<&str> { self.nodes[id].text_value.as_deref() }
fn attribute(&self, id: NodeId, name: &str) -> Option<&str> {
self.nodes[id].attributes.get(name).map(String::as_str)
}
fn children(&self, id: NodeId) -> &[NodeId] { &self.nodes[id].children }
fn parent(&self, id: NodeId) -> Option<NodeId> { self.nodes[id].parent }
fn create_element(&mut self, tag: &str) -> NodeId {
self.alloc(ArenaNode {
node_type: NodeType::Element,
tag_name: Some(tag.to_string()),
text_value: None,
attributes: HashMap::new(),
children: Vec::new(),
parent: None,
})
}
fn create_text(&mut self, value: &str) -> NodeId {
self.alloc(ArenaNode {
node_type: NodeType::Text,
tag_name: None,
text_value: Some(value.to_string()),
attributes: HashMap::new(),
children: Vec::new(),
parent: None,
})
}
fn attach(&mut self, child: NodeId, parent: NodeId, position: Option<usize>) {
self.nodes[child].parent = Some(parent);
match position {
Some(pos) => self.nodes[parent].children.insert(pos, child),
None => self.nodes[parent].children.push(child),
}
}
}
// ── Document implementation 2: Column (SOA) ──────────────────────────────────
// Same interface, completely different internal layout.
// Each field lives in its own Vec; reading one field for many nodes is a single
// contiguous scan with no pointer chasing.
pub struct ColumnDocument {
node_types: Vec<NodeType>,
tag_names: Vec<Option<String>>,
text_values: Vec<Option<String>>,
attributes: Vec<HashMap<String, String>>,
children: Vec<Vec<NodeId>>,
parents: Vec<Option<NodeId>>,
}
impl ColumnDocument {
fn alloc(&mut self, node_type: NodeType) -> NodeId {
let id = self.node_types.len();
self.node_types.push(node_type);
self.tag_names.push(None);
self.text_values.push(None);
self.attributes.push(HashMap::new());
self.children.push(Vec::new());
self.parents.push(None);
id
}
}
impl Document for ColumnDocument {
fn new() -> Self {
Self {
node_types: vec![NodeType::Document],
tag_names: vec![None],
text_values: vec![None],
attributes: vec![HashMap::new()],
children: vec![Vec::new()],
parents: vec![None],
}
}
fn root(&self) -> NodeId { ROOT_ID }
fn node_type(&self, id: NodeId) -> NodeType { self.node_types[id].clone() }
fn tag_name(&self, id: NodeId) -> Option<&str> { self.tag_names[id].as_deref() }
fn text_value(&self, id: NodeId) -> Option<&str> { self.text_values[id].as_deref() }
fn attribute(&self, id: NodeId, name: &str) -> Option<&str> {
self.attributes[id].get(name).map(String::as_str)
}
fn children(&self, id: NodeId) -> &[NodeId] { &self.children[id] }
fn parent(&self, id: NodeId) -> Option<NodeId> { self.parents[id] }
fn create_element(&mut self, tag: &str) -> NodeId {
let id = self.alloc(NodeType::Element);
self.tag_names[id] = Some(tag.to_string());
id
}
fn create_text(&mut self, value: &str) -> NodeId {
let id = self.alloc(NodeType::Text);
self.text_values[id] = Some(value.to_string());
id
}
fn attach(&mut self, child: NodeId, parent: NodeId, position: Option<usize>) {
self.parents[child] = Some(parent);
match position {
Some(pos) => self.children[parent].insert(pos, child),
None => self.children[parent].push(child),
}
}
}
// ── Stub implementations of the other four components ────────────────────────
pub struct SimpleCss;
impl CssSystem for SimpleCss {
fn parse_str(_css: &str) -> Vec<String> { vec![] }
}
// The parser only needs HasDocument — it has no idea a render backend exists.
pub struct SimpleParser;
impl<C: HasDocument> HtmlParser<C> for SimpleParser {
fn parse(_html: &str) -> C::Document {
let mut doc = C::Document::new();
let html = doc.create_element("html");
doc.attach(html, ROOT_ID, None);
let body = doc.create_element("body");
doc.attach(body, html, None);
let text = doc.create_text("hello from the parser");
doc.attach(text, body, None);
doc
}
}
pub struct SimpleLayouter;
impl Layouter for SimpleLayouter {}
pub struct SimpleBackend;
impl RenderBackend for SimpleBackend {}
pub struct NullChrome;
impl ChromeHandle for NullChrome {}
// ── Two configs — one line apart ──────────────────────────────────────────────
config!(
ArenaConfig,
Document = ArenaDocument,
HtmlParser = SimpleParser,
CssSystem = SimpleCss,
Layouter = SimpleLayouter,
RenderBackend = SimpleBackend,
ChromeHandle = NullChrome,
);
config!(
ColumnConfig,
Document = ColumnDocument, // ← the only difference
HtmlParser = SimpleParser,
CssSystem = SimpleCss,
Layouter = SimpleLayouter,
RenderBackend = SimpleBackend,
ChromeHandle = NullChrome,
);
// ── Generic tree walker — only needs HasDocument ──────────────────────────────
// This function has no idea whether it's talking to ArenaDocument or
// ColumnDocument; both satisfy the same Document contract.
fn walk<C: HasDocument>(doc: &C::Document, id: NodeId, depth: usize) {
let pad = " ".repeat(depth);
match doc.node_type(id) {
NodeType::Element => {
let tag = doc.tag_name(id).unwrap_or("?");
println!("{}<{}>", pad, tag);
for &child in doc.children(id) {
walk::<C>(doc, child, depth + 1);
}
println!("</{}>", tag);
}
NodeType::Text => {
println!("{}\"{}\"", pad, doc.text_value(id).unwrap_or(""));
}
NodeType::Document => {
for &child in doc.children(id) {
walk::<C>(doc, child, depth);
}
}
_ => {}
}
}
// ── Demo ──────────────────────────────────────────────────────────────────────
fn main() {
println!("=== ArenaDocument (AOS) ===");
let doc = <SimpleParser as HtmlParser<ArenaConfig>>::parse("<ignored>");
walk::<ArenaConfig>(&doc, ROOT_ID, 0);
println!("\n=== ColumnDocument (SOA) ===");
let doc = <SimpleParser as HtmlParser<ColumnConfig>>::parse("<ignored>");
walk::<ColumnConfig>(&doc, ROOT_ID, 0);
println!("\nBoth produced by the same SimpleParser, walked by the same walk().");
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment