RFC-0011: System Base Classes
Prompt Engineer: Mark Truluck mark@frame-lang.org Status: Implemented Created: 2026-04-18 Updated: 2026-04-20
Implementation status (2026-04-20): Shipped. Parser populates
SystemAst.bases: Vec<String>from the:clause on@@system. Each backend’sLanguageSyntaxincodegen/backend.rscarries anextends_keyword: Option<String>driving per-language emission (extends,:,<, orNonefor languages without inheritance). Normative documentation lives indocs/frame_language.mdunder “Base Classes”.
Problem
Frame systems generate standalone classes with no base class. This blocks scenarios that require inheritance:
- GDScript: requires
extends RefCounted(or similar) for scripts to be loadable viapreload().new() - Java/C#: framework integration often requires extending a base class or implementing interfaces
- Python: mixin patterns, ABC enforcement
- All languages: integration with host frameworks that expect a specific base type
The CodegenNode::Class.base_classes field exists in the codegen AST but is never populated. The infrastructure is ready — the user just has no way to specify base classes.
Syntax
@@system Pong : RefCounted {
interface:
get_serve_direction(): int
machine:
$Start {
get_serve_direction(): int { @@:(1) }
}
}
Multiple bases (for interfaces, traits, protocols):
@@system NetworkPlayer : Node, Serializable {
...
}
The : appears after the system name and optional parameters, before {:
@@system Robot($(x: int), name: str) : Node {
...
}
Semantics
- Frame passes base class names through verbatim — no type validation, no class-vs-interface distinction
- The target language’s compiler enforces inheritance rules (single vs multiple inheritance, class ordering, etc.)
- Systems without
:continue to generate standalone classes (no breaking change) - The base class list is stored in
SystemAst.bases: Vec<String>and flows intoCodegenNode::Class.base_classes
Per-Language Rendering
| Target | @@system Foo : A, B |
|---|---|
| Python | class Foo(A, B): |
| GDScript | extends A (file-level) + module-scope emission |
| TypeScript | class Foo extends A implements B { |
| JavaScript | class Foo extends A { (no interfaces) |
| Java | class Foo extends A implements B { |
| Kotlin | class Foo : A(), B { |
| C# | class Foo : A, B { |
| C++ | class Foo : public A, public B { |
| Swift | class Foo: A, B { |
| Go | embed A as field, no syntax for multiple |
| Dart | class Foo extends A implements B { |
| Rust | no class inheritance; generates impl B for Foo stubs |
| C | no class inheritance; ignored |
| PHP | class Foo extends A implements B { |
| Ruby | class Foo < A (single inheritance) |
| Lua | metatable-based, first base sets metatable |
| Erlang | ignored (gen_statem is functional) |
GDScript Special Behavior
When bases is non-empty and target is GDScript, the backend emits the system at module scope instead of as an inner class:
# With @@system Pong : RefCounted
extends RefCounted
class PongFrameEvent:
...
# System fields and methods at module scope (no class Pong: wrapper)
var _state_stack
var __compartment
...
func get_serve_direction() -> int:
...
This matches GDScript’s one-class-per-file convention and enables preload("res://pong.gd").new().
When bases is empty, the existing inner-class pattern is preserved for backward compatibility.
Implementation
Parser (pipeline_parser/mod.rs)
After parsing @@system Name(params), check for Token::Colon. If present, parse comma-separated type names until Token::LBrace:
@@system Name (params)? (: Base1, Base2, ...)? { ... }
AST (frame_ast.rs)
Add bases: Vec<String> to SystemAst.
Codegen (system_codegen.rs)
Populate CodegenNode::Class.base_classes from system.bases.
Backends
Each backend’s CodegenNode::Class rendering uses base_classes to emit the language-appropriate inheritance syntax. Most backends already have the rendering logic (currently dead code) — they just need the data.