diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 6069359..74cabf6 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -9,9 +9,23 @@ name: Release-plz # the release, creates the GitHub Release(s), and # publishes the changed crates to crates.io. # -# crates.io auth uses OIDC trusted publishing (no stored token) — see the -# `id-token: write` permission on the release job. Configure each crate's -# trusted publisher at https://crates.io/crates//settings once it exists. +# Auth: +# - GitHub: the default GITHUB_TOKEN (no PAT/App, so no org-admin setup). +# Tradeoff: PRs opened by GITHUB_TOKEN do not trigger CI, so the +# release PR won't run the CI workflow. That's acceptable here -- +# it only bumps versions/changelogs, and the feature PRs feeding +# the release already passed CI. To get CI on the release PR, +# swap in a PAT/App token later. +# - crates.io: OIDC trusted publishing (no stored token) -- see the +# `id-token: write` permission on the release job. Configure each +# crate's trusted publisher at crates.io/crates//settings. +# +# NOTE: pinned by version tag (matching release-plz's docs and the other +# actions in this repo). Heads-up for automated edits: a "name@version" ref +# looks like an email address, so some web proxies / AI assistants rewrite it +# into an obfuscated placeholder -- an invalid `uses:` ref that makes GitHub +# reject the whole workflow at startup. Verify the release-plz `uses:` lines +# after editing this file with such tools. on: push: @@ -27,8 +41,9 @@ jobs: name: Release-plz release runs-on: ubuntu-latest permissions: - contents: write # push tags + create GitHub Releases - id-token: write # OIDC: mint a short-lived crates.io token (trusted publishing) + contents: write # push tags + create GitHub Releases + pull-requests: read # link merged PRs in the release notes + id-token: write # OIDC: mint a short-lived crates.io token (trusted publishing) steps: - name: Generate GitHub App token uses: actions/create-github-app-token@v3 @@ -45,7 +60,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run release-plz - uses: release-plz/[email protected] + uses: release-plz/action@v0.5.130 with: command: release env: @@ -82,7 +97,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - name: Run release-plz - uses: release-plz/[email protected] + uses: release-plz/action@v0.5.130 with: command: release-pr env: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4dfbec3..5b6a07f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,7 +52,8 @@ If you do not agree to those terms, please do not open a pull request. - **License headers.** Every source file under `crates/**` carries a two-line SPDX header. Add one to a new file with `mise run license-fix` (or `hawkeye format`); CI enforces it. Line-sensitive trybuild fixtures under - `tests/ui/` and `tests/compile_errors/` are intentionally excluded. + `tests/compile_fail/` and `tests/compile_errors/` are intentionally + excluded. ## Workflow diff --git a/crates/vihaco-cpu/src/component.rs b/crates/vihaco-cpu/src/component.rs index 6df2f66..2010cdd 100644 --- a/crates/vihaco-cpu/src/component.rs +++ b/crates/vihaco-cpu/src/component.rs @@ -8,8 +8,8 @@ use crate::StepOutcome; use crate::data::CPU; use crate::instruction::Instruction; use vihaco::Effects; -use vihaco::value::Value; -use vihaco::{component, frame::Frame, traits::*, value::Type}; +use vihaco::program::{Type, Value}; +use vihaco::{component, frame::Frame, traits::*}; impl Reset for CPU { fn reset(&mut self) { diff --git a/crates/vihaco-cpu/src/data.rs b/crates/vihaco-cpu/src/data.rs index 8007794..774482c 100644 --- a/crates/vihaco-cpu/src/data.rs +++ b/crates/vihaco-cpu/src/data.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: 2026 The vihaco Authors // SPDX-License-Identifier: MIT -use vihaco::value::Value; +use vihaco::program::Value; use vihaco::{ frame::Frame, traits::{FrameMemory, StackFrame, StackMemory}, diff --git a/crates/vihaco-cpu/src/instruction.rs b/crates/vihaco-cpu/src/instruction.rs index df5911e..7d5f907 100644 --- a/crates/vihaco-cpu/src/instruction.rs +++ b/crates/vihaco-cpu/src/instruction.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT use vihaco::Instruction; -use vihaco::value::{Type, Value}; +use vihaco::program::{Type, Value}; /// `#[derive(Parse)]` notes: /// @@ -210,7 +210,7 @@ impl vihaco::CanonicalInstructionSyntax for Instruction { mod parse_tests { use super::Instruction; use chumsky::Parser as _; - use vihaco::value::{Type, Value}; + use vihaco::program::{Type, Value}; use vihaco_parser_core::Parse; fn parse(input: &str) -> Instruction { diff --git a/crates/vihaco-cpu/src/parse_helpers.rs b/crates/vihaco-cpu/src/parse_helpers.rs index c0205c7..9286498 100644 --- a/crates/vihaco-cpu/src/parse_helpers.rs +++ b/crates/vihaco-cpu/src/parse_helpers.rs @@ -21,7 +21,7 @@ use chumsky::error::Simple; use chumsky::extra; use chumsky::prelude::*; -use vihaco::value::{Type, Value}; +use vihaco::program::{Type, Value}; use vihaco_parser_core::Parse; type E<'src> = extra::Err>; diff --git a/crates/vihaco-derive/src/attr_composite.rs b/crates/vihaco-derive/src/attr_composite.rs new file mode 100644 index 0000000..d879f7f --- /dev/null +++ b/crates/vihaco-derive/src/attr_composite.rs @@ -0,0 +1,738 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::{ToTokens, format_ident, quote}; +use std::collections::{BTreeMap, BTreeSet}; +use syn::parse::{Parse, ParseStream}; +use syn::spanned::Spanned; +use syn::{Data, DeriveInput, Fields, GenericParam, Lifetime, LitStr, Token}; + +struct DeviceArgs { + code: u8, + aliases: Vec, +} + +struct SectionLoadArgs { + section_name: Option, +} + +impl Parse for SectionLoadArgs { + fn parse(input: ParseStream) -> syn::Result { + let section_name = if input.is_empty() { + None + } else { + let section_name = input.parse::()?; + if !input.is_empty() { + return Err(input.error("unexpected tokens in loadable attribute")); + } + Some(section_name) + }; + Ok(SectionLoadArgs { section_name }) + } +} + +struct SectionLoadField { + field: syn::Ident, + ty: syn::Type, + section_name: String, +} + +impl Parse for DeviceArgs { + fn parse(input: ParseStream<'_>) -> syn::Result { + let code_lit: syn::LitInt = input.parse()?; + let code = code_lit.base10_parse::()?; + let mut aliases = Vec::new(); + while input.peek(Token![,]) { + input.parse::()?; + if input.is_empty() || !input.peek(syn::Ident) { + break; // trailing comma + } + let ident: syn::Ident = input.parse()?; + input.parse::()?; + match ident.to_string().as_str() { + "alias" => { + aliases.push(input.parse::()?); + } + _ => { + return Err(syn::Error::new_spanned( + ident, + "unsupported device argument", + )); + } + } + } + Ok(Self { code, aliases }) + } +} + +fn pascal_case(ident: &syn::Ident) -> syn::Ident { + let mut out = String::new(); + for part in ident.to_string().split('_') { + if part.is_empty() { + continue; + } + let mut chars = part.chars(); + if let Some(first) = chars.next() { + out.push(first.to_ascii_uppercase()); + out.push_str(chars.as_str()); + } + } + format_ident!("{}", out) +} + +fn validate_loadable_name(name: &str, span: proc_macro2::Span) -> syn::Result<()> { + if name.is_empty() { + return Err(syn::Error::new( + span, + "loadable section name cannot be empty", + )); + } + if name.contains('/') { + return Err(syn::Error::new( + span, + "loadable section name cannot contain `/`", + )); + } + Ok(()) +} + +fn method_where_clause(predicates: &[TokenStream2]) -> TokenStream2 { + if predicates.is_empty() { + quote! {} + } else { + quote! { + where + #( #predicates ),* + } + } +} + +fn stream_contains_ident(stream: TokenStream2, ident: &syn::Ident) -> bool { + stream.into_iter().any(|tree| match tree { + proc_macro2::TokenTree::Ident(found) => found == *ident, + proc_macro2::TokenTree::Group(group) => stream_contains_ident(group.stream(), ident), + _ => false, + }) +} + +fn stream_contains_lifetime(stream: &TokenStream2, lifetime: &Lifetime) -> bool { + stream.to_string().contains(&lifetime.to_string()) +} + +fn generic_param_used(param: &GenericParam, streams: &[TokenStream2]) -> bool { + match param { + GenericParam::Lifetime(param) => streams + .iter() + .any(|stream| stream_contains_lifetime(stream, ¶m.lifetime)), + GenericParam::Type(param) => streams + .iter() + .any(|stream| stream_contains_ident(stream.clone(), ¶m.ident)), + GenericParam::Const(param) => streams + .iter() + .any(|stream| stream_contains_ident(stream.clone(), ¶m.ident)), + } +} + +fn stream_mentions_any_generic(stream: TokenStream2, params: &[GenericParam]) -> bool { + params.iter().any(|param| match param { + GenericParam::Lifetime(param) => stream_contains_lifetime(&stream, ¶m.lifetime), + GenericParam::Type(param) => stream_contains_ident(stream.clone(), ¶m.ident), + GenericParam::Const(param) => stream_contains_ident(stream.clone(), ¶m.ident), + }) +} + +fn enum_generics_for_device_fields( + generics: &syn::Generics, + devices: &[(syn::Ident, syn::Type, DeviceArgs)], +) -> syn::Generics { + let device_streams: Vec<_> = devices + .iter() + .map(|(_, ty, _)| ty.to_token_stream()) + .collect(); + let retained_params: Vec = generics + .params + .iter() + .filter(|param| generic_param_used(param, &device_streams)) + .cloned() + .collect(); + + let mut enum_generics = generics.clone(); + enum_generics.params = retained_params.iter().cloned().collect(); + + if let Some(where_clause) = &mut enum_generics.where_clause { + where_clause.predicates = where_clause + .predicates + .iter() + .filter(|predicate| { + stream_mentions_any_generic(predicate.to_token_stream(), &retained_params) + }) + .cloned() + .collect(); + if where_clause.predicates.is_empty() { + enum_generics.where_clause = None; + } + } + + enum_generics +} + +pub fn expand(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + match try_expand(input) { + Ok(tokens) => tokens.into(), + Err(err) => err.into_compile_error().into(), + } +} + +fn try_expand(input: DeriveInput) -> syn::Result { + let ident = input.ident; + let generics = input.generics; + let data = match input.data { + Data::Struct(data) => data, + _ => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "composite wiring can only be generated for structs", + )); + } + }; + let fields = match data.fields { + Fields::Named(fields) => fields.named, + _ => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "composite wiring requires a struct with named fields", + )); + } + }; + + let mut devices = Vec::new(); + let mut program_field: Option<(syn::Ident, syn::Type)> = None; + let mut header_field: Option<(syn::Ident, syn::Type)> = None; + let mut loadables = Vec::::new(); + + for field in fields { + let field_ident = field.ident.expect("named field"); + let field_ty = field.ty; + let mut is_device = false; + let mut is_program = false; + let mut is_header = false; + let mut loadable_args = None; + for attr in &field.attrs { + let path = attr.path(); + if path.is_ident("program") { + if let Some((existing, _)) = &program_field { + return Err(syn::Error::new( + field_ident.span(), + format!( + "duplicate program field `{}`; `{}` is already marked #[program]", + field_ident, existing + ), + )); + } + is_program = true; + program_field = Some((field_ident.clone(), field_ty.clone())); + } else if path.is_ident("header") { + if !matches!(&attr.meta, syn::Meta::Path(_)) { + return Err(syn::Error::new( + attr.span(), + "header attribute does not take arguments", + )); + } + if let Some((existing, _)) = &header_field { + return Err(syn::Error::new( + field_ident.span(), + format!( + "duplicate header field `{}`; `{}` is already marked #[header]", + field_ident, existing + ), + )); + } + is_header = true; + header_field = Some((field_ident.clone(), field_ty.clone())); + } else if path.is_ident("device") { + is_device = true; + let args = attr.parse_args::()?; + devices.push((field_ident.clone(), field_ty.clone(), args)); + } else if path.is_ident("loadable") { + if loadable_args.is_some() { + return Err(syn::Error::new( + attr.span(), + format!("duplicate loadable attribute on field `{}`", field_ident), + )); + } + loadable_args = Some(if matches!(&attr.meta, syn::Meta::Path(_)) { + SectionLoadArgs { section_name: None } + } else { + attr.parse_args::()? + }); + } + } + if is_header && (is_program || is_device || loadable_args.is_some()) { + return Err(syn::Error::new( + field_ident.span(), + format!( + "field `{}` marked #[header] cannot also be marked #[program], #[device(...)] or #[loadable]", + field_ident + ), + )); + } + if let Some(args) = loadable_args { + if !is_device { + return Err(syn::Error::new( + field_ident.span(), + format!( + "field `{}` marked #[loadable] must also be marked #[device(...)]", + field_ident + ), + )); + } + if is_program { + return Err(syn::Error::new( + field_ident.span(), + format!( + "field `{}` cannot be both #[program] and #[loadable]", + field_ident + ), + )); + } + let section_name = if let Some(lit) = args.section_name { + let value = lit.value(); + validate_loadable_name(&value, lit.span())?; + value + } else { + let value = field_ident.to_string(); + validate_loadable_name(&value, field_ident.span())?; + value + }; + loadables.push(SectionLoadField { + field: field_ident.clone(), + ty: field_ty.clone(), + section_name, + }); + } + } + + let mut seen_device_codes = BTreeMap::::new(); + for (field, _, args) in &devices { + if let Some(existing) = seen_device_codes.insert(args.code, field.clone()) { + return Err(syn::Error::new( + field.span(), + format!( + "duplicate device code 0x{:02X} for fields `{}` and `{}`", + args.code, existing, field + ), + )); + } + } + + let mut seen_source_symbols = BTreeMap::::new(); + for (field, _, args) in &devices { + let field_name = field.to_string(); + if let Some(existing) = seen_source_symbols.insert(field_name.clone(), field.clone()) { + return Err(syn::Error::new( + field.span(), + format!( + "duplicate source symbol `{}` for `{}` and `{}`", + field_name, existing, field + ), + )); + } + + let mut local_aliases = BTreeSet::new(); + for alias in &args.aliases { + let alias_name = alias.value(); + if !local_aliases.insert(alias_name.clone()) { + return Err(syn::Error::new( + alias.span(), + format!("duplicate alias `{}` on field `{}`", alias_name, field), + )); + } + if let Some(existing) = seen_source_symbols.insert(alias_name.clone(), field.clone()) { + return Err(syn::Error::new( + alias.span(), + format!( + "duplicate source symbol `{}` for `{}` and `{}`", + alias_name, existing, field + ), + )); + } + } + } + + let mut seen_loadable_names = BTreeMap::::new(); + for loadable in &loadables { + if let Some(existing) = + seen_loadable_names.insert(loadable.section_name.clone(), loadable.field.clone()) + { + return Err(syn::Error::new( + loadable.field.span(), + format!( + "duplicate loadable section name `{}` for fields `{}` and `{}`", + loadable.section_name, existing, loadable.field + ), + )); + } + } + + let machine_instruction_ident = format_ident!("{}Instruction", ident); + let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); + let enum_generics = enum_generics_for_device_fields(&generics, &devices); + let (_, enum_ty_generics, _) = enum_generics.split_for_impl(); + + let machine_instruction_variants: Vec<_> = devices + .iter() + .map(|(field, field_ty, _)| { + let variant_ident = pascal_case(field); + quote! { + #variant_ident(<#field_ty as ::vihaco::GeneratedComponent>::Instruction) + } + }) + .collect(); + + let device_entries: Vec<_> = devices + .iter() + .map(|(field, _, args)| { + let name = field.to_string(); + let code = args.code; + quote! { ::vihaco::metadata::DeviceMetadata { code: #code, name: #name } } + }) + .collect(); + let source_symbol_alias_entries: Vec<_> = devices + .iter() + .flat_map(|(_, _, args)| { + let code = args.code; + args.aliases.iter().map(move |alias| { + quote! { + ::vihaco::metadata::SourceSymbolAliasMetadata { + name: #alias, + device_code: #code, + } + } + }) + }) + .collect(); + + let program_impl = if let Some((ref field_name, ref field_ty)) = program_field { + quote! { + impl #impl_generics ::vihaco::traits::ProgramCounter for #ident #ty_generics #where_clause { + type Instruction = <#field_ty as ::vihaco::traits::ProgramCounter>::Instruction; + + fn pc(&self) -> u32 { + self.#field_name.pc() + } + + fn pc_mut(&mut self) -> &mut u32 { + self.#field_name.pc_mut() + } + + fn get_instruction(&self, pc: u32) -> eyre::Result<&Self::Instruction> { + self.#field_name.get_instruction(pc) + } + } + } + } else { + quote! {} + }; + + let bc_lifetime = Lifetime::new("'__vihaco_bc", proc_macro2::Span::call_site()); + let loadable_context_param = format_ident!("__VihacoContext"); + let mut loadable_predicates = Vec::::new(); + loadable_predicates.push(quote! { #loadable_context_param: ::vihaco::binary::BytecodeContext }); + if let Some((_, header_ty)) = &header_field { + loadable_predicates.push(quote! { #header_ty: ::vihaco::binary::CompositeHeader }); + } + if let Some((_, program_ty)) = &program_field { + loadable_predicates + .push(quote! { #program_ty: ::vihaco::loader::LoadSection<::std::vec::Vec, #loadable_context_param> }); + } + for loadable in &loadables { + let ty = &loadable.ty; + loadable_predicates + .push(quote! { #ty: ::vihaco::loader::LoadSection<::std::vec::Vec, #loadable_context_param> }); + } + let loadable_method_where = method_where_clause(&loadable_predicates); + + let mut loadable_impl_generics = generics.clone(); + loadable_impl_generics + .params + .push(syn::parse_quote!(#loadable_context_param)); + if !loadable_predicates.is_empty() { + let where_clause = loadable_impl_generics.make_where_clause(); + for predicate in &loadable_predicates { + where_clause + .predicates + .push(syn::parse2(predicate.clone())?); + } + } + let (loadable_impl_generics, _, loadable_where_clause) = + loadable_impl_generics.split_for_impl(); + + let program_load = if let Some((field_name, field_ty)) = &program_field { + quote! { + <#field_ty as ::vihaco::loader::LoadSection<::std::vec::Vec, #loadable_context_param>>::load_section( + &mut self.#field_name, + input.clone(), + )?; + } + } else { + quote! { + if !input.section.bytecode().is_empty() { + return Err(::eyre::eyre!( + "section `{}` has bytecode but `{}` has no #[program] field", + input.section.display_path(), + stringify!(#ident), + )); + } + } + }; + + let header_load = if let Some((field_name, field_ty)) = &header_field { + quote! { + self.#field_name = input.section.parse_header::<#field_ty>()?; + } + } else { + quote! {} + }; + + let loadable_names: Vec<_> = loadables + .iter() + .map(|loadable| loadable.section_name.clone()) + .collect(); + let child_loads: Vec<_> = loadables + .iter() + .map(|loadable| { + let field = &loadable.field; + let ty = &loadable.ty; + let name = &loadable.section_name; + quote! { + if let ::std::option::Option::Some(__vihaco_child) = input.section.child(#name) { + <#ty as ::vihaco::loader::LoadSection<::std::vec::Vec, #loadable_context_param>>::load_section( + &mut self.#field, + ::vihaco::loader::LoadInput::from(__vihaco_child) + )?; + } + } + }) + .collect(); + + let loadable_impl = quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn load_generated_sections<#bc_lifetime, #loadable_context_param>( + &mut self, + input: ::vihaco::loader::LoadInput<#bc_lifetime, ::std::vec::Vec, #loadable_context_param>, + ) -> ::eyre::Result<()> + #loadable_method_where + { + #header_load + #program_load + + let __vihaco_expected_children: &[&str] = &[#(#loadable_names),*]; + + for __vihaco_child in input.section.children() { + let __vihaco_child_name = __vihaco_child.local_name().ok_or_else(|| { + ::eyre::eyre!( + "section `{}` yielded a root section as a child", + input.section.display_path(), + ) + })?; + if !__vihaco_expected_children + .iter() + .any(|__vihaco_expected| *__vihaco_expected == __vihaco_child_name) + { + return Err(::eyre::eyre!( + "section `{}` has unexpected child section `{}`", + input.section.display_path(), + __vihaco_child.display_path(), + )); + } + } + + #( #child_loads )* + Ok(()) + } + } + + impl #loadable_impl_generics ::vihaco::loader::LoadSection<::std::vec::Vec, #loadable_context_param> + for #ident #ty_generics + #loadable_where_clause + { + fn load_section<#bc_lifetime>( + &mut self, + input: ::vihaco::loader::LoadInput<#bc_lifetime, ::std::vec::Vec, #loadable_context_param>, + ) -> ::eyre::Result<()> { + self.load_generated_sections(input) + } + } + }; + + let mut text_loadable_predicates = Vec::::new(); + text_loadable_predicates + .push(quote! { #loadable_context_param: ::vihaco::binary::BytecodeContext }); + if let Some((_, header_ty)) = &header_field { + text_loadable_predicates.push(quote! { #header_ty: ::std::str::FromStr }); + text_loadable_predicates + .push(quote! { <#header_ty as ::std::str::FromStr>::Err: ::std::fmt::Display }); + } + if let Some((_, program_ty)) = &program_field { + text_loadable_predicates + .push(quote! { #program_ty: ::vihaco::loader::LoadSection<::std::string::String, #loadable_context_param> }); + } + for loadable in &loadables { + let ty = &loadable.ty; + text_loadable_predicates + .push(quote! { #ty: ::vihaco::loader::LoadSection<::std::string::String, #loadable_context_param> }); + } + let text_loadable_method_where = method_where_clause(&text_loadable_predicates); + + let mut text_loadable_impl_generics = generics.clone(); + text_loadable_impl_generics + .params + .push(syn::parse_quote!(#loadable_context_param)); + if !text_loadable_predicates.is_empty() { + let where_clause = text_loadable_impl_generics.make_where_clause(); + for predicate in &text_loadable_predicates { + where_clause + .predicates + .push(syn::parse2(predicate.clone())?); + } + } + let (text_loadable_impl_generics, _, text_loadable_where_clause) = + text_loadable_impl_generics.split_for_impl(); + + let text_program_load = if let Some((field_name, field_ty)) = &program_field { + quote! { + <#field_ty as ::vihaco::loader::LoadSection<::std::string::String, #loadable_context_param>>::load_section( + &mut self.#field_name, + input.clone(), + )?; + } + } else { + quote! { + if !input.section.text().trim().is_empty() { + return Err(::eyre::eyre!( + "section `{}` has bytecode but `{}` has no #[program] field", + input.section.display_path(), + stringify!(#ident), + )); + } + } + }; + + let text_header_load = if let Some((field_name, field_ty)) = &header_field { + quote! { + self.#field_name = input + .section + .header_text() + .trim() + .parse::<#field_ty>() + .map_err(|__vihaco_err| { + ::eyre::eyre!( + "failed to parse section `{}` header for `{}`: {}", + input.section.display_path(), + stringify!(#field_ty), + __vihaco_err, + ) + })?; + } + } else { + quote! {} + }; + + let text_child_loads: Vec<_> = loadables + .iter() + .map(|loadable| { + let field = &loadable.field; + let ty = &loadable.ty; + let name = &loadable.section_name; + quote! { + if let ::std::option::Option::Some(__vihaco_child) = input.section.child(#name) { + <#ty as ::vihaco::loader::LoadSection<::std::string::String, #loadable_context_param>>::load_section( + &mut self.#field, + ::vihaco::loader::LoadInput::from(__vihaco_child) + )?; + } + } + }) + .collect(); + + let text_loadable_impl = quote! { + impl #impl_generics #ident #ty_generics #where_clause { + pub fn load_generated_text_sections<#bc_lifetime, #loadable_context_param>( + &mut self, + input: ::vihaco::loader::LoadInput<#bc_lifetime, ::std::string::String, #loadable_context_param>, + ) -> ::eyre::Result<()> + #text_loadable_method_where + { + #text_header_load + #text_program_load + + let __vihaco_expected_children: &[&str] = &[#(#loadable_names),*]; + + for __vihaco_child in input.section.children() { + let __vihaco_child_name = __vihaco_child.local_name().ok_or_else(|| { + ::eyre::eyre!( + "section `{}` yielded a root section as a child", + input.section.display_path(), + ) + })?; + if !__vihaco_expected_children + .iter() + .any(|__vihaco_expected| *__vihaco_expected == __vihaco_child_name) + { + return Err(::eyre::eyre!( + "section `{}` has unexpected child section `{}`", + input.section.display_path(), + __vihaco_child.display_path(), + )); + } + } + + #( #text_child_loads )* + Ok(()) + } + } + + impl #text_loadable_impl_generics ::vihaco::loader::LoadSection<::std::string::String, #loadable_context_param> + for #ident #ty_generics + #text_loadable_where_clause + { + fn load_section<#bc_lifetime>( + &mut self, + input: ::vihaco::loader::LoadInput<#bc_lifetime, ::std::string::String, #loadable_context_param>, + ) -> ::eyre::Result<()> { + self.load_generated_text_sections(input) + } + } + }; + + Ok(quote! { + #[derive(Debug, Clone, ::vihaco::Instruction)] + pub enum #machine_instruction_ident #enum_generics { + #( #machine_instruction_variants ),* + } + + impl #impl_generics ::vihaco::__private::GeneratedMachine for #ident #ty_generics #where_clause { + type Instruction = #machine_instruction_ident #enum_ty_generics; + + fn metadata(&self) -> ::vihaco::CompositeMetadata { + static DEVICES: &[::vihaco::metadata::DeviceMetadata] = &[ + #( #device_entries ),* + ]; + static SOURCE_SYMBOL_ALIASES: &[::vihaco::metadata::SourceSymbolAliasMetadata] = &[ + #( #source_symbol_alias_entries ),* + ]; + ::vihaco::CompositeMetadata { + devices: DEVICES, + source_symbol_aliases: SOURCE_SYMBOL_ALIASES, + } + } + } + + #program_impl + #loadable_impl + #text_loadable_impl + }) +} diff --git a/crates/vihaco-derive/src/derive_instruction.rs b/crates/vihaco-derive/src/derive_instruction.rs index 62877e5..db1ed88 100644 --- a/crates/vihaco-derive/src/derive_instruction.rs +++ b/crates/vihaco-derive/src/derive_instruction.rs @@ -16,6 +16,7 @@ pub fn expand(input: TokenStream) -> TokenStream { } let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); let width_override = match instruction_attrs(&input) { Ok(width) => width, Err(err) => return err.into_compile_error().into(), @@ -115,7 +116,7 @@ pub fn expand(input: TokenStream) -> TokenStream { }; quote! { - impl ::vihaco::instruction::OpCode for #ident { + impl #impl_generics ::vihaco::instruction::OpCode for #ident #ty_generics #where_clause { fn width() -> u32 { #width_impl } @@ -127,7 +128,7 @@ pub fn expand(input: TokenStream) -> TokenStream { } } - impl ::vihaco::instruction::FromBytesWithOpcode for #ident { + impl #impl_generics ::vihaco::instruction::FromBytesWithOpcode for #ident #ty_generics #where_clause { fn from_bytes_with_opcode( bytes: &mut R, opcode: u8, @@ -142,7 +143,7 @@ pub fn expand(input: TokenStream) -> TokenStream { } } - impl ::vihaco::instruction::WriteBytes for #ident { + impl #impl_generics ::vihaco::instruction::WriteBytes for #ident #ty_generics #where_clause { fn write_bytes(&self, io: &mut W) -> ::eyre::Result<()> { let mut payload = Vec::new(); match self { diff --git a/crates/vihaco-derive/src/derive_machine.rs b/crates/vihaco-derive/src/derive_machine.rs deleted file mode 100644 index f4eed8b..0000000 --- a/crates/vihaco-derive/src/derive_machine.rs +++ /dev/null @@ -1,245 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use std::collections::{BTreeMap, BTreeSet}; -use syn::parse::{Parse, ParseStream}; -use syn::{Data, DeriveInput, Fields, Token}; - -struct DeviceArgs { - code: u8, - aliases: Vec, -} - -impl Parse for DeviceArgs { - fn parse(input: ParseStream<'_>) -> syn::Result { - let code_lit: syn::LitInt = input.parse()?; - let code = code_lit.base10_parse::()?; - let mut aliases = Vec::new(); - while input.peek(Token![,]) { - input.parse::()?; - if input.is_empty() || !input.peek(syn::Ident) { - break; // trailing comma - } - let ident: syn::Ident = input.parse()?; - input.parse::()?; - match ident.to_string().as_str() { - "alias" => { - aliases.push(input.parse::()?); - } - _ => { - return Err(syn::Error::new_spanned( - ident, - "unsupported device argument", - )); - } - } - } - Ok(Self { code, aliases }) - } -} - -fn pascal_case(ident: &syn::Ident) -> syn::Ident { - let mut out = String::new(); - for part in ident.to_string().split('_') { - if part.is_empty() { - continue; - } - let mut chars = part.chars(); - if let Some(first) = chars.next() { - out.push(first.to_ascii_uppercase()); - out.push_str(chars.as_str()); - } - } - format_ident!("{}", out) -} - -pub fn expand(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - let ident = input.ident; - let data = match input.data { - Data::Struct(data) => data, - _ => { - return syn::Error::new( - proc_macro2::Span::call_site(), - "composite wiring can only be generated for structs", - ) - .into_compile_error() - .into(); - } - }; - let fields = match data.fields { - Fields::Named(fields) => fields.named, - _ => { - return syn::Error::new( - proc_macro2::Span::call_site(), - "composite wiring requires a struct with named fields", - ) - .into_compile_error() - .into(); - } - }; - - let mut devices = Vec::new(); - let mut program_field: Option<(syn::Ident, syn::Type)> = None; - - for field in fields { - let field_ident = field.ident.expect("named field"); - let field_ty = field.ty; - for attr in &field.attrs { - if attr.path().is_ident("program") { - program_field = Some((field_ident.clone(), field_ty.clone())); - } - } - - for attr in field.attrs { - if attr.path().is_ident("device") { - let args = match attr.parse_args::() { - Ok(args) => args, - Err(err) => return err.into_compile_error().into(), - }; - devices.push((field_ident.clone(), field_ty.clone(), args)); - } - } - } - - let mut seen_device_codes = BTreeMap::::new(); - for (field, _, args) in &devices { - if let Some(existing) = seen_device_codes.insert(args.code, field.clone()) { - return syn::Error::new( - field.span(), - format!( - "duplicate device code 0x{:02X} for fields `{}` and `{}`", - args.code, existing, field - ), - ) - .into_compile_error() - .into(); - } - } - - let mut seen_source_symbols = BTreeMap::::new(); - for (field, _, args) in &devices { - let field_name = field.to_string(); - if let Some(existing) = seen_source_symbols.insert(field_name.clone(), field.clone()) { - return syn::Error::new( - field.span(), - format!( - "duplicate source symbol `{}` for `{}` and `{}`", - field_name, existing, field - ), - ) - .into_compile_error() - .into(); - } - - let mut local_aliases = BTreeSet::new(); - for alias in &args.aliases { - let alias_name = alias.value(); - if !local_aliases.insert(alias_name.clone()) { - return syn::Error::new( - alias.span(), - format!("duplicate alias `{}` on field `{}`", alias_name, field), - ) - .into_compile_error() - .into(); - } - if let Some(existing) = seen_source_symbols.insert(alias_name.clone(), field.clone()) { - return syn::Error::new( - alias.span(), - format!( - "duplicate source symbol `{}` for `{}` and `{}`", - alias_name, existing, field - ), - ) - .into_compile_error() - .into(); - } - } - } - - let machine_instruction_ident = format_ident!("{}Instruction", ident); - - let machine_instruction_variants: Vec<_> = devices - .iter() - .map(|(field, field_ty, _)| { - let variant_ident = pascal_case(field); - quote! { - #variant_ident(<#field_ty as ::vihaco::GeneratedComponent>::Instruction) - } - }) - .collect(); - - let device_entries: Vec<_> = devices - .iter() - .map(|(field, _, args)| { - let name = field.to_string(); - let code = args.code; - quote! { ::vihaco::metadata::DeviceMetadata { code: #code, name: #name } } - }) - .collect(); - let source_symbol_alias_entries: Vec<_> = devices - .iter() - .flat_map(|(_, _, args)| { - let code = args.code; - args.aliases.iter().map(move |alias| { - quote! { - ::vihaco::metadata::SourceSymbolAliasMetadata { - name: #alias, - device_code: #code, - } - } - }) - }) - .collect(); - - let program_impl = if let Some((ref field_name, ref field_ty)) = program_field { - quote! { - impl ::vihaco::traits::ProgramCounter for #ident { - type Instruction = <#field_ty as ::vihaco::traits::ProgramCounter>::Instruction; - - fn pc(&self) -> u32 { - self.#field_name.pc() - } - - fn pc_mut(&mut self) -> &mut u32 { - self.#field_name.pc_mut() - } - - fn get_instruction(&self, pc: u32) -> eyre::Result<&Self::Instruction> { - self.#field_name.get_instruction(pc) - } - } - } - } else { - quote! {} - }; - - quote! { - #[derive(Debug, Clone, ::vihaco::Instruction)] - pub enum #machine_instruction_ident { - #( #machine_instruction_variants ),* - } - - impl ::vihaco::__private::GeneratedMachine for #ident { - type Instruction = #machine_instruction_ident; - - fn metadata(&self) -> ::vihaco::CompositeMetadata { - static DEVICES: &[::vihaco::metadata::DeviceMetadata] = &[ - #( #device_entries ),* - ]; - static SOURCE_SYMBOL_ALIASES: &[::vihaco::metadata::SourceSymbolAliasMetadata] = &[ - #( #source_symbol_alias_entries ),* - ]; - ::vihaco::CompositeMetadata { - devices: DEVICES, - source_symbol_aliases: SOURCE_SYMBOL_ALIASES, - } - } - } - - #program_impl - } - .into() -} diff --git a/crates/vihaco-derive/src/lib.rs b/crates/vihaco-derive/src/lib.rs index 59e7359..81b01f4 100644 --- a/crates/vihaco-derive/src/lib.rs +++ b/crates/vihaco-derive/src/lib.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: MIT mod attr_component; +mod attr_composite; mod attr_observe; mod common; mod derive_instruction; -mod derive_machine; mod derive_message; use proc_macro::TokenStream; @@ -22,15 +22,10 @@ pub fn derive_message(input: TokenStream) -> TokenStream { derive_message::expand(input) } -#[proc_macro_derive(Machine, attributes(device, program))] -pub fn derive_machine(input: TokenStream) -> TokenStream { - derive_machine::expand(input) -} - #[proc_macro_attribute] pub fn composite(_attr: TokenStream, item: TokenStream) -> TokenStream { let original = proc_macro2::TokenStream::from(item.clone()); - let generated = proc_macro2::TokenStream::from(derive_machine::expand(item)); + let generated = proc_macro2::TokenStream::from(attr_composite::expand(item)); let mut sanitized: DeriveInput = match syn::parse2(original) { Ok(input) => input, Err(err) => return err.into_compile_error().into(), @@ -42,7 +37,10 @@ pub fn composite(_attr: TokenStream, item: TokenStream) -> TokenStream { for field in &mut fields.named { field.attrs.retain(|attr| { let path = attr.path(); - !(path.is_ident("device") || path.is_ident("program")) + !(path.is_ident("device") + || path.is_ident("program") + || path.is_ident("loadable") + || path.is_ident("header")) }); } } diff --git a/crates/vihaco/src/binary/common.rs b/crates/vihaco/src/binary/common.rs new file mode 100644 index 0000000..15a9850 --- /dev/null +++ b/crates/vihaco/src/binary/common.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use crate::SectionPath; + +pub(super) fn validate_local_section_name(parent: &SectionPath, child: &str) -> eyre::Result<()> { + if child.is_empty() { + return Err(eyre::eyre!("section `{}` has an empty child name", parent)); + } + if child.contains('/') { + return Err(eyre::eyre!( + "section `{}` child name `{}` must be a local name", + parent, + child + )); + } + Ok(()) +} diff --git a/crates/vihaco/src/binary/context.rs b/crates/vihaco/src/binary/context.rs new file mode 100644 index 0000000..061b2a4 --- /dev/null +++ b/crates/vihaco/src/binary/context.rs @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::sync::Arc; + +use crate::program::ProgramContext; + +/// The global context for a given bytecode file. +/// +/// This should include all context needed for an entire section tree. +/// Anything that should be shared across machines should be in a +/// [`BytecodeContext`]. +pub trait BytecodeContext: Sized { + fn from_bytes(bytes: &[u8]) -> eyre::Result; + + fn from_text(text: &str) -> eyre::Result; + + fn section_name(&self, index: u32) -> Option<&str>; +} + +/// The public handle for a bytecode context. +/// +/// To avoid needing explicit lifetimes permeating throughout +/// machine definitions, we wrap the context in an [`Arc`] to drop it +/// automatically. +#[derive(Debug)] +pub struct ContextHandle(Arc); + +impl Clone for ContextHandle { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +impl ContextHandle { + pub fn new(context: C) -> Self { + Self(Arc::new(context)) + } + + pub fn get(&self) -> &C { + &self.0 + } + + pub fn ptr_eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} + +impl std::ops::Deref for ContextHandle { + type Target = C; + + fn deref(&self) -> &Self::Target { + self.get() + } +} diff --git a/crates/vihaco/src/binary/file.rs b/crates/vihaco/src/binary/file.rs new file mode 100644 index 0000000..b90c44d --- /dev/null +++ b/crates/vihaco/src/binary/file.rs @@ -0,0 +1,197 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::io::Cursor; + +use crate::binary::text::{ + LineCursor, LineKind, TextSectionParseInfo, consume_context, lex_lines, + line_error as text_line_error, parse_section as parse_text_section, verify_version, +}; +use crate::program::ProgramContext; + +use super::{ + context::{BytecodeContext, ContextHandle}, + format::BytecodeHeader, + parser::{SectionParseInfo, checked_add, parse_section}, + section::{SectionNode, SectionPath, SectionView}, +}; + +/// We're sealing [`FileContents`] for two reasons: +/// +/// 1. we don't want downstream authors to implement their own file content +/// support (yet? maybe in the future we can expose an API), and +/// 2. we don't want to use an enum for [`BytecodeFile`] when we know the +/// type of our file's contents statically; when dealing with a +/// [`BytecodeFile`] we'd have to constantly destruct +/// +/// https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed +mod private { + pub trait Sealed {} + + impl Sealed for Vec {} + impl Sealed for String {} +} + +pub trait FileContents: private::Sealed {} + +impl FileContents for Vec {} +impl FileContents for String {} + +/// A parsed bytecode file. +/// +/// This connects the raw bytes of the file with the parsed global context +/// and the tree of section nodes. +#[derive(Debug, Clone)] +pub struct BytecodeFile, C = ProgramContext> +where + F: FileContents, + C: BytecodeContext, +{ + contents: F, + context: ContextHandle, + root: SectionNode, +} + +impl BytecodeFile +where + F: FileContents, + C: BytecodeContext, +{ + pub fn context(&self) -> &C { + self.context.get() + } + + pub fn context_handle(&self) -> ContextHandle { + self.context.clone() + } + + /// The view of the root of the section tree. + pub fn root(&self) -> SectionView<'_, F, C> { + SectionView { + contents: &self.contents, + context: self.context.clone(), + node: &self.root, + } + } +} + +pub type BinaryBytecodeFile = BytecodeFile, C>; + +impl BinaryBytecodeFile +where + C: BytecodeContext, +{ + /// The public entry point for the parsing of a bytecode file. + /// + /// This will automatically split a multi-section file into + /// each individual section node and send it to its corresponding + /// composite loader marked with `#[program]`. + pub fn from_bytes(bytes: Vec) -> eyre::Result> { + let mut cursor = Cursor::new(bytes.as_slice()); + let header = BytecodeHeader::read_from(&mut cursor)?; + + let context_start = BytecodeHeader::ENCODED_LEN; + let context_end = checked_add(context_start, header.context_len, "program context end")?; + let context = C::from_bytes( + bytes + .get(context_start..context_end) + .ok_or_else(|| eyre::eyre!("program context is out of bounds"))?, + )?; + let root = parse_section( + &bytes, + &context, + SectionParseInfo { + start: context_end, + path: SectionPath::root(), + }, + )?; + if root.section.end != bytes.len() { + return Err(eyre::eyre!( + "bytecode length mismatch: root section describes {} bytes, file has {} bytes", + root.section.end, + bytes.len() + )); + } + + Ok(BytecodeFile { + contents: bytes, + context: ContextHandle::new(context), + root, + }) + } +} + +pub type TextBytecodeFile = BytecodeFile; + +impl TextBytecodeFile +where + C: BytecodeContext, +{ + pub fn from_text(text: &str) -> eyre::Result> { + let lines = lex_lines(text)?; + let mut cursor = LineCursor::new(&lines); + + let Some(version) = cursor.next_significant() else { + return Err(eyre::eyre!("expected `vhbc{}`", super::format::VERSION)); + }; + if version.indent != 0 { + return Err(text_line_error( + version, + "version marker must not be indented", + )); + } + match &version.kind { + LineKind::Version(version) => verify_version(*version)?, + _ => { + return Err(text_line_error( + version, + format!("expected `vhbc{}`", super::format::VERSION), + )); + } + } + + let Some(context_begin) = cursor.next_significant() else { + return Err(eyre::eyre!("expected `.global:`")); + }; + if context_begin.kind != LineKind::BeginContext { + return Err(text_line_error(context_begin, "expected `.global:`")); + } + if context_begin.indent != 0 { + return Err(text_line_error( + context_begin, + "context start must not be indented", + )); + } + + let context_start = context_begin.full.end; + let context_end = consume_context(&mut cursor)?; + let context = C::from_text( + text.get(context_start..context_end) + .ok_or_else(|| eyre::eyre!("program context is out of bounds"))?, + )?; + + let Some(section_begin) = cursor.peek_significant() else { + return Err(eyre::eyre!("expected root section")); + }; + let root = parse_text_section( + &mut cursor, + TextSectionParseInfo { + parent: None, + begin: section_begin, + }, + )?; + + if let Some(extra) = cursor.next_significant() { + return Err(text_line_error( + extra, + "unexpected content after root section", + )); + } + + Ok(BytecodeFile { + contents: text.to_string(), + context: ContextHandle::new(context), + root, + }) + } +} diff --git a/crates/vihaco/src/binary/format.rs b/crates/vihaco/src/binary/format.rs new file mode 100644 index 0000000..122f524 --- /dev/null +++ b/crates/vihaco/src/binary/format.rs @@ -0,0 +1,223 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::io::{Cursor, Read}; + +use byteorder::{LittleEndian, ReadBytesExt}; + +use crate::traits::{FromBytes, FromText, Instruction, OpCode, WriteBytes}; + +pub trait CompositeHeader: Sized + FromBytes + FromText + WriteBytes {} + +impl CompositeHeader for T where T: Sized + FromBytes + FromText + WriteBytes {} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct ConstantId(pub u32); + +impl From for ConstantId { + fn from(value: u32) -> Self { + Self(value) + } +} + +impl From for u32 { + fn from(value: ConstantId) -> Self { + value.0 + } +} + +impl FromBytes for ConstantId { + fn from_bytes(bytes: &mut R) -> eyre::Result { + Ok(Self(bytes.read_u32::()?)) + } +} + +impl WriteBytes for ConstantId { + fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { + use byteorder::WriteBytesExt; + + io.write_u32::(self.0)?; + Ok(()) + } +} + +impl OpCode for ConstantId { + fn width() -> u32 { + 4 + } + + fn opcode(&self) -> u8 { + 0 + } +} + +impl std::fmt::Display for ConstantId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "#{}", self.0) + } +} + +/// The vihaco bytecode file header. +/// +/// The file header only frames the shared [`ProgramContext`], with the root section +/// starting immediately after the context. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct BytecodeHeader { + pub(super) context_len: usize, +} + +impl BytecodeHeader { + const MAGIC: &'static [u8; 4] = b"VHBC"; + const VERSION: u16 = 1; + + /// Currently unused in version 1, and we will reject any binary + /// that has flags set + const FLAGS: u16 = 0; + + const CONTEXT_LEN_SIZE: usize = 8; + + pub(super) const ENCODED_LEN: usize = 4 + 2 + 2 + Self::CONTEXT_LEN_SIZE; + + pub(super) fn read_from(reader: &mut R) -> eyre::Result { + let mut magic = [0; 4]; + reader.read_exact(&mut magic)?; + if &magic != Self::MAGIC { + return Err(eyre::eyre!("invalid bytecode magic")); + } + + let version = reader.read_u16::()?; + if version != Self::VERSION { + return Err(eyre::eyre!( + "unsupported bytecode version {} (expected {})", + version, + VERSION + )); + } + + let flags = reader.read_u16::()?; + if flags != Self::FLAGS { + return Err(eyre::eyre!("unsupported bytecode flags 0x{flags:04X}")); + } + + Ok(Self { + context_len: read_usize_u64(reader, "program context length")?, + }) + } +} + +pub const MAGIC: &[u8; 4] = BytecodeHeader::MAGIC; +pub const VERSION: u16 = BytecodeHeader::VERSION; +pub const FLAGS: u16 = BytecodeHeader::FLAGS; + +/// The frame data for a section in a bytecode file. +/// +/// The section length describes the length of the entire section, starting +/// from the beginning of the section frame to the end of the last child +/// section. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct SectionFrame { + pub(super) section_len: usize, + pub(super) composite_header_len: usize, +} + +impl SectionFrame { + pub(super) const SECTION_LEN_SIZE: usize = 8; + pub(super) const HEADER_LEN_SIZE: usize = 8; + + pub(super) const ENCODED_LEN: usize = Self::SECTION_LEN_SIZE + Self::HEADER_LEN_SIZE; + + pub(super) fn read_from(reader: &mut R) -> eyre::Result { + Ok(Self { + section_len: read_usize_u64(reader, "section length")?, + composite_header_len: read_usize_u64(reader, "composite header length")?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct SectionBytecodeHeader { + pub(super) bytecode_len: usize, +} + +impl SectionBytecodeHeader { + pub(super) const BYTECODE_LEN_SIZE: usize = 8; + + pub(super) const ENCODED_LEN: usize = Self::BYTECODE_LEN_SIZE; + + pub(super) fn read_from(reader: &mut R) -> eyre::Result { + Ok(Self { + bytecode_len: read_usize_u64(reader, "bytecode length")?, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ChildSectionTableHeader { + pub(super) child_count: usize, +} + +impl ChildSectionTableHeader { + // Note: this is stored as a `usize` within the `ChildSectionTableHeader` + // struct but will be 4 bytes (a `u32`) in the actual binary. + pub(super) const CHILD_COUNT_SIZE: usize = 4; + + pub(super) const ENCODED_LEN: usize = Self::CHILD_COUNT_SIZE; + + pub(super) fn read_from(reader: &mut R) -> eyre::Result { + Ok(Self { + child_count: reader.read_u32::()? as usize, + }) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) struct ChildSectionTableEntry { + pub(super) local_name_string: u32, + pub(super) section_offset: usize, +} + +impl ChildSectionTableEntry { + pub(super) const LOCAL_NAME_STRING_SIZE: usize = 4; + pub(super) const SECTION_OFFSET_SIZE: usize = 8; + + pub(super) const ENCODED_LEN: usize = Self::LOCAL_NAME_STRING_SIZE + Self::SECTION_OFFSET_SIZE; + + pub(super) fn read_from(reader: &mut R) -> eyre::Result { + Ok(Self { + local_name_string: reader.read_u32::()?, + section_offset: read_usize_u64(reader, "child section offset")?, + }) + } +} + +pub fn decode_instruction_stream(bytes: &[u8]) -> eyre::Result> { + let width = I::width() as usize; + if width == 0 { + if bytes.is_empty() { + return Ok(Vec::new()); + } + return Err(eyre::eyre!( + "cannot decode {} byte(s) into zero-width instructions", + bytes.len() + )); + } + if !bytes.len().is_multiple_of(width) { + return Err(eyre::eyre!( + "bytecode length {} is not a multiple of instruction width {}", + bytes.len(), + width + )); + } + + let mut cursor = Cursor::new(bytes); + let mut code = Vec::with_capacity(bytes.len() / width); + while cursor.position() as usize != bytes.len() { + code.push(I::from_bytes(&mut cursor)?); + } + Ok(code) +} + +fn read_usize_u64(reader: &mut R, label: &str) -> eyre::Result { + usize::try_from(reader.read_u64::()?) + .map_err(|_| eyre::eyre!("{label} does not fit in usize")) +} diff --git a/crates/vihaco/src/binary/mod.rs b/crates/vihaco/src/binary/mod.rs new file mode 100644 index 0000000..65df8a4 --- /dev/null +++ b/crates/vihaco/src/binary/mod.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +mod common; +mod context; +mod file; +mod format; +mod parser; +mod section; +mod text; + +#[cfg(test)] +mod tests; + +pub use context::{BytecodeContext, ContextHandle}; +pub use file::{BinaryBytecodeFile, BytecodeFile, FileContents, TextBytecodeFile}; +pub use format::{CompositeHeader, ConstantId, FLAGS, MAGIC, VERSION, decode_instruction_stream}; +pub use section::{BinarySectionView, SectionPath, SectionView, TextSectionView}; +pub use text::parse_instruction_stream; diff --git a/crates/vihaco/src/binary/parser.rs b/crates/vihaco/src/binary/parser.rs new file mode 100644 index 0000000..c7a7154 --- /dev/null +++ b/crates/vihaco/src/binary/parser.rs @@ -0,0 +1,372 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::{ + collections::BTreeSet, + io::{Cursor, Read}, + ops::Range, +}; + +use crate::binary::common::validate_local_section_name; + +use super::{ + context::BytecodeContext, + format::{ + ChildSectionTableEntry, ChildSectionTableHeader, SectionBytecodeHeader, SectionFrame, + }, + section::{SectionNode, SectionPath}, +}; + +impl SectionFrame { + fn read_at(bytes: &[u8], section_start: usize, path: &SectionPath) -> eyre::Result { + let frame_end = checked_add(section_start, Self::ENCODED_LEN, "section frame end")?; + let frame_bytes = bytes.get(section_start..frame_end).ok_or_else(|| { + eyre::eyre!( + "section `{}` does not contain a complete section frame", + path + ) + })?; + let mut cursor = Cursor::new(frame_bytes); + Self::read_from(&mut cursor) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct SectionParseInfo { + pub(super) start: usize, + pub(super) path: SectionPath, +} + +/// Parse a section of a bytecode file. +/// +/// The vihaco bytecode binary is organized so the structure of +/// the section reads easily from this function. +/// +/// All logic related to the parsing of the current section should +/// come _before_ the parsing logic of its children. +pub(super) fn parse_section( + bytes: &[u8], + context: &C, + info: SectionParseInfo, +) -> eyre::Result +where + C: BytecodeContext, +{ + let SectionParseInfo { + start: section_start, + path, + } = info; + + let frame = SectionFrame::read_at(bytes, section_start, &path)?; + let section_end = checked_add(section_start, frame.section_len, "section end")?; + if section_end > bytes.len() { + return Err(eyre::eyre!( + "section `{}` extends past end of bytecode", + path + )); + } + if frame.composite_header_len > frame.section_len { + return Err(eyre::eyre!( + "section `{}` composite header length {} exceeds section length {}", + path, + frame.composite_header_len, + frame.section_len + )); + } + + let composite_header_start = checked_add( + section_start, + SectionFrame::ENCODED_LEN, + "composite header start", + )?; + let composite_header_end = checked_add( + composite_header_start, + frame.composite_header_len, + "composite header end", + )?; + + // we generate a call to SectionView::parse_header + // if the composite contains a `#[header]` + let composite_header = composite_header_start..composite_header_end; + + let bytecode_header_start = composite_header.end; + if checked_add( + bytecode_header_start, + SectionBytecodeHeader::ENCODED_LEN, + "section bytecode header end", + )? > section_end + { + return Err(eyre::eyre!( + "section `{}` does not contain a complete bytecode header", + path + )); + } + + let mut bytecode_header_cursor = Cursor::new(&bytes[bytecode_header_start..section_end]); + let bytecode_header = SectionBytecodeHeader::read_from(&mut bytecode_header_cursor)?; + let bytecode_start = checked_add( + bytecode_header_start, + SectionBytecodeHeader::ENCODED_LEN, + "bytecode start", + )?; + let bytecode_end = checked_add(bytecode_start, bytecode_header.bytecode_len, "bytecode end")?; + if bytecode_end > section_end { + return Err(eyre::eyre!( + "section `{}` bytecode extends past section end", + path + )); + } + + let child_table_start = bytecode_end; + if checked_add( + child_table_start, + ChildSectionTableHeader::ENCODED_LEN, + "child section table header end", + )? > section_end + { + return Err(eyre::eyre!( + "section `{}` does not contain a complete child section table header", + path + )); + } + + let mut child_table_cursor = Cursor::new(&bytes[child_table_start..section_end]); + let child_table_header = ChildSectionTableHeader::read_from(&mut child_table_cursor)?; + let total_len_of_child_section_entries = child_table_header + .child_count + .checked_mul(ChildSectionTableEntry::ENCODED_LEN) + .ok_or_else(|| eyre::eyre!("section `{}` child table length overflows usize", path))?; + let expected_child_table_len = checked_add( + ChildSectionTableHeader::ENCODED_LEN, + total_len_of_child_section_entries, + "child section table length", + )?; + if checked_add( + child_table_start, + expected_child_table_len, + "child section table end", + )? > section_end + { + return Err(eyre::eyre!( + "section `{}` does not contain a complete child section table", + path + )); + } + + let child_section_entries = read_child_section_table( + &mut child_table_cursor, + child_table_header.child_count, + &path, + context, + )?; + + let child_table_len = child_table_cursor.position() as usize; + let child_table_end = checked_add( + child_table_start, + child_table_len, + "child section table end", + )?; + + let mut claimed_ranges = ClaimedSectionRanges::new(bytecode_start..child_table_end); + let mut children = Vec::with_capacity(child_section_entries.len()); + for child in child_section_entries { + children.push(parse_child_section( + bytes, + context, + ChildSectionParseInfo { + parent_path: &path, + parent_start: section_start, + parent_end: section_end, + parent_child_table_end: child_table_end, + claimed_ranges: &mut claimed_ranges, + child, + }, + )?); + } + + Ok(SectionNode { + path, + section: section_start..section_end, + header: composite_header, + bytecode: bytecode_start..bytecode_end, + children, + }) +} + +/// Represents a [`ChildSectionTableEntry`] with its name; +/// we use this for error reporting. +struct ResolvedChildSectionTableEntry { + name: String, + entry: ChildSectionTableEntry, +} + +fn read_child_section_table( + reader: &mut R, + child_count: usize, + parent_path: &SectionPath, + context: &C, +) -> eyre::Result> +where + R: Read, + C: BytecodeContext, +{ + let mut children = Vec::with_capacity(child_count); + let mut names = BTreeSet::new(); + for _ in 0..child_count { + let entry = ChildSectionTableEntry::read_from(reader)?; + let child_name = context + .section_name(entry.local_name_string) + .ok_or_else(|| { + eyre::eyre!( + "section `{}` references missing section name string {}", + parent_path, + entry.local_name_string + ) + })? + .to_string(); + validate_local_section_name(parent_path, &child_name)?; + if !names.insert(child_name.clone()) { + return Err(eyre::eyre!( + "section `{}` declares duplicate child `{}`", + parent_path, + child_name + )); + } + + children.push(ResolvedChildSectionTableEntry { + name: child_name, + entry, + }); + } + Ok(children) +} + +/// The claimed ranges within a section. +/// +/// This helper struct encapsulates the constraint that no ranges in a section's +/// data should overlap. For example, child data cannot overlap with other child data +/// or its parent's bytecode. +struct ClaimedSectionRanges { + ranges: Vec<(String, Range)>, +} + +impl ClaimedSectionRanges { + fn new(parent_data: Range) -> Self { + Self { + ranges: vec![("parent data".to_string(), parent_data)], + } + } + + fn claim_child( + &mut self, + parent_path: &SectionPath, + child_name: String, + range: Range, + ) -> eyre::Result<()> { + for (existing_name, existing) in &self.ranges { + if ranges_overlap(existing, &range) { + return Err(eyre::eyre!( + "section `{}` child `{}` overlaps `{}`", + parent_path, + child_name, + existing_name + )); + } + } + self.ranges.push((child_name, range)); + Ok(()) + } +} + +struct ChildSectionParseInfo<'a> { + parent_path: &'a SectionPath, + parent_start: usize, + parent_end: usize, + parent_child_table_end: usize, + claimed_ranges: &'a mut ClaimedSectionRanges, + child: ResolvedChildSectionTableEntry, +} + +/// This performs all the logic related to validating a child's bounds against +/// its parent's bounds, then recursively parses the child. +fn parse_child_section( + bytes: &[u8], + context: &C, + info: ChildSectionParseInfo<'_>, +) -> eyre::Result +where + C: BytecodeContext, +{ + let ChildSectionParseInfo { + parent_path, + parent_start, + parent_end, + parent_child_table_end, + claimed_ranges, + child, + } = info; + let ResolvedChildSectionTableEntry { + name: child_name, + entry, + } = child; + + let child_start = checked_add(parent_start, entry.section_offset, "child section start")?; + let child_path = parent_path.child(child_name.clone()); + if child_start < parent_child_table_end { + return Err(eyre::eyre!( + "section `{}` child `{}` begins before parent child table ends", + parent_path, + child_name + )); + } + if child_start >= parent_end { + return Err(eyre::eyre!( + "section `{}` child `{}` extends past section end", + parent_path, + child_name + )); + } + if checked_add( + child_start, + SectionFrame::ENCODED_LEN, + "child section frame end", + )? > parent_end + { + return Err(eyre::eyre!( + "section `{}` child `{}` extends past section end", + parent_path, + child_name + )); + } + + let child_frame = SectionFrame::read_at(bytes, child_start, &child_path)?; + let child_end = checked_add(child_start, child_frame.section_len, "child section end")?; + if child_end > parent_end { + return Err(eyre::eyre!( + "section `{}` child `{}` extends past section end", + parent_path, + child_name + )); + } + + let range = child_start..child_end; + claimed_ranges.claim_child(parent_path, child_name, range)?; + + parse_section( + bytes, + context, + SectionParseInfo { + start: child_start, + path: child_path, + }, + ) +} + +pub(super) fn checked_add(lhs: usize, rhs: usize, label: &str) -> eyre::Result { + lhs.checked_add(rhs) + .ok_or_else(|| eyre::eyre!("{label} overflows usize")) +} + +fn ranges_overlap(lhs: &Range, rhs: &Range) -> bool { + lhs.start < rhs.end && rhs.start < lhs.end +} diff --git a/crates/vihaco/src/binary/section.rs b/crates/vihaco/src/binary/section.rs new file mode 100644 index 0000000..15a606c --- /dev/null +++ b/crates/vihaco/src/binary/section.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::{io::Cursor, ops::Range}; + +use crate::binary::file::FileContents; +use crate::program::ProgramContext; + +use super::{ + context::{BytecodeContext, ContextHandle}, + format::CompositeHeader, +}; + +/// The fully resolved path for a given section. +/// +/// The root section will be empty. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SectionPath { + components: Vec, +} + +impl SectionPath { + pub fn root() -> Self { + Self { + components: Vec::new(), + } + } + + pub fn is_root(&self) -> bool { + self.components.is_empty() + } + + pub fn components(&self) -> &[String] { + &self.components + } + + pub fn local_name(&self) -> Option<&str> { + self.components.last().map(String::as_str) + } + + pub fn child(&self, local_name: impl Into) -> Self { + let mut components = self.components.clone(); + components.push(local_name.into()); + Self { components } + } +} + +impl Default for SectionPath { + fn default() -> Self { + Self::root() + } +} + +impl std::fmt::Display for SectionPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if self.is_root() { + return f.write_str(""); + } + + for (index, component) in self.components.iter().enumerate() { + if index != 0 { + f.write_str("/")?; + } + f.write_str(component)?; + } + Ok(()) + } +} + +impl std::fmt::Debug for SectionPath { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } +} + +/// The internal parser representation of a section. +/// +/// For the public handle of a section, see [`SectionView`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct SectionNode { + pub path: SectionPath, + pub section: Range, + pub header: Range, + pub bytecode: Range, + pub children: Vec, +} + +/// The public handle of a bytecode section. +/// +/// This is a lightweight view into information owned by [`BytecodeFile`]. +pub struct SectionView<'bc, F = Vec, C = ProgramContext> +where + F: FileContents, + C: BytecodeContext, +{ + pub(super) contents: &'bc F, + pub(super) context: ContextHandle, + pub(super) node: &'bc SectionNode, +} + +impl<'bc, F, C> Clone for SectionView<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, +{ + fn clone(&self) -> SectionView<'bc, F, C> { + SectionView { + contents: self.contents, + context: self.context.clone(), + node: self.node, + } + } +} + +impl<'bc, F, C> SectionView<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, +{ + pub fn path(&self) -> &'bc SectionPath { + &self.node.path + } + + pub fn display_path(&self) -> &'bc SectionPath { + &self.node.path + } + + pub fn context_handle(&self) -> ContextHandle { + self.context.clone() + } + + pub fn children(&self) -> impl Iterator> + '_ { + self.node.children.iter().map(|node| SectionView { + contents: self.contents, + context: self.context.clone(), + node, + }) + } + + pub fn child(&self, local_name: &str) -> Option> { + self.node + .children + .iter() + .find(|child| { + child + .path + .local_name() + .is_some_and(|name| name == local_name) + }) + .map(|node| SectionView { + contents: self.contents, + context: self.context.clone(), + node, + }) + } + + pub fn local_name(&self) -> Option<&str> { + self.node.path.local_name() + } +} + +pub type BinarySectionView<'bc, C> = SectionView<'bc, Vec, C>; + +impl<'bc, C> BinarySectionView<'bc, C> +where + C: BytecodeContext, +{ + pub fn header_bytes(&self) -> &'bc [u8] { + &self.contents[self.node.header.clone()] + } + + pub fn bytecode(&self) -> &'bc [u8] { + &self.contents[self.node.bytecode.clone()] + } + + /// Parse the specified composite header from the raw header bytes. + pub fn parse_header(&self) -> eyre::Result { + let bytes = self.header_bytes(); + let mut cursor = Cursor::new(bytes); + let header = H::from_bytes(&mut cursor)?; + if cursor.position() as usize != bytes.len() { + return Err(eyre::eyre!( + "section `{}` header has {} trailing bytes", + self.display_path(), + bytes.len() - cursor.position() as usize + )); + } + Ok(header) + } +} + +impl std::fmt::Debug for BinarySectionView<'_, C> +where + C: BytecodeContext, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SectionView") + .field("path", &self.display_path().to_string()) + .field("header_len", &self.header_bytes().len()) + .field("bytecode_len", &self.bytecode().len()) + .field("child_count", &self.node.children.len()) + .finish() + } +} + +pub type TextSectionView<'bc, C> = SectionView<'bc, String, C>; + +impl<'bc, C> TextSectionView<'bc, C> +where + C: BytecodeContext, +{ + pub fn header_text(&self) -> &'bc str { + &self.contents[self.node.header.clone()] + } + + pub fn text(&self) -> &'bc str { + &self.contents[self.node.bytecode.clone()] + } + + /// Parse the specified composite header from the text format. + pub fn parse_header(&self) -> eyre::Result { + let text = self.header_text(); + let mut cursor = Cursor::new(text); + let header = H::from_text(&mut cursor)?; + if cursor.position() as usize != text.len() { + return Err(eyre::eyre!( + "section `{}` header has {} trailing bytes", + self.display_path(), + text.len() - cursor.position() as usize + )); + } + Ok(header) + } +} + +impl std::fmt::Debug for TextSectionView<'_, C> +where + C: BytecodeContext, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SectionView") + .field("path", &self.display_path().to_string()) + .field("header_len", &self.header_text().len()) + .field("text_len", &self.text().len()) + .field("child_count", &self.node.children.len()) + .finish() + } +} diff --git a/crates/vihaco/src/binary/tests.rs b/crates/vihaco/src/binary/tests.rs new file mode 100644 index 0000000..b97cc7a --- /dev/null +++ b/crates/vihaco/src/binary/tests.rs @@ -0,0 +1,883 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use super::format::{ + ChildSectionTableEntry, ChildSectionTableHeader, SectionBytecodeHeader, SectionFrame, +}; +use super::*; +use crate::binary::file::{BinaryBytecodeFile, TextBytecodeFile}; +use crate::module::{FunctionInfo, LabelInfo, Parameter, Signature, SourceSymbolInfo}; +use crate::program::{ProgramContext, Type, Value}; +use crate::traits::{FromBytes, FromText, WriteBytes}; +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use std::io::Read; + +#[derive(Debug, Clone, PartialEq, crate::Instruction)] +enum TestInst { + Nop, + Load(ConstantId), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CustomValue(u8); + +impl FromBytes for CustomValue { + fn from_bytes(bytes: &mut R) -> eyre::Result { + Ok(Self(bytes.read_u8()?)) + } +} + +impl FromText for CustomValue { + fn from_text(text: &mut R) -> eyre::Result { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + Ok(Self(buffer.trim().parse()?)) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct CustomType(u8); + +impl FromBytes for CustomType { + fn from_bytes(bytes: &mut R) -> eyre::Result { + Ok(Self(bytes.read_u8()?)) + } +} + +impl FromText for CustomType { + fn from_text(text: &mut R) -> eyre::Result { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + Ok(Self(buffer.trim().parse()?)) + } +} + +#[derive(Debug, Clone, PartialEq)] +struct WrappedContext { + inner: ProgramContext, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TextContext { + raw: String, + section_names: Vec, +} + +impl BytecodeContext for WrappedContext { + fn from_bytes(bytes: &[u8]) -> eyre::Result { + Ok(Self { + inner: ProgramContext::from_bytes(bytes)?, + }) + } + + fn section_name(&self, index: u32) -> Option<&str> { + self.inner.section_name(index) + } + + fn from_text(text: &str) -> eyre::Result { + Ok(Self { + inner: ProgramContext::from_text(text)?, + }) + } +} + +impl BytecodeContext for TextContext { + fn from_bytes(bytes: &[u8]) -> eyre::Result { + let raw = std::str::from_utf8(bytes)?.to_string(); + let section_names = raw + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(ToOwned::to_owned) + .collect(); + Ok(Self { raw, section_names }) + } + + fn section_name(&self, index: u32) -> Option<&str> { + self.section_names.get(index as usize).map(String::as_str) + } + + fn from_text(text: &str) -> eyre::Result { + let raw = text.to_string(); + let section_names = raw + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(ToOwned::to_owned) + .collect(); + Ok(Self { raw, section_names }) + } +} + +#[test] +fn parses_binary_context_and_nested_sections() { + const CPU_NAME: u32 = 1; + const ALU_NAME: u32 = 2; + + let context = context_bytes(); + let alu_header = b"alu header"; + let cpu_header = b"cpu header"; + let root_header = b"root header"; + let alu = binary_section_bytes(alu_header, &[TestInst::Nop], vec![]); + let cpu = binary_section_bytes( + cpu_header, + &[TestInst::Load(ConstantId(0))], + vec![(ALU_NAME, alu)], + ); + let root = binary_section_bytes(root_header, &[], vec![(CPU_NAME, cpu)]); + let file = binary_file_bytes(context, root); + + let parsed: BinaryBytecodeFile = BinaryBytecodeFile::from_bytes(file).unwrap(); + + assert_eq!(parsed.context().constants, vec![Value::I64(42)]); + assert_eq!( + parsed.context().strings, + vec!["main".to_string(), "cpu".to_string(), "alu".to_string()] + ); + assert_eq!(parsed.context().main_function, Some(0)); + assert_eq!(parsed.context().file, 7); + + let root = parsed.root(); + assert!(root.path().is_root()); + assert!(root.path().components().is_empty()); + assert_eq!(root.local_name(), None); + assert_eq!(root.display_path().to_string(), ""); + assert_eq!(root.header_bytes(), b"root header"); + + let cpu = root.child("cpu").unwrap(); + assert_eq!(path_components(cpu.path()), vec!["cpu"]); + assert_eq!(cpu.local_name(), Some("cpu")); + assert_eq!(cpu.display_path().to_string(), "cpu"); + assert_eq!(cpu.header_bytes(), b"cpu header"); + assert_eq!( + decode_instruction_stream::(cpu.bytecode()).unwrap(), + vec![TestInst::Load(ConstantId(0))] + ); + + let alu = cpu.child("alu").unwrap(); + assert_eq!(path_components(alu.path()), vec!["cpu", "alu"]); + assert_eq!(alu.local_name(), Some("alu")); + assert_eq!(alu.display_path().to_string(), "cpu/alu"); + assert_eq!(alu.header_bytes(), b"alu header"); + assert_eq!( + decode_instruction_stream::(alu.bytecode()).unwrap(), + vec![TestInst::Nop] + ); +} + +#[test] +fn parses_binary_file_with_custom_context_representation() { + const CPU_NAME: u32 = 1; + + let cpu = binary_section_bytes(b"", &[TestInst::Nop], vec![]); + let root = binary_section_bytes(b"", &[], vec![(CPU_NAME, cpu)]); + let parsed: BinaryBytecodeFile = + BinaryBytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + + assert_eq!(parsed.context().inner.constants, vec![Value::I64(42)]); + assert_eq!( + parsed.root().child("cpu").unwrap().local_name(), + Some("cpu") + ); +} + +#[test] +fn parses_binary_context_with_custom_value_and_type_tables() { + let context = custom_context_bytes(); + let root = binary_section_bytes(b"", &[], vec![]); + let parsed = BinaryBytecodeFile::>::from_bytes( + binary_file_bytes(context, root), + ) + .unwrap(); + + assert_eq!(parsed.context().constants, vec![CustomValue(7)]); + assert_eq!(parsed.context().functions.len(), 1); + assert_eq!( + parsed.context().functions[0].signature.params[0].ty, + CustomType(3) + ); + assert_eq!( + parsed.context().functions[0].signature.ret, + vec![CustomType(4)] + ); +} + +#[test] +fn binary_parse_header_consumes_the_whole_header() { + #[derive(Debug, PartialEq)] + struct Header(u32); + + impl FromBytes for Header { + fn from_bytes(bytes: &mut R) -> eyre::Result { + Ok(Header(bytes.read_u32::()?)) + } + } + + impl FromText for Header { + fn from_text(text: &mut R) -> eyre::Result { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + Ok(Header(buffer.trim().parse()?)) + } + } + + impl WriteBytes for Header { + fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { + io.write_u32::(self.0)?; + Ok(()) + } + } + + let mut header = Vec::new(); + header.write_u32::(99).unwrap(); + let root = binary_section_bytes(&header, &[], vec![]); + let parsed: BinaryBytecodeFile = + BinaryBytecodeFile::from_bytes(binary_file_bytes(empty_context_bytes(), root)).unwrap(); + + assert_eq!(parsed.root().parse_header::
().unwrap(), Header(99)); +} + +#[test] +fn rejects_bad_binary_magic() { + let mut bytes = binary_file_bytes( + empty_context_bytes(), + binary_section_bytes(b"", &[], vec![]), + ); + bytes[0] = b'X'; + + let err = BinaryBytecodeFile::>::from_bytes(bytes).unwrap_err(); + assert!(err.to_string().contains("invalid bytecode magic")); +} + +#[test] +fn rejects_binary_missing_section_name_string() { + let root = raw_binary_section(b"", b"", vec![(0, b"")]); + let err = BinaryBytecodeFile::>::from_bytes(binary_file_bytes( + empty_context_bytes(), + root, + )) + .unwrap_err(); + + assert!(err.to_string().contains("missing section name string")); +} + +#[test] +fn rejects_binary_duplicate_child_names() { + let child_a = binary_section_bytes(b"", &[], vec![]); + let child_b = binary_section_bytes(b"", &[], vec![]); + let root = raw_binary_section(b"", b"", vec![(0, &child_a), (0, &child_b)]); + + let err = BinaryBytecodeFile::>::from_bytes(binary_file_bytes( + context_with_strings(&["cpu"]), + root, + )) + .unwrap_err(); + + assert!(err.to_string().contains("duplicate child")); +} + +#[test] +fn rejects_binary_out_of_bounds_child_section() { + let root = raw_binary_section_with_entry_offsets(b"", b"", vec![(0, 999, b"")]); + + let err = BinaryBytecodeFile::>::from_bytes(binary_file_bytes( + context_with_strings(&["cpu"]), + root, + )) + .unwrap_err(); + + assert!(err.to_string().contains("extends past section end")); +} + +#[test] +fn rejects_binary_overlapping_child_sections() { + let child = binary_section_bytes(b"", &[], vec![]); + let child_offset = (SectionFrame::ENCODED_LEN + + SectionBytecodeHeader::ENCODED_LEN + + ChildSectionTableHeader::ENCODED_LEN + + (2 * ChildSectionTableEntry::ENCODED_LEN)) as u64; + let root = raw_binary_section_with_entry_offsets( + b"", + b"", + vec![(0, child_offset, &child), (1, child_offset, &[])], + ); + + let err = BinaryBytecodeFile::>::from_bytes(binary_file_bytes( + context_with_strings(&["cpu", "gpu"]), + root, + )) + .unwrap_err(); + + assert!(err.to_string().contains("overlaps")); +} + +#[test] +fn rejects_binary_bytecode_that_extends_past_section_end() { + let mut root = Vec::new(); + root.write_u64::( + (SectionFrame::ENCODED_LEN + SectionBytecodeHeader::ENCODED_LEN) as u64, + ) + .unwrap(); + root.write_u64::(0).unwrap(); + root.write_u64::(1).unwrap(); + + let err = BinaryBytecodeFile::>::from_bytes(binary_file_bytes( + empty_context_bytes(), + root, + )) + .unwrap_err(); + + assert!( + err.to_string() + .contains("bytecode extends past section end") + ); +} + +#[test] +fn rejects_binary_instruction_stream_with_non_multiple_width() { + let err = decode_instruction_stream::(&[0, 1, 2]).unwrap_err(); + + assert!(err.to_string().contains("not a multiple")); +} + +#[test] +fn parses_text_context_and_nested_sections() { + let parsed = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n\ +\t.header(root):\n\ +\t\troot header\n\ +\t.header(root).\n\ +\t.text(root):\n\ +\t\troot bytecode\n\ +\t.text(root).\n\ +\t.section(cpu):\n\ +\t\t.header(cpu):\n\ +\t\t\tcpu header\n\ +\t\t.header(cpu).\n\ +\t\t.text(cpu):\n\ +\t\t\tcpu bytecode\n\ +\t\t.text(cpu).\n\ +\t\t.section(alu):\n\ +\t\t\t.header(alu):\n\ +\t\t\t\talu header\n\ +\t\t\t.header(alu).\n\ +\t\t\t.text(alu):\n\ +\t\t\t\talu bytecode\n\ +\t\t\t.text(alu).\n\ +\t\t.section(alu).\n\ +\t.section(cpu).\n\ +.section(root).\n", + )) + .unwrap(); + + assert!(parsed.context().section_names.is_empty()); + + let root = parsed.root(); + assert!(root.path().is_root()); + assert_eq!(root.local_name(), None); + assert_eq!(root.display_path().to_string(), ""); + assert_eq!(root.header_text(), "\t\troot header\n"); + assert_eq!(root.text(), "\t\troot bytecode\n"); + + let cpu = root.child("cpu").unwrap(); + assert_eq!(path_components(cpu.path()), vec!["cpu"]); + assert_eq!(cpu.local_name(), Some("cpu")); + assert_eq!(cpu.display_path().to_string(), "cpu"); + assert_eq!(cpu.header_text(), "\t\t\tcpu header\n"); + assert_eq!(cpu.text(), "\t\t\tcpu bytecode\n"); + + let alu = cpu.child("alu").unwrap(); + assert_eq!(path_components(alu.path()), vec!["cpu", "alu"]); + assert_eq!(alu.local_name(), Some("alu")); + assert_eq!(alu.display_path().to_string(), "cpu/alu"); + assert_eq!(alu.header_text(), "\t\t\t\talu header\n"); + assert_eq!(alu.text(), "\t\t\t\talu bytecode\n"); +} + +#[test] +fn parses_text_section_without_header_or_bytecode_as_empty_ranges() { + let parsed = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n.section(root).\n", + )) + .unwrap(); + + let root = parsed.root(); + assert_eq!(root.header_text(), ""); + assert_eq!(root.text(), ""); +} + +#[test] +fn parses_text_program_context_tables() { + let parsed: TextBytecodeFile = TextBytecodeFile::from_text(&text_file( + ".constants\n\ +i64 42\n\ +bool true\n\ +str 4\n\ +fn 1\n\ +heap 0\n\ +\n\ +.strings\n\ +\"main\"\n\ +\"helper\"\n\ +\"x\"\n\ +\"flag\"\n\ +\"config.json\"\n\ +\"entry\"\n\ +\"loop\"\n\ +\"done\"\n\ +\"cpu\"\n\ +\n\ +.functions\n\ +fn 0 (2: i64, 3: bool) -> i64 2 0 12 4\n\ +fn 1 () -> (bool, i64) 0 12 24 4\n\ +\n\ +.labels\n\ +0 5\n\ +6 6\n\ +12 7\n\ +\n\ +.main 0\n\ +.file 4\n\ +\n\ +.source-symbols\n\ +0 \"cpu\"\n\ +1 \"memory\"\n\ +2 \"timer\"\n", + ".section(root):\n.section(root).\n", + )) + .unwrap(); + + assert_eq!( + parsed.context().constants, + vec![ + Value::I64(42), + Value::Bool(true), + Value::String(4), + Value::FunctionRef(1), + Value::HeapRef(0), + ] + ); + assert_eq!( + parsed.context().strings, + vec![ + "main".to_string(), + "helper".to_string(), + "x".to_string(), + "flag".to_string(), + "config.json".to_string(), + "entry".to_string(), + "loop".to_string(), + "done".to_string(), + "cpu".to_string(), + ] + ); + assert_eq!( + parsed.context().functions, + vec![ + FunctionInfo { + name: 0, + signature: Signature { + params: vec![ + Parameter { + name: 2, + ty: Type::I64, + }, + Parameter { + name: 3, + ty: Type::Bool, + }, + ], + ret: vec![Type::I64], + }, + local_count: 2, + start_address: 0, + end_address: 12, + file: 4, + }, + FunctionInfo { + name: 1, + signature: Signature { + params: vec![], + ret: vec![Type::Bool, Type::I64], + }, + local_count: 0, + start_address: 12, + end_address: 24, + file: 4, + }, + ] + ); + assert_eq!( + parsed.context().labels, + vec![ + LabelInfo { + address: 0, + name: 5, + }, + LabelInfo { + address: 6, + name: 6, + }, + LabelInfo { + address: 12, + name: 7, + }, + ] + ); + assert_eq!(parsed.context().main_function, Some(0)); + assert_eq!(parsed.context().file, 4); + assert_eq!( + parsed.context().source_symbols, + vec![ + SourceSymbolInfo { + index: 0, + name: "cpu".to_string(), + }, + SourceSymbolInfo { + index: 1, + name: "memory".to_string(), + }, + SourceSymbolInfo { + index: 2, + name: "timer".to_string(), + }, + ] + ); +} + +#[test] +fn rejects_text_root_section_with_non_root_name() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(other):\n.section(other).\n", + )) + .unwrap_err(); + + assert!( + err.to_string() + .contains("root section must be named `root`") + ); +} + +#[test] +fn rejects_text_bad_version() { + let err = TextBytecodeFile::::from_text(&format!( + "vhbc{}\n.global:\n.global.\n.section(root):\n.section(root).\n", + VERSION + 1 + )) + .unwrap_err(); + + assert!(err.to_string().contains("unsupported bytecode version")); +} + +#[test] +fn rejects_text_missing_context_end() { + let err = TextBytecodeFile::::from_text( + "vhbc1\n.global:\ncpu\n.section(root):\n.section(root).\n", + ) + .unwrap_err(); + + assert!(err.to_string().contains("unterminated context")); +} + +#[test] +fn rejects_text_non_local_child_section_name() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n\t.section(gpu/core):\n\t.section(gpu/core).\n.section(root).\n", + )) + .unwrap_err(); + + assert!(err.to_string().contains("must be a local name")); +} + +#[test] +fn rejects_text_duplicate_child_sections() { + let err = TextBytecodeFile::::from_text(&text_file( + "cpu\n", + ".section(root):\n\t.section(cpu):\n\t.section(cpu).\n\t.section(cpu):\n\t.section(cpu).\n.section(root).\n", + )) + .unwrap_err(); + + assert!(err.to_string().contains("duplicate child `cpu`")); +} + +#[test] +fn rejects_text_mismatched_section_end_marker() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n.section(other).\n", + )) + .unwrap_err(); + + assert!(err.to_string().contains("mismatched marker `other`")); +} + +#[test] +fn rejects_text_header_marker_with_wrong_section_name() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n\t.header(cpu):\n\t.header(cpu).\n.section(root).\n", + )) + .unwrap_err(); + + assert!( + err.to_string() + .contains("header marker for section `root` uses mismatched name `cpu`") + ); +} + +#[test] +fn rejects_text_bytecode_end_marker_with_wrong_section_name() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n\t.text(root):\n\t.text(cpu).\n.section(root).\n", + )) + .unwrap_err(); + + assert!( + err.to_string() + .contains("bytecode marker for section `root` uses mismatched name `cpu`") + ); +} + +#[test] +fn rejects_text_body_directly_inside_section() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n\tthis line is not in a header or bytecode block\n.section(root).\n", + )) + .unwrap_err(); + + assert!( + err.to_string() + .contains("unexpected content in section ``") + ); +} + +#[test] +fn rejects_text_child_section_indented_with_spaces() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n .section(cpu):\n .section(cpu).\n.section(root).\n", + )) + .unwrap_err(); + + assert!(err.to_string().contains("child section must be indented")); +} + +#[test] +fn rejects_text_header_indented_with_spaces() { + let err = TextBytecodeFile::::from_text(&text_file( + "", + ".section(root):\n .header(root):\n\t\troot header\n .header(root).\n.section(root).\n", + )) + .unwrap_err(); + + assert!(err.to_string().contains("header must be indented")); +} + +fn binary_file_bytes(context: Vec, root: Vec) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(MAGIC); + bytes.write_u16::(VERSION).unwrap(); + bytes.write_u16::(FLAGS).unwrap(); + bytes + .write_u64::(context.len() as u64) + .unwrap(); + bytes.extend_from_slice(&context); + bytes.extend_from_slice(&root); + bytes +} + +fn context_bytes() -> Vec { + let mut bytes = Vec::new(); + + bytes.write_u32::(1).unwrap(); + Value::I64(42).write_bytes(&mut bytes).unwrap(); + + bytes.write_u32::(3).unwrap(); + write_string(&mut bytes, "main"); + write_string(&mut bytes, "cpu"); + write_string(&mut bytes, "alu"); + + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(1).unwrap(); + Type::I64.write_bytes(&mut bytes).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(7).unwrap(); + + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + + bytes.write_u8(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(7).unwrap(); + + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(0).unwrap(); + write_string(&mut bytes, "cpu"); + + bytes +} + +fn custom_context_bytes() -> Vec { + let mut bytes = Vec::new(); + + bytes.write_u32::(1).unwrap(); + bytes.write_u8(7).unwrap(); + + bytes.write_u32::(1).unwrap(); + write_string(&mut bytes, "main"); + + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u8(3).unwrap(); + bytes.write_u32::(1).unwrap(); + bytes.write_u8(4).unwrap(); + bytes.write_u32::(2).unwrap(); + bytes.write_u32::(11).unwrap(); + bytes.write_u32::(22).unwrap(); + bytes.write_u32::(5).unwrap(); + + bytes.write_u32::(0).unwrap(); + bytes.write_u8(1).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(5).unwrap(); + bytes.write_u32::(0).unwrap(); + + bytes +} + +fn empty_context_bytes() -> Vec { + context_with_strings(&[]) +} + +fn context_with_strings(strings: &[&str]) -> Vec { + let mut bytes = Vec::new(); + bytes.write_u32::(0).unwrap(); + bytes + .write_u32::(strings.len() as u32) + .unwrap(); + for string in strings { + write_string(&mut bytes, string); + } + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u8(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes +} + +fn binary_section_bytes( + header: &[u8], + instructions: &[TestInst], + children: Vec<(u32, Vec)>, +) -> Vec { + let mut bytecode = Vec::new(); + for inst in instructions { + inst.write_bytes(&mut bytecode).unwrap(); + } + let children = children + .iter() + .map(|(name_index, bytes)| (*name_index, bytes.as_slice())) + .collect(); + raw_binary_section(header, &bytecode, children) +} + +fn raw_binary_section(header: &[u8], bytecode: &[u8], children: Vec<(u32, &[u8])>) -> Vec { + let child_table_len = + ChildSectionTableHeader::ENCODED_LEN + children.len() * ChildSectionTableEntry::ENCODED_LEN; + let bytecode_start = + SectionFrame::ENCODED_LEN + header.len() + SectionBytecodeHeader::ENCODED_LEN; + let mut child_offset = bytecode_start + bytecode.len() + child_table_len; + let section_len = child_offset + children.iter().map(|(_, child)| child.len()).sum::(); + + let mut bytes = Vec::new(); + bytes.write_u64::(section_len as u64).unwrap(); + bytes + .write_u64::(header.len() as u64) + .unwrap(); + bytes.extend_from_slice(header); + bytes + .write_u64::(bytecode.len() as u64) + .unwrap(); + bytes.extend_from_slice(bytecode); + bytes + .write_u32::(children.len() as u32) + .unwrap(); + for (name_index, child) in &children { + bytes.write_u32::(*name_index).unwrap(); + bytes + .write_u64::(child_offset as u64) + .unwrap(); + child_offset += child.len(); + } + for (_, child) in children { + bytes.extend_from_slice(child); + } + bytes +} + +fn raw_binary_section_with_entry_offsets( + header: &[u8], + bytecode: &[u8], + children: Vec<(u32, u64, &[u8])>, +) -> Vec { + let child_table_len = + ChildSectionTableHeader::ENCODED_LEN + children.len() * ChildSectionTableEntry::ENCODED_LEN; + let bytecode_start = + SectionFrame::ENCODED_LEN + header.len() + SectionBytecodeHeader::ENCODED_LEN; + let section_len = bytecode_start + + bytecode.len() + + child_table_len + + children + .iter() + .map(|(_, _, child)| child.len()) + .sum::(); + + let mut bytes = Vec::new(); + bytes.write_u64::(section_len as u64).unwrap(); + bytes + .write_u64::(header.len() as u64) + .unwrap(); + bytes.extend_from_slice(header); + bytes + .write_u64::(bytecode.len() as u64) + .unwrap(); + bytes.extend_from_slice(bytecode); + bytes + .write_u32::(children.len() as u32) + .unwrap(); + for (name_index, offset, _) in &children { + bytes.write_u32::(*name_index).unwrap(); + bytes.write_u64::(*offset).unwrap(); + } + for (_, _, child) in children { + bytes.extend_from_slice(child); + } + bytes +} + +fn path_components(path: &SectionPath) -> Vec<&str> { + path.components().iter().map(String::as_str).collect() +} + +fn text_file(context: &str, sections: &str) -> String { + format!("vhbc{VERSION}\n\n.global:\n{context}.global.\n\n{sections}") +} + +fn write_string(bytes: &mut Vec, value: &str) { + bytes.write_u32::(value.len() as u32).unwrap(); + bytes.extend_from_slice(value.as_bytes()); +} diff --git a/crates/vihaco/src/binary/text.rs b/crates/vihaco/src/binary/text.rs new file mode 100644 index 0000000..244cbb8 --- /dev/null +++ b/crates/vihaco/src/binary/text.rs @@ -0,0 +1,464 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::{collections::BTreeSet, ops::Range}; + +use chumsky::{error::Simple, extra, prelude::*}; +use eyre::Result; + +use crate::binary::common::validate_local_section_name; + +use super::{ + format::VERSION, + section::{SectionNode, SectionPath}, +}; + +type ParseExtra<'src> = extra::Err>; +const ROOT_SECTION_NAME: &str = "root"; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum LineKind { + Version(u16), + BeginContext, + EndContext, + BeginSection(String), + EndSection(String), + BeginHeader(String), + EndHeader(String), + BeginBytecode(String), + EndBytecode(String), + Body, + Blank, +} + +#[derive(Debug, Clone)] +pub(super) struct SourceLine { + pub(super) kind: LineKind, + pub(super) full: Range, + pub(super) indent: usize, + pub(super) number: usize, +} + +pub(super) fn verify_version(version: u16) -> Result<()> { + if version != VERSION { + return Err(eyre::eyre!( + "unsupported bytecode version {} (expected {})", + version, + VERSION + )); + } + Ok(()) +} + +pub fn parse_instruction_stream<'src, I>(text: &'src str) -> Result> +where + I: crate::traits::Instruction + vihaco_parser_core::Parse<'src>, +{ + use chumsky::IterParser as _; + + if text.trim().is_empty() { + return Ok(Vec::new()); + } + + >::parser() + .padded() + .repeated() + .collect::>() + .then_ignore(end()) + .parse(text) + .into_result() + .map_err(|errors| eyre::eyre!("failed to parse text instruction stream: {:?}", errors)) +} + +fn parse_line(line: &str) -> Result { + line_parser() + .parse(line) + .into_result() + .map_err(format_parse_errors) +} + +fn line_parser<'src>() -> impl Parser<'src, &'src str, LineKind, ParseExtra<'src>> { + let name = any() + .filter(|c: &char| !c.is_whitespace() && !matches!(*c, ':' | '.' | '(' | ')')) + .repeated() + .at_least(1) + .collect::(); + + let version = just("vhbc") + .ignore_then(text::int(10).try_map(|version: &str, span| { + version.parse::().map_err(|_| Simple::new(None, span)) + })) + .map(LineKind::Version); + + let begin_context = just(".global:").to(LineKind::BeginContext); + let end_context = just(".global.").to(LineKind::EndContext); + + let begin_section = just(".section(") + .ignore_then(name) + .then_ignore(just("):")) + .map(LineKind::BeginSection); + let end_section = just(".section(") + .ignore_then(name) + .then_ignore(just(").")) + .map(LineKind::EndSection); + + let begin_header = just(".header(") + .ignore_then(name) + .then_ignore(just("):")) + .map(LineKind::BeginHeader); + let end_header = just(".header(") + .ignore_then(name) + .then_ignore(just(").")) + .map(LineKind::EndHeader); + + let begin_bytecode = just(".text(") + .ignore_then(name) + .then_ignore(just("):")) + .map(LineKind::BeginBytecode); + let end_bytecode = just(".text(") + .ignore_then(name) + .then_ignore(just(").")) + .map(LineKind::EndBytecode); + + let blank = one_of(" \t").repeated().to(LineKind::Blank); + let body = any().repeated().at_least(1).to(LineKind::Body); + + just(' ') + .repeated() + .ignore_then(choice(( + version, + begin_context, + end_context, + begin_section, + end_section, + begin_header, + end_header, + begin_bytecode, + end_bytecode, + body, + blank, + ))) + .then_ignore(end()) +} + +fn format_parse_errors(errors: Vec>) -> eyre::Report { + let error = errors + .into_iter() + .next() + .map(|error| format!("{error:?}")) + .unwrap_or_else(|| "unknown parse error".to_string()); + eyre::eyre!("{error}") +} + +pub(super) fn lex_lines(text: &str) -> Result> { + let mut lines = Vec::new(); + let mut start = 0; + for (index, line) in text.split_inclusive('\n').enumerate() { + let full_end = start + line.len(); + let mut content_end = full_end; + if line.ends_with('\n') { + content_end -= 1; + if text.as_bytes().get(content_end.wrapping_sub(1)) == Some(&b'\r') { + content_end -= 1; + } + } + + let indent = text[start..content_end] + .bytes() + .take_while(|byte| *byte == b'\t') + .count(); + let line_start = start + indent; + lines.push(SourceLine { + kind: parse_line(&text[line_start..content_end]) + .map_err(|err| eyre::eyre!("line {}: {err}", index + 1))?, + full: start..full_end, + indent, + number: index + 1, + }); + start = full_end; + } + + if start < text.len() { + let number = lines.len() + 1; + let indent = text[start..] + .bytes() + .take_while(|byte| *byte == b'\t') + .count(); + let line_start = start + indent; + lines.push(SourceLine { + kind: parse_line(&text[line_start..]) + .map_err(|err| eyre::eyre!("line {}: {err}", number))?, + full: start..text.len(), + indent, + number, + }); + } + + Ok(lines) +} + +pub(super) struct LineCursor<'a> { + lines: &'a [SourceLine], + next: usize, +} + +impl<'a> LineCursor<'a> { + pub(super) fn new(lines: &'a [SourceLine]) -> Self { + Self { lines, next: 0 } + } + + pub(super) fn peek_significant(&self) -> Option<&'a SourceLine> { + self.lines[self.next..] + .iter() + .find(|line| line.kind != LineKind::Blank) + } + + pub(super) fn next_significant(&mut self) -> Option<&'a SourceLine> { + while let Some(line) = self.lines.get(self.next) { + self.next += 1; + if line.kind != LineKind::Blank { + return Some(line); + } + } + None + } +} + +pub(super) fn consume_context(cursor: &mut LineCursor<'_>) -> Result { + while let Some(line) = cursor.next_significant() { + if line.kind == LineKind::EndContext { + ensure_indent(line, 0, "context end")?; + return Ok(line.full.start); + } + } + + Err(eyre::eyre!("unterminated context; expected `.global.`")) +} + +pub(super) struct TextSectionParseInfo<'a> { + pub(super) parent: Option>, + pub(super) begin: &'a SourceLine, +} + +#[derive(Clone, Copy)] +pub(super) struct ParentSection<'a> { + pub(super) path: &'a SectionPath, + pub(super) indent: usize, +} + +pub(super) fn parse_section( + cursor: &mut LineCursor<'_>, + info: TextSectionParseInfo<'_>, +) -> Result { + let TextSectionParseInfo { parent, begin } = info; + let section_name = match &begin.kind { + LineKind::BeginSection(name) => name.clone(), + _ => return Err(line_error(begin, "expected section")), + }; + + let consumed_begin = cursor + .next_significant() + .ok_or_else(|| eyre::eyre!("expected section"))?; + debug_assert_eq!(consumed_begin.full, begin.full); + + let section_indent = begin.indent; + match parent { + Some(parent) => ensure_indent(begin, parent.indent + 1, "child section")?, + None => { + ensure_indent(begin, 0, "root section")?; + if section_name != ROOT_SECTION_NAME { + return Err(line_error( + begin, + format!("root section must be named `{ROOT_SECTION_NAME}`"), + )); + } + } + } + + let path = match parent { + Some(parent) => { + validate_local_section_name(parent.path, §ion_name)?; + parent.path.child(section_name.clone()) + } + None => SectionPath::root(), + }; + + let mut header = None; + let mut bytecode = None; + let mut children = Vec::new(); + let mut child_names = BTreeSet::new(); + + loop { + let Some(line) = cursor.peek_significant() else { + return Err(line_error(begin, "unterminated section")); + }; + + match &line.kind { + LineKind::EndSection(name) => { + ensure_indent(line, section_indent, "section end")?; + if name != §ion_name { + return Err(line_error( + line, + format!( + "section `{}` ended with mismatched marker `{}`", + section_name, name + ), + )); + } + let end = cursor.next_significant().expect("peeked line must exist"); + let fallback = end.full.start..end.full.start; + return Ok(SectionNode { + path, + section: begin.full.start..end.full.end, + header: header.unwrap_or_else(|| fallback.clone()), + bytecode: bytecode.unwrap_or(fallback), + children, + }); + } + LineKind::BeginHeader(name) => { + ensure_indent(line, section_indent + 1, "header")?; + ensure_block_marker_name(line, "header", name, §ion_name)?; + if header.is_some() { + return Err(line_error( + line, + format!("section `{}` declares duplicate header", path), + )); + } + header = Some(consume_named_block( + cursor, + §ion_name, + "header", + section_indent + 1, + )?); + } + LineKind::BeginBytecode(name) => { + ensure_indent(line, section_indent + 1, "bytecode")?; + ensure_block_marker_name(line, "bytecode", name, §ion_name)?; + if bytecode.is_some() { + return Err(line_error( + line, + format!("section `{}` declares duplicate bytecode", path), + )); + } + bytecode = Some(consume_named_block( + cursor, + §ion_name, + "bytecode", + section_indent + 1, + )?); + } + LineKind::BeginSection(child_name) => { + ensure_indent(line, section_indent + 1, "child section")?; + validate_local_section_name(&path, child_name)?; + if !child_names.insert(child_name.clone()) { + return Err(line_error( + line, + format!( + "section `{}` declares duplicate child `{}`", + path, child_name + ), + )); + } + let child = parse_section( + cursor, + TextSectionParseInfo { + parent: Some(ParentSection { + path: &path, + indent: section_indent, + }), + begin: line, + }, + )?; + children.push(child); + } + LineKind::Blank => { + cursor.next_significant(); + } + _ => { + return Err(line_error( + line, + format!("unexpected content in section `{}`", path), + )); + } + } + } +} + +fn consume_named_block( + cursor: &mut LineCursor<'_>, + section_name: &str, + label: &str, + block_indent: usize, +) -> Result> { + let begin = cursor + .next_significant() + .ok_or_else(|| eyre::eyre!("expected `{label}` block in section `{section_name}`"))?; + let start = begin.full.end; + + while let Some(line) = cursor.next_significant() { + if let Some(marker_name) = block_end_marker_name(&line.kind, label) { + ensure_indent(line, block_indent, label)?; + ensure_block_marker_name(line, label, marker_name, section_name)?; + return Ok(start..line.full.start); + } + } + + Err(line_error( + begin, + format!( + "unterminated {label}; expected `{}`", + end_marker(label, section_name) + ), + )) +} + +fn block_end_marker_name<'a>(kind: &'a LineKind, label: &str) -> Option<&'a str> { + match (label, kind) { + ("header", LineKind::EndHeader(name)) | ("bytecode", LineKind::EndBytecode(name)) => { + Some(name) + } + _ => None, + } +} + +fn ensure_block_marker_name( + line: &SourceLine, + label: &str, + marker_name: &str, + section_name: &str, +) -> Result<()> { + if marker_name != section_name { + return Err(line_error( + line, + format!( + "{label} marker for section `{section_name}` uses mismatched name `{marker_name}`" + ), + )); + } + Ok(()) +} + +fn ensure_indent(line: &SourceLine, expected: usize, label: &str) -> Result<()> { + if line.indent != expected { + return Err(line_error( + line, + format!( + "{label} must be indented with {expected} tab(s), found {}", + line.indent + ), + )); + } + Ok(()) +} + +fn end_marker(label: &str, section_name: &str) -> String { + match label { + "header" => format!(".header({section_name})."), + "bytecode" => format!(".text({section_name})."), + _ => "end marker".to_string(), + } +} + +pub(super) fn line_error(line: &SourceLine, message: impl std::fmt::Display) -> eyre::Report { + eyre::eyre!("line {}: {}", line.number, message) +} diff --git a/crates/vihaco/src/lib.rs b/crates/vihaco/src/lib.rs index e1aad9f..8cf47cd 100644 --- a/crates/vihaco/src/lib.rs +++ b/crates/vihaco/src/lib.rs @@ -5,6 +5,7 @@ extern crate self as vihaco; #[doc(hidden)] pub mod __private; +pub mod binary; pub mod color; pub mod effect; pub mod frame; @@ -16,31 +17,41 @@ pub mod macros; pub mod metadata; pub mod module; pub mod observer; +pub mod program; pub mod runtime; pub mod syntax; #[doc(hidden)] pub mod traits; -pub mod value; +pub mod value { + pub use crate::program::{Type, Value}; +} +pub use binary::{ + BinaryBytecodeFile, BinarySectionView, BytecodeContext, BytecodeFile, CompositeHeader, + ConstantId, ContextHandle, SectionPath, SectionView, TextBytecodeFile, TextSectionView, +}; pub use effect::Effects; pub use instruction_syntax::{ CanonicalInstructionSyntax, CanonicalInstructionVariantSyntax, InstructionSugarSyntax, InstructionSugarVariantSyntax, OperandKind, SugarOperandKind, }; -pub use loader::ProgramLoader; +pub use loader::{LoadInput, LoadSection, ModuleProgramLoader, ProgramLoader}; pub use macros::{Instruction, Message, component, composite, observe}; +pub use program::{ProgramContext, ProgramGlobals, Type, Value}; pub use runtime::{ CompositeMetadata, EffectSink, GeneratedComponent, Message as MessageMarker, Observe, expect_exactly_one_effect, }; -pub use traits::Reset; -pub use value::{Type, Value}; +pub use traits::{GetProgramGlobal, Reset}; #[cfg(test)] mod public_api_tests { use crate::{ - EffectSink, Effects, GeneratedComponent, Reset, + BytecodeContext, EffectSink, Effects, GeneratedComponent, LoadSection, ProgramGlobals, + Reset, + binary::ConstantId, instruction::{FromBytes, OpCode, WriteBytes}, + module::FunctionInfo, observer::stdio::StdoutEffect, }; @@ -55,12 +66,20 @@ mod public_api_tests { fn require_effect_sink>() {} fn require_reset() {} fn require_instruction() {} + fn require_bytecode_context() {} + fn require_program_globals() {} + fn require_load_section() {} fn require_stdout_effect(_effect: StdoutEffect) {} fn require_metadata(_metadata: crate::CompositeMetadata) {} require_effect_sink::>(); require_reset::(); require_instruction::(); + require_bytecode_context::(); + require_program_globals::(); + require_load_section::>(); + let _constant = ConstantId(0); + let _function: Option> = None; require_stdout_effect(StdoutEffect(String::new())); require_metadata(crate::CompositeMetadata { devices: &[], diff --git a/crates/vihaco/src/loader.rs b/crates/vihaco/src/loader.rs index 4ab1416..cf824ce 100644 --- a/crates/vihaco/src/loader.rs +++ b/crates/vihaco/src/loader.rs @@ -2,18 +2,92 @@ // SPDX-License-Identifier: MIT use crate::{ + BytecodeContext, BytecodeFile, + binary::{ + ConstantId, ContextHandle, FileContents, SectionView, decode_instruction_stream, + parse_instruction_stream, + }, module::{Module, NoInfo}, + program::{ProgramContext, ProgramGlobals, Type, Value}, traits::{self, GetProgramGlobal, ProgramCounter}, - value::{Type, Value}, }; +/// The input given to a specific loadable machine. +pub struct LoadInput<'bc, F = Vec, C = ProgramContext> +where + F: FileContents, + C: BytecodeContext, +{ + pub section: SectionView<'bc, F, C>, +} + +impl<'bc, F, C> Clone for LoadInput<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, +{ + fn clone(&self) -> Self { + LoadInput { + section: self.section.clone(), + } + } +} + +impl<'bc, F, C> From<&'bc BytecodeFile> for LoadInput<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, +{ + fn from(file: &'bc BytecodeFile) -> Self { + Self { + section: file.root(), + } + } +} + +impl<'bc, F, C> From> for LoadInput<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, +{ + fn from(section: SectionView<'bc, F, C>) -> Self { + Self { section } + } +} + +impl<'bc, F, C> std::fmt::Debug for LoadInput<'bc, F, C> +where + F: FileContents, + C: BytecodeContext, + SectionView<'bc, F, C>: std::fmt::Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LoadInput") + .field("section", &self.section) + .finish_non_exhaustive() + } +} + +/// Allow a machine to load a section. +/// +/// When used with the [`vihaco_derive::composite`] macro, any field marked +/// `#[program]` will automatically have the section routed to it; +/// that field should implement the logic for loading a section. +pub trait LoadSection, C = ProgramContext> +where + F: FileContents, + C: BytecodeContext, +{ + fn load_section<'bc>(&mut self, input: LoadInput<'bc, F, C>) -> eyre::Result<()>; +} + #[derive(Debug, Clone)] -pub struct ProgramLoader { - pub module: Module, +pub struct ModuleProgramLoader { + pub module: Module, pub pc: u32, } -impl Default for ProgramLoader { +impl Default for ModuleProgramLoader { fn default() -> Self { Self { module: Module::default(), @@ -22,7 +96,7 @@ impl Default for ProgramLoader { } } -impl ProgramCounter for ProgramLoader { +impl ProgramCounter for ModuleProgramLoader { type Instruction = I; fn pc(&self) -> u32 { @@ -44,8 +118,12 @@ impl ProgramCounter for ProgramLoader { } } -impl GetProgramGlobal for ProgramLoader { - type Type = Type; +impl GetProgramGlobal for ModuleProgramLoader +where + Ty: Clone, +{ + type Type = Ty; + type Value = V; fn get_function(&self, index: usize) -> eyre::Result> { self.module.functions.get(index).cloned().ok_or_else(|| { @@ -66,4 +144,127 @@ impl GetProgramGlobal for ProgramLoader { )) }) } + + fn get_constant(&self, id: ConstantId) -> eyre::Result<&Self::Value> { + self.module.constants.get(id.0 as usize).ok_or_else(|| { + eyre::eyre!(format!( + "constant index out of bounds: {} (max {})", + id.0, + self.module.constants.len() + )) + }) + } +} + +#[derive(Debug, Clone)] +pub struct ProgramLoader { + pub code: Vec, + pub context: Option>, + pub pc: u32, + pub extra: Info, +} + +impl Default for ProgramLoader { + fn default() -> Self { + Self { + code: Vec::new(), + context: None, + pc: 0, + extra: Info::default(), + } + } +} + +impl ProgramLoader { + pub fn new() -> Self + where + Info: Default, + { + Self::default() + } + + pub fn with_extra(extra: Info) -> Self { + Self { + code: Vec::new(), + context: None, + pc: 0, + extra, + } + } + + pub fn context(&self) -> eyre::Result<&C> { + self.context + .as_ref() + .map(ContextHandle::get) + .ok_or_else(|| eyre::eyre!("bytecode program loader has not been loaded")) + } +} + +impl LoadSection, C> for ProgramLoader +where + I: traits::Instruction, + C: BytecodeContext, +{ + fn load_section<'bc>(&mut self, input: LoadInput<'bc, Vec, C>) -> eyre::Result<()> { + self.code = decode_instruction_stream(input.section.bytecode())?; + self.context = Some(input.section.context_handle()); + self.pc = 0; + Ok(()) + } +} + +impl LoadSection for ProgramLoader +where + I: traits::Instruction, + C: BytecodeContext, + for<'src> I: vihaco_parser_core::Parse<'src>, +{ + fn load_section<'bc>(&mut self, input: LoadInput<'bc, String, C>) -> eyre::Result<()> { + self.code = parse_instruction_stream(input.section.text())?; + self.context = Some(input.section.context_handle()); + self.pc = 0; + Ok(()) + } +} + +impl ProgramCounter for ProgramLoader { + type Instruction = I; + + fn pc(&self) -> u32 { + self.pc + } + + fn pc_mut(&mut self) -> &mut u32 { + &mut self.pc + } + + fn get_instruction(&self, pc: u32) -> eyre::Result<&Self::Instruction> { + self.code.get(pc as usize).ok_or_else(|| { + eyre::eyre!(format!( + "program counter out of bounds: {} (max {})", + pc, + self.code.len() + )) + }) + } +} + +impl GetProgramGlobal for ProgramLoader +where + C: ProgramGlobals, +{ + type Type = C::Type; + type Value = C::Value; + + fn get_function(&self, index: usize) -> eyre::Result> { + self.context()?.get_function(index) + } + + fn get_string(&self, index: usize) -> eyre::Result<&String> { + self.context()?.get_string(index) + } + + fn get_constant(&self, id: ConstantId) -> eyre::Result<&Self::Value> { + self.context()?.get_constant(id) + } } diff --git a/crates/vihaco/src/program.rs b/crates/vihaco/src/program.rs new file mode 100644 index 0000000..2477582 --- /dev/null +++ b/crates/vihaco/src/program.rs @@ -0,0 +1,508 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::io::{Cursor, Read}; + +use byteorder::{LittleEndian, ReadBytesExt}; +use chumsky::{error::Simple, extra, prelude::*}; + +use crate::{ + binary::{BytecodeContext, ConstantId}, + module::{FunctionInfo, LabelInfo, Parameter, Signature, SourceSymbolInfo}, + traits::{FromBytes, FromText}, +}; + +#[path = "value.rs"] +pub mod value; + +pub use value::{Type, Value}; + +type ContextParseExtra<'src> = extra::Err>; + +pub trait ProgramGlobals { + type Type; + type Value; + + fn get_function(&self, index: usize) -> eyre::Result>; + fn get_string(&self, index: usize) -> eyre::Result<&String>; + fn get_constant(&self, id: ConstantId) -> eyre::Result<&Self::Value>; +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct ProgramContext { + pub constants: Vec, + pub strings: Vec, + pub functions: Vec>, + pub labels: Vec, + pub main_function: Option, + pub file: u32, + pub source_symbols: Vec, +} + +impl ProgramContext +where + V: FromBytes, + Ty: FromBytes, +{ + pub fn from_bytes(bytes: &[u8]) -> eyre::Result { + let mut cursor = Cursor::new(bytes); + let context = Self::read_from(&mut cursor)?; + if cursor.position() as usize != bytes.len() { + return Err(eyre::eyre!( + "program context has {} trailing bytes", + bytes.len() - cursor.position() as usize + )); + } + Ok(context) + } + + pub fn read_from(reader: &mut R) -> eyre::Result { + let constants = read_vec(reader, "constant", V::from_bytes)?; + let strings = read_vec(reader, "string", read_string)?; + let functions = read_vec(reader, "function", read_function_info)?; + let labels = read_vec(reader, "label", read_label_info)?; + let main_function = read_optional_u32(reader)?; + let file = reader.read_u32::()?; + let source_symbols = read_vec(reader, "source symbol", read_source_symbol_info)?; + + Ok(Self { + constants, + strings, + functions, + labels, + main_function, + file, + source_symbols, + }) + } +} + +impl ProgramContext +where + V: FromText, + Ty: FromText, +{ + pub fn from_text(text: &str) -> eyre::Result { + let normalized = normalize_context_text(text); + context_text_parser::() + .parse(normalized.as_str()) + .into_result() + .map_err(format_context_parse_errors) + } +} + +impl BytecodeContext for ProgramContext +where + V: FromBytes + FromText, + Ty: FromBytes + FromText, +{ + fn from_bytes(bytes: &[u8]) -> eyre::Result { + ProgramContext::from_bytes(bytes) + } + + fn section_name(&self, index: u32) -> Option<&str> { + self.strings.get(index as usize).map(String::as_str) + } + + fn from_text(text: &str) -> eyre::Result { + ProgramContext::from_text(text) + } +} + +impl ProgramGlobals for ProgramContext +where + Ty: Clone, +{ + type Type = Ty; + type Value = V; + + fn get_function(&self, index: usize) -> eyre::Result> { + self.functions.get(index).cloned().ok_or_else(|| { + eyre::eyre!(format!( + "function index out of bounds: {} (max {})", + index, + self.functions.len() + )) + }) + } + + fn get_string(&self, index: usize) -> eyre::Result<&String> { + self.strings.get(index).ok_or_else(|| { + eyre::eyre!(format!( + "string index out of bounds: {} (max {})", + index, + self.strings.len() + )) + }) + } + + fn get_constant(&self, id: ConstantId) -> eyre::Result<&Self::Value> { + self.constants.get(id.0 as usize).ok_or_else(|| { + eyre::eyre!(format!( + "constant index out of bounds: {} (max {})", + id.0, + self.constants.len() + )) + }) + } +} + +fn read_vec(reader: &mut R, label: &str, mut read_item: F) -> eyre::Result> +where + R: Read, + F: FnMut(&mut R) -> eyre::Result, +{ + let count = reader.read_u32::()? as usize; + let mut values = Vec::with_capacity(count); + for index in 0..count { + values.push( + read_item(reader) + .map_err(|err| eyre::eyre!("failed to read {label} table entry {index}: {err}"))?, + ); + } + Ok(values) +} + +fn read_string(reader: &mut R) -> eyre::Result { + let len = reader.read_u32::()? as usize; + let mut bytes = vec![0; len]; + reader.read_exact(&mut bytes)?; + Ok(String::from_utf8(bytes)?) +} + +fn read_function_info(reader: &mut R) -> eyre::Result> +where + R: Read, + Ty: FromBytes, +{ + let name = reader.read_u32::()?; + let signature = read_signature(reader)?; + let local_count = reader.read_u32::()?; + let start_address = reader.read_u32::()?; + let end_address = reader.read_u32::()?; + let file = reader.read_u32::()?; + + Ok(FunctionInfo { + name, + signature, + local_count, + start_address, + end_address, + file, + }) +} + +fn read_signature(reader: &mut R) -> eyre::Result> +where + R: Read, + Ty: FromBytes, +{ + let params = read_vec(reader, "parameter", read_parameter)?; + let ret = read_vec(reader, "return type", Ty::from_bytes)?; + Ok(Signature { params, ret }) +} + +fn read_parameter(reader: &mut R) -> eyre::Result> +where + R: Read, + Ty: FromBytes, +{ + let name = reader.read_u32::()?; + let ty = Ty::from_bytes(reader)?; + Ok(Parameter { name, ty }) +} + +fn read_label_info(reader: &mut R) -> eyre::Result { + let address = reader.read_u32::()?; + let name = reader.read_u32::()?; + Ok(LabelInfo { address, name }) +} + +fn read_source_symbol_info(reader: &mut R) -> eyre::Result { + let index = reader.read_u32::()?; + let name = read_string(reader)?; + Ok(SourceSymbolInfo { index, name }) +} + +fn read_optional_u32(reader: &mut R) -> eyre::Result> { + match reader.read_u8()? { + 0 => Ok(None), + 1 => Ok(Some(reader.read_u32::()?)), + other => Err(eyre::eyre!( + "invalid optional u32 discriminant {} in program context", + other + )), + } +} + +fn context_text_parser<'src, V, Ty>() +-> impl Parser<'src, &'src str, ProgramContext, ContextParseExtra<'src>> +where + V: FromText + 'src, + Ty: FromText + 'src, +{ + let constants = context_line_entry::(".strings") + .repeated() + .collect::>(); + let strings = string_line().repeated().collect::>(); + let functions = function_line::().repeated().collect::>(); + let labels = label_line().repeated().collect::>(); + let source_symbols = source_symbol_line().repeated().collect::>(); + + heading(".constants") + .ignore_then(constants) + .then_ignore(heading(".strings")) + .then(strings) + .then_ignore(heading(".functions")) + .then(functions) + .then_ignore(heading(".labels")) + .then(labels) + .then(main_function_line()) + .then(file_line()) + .then_ignore(heading(".source-symbols")) + .then(source_symbols) + .then_ignore(end()) + .map( + |( + (((((constants, strings), functions), labels), main_function), file), + source_symbols, + )| { + ProgramContext { + constants, + strings, + functions, + labels, + main_function, + file, + source_symbols, + } + }, + ) +} + +fn heading<'src>( + name: &'static str, +) -> impl Parser<'src, &'src str, (), ContextParseExtra<'src>> + Clone { + just(name).then_ignore(eol()).ignored() +} + +fn eol<'src>() -> impl Parser<'src, &'src str, (), ContextParseExtra<'src>> + Clone { + just('\r').or_not().then_ignore(just('\n')).ignored() +} + +fn inline_ws<'src>() -> impl Parser<'src, &'src str, (), ContextParseExtra<'src>> + Clone { + one_of(" \t").repeated().ignored() +} + +fn required_inline_ws<'src>() -> impl Parser<'src, &'src str, (), ContextParseExtra<'src>> + Clone { + one_of(" \t").repeated().at_least(1).ignored() +} + +fn u32_text<'src>() -> impl Parser<'src, &'src str, u32, ContextParseExtra<'src>> + Clone { + text::int(10) + .try_map(|text: &str, span| text.parse::().map_err(|_| Simple::new(None, span))) +} + +fn context_line_entry<'src, T>( + next_marker: &'static str, +) -> impl Parser<'src, &'src str, T, ContextParseExtra<'src>> +where + T: FromText + 'src, +{ + just(next_marker) + .not() + .ignore_then( + any() + .filter(|ch: &char| !matches!(*ch, '\r' | '\n')) + .repeated() + .at_least(1) + .to_slice(), + ) + .then_ignore(eol()) + .try_map(|text: &str, span| { + parse_text_entry(text.trim()).map_err(|_| Simple::new(None, span)) + }) +} + +fn string_line<'src>() -> impl Parser<'src, &'src str, String, ContextParseExtra<'src>> + Clone { + string_literal().then_ignore(inline_ws()).then_ignore(eol()) +} + +fn function_line<'src, Ty>() +-> impl Parser<'src, &'src str, FunctionInfo, ContextParseExtra<'src>> +where + Ty: FromText + 'src, +{ + just("fn") + .ignore_then(required_inline_ws()) + .ignore_then(u32_text()) + .then_ignore(required_inline_ws()) + .then(signature_text::()) + .then_ignore(required_inline_ws()) + .then(u32_text()) + .then_ignore(required_inline_ws()) + .then(u32_text()) + .then_ignore(required_inline_ws()) + .then(u32_text()) + .then_ignore(required_inline_ws()) + .then(u32_text()) + .then_ignore(inline_ws()) + .then_ignore(eol()) + .map( + |(((((name, signature), local_count), start_address), end_address), file)| { + FunctionInfo { + name, + signature, + local_count, + start_address, + end_address, + file, + } + }, + ) +} + +fn signature_text<'src, Ty>() -> impl Parser<'src, &'src str, Signature, ContextParseExtra<'src>> +where + Ty: FromText + 'src, +{ + just('(') + .ignore_then(inline_ws()) + .ignore_then( + parameter_text::() + .separated_by(comma_separator()) + .collect::>(), + ) + .then_ignore(inline_ws()) + .then_ignore(just(')')) + .then_ignore(inline_ws()) + .then_ignore(just("->")) + .then_ignore(inline_ws()) + .then(return_types_text::()) + .map(|(params, ret)| Signature { params, ret }) +} + +fn parameter_text<'src, Ty>() -> impl Parser<'src, &'src str, Parameter, ContextParseExtra<'src>> +where + Ty: FromText + 'src, +{ + u32_text() + .then_ignore(inline_ws()) + .then_ignore(just(':')) + .then_ignore(inline_ws()) + .then(type_text::()) + .map(|(name, ty)| Parameter { name, ty }) +} + +fn return_types_text<'src, Ty>() -> impl Parser<'src, &'src str, Vec, ContextParseExtra<'src>> +where + Ty: FromText + 'src, +{ + let parenthesized = just('(') + .ignore_then(inline_ws()) + .ignore_then( + type_text::() + .separated_by(comma_separator()) + .collect::>(), + ) + .then_ignore(inline_ws()) + .then_ignore(just(')')); + + parenthesized.or(type_text::().map(|ty| vec![ty])) +} + +fn type_text<'src, Ty>() -> impl Parser<'src, &'src str, Ty, ContextParseExtra<'src>> +where + Ty: FromText + 'src, +{ + any() + .filter(|ch: &char| !ch.is_whitespace() && !matches!(*ch, ',' | ')')) + .repeated() + .at_least(1) + .to_slice() + .try_map(|text: &str, span| parse_text_entry(text).map_err(|_| Simple::new(None, span))) +} + +fn comma_separator<'src>() -> impl Parser<'src, &'src str, (), ContextParseExtra<'src>> + Clone { + inline_ws() + .ignore_then(just(',')) + .then_ignore(inline_ws()) + .ignored() +} + +fn label_line<'src>() -> impl Parser<'src, &'src str, LabelInfo, ContextParseExtra<'src>> + Clone { + u32_text() + .then_ignore(required_inline_ws()) + .then(u32_text()) + .then_ignore(inline_ws()) + .then_ignore(eol()) + .map(|(address, name)| LabelInfo { address, name }) +} + +fn main_function_line<'src>() +-> impl Parser<'src, &'src str, Option, ContextParseExtra<'src>> + Clone { + just(".main") + .ignore_then(required_inline_ws()) + .ignore_then(just("none").to(None).or(u32_text().map(Some))) + .then_ignore(inline_ws()) + .then_ignore(eol()) +} + +fn file_line<'src>() -> impl Parser<'src, &'src str, u32, ContextParseExtra<'src>> + Clone { + just(".file") + .ignore_then(required_inline_ws()) + .ignore_then(u32_text()) + .then_ignore(inline_ws()) + .then_ignore(eol()) +} + +fn source_symbol_line<'src>() +-> impl Parser<'src, &'src str, SourceSymbolInfo, ContextParseExtra<'src>> + Clone { + u32_text() + .then_ignore(required_inline_ws()) + .then(string_literal()) + .then_ignore(inline_ws()) + .then_ignore(eol()) + .map(|(index, name)| SourceSymbolInfo { index, name }) +} + +fn string_literal<'src>() -> impl Parser<'src, &'src str, String, ContextParseExtra<'src>> + Clone { + let escape = just('\\').ignore_then(choice(( + just('"').to('"'), + just('\\').to('\\'), + just('n').to('\n'), + just('t').to('\t'), + just('r').to('\r'), + just('0').to('\0'), + ))); + let char_or_escape = choice(( + escape, + any().and_is(just('"').not()).and_is(just('\\').not()), + )); + just('"') + .ignore_then(char_or_escape.repeated().collect::()) + .then_ignore(just('"')) +} + +fn parse_text_entry(text: &str) -> eyre::Result { + let mut cursor = Cursor::new(text.as_bytes()); + T::from_text(&mut cursor) +} + +fn normalize_context_text(text: &str) -> String { + let mut normalized = String::new(); + for line in text.lines().map(str::trim).filter(|line| !line.is_empty()) { + normalized.push_str(line); + normalized.push('\n'); + } + normalized +} + +fn format_context_parse_errors(errors: Vec>) -> eyre::Report { + let error = errors + .into_iter() + .next() + .map(|error| format!("{error:?}")) + .unwrap_or_else(|| "unknown parse error".to_string()); + eyre::eyre!("failed to parse text program context: {error}") +} diff --git a/crates/vihaco/src/syntax/types.rs b/crates/vihaco/src/syntax/types.rs index 80c667c..aa7a3c6 100644 --- a/crates/vihaco/src/syntax/types.rs +++ b/crates/vihaco/src/syntax/types.rs @@ -31,7 +31,7 @@ pub struct Param { } /// Bare type token — `"i64"`, `"f64"`, …. The resolver translates to -/// [`crate::value::Type`]. +/// [`crate::program::Type`]. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct RawType(pub String); diff --git a/crates/vihaco/src/traits/instruction.rs b/crates/vihaco/src/traits/instruction.rs index 643f6ee..6e2b9cf 100644 --- a/crates/vihaco/src/traits/instruction.rs +++ b/crates/vihaco/src/traits/instruction.rs @@ -7,6 +7,12 @@ pub trait FromBytes { Self: Sized; } +pub trait FromText { + fn from_text(text: &mut R) -> eyre::Result + where + Self: Sized; +} + pub trait FromBytesWithOpcode: Sized { fn from_bytes_with_opcode(bytes: &mut R, opcode: u8) -> eyre::Result; } diff --git a/crates/vihaco/src/traits/machine.rs b/crates/vihaco/src/traits/machine.rs index 88b206a..95c8807 100644 --- a/crates/vihaco/src/traits/machine.rs +++ b/crates/vihaco/src/traits/machine.rs @@ -5,7 +5,7 @@ use eyre::Result; use super::Instruction; -use crate::{frame::Frame, module::FunctionInfo}; +use crate::{binary::ConstantId, frame::Frame, module::FunctionInfo}; pub trait ProgramCounter { type Instruction: Instruction; @@ -89,8 +89,11 @@ pub trait FrameMemory: StackFrame + StackMemory { pub trait GetProgramGlobal { type Type; + type Value; + fn get_function(&self, index: usize) -> Result>; fn get_string(&self, index: usize) -> Result<&String>; + fn get_constant(&self, id: ConstantId) -> Result<&Self::Value>; } pub trait Stdout { diff --git a/crates/vihaco/src/traits/mod.rs b/crates/vihaco/src/traits/mod.rs index 465af99..57e5c7b 100644 --- a/crates/vihaco/src/traits/mod.rs +++ b/crates/vihaco/src/traits/mod.rs @@ -6,7 +6,7 @@ mod instruction; mod machine; pub use event_sink::EffectSink; -pub use instruction::{FromBytes, FromBytesWithOpcode, Instruction, OpCode, WriteBytes}; +pub use instruction::{FromBytes, FromBytesWithOpcode, FromText, Instruction, OpCode, WriteBytes}; pub use machine::{FrameMemory, GetProgramGlobal, ProgramCounter, StackFrame, StackMemory, Stdout}; pub trait Reset { diff --git a/crates/vihaco/src/value.rs b/crates/vihaco/src/value.rs index d359eb7..e90cffa 100644 --- a/crates/vihaco/src/value.rs +++ b/crates/vihaco/src/value.rs @@ -4,8 +4,11 @@ use std::convert::TryFrom; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use chumsky::{error::Simple, extra, prelude::*}; use eyre::Result; +type ValueParseExtra<'src> = extra::Err>; + #[derive(Debug, Clone, Copy, PartialEq)] pub enum Value { Undefined, @@ -194,6 +197,21 @@ impl crate::traits::FromBytes for Type { } } +impl crate::traits::FromText for Type { + fn from_text(text: &mut R) -> eyre::Result + where + Self: Sized, + { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + type_text_parser() + .then_ignore(end()) + .parse(buffer.trim()) + .into_result() + .map_err(format_value_parse_errors) + } +} + impl crate::traits::WriteBytes for Type { fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { let type_byte = match self { @@ -257,6 +275,21 @@ impl crate::traits::FromBytes for Value { } } +impl crate::traits::FromText for Value { + fn from_text(text: &mut R) -> eyre::Result + where + Self: Sized, + { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + value_text_parser() + .then_ignore(end()) + .parse(buffer.trim()) + .into_result() + .map_err(format_value_parse_errors) + } +} + impl crate::traits::WriteBytes for Value { fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { match self { @@ -299,3 +332,79 @@ impl crate::traits::WriteBytes for Value { Ok(()) } } + +fn type_text_parser<'src>() -> impl Parser<'src, &'src str, Type, ValueParseExtra<'src>> + Clone { + choice(( + just("undefined").to(Type::Undefined), + just("string").to(Type::String), + just("str").to(Type::String), + just("bool").to(Type::Bool), + just("i64").to(Type::I64), + just("u32").to(Type::U32), + just("u64").to(Type::U64), + just("f64").to(Type::F64), + just("fn").to(Type::FunctionRef), + just("heap").to(Type::HeapRef), + )) +} + +fn value_text_parser<'src>() -> impl Parser<'src, &'src str, Value, ValueParseExtra<'src>> + Clone { + let ws = one_of(" \t").repeated().at_least(1).ignored(); + choice(( + just("undefined").to(Value::Undefined), + just("string") + .or(just("str")) + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::String), + just("bool") + .ignore_then(ws) + .ignore_then(choice((just("true").to(true), just("false").to(false)))) + .map(Value::Bool), + just("i64") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::I64), + just("u32") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::U32), + just("u64") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::U64), + just("f64") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::F64), + just("fn") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::FunctionRef), + just("heap") + .ignore_then(ws) + .ignore_then(scalar_text::()) + .map(Value::HeapRef), + )) +} + +fn scalar_text<'src, T>() -> impl Parser<'src, &'src str, T, ValueParseExtra<'src>> + Clone +where + T: std::str::FromStr, +{ + any() + .filter(|ch: &char| !ch.is_whitespace()) + .repeated() + .at_least(1) + .to_slice() + .try_map(|text: &str, span| text.parse::().map_err(|_| Simple::new(None, span))) +} + +fn format_value_parse_errors(errors: Vec>) -> eyre::Report { + let error = errors + .into_iter() + .next() + .map(|error| format!("{error:?}")) + .unwrap_or_else(|| "unknown parse error".to_string()); + eyre::eyre!("failed to parse value text: {error}") +} diff --git a/crates/vihaco/tests/ui/rfc0007-machine-duplicate-device-code.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs similarity index 100% rename from crates/vihaco/tests/ui/rfc0007-machine-duplicate-device-code.rs rename to crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.rs diff --git a/crates/vihaco/tests/ui/rfc0007-machine-duplicate-device-code.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr similarity index 57% rename from crates/vihaco/tests/ui/rfc0007-machine-duplicate-device-code.stderr rename to crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr index 2de85ec..f7c47c5 100644 --- a/crates/vihaco/tests/ui/rfc0007-machine-duplicate-device-code.stderr +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-device-code.stderr @@ -1,5 +1,5 @@ error: duplicate device code 0x01 for fields `a` and `b` - --> tests/ui/rfc0007-machine-duplicate-device-code.rs:26:5 + --> tests/compile_fail/composite_machine/duplicate-device-code.rs:26:5 | 26 | b: DemoDevice, | ^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.rs new file mode 100644 index 0000000..8965f16 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.rs @@ -0,0 +1,13 @@ +#[derive(Default)] +struct Header; + +#[vihaco::composite] +struct BadMachine { + #[header] + first: Header, + + #[header] + second: Header, +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.stderr new file mode 100644 index 0000000..599c98a --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-header.stderr @@ -0,0 +1,5 @@ +error: duplicate header field `second`; `first` is already marked #[header] + --> tests/compile_fail/composite_machine/duplicate-header.rs:10:5 + | +10 | second: Header, + | ^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs new file mode 100644 index 0000000..636daea --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.rs @@ -0,0 +1,13 @@ +struct Child; + +#[vihaco::composite] +struct BadMachine { + #[device(0x01)] + #[loadable("child")] + a: Child, + #[device(0x02)] + #[loadable("child")] + b: Child, +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.stderr b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.stderr new file mode 100644 index 0000000..a2f2962 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/duplicate-loadable-name.stderr @@ -0,0 +1,5 @@ +error: duplicate loadable section name `child` for fields `a` and `b` + --> tests/compile_fail/composite_machine/duplicate-loadable-name.rs:10:5 + | +10 | b: Child, + | ^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.rs b/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.rs new file mode 100644 index 0000000..90a60a3 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.rs @@ -0,0 +1,10 @@ +struct HeaderDevice; + +#[vihaco::composite] +struct BadMachine { + #[header] + #[device(0x01)] + header: HeaderDevice, +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.stderr b/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.stderr new file mode 100644 index 0000000..269f8da --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/header-with-device.stderr @@ -0,0 +1,5 @@ +error: field `header` marked #[header] cannot also be marked #[program], #[device(...)] or #[loadable] + --> tests/compile_fail/composite_machine/header-with-device.rs:7:5 + | +7 | header: HeaderDevice, + | ^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs new file mode 100644 index 0000000..997dbdf --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.rs @@ -0,0 +1,10 @@ +struct Child; + +#[vihaco::composite] +struct BadMachine { + #[device(0x01)] + #[loadable("child/nested")] + child: Child, +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr new file mode 100644 index 0000000..4b6a865 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/invalid-loadable-name.stderr @@ -0,0 +1,5 @@ +error: loadable section name cannot contain `/` + --> tests/compile_fail/composite_machine/invalid-loadable-name.rs:6:16 + | +6 | #[loadable("child/nested")] + | ^^^^^^^^^^^^^^ diff --git a/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs new file mode 100644 index 0000000..50e4a13 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.rs @@ -0,0 +1,9 @@ +struct Child; + +#[vihaco::composite] +struct BadMachine { + #[loadable("child")] + child: Child, +} + +fn main() {} diff --git a/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.stderr b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.stderr new file mode 100644 index 0000000..bfaefc6 --- /dev/null +++ b/crates/vihaco/tests/compile_fail/composite_machine/loadable-without-device.stderr @@ -0,0 +1,5 @@ +error: field `child` marked #[loadable] must also be marked #[device(...)] + --> tests/compile_fail/composite_machine/loadable-without-device.rs:6:5 + | +6 | child: Child, + | ^^^^^ diff --git a/crates/vihaco/tests/composite_machine_compile_fail.rs b/crates/vihaco/tests/composite_machine_compile_fail.rs new file mode 100644 index 0000000..a777d63 --- /dev/null +++ b/crates/vihaco/tests/composite_machine_compile_fail.rs @@ -0,0 +1,13 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +#[test] +fn composite_machine_rejects_ambiguous_wiring() { + let t = trybuild::TestCases::new(); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-device-code.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-header.rs"); + t.compile_fail("tests/compile_fail/composite_machine/duplicate-loadable-name.rs"); + t.compile_fail("tests/compile_fail/composite_machine/header-with-device.rs"); + t.compile_fail("tests/compile_fail/composite_machine/invalid-loadable-name.rs"); + t.compile_fail("tests/compile_fail/composite_machine/loadable-without-device.rs"); +} diff --git a/crates/vihaco/tests/multisection_bytecode.rs b/crates/vihaco/tests/multisection_bytecode.rs new file mode 100644 index 0000000..c4a7d8d --- /dev/null +++ b/crates/vihaco/tests/multisection_bytecode.rs @@ -0,0 +1,694 @@ +// SPDX-FileCopyrightText: 2026 The vihaco Authors +// SPDX-License-Identifier: MIT + +use std::{io::Read, str::FromStr}; + +use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; +use vihaco::{ + BytecodeContext, BytecodeFile, ConstantId, Effects, GeneratedComponent, GetProgramGlobal, + Instruction, LoadInput, LoadSection, ProgramLoader, Value, + binary::{FLAGS, MAGIC, VERSION}, + traits::{FromBytes, FromText, WriteBytes}, +}; + +const CHILD_NAME: u32 = 0; +const DEFAULT_CHILD_NAME: u32 = 1; +const EXTRA_NAME: u32 = 2; +const MIDDLE_NAME: u32 = 3; +const LEAF_NAME: u32 = 4; +const SECTION_FRAME_LEN: usize = 8 + 8; +const SECTION_BYTECODE_HEADER_LEN: usize = 8; +const CHILD_SECTION_TABLE_HEADER_LEN: usize = 4; +const CHILD_SECTION_TABLE_ENTRY_LEN: usize = 4 + 8; + +#[derive(Debug, Clone, PartialEq, Instruction)] +enum TestInst { + Nop, + Load(ConstantId), +} + +#[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser::Parse)] +enum TextInst { + Nop, + Alt, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +struct TestHeader { + cores: u32, +} + +impl FromBytes for TestHeader { + fn from_bytes(bytes: &mut R) -> eyre::Result { + Ok(Self { + cores: bytes.read_u32::()?, + }) + } +} + +impl FromText for TestHeader { + fn from_text(text: &mut R) -> eyre::Result { + let mut buffer = String::new(); + text.read_to_string(&mut buffer)?; + Ok(buffer.trim().parse()?) + } +} + +impl WriteBytes for TestHeader { + fn write_bytes(&self, io: &mut W) -> eyre::Result<()> { + io.write_u32::(self.cores)?; + Ok(()) + } +} + +impl FromStr for TestHeader { + type Err = std::num::ParseIntError; + + fn from_str(text: &str) -> Result { + Ok(Self { + cores: text.parse()?, + }) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +struct TextContext { + section_names: Vec, +} + +impl BytecodeContext for TextContext { + fn from_bytes(bytes: &[u8]) -> eyre::Result { + let text = std::str::from_utf8(bytes)?; + Self::from_text(text) + } + + fn from_text(text: &str) -> eyre::Result { + Ok(Self { + section_names: text + .lines() + .map(str::trim) + .filter(|line| !line.is_empty()) + .map(ToOwned::to_owned) + .collect(), + }) + } + + fn section_name(&self, index: u32) -> Option<&str> { + self.section_names.get(index as usize).map(String::as_str) + } +} + +#[derive(Debug, Clone, Default)] +struct LoadedDevice { + program: ProgramLoader, +} + +#[derive(Debug, Clone, Default)] +struct TextLoadedDevice { + program: ProgramLoader, +} + +impl GeneratedComponent for LoadedDevice { + type Instruction = TestInst; + type Message = (); + type Effect = (); + + fn execute_generated( + &mut self, + _inst: Self::Instruction, + _msg: Self::Message, + ) -> eyre::Result> { + Ok(Effects::none()) + } +} + +impl GeneratedComponent for TextLoadedDevice { + type Instruction = TextInst; + type Message = (); + type Effect = (); + + fn execute_generated( + &mut self, + _inst: Self::Instruction, + _msg: Self::Message, + ) -> eyre::Result> { + Ok(Effects::none()) + } +} + +impl LoadSection for LoadedDevice { + fn load_section<'bc>(&mut self, input: LoadInput<'bc>) -> eyre::Result<()> { + self.program.load_section(input) + } +} + +impl LoadSection for TextLoadedDevice { + fn load_section<'bc>( + &mut self, + input: LoadInput<'bc, String, TextContext>, + ) -> eyre::Result<()> { + self.program.load_section(input) + } +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct Machine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("child")] + child: LoadedDevice, + + #[device(0x02)] + #[loadable] + default_child: LoadedDevice, +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct NestedMachine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("leaf")] + leaf: LoadedDevice, +} + +impl GeneratedComponent for NestedMachine { + type Instruction = NestedMachineInstruction; + type Message = (); + type Effect = (); + + fn execute_generated( + &mut self, + _inst: Self::Instruction, + _msg: Self::Message, + ) -> eyre::Result> { + Ok(Effects::none()) + } +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct HostMachine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("middle")] + middle: NestedMachine, +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct HeaderMachine { + #[header] + info: TestHeader, + + #[program] + program: ProgramLoader, + + #[device(0x01)] + device: LoadedDevice, +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct TextMachine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("child")] + child: TextLoadedDevice, + + #[device(0x02)] + #[loadable] + default_child: TextLoadedDevice, +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct TextNestedMachine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("leaf")] + leaf: TextLoadedDevice, +} + +impl GeneratedComponent for TextNestedMachine { + type Instruction = TextNestedMachineInstruction; + type Message = (); + type Effect = (); + + fn execute_generated( + &mut self, + _inst: Self::Instruction, + _msg: Self::Message, + ) -> eyre::Result> { + Ok(Effects::none()) + } +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct TextHostMachine { + #[program] + program: ProgramLoader, + + #[device(0x01)] + #[loadable("middle")] + middle: TextNestedMachine, +} + +#[vihaco::composite] +#[derive(Debug, Default)] +#[allow(dead_code)] +struct TextHeaderMachine { + #[header] + info: TestHeader, + + #[program] + program: ProgramLoader, + + #[device(0x01)] + device: TextLoadedDevice, +} + +#[test] +fn binary_generated_loadable_routes_program_and_child_sections() { + let child = binary_section_bytes(b"", &[TestInst::Load(ConstantId(0))], vec![]); + let default_child = binary_section_bytes(b"", &[TestInst::Nop], vec![]); + let root = binary_section_bytes( + b"", + &[TestInst::Nop], + vec![(CHILD_NAME, child), (DEFAULT_CHILD_NAME, default_child)], + ); + let file: BytecodeFile> = + BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + + let mut machine = Machine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TestInst::Nop]); + assert_eq!( + machine.child.program.code, + vec![TestInst::Load(ConstantId(0))] + ); + assert_eq!(machine.default_child.program.code, vec![TestInst::Nop]); + assert!( + machine + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .child + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert_eq!( + machine.program.get_constant(ConstantId(0)).unwrap(), + &Value::I64(9) + ); +} + +#[test] +fn text_generated_loadable_routes_program_and_child_sections() { + let file = text_file( + &["child", "default_child"], + ".section(root):\n\ +\t.text(root):\n\ +\t\tnop\n\ +\t.text(root).\n\ +\t.section(child):\n\ +\t\t.text(child):\n\ +\t\t\talt\n\ +\t\t.text(child).\n\ +\t.section(child).\n\ +\t.section(default_child):\n\ +\t\t.text(default_child):\n\ +\t\t\tnop\n\ +\t\t.text(default_child).\n\ +\t.section(default_child).\n\ +.section(root).\n", + ); + + let mut machine = TextMachine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TextInst::Nop]); + assert_eq!(machine.child.program.code, vec![TextInst::Alt]); + assert_eq!(machine.default_child.program.code, vec![TextInst::Nop]); + assert!( + machine + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .child + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); +} + +#[test] +fn binary_generated_loadable_parses_marked_header() { + let mut header = Vec::new(); + TestHeader { cores: 8 }.write_bytes(&mut header).unwrap(); + let root = binary_section_bytes(&header, &[TestInst::Nop], vec![]); + let file: BytecodeFile> = + BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + + let mut machine = HeaderMachine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.info, TestHeader { cores: 8 }); + assert_eq!(machine.program.code, vec![TestInst::Nop]); +} + +#[test] +fn text_generated_loadable_parses_marked_header() { + let file = text_file( + &[], + ".section(root):\n\ +\t.header(root):\n\ +\t\t8\n\ +\t.header(root).\n\ +\t.text(root):\n\ +\t\tnop\n\ +\t.text(root).\n\ +.section(root).\n", + ); + + let mut machine = TextHeaderMachine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.info, TestHeader { cores: 8 }); + assert_eq!(machine.program.code, vec![TextInst::Nop]); +} + +#[test] +fn binary_generated_loadable_routes_three_level_section_tree() { + let leaf = binary_section_bytes(b"", &[TestInst::Nop, TestInst::Load(ConstantId(0))], vec![]); + let middle = binary_section_bytes( + b"", + &[TestInst::Load(ConstantId(0))], + vec![(LEAF_NAME, leaf)], + ); + let root = binary_section_bytes(b"", &[TestInst::Nop], vec![(MIDDLE_NAME, middle)]); + let file: BytecodeFile> = + BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + + let mut machine = HostMachine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TestInst::Nop]); + assert_eq!( + machine.middle.program.code, + vec![TestInst::Load(ConstantId(0))] + ); + assert_eq!( + machine.middle.leaf.program.code, + vec![TestInst::Nop, TestInst::Load(ConstantId(0))] + ); + assert!( + machine + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .middle + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .middle + .leaf + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); +} + +#[test] +fn text_generated_loadable_routes_three_level_section_tree() { + let file = text_file( + &["middle", "leaf"], + ".section(root):\n\ +\t.text(root):\n\ +\t\tnop\n\ +\t.text(root).\n\ +\t.section(middle):\n\ +\t\t.text(middle):\n\ +\t\t\talt\n\ +\t\t.text(middle).\n\ +\t\t.section(leaf):\n\ +\t\t\t.text(leaf):\n\ +\t\t\t\tnop\n\ +\t\t\t\talt\n\ +\t\t\t.text(leaf).\n\ +\t\t.section(leaf).\n\ +\t.section(middle).\n\ +.section(root).\n", + ); + + let mut machine = TextHostMachine::default(); + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TextInst::Nop]); + assert_eq!(machine.middle.program.code, vec![TextInst::Alt]); + assert_eq!( + machine.middle.leaf.program.code, + vec![TextInst::Nop, TextInst::Alt] + ); + assert!( + machine + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .middle + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); + assert!( + machine + .middle + .leaf + .program + .context + .as_ref() + .unwrap() + .ptr_eq(&file.context_handle()) + ); +} + +#[test] +fn binary_generated_loadable_allows_missing_marked_children() { + let root = binary_section_bytes(b"", &[TestInst::Nop], vec![]); + let file: BytecodeFile> = + BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + let mut machine = Machine::default(); + + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TestInst::Nop]); + assert!(machine.child.program.code.is_empty()); + assert!(machine.child.program.context.is_none()); + assert!(machine.default_child.program.code.is_empty()); + assert!(machine.default_child.program.context.is_none()); +} + +#[test] +fn text_generated_loadable_allows_missing_marked_children() { + let file = text_file( + &["child", "default_child"], + ".section(root):\n\ +\t.text(root):\n\ +\t\tnop\n\ +\t.text(root).\n\ +.section(root).\n", + ); + let mut machine = TextMachine::default(); + + machine.load_section(LoadInput::from(&file)).unwrap(); + + assert_eq!(machine.program.code, vec![TextInst::Nop]); + assert!(machine.child.program.code.is_empty()); + assert!(machine.child.program.context.is_none()); + assert!(machine.default_child.program.code.is_empty()); + assert!(machine.default_child.program.context.is_none()); +} + +#[test] +fn binary_generated_loadable_rejects_unexpected_direct_children() { + let extra = binary_section_bytes(b"", &[], vec![]); + let root = binary_section_bytes(b"", &[TestInst::Nop], vec![(EXTRA_NAME, extra)]); + let file: BytecodeFile> = + BytecodeFile::from_bytes(binary_file_bytes(context_bytes(), root)).unwrap(); + let mut machine = Machine::default(); + + let err = machine.load_section(LoadInput::from(&file)).unwrap_err(); + + assert!(err.to_string().contains("unexpected child section")); +} + +#[test] +fn text_generated_loadable_rejects_unexpected_direct_children() { + let file = text_file( + &["child", "default_child", "extra"], + ".section(root):\n\ +\t.text(root):\n\ +\t\tnop\n\ +\t.text(root).\n\ +\t.section(child):\n\ +\t.section(child).\n\ +\t.section(default_child):\n\ +\t.section(default_child).\n\ +\t.section(extra):\n\ +\t.section(extra).\n\ +.section(root).\n", + ); + let mut machine = TextMachine::default(); + + let err = machine.load_section(LoadInput::from(&file)).unwrap_err(); + + assert!(err.to_string().contains("unexpected child section")); +} + +fn binary_file_bytes(context: Vec, root: Vec) -> Vec { + let mut bytes = Vec::new(); + bytes.extend_from_slice(MAGIC); + bytes.write_u16::(VERSION).unwrap(); + bytes.write_u16::(FLAGS).unwrap(); + bytes + .write_u64::(context.len() as u64) + .unwrap(); + bytes.extend_from_slice(&context); + bytes.extend_from_slice(&root); + bytes +} + +fn text_file(section_names: &[&str], sections: &str) -> BytecodeFile { + let context = section_names.join("\n"); + let context = if context.is_empty() { + String::new() + } else { + format!("{context}\n") + }; + BytecodeFile::::from_text(&format!( + "vhbc{VERSION}\n\n.global:\n{context}.global.\n\n{sections}" + )) + .unwrap() +} + +fn context_bytes() -> Vec { + let mut bytes = Vec::new(); + + bytes.write_u32::(1).unwrap(); + Value::I64(9).write_bytes(&mut bytes).unwrap(); + + bytes.write_u32::(5).unwrap(); + write_string(&mut bytes, "child"); + write_string(&mut bytes, "default_child"); + write_string(&mut bytes, "extra"); + write_string(&mut bytes, "middle"); + write_string(&mut bytes, "leaf"); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u8(0).unwrap(); + bytes.write_u32::(0).unwrap(); + bytes.write_u32::(0).unwrap(); + + bytes +} + +fn binary_section_bytes( + header: &[u8], + instructions: &[TestInst], + children: Vec<(u32, Vec)>, +) -> Vec { + let mut bytecode = Vec::new(); + for inst in instructions { + inst.write_bytes(&mut bytecode).unwrap(); + } + + let child_table_len = + CHILD_SECTION_TABLE_HEADER_LEN + children.len() * CHILD_SECTION_TABLE_ENTRY_LEN; + let bytecode_start = SECTION_FRAME_LEN + header.len() + SECTION_BYTECODE_HEADER_LEN; + let mut child_offset = bytecode_start + bytecode.len() + child_table_len; + let section_len = child_offset + children.iter().map(|(_, child)| child.len()).sum::(); + + let mut bytes = Vec::new(); + bytes.write_u64::(section_len as u64).unwrap(); + bytes + .write_u64::(header.len() as u64) + .unwrap(); + bytes.extend_from_slice(header); + bytes + .write_u64::(bytecode.len() as u64) + .unwrap(); + bytes.extend_from_slice(&bytecode); + bytes + .write_u32::(children.len() as u32) + .unwrap(); + for (name_index, child) in &children { + bytes.write_u32::(*name_index).unwrap(); + bytes + .write_u64::(child_offset as u64) + .unwrap(); + child_offset += child.len(); + } + for (_, child) in children { + bytes.extend_from_slice(&child); + } + bytes +} + +fn write_string(bytes: &mut Vec, value: &str) { + bytes.write_u32::(value.len() as u32).unwrap(); + bytes.extend_from_slice(value.as_bytes()); +} diff --git a/crates/vihaco/tests/rfc0007_machine_compile_fail.rs b/crates/vihaco/tests/rfc0007_machine_compile_fail.rs deleted file mode 100644 index 9f9ebda..0000000 --- a/crates/vihaco/tests/rfc0007_machine_compile_fail.rs +++ /dev/null @@ -1,8 +0,0 @@ -// SPDX-FileCopyrightText: 2026 The vihaco Authors -// SPDX-License-Identifier: MIT - -#[test] -fn machine_derive_rejects_ambiguous_wiring() { - let t = trybuild::TestCases::new(); - t.compile_fail("tests/ui/rfc0007-machine-duplicate-device-code.rs"); -} diff --git a/docs/src/pages/guide/composites.md b/docs/src/pages/guide/composites.md index 0470c51..620dc2a 100644 --- a/docs/src/pages/guide/composites.md +++ b/docs/src/pages/guide/composites.md @@ -138,6 +138,169 @@ pub struct Machine { Here `Machine` drives its instruction pointer through the `cpu` field. +### `#[header]` + +Marks the field that should receive the section's typed composite header during generated bytecode loading. + +```rust ignore +#[derive(Default)] +pub struct CpuHeader { + cores: u32, +} + +#[composite] +#[derive(Default)] +pub struct CpuMachine { + #[header] + info: CpuHeader, + + #[program] + program: vihaco::ProgramLoader, +} +``` + +For binary bytecode, the field type must implement `CompositeHeader`, which has a blanket impl for types implementing `FromBytes + WriteBytes`. Generated binary loading parses `input.section.header_bytes()` with `BinarySectionView::parse_header::()` and assigns the result to the field before loading the program or child sections. For text bytecode, generated loading trims `input.section.header_text()` and parses it with `FromStr`, so the header type must also implement `FromStr` when you load `TextBytecodeFile`. Composites without a `#[header]` field generate no header parsing code, no header trait bound, and no runtime header check. + +### `#[loadable]` + +Marks a device field that should receive its own direct child section when loading v1 multi-section bytecode. The device owns its loader internally; the generated composite loader only routes the section to the marked device's `LoadSection` impl. + +```rust ignore +#[composite] +#[derive(Default)] +pub struct Machine { + #[program] + program: vihaco::ProgramLoader, + + #[device(0x01)] + #[loadable("signal")] + signal: SignalMachine, +} +``` + +`#[loadable]` must be used on a `#[device(...)]` field whose type implements `LoadSection` for the bytecode representation you are loading as well as `GeneratedComponent`. Binary bytecode delegates through `LoadSection, C>`; text bytecode delegates through `LoadSection`. The attribute uses the field name as the local section name. `#[loadable("name")]` overrides it. Names must be non-empty direct child names, so they cannot contain `/`. + +Section identity is represented as a `SectionPath`, which is a vector of resolved local section names. The root section is `SectionPath::root()` with zero components. A root child named `cpu` has the path `cpu`; a child named `alu` inside that section has `cpu/alu`. Generated loading asks the current section for direct children by local name, so the same composite can be loaded at the root or under another parent section. + +Manual loaders can inspect a whole section subtree with `SectionView::walk()`, which yields the current section followed by its descendants in depth-first order. `SectionView::descendants()` uses the same order but skips the current section. To walk an entire file, use `BytecodeFile::sections()`. + +The generated loader is strict: + +- every `#[loadable]` device field must have exactly one matching direct child section +- every direct child section must correspond to a `#[loadable]` device field +- if the composite has no `#[program]` field, its own section bytecode must be empty +- composite headers are parsed by the macro only when the composite has a `#[header]` field; manual loaders can still inspect binary headers through `BinarySectionView::header_bytes()` / `BinarySectionView::parse_header::()`, or text headers through `TextSectionView::header_text()` + +## Multi-Section Bytecode + +The read-side bytecode API lives in `vihaco::binary` and `vihaco::loader`. + +```rust ignore +fn load_machine<'bc>(file: &'bc vihaco::BinaryBytecodeFile) -> eyre::Result { + let mut machine = Machine::default(); + machine.load_section(vihaco::LoadInput::from(file))?; + Ok(machine) +} + +let file: vihaco::BinaryBytecodeFile = vihaco::BinaryBytecodeFile::from_bytes(bytes)?; +let machine = load_machine(&file)?; +``` + +The v1 file layout is: + +```text +VHBC magic +u16 version = 1 +u16 flags = 0 +u64 program_context_len +program context bytes +root section bytes +``` + +The program context contains the shared `Module` tables except `code` and `extra`: constants, strings, functions, labels, `main_function`, `file`, and source symbols. `vihaco::program::ProgramContext` is the default context representation and is generic over the VM's constant value and type encodings. A binary bytecode file can also use a custom context type by implementing `BytecodeContext`; generated composite loading is generic over that context type, so Rust infers it from `BinaryBytecodeFile` / `LoadInput, C>`. Section bytecode can refer to shared constants with `vihaco::ConstantId`, a `u32` newtype that implements the bytecode field traits. + +Each section is: + +```text +section frame: +u64 section_len +u64 header_len +composite header bytes +section bytecode header: +u64 bytecode_len +bytecode bytes +child section table header: +u32 child_count +child table entries +child section bytes +``` + +Each child table entry stores: + +```text +u32 local_name_string +u64 section_offset +``` + +The section frame is part of every section, including the root section. The bytecode header starts after the composite header, and the section's bytecode immediately follows that length. Child-related metadata comes after the parent bytecode. `local_name_string` is resolved through `BytecodeContext::section_name` and represents the child's local section name. The parser builds each child's `SectionPath` by appending that resolved name to the parent path. Child section offsets are relative to the start of the containing section. + +### Text Multi-Section Bytecode + +Text bytecode uses `TextBytecodeFile`, `TextSectionView<'bc, C>`, `LoadInput`, and generated `LoadSection` machinery. The backing contents are the original source text and each section stores ranges into that string. Parse text files with `TextBytecodeFile::::from_text(source)`: + +```rust ignore +let file: vihaco::TextBytecodeFile = + vihaco::TextBytecodeFile::from_text(source)?; + +let mut machine = Machine::default(); +machine.load_section(vihaco::LoadInput::from(&file))?; +``` + +The file begins with the text magic/version marker, then a global context block: + +```text +vhbc1 + +.global: +global context text +.global. +``` + +`vhbc1` is the text spelling of version 1. The context body is delegated to `C::from_text(context_text)`; custom text formats usually provide a custom `BytecodeContext` that interprets this block. The context start marker `.global:` and end marker `.global.` must be at indentation level 0. + +After the context comes the root section. Sections use `.section(name):` to begin and `.section(name).` to end. The top-level section must be named `root` (`.section(root):` and `.section(root).`), and it is parsed as `SectionPath::root()`. Direct child section names become path components. + +```text +.section(root): + .header(root): + root header + .header(root). + + .text(root): + root bytecode + .text(root). + + .section(cpu): + .text(cpu): + cpu bytecode + .text(cpu). + .section(cpu). +.section(root). +``` + +Inside a section: + +- `.header(name):` / `.header(name).` delimit the composite header text for `TextSectionView::header_text()` +- `.text(name):` / `.text(name).` delimit the section bytecode text for `TextSectionView::text()` +- child sections are nested directly inside their parent section +- header, bytecode, and direct child section markers must be indented with exactly one tab more than their parent section +- section end markers must use the same indentation as their matching section start marker +- section names must be local names; `/` is rejected in a child marker name + +The text parser preserves the original header and bytecode ranges, including their leading tabs. Generated header loading trims `section.header_text()` before calling `FromStr` for the `#[header]` field type. Generated program loading delegates to `ProgramLoader`, which parses `section.text()` with `parse_instruction_stream::()`; instruction types loaded this way must implement both `Instruction` and `vihaco_parser_core::Parse`. + +`ProgramLoader` is the standard loader for section program streams. For binary bytecode it decodes `section.bytecode()` with `decode_instruction_stream::()`; for text bytecode it parses `section.text()` with `parse_instruction_stream::()`. In both cases it stores a cloned `ContextHandle`, implements `ProgramCounter`, and exposes functions, strings, and constants through `GetProgramGlobal` when `C: ProgramGlobals`. + ## Effect Continuation Is Hand-Written `#[composite]` generates the instruction enum and metadata, but it does **not** auto-deliver effects to observers. Continuing effects is something the runtime does explicitly: execute a component, then hand each returned effect to the types that observe it by calling their `Observe` impls. diff --git a/docs/src/pages/guide/parser.md b/docs/src/pages/guide/parser.md index d499287..79c6c46 100644 --- a/docs/src/pages/guide/parser.md +++ b/docs/src/pages/guide/parser.md @@ -115,7 +115,7 @@ That's it for the instruction-level surface. CPU instructions are mostly bare-form mnemonics with optional dot-qualified types: ```rust ignore -use vihaco::value::{Type, Value}; +use vihaco::program::{Type, Value}; use vihaco::Instruction; #[derive(Debug, Clone, PartialEq, Instruction, vihaco_parser::Parse)] diff --git a/licenserc.toml b/licenserc.toml index 9bccd14..6077540 100644 --- a/licenserc.toml +++ b/licenserc.toml @@ -15,12 +15,12 @@ excludes = [ "**/*.lock", "docs/**", "examples/**", - # trybuild UI fixtures: their .stderr snapshots are line-number - # sensitive, so a header would shift the diagnostics and break the - # tests. The trybuild *runner* files (tests/compile_errors.rs, - # tests/rfc0007_machine_compile_fail.rs) are not under these dirs and - # are still stamped. - "**/tests/ui/**", + # trybuild compile-fail fixtures: their .stderr snapshots are + # line-number sensitive, so a header would shift the diagnostics and + # break the tests. The trybuild *runner* files + # (tests/compile_errors.rs, tests/composite_machine_compile_fail.rs) + # are not under these dirs and are still stamped. + "**/tests/compile_fail/**", "**/tests/compile_errors/**", ]