diff --git a/fuzz/.gitignore b/fuzz/.gitignore new file mode 100644 index 0000000..1a45eee --- /dev/null +++ b/fuzz/.gitignore @@ -0,0 +1,4 @@ +target +corpus +artifacts +coverage diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock new file mode 100644 index 0000000..2448438 --- /dev/null +++ b/fuzz/Cargo.lock @@ -0,0 +1,438 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" + +[[package]] +name = "ariadne" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44055e597c674aef7cb903b2b9f6e4cba1277ed0d2d61dae7cd52d7ffa81f8e2" +dependencies = [ + "unicode-width", + "yansi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cc" +version = "1.2.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom", + "libc", +] + +[[package]] +name = "libc" +version = "0.2.184" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d" +dependencies = [ + "arbitrary", + "cc", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "nescript" +version = "0.1.0" +dependencies = [ + "ariadne", + "clap", + "image", +] + +[[package]] +name = "nescript-fuzz" +version = "0.0.0" +dependencies = [ + "libfuzzer-sys", + "nescript", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml new file mode 100644 index 0000000..af8bedd --- /dev/null +++ b/fuzz/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nescript-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys = "0.4" + +[dependencies.nescript] +path = ".." + +[[bin]] +name = "fuzz_target_1" +path = "fuzz_targets/fuzz_target_1.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_lexer" +path = "fuzz_targets/fuzz_lexer.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_parser" +path = "fuzz_targets/fuzz_parser.rs" +test = false +doc = false +bench = false + +[[bin]] +name = "fuzz_compile" +path = "fuzz_targets/fuzz_compile.rs" +test = false +doc = false +bench = false diff --git a/fuzz/fuzz_targets/fuzz_compile.rs b/fuzz/fuzz_targets/fuzz_compile.rs new file mode 100644 index 0000000..2724992 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_compile.rs @@ -0,0 +1,33 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // The full compilation pipeline must never panic — errors are reported + // via diagnostics, not panics. + if let Ok(source) = std::str::from_utf8(data) { + let (program, parse_diags) = nescript::parser::parse(source); + if parse_diags.iter().any(|d| d.is_error()) { + return; + } + let Some(program) = program else { return }; + + let analysis = nescript::analyzer::analyze(&program); + if analysis.diagnostics.iter().any(|d| d.is_error()) { + return; + } + + // IR lowering + optimization + let mut ir = nescript::ir::lower(&program, &analysis); + nescript::optimizer::optimize(&mut ir); + + // Codegen + assembly + let codegen = + nescript::codegen::CodeGen::new(&analysis.var_allocations, &program.constants); + let instructions = codegen.generate(&program); + + // Linking + let linker = nescript::linker::Linker::new(program.game.mirroring); + let _rom = linker.link(&instructions); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_lexer.rs b/fuzz/fuzz_targets/fuzz_lexer.rs new file mode 100644 index 0000000..53f803f --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_lexer.rs @@ -0,0 +1,10 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // The lexer must never panic on any input — always produce tokens or errors. + if let Ok(source) = std::str::from_utf8(data) { + let _ = nescript::lexer::lex(source); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_parser.rs b/fuzz/fuzz_targets/fuzz_parser.rs new file mode 100644 index 0000000..f4182b6 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_parser.rs @@ -0,0 +1,10 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // The parser must never panic — always produce an AST or diagnostics. + if let Ok(source) = std::str::from_utf8(data) { + let _ = nescript::parser::parse(source); + } +}); diff --git a/fuzz/fuzz_targets/fuzz_target_1.rs b/fuzz/fuzz_targets/fuzz_target_1.rs new file mode 100644 index 0000000..43a88c1 --- /dev/null +++ b/fuzz/fuzz_targets/fuzz_target_1.rs @@ -0,0 +1,7 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; + +fuzz_target!(|data: &[u8]| { + // fuzzed code goes here +}); diff --git a/src/parser/tests.rs b/src/parser/tests.rs index 9505467..4b4992a 100644 --- a/src/parser/tests.rs +++ b/src/parser/tests.rs @@ -678,3 +678,240 @@ fn parse_bank_decl() { assert_eq!(prog.banks[0].name, "Level1Data"); assert_eq!(prog.banks[0].bank_type, BankType::Prg); } + +// ── Edge cases and regression tests ── + +#[test] +fn draw_followed_by_statement() { + // Regression: draw keyword-arg parser used to consume the next statement's + // identifier as a keyword arg, causing "expected ':'" errors. + let src = r#" + game "T" { mapper: NROM } + var i: u8 = 0 + on frame { + draw Sprite at: (0, 0) + i += 1 + } + start Main + "#; + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + assert_eq!(frame.statements.len(), 2); // draw + assignment +} + +#[test] +fn draw_with_frame_followed_by_statement() { + let src = r#" + game "T" { mapper: NROM } + var x: u8 = 0 + on frame { + draw Sprite at: (0, 0) frame: 1 + x = 5 + } + start Main + "#; + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + assert_eq!(frame.statements.len(), 2); +} + +#[test] +fn nested_if_else_chain() { + let src = r#" + game "T" { mapper: NROM } + var x: u8 = 0 + on frame { + if x == 0 { + x = 1 + } else if x == 1 { + x = 2 + } else if x == 2 { + x = 3 + } else { + x = 0 + } + } + start Main + "#; + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + match &frame.statements[0] { + Statement::If(_, _, else_ifs, else_block, _) => { + assert_eq!(else_ifs.len(), 2); + assert!(else_block.is_some()); + } + other => panic!("expected If, got {other:?}"), + } +} + +#[test] +fn deeply_nested_blocks() { + let src = r#" + game "T" { mapper: NROM } + var x: u8 = 0 + on frame { + if x == 0 { + if x == 0 { + if x == 0 { + if x == 0 { + x = 1 + } + } + } + } + } + start Main + "#; + parse_ok(src); // should not stack overflow +} + +#[test] +fn empty_function_body() { + let src = r#" + game "T" { mapper: NROM } + fun noop() {} + on frame { wait_frame } + start Main + "#; + let prog = parse_ok(src); + assert_eq!(prog.functions.len(), 1); + assert!(prog.functions[0].body.statements.is_empty()); +} + +#[test] +fn empty_state_handlers() { + let src = r#" + game "T" { mapper: NROM } + state Main { + on enter {} + on exit {} + on frame {} + } + start Main + "#; + let prog = parse_ok(src); + assert!(prog.states[0] + .on_enter + .as_ref() + .unwrap() + .statements + .is_empty()); + assert!(prog.states[0] + .on_exit + .as_ref() + .unwrap() + .statements + .is_empty()); + assert!(prog.states[0] + .on_frame + .as_ref() + .unwrap() + .statements + .is_empty()); +} + +#[test] +fn button_start_in_condition() { + // "start" is a keyword but valid as a button name + let src = r#" + game "T" { mapper: NROM } + on frame { + if button.start { wait_frame } + } + start Main + "#; + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + match &frame.statements[0] { + Statement::If(Expr::ButtonRead(_, button, _), _, _, _, _) => { + assert_eq!(button, "start"); + } + other => panic!("expected if with button.start, got {other:?}"), + } +} + +#[test] +fn button_select_in_condition() { + let src = r#" + game "T" { mapper: NROM } + on frame { + if button.select { wait_frame } + } + start Main + "#; + parse_ok(src); // "select" is a keyword too — must parse as button name +} + +#[test] +fn multiple_draws_in_sequence() { + let src = r#" + game "T" { mapper: NROM } + var x: u8 = 0 + on frame { + draw A at: (0, 0) + draw B at: (10, 10) + draw C at: (20, 20) + x += 1 + } + start Main + "#; + let prog = parse_ok(src); + let frame = prog.states[0].on_frame.as_ref().unwrap(); + assert_eq!(frame.statements.len(), 4); // 3 draws + 1 assignment +} + +#[test] +fn parser_no_panic_on_garbage() { + // The parser should never panic, only return errors. + let garbage_inputs = [ + "", + "{}{}{}", + "game", + "game \"\"", + "game \"\" {", + "game \"\" { mapper: }", + "var : = 0", + "fun () {}", + "if if if", + "while { }", + "draw", + "draw X", + "draw X at:", + "draw X at: (", + "draw X at: (0", + "draw X at: (0,", + "draw X at: (0, 0", + "sprite {", + "palette { colors: }", + "[[[", + "))))", + "0x 0b", + "\"unterminated", + "/* no block comments */", + "!@#$%", + &"x ".repeat(1000), // long input + ]; + for input in &garbage_inputs { + let _ = parse(input); // must not panic + } +} + +#[test] +fn lexer_no_panic_on_garbage() { + use crate::lexer::lex; + let garbage_inputs = [ + "\0\0\0", + "\x01\x02\x03", + "\"\\", + "0x", + "0b", + "99999999999999999999", + "0xFFFFFFFFFF", + &"a".repeat(10000), + "!!!!!!", + "\t\r\n \t\r\n", + ]; + for input in &garbage_inputs { + let _ = lex(input); // must not panic + } +}