Skip to content

Commit 50029ae

Browse files
committed
Add named state layouts and source-defined display
1 parent 8b0c11d commit 50029ae

68 files changed

Lines changed: 2865 additions & 322 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ compile-time and output-size measurements are recorded in
220220
and `size` fields.
221221
- Compile-time checked `sig"..."` literals with byte/nibble wildcards and
222222
overlapping page-based module scans.
223+
- Checked `v"1.2.3.4"` Windows file-version literals and source-defined PE
224+
resource parsing through `Module.fileVersion()`.
223225
- Nominal `address` values, typed synchronous/retried primitive reads,
224226
RIP-relative 32-bit address decoding, arbitrary-range scans, and reusable
225227
64-bit pointer traversal.

‎TODO.md‎

Lines changed: 63 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,15 @@ type `GbaEmulator`; scripts never retain or attach the emulator themselves.
349349
represented in the catalog. Begin with memory reading, formatting/
350350
interpolation, equality, and numeric operations; decide separately whether
351351
user code can declare or implement traits.
352+
- [x] Make nominal standard-library `Display` implementations source-driven.
353+
A type declaring `Display` can tag one parameterless `String`-returning method
354+
with `@display`; generated catalog metadata owns the implementation identity,
355+
validates the contract, retains the ordinary hidden function through
356+
reachability, and dispatches `as String`, interpolation, `print`, and
357+
`setVariable` through the same body. `FileVersion.toString()` now renders the
358+
dotted four-part version without any type-specific formatting branch in
359+
codegen. Primitive formatting remains the built-in fallback; user-defined
360+
trait implementations remain part of the broader trait design.
352361
- [x] Add source-defined standard-library capability inheritance. Declarations
353362
such as `Numeric<T: Equatable>` and `Integer<T: Numeric + Display>` now drive transitive
354363
satisfaction, method availability, inference, and minimal rendered bounds;
@@ -389,23 +398,33 @@ executable name.
389398
address global, while a single ordinary state expression reads it. Both
390399
supported builds and the never-ready unsupported-build path have runtime
391400
coverage; no layout DSL improves this shape.
392-
- [ ] Separate named memory layouts from layout detection. Layout declarations
393-
should co-locate concise `at` fields, documentation, and a stable source
394-
identity; a distinct implicitly-suspending attachment selector should use
395-
ordinary typed SplitScript expressions, `await`/`retry`, matches, process
396-
metadata, module sizes, signatures, and reads to select exactly one layout.
397-
Do not encode `when module(...).size == ...` as the only probe grammar.
398-
- [ ] Generate a typed layout enum/value rather than exposing ASL's mutable
399-
string `version`. Lifecycle code must be able to compare and exhaustively
400-
match the selected layout without stringly-typed names. Decide the final
401-
selector syntax together with its error policy: unsupported and ambiguous
402-
builds must stay visibly unattached or fail with one useful diagnostic,
403-
never silently fall back to the first declaration.
404-
- [ ] Implement the first vertical slice for layouts with an identical field
405-
name/type interface. Validate every layout against one canonical snapshot
406-
shape and select its pointer sources before `onAttach` completes. This
407-
covers roughly four fifths of the surveyed versioned scripts while keeping
408-
`current.field` and `old.field` unchanged.
401+
- [x] Separate named memory layouts from layout detection. `layout Name { ... }`
402+
declarations co-locate concise `at` fields, documentation, and stable enum
403+
variant identities. The existing implicitly-suspending `onAttach` block is
404+
the selector: it uses ordinary typed SplitScript expressions, `await`/`retry`,
405+
matches, process metadata, module sizes, signatures, and reads, then returns
406+
exactly one generated layout variant. This keeps one attachment lifecycle
407+
phase instead of adding a separate `selectLayout` action and does not encode
408+
`when module(...).size == ...` as a privileged probe grammar.
409+
- [x] Generate a typed `StateLayout` enum and read-only `layout` value rather
410+
than exposing ASL's mutable string `version`. Lifecycle code can compare and
411+
exhaustively match it. Named layouts require `onAttach` to return a variant
412+
on every completing path; `await process.closed()` is recognized as the
413+
explicit non-completing unsupported-build path, so there is no silent first-
414+
layout fallback. `layout` is rejected inside `onAttach` before it exists.
415+
- [x] Implement the first vertical slice for layouts with an identical field
416+
name/type interface. Every layout is validated order-independently against
417+
one canonical snapshot shape, and the selected variant dispatches each
418+
transactional field read to the correct pointer source after `onAttach`
419+
completes. `current.field` and `old.field` remain unchanged. Parser,
420+
formatter, completion, hover, semantic highlighting, Wasm validation, and a
421+
Steam/GOG/unsupported runtime fixture cover the complete feature.
422+
The corpus-derived Martha Is Dead port additionally validates three real
423+
legacy-ASL layouts, module-size selection, module-relative pointer paths,
424+
load removal, and the unsupported-build lifecycle end to end.
425+
The Borderlands port validates typed `FileVersion` selection and a shared
426+
interface whose old build uses direct module offsets while its patched
427+
build uses expression-backed PE32 pointer paths.
409428
- [ ] Design non-uniform layouts separately. The survey found 61 differing
410429
interfaces with stable field types and 28 with at least one same-named type
411430
conflict. Evaluate common-field projection plus typed variant payloads;
@@ -451,16 +470,39 @@ executable name.
451470
- [x] Expose `await process.mainModule()` using that matched candidate, so
452471
common `modules.First().ModuleMemorySize` selectors do not repeat executable
453472
strings or need a special module-size grammar.
454-
- [ ] Add module enumeration/search, path or host-supplied file/product-version
455-
identity, and a deterministic executable fingerprint as corpus ports require
456-
them. Prefer host-provided metadata/fingerprinting over unrestricted
457-
filesystem access or reading and hashing an entire module from Wasm.
473+
- [ ] Add module enumeration/search, product-version identity, and a
474+
deterministic executable fingerprint as corpus ports require them. Prefer
475+
host-provided metadata/fingerprinting over unrestricted filesystem access
476+
or reading and hashing an entire module from Wasm.
477+
- [x] Preserve the queried module name privately in each `Module` value and
478+
expose fallible `Module.path()`. It uses ASR's host-provided portable path
479+
ABI, bounds the returned UTF-8 payload, and does not grant general file
480+
access. Main-module values retain the actual matched process candidate;
481+
named-module values retain their requested module identity.
482+
- [x] Add source-defined `Module.fileVersion() -> FileVersion!` by porting
483+
ASR's bounded PE-resource traversal into `standard.split`. The typed
484+
`FileVersion` record exposes `major`, `minor`, `build`, and `privatePart`
485+
components and is equatable, avoiding legacy `FileVersionInfo` strings
486+
whose punctuation differs between games and APIs. Only typed process
487+
reads remain intrinsic; DOS/PE validation, resource-directory loops,
488+
`Result` propagation, and record construction are ordinary library code.
489+
The checked `v"major.minor.build.private"` literal produces the same
490+
typed value, validates exactly four decimal `u16` components at parse
491+
time, and keeps version comparisons concise without exposing record
492+
construction. The compiler lowers it through source-defined
493+
`FileVersion.fromParts` rather than a second backend implementation.
458494
- [ ] Make the existing attach-time-discovered state-source pattern a documented
459495
first-class contract. Scripts can already scan/follow/resolve into globals in
460496
`onAttach` and use expression-backed state fields; add a canonical recipe,
461497
clearer typing/tooling, and specified behavior for temporarily unreadable and
462498
optional fields so authors do not recreate `MemoryWatcher` manually. Keep
463499
process-close cancellation and transactional snapshot rotation automatic.
500+
The Borderlands port is a focused syntax-pressure case: its PE32 layout must
501+
currently spell source-defined `MemoryPath` expressions because declarative
502+
pointer paths have one implicit pointer width. Use it to decide whether
503+
pointer width belongs on an individual path, a named layout, retained module
504+
metadata, or a more general discovered state-source value; do not add an
505+
`at32` pseudo-keyword.
464506
- [ ] Extend the existing signature APIs only where the corpus proves a gap:
465507
reusable scan targets, multiple/fallback signatures, range and memory-page
466508
selection, capture/offset transforms, relative-address decoding, and concise

‎crates/splitscript-stdlib-loader/src/generate.rs‎

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,25 @@ impl<'a> CatalogGenerator<'a> {
185185

186186
fn emit_type(&self, output: &mut String, declaration: &StructDeclaration, kind: &str) {
187187
let id = ident(&declaration.name);
188+
let display = declaration
189+
.functions
190+
.iter()
191+
.find(|function| has_attribute(&function.attributes, "display"))
192+
.map_or_else(
193+
|| "None".to_owned(),
194+
|function| {
195+
format!(
196+
"Some(StdlibItemId::{}{})",
197+
path_ident(&declaration.name),
198+
ident(&function.name)
199+
)
200+
},
201+
);
188202
if has_attribute(&declaration.attributes, "testOnly") {
189203
output.push_str("#[cfg(test)] ");
190204
}
191205
output.push_str(&format!(
192-
"StdlibType {{ id: StdlibTypeId::{id}, name: {}, kind: StdlibTypeKind::{kind}, capabilities: {}, representation: {}, value_usage: {}, documentation: documentation({}, {}) }},\n",
206+
"StdlibType {{ id: StdlibTypeId::{id}, name: {}, kind: StdlibTypeKind::{kind}, capabilities: {}, display: {display}, representation: {}, value_usage: {}, documentation: documentation({}, {}) }},\n",
193207
quote(&declaration.name), self.capabilities(&declaration.attributes),
194208
self.representation(&declaration.attributes), self.value_usage(&declaration.attributes),
195209
quote(&declaration.documentation.summary), quote(&declaration.documentation.details)
@@ -199,7 +213,7 @@ impl<'a> CatalogGenerator<'a> {
199213
fn emit_enum_type(&self, output: &mut String, declaration: &crate::EnumDeclaration) {
200214
let id = ident(&declaration.name);
201215
output.push_str(&format!(
202-
"StdlibType {{ id: StdlibTypeId::{id}, name: {}, kind: StdlibTypeKind::Enum, capabilities: {}, representation: {}, value_usage: {}, documentation: documentation({}, {}) }},\n",
216+
"StdlibType {{ id: StdlibTypeId::{id}, name: {}, kind: StdlibTypeKind::Enum, capabilities: {}, display: None, representation: {}, value_usage: {}, documentation: documentation({}, {}) }},\n",
203217
quote(&declaration.name), self.capabilities(&declaration.attributes),
204218
self.representation(&declaration.attributes), self.value_usage(&declaration.attributes),
205219
quote(&declaration.documentation.summary), quote(&declaration.documentation.details)
@@ -871,6 +885,50 @@ struct Duration {
871885
assert!(!generated.contains("IntrinsicId::DurationFromFrames"));
872886
}
873887

888+
#[test]
889+
fn standard_types_can_name_a_source_defined_display_implementation() {
890+
let source = r#"
891+
/// Displayable values.
892+
@behavior(declared)
893+
capability Display<T> {}
894+
895+
/// Text.
896+
@representation(gcArray, u8, mutable, nullable)
897+
@valueUsage(localVariable)
898+
@capabilities(Display)
899+
intrinsic type String {}
900+
901+
/// A file version.
902+
@representation(gcStruct, nullable)
903+
@valueUsage(localVariable)
904+
@capabilities(Display)
905+
struct FileVersion {
906+
/// Major component.
907+
major: u16,
908+
909+
/// Formats the version.
910+
///
911+
/// Uses dotted components.
912+
///
913+
/// # Example
914+
///
915+
/// Display a version
916+
///
917+
/// ```splitscript
918+
/// print(version)
919+
/// ```
920+
@display
921+
fn toString() -> String {
922+
return `{self.major}`
923+
}
924+
}
925+
"#;
926+
let generated = generate_catalog(&parse(source).unwrap()).unwrap();
927+
assert!(generated.contains("display: Some(StdlibItemId::FileVersionToString)"));
928+
assert!(generated.contains("Implementation::LibraryBody"));
929+
assert!(generated.contains("__splitscript_stdlib_FileVersionToString"));
930+
}
931+
874932
#[test]
875933
fn generic_capability_body_is_preserved_as_a_typed_template() {
876934
let source = r#"

‎crates/splitscript-stdlib-loader/src/validation.rs‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,18 @@ impl<'a> Validator<'a> {
221221
attributes: value.attributes.clone(),
222222
functions: value.functions.clone(),
223223
};
224+
if owner
225+
.functions
226+
.iter()
227+
.filter(|function| has_attribute(&function.attributes, "display"))
228+
.count()
229+
> 1
230+
{
231+
self.error(format!(
232+
"standard-library type `{}` has multiple display implementations",
233+
owner.name
234+
));
235+
}
224236
self.validate_functions(&owner.name, &owner.functions, &[]);
225237
}
226238

@@ -247,7 +259,62 @@ impl<'a> Validator<'a> {
247259
self.error(format!("`{owner}` repeats function `{}`", function.name));
248260
}
249261
self.validate_documentation(&qualified, &function.documentation, true);
250-
self.validate_attributes(&qualified, &function.attributes, &["intrinsic"]);
262+
self.validate_attributes(&qualified, &function.attributes, &["intrinsic", "display"]);
263+
if has_attribute(&function.attributes, "display") {
264+
if function
265+
.attributes
266+
.iter()
267+
.find(|attribute| attribute.name == "display")
268+
.is_some_and(|attribute| !attribute.arguments.is_empty())
269+
{
270+
self.error(format!(
271+
"`{qualified}` marker attribute `@display` does not accept arguments"
272+
));
273+
}
274+
let owner_is_standard_type = self.types.contains(owner);
275+
let owner_has_display = self.library.declarations.iter().any(|declaration| {
276+
matches!(
277+
declaration,
278+
Declaration::Struct(value) | Declaration::IntrinsicType(value)
279+
if value.name == owner
280+
&& value.attributes.iter().any(|attribute| {
281+
attribute.name == "capabilities"
282+
&& attribute.arguments.iter().any(|argument| {
283+
matches!(
284+
argument,
285+
AttributeArgument::Name(capability)
286+
if capability == "Display"
287+
)
288+
})
289+
})
290+
)
291+
});
292+
if !owner_is_standard_type {
293+
self.error(format!(
294+
"`{qualified}` uses `@display` outside a standard-library type"
295+
));
296+
}
297+
if function.is_static || !function.parameters.is_empty() {
298+
self.error(format!(
299+
"`{qualified}` display implementation must be a parameterless method"
300+
));
301+
}
302+
if function.result != Type::Name("String".to_owned()) {
303+
self.error(format!(
304+
"`{qualified}` display implementation must return `String`"
305+
));
306+
}
307+
if !owner_has_display {
308+
self.error(format!(
309+
"`{qualified}` provides a display implementation but `{owner}` does not declare `Display`"
310+
));
311+
}
312+
if function.body.is_none() {
313+
self.error(format!(
314+
"`{qualified}` display implementation must have a source body"
315+
));
316+
}
317+
}
251318
let intrinsic = self
252319
.optional_name_attribute(&qualified, &function.attributes, "intrinsic")
253320
.is_some();
@@ -574,6 +641,10 @@ fn declaration_name(declaration: &Declaration) -> &str {
574641
}
575642
}
576643

644+
fn has_attribute(attributes: &[Attribute], name: &str) -> bool {
645+
attributes.iter().any(|attribute| attribute.name == name)
646+
}
647+
577648
fn capability_hierarchy_has_cycle<'a>(
578649
capability: &'a str,
579650
hierarchy: &HashMap<&'a str, &'a [String]>,

‎crates/splitscript-syntax/src/ast.rs‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,21 @@ pub struct Program {
253253
}
254254

255255
impl Program {
256+
/// Iterates both ordinary source enums and the enum generated by named
257+
/// state layouts.
258+
pub fn enum_declarations(&self) -> impl Iterator<Item = &EnumDecl> {
259+
self.enums.iter().chain(
260+
self.state
261+
.as_ref()
262+
.and_then(|state| state.layout_enum.as_ref()),
263+
)
264+
}
265+
266+
pub fn enum_declaration(&self, id: EnumId) -> Option<&EnumDecl> {
267+
self.enum_declarations()
268+
.find(|enumeration| enumeration.id == id)
269+
}
270+
256271
/// Iterates nominal type names together with their stable syntax identity
257272
/// and source span.
258273
pub fn type_names(&self) -> impl Iterator<Item = (TypeNameId, &str, Span)> {
@@ -400,6 +415,58 @@ pub struct Parameter {
400415
pub struct StateDecl {
401416
pub provider: Option<StateProviderRef>,
402417
pub processes: Vec<String>,
418+
/// Fields of the ordinary single-layout form. This is empty when named
419+
/// layouts are present.
420+
pub fields: Vec<StateField>,
421+
/// Versioned memory layouts. Every layout is required to expose the same
422+
/// field interface by semantic analysis.
423+
pub layouts: Vec<StateLayoutDecl>,
424+
/// The generated enum represented by the named layout declarations.
425+
pub layout_enum: Option<EnumDecl>,
426+
/// Stable identity of the implicit read-only `layout` value.
427+
pub layout_value: Option<ValueId>,
428+
pub span: Span,
429+
}
430+
431+
impl StateDecl {
432+
pub fn canonical_fields(&self) -> &[StateField] {
433+
self.layouts
434+
.first()
435+
.map_or(self.fields.as_slice(), |layout| layout.fields.as_slice())
436+
}
437+
438+
pub fn all_fields(&self) -> impl Iterator<Item = &StateField> {
439+
self.fields
440+
.iter()
441+
.chain(self.layouts.iter().flat_map(|layout| &layout.fields))
442+
}
443+
444+
/// Returns the concrete read declarations in canonical field order for
445+
/// each layout. This is the backend ABI between read-function planning and
446+
/// per-tick dispatch.
447+
pub fn ordered_read_fields(&self) -> Vec<&StateField> {
448+
if self.layouts.is_empty() {
449+
return self.fields.iter().collect();
450+
}
451+
let canonical = self.canonical_fields();
452+
self.layouts
453+
.iter()
454+
.flat_map(|layout| {
455+
canonical.iter().map(|field| {
456+
layout
457+
.fields
458+
.iter()
459+
.find(|candidate| candidate.name == field.name)
460+
.expect("checked layouts expose the canonical state interface")
461+
})
462+
})
463+
.collect()
464+
}
465+
}
466+
467+
#[derive(Debug, Clone)]
468+
pub struct StateLayoutDecl {
469+
pub variant: EnumVariantId,
403470
pub fields: Vec<StateField>,
404471
pub span: Span,
405472
}

0 commit comments

Comments
 (0)