Implement safe public LSL delivery (#129)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m45s
CI / required (push) Failing after 1m0s

This commit is contained in:
2026-08-18 10:38:12 +02:00
parent 6370b3e416
commit a749111657
59 changed files with 3235 additions and 2144 deletions

View File

@@ -0,0 +1,339 @@
//! Focused compatibility fixtures for issue #83's checked-in LSL generator.
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use libremetaverse_types::compat::{Object, Utf16CodeUnit};
use metacrate_lsl_tools::{
BASE, CSymbol, CSymbolSymType, Dfa, ErrorHandler, GENERATED_PARSER_DATA, ID,
LSL_GENERATOR_SERIALIZATION_VERSION, Lexer, Nfa, NfaNode, ObjectList,
ObjectListOListEnumerator, Path, Regex, SCreator, Serialiser, Sfactory, SymbolType, SymbolsGen,
TCreator, Tfactory, TokClassDef, TokensGen, generated_lexer, generated_parser,
};
struct SharedWriter(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for SharedWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.map_err(|_| std::io::Error::other("poisoned output"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
fn capture<F>(function: F) -> Vec<u8>
where
F: FnOnce(Box<dyn std::io::Write + Send>),
{
let output = Arc::new(Mutex::new(Vec::new()));
function(Box::new(SharedWriter(output.clone())));
Arc::try_unwrap(output)
.expect("single output owner")
.into_inner()
.expect("output")
}
#[test]
fn generated_lexer_preserves_tokens_reserved_words_and_eof() {
let mut lexer = Lexer::new(generated_lexer().expect("generated lexer")).expect("runtime");
lexer
.start_with_string("Widget(value;):base(new Child[])".to_owned())
.expect("source");
let mut tokens = Vec::new();
while let Some(token) = lexer.next_token().expect("lexing") {
tokens.push((token.yyname(), token.yytext(), token.yynum()));
}
assert_eq!(
tokens,
[
("ID".to_owned(), "Widget".to_owned(), 6),
("LPAREN".to_owned(), "(".to_owned(), 12),
("ID".to_owned(), "value".to_owned(), 6),
("SEMICOLON".to_owned(), ";".to_owned(), 9),
("RPAREN".to_owned(), ")".to_owned(), 13),
("COLON".to_owned(), ":".to_owned(), 8),
("BASE".to_owned(), "base".to_owned(), 3),
("LPAREN".to_owned(), "(".to_owned(), 12),
("NEW".to_owned(), "new".to_owned(), 5),
("ID".to_owned(), "Child".to_owned(), 6),
("LBRACK".to_owned(), "[".to_owned(), 14),
("RBRACK".to_owned(), "]".to_owned(), 15),
("RPAREN".to_owned(), ")".to_owned(), 13),
("EOF".to_owned(), "EOF".to_owned(), 2),
]
);
}
#[test]
fn generated_parser_accepts_the_reviewed_class_body_grammar() {
let table = generated_parser().expect("generated parser");
assert_eq!(table.arr, GENERATED_PARSER_DATA);
let lexer = Lexer::new(generated_lexer().expect("generated lexer")).expect("runtime");
let mut parser = metacrate_lsl_tools::Parser::new(table, lexer).expect("parser");
let result = parser
.parse_with_string("Widget(value;):base(new Child[])".to_owned())
.expect("class body");
assert_eq!(result.yyname(), "ClassBody");
assert_eq!(result.yynum(), 19);
}
#[test]
fn parser_and_lexer_emission_is_byte_deterministic() {
let first_parser = capture(|output| {
generated_parser()
.expect("parser")
.emit(output)
.expect("emit");
});
let second_parser = capture(|output| {
generated_parser()
.expect("parser")
.emit(output)
.expect("emit");
});
assert_eq!(first_parser, second_parser);
let first_lexer = capture(|output| {
generated_lexer()
.expect("lexer")
.emit_dfa(output)
.expect("emit");
});
let second_lexer = capture(|output| {
generated_lexer()
.expect("lexer")
.emit_dfa(output)
.expect("emit");
});
assert_eq!(first_lexer, second_lexer);
}
#[test]
fn generated_token_classes_have_live_identity_and_text() {
let mut lexer = Lexer::new(generated_lexer().expect("lexer")).expect("runtime");
lexer
.start_with_string("base name".to_owned())
.expect("source");
let base = lexer.next_token().expect("lexing").expect("base");
let mapped_base = BASE::new(lexer.clone()).expect("mapped base");
assert_eq!((base.yyname(), base.yynum()), ("BASE".to_owned(), 3));
assert_eq!(
(mapped_base.yyname(), mapped_base.yynum()),
("BASE".to_owned(), 3)
);
let _ = lexer.next_token().expect("lexing").expect("name");
let mapped_id = ID::new(lexer).expect("mapped id");
assert_eq!(
(mapped_id.yyname(), mapped_id.yynum()),
("ID".to_owned(), 6)
);
}
#[test]
fn regex_and_nfa_build_a_functional_native_dfa() {
let handler = ErrorHandler::default();
let tokens = TokensGen::new(handler.clone()).expect("generator");
let regex = Regex::new(tokens.clone(), 0, "[A-Z][a-z]+".to_owned()).expect("regex");
assert!(
!regex
.match__with_char(Utf16CodeUnit(u16::from(b'Q')))
.expect("single-unit mismatch")
);
assert!(
!regex
.match__with_char(Utf16CodeUnit(u16::from(b'q')))
.expect("mismatch")
);
assert_eq!(
regex
.match__with_string("Zebra".to_owned())
.expect("prefix"),
5
);
let mut nfa = Nfa::new_with_tokens_gen_regex(tokens, regex).expect("nfa");
nfa.m_end.m_s_terminal = "CAPITAL".to_owned();
let dfa = Dfa::new_with_nfa(nfa).expect("dfa");
let mut table = metacrate_lsl_tools::YyLexer::new(handler).expect("table");
table.set_start_dfa("YYINITIAL", dfa).expect("start");
table.using_eof = true;
let mut lexer = Lexer::new(table).expect("runtime");
lexer.start_with_string("Rust".to_owned()).expect("source");
let token = lexer.next_token().expect("lexing").expect("token");
assert_eq!(
(token.yyname(), token.yytext()),
("CAPITAL".to_owned(), "Rust".to_owned())
);
let tokens = TokensGen::new(ErrorHandler::default()).expect("manual generator");
let mut manual = Nfa::new_with_tokens_gen(tokens.clone()).expect("manual nfa");
manual.m_end.m_s_terminal = "PAIR".to_owned();
let middle = NfaNode::new(tokens).expect("middle state");
manual
.start
.add_arc(Utf16CodeUnit(u16::from(b'a')), middle.clone())
.expect("first arc");
middle
.add_arc(Utf16CodeUnit(u16::from(b'b')), manual.m_end.clone())
.expect("second arc");
let dfa = Dfa::new_with_nfa(manual).expect("determinised nfa");
let mut action = -1;
assert_eq!(
dfa.match_("ab".to_owned(), 0, &mut action).expect("pair"),
2
);
}
#[test]
fn serialiser_round_trips_deterministically_with_versioning() {
let value = Object::Map(HashMap::from([
("b".to_owned(), Object::Integer(7)),
(
"a".to_owned(),
Object::Array(vec![Object::Boolean(true), Object::String("λ".to_owned())]),
),
]));
let encoded = capture(|output| {
let serialiser = Serialiser::new_with_text_writer(output).expect("writer");
serialiser.version_check().expect("version");
serialiser.serialise(value.clone()).expect("serialise");
});
let encoded_again = capture(|output| {
let serialiser = Serialiser::new_with_text_writer(output).expect("writer");
serialiser.version_check().expect("version");
serialiser.serialise(value.clone()).expect("serialise");
});
assert_eq!(encoded, encoded_again);
let integers = String::from_utf8(encoded)
.expect("ASCII integers")
.split(',')
.filter(|value| !value.trim().is_empty())
.map(|value| value.trim().parse::<i32>().expect("integer"))
.collect::<Vec<_>>();
let decoder = Serialiser::new_with_int32_array(integers).expect("reader");
decoder
.version_check()
.expect(LSL_GENERATOR_SERIALIZATION_VERSION);
assert_eq!(decoder.deserialise().expect("deserialise"), value);
}
#[test]
fn compatibility_enumerator_and_generator_state_are_live() {
let mut values = ObjectList::new().expect("list");
values.add(Object::Integer(1)).expect("first");
values
.add(Object::String("two".to_owned()))
.expect("second");
let enumerator = ObjectListOListEnumerator::new(values).expect("enumerator");
assert_eq!(enumerator.current(), Object::Undefined);
assert!(enumerator.move_next().expect("first"));
assert_eq!(enumerator.current(), Object::Integer(1));
assert!(enumerator.move_next().expect("second"));
assert_eq!(enumerator.current(), Object::String("two".to_owned()));
assert!(!enumerator.move_next().expect("end"));
enumerator.reset().expect("reset");
assert!(enumerator.move_next().expect("first again"));
let generator = TokensGen::new(ErrorHandler::default()).expect("generator");
assert_eq!(generator.new_state().expect("state one"), 1);
assert_eq!(generator.new_state().expect("state two"), 2);
assert_eq!(
generator
.fix_actions("yybegin(); yyl".to_owned())
.expect("rewritten action"),
"yym.yy_begin(); ((tokens)yym)"
);
assert!(Dfa::new_with_tokens_gen(generator).is_ok());
}
#[test]
fn symbol_and_token_factories_execute_registered_rust_closures() {
let parser = metacrate_lsl_tools::Parser::new(
generated_parser().expect("parser table"),
Lexer::new(generated_lexer().expect("lexer table")).expect("lexer"),
)
.expect("parser");
let symbol_creator = SCreator::from_fn(|_| Ok(Object::String("symbol".to_owned())));
let registration = Sfactory::new(
generated_parser().expect("parser table"),
"GeneratedSymbol".to_owned(),
symbol_creator,
)
.expect("symbol registration");
assert_eq!(registration.name(), "GeneratedSymbol");
assert_eq!(
Sfactory::create("GeneratedSymbol_Derived".to_owned(), parser).expect("symbol factory"),
Object::String("symbol".to_owned())
);
let lexer = Lexer::new(generated_lexer().expect("lexer table")).expect("lexer");
let token_creator = TCreator::from_fn(|_| Ok(Object::Integer(83)));
let registration = Tfactory::new(
generated_lexer().expect("lexer table"),
"GeneratedToken".to_owned(),
token_creator,
)
.expect("token registration");
assert_eq!(registration.name(), "GeneratedToken");
assert_eq!(
Tfactory::create("GeneratedToken_Derived".to_owned(), lexer).expect("token factory"),
Object::Integer(83)
);
let callback_ran = Arc::new(AtomicBool::new(false));
let callback_state = callback_ran.clone();
let async_creator = TCreator::from_fn(|_| Ok(Object::String("async".to_owned())));
let result = async_creator
.begin_invoke(
Lexer::new(generated_lexer().expect("lexer table")).expect("lexer"),
Box::new(move |_| callback_state.store(true, Ordering::SeqCst)),
Object::Undefined,
)
.expect("begin invoke");
assert!(callback_ran.load(Ordering::SeqCst));
assert_eq!(
async_creator.end_invoke(result).expect("end invoke"),
Object::String("async".to_owned())
);
}
#[test]
fn symbols_paths_and_token_class_definitions_keep_native_state() {
let symbols = SymbolsGen::new(ErrorHandler::default()).expect("symbols generator");
let declared = SymbolType::new_with_symbols_gen_string_boolean(
symbols.clone(),
"SemanticValue".to_owned(),
true,
)
.expect("symbol type");
assert_eq!(
declared
.find("SemanticValue".to_owned())
.expect("lookup")
.name,
"SemanticValue"
);
let identifier = CSymbol::native("ID", 6, CSymbolSymType::Terminal).expect("symbol");
let path = Path::new_with_c_symbol_array(vec![identifier.clone()]).expect("path");
assert_eq!(path.spelling()[0].m_yynum, 6);
assert!(!path.valid);
assert_eq!(path.top().m_state, 0);
let output = Arc::new(Mutex::new(Vec::new()));
let generator =
metacrate_lsl_tools::GenBase::new(ErrorHandler::default(), Box::new(SharedWriter(output)))
.expect("generator base");
let definition = TokClassDef::new(generator, "IDENTIFIER".to_owned(), "TOKEN".to_owned())
.expect("token class");
assert_eq!(definition.m_name, "IDENTIFIER");
assert_eq!(definition.m_ref_token, "TOKEN");
assert_eq!(definition.m_yynum, 3);
}

View File

@@ -0,0 +1,391 @@
//! Focused compatibility fixtures translated from the pinned lexer runtime.
use std::collections::BTreeSet;
use std::sync::{Arc, Mutex};
use libremetaverse_types::compat::{Object, UnicodeCategory, Utf16CodeUnit};
use metacrate_lsl_tools::{
CSToolsException, CharacterMatcher, CsReader, Dfa, DfaAccept, DfaState, DiagnosticCategory,
DotNetUnicodeCategory, Error, ErrorHandler, InputEncoding, Lexer, LexerAction, LineManager,
ObjectList, ResWds, SourceLineInfo, TOKEN, TokenDefinition, YyLexer,
};
fn token(name: &str, number: i32) -> TokenDefinition {
TokenDefinition::new(name, number).expect("valid definition")
}
fn accept(name: &str, number: i32, action: LexerAction, reserved_words: Option<&str>) -> DfaAccept {
DfaAccept {
token: token(name, number),
action,
action_number: number,
reserved_words: reserved_words.map(ToOwned::to_owned),
}
}
fn language_table() -> YyLexer {
let letters = vec![
DotNetUnicodeCategory::UppercaseLetter,
DotNetUnicodeCategory::LowercaseLetter,
DotNetUnicodeCategory::TitlecaseLetter,
DotNetUnicodeCategory::ModifierLetter,
DotNetUnicodeCategory::OtherLetter,
];
let whitespace = vec![
DotNetUnicodeCategory::Control,
DotNetUnicodeCategory::SpaceSeparator,
DotNetUnicodeCategory::LineSeparator,
DotNetUnicodeCategory::ParagraphSeparator,
];
let states = vec![
DfaState::default()
.transition(CharacterMatcher::Categories(letters.clone()), 1)
.transition(CharacterMatcher::Exact(u16::from(b'_')), 1)
.transition(
CharacterMatcher::Category(DotNetUnicodeCategory::DecimalDigitNumber),
2,
)
.transition(CharacterMatcher::Categories(whitespace.clone()), 3)
.transition(CharacterMatcher::Exact(u16::from(b'+')), 4),
DfaState::default()
.transition(CharacterMatcher::Categories(letters), 1)
.transition(CharacterMatcher::Exact(u16::from(b'_')), 1)
.transition(
CharacterMatcher::Category(DotNetUnicodeCategory::DecimalDigitNumber),
1,
)
.accepting(accept("ID", 3, LexerAction::Emit, Some("keywords"))),
DfaState::default()
.transition(
CharacterMatcher::Category(DotNetUnicodeCategory::DecimalDigitNumber),
2,
)
.accepting(accept("INTEGER", 4, LexerAction::Emit, None)),
DfaState::default()
.transition(CharacterMatcher::Categories(whitespace), 3)
.accepting(accept("WS", 5, LexerAction::Skip, None)),
DfaState::default().accepting(accept("PLUS", 6, LexerAction::Emit, None)),
];
let mut table = YyLexer::new(ErrorHandler::default()).expect("table");
table.using_eof = true;
table
.set_start_dfa("YYINITIAL", Dfa::from_states(states, 0).expect("dfa"))
.expect("start state");
table
.set_reserved_words(
"keywords",
ResWds::from_pairs(
[("if", token("IF", 20)), ("while", token("WHILE", 21))],
false,
)
.expect("reserved words"),
)
.expect("reserved table");
table
}
fn lexer(source: &str) -> Lexer {
let mut lexer = Lexer::new(language_table()).expect("lexer");
lexer
.start_with_string(source.to_owned())
.expect("valid source");
lexer
}
#[test]
fn token_fixture_preserves_names_text_numbers_positions_and_eof() {
let mut lexer = lexer("if café_2 + 19");
let mut tokens = Vec::new();
while let Some(value) = lexer.next_token().expect("tokenization") {
tokens.push(value);
}
let actual = tokens
.iter()
.map(|token| {
(
token.yyname(),
token.yytext(),
token.yynum(),
token.pos,
token.end,
)
})
.collect::<Vec<_>>();
assert_eq!(
actual,
[
("IF".to_owned(), "if".to_owned(), 20, 0, 2),
("ID".to_owned(), "café_2".to_owned(), 3, 3, 9),
("PLUS".to_owned(), "+".to_owned(), 6, 10, 11),
("INTEGER".to_owned(), "19".to_owned(), 4, 12, 14),
("EOF".to_owned(), "EOF".to_owned(), 2, 15, 15),
]
);
}
#[test]
fn reserved_words_are_exact_unless_the_table_requests_uppercase() {
let mut lexer = lexer("IF if");
assert_eq!(lexer.next().expect("identifier").yyname(), "ID");
assert_eq!(lexer.next().expect("keyword").yyname(), "IF");
let words = ResWds::from_pairs([("while", token("WHILE", 21))], true).expect("words");
let mut value = TOKEN::new_with_lexer_string(lexer.clone(), "WhIlE".to_owned()).expect("token");
words.check(lexer, &mut value).expect("check");
assert_eq!((value.yyname(), value.yynum()), ("WHILE".to_owned(), 21));
}
#[test]
fn invalid_input_reports_stable_category_location_and_text() {
let mut lexer = lexer("ok @ nope");
assert_eq!(lexer.next().expect("first").yytext(), "ok");
assert_eq!(
lexer.next().expect_err("invalid character"),
Error::Parse {
position: 3,
context: "input does not match any lexer rule"
}
);
let diagnostic = &lexer.diagnostics()[0];
assert_eq!(diagnostic.category, DiagnosticCategory::InvalidCharacter);
assert_eq!(diagnostic.location.line_number, 1);
assert_eq!(diagnostic.location.raw_char_position, 3);
assert_eq!(diagnostic.input, "@");
}
#[test]
fn unknown_start_condition_reports_invalid_state_without_changing_state() {
let mut lexer = lexer("value");
assert_eq!(
lexer.yy_begin("MISSING".to_owned()),
Err(Error::Parse {
position: 0,
context: "unknown lexer start condition"
})
);
assert_eq!(lexer.m_state, "YYINITIAL");
assert_eq!(
lexer.diagnostics()[0].category,
DiagnosticCategory::InvalidState
);
}
#[test]
fn source_reader_removes_both_comment_forms_and_keeps_newlines() {
let mut reader = CsReader::new_with_string("a/*x\ny*/b//z\nc".to_owned()).expect("reader");
assert_eq!(reader.read_line().expect("line"), "a");
assert_eq!(reader.read_line().expect("line"), "b");
assert_eq!(reader.read_line().expect("line"), "c");
assert!(reader.eof().expect("eof"));
}
#[test]
fn source_reader_normalizes_crlf_and_lone_cr() {
let mut reader = CsReader::new_with_string("a\r\nb\rc\n".to_owned()).expect("reader");
assert_eq!(reader.read_line().expect("line"), "a");
assert_eq!(reader.read_line().expect("line"), "b");
assert_eq!(reader.read_line().expect("line"), "c");
}
#[test]
fn source_reader_rejects_unterminated_block_comment_at_eof() {
assert_eq!(
CsReader::new_with_string("before /* never closed".to_owned()).expect_err("invalid"),
Error::Parse {
position: 22,
context: "unterminated block comment"
}
);
}
#[test]
fn line_directive_updates_file_and_logical_line() {
let reader =
CsReader::new_with_string("#line 700 \"table.lex\"\nword\n".to_owned()).expect("reader");
assert_eq!(reader.fname, "table.lex");
let info = SourceLineInfo::new_with_line_manager_int32(reader.lm, 1).expect("location");
assert_eq!(info.line_number, 700);
}
#[test]
fn utf16_input_decoding_and_surrogate_validation_are_deterministic() {
let mut reader = CsReader::from_bytes(
&[0xFF, 0xFE, b'A', 0, 0x3D, 0xD8, 0, 0xDE],
InputEncoding::Utf16Le,
"source.lex",
)
.expect("utf16");
assert_eq!(reader.read_line().expect("line"), "A😀");
let mut bom_override =
CsReader::from_bytes(&[0xFE, 0xFF, 0, b'B'], InputEncoding::Utf16Le, "source.lex")
.expect("BOM override");
assert_eq!(bom_override.read_line().expect("line"), "B");
assert!(matches!(
CsReader::from_bytes(&[0x00, 0xD8], InputEncoding::Utf16Le, "bad"),
Err(Error::Parse {
position: 0,
context: "source contains an unpaired UTF-16 surrogate"
})
));
}
#[test]
fn ascii_input_rejects_non_ascii_instead_of_lossily_replacing_it() {
assert!(matches!(
CsReader::from_bytes(&[b'a', 0x80], InputEncoding::Ascii, "bad"),
Err(Error::Parse {
position: 1,
context: "source contains a non-ASCII byte"
})
));
}
#[test]
fn category_predicates_match_dotnet_values_and_groups() {
let mut table = YyLexer::new(ErrorHandler::default()).expect("table");
let punctuation = table.get_test("Punctuation").expect("punctuation");
let whitespace = table.get_test("WhiteSpace").expect("whitespace");
let states = vec![
DfaState::default()
.transition(punctuation, 1)
.transition(whitespace, 2),
DfaState::default().accepting(accept("PUNCT", 1, LexerAction::Emit, None)),
DfaState::default().accepting(accept("SPACE", 2, LexerAction::Emit, None)),
];
let dfa = Dfa::from_states(states, 0).expect("dfa");
let mut action = -1;
assert_eq!(dfa.match_("".to_owned(), 0, &mut action), Ok(1));
assert_eq!(dfa.match_("\t".to_owned(), 0, &mut action), Ok(1));
assert_eq!(dfa.match_("\0".to_owned(), 0, &mut action), Ok(-1));
assert!(
metacrate_lsl_tools::CatTest::new(UnicodeCategory(16))
.expect("surrogate")
.test(Utf16CodeUnit(0xD800))
.expect("test")
);
assert_eq!(table.get_test("MissingClass"), Err(Error::Argument));
assert_eq!(
table.erh.diagnostics()[0].category,
DiagnosticCategory::UnknownCharacterSet
);
}
#[test]
fn dfa_uses_maximum_munch_and_exposes_action_number() {
let states = vec![
DfaState::default().transition(CharacterMatcher::Exact(b'a'.into()), 1),
DfaState::default()
.transition(CharacterMatcher::Exact(b'b'.into()), 2)
.accepting(accept("A", 1, LexerAction::Emit, None)),
DfaState::default().accepting(accept("AB", 2, LexerAction::Emit, None)),
];
let dfa = Dfa::from_states(states, 0).expect("dfa");
let mut action = -1;
assert_eq!(dfa.match_("abc".to_owned(), 0, &mut action), Ok(2));
assert_eq!(action, 2);
}
#[test]
fn deterministic_table_output_is_independent_of_hash_iteration() {
let table = language_table();
let first = Arc::new(Mutex::new(Vec::new()));
table
.emit_dfa(Box::new(SharedWriter(first.clone())))
.expect("emit first");
let second = Arc::new(Mutex::new(Vec::new()));
table
.emit_dfa(Box::new(SharedWriter(second.clone())))
.expect("emit second");
let first = first.lock().expect("first output").clone();
let second = second.lock().expect("second output").clone();
assert_eq!(first, second);
assert!(
String::from_utf8(first)
.expect("utf8")
.starts_with("LSLLEXER 1\n")
);
}
struct SharedWriter(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for SharedWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.map_err(|_| std::io::Error::other("poisoned output"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn compatibility_enumerator_resets_to_the_first_token() {
let lexer = lexer("one two");
let mut enumerator = lexer.get_enumerator().expect("enumerator");
assert!(enumerator.move_next().expect("first"));
assert_eq!(enumerator.current().yytext(), "one");
assert!(enumerator.move_next().expect("second"));
assert_eq!(enumerator.current().yytext(), "two");
enumerator.reset().expect("reset");
assert!(enumerator.move_next().expect("first again"));
assert_eq!(enumerator.current().yytext(), "one");
}
#[test]
fn error_handler_counts_reports_and_honors_throw_mode() {
let error =
CSToolsException::new_with_int32_string(43, "bad encoding".to_owned()).expect("diagnostic");
let mut collecting = ErrorHandler::new_with_constructor().expect("handler");
collecting.error(error.clone()).expect("reported");
assert_eq!(collecting.counter, 1);
assert_eq!(
collecting.diagnostics()[0].category,
DiagnosticCategory::Encoding
);
let mut throwing = ErrorHandler::new_with_boolean(true).expect("handler");
assert_eq!(throwing.error(error), Err(Error::InvalidOperation));
assert_eq!(throwing.counter, 1);
assert!(throwing.diagnostics().is_empty());
}
#[test]
fn line_manager_backtracking_removes_future_lines() {
let mut manager = LineManager::new().expect("manager");
manager.newline(4).expect("line 2");
let first_on_second =
SourceLineInfo::new_with_line_manager_int32(manager.clone(), 4).expect("location");
assert_eq!(first_on_second.raw_char_position, 1);
manager.newline(8).expect("line 3");
manager.backto(6).expect("rewind");
assert_eq!(manager.lines, 2);
assert_eq!(manager.end, 8);
}
#[test]
fn object_list_preserves_push_add_top_pop_and_index_order() {
let mut values = ObjectList::new().expect("list");
values.add(Object::Integer(2)).expect("add");
values.push(Object::Integer(1)).expect("push");
assert_eq!(values.count(), 2);
assert_eq!(values.top(), Object::Integer(1));
assert_eq!(values.item(1), Object::Integer(2));
assert_eq!(values.pop(), Ok(Object::Integer(1)));
}
#[test]
fn matcher_set_is_value_based_and_bounded() {
let matcher = CharacterMatcher::Set(BTreeSet::from([b'x'.into(), b'y'.into()]));
let states = vec![
DfaState::default().transition(matcher, 1),
DfaState::default().accepting(accept("XY", 1, LexerAction::Emit, None)),
];
let dfa = Dfa::from_states(states, 0).expect("dfa");
let mut action = 0;
assert_eq!(dfa.match_("y".to_owned(), 0, &mut action), Ok(1));
assert_eq!(dfa.match_("z".to_owned(), 0, &mut action), Ok(-1));
}

View File

@@ -0,0 +1,473 @@
//! Focused compatibility fixtures for the native issue #82 parser boundary.
use std::sync::{Arc, Mutex};
use metacrate_lsl_tools::{
Associativity, CSymbol, CSymbolSymType, CharacterMatcher, Dfa, DfaAccept, DfaState,
DiagnosticCategory, Error, ErrorHandler, Grammar, Lexer, LexerAction, LslError, ParseTree,
Parser, ParserEntry, Precedence, PrecedencePrecType, Production, ResWds, SymbolSet,
TokenDefinition, YyLexer,
};
const EOF: i32 = 2;
const ID: i32 = 3;
const INTEGER: i32 = 4;
const PLUS: i32 = 5;
const STAR: i32 = 6;
const MINUS: i32 = 7;
const LPAREN: i32 = 8;
const RPAREN: i32 = 9;
const SEMICOLON: i32 = 10;
const COMMA: i32 = 11;
const STRING: i32 = 12;
const LBRACE: i32 = 13;
const RBRACE: i32 = 14;
const EQUAL: i32 = 15;
const DEFAULT: i32 = 20;
const STATE_ENTRY: i32 = 21;
fn definition(name: &str, number: i32) -> TokenDefinition {
TokenDefinition::new(name, number).expect("valid token")
}
fn accept(name: &str, number: i32, action: LexerAction) -> DfaAccept {
DfaAccept {
token: definition(name, number),
action,
action_number: number,
reserved_words: (number == ID).then(|| "keywords".to_owned()),
}
}
fn parser_lexer(source: &str) -> Lexer {
use metacrate_lsl_tools::DotNetUnicodeCategory as Category;
let letters = vec![
Category::UppercaseLetter,
Category::LowercaseLetter,
Category::TitlecaseLetter,
Category::ModifierLetter,
Category::OtherLetter,
];
let whitespace = vec![
Category::Control,
Category::SpaceSeparator,
Category::LineSeparator,
Category::ParagraphSeparator,
];
let states = vec![
DfaState::default()
.transition(CharacterMatcher::Categories(letters.clone()), 1)
.transition(CharacterMatcher::Exact(b'_'.into()), 1)
.transition(CharacterMatcher::Category(Category::DecimalDigitNumber), 2)
.transition(CharacterMatcher::Categories(whitespace.clone()), 3)
.transition(CharacterMatcher::Exact(b'+'.into()), 4)
.transition(CharacterMatcher::Exact(b'*'.into()), 5)
.transition(CharacterMatcher::Exact(b'-'.into()), 6)
.transition(CharacterMatcher::Exact(b'('.into()), 7)
.transition(CharacterMatcher::Exact(b')'.into()), 8)
.transition(CharacterMatcher::Exact(b';'.into()), 9)
.transition(CharacterMatcher::Exact(b','.into()), 10)
.transition(CharacterMatcher::Exact(b'{'.into()), 11)
.transition(CharacterMatcher::Exact(b'}'.into()), 12)
.transition(CharacterMatcher::Exact(b'"'.into()), 13)
.transition(CharacterMatcher::Exact(b'='.into()), 15),
DfaState::default()
.transition(CharacterMatcher::Categories(letters.clone()), 1)
.transition(CharacterMatcher::Exact(b'_'.into()), 1)
.transition(CharacterMatcher::Category(Category::DecimalDigitNumber), 1)
.accepting(accept("ID", ID, LexerAction::Emit)),
DfaState::default()
.transition(CharacterMatcher::Category(Category::DecimalDigitNumber), 2)
.accepting(accept("INTEGER", INTEGER, LexerAction::Emit)),
DfaState::default()
.transition(CharacterMatcher::Categories(whitespace), 3)
.accepting(accept("WS", 30, LexerAction::Skip)),
DfaState::default().accepting(accept("PLUS", PLUS, LexerAction::Emit)),
DfaState::default().accepting(accept("STAR", STAR, LexerAction::Emit)),
DfaState::default().accepting(accept("MINUS", MINUS, LexerAction::Emit)),
DfaState::default().accepting(accept("LPAREN", LPAREN, LexerAction::Emit)),
DfaState::default().accepting(accept("RPAREN", RPAREN, LexerAction::Emit)),
DfaState::default().accepting(accept("SEMICOLON", SEMICOLON, LexerAction::Emit)),
DfaState::default().accepting(accept("COMMA", COMMA, LexerAction::Emit)),
DfaState::default().accepting(accept("LBRACE", LBRACE, LexerAction::Emit)),
DfaState::default().accepting(accept("RBRACE", RBRACE, LexerAction::Emit)),
DfaState::default()
.transition(CharacterMatcher::Categories(letters), 13)
.transition(CharacterMatcher::Exact(b' '.into()), 13)
.transition(CharacterMatcher::Exact(b'"'.into()), 14),
DfaState::default().accepting(accept("STRING", STRING, LexerAction::Emit)),
DfaState::default().accepting(accept("EQUAL", EQUAL, LexerAction::Emit)),
];
let mut table = YyLexer::new(ErrorHandler::default()).expect("lexer table");
table.using_eof = true;
table
.set_start_dfa("YYINITIAL", Dfa::from_states(states, 0).expect("dfa"))
.expect("start state");
table
.set_reserved_words(
"keywords",
ResWds::from_pairs(
[
("default", definition("DEFAULT", DEFAULT)),
("state_entry", definition("STATE_ENTRY", STATE_ENTRY)),
],
false,
)
.expect("reserved words"),
)
.expect("reserved table");
let mut lexer = Lexer::new(table).expect("lexer");
lexer.start_with_string(source.to_owned()).expect("source");
lexer
}
fn add_common_terminals(grammar: &mut Grammar) {
for (name, number) in [
("EOF", EOF),
("ID", ID),
("INTEGER", INTEGER),
("PLUS", PLUS),
("STAR", STAR),
("MINUS", MINUS),
("LPAREN", LPAREN),
("RPAREN", RPAREN),
("SEMICOLON", SEMICOLON),
("COMMA", COMMA),
("STRING", STRING),
("LBRACE", LBRACE),
("RBRACE", RBRACE),
("EQUAL", EQUAL),
("DEFAULT", DEFAULT),
("STATE_ENTRY", STATE_ENTRY),
("Error", 0),
] {
grammar.add_symbol(name, number, true).expect("terminal");
}
}
fn expression_parser(source: &str) -> Parser {
const EXPRESSION: i32 = 100;
let mut grammar = Grammar::new(EXPRESSION, EOF).expect("grammar");
add_common_terminals(&mut grammar);
grammar
.add_symbol("Expression", EXPRESSION, false)
.expect("nonterminal");
grammar
.set_precedence(PLUS, 1, Associativity::Left)
.expect("plus precedence");
grammar
.set_precedence(STAR, 2, Associativity::Left)
.expect("star precedence");
grammar
.set_precedence(MINUS, 3, Associativity::Right)
.expect("minus precedence");
grammar
.add_production(EXPRESSION, vec![EXPRESSION, PLUS, EXPRESSION])
.expect("plus");
grammar
.add_production(EXPRESSION, vec![EXPRESSION, STAR, EXPRESSION])
.expect("multiply");
grammar
.add_production_with_precedence(EXPRESSION, vec![MINUS, EXPRESSION], Some(MINUS))
.expect("unary");
grammar
.add_production(EXPRESSION, vec![LPAREN, EXPRESSION, RPAREN])
.expect("parentheses");
grammar
.add_production(EXPRESSION, vec![ID])
.expect("identifier");
Parser::new(grammar.build().expect("table"), parser_lexer(source)).expect("parser")
}
fn tree(symbol: &metacrate_lsl_tools::SYMBOL) -> &ParseTree {
symbol
.m_dollar
.downcast_ref::<ParseTree>()
.expect("parse tree")
}
#[test]
fn precedence_and_associativity_choose_the_reference_tree() {
let mut parser = expression_parser("a + b * c + d");
let result = parser
.parse_with_string("a + b * c + d".to_owned())
.expect("parse");
let root = tree(&result);
assert_eq!(root.name, "Expression");
assert_eq!(root.children[1].number, PLUS);
assert_eq!(root.children[0].children[1].number, PLUS);
assert_eq!(root.children[0].children[2].children[1].number, STAR);
assert!(!parser.m_symbols.conflicts().is_empty());
}
#[test]
fn right_associative_unary_precedence_is_deterministic() {
let mut parser = expression_parser("- - a");
let result = parser.parse_with_string("- - a".to_owned()).expect("parse");
let root = tree(&result);
assert_eq!(root.children[0].number, MINUS);
assert_eq!(root.children[1].children[0].number, MINUS);
}
#[test]
fn nonassociative_conflict_rejects_a_chained_operator() {
const EXPRESSION: i32 = 100;
let mut grammar = Grammar::new(EXPRESSION, EOF).expect("grammar");
add_common_terminals(&mut grammar);
grammar
.add_symbol("Expression", EXPRESSION, false)
.expect("nonterminal");
grammar
.set_precedence(PLUS, 1, Associativity::Nonassoc)
.expect("precedence");
grammar
.add_production(EXPRESSION, vec![EXPRESSION, PLUS, EXPRESSION])
.expect("binary");
grammar
.add_production(EXPRESSION, vec![ID])
.expect("identifier");
let mut parser =
Parser::new(grammar.build().expect("table"), parser_lexer("a+a+a")).expect("parser");
assert!(matches!(
parser.parse_with_string("a+a+a".to_owned()),
Err(Error::Parse {
context: "syntax error",
..
})
));
assert_eq!(
parser.diagnostics().expect("diagnostics")[0].category,
DiagnosticCategory::Syntax
);
}
#[test]
fn empty_production_accepts_an_empty_input() {
const START: i32 = 100;
const ITEMS: i32 = 101;
let mut grammar = Grammar::new(START, EOF).expect("grammar");
add_common_terminals(&mut grammar);
grammar.add_symbol("Start", START, false).expect("start");
grammar.add_symbol("Items", ITEMS, false).expect("items");
grammar.add_production(START, vec![ITEMS]).expect("root");
grammar.add_production(ITEMS, vec![]).expect("empty");
let mut parser =
Parser::new(grammar.build().expect("table"), parser_lexer("")).expect("parser");
let result = parser.parse_with_string(String::new()).expect("parse");
assert_eq!(tree(&result).children[0].name, "Items");
}
#[test]
fn lalr_lookaheads_avoid_the_classic_slr_assignment_conflict() {
const START: i32 = 100;
const LEFT: i32 = 101;
const RIGHT: i32 = 102;
let mut grammar = Grammar::new(START, EOF).expect("grammar");
add_common_terminals(&mut grammar);
for (name, number) in [("Start", START), ("Left", LEFT), ("Right", RIGHT)] {
grammar
.add_symbol(name, number, false)
.expect("nonterminal");
}
grammar
.add_production(START, vec![LEFT, EQUAL, RIGHT])
.expect("assignment");
grammar.add_production(START, vec![RIGHT]).expect("value");
grammar
.add_production(LEFT, vec![STAR, RIGHT])
.expect("dereference");
grammar.add_production(LEFT, vec![ID]).expect("name");
grammar.add_production(RIGHT, vec![LEFT]).expect("right");
let table = grammar.build().expect("LALR table");
assert!(table.conflicts().is_empty());
for source in ["a = b", "* a = b"] {
let mut parser = Parser::new(table.clone(), parser_lexer(source)).expect("parser");
let result = parser
.parse_with_string(source.to_owned())
.expect("assignment parse");
assert_eq!(tree(&result).name, "Start");
}
}
#[test]
fn error_token_recovery_discards_input_and_returns_recovered_tree() {
const START: i32 = 100;
const LIST: i32 = 101;
const STATEMENT: i32 = 102;
let mut grammar = Grammar::new(START, EOF).expect("grammar");
add_common_terminals(&mut grammar);
for (name, number) in [("Start", START), ("List", LIST), ("Statement", STATEMENT)] {
grammar
.add_symbol(name, number, false)
.expect("nonterminal");
}
grammar.add_production(START, vec![LIST]).expect("start");
grammar
.add_production(LIST, vec![LIST, STATEMENT])
.expect("list");
grammar
.add_production(LIST, vec![STATEMENT])
.expect("single");
grammar
.add_production(STATEMENT, vec![ID, SEMICOLON])
.expect("statement");
grammar
.add_production(STATEMENT, vec![0, ID, SEMICOLON])
.expect("recovery");
let mut parser =
Parser::new(grammar.build().expect("table"), parser_lexer("a; + b;")).expect("parser");
let result = parser
.parse_with_string("a; + b;".to_owned())
.expect("recovered parse");
assert_eq!(result.yyname(), "recoveredError");
let diagnostics = parser.diagnostics().expect("diagnostics");
assert_eq!(diagnostics[0].category, DiagnosticCategory::Syntax);
assert_eq!(diagnostics[1].category, DiagnosticCategory::ParserRecovery);
}
#[test]
fn representative_lsl_script_parses_to_expected_model() {
const SCRIPT: i32 = 100;
const EVENT: i32 = 101;
const CALL: i32 = 102;
let source = r#"default { state_entry() { llSay(0, "hello"); } }"#;
let mut grammar = Grammar::new(SCRIPT, EOF).expect("grammar");
add_common_terminals(&mut grammar);
for (name, number) in [("Script", SCRIPT), ("Event", EVENT), ("Call", CALL)] {
grammar
.add_symbol(name, number, false)
.expect("nonterminal");
}
grammar
.add_production(SCRIPT, vec![DEFAULT, LBRACE, EVENT, RBRACE])
.expect("script");
grammar
.add_production(
EVENT,
vec![STATE_ENTRY, LPAREN, RPAREN, LBRACE, CALL, SEMICOLON, RBRACE],
)
.expect("event");
grammar
.add_production(CALL, vec![ID, LPAREN, INTEGER, COMMA, STRING, RPAREN])
.expect("call");
let mut parser =
Parser::new(grammar.build().expect("table"), parser_lexer(source)).expect("parser");
let result = parser
.parse_with_string(source.to_owned())
.expect("LSL source");
let root = tree(&result);
assert_eq!(root.name, "Script");
assert_eq!(root.children[2].name, "Event");
assert_eq!(root.children[2].children[4].name, "Call");
assert_eq!(
root.children[2].children[4].children[0].text.as_deref(),
Some("llSay")
);
}
#[test]
fn parser_table_emission_is_byte_deterministic() {
let parser = expression_parser("a");
let first = Arc::new(Mutex::new(Vec::new()));
parser
.m_symbols
.emit(Box::new(SharedWriter(first.clone())))
.expect("first");
let second = Arc::new(Mutex::new(Vec::new()));
parser
.m_symbols
.emit(Box::new(SharedWriter(second.clone())))
.expect("second");
assert_eq!(
first.lock().expect("first output").as_slice(),
second.lock().expect("second output").as_slice()
);
}
struct SharedWriter(Arc<Mutex<Vec<u8>>>);
impl std::io::Write for SharedWriter {
fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
self.0
.lock()
.map_err(|_| std::io::Error::other("poisoned output"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
fn malformed_corpus_terminates_without_panics_or_unbounded_growth() {
for source in ["+", "a +", "((a", "a * * b", "-", "a )", "@"] {
let mut parser = expression_parser(source);
let _ = parser.parse_with_string(source.to_owned());
assert!(parser.diagnostics().expect("diagnostics").len() <= 2);
}
}
#[test]
fn mapped_symbol_set_production_precedence_and_entry_apis_are_live() {
let terminal = CSymbol::native("PLUS", PLUS, CSymbolSymType::Terminal).expect("terminal");
let nonterminal =
CSymbol::native("Expression", 100, CSymbolSymType::Nonterminal).expect("nonterminal");
let mut set = SymbolSet::native();
assert!(set.check_in(terminal.clone()).expect("insert"));
assert!(!set.check_in(terminal.clone()).expect("deduplicate"));
assert!(set.contains(terminal.clone()).expect("contains"));
assert!(terminal.matches("PLUS".to_owned()).expect("symbol text"));
let mut followed = nonterminal.clone();
assert!(
followed
.add_follow(terminal.m_first.clone())
.expect("first follow insertion")
);
assert_eq!(nonterminal.m_follow.count(), 1);
let nullable =
CSymbol::native("Optional", 101, CSymbolSymType::Nonterminal).expect("nullable symbol");
let _empty = Production::native(8, nullable.clone(), vec![]).expect("empty production");
assert!(nullable.is_nullable().expect("registered empty production"));
let mut production = Production::native(7, nonterminal.clone(), vec![]).expect("production");
production.add_to_rhs(nonterminal.clone()).expect("lhs");
production.add_to_rhs(terminal.clone()).expect("operator");
assert_eq!(production.prefix(1).expect("prefix")[0].m_yynum, 100);
let target = CSymbol::native("Target", 102, CSymbolSymType::Nonterminal).expect("target");
production
.add_first(target.clone(), 1)
.expect("FIRST/FOLLOW propagation");
assert!(
target
.m_follow
.contains(terminal.clone())
.expect("shared follow mutation")
);
let precedence = Precedence::first(PrecedencePrecType::Left, 5).expect("precedence");
assert_eq!(
Precedence::check_with_precedence_prec_type_int32(precedence, PrecedencePrecType::Left, 0,),
Ok(5)
);
let parser = expression_parser("a");
let mut lexer = parser.m_lexer.clone();
let symbol = lexer.next_token().expect("lexing").expect("token");
let mut entry = ParserEntry::default();
assert_eq!(
symbol.pass_(parser.m_symbols.clone(), 0, &mut entry),
Ok(true)
);
assert_eq!(entry.str(), "shift 1");
let lsl_error = LslError {
state: 4,
sym: metacrate_lsl_tools::SYMBOL::new_with_lexer(parser.m_lexer.clone()).expect("symbol"),
};
assert!(lsl_error.to_string().contains("state 4"));
}