summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorBrian Cully <bjc@spork.org>2025-08-24 09:16:11 -0400
committerBrian Cully <bjc@spork.org>2025-08-24 09:31:51 -0400
commit49ab2791b861884d01488de68f63bdd49d71c1b2 (patch)
tree54ee6f6db5be39093ffd22995461001d732f7dc2
parent365312b14723503424a601a49827c191af82ad7a (diff)
downloadautomathon-49ab2791b861884d01488de68f63bdd49d71c1b2.tar.gz
automathon-49ab2791b861884d01488de68f63bdd49d71c1b2.zip
pass annotations to js so we can highlight program text
-rw-r--r--index.html7
-rw-r--r--main.css7
-rw-r--r--main.mjs27
-rw-r--r--src/forth/parser.rs83
-rwxr-xr-xsrc/lib.rs60
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 @@
<body>
<h1>automathon</h1>
+
<div id='editor'>
<div id='code'>
<button id='compile'>compile</button>
<br>
- <textarea id='src'>
+ <pre id='src' contenteditable='plaintext-only'>
: fac
dup
1 > if
@@ -19,8 +20,9 @@
then
;
5 fac
-drop</textarea>
+drop</pre>
</div>
+
<div id='state-container'>
<div id='state'>
<div class='controls'>
@@ -44,7 +46,6 @@ drop</textarea>
</div>
</div>
<br>
- <canvas id='arena' width='512' height='512'>no canvas!</canvas>
<script src='./main.mjs' type='module'></script>
</body>
</html>
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<T> = Result<T, ParseError>;
+// 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<Vec<Annotation>>,
// 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<usize>,
// number of clauses currently being defined for a particular ‘if’
// construct. a stack is needed in order to allow for nesting.
- if_stack: Vec<IfClauses>,
+ 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<Annotation> {
+ 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::<i32>() {
- 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<ExportedAnnotation>);
+
+impl ExportedWordAnnotations {
+ pub fn from_word_annotations(annos: &Vec<forth::parser::Annotation>) -> 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<ExportedWordAnnotations>,
i: Option<forth::interp::Interp>,
}
#[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<ExportedWordAnnotations> {
+ self.annos.clone()
+ }
+
+ pub fn annotation_at(&self, ip: &ExportedInstructionPointer) -> ExportedAnnotation {
+ self.annos[ip.word].0[ip.offset].clone()
+ }
+
pub fn callstack(&self) -> Vec<ExportedInstructionPointer> {
let Some(interp) = &self.i else {
return vec![]