NQP (Not Quite Perl)
repository·main·Indexed 18 days ago
https://github.com/raku/nqpA lightweight, Raku-like environment designed as a small runtime footprint for virtual machines, used for building compilers and libraries for platforms including MoarVM, the JVM, and JavaScript. It includes 6model, a framework for implementing object-oriented features through meta-objects and representations, and provides tools like nqp-m for executing scripts and a REPL.
What's inside nqp
- 6model is a framework for building implementations of object-oriented features (such as classes, interfaces, roles, or prototype objects). It does not provide a pre-built object system; instead, it provides the building blocks (meta-objects and representations) that allow you to define how your language's types behave and how they are stored in memory.
Rubyish Syntax and Language Features
mainThe
rubyishimplementation includes several Ruby-inspired features, but note that some behaviors (like string concatenation and truthiness) follow Rakuish rules.Supported Features
- Strings: Simple strings
'...',%q{...}, interpolating strings"...#{...}", and%Q{...}. - Quoted Words:
%w[aa bb cc]. - Control Flow:
if...then...elsif...else...endif,unless...end,while,until, andforloops. - Loops:
- Arrays:
for val in [10, 20, 30] do ... end - Hashes:
for kv in h do ... end(iterates by pairs).
- Arrays:
- Blocks/Closures: Supports lambda blocks and code block arguments (e.g.,
grep(arr) {|n| n % 2 == 0}). - Classes: Simple classes, objects with attributes, and method inheritance.
- Named Parameters: Supports Ruby 2.x style named parameters:
def foo(bar:42, baz:). - Heredocs: Literal
<<EOF ... EOFand interpolating<<"END" ... END. - Constants: Package constants via
Trig::PI = 3.1415926.
Important Syntax Deviations (Rakuish behavior)
- String Concatenation: Use the
~operator. The+operator is strictly for arithmetic addition. - Comparisons:
- Arithmetic:
>,==,<=. - String:
gt,eq,le.
- Arithmetic:
- Truthiness:
0,'0', and''are all consideredfalse. - Hash Access: Use angle braces
hash<key>or curlieshash{'key'}. - Hash Iteration: Iteration is done by pairs. Use the
keyandvaluebuilt-ins to access elements during iteration.
# Hash iteration example for item in {"apples" => 20, "bananas" => 35, "potatos" => 12} puts "#{key item} are #{value item} cents per Kg" end- Strings: Simple strings
How meta-objects work in 6model
mainA meta-object is a standard object that defines the behavior of other objects. It contains methods that respond to specific events in a type's lifetime (like method dispatch or type creation).
Key concepts:
- Meta-objects are just objects: There is no special distinction in 6model between a 'normal' object and a 'meta-object'; a meta-object is simply an object serving a specific role.
- Convention over enforcement: While 6model uses certain method names (like
find_method) to drive internal logic, the specific names of your meta-object methods (likeadd_method) are conventions you define. - The .HOW macro: In NQP, use the
.HOWmacro to access the meta-object of an instance. - Prototype-friendly: Meta-object methods typically take the type object as the first parameter to support prototype-based OO systems.
class SimpleMetaObject { has %!methods; method new_type() { my $meta-object := self.new(); return nqp::newtype($meta-object, 'HashAttrStore'); } method add_method($type, $name, $code) { %!methods{$name} := $code; } method find_method($type, $name) { %!methods{$name} } }How to implement an object-oriented type system with 6model
mainTo implement an object system, follow this four-step workflow:
- Identify Types: Determine the OO types your language needs (e.g., classes, roles, or prototype objects).
- Pick a Representation: Choose how objects are stored in memory. For example, use a fixed-slot allocation for known attributes or a hash-like structure for dynamic attributes.
- Implement Meta-objects: Create meta-objects that respond to lifecycle events (e.g., type declaration, adding methods, inheritance, or dynamic type checks).
- Compile Declarations: Map your language's type declarations to calls on the meta-objects.
Core Formula:
meta-object(behavior) +representation(storage/allocation) =full implementation of an OO type.Create POD blocks
mainTo define a multi-line documentation block, use the
=beginand=endsyntax. If you provide an identifier to the=begintag, you must provide the exact same identifier to the=endtag to close the block.=begin my_block This is some multi-line documentation text. =end my_blockCreate single-line POD entries
mainYou can create single-line documentation entries using the
=identifiersyntax. Any word can be used as an identifier except for the reserved wordcut. These entries can be placed inside a POD block.=identifier This is a documentation entryUnderstand Parameterization Interning Data
mainTo optimize memory and identity, the VM uses parameterization interning. When deserializing multiple compilation units (SCs) that contain identical parameterizations, the first unit's parameterization is reused ('wins').
An entry in the Parameterization Interning Data section is included only if:
- The parametric type originates from a different SC.
- All parameters are objects from a different SC.
Each entry follows this structure:
- Base-1 index of the owning SC: 32-bit integer (0 is invalid; 1 is the current SC).
- Index in owning SC: 32-bit integer locating the type object.
- Object list index: 32-bit integer locating the type object to be interned.
- STable list index: 32-bit integer locating the STable to be interned.
- Parameter count: 32-bit integer.
- Parameters: A sequence of object references for each parameter.
How to use type objects for object instantiation
mainIn 6model, a type object is the handle created when you pair a meta-class with a representation. It serves as the primary mechanism for creating new instances of an object.
Depending on your language's paradigm, you should use type objects as follows:
Class-based languages
Use the type object as the handle for creating instances. You can:
- Maintain a lookup table of type objects to manage instantiations.
- Store the handle within a 'class object' that manages the instantiation process.
Prototype-based languages
- You may only need to create a single type object. Store it in a persistent location to use for future instantiations.
- Alternatively, if instances are created via cloning, you can simply install the initial instance into the appropriate namespace and clone from there, bypassing the need to manually manage the type object handle.
Handling languages without methods in 6model
mainIf your target language supports objects but does not support methods, 6model is still a viable fit. You are not required to implement methods within the meta-object to handle
add_methodorfind_methodoperations.Implications:
- Your meta-object will not need to allocate storage for a method table.
- The only overhead is a small amount of unused space in the
s-table(specifically the method cache and v-table slots). This overhead is per-type, not per-instance, and is minimal (a few pointers per type).
Understand the REPR Compose Protocol
mainIn the 6model architecture, representations (REPRs) are responsible for memory layout, while meta-objects handle type-ness (dispatch, type checking, etc.).
When a meta-object's type definition is complete, it must configure the REPR. This configuration is performed via the REPR composition protocol using the
nqp::composetypeprimitive.To avoid complex object system dependencies, the protocol is defined entirely using standard arrays and hashes. A top-level hash is passed to the REPR, where each key represents a specific part of the protocol and its value provides the necessary configuration data.
Understand Code Reference Types and Serialization
mainCode references are handled based on whether the code is static or dynamic:
VM Static Code Reference
Occurs when the VM has never invoked the code during compilation. The thunk for dynamic compilation is tagged with a
STATIC_CODE_REFproperty and placed in the SC. Serialization includes the SC owning the code and the code ref index.Dynamic Compilation
When dynamic compilation occurs, the SC is updated with the code ref to the compiled code, which is then tagged as a static code reference (including the owning SC).
Closures
- Tagged Closures: If a code object is not marked static but already has an assigned SC, it is serialized like a static code object.
- Untagged Closures: If a code ref hasn't been tagged with an SC, the serializer:
- Traces back to the correct static code ref (via static lexical scope info).
- Creates an entry in the closures table indicating the static code ref to be cloned.
- Evaluates the outer scope context.
Optimize meta-object performance with caches
mainBecause meta-object method calls can be heavyweight, 6model allows meta-objects to publish "caches" that provide low-level, high-speed views of their data to the VM. The meta-object remains the authoritative source and is responsible for updating these caches.
Supported cache types:
- Name to method cache: A flat view (including inherited methods) used for fast dynamic method dispatch via hash table lookup.
- v-table: An index to the method cache, mapping method calls to specific slots. Ideal for static or gradually typed languages to enable fast array-based lookups.
- Type check cache: A low-level array used for fast
is-aordoes-aoperations.