#include"wasm/WasmBaselineCompile.h"// For BaseLocalIter #include"wasm/WasmBCDefs.h" #include"wasm/WasmBCRegDefs.h" #include"wasm/WasmBCStk.h" #include"wasm/WasmConstants.h"// For MaxFrameSize
// [SMDOC] Wasm baseline compiler's stack frame. // // For background, see "Wasm's ABIs" in WasmFrame.h, the following should never // be in conflict with that. // // The stack frame has four parts ("below" means at lower addresses): // // - the Frame element; // - the Local area, including the DebugFrame element and possibly a spilled // pointer to stack results, if any; allocated below the header with various // forms of alignment; // - the Dynamic area, comprising the temporary storage the compiler uses for // register spilling, allocated below the Local area; // - the Arguments area, comprising memory allocated for outgoing calls, // allocated below the Dynamic area. // // +==============================+ // | Incoming stack arg | // | ... | // ------------- +==============================+ // | Frame (fixed size) | // ------------- +==============================+ <-------------------- FP // ^ | DebugFrame (optional) | ^ ^ ^^ // localSize | Register arg local | | | || // | | ... | | | framePushed // | | Register stack result ptr?| | | || // | | Non-arg local | | | || // | | ... | | | || // | | (padding) | | | || // | | Instance pointer | | | || // | +------------------------------+ | | || // v | (padding) | | v || // ------------- +==============================+ currentStackHeight || // ^ | Dynamic (variable size) | | || // dynamicSize | ... | | || // v | ... | v || // ------------- | (free space, sometimes) | --------- v| // +==============================+ <----- SP not-during calls // | Arguments (sometimes) | | // | ... | v // +==============================+ <----- SP during calls // // The Frame is addressed off the stack pointer. masm.framePushed() is always // correct, and masm.getStackPointer() + masm.framePushed() always addresses the // Frame, with the DebugFrame optionally below it. // // The Local area (including the DebugFrame and, if needed, the spilled value of // the stack results area pointer) is laid out by BaseLocalIter and is allocated // and deallocated by standard prologue and epilogue functions that manipulate // the stack pointer, but it is accessed via BaseStackFrame. // // The Dynamic area is maintained by and accessed via BaseStackFrame. On some // systems (such as ARM64), the Dynamic memory may be allocated in chunks // because the SP needs a specific alignment, and in this case there will // normally be some free space directly above the SP. The stack height does not // include the free space, it reflects the logically used space only. // // The Dynamic area is where space for stack results is allocated when calling // functions that return results on the stack. If a function has stack results, // a pointer to the low address of the stack result area is passed as an // additional argument, according to the usual ABI. See // ABIResultIter::HasStackResults. // // The Arguments area is allocated and deallocated via BaseStackFrame (see // comments later) but is accessed directly off the stack pointer.
namespace js { namespace wasm {
usingnamespace js::jit;
// Abstraction of the height of the stack frame, to avoid type confusion.
class StackHeight { friendclass BaseStackFrameAllocator;
// Abstraction of the baseline compiler's stack frame (except for the Frame / // DebugFrame parts). See comments above for more. Remember, "below" on the // stack means at lower addresses. // // The abstraction is split into two parts: BaseStackFrameAllocator is // responsible for allocating and deallocating space on the stack and for // performing computations that are affected by how the allocation is performed; // BaseStackFrame then provides a pleasant interface for stack frame management.
class BaseStackFrameAllocator {
MacroAssembler& masm;
#ifdef RABALDR_CHUNKY_STACK // On platforms that require the stack pointer to be aligned on a boundary // greater than the typical stack item (eg, ARM64 requires 16-byte alignment // but items are 8 bytes), allocate stack memory in chunks, and use a // separate stack height variable to track the effective stack pointer // within the allocated area. Effectively, there's a variable amount of // free space directly above the stack pointer. See diagram above.
// The following must be true in order for the stack height to be // predictable at control flow joins: // // - The Local area is always aligned according to WasmStackAlignment, ie, // masm.framePushed() % WasmStackAlignment is zero after allocating // locals. // // - ChunkSize is always a multiple of WasmStackAlignment. // // - Pushing and popping are always in units of ChunkSize (hence preserving // alignment). // // - The free space on the stack (masm.framePushed() - currentStackHeight_) // is a predictable (nonnegative) amount.
// As an optimization, we pre-allocate some space on the stack, the size of // this allocation is InitialChunk and it must be a multiple of ChunkSize. // It is allocated as part of the function prologue and deallocated as part // of the epilogue, along with the locals. // // If ChunkSize is too large then we risk overflowing the stack on simple // recursions with few live values where stack overflow should not be a // risk; if it is too small we spend too much time adjusting the stack // pointer. // // Good values for ChunkSize are the subject of future empirical analysis; // eight words is just an educated guess.
// The current logical height of the frame is // currentStackHeight_ = localSize_ + dynamicSize // where dynamicSize is not accounted for explicitly and localSize_ also // includes size for the DebugFrame. // // The allocated size of the frame, provided by masm.framePushed(), is usually // larger than currentStackHeight_, notably at the beginning of execution when // we've allocated InitialChunk extra space.
uint32_t currentStackHeight_; #endif
// Size of the Local area in bytes (stable after BaseCompiler::init() has // called BaseStackFrame::setupLocals(), which in turn calls // BaseStackFrameAllocator::setLocalSize()), always rounded to the proper // stack alignment. The Local area is then allocated in beginFunction(), // following the allocation of the Header. See onFixedStackAllocated() // below.
protected: ////////////////////////////////////////////////////////////////////// // // The Local area - the static part of the frame.
// Record the size of the Local area, once it is known.
void setLocalSize(uint32_t localSize) {
MOZ_ASSERT(localSize == AlignBytes(localSize, sizeof(void*)), "localSize_ should be aligned to at least a pointer");
MOZ_ASSERT(localSize_ == UINT32_MAX);
localSize_ = localSize;
}
// Record the current stack height, after it has become stable in // beginFunction(). See also BaseStackFrame::onFixedStackAllocated().
public: // The fixed amount of memory, in bytes, allocated on the stack below the // Header for purposes such as locals and other fixed values. Includes all // necessary alignment, and on ARM64 also the initial chunk for the working // stack memory.
#ifdef RABALDR_CHUNKY_STACK // The allocated frame size is frequently larger than the logical stack // height; we round up to a chunk boundary, and special case the initial // chunk.
uint32_t framePushedForHeight(uint32_t logicalHeight) { if (logicalHeight <= fixedAllocSize()) { return fixedAllocSize();
} return fixedAllocSize() +
AlignBytes(logicalHeight - fixedAllocSize(), ChunkSize);
} #endif
protected: ////////////////////////////////////////////////////////////////////// // // The Dynamic area - the dynamic part of the frame, for spilling and saving // intermediate values.
// Offset off of sp_ for the slot at stack area location `offset`.
void popChunkyBytes(uint32_t bytes) {
checkChunkyInvariants();
currentStackHeight_ -= bytes; // Sometimes, popChunkyBytes() is used to pop a larger area, as when we drop // values consumed by a call, and we may need to drop several chunks. But // never drop the initial chunk. Crucially, the amount we drop is always an // integral number of chunks.
uint32_t freeSpace = masm.framePushed() - currentStackHeight_; if (freeSpace >= ChunkSize) {
uint32_t targetAllocSize = framePushedForHeight(currentStackHeight_);
uint32_t amountToFree = masm.framePushed() - targetAllocSize;
MOZ_ASSERT(amountToFree % ChunkSize == 0); if (amountToFree) {
masm.freeStack(amountToFree);
}
}
checkChunkyInvariants();
} #endif
// Before branching to an outer control label, pop the execution stack to // the level expected by that region, but do not update masm.framePushed() // as that will happen as compilation leaves the block. // // Note these operate directly on the stack pointer register.
// Given that there are |stackParamSize| bytes on the dynamic stack // corresponding to the stack results, return the stack height once these // parameters are popped.
// For most of WebAssembly, adjacent instructions have fallthrough control // flow between them, which allows us to simply thread the current stack // height through the compiler. There are two exceptions to this rule: when // leaving a block via dead code, and when entering the "else" arm of an "if". // In these cases, the stack height is the block entry height, plus any stack // values (results in the block exit case, parameters in the else entry case).
public: ////////////////////////////////////////////////////////////////////// // // The Argument area - for outgoing calls. // // We abstract these operations as an optimization: we can merge the freeing // of the argument area and dropping values off the stack after a call. But // they always amount to manipulating the real stack pointer by some amount. // // Note that we do not update currentStackHeight_ for this; the frame does // not know about outgoing arguments. But we do update framePushed(), so we // can still index into the frame below the outgoing arguments area.
// This is always equivalent to a masm.reserveStack() call.
void allocArgArea(size_t argSize) { if (argSize) {
masm.reserveStack(argSize);
}
}
// This frees the argument area allocated by allocArgArea(), and `argSize` // must be equal to the `argSize` argument to allocArgArea(). In addition // we drop some values from the frame, corresponding to the values that were // consumed by the call.
void freeArgAreaAndPopBytes(size_t argSize, size_t dropSize) { // The method is called to re-initialize SP after the call. Note that // this operation shall not be optimized for argSize + dropSize == 0. #ifdef RABALDR_CHUNKY_STACK // Freeing the outgoing arguments and freeing the consumed values have // different semantics here, which is why the operation is split.
masm.freeStackTo(masm.framePushed() - argSize);
popChunkyBytes(dropSize); #else
masm.freeStackTo(masm.framePushed() - (argSize + dropSize)); #endif
}
};
class BaseStackFrame final : public BaseStackFrameAllocator {
MacroAssembler& masm;
// The largest observed value of masm.framePushed(), ie, the size of the // stack frame. Read this for its true value only when code generation is // finished.
uint32_t maxFramePushed_;
// Patch point where we check for stack overflow.
CodeOffset stackAddOffset_;
// Low byte offset of pointer to stack results, if any.
mozilla::Maybe<int32_t> stackResultsPtrOffset_;
// The offset of instance pointer.
uint32_t instancePointerOffset_;
// Low byte offset of local area for true locals (not parameters).
uint32_t varLow_;
// High byte offset + 1 of local area for true locals.
uint32_t varHigh_;
// The stack pointer, cached for brevity.
RegisterOrSP sp_;
/////////////////////////////////////////////////////////////////////////// // // Stack management and overflow checking
// This must be called once beginFunction has allocated space for the Header // (the Frame and DebugFrame) and the Local area, and will record the current // frame size for internal use by the stack abstractions.
// We won't know until after we've generated code how big the frame will be // (we may need arbitrary spill slots and outgoing param slots) so emit a // patchable add that is patched in endFunction(). // // Note the platform scratch register may be used by branchPtr(), so // generally tmp must be something else.
/////////////////////////////////////////////////////////////////////////// // // Local area
struct Local { // Type of the value. const MIRType type;
// Byte offset from Frame "into" the locals, ie positive for true locals // and negative for incoming args that read directly from the arg area. // It assumes the stack is growing down and that locals are on the stack // at lower addresses than Frame, and is the offset from Frame of the // lowest-addressed byte of the local. const int32_t offs;
// Profiling shows that the number of parameters and locals frequently // touches or exceeds 8. So 16 seems like a reasonable starting point. using LocalVector = Vector<Local, 16, SystemAllocPolicy>;
// Initialize `localInfo` based on the types of `locals` and `args`.
[[nodiscard]] bool setupLocals(const ValTypeVector& locals, const ArgTypeVector& args, bool debugEnabled,
LocalVector* localInfo) { if (!localInfo->reserve(locals.length())) { returnfalse;
}
// Offset off of sp_ for `local`.
int32_t localOffsetFromSp(const Local& local) {
MOZ_ASSERT(!local.isStackArgument()); return localOffset(local.offs);
}
// Offset off of frame pointer for `stack argument`.
int32_t stackArgumentOffsetFromFp(const Local& local) {
MOZ_ASSERT(local.isStackArgument()); return -local.offs;
}
// The incoming stack result area pointer is for stack results of the function // being compiled. void loadIncomingStackResultAreaPtr(RegPtr reg) { const int32_t offset = stackResultsPtrOffset_.value();
Address src = offset < 0 ? Address(FramePointer, -offset)
: Address(sp_, stackOffset(offset));
masm.loadPtr(src, reg);
}
void storeIncomingStackResultAreaPtr(RegPtr reg) { // If we get here, that means the pointer to the stack results area was // passed in as a register, and therefore it will be spilled below the // frame, so the offset is a positive height.
MOZ_ASSERT(stackResultsPtrOffset_.value() > 0);
masm.storePtr(reg,
Address(sp_, stackOffset(stackResultsPtrOffset_.value())));
}
void loadInstancePtr(Register dst) { // Sometimes loadInstancePtr is used in context when SP is not sync is FP, // e.g. just after tail calls returns.
masm.loadPtr(Address(FramePointer, -instancePointerOffset_), dst);
}
// An outgoing stack result area pointer is for stack results of callees of // the function being compiled. void computeOutgoingStackResultAreaPtr(const StackResultsLoc& results,
RegPtr dest) {
MOZ_ASSERT(results.height() <= masm.framePushed());
uint32_t offsetFromSP = masm.framePushed() - results.height();
masm.moveStackPtrTo(dest); if (offsetFromSP) {
masm.addPtr(Imm32(offsetFromSP), dest);
}
}
private: // Offset off of sp_ for a local with offset `offset` from Frame.
int32_t localOffset(int32_t offset) { return masm.framePushed() - offset; }
public: /////////////////////////////////////////////////////////////////////////// // // Dynamic area
// Copy results from the top of the current stack frame to an area of memory, // and pop the stack accordingly. `dest` is the address of the low byte of // that memory. void popStackResultsToMemory(Register dest, uint32_t bytes, Register temp) {
MOZ_ASSERT(bytes <= currentStackHeight());
MOZ_ASSERT(bytes % sizeof(uint32_t) == 0);
uint32_t bytesToPop = bytes;
uint32_t srcOffset = stackOffset(currentStackHeight());
uint32_t destOffset = 0; while (bytes >= sizeof(intptr_t)) {
masm.loadPtr(Address(sp_, srcOffset), temp);
masm.storePtr(temp, Address(dest, destOffset));
destOffset += sizeof(intptr_t);
srcOffset += sizeof(intptr_t);
bytes -= sizeof(intptr_t);
} if (bytes) {
MOZ_ASSERT(bytes == sizeof(uint32_t));
masm.load32(Address(sp_, srcOffset), temp);
masm.store32(temp, Address(dest, destOffset));
}
popBytes(bytesToPop);
}
void storeImmediateF32ToStack(float imm, uint32_t destHeight, Register temp) { union {
int32_t i32; float f32;
} bits = {.f32 = imm};
static_assert(sizeof(bits) == 4); // Do not store 4 bytes if StackSizeOfFloat == 8. It's probably OK to do // so, but it costs little to store something predictable. if (StackSizeOfFloat == 4) {
store32BitsToStack(bits.i32, destHeight, temp);
} else {
store64BitsToStack(uint32_t(bits.i32), destHeight, temp);
}
}
#ifdef ENABLE_WASM_SIMD void storeImmediateV128ToStack(V128 imm, uint32_t destHeight, Register temp) { union {
int32_t i32[4];
uint8_t bytes[16];
} bits{};
static_assert(sizeof(bits) == 16);
memcpy(bits.bytes, imm.bytes, 16); for (unsigned i = 0; i < 4; i++) {
store32BitsToStack(bits.i32[i], destHeight - i * sizeof(int32_t), temp);
}
} #endif
};
////////////////////////////////////////////////////////////////////////////// // // MachineStackTracker, used for stack-slot pointerness tracking.
// An expensive operation in stack-map creation is copying of the // MachineStackTracker (MST) into the final StackMap. This is done in // StackMapGenerator::createStackMap. Given that this is basically a // bit-array copy, it is reasonable to ask whether the two classes could have // a more similar representation, so that the copy could then be done with // `memcpy`. // // Although in principle feasible, the follow complications exist, and so for // the moment, this has not been done. // // * StackMap is optimised for compact size (storage) since there will be // many, so it uses a true bitmap. MST is intended to be fast and simple, // and only one exists at once (per compilation thread). Doing this would // require MST to use a true bitmap, and hence .. // // * .. the copying can't be a straight memcpy, since StackMap has entries for // words not covered by MST. Hence the copy would need to shift bits in // each byte left or right (statistically speaking, in 7 cases out of 8) in // order to ensure no "holes" in the resulting bitmap. // // * Furthermore the copying would need to logically invert the direction of // the stacks. For MST, index zero in the vector corresponds to the highest // address in the stack. For StackMap, bit index zero corresponds to the // lowest address in the stack. // // * Finally, StackMap is a variable-length structure whose size must be known // at creation time. The size of an MST by contrast isn't known at creation // time -- it grows as the baseline compiler pushes stuff on its value // stack. That's why it has to have vector entry 0 being the highest address. // // * Although not directly relevant, StackMaps are also created by the via-Ion // compilation routes, by translation from the pre-existing "JS-era" // LSafePoints (CreateStackMapFromLSafepoint). So if we want to mash // StackMap around to suit baseline better, we also need to ensure it // doesn't break Ion somehow.
class MachineStackTracker { // Simulates the machine's stack, with one bool per word. The booleans are // represented as `uint8_t`s so as to guarantee the element size is one // byte. Index zero in this vector corresponds to the highest address in // the machine's stack. The last entry corresponds to what SP currently // points at. This all assumes a grow-down stack. // // numPtrs_ contains the number of "true" values in vec_, and is therefore // redundant. But it serves as a constant-time way to detect the common // case where vec_ holds no "true" values.
size_t numPtrs_;
Vector<uint8_t, 64, SystemAllocPolicy> vec_;
public:
MachineStackTracker() : numPtrs_(0) {}
~MachineStackTracker() { #ifdef DEBUG
size_t n = 0; for (uint8_t b : vec_) {
n += (b ? 1 : 0);
}
MOZ_ASSERT(n == numPtrs_); #endif
}
// Clone this MachineStackTracker, writing the result at |dst|.
[[nodiscard]] bool cloneTo(MachineStackTracker* dst);
// Notionally push |n| non-pointers on the stack.
[[nodiscard]] bool pushNonGCPointers(size_t n) { return vec_.appendN(uint8_t(false), n);
}
// Mark the stack slot |offsetFromSP| up from the bottom as holding a // pointer. void setGCPointer(size_t offsetFromSP) { // offsetFromSP == 0 denotes the most recently pushed item, == 1 the // second most recently pushed item, etc.
MOZ_ASSERT(offsetFromSP < vec_.length());
// Query the pointerness of the slot |offsetFromSP| up from the bottom. bool isGCPointer(size_t offsetFromSP) const {
MOZ_ASSERT(offsetFromSP < vec_.length());
// Return the number of words tracked by this MachineStackTracker.
size_t length() const { return vec_.length(); }
// Return the number of pointer-typed words tracked by this // MachineStackTracker.
size_t numPtrs() const {
MOZ_ASSERT(numPtrs_ <= length()); return numPtrs_;
}
// Discard all contents, but (per mozilla::Vector::clear semantics) don't // free or reallocate any dynamic storage associated with |vec_|. void clear() {
vec_.clear();
numPtrs_ = 0;
}
// An iterator that produces indices of reftyped slots, starting at the // logical bottom of the (grow-down) stack. Indices have the same meaning // as the arguments to `isGCPointer`. That is, if this iterator produces a // value `i`, then it means that `isGCPointer(i) == true`; if the value `i` // is never produced then `isGCPointer(i) == false`. The values are // produced in ascending order. // // Because most slots are non-reftyped, some effort has been put into // skipping over large groups of non-reftyped slots quickly. class Iter { // Both `bufU8_` and `bufU32_` are made to point to `vec_`s array of // `uint8_t`s, so we can scan (backwards) through it either in bytes or // 32-bit words. Recall that the last element in `vec_` pertains to the // lowest-addressed word in the machine's grow-down stack, and we want to // iterate logically "up" this stack, so we need to iterate backwards // through `vec_`. // // This dual-pointer scheme assumes that the `vec_`s content array is at // least 32-bit aligned. const uint8_t* bufU8_; const uint32_t* bufU32_; // The number of elements in `bufU8_`. const size_t nElems_; // The index in `bufU8_` where the next search should start.
size_t next_;
// It is important, for termination of the search loop in `next()`, that // this has the value obtained by subtracting 1 from size_t(0). static constexpr size_t FINISHED = ~size_t(0);
static_assert(FINISHED == size_t(0) - 1);
// Returns the next index `i` for which `isGCPointer(i) == true`.
size_t get() { while (next_ != FINISHED) { if (bufU8_[next_]) {
next_--; return nElems_ - 1 - (next_ + 1);
} // Invariant: next_ != FINISHED (so it's still a valid index) // and: bufU8_[next_] == 0 // (so we need to move backwards by at least 1) // // BEGIN optimization -- this could be removed without affecting // correctness. if ((next_ & 7) == 0) { // We're at the "bottom" of the current dual-4-element word. Check // if we can jump backwards by 8. This saves a conditional branch // and a few cycles by ORing two adjacent 32-bit words together, // whilst not requiring 64-bit alignment of `bufU32_`. while (next_ >= 8 &&
(bufU32_[(next_ - 4) >> 2] | bufU32_[(next_ - 8) >> 2]) == 0) {
next_ -= 8;
}
} // END optimization
next_--;
} return FINISHED;
}
};
};
////////////////////////////////////////////////////////////////////////////// // // StackMapGenerator, which carries all state needed to create stackmaps.
enumclass HasDebugFrameWithLiveRefs { No, Maybe };
struct StackMapGenerator { private: // --- These are constant for the life of the function's compilation ---
// For generating stackmaps, we'll need to know the offsets of registers // as saved by the trap exit stub. const RegisterOffsets& trapExitLayout_; const size_t trapExitLayoutNumWords_;
// Completed stackmaps are added here
StackMaps* stackMaps_;
// So as to be able to get current offset when creating stackmaps const MacroAssembler& masm_;
public: // --- These are constant once we've completed beginFunction() ---
// The number of bytes of arguments passed to this function in memory.
size_t numStackArgBytes;
// This holds masm.framePushed at entry to the function's body. It is a // Maybe because createStackMap needs to know whether or not we're still // in the prologue. It makes a Nothing-to-Some transition just once per // function.
mozilla::Maybe<uint32_t> framePushedAtEntryToBody;
// --- These can change at any point ---
// This holds masm.framePushed at it would be be for a function call // instruction, but excluding the stack area used to pass arguments in // memory. That is, for an upcoming function call, this will hold // // masm.framePushed() at the call instruction - // StackArgAreaSizeAligned(argumentTypes) // // This value denotes the lowest-addressed stack word covered by the current // function's stackmap. Words below this point form the highest-addressed // area of the callee's stackmap. Note that all alignment padding above the // arguments-in-memory themselves belongs to the callee's stackmap, as return // calls will replace the function arguments with a new set of arguments which // may have different alignment. // // When not inside a function call setup/teardown sequence, it is Nothing. // It can make Nothing-to/from-Some transitions arbitrarily as we progress // through the function body.
mozilla::Maybe<uint32_t> framePushedExcludingOutboundCallArgs;
// The number of memory-resident, ref-typed entries on the containing // BaseCompiler::stk_.
size_t memRefsOnStk;
// This is a copy of machineStackTracker that is used only within individual // calls to createStackMap. It is here only to avoid possible heap allocation // costs resulting from making it local to createStackMap().
MachineStackTracker augmentedMst;
// At the beginning of a function, we may have live roots in registers (as // arguments) at the point where we perform a stack overflow check. This // method generates the "extra" stackmap entries to describe that, in the // case that the check fails and we wind up calling into the wasm exit // stub, as generated by GenerateTrapExit(). // // The resulting map must correspond precisely with the stack layout // created for the integer registers as saved by (code generated by) // GenerateTrapExit(). To do that we use trapExitLayout_ and // trapExitLayoutNumWords_, which together comprise a description of the // layout and are created by GenerateTrapExitRegisterOffsets().
[[nodiscard]] bool generateStackmapEntriesForTrapExit( const ArgTypeVector& args, ExitStubMapVector* extras);
// Creates a stackmap incorporating pointers from the current operand // stack |stk|, incorporating possible extra pointers in |extra| at the // lower addressed end, and possibly with the associated frame having a // DebugFrame that must be traced, as indicated by |debugFrameWithLiveRefs|.
[[nodiscard]] bool createStackMap( constchar* who, const ExitStubMapVector& extras,
HasDebugFrameWithLiveRefs debugFrameWithLiveRefs, const StkVector& stk,
wasm::StackMap** result);
};
} // namespace wasm
} // namespace js
#endif// wasm_wasm_baseline_frame_h
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.17 Sekunden
(vorverarbeitet am 2026-09-30)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.