The Criteria: Every Mechanism, Its Harm, and Its Points

What each of the 75 mechanisms means, why it helps or hurts, how harmful it is, and why it removes the points it removes — the reference behind every score in the 65-language comparison.

1 The Criteria

Companion to: The Great Language Smackdown · the Scoreboard and the full per-language data · the scoring model

Every language on this site is measured against one inventory of 75 mechanisms — the same list, with the same weights, and the same arithmetic, for all 65. The inventory has three parts: Core coupling (34 mechanisms), Ecosystem coupling (29), and Engineering readiness (12). No language gets a private criterion, and no mechanism is applied to one language and then set aside for another. That is what makes the comparison a comparison: a difference between two scores is a difference in which mechanisms are present, never a difference in how strictly the two languages were judged.

This page explains each criterion. For every mechanism it gives what the mechanism is, the kind of coupling it imposes and why, how harmful it is — the weight tier that removes the points — and a concrete example. The Scoreboard publishes the other half: which mechanisms each language has, and the score that follows. The scoring model gives the arithmetic and the tests that decide presence.

1.1 What coupling means here

Two parts of a program are coupled where they must agree about one shared thing: a type, a heap, a lock, a name space, a platform ABI, a runtime. The agreement is the cost. It has to be established when the code is written, it has to be maintained while the code lives, and it has to be re-established whenever either side changes.

A language mechanism is a coupling criterion when it forces agreement that the programmer cannot refuse. Three properties decide how much that costs:

Property Question Effect on the weight
Breadth How much of the program has to agree — one call site, one module, or the whole program? A wider agreement costs more
Escape Can a programmer opt out of the mechanism locally? No escape costs more
Silence Does the language say so, or must a reader discover it? A silent obligation costs more

A mechanism that makes the whole program agree, with no local way out, and nothing in the language that records the agreement, is High. A mechanism that forces agreement across ordinary code, escapable only by a convention that the whole codebase must keep, is Moderate. A mechanism whose agreement is confined to a boundary, and removable by a contained workaround, is Minor.

That is the whole rationale for the weights. The tier is a judgment about breadth, escape, and silence — not a count of incidents, and not a substitute for measurement.

Every entry names its coupling. After the tier, each entry states the kind of coupling the mechanism imposes and why it cannot be refused: which two parties must agree, what they must agree about, and which property of the language forces the agreement. The kind is the name of the shared resource — a type, a call target, a heap, a schedule, a version, a supply — so the entries read as a catalogue of shared resources rather than as a list of complaints.

1.2 The three tiers

Tier Weight What the tier asserts What saturates a dimension
High −15 Agreement across the whole program, no local escape, and nothing in the language that records it One High mechanism, alone
Moderate −8 Agreement across ordinary code, escapable only by a whole-codebase convention Two Moderate mechanisms
Minor −4 Agreement confined to a boundary, removable by a contained workaround Four Minor mechanisms, clamped to −15

Within one dimension the present weights are added and the total is clamped at −15, the weight of one High mechanism. The dimension score is 100 minus that clamped total. The clamp is a rubric choice, not a measurement: it keeps the scores bounded (a dimension never costs more than one severe mechanism), monotone (adding a mechanism never improves the score), and comparable (no score collapses to zero). It is deliberate, and it is arbitrary.

1.3 How to read an entry

Each entry gives the mechanism’s code and tier, what the mechanism is, the kind of coupling it imposes and why, an example, and — where the criterion is easy to misread — what its presence does not mean.

Presence is a factual question about the language: does the language hand the programmer this construct? It is not a judgment about the code. Every language permits a global; C1 asks whether a global is the default home for state.

A check behind a project switch. Some mechanisms depend on a compiler switch. The mark uses the configuration the language’s own project initialiser writes for the version shown, because that is what a new project gets without a decision. tsc --init writes "strict": true (which sets strictNullChecks), so TypeScript is not charged T10; dotnet new writes <Nullable>enable</Nullable>, so C# is not charged T10; Visual Basic’s compiler default is Option Strict Off and the SDK template sets no OptionStrict, so the narrowing conversion is charged T2. Where the initialiser writes no switch, the compiler default governs. A check that only a separate tool supplies is never counted — that is what T1 already says about mypy in CI.

A mitigation is not an absence. Some languages offer an opt-in construct that avoids a mechanism — @CompileStatic in Groovy, try-with-resources in Java, reified in Kotlin, a lint rule, or a flag the initialiser does not set. The mechanism stays charged, because the default code still pays it and avoiding it needs knowledge the language does not force. The model measures what the language imposes, not what a disciplined team can achieve. If a mitigation lowered the score, two teams on the same language would score differently, and the language would have no score. The mitigation is recorded on the language page as a strength or a trade-off, never as a discount. The line between a mitigation and an absence is the project-switch rule above.

Core (34) Type (14) — T1 · T2 · T3 · T4 · T5 · T6 · T7 · T8 · T9 · T10 · T11 · T12 · T13 · T14 Construction (4) — C1 · C2 · C3 · C4 Memory (4) — M1 · M2 · M3 · M4 Error (4) — Er1 · Er2 · Er3 · Er4 Concurrency (5) — K1 · K2 · K3 · K4 · K5 Platform (3) — P1 · P2 · P3

Meta (29) Performance (3) — R1 · R2 · R3 Ecosystem (7) — Ec1 · Ec2 · Ec3 · Ec4 · Ec5 · Ec6 · Ec7 Evolution (4) — V1 · V2 · V3 · V4 Domain (3) — D1 · D2 · D3 Learning (3) — L1 · L2 · L3 Syntax (3) — S1 · S2 · S3 Testing (3) — G1 · G2 · G3 Security (3) — Q1 · Q2 · Q3

1.4 Core criteria (34)

1.4.1 Type

What the Type dimension asks: can the language keep the meaning of a value in one place, or must every reader reconstruct it?

1.4.1.1 T1 — Dynamic typing, no static check

High · −15. A value carries its type at run time, so nothing checks that an operation suits its operand until the operation runs. The meaning of every value becomes a shared agreement between the site that produces it and every site that consumes it, and no tool holds that agreement: it lives in the tests and in the reader’s memory.

Coupling imposed. Type-identity coupling across the whole program: producer and consumer agree on what a value is, and on every operation that may apply to it, and nothing in the source records that agreement. Because the check happens when the operation runs, the agreement survives only while both sides are read together, so a change to one reaches all the others at once.

Example. Python accepts n = 1 and then n = "one", and n + 1 fails only on the path that reaches it, and only for the inputs that take that path. MATLAB, R, SAS, Lua, and every POSIX shell behave the same way.

Presence does not mean a checker is unavailable. The criterion asks whether the language enforces a check. A program that runs mypy --strict in CI has a check the language did not give it, and the score still records the language.

1.4.1.2 T2 — Weak static typing

Moderate · −8. The compiler knows the types, but it converts between them implicitly, so a check that would catch a mistake becomes a silent conversion. The conversion rules become the shared resource: every expression depends on a table that nothing forces a reader to know.

Coupling imposed. Conversion coupling: every expression that mixes types agrees with the language’s conversion table rather than with its neighbour. The table is the shared resource, so a reader who does not hold it cannot tell a checked expression from a silently narrowed one, and an implicit conversion hides the moment the meaning changed.

Example. C compiles int i = 3.7; and discards the fraction, and lets an integer stand in for a pointer. COBOL converts between two different PICTURE clauses on a MOVE without complaint. Visual Basic’s Option Strict Off default admits a narrowing conversion and a late-bound call.

1.4.1.3 T3 — Dispatch by run-time type

Moderate · −8. Which code a call reaches is decided by the value’s run-time type rather than by a declared contract. The set of operations a value must answer becomes a shared agreement that no declaration holds, so a rename in one module breaks a caller in another — and only at run time.

Coupling imposed. Call-target coupling: a caller and a callee agree on a name and an arity that no declaration enforces, and the agreement must hold for every type that can arrive at the call site. Because the target is chosen from the value, the coupling spans types the author of the call never named.

Example. Erlang resolves Mod:Fun(Args) when it is called, so a missing function fails there; Elixir and Clojure choose a protocol or a pattern clause from the value they receive; Ruby and Python resolve an attribute at the moment of the call.

Presence does not mean T1. Objective-C is statically typed at the pointer level and still dispatches every message at run time, so it is charged T3 and not T1. TypeScript is statically typed and still reaches a method through the prototype at run time, so it is charged the same way. Duck typing is about which code runs; T1 is about what a value is.

1.4.1.4 T4 — No sum types

Minor · −4. The language cannot declare that a value is one of several alternatives and have the compiler track which one. The alternatives are encoded by convention, so the tag and its payload are a shared resource that every consumer re-derives, and the compiler cannot prove that a consumer handles every case.

Coupling imposed. Encoding coupling: producer and consumer agree on how an alternative is written down — a tag beside a payload, a nil, a nullable column — rather than on the alternative itself. The encoding is the shared resource, so a new case has to be taught to every reader of it, and exhaustiveness stays a convention.

Example. Go has no sum type, so “a value or a failure” is a pair and a nil check stands in for a match. SQL models an alternative as nullable columns, and the caller must know the encoding. Rust’s enum and C++’s std::variant are the constructs whose absence is charged.

Minor because it is containable. A constructor plus its accessors can confine the cost to one place — but never to zero, because exhaustiveness stays unchecked.

1.4.1.5 T5 — No generic type parameter

Minor · −4. The language cannot write an algorithm once over a type parameter, so it duplicates the code per type or falls back on one universal representation. That representation becomes the shared resource, and every user casts out of it, so the type is known by convention.

Coupling imposed. Representation coupling: every use of the algorithm agrees on the one representation it can carry — void *, Object, interface{} — and casts back out by hand. The universal representation is the shared resource, so the real type of a value becomes knowledge held by the programmer rather than a fact held by the compiler.

Example. Pre-generics Java needed (String) list.get(0) from an Object container; C passes void * with a callback; Go before 1.18 used interface{}; SQL has no type parameter at all and generates DDL per column type instead.

Presence does not mean the checking half of generics. A language with generics whose parameters cannot be constrained still leaves a gap — but that gap is charged under T2 or T3. T5 records only the absence of the type parameter itself.

1.4.1.6 T6 — One name space

Moderate · −8. The language has a single flat name space, so two declarations with the same name collide whichever module they live in. The global name space is the clearest shared resource there is: every name is a claim on it, and a new module can break an unrelated one.

Coupling imposed. Name coupling over one flat space: every declaration competes for the same set of identifiers, so two modules that never call each other are coupled through the name they both want. The name space is the shared resource, and nothing local bounds a claim on it, which is why the convention that resolves it — a prefix — must be kept by the whole codebase.

Example. Two C libraries that both define init cannot be linked without renaming one, which is why C libraries prefix their symbols (gl_, png_). Lua keeps every global in one shared table. An assembler puts every global symbol in one flat symbol table.

Presence asks about the mechanism, not the idiom. MATLAB is not charged, because +pkg folders scope a name, and R is not charged, because a package declares a NAMESPACE. C is charged, because a header’s declarations all land in one translator-level name space.

1.4.1.7 T7 — Type erasure and reflection

Minor · −4. The type parameter is present in the source and gone from the artifact, and the program can inspect and alter its own structure at run time. The erased type is a shared resource that no longer travels with the value, so a consumer must recover it by convention — and reflection turns every private field into a reachable one.

Coupling imposed. Type-recovery coupling at the boundary: the consumer agrees with the producer on a type argument that the artifact no longer carries. The agreement sits where the cast is written, so it holds per call site and cannot be checked back at the place the value was made.

Example. Java’s List<String> is a List at run time, so get(0) returns Object and the cast is unchecked. Objective-C’s id carries no compile-time type at all. TypeScript’s types exist in the editor and the build, and not in the running program.

Minor because the cost sits at the boundary where the type is recovered — a cast, a Class<T> token, an isKindOfClass: test — not in the body of the algorithm.

1.4.1.8 T8 — Operator overloading

Minor · −4. A symbol such as +, ==, or [] takes its meaning from the operand types. A symbol is a shared resource with one meaning by default, and overloading makes that meaning depend on a lookup the reader has to perform.

Coupling imposed. Symbol-meaning coupling: the reader agrees with the definer on what + or == does for the type at hand. The meaning is resolved by a lookup at the use, so the coupling is real but bounded to one expression — the reader resolves one type and the answer arrives.

Example. C++’s << both shifts and inserts into a stream. Scala’s + on a Set performs a union. Racket’s + accepts exact and inexact numbers and returns whichever type the operands dictate.

Minor because the operator still carries a type signature, so its meaning is discoverable by resolving one type — unlike a macro, which can mean anything at all.

1.4.1.9 T9 — Inheritance required for polymorphism

Minor · −4. The only way to use two types through one interface is to place both in an inheritance chain. The base type becomes a shared resource that the types must be placed into, which forces an “is-a” commitment wherever the domain has only “does-this”, and a change to the base changes every subtype.

Coupling imposed. Hierarchy coupling: a type agrees to be placed under the interface it will be used through, so it inherits its base’s identity and its base’s future changes. The chain is the shared resource, so a change at the root reaches every leaf, and an unrelated type must be moved into the chain before it can play the role.

Example. Eiffel’s polymorphism runs through its inheritance and genericity rules. C approximates it with a struct holding function pointers, or with a void * and a convention. TclOO expresses a shared protocol as a class or a mixin rather than a declared type.

Minor, and the cheapest to remove. Go’s implicit interfaces, Rust’s traits, and Elixir’s protocols supply polymorphism without a chain, so those languages are not charged.

1.4.1.10 T10 — No null-safety

Moderate · −8. Any reference can be the absent value, and nothing in the type says whether it is. “May be absent” becomes a shared resource that every consumer must guard, and no signature records the guard, so each reader repeats the check or forgets it.

Coupling imposed. Absence coupling: every consumer agrees with every producer about whether a reference may be missing, and no signature records the answer. The unwritten agreement is the shared resource, so each guard is written per use and each omission stays invisible until the missing value arrives.

Example. Java compiles String s = null; s.length(). SQL’s NULL is compatible with every domain and propagates through three-valued logic, so IF x = 5 is not true when x is NULL. In PL/SQL a variable that is never assigned is NULL. A null pointer in Assembly is a zero register, and a load from it is an ordinary instruction.

Presence does not mean “the language has an absent value”. Kotlin’s String? against String, Swift’s optionals, Rust’s Option<T>, and TypeScript’s string | null under strict are exactly the construct whose absence is charged, which is why those languages are not charged T10.

1.4.1.11 T11 — No module boundary

Moderate · −8. There is no construct that declares what a module exposes and what it keeps. The boundary is the shared resource, and its absence means every declaration is reachable by every consumer, so an internal change can break an unrelated caller.

Coupling imposed. Visibility coupling in its total form: every declaration is a public fact, so a consumer may depend on anything a producer contains. The missing boundary is what makes the coupling unbounded — a change to an internal name travels to callers the module never intended to have.

Example. A C header makes everything it declares public, and static is the only file-scoped boundary. A POSIX shell script sources another script into the same variable space. VBScript’s ExecuteGlobal writes into the same global scope. Scratch exposes every sprite’s blocks with no declared interface.

Presence does not mean “no files”. Splitting code across files is not a boundary. Go is not charged, because a package-level unexported name is a real boundary, and neither is MATLAB, whose +pkg and private folders hide what they do not export.

1.4.1.12 T12 — No effect or capability tracking

Moderate · −8. A call’s signature says what it returns, not what it does. The effects a call may perform — input and output, mutation of shared state, exceptions, randomness — are a shared resource that no declaration carries, so every caller learns them by reading the body or by testing. A change to an effect is invisible to every caller [1].

Coupling imposed. Effect coupling: a caller agrees with a callee on what the call may do, and reads that agreement from the body rather than from the signature. Because the effect set is unwritten, every caller is exposed to every effect the implementation may later acquire.

Example. Haskell’s IO, PureScript’s Effect, and Unison’s abilities put the effects in the type, so a function cannot hide a socket behind a pure signature. In Java, Python, Go, and C a function that writes a file, mutates a global, and throws looks like any other function.

Held at Moderate, not High. The breadth is the whole program — any call may perform any effect — but nothing breaks when the effect set is wrong. The cost is the reading and refactoring work an untracked effect adds, not a missed check, so the mechanism does not carry a High mechanism’s weight.

Presence records whether the type states the effects. Every language has effects. Haskell, PureScript, Unison, Lean 4, and Rocq carry them in the type; so do Nim (its raises and tags lists are part of a proc type) and D (pure, nothrow, and @nogc are constituents of the function type). Java’s throws clause is a signature annotation, not part of the method type, so Java is charged.

1.4.1.13 T13 — Function colouring

Moderate · −8. The language has two kinds of function — one that may suspend and one that may not — and the two do not compose. A synchronous function cannot call an asynchronous one without a bridge, so the kind spreads outward through the call graph and every caller must agree on it. The kind is a shared resource that a change in one function imposes on the whole chain [2].

Coupling imposed. Function-kind coupling along the call chain: a caller must share the kind of its callee, so the kind propagates from the smallest suspending leaf to the entry point. The kind is the shared resource, so adopting it at one site is a decision that the whole chain must repeat.

Presence records a function-kind split, not the existence of concurrency. Go’s goroutines and Erlang’s processes are concurrency without a second function kind, so neither is charged.

Example. In JavaScript, C#, and Dart an async function returns a future, and a plain function that calls it must itself become async or block. Rust’s async fn returns a Future that only an executor drives, Nim’s {.async.} proc can be awaited only inside another async proc, and Python’s async def and Kotlin’s suspend draw the same line.

1.4.1.14 T14 — Unchecked foreign boundary

Minor · −4. The language can call code outside its own type system, and nothing checks that the two sides agree. The foreign interface — argument order, ownership, layout, lifetime, and error convention — is a shared resource held by a hand-written declaration, so a mismatch is found at run time or not at all.

Coupling imposed. ABI coupling at the foreign boundary: the two sides agree on argument order, layout, ownership, and error convention through a hand-written declaration that neither compiler validates. The declaration is the shared resource, so the agreement holds until run time and then fails as corruption rather than as a diagnostic.

Example. Rust’s extern "C" block is an unchecked promise. Python’s ctypes and Ruby’s Fiddle accept any signature. JNI requires the C side to agree by hand, and a Java exception that crosses the boundary unwinds nothing on the other side.

Minor because the boundary is containable to one module — but never zero, because the compiler cannot check across it.

Covers the native boundary, not a boundary inside one runtime. Unison 1.0 added a C FFI, so it is charged. PostgreSQL, Oracle, and SQL Server can load native code through a user-defined function, so the procedural SQL dialects are charged with them. Solidity calls only inside the EVM, and a shell reaches foreign code by starting a process, so neither is charged. The readiness axis prices the other side of the same boundary, under Y9: a language is charged there for having no interface, and here for the interface being unchecked. Both can be charged at once.

1.4.2 Construction

What the Construction dimension asks: can the language keep state local, and can a value only come into existence through a path that establishes its invariant?

1.4.2.1 C1 — Global mutable state by default

High · −15. The default home for a value is a global that any code can read and write; state is local only when the programmer asks for it. The global is the widest shared resource there is — the whole program is its consumer — so any module can change it, and any change can surprise any reader.

Coupling imposed. State coupling to one cell: every reader and every writer of the global agrees on its meaning at each instant, and the whole program is a party to that agreement. Because the cell has no owner, no module can change its own behaviour without the possibility of changing another’s.

Example. Python’s module-level names and global. MATLAB’s base workspace, and R’s global environment with <<-. A POSIX shell, where x=1 inside a function can write the caller’s variable. COBOL’s WORKING-STORAGE. A PL/SQL package variable, which lives for the whole session.

Presence asks whether a global is the default. Every language permits a global. Kotlin and Rust make top-level state immutable unless it is declared otherwise, so neither is charged.

1.4.2.2 C2 — No immutability by default

Minor · −4. A binding, and the value behind it, can be changed unless the programmer says otherwise. “Who may write this?” then has no default answer, so every reader has to trace the writes.

Coupling imposed. Write-permission coupling: every holder of a binding must know which other holders may change it, and the language states no default answer. The permissive default makes the coupling silent at the definition and forces it to be reconstructed at every read.

Example. Java fields are mutable unless they are final; C++ members unless they are const; a MATLAB array is mutable with no const at all; a SAS DATA step may overwrite the dataset it is reading.

Minor because immutability is usually a local decision. A language that inverts the default — Rust’s let with mut, Haskell’s bindings — is not charged.

1.4.2.3 C3 — Construction coupled to allocation

Minor · −4. Creating a value and obtaining its storage are one act, and the construction cannot be redirected to different storage. The storage site becomes a shared resource, so a caller cannot choose where a value lives, and the code that builds a value cannot be separated from the code that reserves it.

Coupling imposed. Storage coupling: the constructor and the caller must agree on where the value will live, because the language offers no way to separate the act of construction from the act of reservation. The agreement is narrow — one allocation site — which is why a single indirection removes it.

Example. In C a value exists only because storage was reserved and then filled, and a constructor is a naming convention over malloc. In Assembly the reservation is a register or a stack slot, and nothing constructs.

Minor because one indirection restores the separation. C++’s placement construction and allocator-aware containers, and Rust’s Box and Vec::with_capacity, build in storage that already exists, so those languages are not charged.

1.4.2.4 C4 — No constructor or validation

Minor · −4. A value of the type can exist without having passed any validating path. The invariant becomes a shared resource that nothing enforces, so every consumer either re-validates or simply trusts.

Coupling imposed. Invariant coupling: every consumer must agree with every producer that a value is valid, because no constructor stands between the type and its instances. The agreement is silent — a consumer cannot tell a validated value from an unvalidated one.

Example. A C struct is uninitialized memory until each field is assigned. A Fortran variable can be referenced before it is set. A Solidity contract without a constructor is deployed with whatever the default state is. In a POSIX shell a variable comes into existence on first assignment, and no construct can state an invariant about it.

Minor because the invariant can often be re-established cheaply — a smart constructor, a factory, a private field plus a validating entry point. The criterion is the same either way.

1.4.3 Memory

What the Memory dimension asks: can the language state when storage and other resources are released, and who owns them?

1.4.3.1 M1 — Manual deallocation

High · −15. The programmer decides when storage is released, and the program is wrong if the decision is wrong in either direction — a missed release leaks, and a doubled release corrupts. The heap becomes a shared resource, and “who frees this, and when” is a question that every path through the program must answer identically.

Coupling imposed. Lifetime coupling: every path that holds a value must agree on when it stops being valid, because the decision is written per path rather than once for the value. The agreement is global in effect and local in expression, so one divergent path corrupts what the others still trust.

Example. malloc and free in C. A stack frame plus malloc plus free in Assembly. New and Dispose in Turbo Pascal. Zig’s allocator.free. In Fortran, allocate and deallocate are written by hand for a pointer, and a missing statement leaks.

Presence follows the ordinary path. A language with an optional collector for one allocation class is charged when the ordinary path is manual, because the ordinary path carries the cost.

1.4.3.2 M2 — Garbage collection on a schedule the program cannot state

Moderate · −8. Memory returns when the collector decides, not when the last reference goes away. The heap becomes a shared resource whose release schedule the program cannot name, so a latency-sensitive path is coupled to a component it never calls.

Coupling imposed. Schedule coupling: every path that depends on latency and the runtime must agree on when memory returns, because the release is the collector’s decision and not the program’s. The agreement is unavoidable and unnamed: no declaration states the schedule that the path is relying on.

Example. The JVM family (Java, Kotlin, Scala, Groovy, Clojure), the CLR (C#, F#), Go, Crystal, D, Nim, OCaml, Haskell, Dart, Unison, and the Lisp family (Common Lisp, Racket, Scheme). Elixir, Erlang, JavaScript, and Ruby each collect on their own runtime’s schedule, and TypeScript inherits the JavaScript runtime’s.

Presence does not mean “the language has a collector for one thing”. Reference counting is deterministic and is not M2: Swift’s ARC and Delphi’s strings and interfaces release at the last reference, and Delphi on Windows frees objects explicitly, so neither is charged. Objective-C is charged, because an autorelease pool defers a release past the last reference and a Core Foundation handle is released by hand.

1.4.3.3 M3 — No release tied to scope

Minor · −4. Nothing runs automatically when a scope ends, so a resource is released in a finally, by a project convention, or not at all. The resource — a lock, a file, a connection — is shared, and its release depends on every path remembering.

Coupling imposed. Release coupling: every exit path must run the same release, because no scope boundary runs it for them. The agreement is duplicated on every branch, so its cost grows with the number of paths rather than with the resource.

Example. C reaches a release path through goto cleanup. R uses on.exit and MATLAB uses onCleanup. Java’s try-with-resources was added precisely to supply the destructor the language lacks. Python’s with covers context managers, but only the ones that exist.

Minor because one convention removes it, and because a missed release is often caught in testing. Rust’s Drop and Zig’s defer run at scope exit by construction, so neither is charged.

1.4.3.4 M4 — No ownership model

Moderate · −8. The language does not track which of several handles owns a resource, and aliasing is unrestricted. “Who may free or mutate this?” becomes a shared resource that the program states in comments rather than in the type, and every reader has to reconstruct the answer.

Coupling imposed. Ownership coupling: every holder of a handle must agree on who may free it and who may mutate it, because the type records neither. The agreement is left to prose, so it cannot be reviewed and does not survive the author.

Example. In C, two pointers can share one buffer with no way to say who owns it. In C++ a raw pointer and a reference alias the same object. Go offers no way to declare that a value moves, so a slice and its backing array stay aliased. In C# and Java, any two references can reach the same mutable object.

Presence does not mean “the language has references”. Rust is not charged: moves, borrows, and lifetimes are exactly the construct whose absence M4 records.

1.4.4 Error

What the Error dimension asks: can a failure travel from where it is detected to where it can be handled, and does anything require a caller to decide?

1.4.4.1 Er1 — No structured error path

High · −15. The language has no construct that carries a failure from where it is detected to where it is handled. The failure travels by side channel — a return code, an out-parameter, a log line, a global status flag — so every caller must remember to check it, and nothing checks that a caller did.

Coupling imposed. Failure-signalling coupling: producer and consumer must agree that a particular value means failure, because the language gives the failure no type. The agreement is a convention that every call site repeats, so it is invisible where it is skipped.

Example. C returns a sentinel and sets errno. A shell returns an exit code that nothing reads unless set -e is set. Fortran reports through iostat=, and COBOL through FILE STATUS. SAS writes a note to the log and moves to the next observation. VBScript’s On Error Resume Next turns a failure into a silent continuation.

Presence records the path, not its enforcement. A language whose ordinary failure path is an exception is charged Er2 instead.

1.4.4.2 Er2 — Failures nobody is required to handle

Moderate · −8. A failure can be raised without the signature saying so, and no caller is required to handle it. The set of failures a call can produce becomes a shared resource that the type system does not carry, so a caller discovers it in production.

Coupling imposed. Failure-enumeration coupling: the caller must agree with the callee on the set of failures that may arrive, because no signature lists them. The agreement is learned from production, so it is discovered after it is needed.

Example. A MATLAB MException propagates until something catches it, and nothing must. An R condition is signalled and carried on. D’s throw needs no declaration, and nothrow is an opt-in promise that no caller sees. Go’s panic is unchecked and recover is optional. Java’s unchecked exceptions carry the failures that actually occur — NullPointerException, IllegalArgumentException — even though the language also has checked ones.

Presence does not mean “the language has no checked failures”. The criterion is charged when the failures that arrive in practice are the unchecked ones, because those are the ones no signature can enumerate.

1.4.4.3 Er3 — A failure can be ignored

Minor · −4. The language lets a failure pass without any decision, and it does not force exhaustiveness. “Handled or not” becomes a shared decision that nothing records, so a caller’s silence is indistinguishable from its approval.

Coupling imposed. Handling-decision coupling: each caller must agree with the rest of the system that silence is an adequate response, because nothing forces the decision to be recorded. The agreement is indistinguishable from neglect, so review cannot separate the two.

Example. In C a return value and an errno value carry no consequence until the code branches on them, and a switch over an enum may leave cases unhandled without a diagnostic. In Haskell a non-exhaustive pattern compiles and fails only when reached. A -1 from an Assembly call is a value, not a signal.

Minor because one discipline removes it locally — a checked return, a total switch, an exhaustive match — but the discipline has to be written down, and the language will not check that it is kept.

1.4.4.4 Er4 — No composable result type

Minor · −4. A failure cannot be carried as an ordinary value through an ordinary expression, so it cannot be threaded, combined, or returned from a helper. The success-and-failure pair has to be rebuilt at every level, through out-parameters, sentinel values, or a global status, so the shape of the error path becomes a shared convention.

Coupling imposed. Error-shape coupling: every level must agree on where a failure travels, because no value can carry it through an expression. The agreement is repeated at each boundary, so a change to the shape changes every signature in the chain.

Example. Ada reports through raise, so a fallible operation cannot be folded into an expression. In C the pair is an out-parameter or errno. In Go it is a second return value, which cannot be mapped or chained without a second statement. In Assembly there is no value that can carry “this failed”.

Minor because the cost is syntactic rather than structural — but it is the difference between an error path that composes and one that is everywhere a special case. Rust’s Result, Haskell’s Either, and C++’s std::expected are the constructs whose absence is charged.

1.4.5 Concurrency

What the Concurrency dimension asks: can the language express parallel work, and can it keep that work from touching shared state unintentionally?

1.4.5.1 K1 — No parallel execution

High · −15. The ordinary execution model offers no second thread of control: running two computations at once needs a worker, a separate process, or a native extension. “How much may run at once” is fixed by the runtime, so every part of the program that wants throughput is coupled to that one decision.

Coupling imposed. Throughput coupling: every part of a design that needs concurrency must agree with the runtime on how a second thread may exist, because the execution model is fixed outside the language. The agreement cannot be made locally: a second core is available only by leaving the program’s own structure behind.

Example. Python’s global interpreter lock serialises bytecode execution, so a CPU-bound program must leave the language — multiprocessing, a native extension — to use a second core. JavaScript and PHP take a request or an event as the unit of work. Solidity runs a transaction on one EVM thread, and PureScript and TypeScript inherit the single-threaded JavaScript runtime.

Presence is about what the language can express, not about what a library can reach. A language whose only route to a second core is a separate process or a native extension is charged, because the program’s structure is then not the language’s.

1.4.5.2 K2 — Shared mutable state by default

Moderate · −8. Concurrency shares memory by default, and nothing marks a value as thread-local, immutable, or owned. The shared cell becomes the resource, and every reader has to reconstruct which lock protects it.

Coupling imposed. Shared-cell coupling: every thread that reaches a value must agree on its content, because the default offers no privacy and no immutability. The agreement is unbounded — a thread’s correctness depends on code it does not contain.

Example. An Assembly store to a global is visible to every core, with no privacy notion attached to the address. A PL/SQL package variable is shared by every program unit in the session. In T-SQL, SET options and session-scoped state are visible to code the caller did not write. A C file-scope object is shared by every thread that reaches it.

Presence records the default. K2 and K3 often appear together, and they are different claims: K2 says the shared state is the default, and K3 says the language gives no help in finding the resulting races.

1.4.5.3 K3 — Manual synchronisation, no race detection

Moderate · −8. Locks and atomics are library types, and the language neither proves the locking discipline nor detects a race. The discipline becomes an agreement across every file that touches the data, and the compiler checks none of it.

Coupling imposed. Protocol coupling: every file that touches the data must agree on one locking discipline, because the language supplies primitives but no proof. The agreement is horizontal — it runs across files that never call each other — so no compiler can hold it and no test can localise a violation.

Example. Java’s synchronized, C#’s lock, Go’s sync.Mutex, Kotlin’s @Synchronized, and Scala’s synchronized blocks serialise access but do not find a missing lock. D’s core.atomic and Crystal’s Mutex and WaitGroup are libraries over the same primitive. A lock prefix on one x86-64 instruction is the whole of Assembly’s provision.

Presence is about detection, not about threads. A language that proves thread safety at compile time is not charged, even though it has threads.

1.4.5.4 K4 — No structured concurrency

Minor · −4. The language gives no construct that scopes a task’s lifetime to a block, so nothing guarantees that a task has finished when the block ends. The task’s lifetime becomes a shared resource that the program tracks by hand, and a scope can end while work it started is still running.

Coupling imposed. Task-lifetime coupling: the block that starts work must agree with the work about when it ends, because nothing scopes the two together. The agreement is kept by hand, so every starter must also be the joiner.

Example. C’s threads and Zig’s threads have no scope that joins them. Perl’s threads leaves the task tree to the program. A Groovy executor service is joined by hand or not at all. In Assembly a thread is a clone or pthread_create call with nothing around it.

Minor because a scope construct can be added by convention, and because the failure it causes — work outliving its scope — is usually visible in testing. Kotlin’s coroutineScope and Swift’s task groups are the constructs whose absence is charged.

1.4.5.5 K5 — No concurrency primitives

Minor · −4. The language has no first-class lock, channel, or await, so concurrent work is started and steered outside the language. The synchronisation becomes a shared resource that lives in signals, exit codes, or a foreign library, so no declaration states the protocol.

Coupling imposed. Coordination coupling: the concurrent units must agree on a protocol, because the language supplies no first-class lock, channel, or await to state it. The agreement is carried by the operating system’s facilities rather than by the program’s declarations.

Example. A shell starts a job with &, steers it with signals, and communicates through exit codes, with no lock and no channel anywhere in the language. Turbo Pascal 7.0 in DOS reaches concurrent work through an interrupt procedure, and its synchronisation is the hardware’s.

Presence is the complement of K1. K1 is a runtime that forbids parallelism; K5 is a language that offers nothing to coordinate it.

1.4.6 Platform

What the Platform dimension asks: can the artifact run anywhere other than where it was built for?

1.4.6.1 P1 — Cannot leave one vendor or one domain

High · −15. The program runs only inside one vendor’s product or one domain, so the deployment unit is that platform. The platform is the widest shared resource of all: every line is coupled to a runtime, an API surface, and a release cycle the program does not control.

Coupling imposed. Platform coupling: the whole program must agree with an environment it cannot change, because the deployment unit and the artifact are the same thing. Nothing in the source stands outside the agreement, which is what makes the tier High.

Example. A PL/SQL program needs an Oracle-family database and deploys as a schema. SAS needs a SAS installation and deploys as a library on a SAS host. Transact-SQL needs SQL Server, Azure SQL, or SAP ASE. A Rocq development runs only after extraction to another language. An x86-64 Assembly artifact runs only where that instruction set runs. A Solidity contract runs only on an EVM.

Presence is about the artifact. A language whose compiler runs anywhere but whose programs run in one place is still charged.

1.4.6.2 P2 — One vendor dominates, other uses exist

Moderate · −8. The language has a single dominant implementation or platform, even though alternatives exist in principle. The toolchain, the library set, and the release schedule of one vendor become the program’s agreed reality.

Coupling imposed. Implementation coupling: a single implementation’s compiler, libraries, and release cycle become the agreement every consumer makes, because the alternatives are theoretical rather than practical. The escape exists on paper, and the working reality is one vendor’s.

Example. Groovy has no non-JVM implementation. Objective-C’s complete language, reference compiler, and flagship frameworks are one vendor’s. Most production COBOL runs on one mainframe platform with that vendor’s compilers. Eiffel’s practical toolchain is EiffelStudio, and Tcl’s is its own interpreter. Swift’s toolchain and frameworks are one platform’s.

P2 is a degree below P1. The artifact could in principle run elsewhere, and in practice does not.

1.4.6.3 P3 — A legacy or discontinued platform

Moderate · −8. The program targets a platform that is no longer developed, so the artifact is frozen with it. The platform’s limits — its word size, its memory model, its APIs — become permanent constraints on the program.

Coupling imposed. Era coupling: the program must agree with a platform whose own development has ended, because the artifact is frozen at that platform’s last state. The agreement has no revision path, only an exit.

Example. Turbo Pascal 7.0 compiles real-mode DOS executables, and it was the last release of that line: the target, not just the tool, has ended.

P3 is not P1. P1 is one live vendor with one domain; P3 is a platform whose own life has ended. Assembly on x86-64 is charged P1 for its instruction-set lock-in, and not P3, because that instruction set is current.

1.5 Meta criteria (29)

1.5.1 Performance

What the Performance dimension asks: is there a cost the program pays at run time that its source does not show?

1.5.1.1 R1 — Interpreted, with no ahead-of-time compilation

Moderate · −8. The source is executed by an interpreter or a virtual machine rather than compiled to the machine’s code ahead of time. The execution model becomes a shared resource fixed by the runtime, so the shape of every hot loop is decided by the interpreter’s dispatch rather than by the algorithm.

Coupling imposed. Execution-model coupling: the shape of every hot path must agree with the interpreter’s dispatch rather than with the algorithm, because the runtime decides how a statement is executed. The agreement is invisible in the source, so a hot loop is tuned by leaving the language rather than by rewriting it.

Example. A Prolog goal is a search over the clause database through the engine’s instruction set, so a tight arithmetic loop trails a compiled equivalent. A shell parses and evaluates text at run time, so a loop over files pays parser cost per iteration. Base R and base MATLAB execute an interpreted form, and Perl’s reference interpreter has no JIT. Ruby is charged because its JIT is opt-in, so a default run stays interpreted.

Presence does not mean “slow”. The criterion is about the absence of an ahead-of-time path, which is why a language with a mature library behind it (MATLAB’s BLAS and LAPACK calls) can still be charged while remaining fast where it matters.

1.5.1.2 R2 — Collector pauses visible to the program

Moderate · −8. A collector’s pause is part of the program’s timing, and the program cannot state when it will occur. The heap’s release schedule becomes a shared resource decided outside the program, so a latency target couples to the runtime’s collector.

Coupling imposed. Timing coupling: a latency budget must agree with the collector’s pause, because the program cannot schedule the pause. The agreement is temporal rather than structural — it constrains when the program runs, not what it contains.

Example. Dart’s generational collector stops the mutator, which shows as a frame-time spike. SBCL and CCL stop the program to collect Common Lisp, GHC pauses to collect Haskell, and Nim’s collector stalls its mutator. The JVM’s pause reaches Java, Kotlin, Scala, and Groovy alike, and the CLR’s reaches C#, F#, and the Visual Basic runtime. JavaScript pauses the one event loop it has, so a pause blocks every pending callback, and TypeScript and PureScript inherit that runtime. Go’s pause is short but present, Eiffel’s and D’s collectors remain a latency risk, and Unison’s own runtime collects on its own schedule.

R2 is not M2. M2 records that the heap’s schedule is not the program’s; R2 records that the schedule is observable — a pause a latency budget has to absorb. The two are marked independently: a language can hold M2 without R2, because a collector whose pause the page does not record as a cost (Python’s, Ruby’s, Crystal’s, Racket’s) is not charged. Where the page records the pause as a cost, the mechanism is charged — which is what keeps the charge set checkable against the pages themselves.

1.5.1.3 R3 — JIT warm-up

Minor · −4. The first execution of a function compiles it, so the code’s speed depends on which path runs first and on how long the process has been up. Measurement itself becomes the shared convention: a benchmark has to agree on a warm-up, and a short-lived process never reaches the optimized state.

Coupling imposed. Measurement coupling: a benchmark, a latency target, and the compiler must agree on what “steady state” means, because the first execution compiles the code. The agreement must be written down, so a short-lived process and a long-lived one are not measuring the same program.

Example. Julia compiles a method on its first call, so latency depends on which path runs first. The JVM and the CLR tier their compilation, so Groovy, Clojure, Kotlin, F#, C#, Java, and PowerShell are slower on their first requests than in steady state.

Minor because a long-running process amortises it, and because it can be pre-warmed.

1.5.2 Ecosystem

What the Ecosystem dimension asks: does the surrounding supply of libraries, tools, and people make the language’s own costs worse?

1.5.2.1 Ec1 — No package manager

Minor · −4. The language has no standard way to declare, fetch, and resolve a dependency. The build’s dependency graph becomes a shared resource owned by the build system rather than the language, so a project’s dependencies are a local convention that no tool can check.

Coupling imposed. Dependency-graph coupling: a project’s dependencies must agree with a build script rather than with a resolver, because the language defines no way to declare them. The agreement is local and unchecked, so two checkouts can disagree about the graph.

Example. In Assembly a dependency is an object file or a linked library resolved by the build script. The C and C++ standard defines no distribution, so the platform’s toolchain supplies one. Prolog’s standard defines no distribution, so each system ships its own. A shell has no dependency declaration beyond what the script itself probes for.

Minor because the cost is confined to the build — but the build’s convention is the whole program’s convention.

1.5.2.2 Ec2 — Fragmented tooling

Moderate · −8. No single toolchain covers the language, so every tool targets a dialect, a version, or a vendor. The workflow becomes a shared resource with several incompatible owners, and a team’s tooling choices must agree file by file.

Coupling imposed. Toolchain coupling: a project’s tools must agree with a dialect or a version rather than with the language, because no single toolchain covers the whole of it. The agreement is assembled per tool, so each choice constrains the next one.

Example. ORMs, migration tools, linters, and formatters each target one SQL engine’s dialect, so there is no single SQL toolchain. C and C++ are split across platform toolchains with different extensions and different warning sets. Go’s module and build conventions grew in parallel with the wider Go toolchain, so a project’s conventions are partly its own.

Moderate because the fragmentation is structural — it comes from the language’s reach, not from anyone’s neglect.

1.5.2.3 Ec3 — Small, niche, or discontinued ecosystem

Moderate · −8. The library pool, the hiring market, or the vendor’s investment is small, so more code is written than found. The library surface is a shared resource the whole community draws on, and when it is thin, each project re-invents and the local convention becomes the only one.

Coupling imposed. Supply coupling: the program must agree to write what the community does not supply, because the library pool is thin. The agreement is with a market as much as with a language — hiring, support, and examples move with it.

Example. dub’s registry is far smaller than crates.io, npm, or Maven, so a D project is likelier to write a library than to find one. SAS’s market shrank against R and Python, so new packages arrive more slowly. Tcl, Eiffel, Prolog, Delphi, Crystal, and Lua sit in the same position. This criterion is charged in just over half the inventory, and it is deliberately held at Moderate: a niche language is a cost, not a defect.

1.5.2.4 Ec4 — Proprietary ecosystem

Moderate · −8. The engine, the tooling, and the documentation come from one vendor under licence. The right to continue becomes a shared resource owned by a licensor, so the program’s future depends on a commercial decision.

Coupling imposed. Licence coupling: the program must agree with a commercial owner’s terms, because the engine, the tools, and the documentation come under one licence. The agreement reaches past the code into procurement and support.

Example. MATLAB’s toolboxes, editor, and reference documentation come from one vendor under licence. PL/SQL’s engine, tooling, and support come from Oracle. SAS, COBOL, Eiffel, Transact-SQL, Objective-C, and VBScript are in the same position, and so is any language whose manuals are behind a paywall.

Moderate because the licence is a structural dependency, not merely a price: the artifacts, the formats, and the support channel are all the vendor’s.

1.5.2.5 Ec5 — Tooling locked to one platform

Moderate · −8. The build, the debugger, or the package tooling runs on one operating system or one runtime only. The development environment becomes a shared resource the team cannot choose, so the workflow is coupled to a platform.

Coupling imposed. Environment coupling: the team’s build, debugger, and package tools must agree with one operating system, because the toolchain is written for it alone. The agreement constrains the team rather than the program — the artifact may still be portable.

Example. Swift’s toolchain, its debugger, and its frameworks belong to one platform, so a Swift team outside it works with a second-class toolchain.

Ec5 is not P2. P2 is about where the program runs; Ec5 is about where the tools run.

1.5.2.6 Ec6 — Ecosystem monoculture

Moderate · −8. One framework, library, or runtime dominates to the point that the alternatives are not really alternatives. The dominant choice becomes a shared resource with one owner, so the whole community’s assumptions move together — and a defect or a policy change in that one component reaches everything.

Coupling imposed. Concentration coupling: the community’s assumptions must agree with one dominant component, because the alternatives are not maintained enough to be escapes. The agreement delivers consistency and removes the exit at the same time.

Example. Elixir’s ecosystem is a monoculture around Phoenix and OTP, so alternatives to the dominant framework are few. Java’s and C#’s enterprise worlds, PHP’s web stack, and Ruby’s Rails are each dominated by one framework family.

Moderate because the concentration is a risk, not a defect: it delivers consistency, and it removes the escape route.

1.5.2.7 Ec7 — Legacy burden

Moderate · −8. The ecosystem carries versions, APIs, or idioms that must still be supported, so old and new coexist. The compatibility surface becomes a shared resource that every new feature has to respect, so no part of the ecosystem can change freely.

Coupling imposed. Compatibility coupling: every new feature must agree with every version still in service, because the ecosystem will not drop them. The agreement is longitudinal — it runs between old and new code rather than between modules.

Example. Java’s enterprise stack keeps a 1999-era API in service next to a 2026 one, and a library has to keep both working. Ruby’s ecosystem pins older runtimes through older gems, so an upgrade has to wait for the slowest dependency.

Ec7 is the twin of V1. V1 is the language changing under the code; Ec7 is the ecosystem refusing to change.

1.5.3 Evolution

What the Evolution dimension asks: will the code still mean what it means after the language moves?

1.5.3.1 V1 — Breaking changes

Moderate · −8. The language, or its standard library, changes in a way that invalidates existing code. The language itself becomes the shared resource, so every consumer has to revisit code when the language moves.

Coupling imposed. Version coupling: the source must agree with one version of the language, because a later version may change the meaning it had. The agreement is temporal — the same file means different things to two compilers.

Example. Rust’s edition 2024 rewrote the capture rules of impl Trait, required unsafe extern, and reserved gen, so a crate that adopts the edition has to be revisited. Scala’s 2-to-3 migration, Nim’s and Zig’s moving standard libraries, and Haskell’s library-level breakage put the same cost on their users.

An edition system is the mitigation, not the absence. Rust isolates the changes to an opt-in edition and is still charged, because the cost of the migration is real.

1.5.3.2 V2 — Frozen definition

Minor · −4. The language is complete and will not change further. “What the language will do next” becomes settled, which removes one shared uncertainty — and also means that a missing feature or a defect is permanent.

Coupling imposed. Definition coupling: the program must agree with a definition that will not move again, because the language is complete. The agreement is settled rather than absent, which is why a freeze can be a benefit and a trap at once.

Example. Objective-C 2.0 is effectively the last revision of the language, and platform-facing work moved to a successor. Turbo Pascal’s definition is frozen at 7.0.

Minor because a freeze is a promise. Nothing in the existing code will break.

1.5.3.3 V3 — Discontinued

High · −15. The language has no vendor, no steward, and no expectation of another version. The language becomes a shared resource that will never be maintained again, so every defect and every security issue is permanent, and every dependency is a dead end.

Coupling imposed. Stewardship coupling: the program must agree with a language that no one maintains, because there is no second party left to renegotiate with. Every defect becomes a term of the agreement, and no local escape exists because a migration path would have to be supplied by the missing steward.

Example. Borland ended the Turbo Pascal line in the mid-1990s and moved it to a successor product. Microsoft announced VBScript’s deprecation in 2023, with removal from the operating system planned after 2027, so the remaining life of the language is a published countdown.

High because the cost is total and irreversible. There is no migration path inside the language, and none can be added.

1.5.3.4 V4 — Standards that move glacially

Minor · −4. The language is defined by a standard that revises on a multi-year cycle, so a needed feature waits for the committee. The definition becomes a shared resource whose revision schedule is not the user’s, and a program’s design is bounded by when the standard catches up.

Coupling imposed. Revision coupling: the program’s design must agree with a committee’s calendar, because the definition moves on a multi-year cycle. The agreement is with a date rather than with a decision, so no local act can advance it.

Example. SQL:2023 appeared seven years after SQL:2016, so a feature outside the standard stays an extension in the meantime. Ada 2022 followed Ada 2012 by a decade, and the POSIX shell specification moves slowly enough that a shell’s own extensions are the practical definition of its behaviour.

Minor because implementations commonly lead the standard with extensions — at the cost of portability, which is charged elsewhere, under P2 and Ec2.

1.5.4 Domain

What the Domain dimension asks: is the language shaped for one target so tightly that adopting it means adopting that target?

1.5.4.1 D1 — Domain or vendor lock-in

High · −15. The language exists to serve one domain or one vendor’s product, so adopting it is also adopting that target. The domain’s model becomes the shared resource, and every abstraction in the program has to be expressible in it.

Coupling imposed. Domain-model coupling: every abstraction must agree with one target’s model, because adopting the language is adopting the target. The agreement reaches the design and not only the deployment, so a choice of language settles what the program can say.

Example. MATLAB’s library set targets one vendor’s platform, so adoption is a procurement decision as much as a technical one. PL/SQL exists to run inside an Oracle database, so the Oracle data tier is the only domain. SAS’s data-step model is the SAS library. Transact-SQL’s domain is one database engine, and Solidity’s is one blockchain’s execution model. Rocq’s, SPARK’s, and Lean’s domain is machine-checked proof.

D1 and P1 ask different questions. P1 is where the artifact can run; D1 is what the language is for. MATLAB is charged D1 and not P1: the vendor’s platform is the language’s purpose, and the artifact is not confined to one machine.

1.5.4.2 D2 — Poor fit for general application work

Moderate · −8. The language is a bad fit for ordinary application work, because the constructs it lacks make ordinary logic expensive. Every abstraction the program needs has to be built from primitives, so the program’s structure becomes a local invention rather than a shared convention.

Coupling imposed. Expressiveness coupling: every ordinary abstraction must agree with the primitives the language supplies, because nothing above them is standard. The agreement is made per project, so two programs in the language share a language and little else.

Example. Assembly has no types, no data abstraction, and no error path, so application logic is expensive to write and to change. Scratch has no file access, no network, and no general library, so it is not usable for a production application. Prolog’s computation model fits relational search and resists ordinary imperative application code. Ada’s and Fortran’s constructs fit their own problem classes first.

Presence is judged against general-purpose use, not against the language’s intended domain — otherwise every domain-specific language would be charged twice, once for its fit and once for its narrowness.

1.5.4.3 D3 — A framework dictates the architecture

Moderate · −8. The idiomatic way to use the language is inside a host framework whose execution model decides the program’s structure. The framework’s callbacks, lifecycles, and conventions become the shared resource, so the program’s architecture is set by a third party.

Coupling imposed. Architecture coupling: the program’s structure must agree with a host framework’s lifecycle, callbacks, and conventions, because the idiomatic path into the language runs through the framework. The agreement sits above the code — changing it means changing what calls the code, not what the code does.

Example. A Java program is idiomatically assembled as a Spring application, so its wiring, its transactions, and its entry points follow the framework’s model. A C# program follows the same path through ASP.NET. In Groovy the build script and the pipeline run inside a host’s execution model and obey its conventions. Go’s project layout and module conventions are conventions the toolchain assumes.

Moderate because the framework is hard to leave: the code is written against the framework’s lifecycle, not merely called by it.

1.5.5 Learning

What the Learning dimension asks: what does a practitioner pay to become productive, and who bears that cost?

1.5.5.1 L1 — Steep learning curve

Moderate · −8. A practitioner has to learn concepts that build on nothing else they already know. That knowledge becomes a shared resource with a narrow entry, so the onboarding cost is borne by every team and every hiring decision.

Coupling imposed. Onboarding coupling: productivity must agree with a body of concepts that the practitioner’s background does not supply, because the language builds on nothing familiar. The agreement is with people rather than with code, so it is paid again at every hire.

Example. Erlang asks for functional programming, the actor model, and OTP at once, on top of a Prolog-derived syntax. Rust asks for ownership, borrows, and lifetimes. Haskell asks for type classes, monads, and laziness. Lean and Rocq ask for dependent types and proof engineering. Ada’s language and its annexes are large enough that a team learns them for a while.

L1 measures distance from the mainstream, so a language whose concepts are unusual is charged even when the concepts are excellent. The cost is real even when the design is good.

1.5.5.2 L2 — Large surface, or several paradigms at once

Minor · −4. The language is large, or it supports several paradigms at once, so there are many ways to write the same thing. “The way we do it here” becomes the shared resource, and it has to be written down, because the language will not narrow the choice.

Coupling imposed. Idiom coupling: each contributor must agree with the team on which of several available ways to write the same thing, because the language refuses to narrow the choice. The agreement is a document rather than a check — the language will not enforce it.

Example. Ada’s standard library, annexes, and profiles add to a large core. C++ mixes four paradigms and several generations of idiom. Scala, Kotlin, F#, Elixir, Erlang, Nim, and Swift each admit both object and functional styles. PowerShell mixes cmdlets, .NET calls, and pipelines.

Minor because a team can cut the surface with a style guide — but the guide is the team’s, not the language’s.

1.5.5.3 L3 — Sparse or unintuitive documentation

Minor · −4. The reference material is thin, or it omits the knowledge a practitioner actually needs. The understanding of the language becomes a shared resource held privately, so it is rediscovered by each practitioner in turn.

Coupling imposed. Knowledge coupling: understanding must agree with whoever already holds it, because the reference does not supply it. The agreement is private, so it leaves when the practitioner leaves.

Example. In Lean 4, why a simp call succeeded, or why an instance was not found, has to be read out of the goal state and the option list rather than from a reference. Zig’s language reference is still evolving with the language, so the authoritative answer is sometimes the compiler’s source.

Minor because it is the cheapest criterion to fix externally: a book, a wiki, or a community can supply what the vendor did not.

1.5.6 Syntax

What the Syntax dimension asks: how much of the reader’s attention does the notation itself consume?

1.5.6.1 S1 — Verbose

Moderate · −8. The notation requires many words or lines to express a small idea. The reading surface grows, so a lower signal-to-noise ratio is a cost every reader pays on every review.

Coupling imposed. Reading-surface coupling: every reader must pay the same notation cost for the same idea, because the language requires the words. The agreement is uniform and unavoidable, which is why a local style cannot reduce it.

Example. A PL/SQL unit needs a declaration section, a BEGIN ... END block, and an exception section before it does anything. A SAS step is a list of KEYWORD option; lines, and LENGTH, FORMAT, and LABEL add lines before any data is touched. COBOL and Ada require a declaration before nearly every statement, and Fortran and Delphi carry a long legacy of required keywords.

S1 is not a style preference. It is charged where the language requires the words, not where a programmer chose them.

1.5.6.2 S2 — Inconsistent

Moderate · −8. Comparable things are written differently, so there is no rule to learn, only cases. The syntax becomes a shared resource whose mapping is not predictable, so every reader has to look things up.

Coupling imposed. Mapping coupling: the reader must agree with a name’s behaviour through memorised cases rather than through a rule, because the language supplies no predictable mapping. The agreement is per name, so it grows with the library and never generalises.

Example. PHP’s standard library names follow no single rule: str_replace, strpos, and str_contains disagree on placement of the underscore and on argument order between neighbours. Perl’s built-ins read the current context, so $_, @_, and %+ change what a call does.

S2 is the most expensive syntax criterion because it cannot be automated away. A formatter normalises layout, not names.

1.5.6.3 S3 — Punctuation-heavy

Minor · −4. The notation leans on dense delimiters, so the structure is carried by characters the reader has to match. Reading then requires tracking a delimiter stack, which couples comprehension to an editor’s matching and folding.

Coupling imposed. Delimiter coupling: the reader must agree with the editor on where a structure begins and ends, because the structure is carried by characters rather than by layout. The agreement is paid per reading, not per change, which is why tooling can absorb most of it.

Example. Common Lisp’s parentheses nest deeply, and the text is hard to read without an editor that matches them. Perl’s sigils add a character to nearly every variable reference.

Minor because tooling removes most of it, and because the cost is paid once per reading rather than once per change.

1.5.7 Testing

What the Testing dimension asks: how does the language help the program find its own mistakes, and how much of that work is left to the team?

1.5.7.1 G1 — Weak test tooling

Moderate · −8. The language or its standard toolchain provides no test framework, assertion, or runner. The verification of the code becomes a shared resource that each project has to build and maintain for itself.

Coupling imposed. Verification coupling: each project must agree with its own harness about what “tested” means, because the language ships no framework, assertion, or runner. The agreement is local, so a result is only as meaningful as the harness that produced it.

Example. In Assembly a test is another program that runs and compares, and the harness lives outside the assembler. Scratch has no assertion and no runner, so a test is a manual run. Turbo Pascal ships no test framework, and neither do most shells beyond what an exit code can say.

G1 records what the language and its standard toolchain ship. A mature third-party framework can remove the practical cost without changing what the language provides — which is what the criterion is about.

1.5.7.2 G2 — Poor debugging

Moderate · −8. Finding a failure means reading machine state or log output rather than a named abstraction. The diagnosis becomes a shared resource held only by whoever can read the raw state, so debugging skill does not travel with the code.

Coupling imposed. Diagnosis coupling: whoever debugs must agree with the machine’s own state about what happened, because the language offers no named abstraction over it. The agreement is personal — it exists in one practitioner’s reading skill and nowhere in the code.

Example. A debugger that exposes registers and raw memory turns a fault into a machine-state question. Haskell’s laziness means the failing expression is not the one that produced the value. Zig’s comptime evaluation and Unison’s content-addressed code make the executed form differ from the source. Turbo Pascal’s IDE steps and watches variables, with no assertion or coverage support.

G2 is not G3. G2 is “you cannot see what happened”; G3 is “nothing told you beforehand”.

1.5.7.3 G3 — Errors only at run time

Minor · −4. A mistake of the kind a compiler could catch is deferred to the executed path. The correctness of an unexecuted path is then unknown, so coverage becomes the only evidence of safety — a shared resource that every contributor has to maintain.

Coupling imposed. Coverage coupling: the correctness of an unexecuted path must agree with whatever the tests happened to reach, because the compiler declined to decide it. The agreement is a property of the suite rather than of the source, so it changes when the tests change.

Example. A wrong block in Scratch is found by running the project. A type mistake in Elixir, Erlang, Python, Ruby, JavaScript, or Perl surfaces on the path that reaches it. TypeScript’s any re-opens the gap the language closes elsewhere. In Prolog a goal fails at run time, and in R a coercion fails when it is evaluated.

G3 is deliberately T1 counted again. The same language property costs once as coupling, under T1, and once as deferred verification, here. The model counts both because a program pays both — and the dimensions are separate because the remedies are separate.

1.5.8 Security

What the Security dimension asks: does the language make a whole class of exploit possible, or a whole class of mistake silent?

1.5.8.1 Q1 — Memory-unsafe

High · −15. The language permits access to memory outside the bounds of an object, and to storage that has already been freed. Every pointer becomes a shared resource with no owner and no bound, so a fault in one module can corrupt another module’s data.

Coupling imposed. Memory-bound coupling: every pointer must agree with the object it addresses, because the language keeps no record of the bound. The agreement crosses module and trust boundaries alike, which is why a violation is an exploit rather than a defect.

Example. In C a buffer overflow, a use-after-free, a double free, and an out-of-bounds write are all possible, and nothing in the language prevents them. In C++ the same faults compile: reinterpret_cast, pointer arithmetic, and manual new/delete remain available, and the standard library is safe only where a program uses it. Zig keeps manual memory management and undefined behaviour, so a use-after-free and a mistyped @ptrCast are reachable. In Assembly an address is a number, with no abstraction in between.

Q1 is the only criterion attached to a class of vulnerability. The others price maintainability; this one prices an exploit, which is why it carries the top weight.

1.5.8.2 Q2 — Dynamic or weak typing as a security risk

Moderate · −8. A value from an untrusted source arrives with no declared type, so keeping it away from a dangerous operation is the programmer’s check. The trust boundary becomes a shared resource that the type system does not mark, so a missing check leaves no trace.

Coupling imposed. Trust-boundary coupling: a value from outside must agree with the dangerous operation it will reach, because the type system does not mark which values are trusted. The agreement is written by hand at the boundary and leaves no trace when it is missing.

Example. In Python, Ruby, JavaScript, and PHP a request value is whatever the input made it, so a check has to be written and kept. A Prolog term carries no declared type, so a value from an untrusted source is separated from a goal only by an explicit test. In Smalltalk a type confusion is discovered at run time, so an unexecuted path stays unchecked.

Q2 is Q1’s sibling for dynamic languages. The same missing type information costs once as coupling, under T1 or T2, and once as a security surface here.

1.5.8.3 Q3 — A known injection surface

Moderate · −8. The language turns text into code or commands as an ordinary operation, so concatenated input is an injection path. The code’s own text becomes a shared resource reachable from data, so a data path can become a control path.

Coupling imposed. Data-to-control coupling: text from a data path must agree with the parser that will execute it, because the language supplies conversion from text to code as a built-in operation. The agreement is available to whoever controls the text, so the boundary that matters is the input rather than the code.

Example. PL/SQL’s EXECUTE IMMEDIATE and DBMS_SQL build a statement as text. R’s eval(parse(text = ...)), do.call on a string, and system() turn text into code or into a command. A shell’s eval sends a quoted variable back through the parser. In Transact-SQL and SQL a statement assembled by concatenation carries the caller’s text. PowerShell’s Invoke-Expression does the same.

Q3 records the language-level facility, not its misuse. Parameterised queries and prepared statements are the mitigation, and the criterion is charged because the facility remains available and remains the path of least resistance.

1.6 Readiness criteria

The readiness axis uses the same three tiers and the same capped-sum-per-dimension arithmetic as coupling. It asks a different question: not how much the language forces two parts of a program to agree, but whether the artifact can be rebuilt, verified, observed, worked in quickly, connected to another language, and governed openly. Breadth, escape, and silence decide the tier here too.

Readiness (12) Reproducibility (2) — Y1 · Y2 Supply chain (2) — Y3 · Y4 Observability (2) — Y5 · Y6 Feedback (2) — Y7 · Y8 Interop (2) — Y9 · Y10 Governance (2) — Y11 · Y12

1.6.1 Reproducibility

What the Reproducibility dimension asks: can you rebuild the artifact and get the same thing?

1.6.1.1 Y1 — Build not reproducible

Moderate · −8. Two builds of the same source do not produce the same artifact. The build environment — timestamps, absolute paths, file order, a random seed — becomes an input the source does not name, so the artifact is tied to the machine that made it and no third party can confirm that it matches the source.

Coupling imposed. Build-environment coupling: the artifact must agree with inputs that the source does not name — timestamps, paths, file order, a seed — because the build records them rather than normalises them. The agreement is invisible in the source and fixed at build time.

Example. Maven, PyPI, and RubyGems packages are largely unreproducible as published, because archive metadata and build paths leak into the artifact; Cargo and npm fix those values and reach reproducible builds [3]; the general problem is surveyed in [4].

The criterion records the default of the language’s own build tool, not what a careful team can arrange.

1.6.1.2 Y2 — Dependencies not pinned

Moderate · −8. The dependency set is not pinned, so the build resolves to different versions at two points in time. The resolved graph is a shared resource that the source does not record, and the build that passed is not the build that ships.

Coupling imposed. Resolution coupling: the build must agree with the index about which versions exist, because nothing in the source fixes the answer. The agreement is made at build time and then forgotten, so two builds of one source can differ.

Example. A Python requirements.txt without hashes, or a setup.py that names ranges, leaves the resolution to the index. Cargo’s Cargo.lock, npm’s package-lock.json, and Go’s go.sum pin the graph.

1.6.2 Supply chain

What the Supply chain dimension asks: can you verify what you shipped?

1.6.2.1 Y3 — No signed provenance for published packages

Moderate · −8. A published package carries no verifiable statement of who built it and from what source. The artifact’s origin is a shared resource that the consumer must take on trust, so a compromised build is indistinguishable from a genuine one.

Coupling imposed. Origin coupling: the consumer must agree with the publisher’s build, because the artifact carries no evidence of who made it or from what. The agreement rests on trust in a supply chain the consumer cannot inspect.

Example. npm and PyPI can attach Sigstore-signed provenance, and Maven Central now validates Sigstore signatures [5], [6]. A bare checksum gives integrity against corruption, not against substitution.

Presence is “no signing or provenance facility”, not “no Sigstore”. Maven Central requires a PGP signature, npm ships registry signatures, PyPI ships TUF metadata and opt-in attestations, RubyGems and NuGet have their own signing, and Go’s checksum database is a signed transparency log. A registry that offers only a bare checksum, and a language with no registry, are charged.

1.6.2.2 Y4 — Registry without namespacing

Minor · −4. The package registry has one flat name space and no ownership model, so a name can be claimed or confused. The name is the shared resource, and a dependency can be redirected to a different author without a change in the consumer’s source.

Coupling imposed. Name-ownership coupling: the consumer must agree with whoever holds the name, because the registry records a first claim rather than an author. The agreement can be reassigned without the consumer’s code changing at all.

Example. PyPI, npm, crates.io, RubyGems, Hex, NuGet, and pub.dev reserve a flat name first-come, and npm and PyPI have both suffered dependency-confusion and typosquatting attacks. Maven Central’s group coordinates, Go’s module paths, CPAN’s nested namespaces, SwiftPM’s scope.package-name, and Packagist’s vendor/package carry a namespace. A language with no package registry is not charged: the mechanism is about a name space that exists.

1.6.3 Observability

What the Observability dimension asks: can you see what the program does in production?

1.6.3.1 Y5 — No stable tracing and metrics standard

Moderate · −8. The language and its standard library provide no agreed way to emit traces and metrics, so each project invents one. The instrumentation contract is a shared resource, and a distributed call cannot be followed across a service the project did not write.

Coupling imposed. Instrumentation-contract coupling: every service must agree with every other on a format for traces and metrics, because no standard supplies one. The agreement is invisible until a call crosses a boundary the project does not own.

Example. OpenTelemetry ships stable tracing for C++, C#, Erlang/Elixir, Go, Java, JavaScript, PHP, Python, Ruby, and Swift, beta for Rust, and development status for Kotlin [7]. A language on the JVM or the CLR can use the stable Java or .NET implementation.

1.6.3.2 Y6 — No structured logging in the standard library

Minor · −4. Diagnostics are free text, and the standard library exposes no structured record of them. The program’s state is a shared resource held in an unstructured log, so a question about a running system requires a new deployment to answer.

Coupling imposed. Log-schema coupling: the running system must agree with whoever parses its output on a format, because the standard library records no structure. The agreement is informal and unwritten, so it breaks when a message changes.

1.6.4 Feedback

What the Feedback dimension asks: how fast is the edit-build-test loop?

1.6.4.1 Y7 — Slow or non-incremental build

Moderate · −8. A change costs a long rebuild, and the rebuild repeats work it did not need to. The build is a shared resource on the critical path of every edit, so the feedback loop is set by the compiler rather than by the size of the change.

Coupling imposed. Feedback coupling: every edit must agree with the compiler’s rebuild cost, because the build repeats work that the change did not require. The agreement is paid on every edit that ends in a build, whatever the size of the change.

Example. A large C++ translation unit and a large Rust crate both pay a long codegen and link step. Go and a scripting language pay almost none.

1.6.4.2 Y8 — No fast in-editor analysis

Minor · −4. The language has no incremental analyser that answers a question about the code as it is typed. The type and reference information is a shared resource available only after a full build, so the editor cannot answer while the author is working.

Coupling imposed. Answer-latency coupling: the editor must agree with a full build about what the code means, because no incremental analyser holds the answer. The agreement arrives after the fact, so the author works without it.

1.6.5 Interop

What the Interop dimension asks: can the language work with code written in another language?

1.6.5.1 Y9 — No foreign-function interface

Moderate · −8. The language has no in-process foreign-function interface: a library written in another language is reachable only by starting a process or through an external binding. Every library must be reimplemented in the language, so the program is coupled to the ecosystem of one language and cannot reuse the rest. This is the mirror of T14: T14 prices an unchecked boundary, and Y9 prices the absence of one.

Coupling imposed. Ecosystem-reach coupling: the program must agree to live inside one language’s library supply, because no in-process path reaches another language. The agreement is with a community rather than with a component, so a library choice cannot renegotiate it.

Example. A shell reaches another language only by starting a process. Scratch, Solidity, and Rocq reach none. SQL is not charged: PostgreSQL, Oracle, and SQL Server each load native code through a user-defined function (LANGUAGE C, extproc, SQLCLR).

1.6.5.2 Y10 — No maintained portable target

Minor · −4. The program cannot be compiled to a portable target, so it runs only where the language’s own runtime runs. The deployment surface is a shared resource fixed by the runtime.

Coupling imposed. Deployment-target coupling: the program’s reach must agree with the runtime’s reach, because the language compiles only to its own execution environment. The agreement is fixed by the implementation, so a new target is a new implementation rather than a new build flag.

Example. Popcorn and AtomVM compile Erlang and Elixir to WebAssembly; GHC, wasm_of_ocaml, Pyodide, php-wasm, webR, and SwiftWasm cover their languages. Julia has no maintained official target.

1.6.6 Governance

What the Governance dimension asks: will the language be maintained, and by whom?

1.6.6.1 Y11 — Single-vendor governance

Moderate · −8. One company decides the language’s direction, and no independent body can change it. The language’s future is a shared resource held by a commercial owner, so the program’s roadmap follows that owner’s priorities.

Coupling imposed. Roadmap coupling: the program’s future must agree with one owner’s priorities, because no independent body can change the language’s direction. The agreement runs one way — the program adapts to the owner’s schedule, never the reverse.

Example. MATLAB, SAS, PL/SQL, Transact-SQL, Objective-C, Delphi, and VBScript are governed by one vendor, and Swift’s project lead is appointed by Apple. Rust, Python, JavaScript, PHP, Kotlin, C#, and Eiffel sit with an independent foundation or a standards body.

1.6.6.2 Y12 — Closed specification

Moderate · −8. The language has no public, complete definition that an independent implementation could follow. The meaning of a program is a shared resource held by one implementation, so portability and third-party tooling depend on reverse engineering.

Coupling imposed. Specification coupling: the program must agree with one implementation’s behaviour, because there is no public definition to agree with instead. The agreement is discovered by experiment, and third-party tooling is built on the same experiment.

Example. ISO C, ISO SQL, Ada, Fortran, and Scheme have published standards. MATLAB, SAS, PL/SQL, Transact-SQL, and Objective-C do not.

1.7 The arithmetic, once more

Present weights are added within a dimension, the total is clamped at −15, and the score is 100 minus the sum of the clamped per-dimension contributions. Here is Go’s Core score, from the criteria above:

Worked example — Go (Core 43): present Core = T4, T12, T14, M2, M3, M4, Er2, Er4, K2, K3.

Dimension Present Clamped sum
Type T4(−4) + T12(−8) + T14(−4) = −16 −15 (clamped)
Construction — 0
Memory M2(−8) + M3(−4) + M4(−8) = −20 −15 (clamped)
Error Er2(−8) + Er4(−4) −12
Concurrency K2(−8) + K3(−8) = −16 −15 (clamped)
Platform — 0

Total clamped = 15+15+12+15 = 57. Core = 100 − 57 = 43. ✓

The Memory and Concurrency dimensions cost more than −15 in raw weight; the clamp holds each at the weight of one High mechanism. The Scoreboard holds the same data for all 65 languages and reproduces every score the same way.

1.8 What the criteria do not measure

The inventory prices the language, before any design decision, and it is honest about its limits.

  • Not design quality. Two programs in the same language differ more from each other than the languages differ from one another. A program can cluster what the language scatters, and a good design decision is exactly that act. The score is the raw material, not the outcome.
  • Not team skill or process. A discipline that a team keeps — a checked build, a mandatory code review of ownership, a lint rule — can hold a High mechanism at bay in practice. The model charges it anyway, because the discipline is not in the language and cannot be relied on by the next team.
  • Not severity in a particular system. The weight judges breadth, escape, and silence. A Minor mechanism in a hot path can cost more than a High one in a corner of the program, and the model deliberately does not try to price that. A weight is a property of the mechanism; a cost is a property of a system.
  • Not the last word on any one language. Every mechanism mark is a factual claim about the language, and a factual claim can be wrong. When one is, the mark is corrected, and every score, table, and ranking derived from it is regenerated. The Scoreboard carries the data so that the claim can be checked rather than trusted.

The formal development. The definitions, theorems, and proofs behind this page are published in:

All published papers

References

[1]
J. I. Brachthäuser, P. Schuster, and K. Ostermann, “Effects as capabilities: Effect handlers and lightweight effect polymorphism,” in Proceedings of the ACM on programming languages (OOPSLA), 2020. doi: 10.1145/3428194.
[2]
B. Nystrom, “What color is your function?” 2015. Available: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/
[3]
G. Benedetti et al., “An empirical study on reproducible packaging in open-source ecosystems,” in Proceedings of the IEEE/ACM 47th international conference on software engineering (ICSE), 2025, pp. 1052–1063. doi: 10.1109/ICSE55347.2025.00136.
[4]
C. Lamb and S. Zacchiroli, “Reproducible builds: Increasing the integrity of software supply chains,” IEEE Software, vol. 39, no. 2, pp. 62–70, 2022, doi: 10.1109/MS.2021.3073045.
[5]
Sigstore Project, “Sigstore: Software signing for everybody.” 2026. Available: https://www.sigstore.dev/
[6]
SLSA Project, “Supply-chain levels for software artifacts (SLSA), specification v1.1.” 2025. Available: https://slsa.dev/spec/v1.1/
[7]
OpenTelemetry Authors, “OpenTelemetry language APIs and SDKs: Status and releases.” 2026. Available: https://opentelemetry.io/docs/languages/