Cleaning language

This commit is contained in:
2026-01-29 01:10:53 +01:00
parent db5237480a
commit 7e4f8d0e46
10 changed files with 57 additions and 68 deletions

View File

@@ -8,8 +8,6 @@ enum Token {
Float(f64, SourceSpan),
Str(String, SourceSpan),
Word(String, SourceSpan),
QuoteStart(usize),
QuoteEnd(usize),
}
pub(super) fn compile_script(input: &str, dict: &Dictionary) -> Result<Vec<Op>, String> {
@@ -44,25 +42,21 @@ fn tokenize(input: &str) -> Vec<Token> {
continue;
}
if c == '(' {
if c == ';' {
chars.next(); // consume first ;
if let Some(&(_, ';')) = chars.peek() {
// ;; starts a comment to end of line
chars.next(); // consume second ;
while let Some(&(_, ch)) = chars.peek() {
chars.next();
if ch == ')' {
if ch == '\n' {
break;
}
chars.next();
}
continue;
}
if c == '{' {
chars.next();
tokens.push(Token::QuoteStart(pos));
continue;
}
if c == '}' {
chars.next();
tokens.push(Token::QuoteEnd(pos));
// single ; is a word, create token
tokens.push(Token::Word(";".to_string(), SourceSpan { start: pos, end: pos + 1 }));
continue;
}
@@ -70,7 +64,7 @@ fn tokenize(input: &str) -> Vec<Token> {
let mut word = String::new();
let mut end = start;
while let Some(&(i, ch)) = chars.peek() {
if ch.is_whitespace() || ch == '{' || ch == '}' {
if ch.is_whitespace() {
break;
}
end = i + ch.len_utf8();
@@ -101,18 +95,16 @@ fn compile(tokens: &[Token], dict: &Dictionary) -> Result<Vec<Op>, String> {
Token::Int(n, span) => ops.push(Op::PushInt(*n, Some(*span))),
Token::Float(f, span) => ops.push(Op::PushFloat(*f, Some(*span))),
Token::Str(s, span) => ops.push(Op::PushStr(s.clone(), Some(*span))),
Token::QuoteStart(start_pos) => {
let (quote_ops, consumed, end_pos) = compile_quotation(&tokens[i + 1..], dict)?;
i += consumed;
let body_span = SourceSpan { start: *start_pos, end: end_pos + 1 };
ops.push(Op::Quotation(quote_ops, Some(body_span)));
}
Token::QuoteEnd(_) => {
return Err("unexpected }".into());
}
Token::Word(w, span) => {
let word = w.as_str();
if word == ":" {
if word == "{" {
let (quote_ops, consumed, end_span) = compile_quotation(&tokens[i + 1..], dict)?;
i += consumed;
let body_span = SourceSpan { start: span.start, end: end_span.end };
ops.push(Op::Quotation(quote_ops, Some(body_span)));
} else if word == "}" {
return Err("unexpected }".into());
} else if word == ":" {
let (consumed, name, body) = compile_colon_def(&tokens[i + 1..], dict)?;
i += consumed;
dict.lock().unwrap().insert(name, body);
@@ -163,14 +155,15 @@ fn is_list_end(word: &str) -> bool {
matches!(word, "]" | ">" | ">>")
}
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, usize), String> {
fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, usize, SourceSpan), String> {
let mut depth = 1;
let mut end_idx = None;
for (i, tok) in tokens.iter().enumerate() {
match tok {
Token::QuoteStart(_) => depth += 1,
Token::QuoteEnd(_) => {
if let Token::Word(w, _) = tok {
match w.as_str() {
"{" => depth += 1,
"}" => {
depth -= 1;
if depth == 0 {
end_idx = Some(i);
@@ -180,21 +173,20 @@ fn compile_quotation(tokens: &[Token], dict: &Dictionary) -> Result<(Vec<Op>, us
_ => {}
}
}
}
let end_idx = end_idx.ok_or("missing }")?;
let byte_pos = match &tokens[end_idx] {
Token::QuoteEnd(pos) => *pos,
let end_span = match &tokens[end_idx] {
Token::Word(_, span) => *span,
_ => unreachable!(),
};
let quote_ops = compile(&tokens[..end_idx], dict)?;
Ok((quote_ops, end_idx + 1, byte_pos))
Ok((quote_ops, end_idx + 1, end_span))
}
fn token_span(tok: &Token) -> Option<SourceSpan> {
match tok {
Token::Int(_, s) | Token::Float(_, s) | Token::Str(_, s) | Token::Word(_, s) => Some(*s),
Token::QuoteStart(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
Token::QuoteEnd(p) => Some(SourceSpan { start: *p, end: *p + 1 }),
}
}
@@ -218,7 +210,6 @@ fn compile_colon_def(tokens: &[Token], dict: &Dictionary) -> Result<(usize, Stri
let semi_pos = semi_pos.ok_or("missing ';' in word definition")?;
let body_tokens = &tokens[1..semi_pos];
let body_ops = compile(body_tokens, dict)?;
// consumed = name + body + semicolon
Ok((semi_pos + 1, name, body_ops))
}

View File

@@ -559,7 +559,7 @@ impl Forth {
Op::Pick => {
let idx_i = stack.pop().ok_or("stack underflow")?.as_int()?;
if idx_i < 0 {
return Err(format!("pick index must be >= 0, got {}", idx_i));
return Err(format!("pick index must be >= 0, got {idx_i}"));
}
let idx = idx_i as usize;
let mut quots: Vec<Value> = Vec::new();

View File

@@ -2300,5 +2300,7 @@ pub(super) fn compile_word(
return true;
}
false
// Unrecognized token becomes a string
ops.push(Op::PushStr(name.to_string(), span));
true
}

View File

@@ -197,6 +197,7 @@ impl AudioState {
}
}
#[allow(clippy::too_many_arguments)]
pub fn spawn_sequencer(
link: Arc<LinkState>,
playing: Arc<std::sync::atomic::AtomicBool>,

View File

@@ -187,6 +187,7 @@ fn render_devices(frame: &mut Frame, app: &App, area: Rect) {
);
}
#[allow(clippy::too_many_arguments)]
fn render_device_column(
frame: &mut Frame,
area: Rect,

View File

@@ -242,7 +242,7 @@ fn parse_markdown(md: &str) -> Vec<RLine<'static>> {
match line {
Line::Normal(composite) if composite.style == CompositeStyle::Code => {
code_line_nr += 1;
let raw: String = composite.compounds.iter().map(|c| &*c.src).collect();
let raw: String = composite.compounds.iter().map(|c: &minimad::Compound| c.src).collect();
let mut spans = vec![
Span::styled(format!(" {code_line_nr:>2} "), code_border_style()),
Span::styled("", code_border_style()),

View File

@@ -97,25 +97,15 @@ pub fn tokenize_line(line: &str) -> Vec<Token> {
continue;
}
if c == '(' {
let end = line.len();
let comment_end = line[start..]
.find(')')
.map(|i| start + i + 1)
.unwrap_or(end);
if c == ';' && chars.peek().map(|(_, ch)| *ch) == Some(';') {
// ;; starts a comment to end of line
tokens.push(Token {
start,
end: comment_end,
end: line.len(),
kind: TokenKind::Comment,
});
while let Some((i, _)) = chars.peek() {
if *i >= comment_end {
break;
}
chars.next();
}
continue;
}
if c == '"' {
let mut end = start + 1;

View File

@@ -46,8 +46,8 @@ fn word_defined_in_one_forth_available_in_same() {
}
#[test]
fn unknown_word_errors() {
expect_error("nosuchword", "unknown word");
fn unknown_word_becomes_string() {
expect_str("nosuchword", "nosuchword");
}
#[test]

View File

@@ -11,8 +11,8 @@ fn whitespace_only() {
}
#[test]
fn unknown_word() {
expect_error("foobar", "unknown word");
fn unknown_word_becomes_string() {
expect_str("foobar", "foobar");
}
#[test]
@@ -22,12 +22,12 @@ fn string_not_number() {
#[test]
fn comment_ignored() {
expect_int("1 (this is a comment) 2 +", 3);
expect_int("1 ;; this is a comment\n2 +", 3);
}
#[test]
fn multiline_comment() {
expect_int("1 (multi\nline\ncomment) 2 +", 3);
expect_int("1 ;; first comment\n;; entire line comment\n2 +", 3);
}
#[test]

View File

@@ -88,6 +88,10 @@ pub fn expect_int(script: &str, expected: i64) {
expect_stack(script, &[Value::Int(expected, None)]);
}
pub fn expect_str(script: &str, expected: &str) {
expect_stack(script, &[Value::Str(expected.to_string(), None)]);
}
pub fn expect_float(script: &str, expected: f64) {
let f = run(script);
let stack = f.stack();