The U Language

One representation.
Every language.
Every answer.

Upload a PHP codebase — or JavaScript, or Python, or all three mixed together. Get back a capability graph that tells you exactly what each module can do, a data-flow analysis that traces user input to database queries, and a security audit with every finding on the original source line. No new language to learn. No code to rewrite. The analysis runs on U, a language designed to be transpiled into, not written by hand.

2,359PHP files transpiled
389Klines of PHP
100%success rate
190Klines of U output

Tested against the full Qbix Platform — core framework, 16 plugins, 453 functions analyzed for capabilities.

The pitch

What U is for

Most programming languages are designed to be written by humans. U is designed to be transpiled into by machines and analyzed by compilers. It is a universal intermediate representation for capability analysis — a common target that PHP, JavaScript, TypeScript, and Python all compile down to, so that one set of analysis tools can examine codebases written in any combination of those languages.

The developer never writes U. They write PHP, or JavaScript, or Python — whatever they already know. The transpiler converts their code to U with a V3 source map that traces every generated line back to the original file and line number. The analysis runs on U. The results reference the original source. The developer sees "your code at Row.php:619 calls a network API that isn't in this module's declared capability set." They fix the PHP. They never touch U.

U is not a language you adopt. It's a language your tools use behind the scenes to tell you things about your code that no single-language tool can.

Design

The language itself

U is deliberately minimal. Fewer constructs means less ambiguity for the analyzer, and a smaller surface for transpilers to target. The entire syntax fits on one page:

d Db.Row                              // class declaration

    name: S                           // typed property (String)
    age: I                            // typed property (Integer)

    f __setup__(name: S, age: I)      // constructor (typed params)
        t.name = name                 // t = this
        t.age = age

    f+G fetch(id: I) -> Db.Row       // static method, return type
        row = DB.query("...", [id])   // +DB capability used here
        r => Db.Row(row.name, row.age)// r => = return

    f save() +IO +DB                  // declared capabilities
        DB.insert(t)
        Files.write("/log", t.name)

Design choices and what they enable:

FeatureSyntaxWhat it enables
Capability modifiers+IO, +Net, +DBCompiler-verified side effect tracking
Mutability tracking+M (mutable), +R (reference)Race condition detection, alias analysis
Static methodsf+G name()Clear call graph — no dynamic dispatch ambiguity
Type annotationsI, S, R, B, TreeCross-language type checking
Pattern matchingx :: Type ? (…)Exhaustiveness checking, type narrowing
Module importso ModuleNameDependency graph, supply chain analysis
Fiber-based async+A modifierConcurrency analysis without callback ambiguity

Every U function signature declares what it can do. A function without +IO cannot touch the filesystem. A function without +Net cannot make network calls. The compiler enforces this as a compile-time proof. If the code compiles, the capabilities are truthfully declared.

Core mechanism

Capability tracking

Every program does things. It reads files, queries databases, makes HTTP calls, hashes passwords, spawns processes. Today, you discover what a program does by reading it — or by running it and watching. U makes these capabilities statically visible.

Six capability modifiers, each representing a class of side effect:

ModifierGrantsPHP examples
+IOFilesystem accessfile_get_contents, fopen, mkdir, unlink
+NetNetwork accesscurl_exec, fsockopen, mail, Http::get
+DBDatabase accessPDO::exec, mysqli_query, DB::select
+CryptoCryptographic opsopenssl_encrypt, password_hash, hash_hmac
+ExecProcess executionshell_exec, exec, eval, proc_open
+UnsafeRaw memory, FFIunserialize, ReflectionClass, variable variables

The analyzer walks the call graph and propagates capabilities upward. If function A calls function B, and B uses +Net, then A transitively has +Net. This propagation continues until a fixed point — every function in the codebase has a complete capability set derived from what it actually does, not what it claims.

The analysis is bottom-up and evidence-based. Every capability tag is grounded in a specific function call on a specific source line.

Infrastructure

Transpilers and source maps

Four transpilers, all producing the same output format:

TranspilerInputLinesSource maps
php-to-u.jsPHP 7/82,113✓ V3/VLQ
js-to-u-treesitter.jsJavaScript/TS936✓ V3/VLQ
py-to-u-treesitter.jsPython 3897✓ V3/VLQ
js2u_convert.jsJS (acorn)155✓ AST-level

Every transpiler returns {code, messages, sourceMap}. The source map entries are {genLine, genCol, srcLine, srcCol, name}, following the V3 source map specification with VLQ encoding. They work in browsers (via btoa), in Node.js (via Buffer), and as standalone .map files.

Source maps are what make U invisible to the developer. The analysis runs on U; the report references PHP line 619. The playground shows PHP on the left, U on the right; clicking a line in either highlights the corresponding line in the other. Errors from the U compiler appear as annotations on the original source.

The PHP transpiler handles: classes, interfaces, abstract classes, traits, namespaces, closures, arrow functions, match expressions, named arguments, null-safe operator (?->), spread operator, fibers, type casts, dynamic instantiation (new $className()), dynamic method calls ($obj->$method()), variable variables ($$var), pass-by-reference (&$param), heredocs, extract/compact, and 1,246 PHP standard library function mappings.

Comparison — linters

Vs PHPStan, ESLint, mypy

Linters find bugs in one language. They check types, flag unused variables, enforce style rules. They are good at what they do. But they operate on syntax, not on semantics. They can tell you a variable might be null. They cannot tell you what your module does to the outside world.

CapabilityPHPStan / PsalmESLint / TSU Analysis
Type checking✓ deep✓ deep✓ cross-language
Null safety
Dead code✓ cross-language
Capability tracking✓ six categories
Data-flow taint analysis✓ source→sink
Supply chain diff✓ capability diff
Cross-language types✓ PHP↔JS↔Python
Access control verification✓ pattern-based
M-of-N signing✓ module-level
Concurrency analysis✓ fiber-aware

PHPStan at level 9 is the most sophisticated PHP static analyzer available. It catches type errors, method existence, return type mismatches, and dead code within PHP. What it cannot do is tell you that UserController::uploadAvatar touches the filesystem and the network, while UserController::index is pure computation. That distinction is invisible at the PHP syntax level because the capabilities flow through framework abstractions — facades, service containers, middleware chains.

U makes the distinction visible because the transpiler resolves those abstractions. Storage::disk('s3')->put(...) transpiles to Storage.disk("s3").put(...), and the capability analyzer recognizes Storage.* as +IO. Mail::to(...)->send(...) transpiles to Mail.to(...).send(...), recognized as +Net. The framework patterns that are opaque to PHPStan are transparent to U because the transpiler flattens them into a representation the analyzer was designed to read.

Even for PHP alone, U analysis finds things PHPStan cannot — because the question is different. PHPStan asks "is this type-safe?" U asks "what can this code do to the world?"

Comparison — SAST

Vs Semgrep, CodeQL, SonarQube

Static Application Security Testing tools are closer to what U does. They look for vulnerability patterns — SQL injection, XSS, path traversal. They scan source code for known-bad patterns and flag them.

CapabilitySemgrepCodeQLSonarQubeU Analysis
Pattern matching✓ excellent✓ deep
Taint tracking✓ basic✓ deep
Multi-language✓ 30+ langs✓ 12 langs✓ 29 langs3 langs + growing
Cross-language analysis✗ per-lang✗ per-lang✗ per-lang✓ unified graph
Capability inventory✓ per-module
Supply chain capability diff✓ version-to-version
Deterministic/reproducible~ cloud-dependent✓ same input = same output
Custom rule authoring✓ YAML✓ QL lang~ XML✓ U pattern rules
Module signing/attestation✓ M-of-N
CostFree tier / paidFree for OSS / paidFree tier / paidOpen analysis

Semgrep is pattern-based: you write rules that match syntax trees across 30+ languages. But each language is analyzed independently. A Semgrep scan of a PHP backend and a JS frontend produces two separate reports with no shared understanding of how data flows between them.

CodeQL builds a database of your code and lets you query it with a purpose-built query language. It's the most powerful SAST tool available. But it's per-language — a CodeQL database for PHP and a CodeQL database for JavaScript are separate artifacts. You can't write a single query that traces data from a PHP API endpoint through a JS frontend to a database call.

U analysis can, because the PHP and the JS are both transpiled to the same representation. A value that enters through a PHP $_GET and gets passed to a JS fetch() is one data flow in one graph. The source map traces it back to both the PHP line and the JS line.

The other difference is the question being asked. SAST tools look for vulnerabilities — known-bad patterns. U analysis produces a capability inventory — a complete picture of what each module does, not just what it does wrong. The inventory is useful even when there are zero vulnerabilities, because it answers the question an auditor, regulator, or M-of-N signer needs answered: "what can this code actually do?"

Comparison — capability systems

Vs WebAssembly, Deno, Austral

Several systems enforce capability restrictions at runtime or compile time. U is not the first to track capabilities. It's the first to do it as an analysis target for existing codebases.

SystemApproachExisting code?Cross-language?
WebAssemblyRuntime sandbox — no filesystem, no network unless host providesRewrite in Rust/C/GoWasm-only
DenoPermission flags at startup: --allow-net, --allow-readJS/TS only, must run in DenoJS/TS only
AustralLinear types with capability tracking in the type systemNew language, rewrite requiredAustral only
CapsicumOS-level capability mode — process loses ambient authorityWorks with existing C codeC/OS-level only
UTranspile existing code, analyze capabilities staticallyPHP, JS, Python as-isUnified graph

Deno's permission model is the closest analogy. Deno says "this program can access the network" at the process level. U says "this function can access the network, and here's the call chain that proves it" at the function level. Deno requires you to rewrite your code for the Deno runtime. U analyzes the code you already have.

WebAssembly's sandbox is the strongest runtime guarantee — a Wasm module literally cannot call filesystem APIs unless the host provides them. But you have to rewrite your code in a Wasm-compatible language. U gives you a static proof of the same property — this module doesn't touch the filesystem — without changing your runtime.

Austral is the most intellectually similar project. It's a new language with linear types and capability tracking built into the type system. It's elegant and correct. But it requires you to write new code in Austral. U's contribution is that you don't have to. You keep your PHP. You keep your JavaScript. The capability analysis happens on the transpiled representation, and the results map back to your original code.

Architecture

Five layers of capability inference

The capability analyzer doesn't use a single technique. It stacks five layers, each catching what the others miss:

01Known-API signature tables

A curated map from 403 standard library and popular library calls to capabilities. file_get_contents+IO. curl_exec+Net. PDO::exec+DB. openssl_encrypt+Crypto. shell_exec+Exec. This handles the majority of real-world code because most capability usage goes through standard APIs.

The table includes receiver-type tracking: pdo.exec("SQL") is tagged +DB, not +Exec. criteria.copy() is pure, not +IO. Http.get(url) is +Net, not skipped as a harmless .get() method.

02Import/require analysis

104 module-level rules. Importing the module is enough to tag the capability. require('fs')+IO. import requests+Net. use PDO+DB. This is the conservative layer — if you import a capability-bearing module, you get tagged, whether you call a function from it or not.

03Call graph + transitive propagation

Parse all U modules, extract function definitions and call sites, build a directed graph. Propagate capabilities upward through fixed-point iteration. If function A calls function B, and B uses +Net, then A inherits +Net. The propagation works across module boundaries in a full-project analysis.

04Dynamic dispatch pattern detection

21 patterns that defeat static analysis get tagged conservatively. eval()+Exec (it could do anything). $$var+Unsafe (variable variables are unpredictable). call_user_func()+Exec. unserialize()+Unsafe. pickle.load()+Unsafe. If you can't prove what it calls, assume the worst.

05Source annotations + verification

Developers can declare intended capabilities in comments: /** @capability +DB +Crypto */. The analyzer extracts these declarations and verifies them against actual usage. If the code declares [+DB, +Crypto] but also calls curl_exec, that's an error: undeclared +Net. If it declares +IO but never touches the filesystem, that's a warning: unnecessary declaration. The annotations are claims; the analysis is verification.

Real-world test

Framework support: Laravel and Symfony

The analyzer was tested against realistic Laravel and Symfony controllers using idiomatic framework patterns — facades, dependency injection, components, middleware:

Laravel — 10 functions, 10 correct

FunctionDetectedHow
index(pure)Eloquent facade — no direct call visible
stats+DBDB.select(), DB.table()
uploadAvatar+IOStorage.disk() + Files.read()
sendWelcome+NetMail.to()
cachedProfile+DBCache.remember()
fetchExternalApi+NetHttp.get()
generateReport+ExecPHP.shell_exec()
encryptData+Cryptoopenssl_encrypt + password_hash
processOrder+Eventdispatch() + event()
clearCache+ExecArtisan.call()

Symfony — 9 functions, 9 correct

FunctionDetectedHow
list(pure)DI-injected repository
create(pure)DI-injected EntityManager
rawQuery(pure)DI-injected EM
writeFile+IOFilesystem component: dumpFile, mkdir
readConfig+IOfile_get_contents + file_put_contents
callApi+NetHttpClient.create()
sendNotification+Netmailer.send()
generatePdf+Execnew Process([...])
hashPassword+Cryptopassword_hash + random_bytes + hash_hmac

The functions marked "(pure)" are correctly classified. A Symfony controller method that receives an EntityManagerInterface through dependency injection doesn't itself call any capability API — it calls $em->persist(), and the capability lives in the EntityManager implementation, not in the controller. When the full project is analyzed and Doctrine's EntityManager class comes through the transpiler, Layer 3 (call graph propagation) connects them transitively.

Unique advantage

Cross-language analysis

No existing tool can analyze a mixed PHP/JS/Python codebase as a single system. ESLint cannot see PHP types. PHPStan cannot see JavaScript callbacks. Semgrep scans each language separately. CodeQL builds separate databases per language.

U unifies them. A PHP backend and a JavaScript frontend transpile to the same representation. The analyzer builds one call graph, one capability graph, one data-flow model. A value that enters through a PHP $_GET['id'], gets passed to a JS fetch('/api/item/' + id), and ends up in a Python cursor.execute("SELECT * WHERE id=" + id) is one data-flow path in one analysis, traceable through source maps to all three original files.

This matters for the Qbix Platform specifically, which has parallel PHP and JS implementations of the same classes. The analyzer can verify that Db_Row::save() in PHP and Db.Row.prototype.save in JavaScript have compatible signatures, compatible capability profiles, and compatible data-flow patterns. Discrepancies are reported with source lines in both languages.

Integration

The Safebox connection

U was designed for Safebox — a sealed execution environment where code runs with verified capabilities. The U compiler is closed-source, injected into every Safebox post-seal. The capability analysis is the trust boundary.

Before code runs inside a Safebox, the analysis verifies that its capability declarations match its actual behavior. A module that declares [+DB] but actually uses [+DB, +Net] is rejected. M-of-N signers attest that the analysis passed and the capabilities are truthfully declared.

The signing is baked into the .u module format — a header with signature slots. Each signer independently runs the analysis, verifies the results, and signs. The Safebox JIT checks signatures before linking modules. A module without enough valid signatures doesn't load.

The pipeline is deterministic. Same input, same transpiler version, same analysis version, same output. The signers don't need to trust each other. They need to trust the math.

Transpile the code. Analyze the capabilities. Sign the results. Verify at load time. Every step is reproducible, every finding is traceable, every signature is independently verifiable. — The simple version

Evidence

The proof: 389,083 lines

The entire Qbix Platform was transpiled — every PHP file in the repository, including all 16 plugins. The Qbix Platform is a production social framework with real database adapters tested against three running database servers, real user authentication, real content streaming, real payment processing.

2,359PHP files
389Klines transpiled
0errors
75.5%pure modules

Capability breakdown

CapabilityModules% of total
(pure)1,78275.5%
+IO29312.4%
+DB25010.6%
+Crypto893.8%
+Net512.2%
+Exec291.2%
+Unsafe150.6%

Only one file in the entire platform has all six capabilities: Q/Utils.php, the kitchen-sink utility class. The core Q.php has five. Most plugin code — the actual application logic — is pure or single-capability. This is the pattern you'd expect in a well-structured codebase: capabilities concentrated in the framework core, application code mostly pure.

Database ORM — verified against real servers

The Db ORM classes (19 files, 15,057 PHP lines → 7,428 U lines) were not just transpiled — they were tested against real running databases through both the Zend engine (PHP 8.3 embed, 40/40 tests) and Node.js (v22 native addon, 24/24 tests), across SQLite, MySQL, and PostgreSQL. The transpiled code represents a working system, not just a syntactic transformation.

Implications

What this means for the industry

Software security today is done by humans reading code, or by tools that look at one language at a time and find known-bad patterns. Both are slow and incomplete. The U analysis pipeline changes the economics.

For security auditors

A 100-file PHP codebase that takes a human two weeks to audit can have its mechanical findings produced in minutes: capability inventory, data-flow paths, access-control verification, supply chain analysis. The auditor reviews the findings, each linked to original source lines, and spends their time on the judgments that require human expertise — not on reading code.

For regulated industries

Financial services, healthcare, and government systems need to demonstrate that their software meets specific security requirements. The capability report is a machine-generated, deterministic, reproducible artifact. An auditor verifies by re-running: same input, same output. This is the kind of evidence that regulators can build compliance frameworks around.

For supply chain security

When a dependency updates, the analyzer diffs the capability graphs. If version 2.3.1 of a library had [+DB, +Crypto] and version 2.4.0 has [+DB, +Crypto, +Net, +Exec], that's a supply chain risk flag. The new capabilities are traced to specific lines in the updated library. Capability diffing — what can this code do now that it couldn't do before? — rather than dependency scanning against CVE databases.

For AI code generation

As AI generates more code, the question "what does this code do?" becomes harder to answer by reading it. U analysis answers it structurally: the AI-generated module has capabilities [+DB, +IO], here are the specific calls that create those capabilities, and here's the call chain from each entry point. The human reviewer checks the capability report, not the code.

For open-source trust

An npm package or Composer package that publishes its U capability report alongside its source gives downstream users a machine-verifiable claim about what the package can do. If the report says [+Crypto] and nothing else, and you can reproduce that report from the published source, you know the package doesn't touch the filesystem, doesn't make network calls, and doesn't spawn processes. That's a stronger statement than any amount of human code review.

"What can this code do?" has a precise, machine-checkable answer. U provides it. — The simple version

The hard question

Trusting trust

In 1984, Ken Thompson gave a Turing Award lecture called "Reflections on Trusting Trust." He demonstrated that a compiler can be modified to insert a backdoor into any program it compiles — including future versions of itself — and the modification is invisible in the source code. The conclusion: you can't trust code you didn't write, and you can't even trust code you did write if the compiler is untrustworthy.

Forty years later, the problem is unsolved in general. But it can be bounded.

The U compiler is closed-source. It ships as a binary, injected into the Safebox after seal. This is a deliberate choice, not an evasion. The compiler is the security boundary — its analysis passes, its capability verification, its proof generation are what make the Safebox trust model work. Making the compiler open-source would let adversaries study and defeat the analysis. Making it closed-source means the trust has to come from somewhere else.

Where it comes from:

Hash pinning. The compiler binary has a known hash. The Safebox attestation chain includes the compiler hash. Anyone can verify that the compiler running inside the box is the same one that was audited.

Reputable auditors. The compiler is audited by independent security firms with access to the source. Their attestation — "we reviewed this compiler, it does what it claims, it doesn't insert backdoors" — is a public artifact. The auditors' reputation is their bond. This is the same trust model as a certificate authority, but for a compiler rather than a domain name.

Reproducible output. The compiler is deterministic. Same input, same output, every time. If the compiler inserts a backdoor, the backdoor is in every compilation of the same source. This makes detection far easier than Thompson's scenario, where the backdoor is invisible in the source — here, you can diff the output of two different compiler versions against the same source.

Eventually, open source. The compiler may be open-sourced when the analysis techniques are mature enough that openness doesn't compromise the security model. The architecture is designed so that this transition is possible without breaking existing attestation chains.

Thompson's essay ends with "you can't trust code that you did not totally create yourself." The Safebox response: you can't, but you can trust a chain — attested hardware, hash-pinned compiler, reproducible analysis, M-of-N signatures on the results. Breaking the chain requires compromising multiple independent parties simultaneously.

For a deeper treatment of how U fits inside the Safebox trust model — the attested analysis chain, .u.meta proof sections, and compositional reasoning — see U — The Intermediate Form.

Unique to U

M-of-N signing in the loader

Every U module carries a .u.meta section: a structured record of its capability surface, emitted by the compiler, covered by the binary's hash. Signatures are endorsements of that record. The loader counts them before linking.

The signing lives in the language runtime, not bolted onto a package manager. The loader refuses to link a module without sufficient valid signatures for every capability it declares. A module with [+DB, +Net] needs M-of-N signatures endorsing both +DB and +Net — and the signatures can be partial. One signer vouches for +DB. Another vouches for +Net. A third vouches for both. The capability set accumulates across endorsements.

Does anyone else do this? Deno has permission flags but no signing. WebAssembly has capability restrictions but no attestation chain. npm has package signing but it attests authorship, not capabilities — "this package was published by user X," not "this package can only access the database." NuGet, Maven, Cargo — same: identity signing, not capability signing.

U is the only language where the loader verifies, before linking, that the code's capability surface has been endorsed by enough independent parties. The signatures bind a capability claim to a specific binary hash. Widen the capabilities after signing and every endorsement is invalidated — the hash moved.

Open-source U programs are particularly powerful here. A library published as open-source U code can be compiled by anyone, analyzed by anyone, and signed by anyone. The signatures accumulate from independent auditors who each verified the capability surface independently. The downstream consumer checks: does this module have enough signatures from parties I trust? If yes, link it. If no, don't. No trust in the author required. No trust in any single auditor required. Trust in the count.

Patent pending

The LLM auditor inside the box

Here's the twist that makes closed-source software analyzable without exposing the source.

An LLM with open weights — a model anyone can inspect, whose behavior is reproducible — runs inside the Safebox. It has access to the source code of the program it's analyzing, because the source is inside the sealed environment. It produces a capability analysis, a logic review, and a security assessment. The assessment is signed by the attested environment and published. The source never leaves the box.

The AI is a pinned model version with specific weights running a specific prompt inside an attested environment. Its output is near-deterministic: same code, same prompt, same verdict. The attestation chain covers the model weights, the prompt, the compiler, and the hardware. Anyone can verify that the analysis was performed by the claimed model on the claimed code inside the claimed environment.

What this unlocks: source-available closed source. A company keeps its proprietary code inside the Safebox. The code is not open-source — the company doesn't publish it. But the code is source-available inside the box, where the pinned LLM and the U compiler can analyze it. The analysis results are published. The source is not.

The auditor reads facts, not code. "This module declares [+DB, +Crypto]. The compiler verified the declarations. The LLM found no logic bugs in 47 functions. Three independent signers endorsed the capability surface." The auditor doesn't need the source. The auditor needs the proof.

The Safebox approach asks you to trust the chain — attested hardware, hash-pinned compiler, reproducible analysis, independently verifiable signatures. The chain is short enough to actually inspect.

For the full argument, see U — The Intermediate Form — particularly sections 4–6 on pinned LLMs, the attested analysis chain, and why this is more trustworthy than human auditors.

The big picture

The AI operating system

BeOS had C++. Mac OS X had Objective-C. Android had Java. Every operating system that mattered was defined by a language — the language its kernel was written in, its applications were built with, its APIs were expressed through. The language shaped what programs could do and how the system reasoned about them.

Safebox has U.

U is the best language for a system to reason about. Every function declares its capabilities. Every module carries a compiler-verified proof of what it can do. Every binary embeds a .u.meta section that the loader reads before linking. The M-of-N signing system means untrusted code can be loaded safely — proven at compile time to be incapable of exceeding its declared capabilities.

This is what makes Safebox an operating system rather than a container. A container runs arbitrary code and walls it off from the host. An operating system understands the code it runs — types, capabilities, effects, error surfaces — and uses that understanding to compose programs safely. The U compiler is the kernel of that understanding.

Existing code doesn't have to be rewritten. PHP, JavaScript, Python transpile to U with source maps. The transpiled code runs inside the Safebox with the same capability analysis, the same M-of-N signing, the same .u.meta proofs. The developer writes PHP. The system reasons about U. The gap is bridged by transpilation, not by adoption.

Open-source U programs in the open partition of the Safebox are the system libraries — the equivalents of libc, the standard frameworks, the middleware. They are auditable by anyone, signable by anyone, improvable by anyone. Closed-source programs in the sealed partition are the applications — proprietary, protected, but analyzable by the pinned LLM inside the box. Both partitions speak U. Both are subject to the same capability verification. Both carry .u.meta proofs.

The LLM runner itself is written in U. The webserver is written in U. The module loader is written in U. You can analyze the system with the system. You can verify the analyzer. You can sign the verifier. It's turtles some of the way down — and then it's attested hardware.

An operating system is defined by the language it thinks in. Safebox thinks in U — a language where every program carries a machine-verifiable proof of what it can do. Trust the proof. — The simple version