From 49ab2791b861884d01488de68f63bdd49d71c1b2 Mon Sep 17 00:00:00 2001 From: Brian Cully Date: Sun, 24 Aug 2025 09:16:11 -0400 Subject: pass annotations to js so we can highlight program text --- index.html | 7 +++-- main.css | 7 ++++- main.mjs | 27 ++++++++++++++++- src/forth/parser.rs | 83 ++++++++++++++++++++++++++++++++--------------------- src/lib.rs | 60 +++++++++++++++++++++++++++++++++++++- 5 files changed, 145 insertions(+), 39 deletions(-) diff --git a/index.html b/index.html index 8b96298..0882b65 100644 --- a/index.html +++ b/index.html @@ -7,11 +7,12 @@

automathon

+

- +drop
+
@@ -44,7 +46,6 @@ drop

- no canvas! diff --git a/main.css b/main.css index 960db69..a1ea31a 100644 --- a/main.css +++ b/main.css @@ -16,11 +16,16 @@ html { grid-row: 1; } -#code textarea { +#code #src { width: 100%; height: 25ex; margin-top: 1ex; padding: 1em; + border: 2px inset lightgray; +} + +#src .exec { + background-color: yellowgreen; } #compile { diff --git a/main.mjs b/main.mjs index dcd2ec7..801736e 100644 --- a/main.mjs +++ b/main.mjs @@ -55,6 +55,28 @@ function renderCallStack(interp) { }); } +function renderTextHighlight(interp) { + const ip = interp.ip(); + const anno = interp.annotation_at(ip) + const src = document.querySelector('#src') + const text = src.textContent; + + // split textcontent into 3 spans: pre-highlight, highlight, + // post-highlight + const pre = document.createElement('span'); + pre.textContent = text.substring(0, anno.start); + const high = document.createElement('span'); + high.classList.add('exec'); + high.textContent = text.substring(anno.start, anno.end); + const post = document.createElement('span'); + post.textContent = text.substring(anno.end); + + while (src.lastChild) { src.removeChild(src.lastChild) } + src.appendChild(pre); + src.appendChild(high); + src.appendChild(post); +} + function tick(interp) { if (!interp.tick()) { interp.reset_ip(); @@ -68,6 +90,7 @@ function tick(interp) { }); renderStack(interp); renderCallStack(interp); + renderTextHighlight(interp); } async function loaded() { @@ -86,7 +109,8 @@ async function loaded() { } // always add a newline until i decide what to do with the parser. - const text = document.querySelector('textarea').value + '\n'; + const text = document.querySelector('#src').textContent + '\n'; + console.debug('compiling', text); const start = performance.now(); const res = interp.compile(text); const end = performance.now(); @@ -97,6 +121,7 @@ async function loaded() { initWordlist(); renderStack(interp); renderCallStack(interp); + renderTextHighlight(interp); } }; document.querySelector('#tick').onclick = e => { diff --git a/src/forth/parser.rs b/src/forth/parser.rs index 1eb93df..c934c4a 100644 --- a/src/forth/parser.rs +++ b/src/forth/parser.rs @@ -27,6 +27,14 @@ impl std::error::Error for ParseError {} type ParseResult = Result; +// todo: the annotations should be directly tied to the wordlist so +// they can't get out of sync. +#[derive(Debug)] +pub struct Annotation { + // (start, end) index in program text + pub loc: (usize, usize), +} + #[derive(Debug)] pub struct WordCatalog<'a>(pub(super) HashMap<&'a str, usize>); @@ -44,6 +52,7 @@ pub struct Parser<'a> { // routine is always in the first entry. // todo: don't be pub, have a method to extract a wordlist pub wordlist: WordList, + pub annotations: Vec>, // catalog of word to word index in `wordlist` pub wordalog: WordCatalog<'a>, // holds a stack of indices into `wordlist` that are currently @@ -52,7 +61,7 @@ pub struct Parser<'a> { defstack: Vec, // number of clauses currently being defined for a particular ‘if’ // construct. a stack is needed in order to allow for nesting. - if_stack: Vec, + if_stack: Vec<(IfClauses, Annotation)>, } impl<'a> Parser<'a> { @@ -64,6 +73,7 @@ impl<'a> Parser<'a> { text, enumerator: text.chars().enumerate(), wordlist: WordList(wl), + annotations: vec![vec![]], wordalog: WordCatalog(HashMap::new()), defstack: vec![], if_stack: vec![], @@ -93,21 +103,25 @@ impl<'a> Parser<'a> { &self.wordlist.0[*word_index] } + fn anno_mut(&mut self) -> &mut Vec { + let word_index = self.defstack.last().unwrap_or(&0); + &mut self.annotations[*word_index] + } + // push `op` onto the currently building bytecode, as determined // by the top of the `namestack`. - fn bc_push(&mut self, op: OpCode) -> ParseResult<()> { - // let word_index = self.defstack.last().unwrap_or(&0); - // self.wordlist.0[*word_index].0.push(op); + fn bc_push(&mut self, op: OpCode, anno: Annotation) { self.bc_mut().0.push(op); - Ok(()) + self.anno_mut().push(anno); } pub fn parse(&mut self) -> ParseResult<()> { - while let Some((word, _start, end)) = self.next_word() { + while let Some((word, start, end)) = self.next_word() { + let anno = Annotation { loc: (start, end) }; if let Ok(i) = word.parse::() { - self.bc_push(OpCode::Num(i))?; + self.bc_push(OpCode::Num(i), anno); } else if let Some(i) = self.wordalog.0.get(word) { - self.bc_push(OpCode::Call(*i))?; + self.bc_push(OpCode::Call(*i), anno); } else { match word { r#"s""# => { @@ -115,13 +129,14 @@ impl<'a> Parser<'a> { self.enumerator .find(|(_i, c)| return *c == '"') .ok_or(ParseError::MissingQuote)?; - self.bc_push(OpCode::Str(end+1, s_end))?; + self.bc_push(OpCode::Str(end+1, s_end), anno); }, ":" => { let (name, _, _) = self.next_word().ok_or(ParseError::EOF)?; self.wordalog.0.insert(name, self.wordlist.0.len()); self.defstack.push(self.wordlist.0.len()); self.wordlist.0.push(ByteCode(vec![])); + self.annotations.push(vec![]); }, ";" => { match self.bc().0.last() { @@ -136,59 +151,61 @@ impl<'a> Parser<'a> { self.bc_mut().0.pop(); self.bc_mut().0.push(OpCode::TIf(t, f)); // technically only needed if ‘f’ is None, but whatever. - self.bc_mut().0.push(OpCode::Ret); + self.bc_push(OpCode::Ret, anno); }, - _ => self.bc_push(OpCode::Ret)?, + _ => self.bc_push(OpCode::Ret, anno), } self.defstack.pop().ok_or(ParseError::DefStackEmpty)?; }, "if" => { let i = self.wordlist.0.len(); self.wordlist.0.push(ByteCode(vec![])); + self.annotations.push(vec![]); self.defstack.push(i); - self.if_stack.push(IfClauses::True(i)); + self.if_stack.push((IfClauses::True(i), anno)); }, "else" => { - self.bc_push(OpCode::Ret)?; + self.bc_push(OpCode::Ret, anno); self.defstack.pop(); let i = self.wordlist.0.len(); self.wordlist.0.push(ByteCode(vec![])); + self.annotations.push(vec![]); self.defstack.push(i); - let true_clause = match self.if_stack.pop() { + let (true_clause, anno) = match self.if_stack.pop() { None => return Err(ParseError::MissingIf), - Some(IfClauses::TrueFalse(_, _)) => return Err(ParseError::MissingIf), - Some(IfClauses::True(cl)) => cl, + Some((IfClauses::TrueFalse(_, _), _)) => return Err(ParseError::MissingIf), + Some((IfClauses::True(cl), anno)) => (cl, anno), }; - self.if_stack.push(IfClauses::TrueFalse(true_clause, i)); + self.if_stack.push((IfClauses::TrueFalse(true_clause, i), anno)); }, "then" => { - self.bc_push(OpCode::Ret)?; + self.bc_push(OpCode::Ret, anno); self.defstack.pop(); match self.if_stack.pop() { None => return Err(ParseError::MissingIf), - Some(IfClauses::True(true_clause)) => { - self.bc_push(OpCode::If(true_clause, None))?; + Some((IfClauses::True(true_clause), anno)) => { + self.bc_push(OpCode::If(true_clause, None), anno); }, - Some(IfClauses::TrueFalse(true_clause, false_clause)) => { - self.bc_push(OpCode::If(true_clause, Some(false_clause)))?; + Some((IfClauses::TrueFalse(true_clause, false_clause), anno)) => { + self.bc_push(OpCode::If(true_clause, Some(false_clause)), anno); } } }, - "+" => self.bc_push(OpCode::Add)?, - "-" => self.bc_push(OpCode::Sub)?, - "*" => self.bc_push(OpCode::Mul)?, - "/" => self.bc_push(OpCode::Div)?, - "dup" => self.bc_push(OpCode::Dup)?, - "drop" => self.bc_push(OpCode::Drop)?, - "=" => self.bc_push(OpCode::EQ)?, - ">" => self.bc_push(OpCode::GT)?, - ">=" => self.bc_push(OpCode::GTE)?, - "<" => self.bc_push(OpCode::LT)?, - "<=" => self.bc_push(OpCode::LTE)?, + "+" => self.bc_push(OpCode::Add, anno), + "-" => self.bc_push(OpCode::Sub, anno), + "*" => self.bc_push(OpCode::Mul, anno), + "/" => self.bc_push(OpCode::Div, anno), + "dup" => self.bc_push(OpCode::Dup, anno), + "drop" => self.bc_push(OpCode::Drop, anno), + "=" => self.bc_push(OpCode::EQ, anno), + ">" => self.bc_push(OpCode::GT, anno), + ">=" => self.bc_push(OpCode::GTE, anno), + "<" => self.bc_push(OpCode::LT, anno), + "<=" => self.bc_push(OpCode::LTE, anno), other => return Err(ParseError::UnknownWord(String::from(other))), } } diff --git a/src/lib.rs b/src/lib.rs index 850070a..71f7b7d 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,15 +50,55 @@ impl ExportedByteCode { } } +// ibid. +#[wasm_bindgen] +#[derive(Clone)] +pub struct ExportedAnnotation { + pub start: usize, + pub end: usize, +} + +impl From<(usize, usize)> for ExportedAnnotation { + fn from(v: (usize, usize)) -> Self { + Self { + start: v.0, + end: v.1, + } + } +} + +// ibid. +#[wasm_bindgen] +#[derive(Clone)] +pub struct ExportedWordAnnotations(Vec); + +impl ExportedWordAnnotations { + pub fn from_word_annotations(annos: &Vec) -> Self { + ExportedWordAnnotations(annos.iter().map(|anno| anno.loc.into()).collect()) + } +} + +#[wasm_bindgen] +impl ExportedWordAnnotations { + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn at(&self, offset: usize) -> ExportedAnnotation { + self.0[offset].clone() + } +} + #[wasm_bindgen] pub struct ExportedInterp { + annos: Vec, i: Option, } #[wasm_bindgen] impl ExportedInterp { fn new() -> Self { - Self { i: None } + Self { annos: vec![], i: None } } pub fn compile(&mut self, text: &str) -> bool { @@ -67,6 +107,16 @@ impl ExportedInterp { error!("couldn't parse program text: {:?}", e); return false } + self.annos = p.annotations.iter() + .map(|word_anno| -> ExportedWordAnnotations { + let v = word_anno.into_iter() + .map(|anno| -> ExportedAnnotation { + anno.loc.into() + }) + .collect(); + ExportedWordAnnotations(v) + }) + .collect(); let interp = forth::interp::Interp::new(p.wordlist); let _ = self.i.insert(interp); true @@ -105,6 +155,14 @@ impl ExportedInterp { interp.wordlist.iter().map(|bc| ExportedByteCode::from_bc(bc)).collect() } + pub fn annotations(&self) -> Vec { + self.annos.clone() + } + + pub fn annotation_at(&self, ip: &ExportedInstructionPointer) -> ExportedAnnotation { + self.annos[ip.word].0[ip.offset].clone() + } + pub fn callstack(&self) -> Vec { let Some(interp) = &self.i else { return vec![] -- cgit v1.3