Ironwall

Ironwall Motivation

Motivation: Why a New Programming Language Is Still Needed

This summary is not merely about which syntax style I prefer, nor is it based on likes or dislikes toward any particular language community. Its purpose is to explain why a new language is still needed.

What I care about is this: which historical lessons existing languages have failed to absorb, where history has regressed, and which design mistakes a language aimed at large-scale engineering, static analysis, systems development, and security should avoid.

My core standards are:

  • whether it can prevent RCE;
  • whether it can avoid null pointer dereference and segmentation faults;
  • whether it can compile to native code;
  • whether it can be used to write operating systems, runtimes, compilers, and other low-level systems;
  • whether static analysis tools can understand it reliably and at low cost;
  • whether it can support IDEs, refactoring, rename, jump to definition, and find usages in large projects;
  • whether it has clear type boundaries;
  • whether it avoids introducing syntax sugar, backdoors, macros, type inference, and overly complex syntax rules merely for "convenience."

What I oppose is not high-level languages, nor abstraction itself, but language designs that sacrifice engineering controllability, static analyzability, and long-term maintainability just to save a few keystrokes, look flexible, or make syntax shorter.


Problems with Rust

Rust's biggest problem is that it builds memory safety on the myth of static checking.

Rust claims that ownership, the borrow checker, and lifetimes can bring memory safety, but this fundamentally relies on static analysis. Static analysis cannot predict every future execution path of a program, nor can it fully understand all runtime states. Forcing runtime memory safety problems into compile time is, at its root, an unreliable path.

Rust's actual safety gains are far lower than advertised, while its language complexity is extremely high. Mechanisms such as ownership, lifetimes, Pin, async, traits, Send/Sync, and interior mutability make code very hard to read, write, and maintain. Much of the time, one is not writing business logic, but litigating against the compiler.

Another problem with Rust is that, to satisfy the borrow checker, programmers are often forced to change otherwise natural data structures and API designs. This is not safety; it is pushing the limitations of language mechanisms onto engineers. Large combinations of Arc, Mutex, RefCell, Rc, and Box ultimately do not achieve much more than disciplined C++ smart pointer patterns, yet they impose a massive complexity cost.

1. The Fundamental Limits of Static Checking

Rust's memory safety relies on the borrow checker and ownership system, but this is still static analysis in essence.

Static analysis cannot predict all future execution paths of a program, and therefore cannot truly guarantee memory safety. Even without unsafe, safe Rust still cannot provide a sufficiently strong engineering guarantee of memory safety. Dependency chains, the standard library, the compiler, abstraction boundaries, and implementation details may all allow double free, use-after-free, or other memory errors to reappear.

The fundamental problem is this: building memory safety on a compile-time model is itself a fantasy.

2. Limited Real-World Safety Gains

The safety gains of Rust's safe code in real large-scale engineering are severely exaggerated. In the end, it is closer to a complicated C++ smart pointer discipline than to a thoroughly reliable memory safety solution.

Rust is advertised as being able to substantially solve memory safety problems, but in practice it brings a high-cost, limited-probability safety improvement. That gain does not match the language complexity it introduces.

3. Poor Language Design

To enforce ownership rules and lifetimes, Rust greatly increases code complexity.

When async, trait objects, Pin, GAT, HRTB, interior mutability, and similar mechanisms are combined, they seriously reduce code readability and maintainability. Many API designs become awkward, and many simple tasks become "compiler puzzles."

More seriously, code complexity itself introduces security risks. Making code hard to understand in pursuit of safety can ultimately have a negative effect on safety.

4. Excessive Cognitive and Engineering Cost

Rust has long compile times, serious abstraction leakage, obvious trait/lifetime hell, and substantially reduced development efficiency.

This is not simply a "steep learning curve." The core route is wrong: it trades extremely high language complexity for limited and exaggerated safety gains. From an engineering cost-benefit perspective, this is inefficient.

5. Comparison with C/C++ and GC-Based Approaches

C/C++ rely on engineers to manage memory manually, and decades of painful RCE history have already proven this to be a disaster.

Rust tries to replace manual engineer reasoning with static analysis, but the effect remains limited and cannot reach truly reliable memory safety. The only truly reliable route to memory safety is garbage collection, not forcibly moving runtime problems into compile time.

Rust's safety myth has been over-promoted. It trades complexity for a limited-probability memory safety improvement, and its safety promise is unreliable in essence.


Problems with Go

Go's problem is that, despite having GC, it still preserves a large amount of C-like pointer semantics and nil problems.

GC solves lifetime problems such as double free and use-after-free, but Go still allows nil pointer dereference. This is a serious design flaw in a modern language. Since the runtime has already taken over memory lifetime, continuing to allow nil references everywhere at the language-design level is only half-baked safety.

Another serious problem with Go is type aliasing. Designs such as type A = string destroy type boundaries. Programmers think they have created a domain type, but what the compiler sees is still just the primitive type. This causes the static type system to lose its ability to prevent accidental mixing. Type aliasing is an extremely serious design regression. It greatly increases type-system complexity and also makes static analysis harder.

Go's interface design is likewise an extremely serious design regression. Go interfaces are not implemented explicitly, but matched structurally: as long as a type has certain methods, it is automatically regarded as implementing a certain interface. This design appears flexible on the surface, but in practice it introduces structural typing into the core of the language.

The problem with structural typing is that it weakens type identity and semantic boundaries. Whether two types can substitute for one another no longer depends on an explicit nominal declaration, but on whether they currently "happen to look the same." This makes API contracts implicit and makes it hard for IDEs, refactoring tools, and static analysis tools to determine stably whether a type intentionally implements an interface or merely happens to have a matching method set.

When type aliases and structural interfaces are combined, Go's type judgments become extremely unreliable. Aliases destroy type identity, while interfaces bypass explicit implementation relationships through structural matching. When the two are stacked together, the analyzability of the type system drops sharply.

This seriously conflicts with Go's advertised "simplicity." Go's syntax looks simple, but inside its type system it introduces structural typing mechanisms that are very harmful to large-scale engineering analysis.

Go's advantage is that it is safer than C, but it is still several serious defects away from an ideal engineering language.


Problems with Python

Python is only suitable for small programs, scripts, and quick prototypes. Writing one hundred lines of code is fine; once it enters large-scale engineering, dynamic typing and excessive flexibility explode directly.

Python's core problem is that its semantics are too open. Mechanisms such as monkey patching, dynamic attributes, metaclasses, decorators, dynamic import, eval, getattr, and setattr make programs difficult for static tools to understand. IDEs have a hard time reliably knowing what type a variable is, whether a method exists, or whether an attribute will be injected at runtime.

This means Python has difficulty supporting reliable refactoring, rename, call graph analysis, dead code analysis, and large-codebase navigation.

Another serious problem with Python is indentation syntax. Requiring programmers to distinguish tabs and spaces with their eyes is a hard flaw in syntax design. For small scripts this is not a big issue, but for large multi-person collaborative projects, this design is very fragile.

Python's "flexibility" is, in essence, a sacrifice of static analyzability. It is convenient for small programs and disastrous for large projects.


Problems with C / C++

The biggest problem with C/C++ is that manual memory management has already been proven by history to fail in large-scale engineering. On the most core issue, preventing RCE, C/C++ perform far too poorly.

For decades, large numbers of RCE, use-after-free, double free, buffer overflow, type confusion, iterator invalidation, and similar vulnerabilities have come from the C/C++ memory model. This is not because engineers are careless, but because human beings fundamentally cannot reliably manage complex memory ownership manually over the long term in large systems.

The most typical real-world example is browser JavaScript engines, especially V8. Chrome / V8 is already one of the projects in the world with the highest security investment, the most advanced testing tools, and the densest fuzzing. Even today, the entire industry must still expect that V8, written in C++, will continue to produce high-risk vulnerabilities such as type confusion, out-of-bounds write, use-after-free, and heap corruption. Once exploited by attackers, such vulnerabilities may lead to arbitrary code execution inside the browser sandbox, and may even combine with sandbox escapes to cause larger disasters.

C++'s problem is more serious than C's, because it is not only unsafe but also extremely complex. Mechanisms such as the C++ parser, templates, macros, SFINAE, operator overloading, multiple inheritance, implicit conversion, and UB almost block static analysis tools at the door. C++'s language surface area and semantic complexity have already exceeded the scope of normal engineering control.

But C still has a reason to exist. At the lowest level, there must be a bare language for bootloaders, kernel bootstrap, allocators, GC runtimes, VM startup, compiler backends, hardware register operations, and similar scenarios. The runtime of a safe language also needs to be started by some kind of bare language.

Therefore, C should not be completely eliminated, but should be restricted to a very small low-level trusted area. The bare-language layer must be extremely minimal, because it works in an unprotected environment. Once it becomes complex, failures are inevitable.


Problems with Scheme

Scheme's design is very unsuitable for engineering practice.

Many people think that because Scheme is based on S-expressions, its syntax is simple, its parser is easy to write, and static analysis is easy. This is actually a major misunderstanding. Scheme appears to have only parentheses on the surface, but the real lexer, reader, parser, and semantic analysis are not simple at all, and implementation is very difficult.

First, Scheme's token rules and identifier rules are too free. It allows variable names to use many symbols, and many symbols may be either part of an ordinary identifier or involved in reader syntax such as numbers, quote, quasiquote, unquote, vectors, bytevectors, characters, strings, and datum comments. As a result, the lexer is not clean, token boundaries are not intuitive, and syntax highlighting, error positioning, and IDE analysis all become difficult. This freedom provides almost no substantive help to expressive power, but greatly increases tool complexity.

Second, Scheme's S-expressions only unify surface shape; they do not truly unify semantics. if, lambda, define, let, let*, letrec, set!, begin, quote, quasiquote, syntax-rules, call/cc, and dynamic-wind are not ordinary function calls, but special forms or syntax with special evaluation rules. In other words, the parser sees lists, but semantic analysis must then determine entirely different rules based on the first symbol.

This means S-expressions do not bring the imagined benefit of being "easy to analyze." They merely turn the surface syntax into a uniform parenthesized form, while the truly difficult parts are moved into the reader, macro expansion, special-form analysis, binding resolution, and evaluation rules. For IDEs and static analysis tools, these are the genuinely difficult parts.

Second, Scheme has too many special forms. A large number of special syntax rules do not bring proportional expressive power, but instead increase the implementation cost of the language and compiler.

Third, Scheme is dynamically typed. This is a hard flaw for large-scale engineering. Runtime type errors, lack of static contracts, and inability to refactor reliably all limit its engineering scale.

Fourth, macros are a major failure. Macros destroy the static analyzability of a language by rewriting code shape at compile time, making tools, debugging, reading, and refactoring all difficult.

Finally, features such as call/cc and dynamic-wind have very low value in real engineering, yet they seriously increase compiler and runtime complexity.


Problems with OCaml / Standard ML

The problem with OCaml and Standard ML is that they pay too high a cost for Hindley-Milner type inference.

Type inference looks like it saves a few type annotations, but what large-scale engineering truly needs is explicit type boundaries. Explicit types are not redundancy; they are documentation, contracts, API boundaries, and refactoring anchors.

HM type inference turns type information into implicit global constraints. This often forces programmers to guess how the compiler will infer types, and it also makes it hard for IDEs to build reliable semantic models. Mechanisms such as value restriction, weak polymorphism, module systems, functors, GADTs, and first-class modules make the languages increasingly complex.

OCaml's syntax and feature stacking are especially serious. In many cases, even reliable rename refactoring, jump to definition, and find usages are hard to implement well.

This is why OCaml / Standard ML still have a strong "developing with a notepad" feeling. It is not because tool authors are not trying hard enough, but because the languages themselves are not tool-friendly enough.

They have GC, algebraic data types, and pattern matching, which are progress. But their excessive reliance on type inference, complex module systems, too many syntax features, and difficult tool support make it hard for them to become truly suitable languages for large-scale engineering.


Problems with C#

C# is a major improvement over C++, but many of C#'s changes relative to Java are regressions.

One major regression in C# relative to Java is type aliasing. Type aliases weaken type boundaries, increase the difficulty of static analysis, and damage the engineering value of nominal typing.

Second is the exception mechanism. Java's checked exceptions are imperfect, but at least they expose "this function may fail" in the API contract. C# removed checked exceptions, turning exceptions back into implicit control flow. Callers cannot know from the type signature what errors a function may throw, which is unfavorable for large-scale engineering.

Third is type inference. Designs such as var hide type boundaries just to save a few keystrokes. In large-scale engineering, type annotations are not noise, but an important part of readability and static analysis.

C# also has a typical problem: too many backdoors. unsafe, reflection, dynamic, nullable suppression, and similar mechanisms can all bypass safety mechanisms. This design philosophy is "make it convenient for programmers to bypass restrictions," but the cost is reduced static analyzability and safety.

A common problem in languages designed by Anders Hejlsberg is that they emphasize type systems and engineering capability on the surface, but always leave many escape hatches.


Problems with TypeScript

TypeScript's problem is that it has too many backdoors.

On the surface, TypeScript adds static types to JavaScript, but because it must remain compatible with JavaScript, it preserves a large number of escape hatches that break static analysis. any can directly disable the type system, as can force assertions, non-null assertions can bypass null checks, and mechanisms such as declaration merging, dynamic objects, and prototype mutation also break tool reliability.

TypeScript's type system is powerful, but it is not a hard constraint; it is more like an optional layer of static hints. Even with the strictest settings enabled, there are still many backdoors available. Engineers can bypass it at any time if they are willing.

This creates a serious problem: programmers think they are using a statically typed language, while in practice much of the safety still depends on discipline. This design, which "looks safe but can be bypassed everywhere," is dangerous.


Problems with Java

Java is one of the few languages that truly learned lessons from the engineering disasters of C/C++.

It removed macros, removed raw pointers, removed manual free, has no type aliases, binds filenames to public classes, keeps package/class structures stable, has clear nominal typing, and has very strong IDE and refactoring capabilities. Java's module and engineering structures have been validated over the long term by ultra-large codebases. Many later languages have failed to achieve this.

But Java also has serious flaws.

First, null pointer dereference is a hard flaw. Java has GC, but still allows references to be null, which means it has not truly solved the reference validity problem.

Second, Java's generics implementation is poor. Type erasure is a historical compatibility burden, leading to a series of problems involving generic arrays, raw types, wildcards, bridge methods, and more.

Third, early Java lacked expressive power. It had no higher-order functions, anonymous classes were too heavy, and functional abstraction was weak.

Fourth, Java was influenced by the OOP trend of its era and introduced inheritance as a core mechanism. Inheritance is a serious blow to the type system, because it creates fragile coupling between subclasses and parent classes, easily violates the Liskov Substitution Principle, and makes type boundaries unclean.

Fifth, Java depends on a VM. Many features must rely on the JVM runtime, so Java cannot serve as a truly native systems language and cannot be used to write operating systems or low-level runtimes.

Sixth, modern Java has also started adding some regressive mechanisms, such as local type inference. These designs weaken the original advantages of explicitness, stability, and tool analyzability for the sake of convenience.

So Java was a major step forward, but it is not the final answer. Its engineering analyzability is strong, but its syntax surface area is still too large, the barrier for static analysis is still too high, and it cannot handle low-level systems problems.

The Fundamental Problem with Type Aliases

The serious problem with type aliases is not merely that they make it easy to mix up UserId and OrderId; it is that they pull the type system back from nominal typing toward structural typing.

In large-scale engineering, the most important value of a type system is not saving a few keystrokes, but establishing stable, explicit, statically analyzable semantic boundaries. Concepts such as UserId, OrderId, Email, Token, FilePath, and SqlFragment should be different nominal types even if their underlying representation is the same. Their semantics differ, their security boundaries differ, and the operations allowed on them differ.

Type aliases destroy this boundary. They make it look as if the programmer is defining a new type, when in reality they are merely giving an old type another name. This is worse than directly using the primitive type, because it creates a false sense of type safety.

The deeper problem is that type aliases make type equivalence judgment complex. The advantage of nominal typing is clear type identity: to determine whether two types are the same, one only needs to look at their definition identity. But once aliases, structural equivalence, expansion, generics, module boundaries, and cross-package renaming are mixed together, type equivalence judgment becomes a complex reduction problem.

Structural typing itself easily introduces many static analysis difficulties. When "whether two types are the same" is no longer determined by explicit names, but depends on structure, expansion, compatibility, recursive definitions, generic instantiation, and contextual inference, type analysis quickly approaches uncontrollability. Merely judging type equivalence and substitutability may become unstable, unintuitive, and even close to undecidable under certain combinations of features.

Therefore, type aliasing is not a small convenience, but a design that corrodes the analyzability of the entire type system. It punches through the engineering value of nominal typing and causes static analysis tools to lose their stable foundation. For a large-scale engineering language, this is a serious regression.

The correct design should be: if a new domain concept needs to be expressed, it must be defined as a real new type, not an alias. The first responsibility of a type system is to protect semantic boundaries, not to provide name abbreviations.

Why Fancy Type Systems Inevitably Fail

All kinds of fancy type systems are ultimately bound to fail for two fundamental reasons: first, type systems are extremely sensitive to features and can easily become undecidable everywhere; second, static analysis cannot predict the future.

A type system is not a syntactic decoration that can freely accumulate features. Every added feature may change the decidability, analyzability, and tool implementability of the entire type system.

A feature that looks small in isolation, such as type aliasing, structural typing, subtyping, variance, implicit conversion, type inference, macros, higher-kinded types, conditional types, GADTs, or dependent-like features, may interact with other features in complex ways once placed into the whole language.

The issue is not whether a feature seems reasonable in isolation, but whether, when combined with the entire language system, it makes type checking, type equivalence judgment, symbol resolution, refactoring, IDE indexing, and static analysis uncontrollable.

The most fragile part of a type system is that many problems do not become harder linearly; they explode instantly. Once the wrong combination of features is introduced, undecidability or near-undecidability can appear everywhere. Even when something is theoretically decidable, it may be unusable in engineering because the cost is too high.

For example, type aliases punch through nominal typing and cause the type system to regress toward structural typing. The advantage of nominal typing is clear type identity: to determine whether two types are the same, look at their definition identity. But structural typing turns type equivalence into problems of structure expansion, recursive comparison, generic instantiation, contextual compatibility, and more. Merely judging type equivalence may become unreliable, and static analysis may approach uselessness.

The problem with OCaml / Standard ML is that Hindley-Milner type inference turns type information into implicit global constraints. Programmers write a few fewer type annotations, but IDEs and static analysis tools struggle to build stable semantic models. Once value restriction, module systems, functors, GADTs, first-class modules, and similar mechanisms are added, tool support becomes very difficult.

Scala and TypeScript are another kind of failure. They mix large amounts of higher-order type functionality, inference, structural types, implicit mechanisms, conditional types, backdoors, and syntax sugar, and in the end the type system becomes black magic. Programmers think they have gained stronger type capabilities, but what they actually get is an engineering disaster that is harder to predict, harder to analyze, and harder to refactor.

Rust has the same class of problem. It combines ownership, lifetimes, trait resolution, async, Pin, Send/Sync, and other mechanisms, trying to use a static system to solve runtime memory safety problems. But static analysis cannot predict all future execution paths, nor can it fully understand runtime state. Therefore, this design inevitably leads to a complexity explosion.

There is a more fundamental limit here: static analysis can never fully predict the future. A program's real execution paths, data flow, concurrency timing, external inputs, IO behavior, runtime state, GC triggering, and hardware behavior all happen in the future. What compile time can see is only a model, not complete reality.

Therefore, stuffing more and more safety, correctness, resource management, control flow, and semantic constraints into the type system is the wrong direction. This turns the type system into a huge and fragile static reasoning machine, which ultimately can neither truly guarantee runtime safety nor preserve engineering analyzability.

What a type system should really do is limited: provide clear nominal type boundaries, prevent obvious type mixing, make API contracts visible, and allow IDEs and static analysis tools to understand programs stably. It should not become a playground for language designers to show off, nor should it take on responsibilities that should be handled by the runtime, GC, module system, tests, sandboxing, and permission model.

Therefore, the type system of an engineering language should be conservative, explicit, stable, low in syntax surface area, and easy to analyze. It should reject fancy type systems.


What Kind of Language I Want

The language I want should absorb historical lessons and explicitly address the most serious hard flaws in existing languages.

It should be able to thoroughly prevent RCE, get rid of null pointer dereference and segmentation fault problems, compile to native code, write operating systems, have high analyzability with low analysis cost, and still have sufficient expressive power.

It should have the following characteristics.

0. The Core Goal: Prevent RCE

The core goal of a language is not pretty syntax, fewer keystrokes, or convenience for small programs. It must prevent RCE.

If a language fails on the core issue of preventing RCE, its value in systems engineering and security engineering drops sharply.

1. GC, but GC Behavior Should Be as Analyzable as Possible

GC is the correct correction to manual memory management in C/C++, but the problem with modern GC is that its triggering mechanism is too implicit.

From the perspective of static analysis, if GC can be triggered anywhere, then it is as hard to analyze as dynamic typing. An ideal language should make a clearer design tradeoff between memory safety and analyzability.

2. No Null Pointer Dereference

Reference validity must be part of the core language design.

A language must not do what Java, Go, Kotlin, and Swift do: allow null or nil to exist long-term, then rely on runtime panics or exceptions as a remedy.

3. Explicit Nominal Typing

No type aliases.

Domain concepts must become real new types, not another name for an old type. The first value of a type system is to establish statically analyzable semantic boundaries.

4. Higher-Order Functions and Simple Generics

The language needs enough expressive power, so it should support higher-order functions and simple, analyzable generics.

But generics must not become complex monsters like C++ templates, Scala's type system, or TypeScript conditional types. Generics should serve engineering abstraction, not turn the type system into a research project.

5. Reject Macros and Syntax Rewriting

Macros destroy static analyzability, tool support, and code readability.

A large-scale engineering language should not allow programs to arbitrarily rewrite the language itself.

6. Eliminate Type Inference

Type annotations are not noise; they are engineering documentation and API contracts.

Sacrificing static analyzability just to save a few keystrokes is the wrong direction.

7. The Module System Should Be Engineering-Verified Like Java's

There should be stable relationships among namespaces, files, types, packages, and modules.

A module system is first an engineering structure, not a syntax toy.

8. IDE and Refactoring Must Be First-Class Design Goals

If a modern engineering language cannot support reliable rename, jump to definition, find usages, and extract method, then it is not suitable for large-scale engineering.

A language is not only for compilers; it is also for IDEs, static analysis tools, and long-term maintainers.

9. Syntax Surface Area Must Be Small

More language features are not necessarily better.

Type systems and syntax rules are extremely sensitive to static analysis. A small feature may cause tool complexity to explode. An engineering language should be conservative, stable, and simple.

10. A Low-Level Bare Language Must Exist, but It Must Be Minimal

Operating systems, runtime startup, GC startup, allocators, bootloaders, and similar scenarios still need a bare language.

But this layer must be very small and very simple. It must not become complicated like C++. A bare language works in an unprotected environment and requires highly precise safe operations. Once the language itself becomes complex, problems are inevitable.


Summary

What I want is not a more "flexible" language, nor a language with shorter syntax, but a large-scale engineering language that is:

explicit, analyzable, refactorable, low in syntax surface area, strongly constrained for engineering, equipped with GC without implicit loss of control, without null, without macros, without type aliases, without a pile of backdoors, able to compile to native code, able to write operating systems, and able to truly prevent RCE.