Quartz Language Ergonomics Audit
Date: March 2, 2026 Version: v5.25.0-alpha Scope: Full language audit — syntax, semantics, API cohesion, table stakes, dogfooding, freeze readiness
Table of Contents
- Executive Summary
- Documentation vs Reality
- Syntax & Ergonomic Awkwardness
- API Unification Issues
- Table Stakes Gap Analysis
- Dogfooding Opportunities
- Missing Language Constructs
- Before & After Vision
- Prioritized Recommendations
1. Executive Summary
Quartz is remarkably feature-complete for an alpha-stage systems language. The feature set is competitive with — and in several areas exceeds — Swift, Go, Rust, Zig, and Nim. Union types, intersection types, record types, UFCS, placement allocation, list/map comprehensions, symbols, pipeline operators, and postfix guards are genuine differentiators that no single competitor matches.
However, the language has significant ergonomic debt accumulated during rapid development. The gap isn’t in what Quartz can do, but in how it feels to do it. The core issues fall into four categories:
- The
as_int()/as_string()tax — The existential i64 model forces unsafe type-erasure casts throughout real code. 266 occurrences in the compiler alone. - API fragmentation — Every operation exists in 2-3 calling conventions (free function, UFCS method, intrinsic name) with no clear canonical form.
- Bool/Int conflation — Predicates return
Int(0/1) instead ofBool, forcing== 1checks everywhere. - Missing syntactic sugar for common patterns — Character literals, negative const literals, struct default values,
Vec.clone(), iterator protocol.
The language is not ready for freeze. The issues identified here are not nice-to-haves — they are the kind of paper cuts that, once frozen, become permanent warts that every user encounters daily.
2. Documentation vs Reality
Confirmed Working (19/20 features verified)
| Feature | Status | Notes |
|---|---|---|
| Negative integer literals | YES | Parser handles unary minus. -1, -42 work in expressions |
String == content comparison | YES | MIR detects string operands, emits str_eq intrinsic |
loop keyword | YES | Desugars to while true |
for item in vec | YES | Works for ranges, arrays, vecs, custom iterators |
try/catch syntax | YES | Full pipeline: parser, MIR, codegen |
| Operator overloading | YES | Via op_add, op_eq, op_lt etc. in extend blocks |
| Labeled loops | YES | Uses :label syntax (NOT @label) |
| List comprehensions | YES | Transform + optional filter |
unless | YES | Statement + postfix guard forms |
| Custom iterators | YES | StructName$next() protocol returning Option |
select (channels) | YES | recv/send/default arms |
parallel_map/for/reduce | YES | Thread pool intrinsics |
@heap/@arena placement | YES | Parser, MIR, codegen all implemented |
await | YES | Thread join semantics (pthread_join) |
task_group | YES | Structured concurrency with cancel support |
| Enum exhaustiveness | YES | Enforced in typechecker |
Doc comments ## | YES | Three forms: ##, ##!, ##^ |
const def / const eval | YES | Arithmetic, functions, loops, static_assert |
| Pipe lambda ` | x | ` removed |
Confirmed NOT Working
| Feature | Status | Notes |
|---|---|---|
? operator | REMOVED | Deliberately removed v5.12.27, replaced by $try() macro |
Documentation Errors Found
| Document | Claim | Reality |
|---|---|---|
| QUARTZ_REFERENCE.md | Shows pipe lambda syntax |x| expr as “legacy” | Pipe lambdas are removed, not legacy. Doc still shows examples. |
| QUARTZ_REFERENCE.md | Says newtypes are “planned, currently transparent” | Newtype enforcement is fully implemented (S2.4 complete). |
| QUARTZ_REFERENCE.md | Shows ? operator for error propagation | Removed in v5.12.27. Only $try() exists. |
| QUARTZ_REFERENCE.md | Lists labeled loops as @outer | Actual syntax is :outer (symbol syntax). |
| QUARTZ_REFERENCE.md | ”Dot access is same-line only” in Common Mistakes | Contradicted by Style Guide which encourages leading-dot chaining. |
| QUARTZ_REFERENCE.md | ”Strings are Int handles. Compare to 0, not ""” | String == now does content comparison. This note is stale. |
| ARCHITECTURE.md | References “v1.0.0”, “730 tests”, C bootstrap | Entire doc is stale. v5.25.0, ~3000+ tests, C bootstrap retired. |
| GRAMMAR.md | Has let_stmt production | CLAUDE.md anti-hallucination table says let doesn’t exist. |
| GRAMMAR.md | Uses fn keyword in extern_decl | All other functions use def. Inconsistency. |
| GRAMMAR.md | Shows do -> params | Actual syntax is do params -> (params before arrow). |
| STYLE.md | Says “Arrow syntax is the only syntax” | True, but REFERENCE.md still documents pipe syntax. |
| INTRINSICS.md | vec_new returns Int, vec_push(vec: Int, val: Int) | Parameterized: vec_new<Int>(), v.push(42) in practice. |
| INTRINSICS.md | Shows hashmap_del | Reference doc uses hashmap_delete and .delete(). |
Verdict: The documentation has significant staleness. At least 13 material errors where docs describe removed features, wrong syntax, or outdated behavior. A documentation pass is required before freeze.
3. Syntax & Ergonomic Awkwardness
3.1. The 0 - 1 Problem
75 occurrences of 0 - 1 across 21 compiler source files, despite negative literals working via unary minus.
# Current (compiler source, stdlib)
var eq_pos = 0 - 1
var found_idx = 0 - 1
return 0 - 1
_head: 0 - 1,
_tail: 0 - 1,
# Should be
var eq_pos = -1
var found_idx = -1
return -1
_head: -1,
_tail: -1,
Root cause: Negative literals work in expressions but likely don’t work in struct field initializers or const declarations. The stdlib predates the unary minus fix. This needs a cleanup pass.
3.2. Magic Number Character Constants
The stdlib and compiler are littered with raw ASCII codes where character literals should be used:
# Current (std/string.qz)
if c >= 97 and c <= 122 # lowercase a-z
sb.append_char(c - 32) # to uppercase
if c == 32 or c == 9 or c == 10 or c == 13 # space, tab, newline, CR
# Current (std/json.qz — hundreds of instances)
if c == 123 # '{'
if c == 125 # '}'
if c == 91 # '['
if c == 34 # '"'
if c == 92 # '\'
# Current (backend/mir.qz)
if str_char_at(name, 0) == 38 and str_char_at(name, 1) == 109 # "&m"
# Should be (if character literals exist)
if c >= 'a' and c <= 'z'
sb.append_char(c - 32)
if c == ' ' or c == '\t' or c == '\n' or c == '\r'
if c == '{'
if c == '}'
Status: There are char_literal_spec.qz and char_predicates_spec.qz test files, suggesting character literal support exists. But the stdlib never uses it. Either the feature has limitations, or the stdlib predates it and needs migration.
3.3. String Concatenation Chains
299 .concat() chains in the compiler despite string interpolation existing:
# Current (Quakefile.qz)
var cmd = compiler.concat(" --no-opt --no-cache").concat(debug_flag).concat(" ").concat(include_flags).concat(" ").concat(source).concat(" > ").concat(ir_path).concat(" 2>tmp/build.err")
# Current (quartz.qz)
eputs("[cache] Tier 2: skipping TC+MIR for ".concat(str_from_int(skip_count)).concat("/").concat(str_from_int(total_mods)).concat(" modules\n"))
# Should be
var cmd = "#{compiler} --no-opt --no-cache#{debug_flag} #{include_flags} #{source} > #{ir_path} 2>tmp/build.err"
eputs("[cache] Tier 2: skipping TC+MIR for #{skip_count}/#{total_mods} modules\n")
Root cause: String interpolation may have limitations with method calls or long expressions inside #{}. Or the code predates interpolation. Either way, this is the single highest-frequency ergonomic issue in the codebase.
3.4. == 1 / == 0 for Boolean Checks
Pervasive. Functions that should return Bool return Int:
# Current
if file_exists(path) == 1
if str_eq(a, b) == 0
if vec_contains(v, x) == 1
if hashmap_has(m, key) == 1
if entry_name.contains("$") == true # inconsistent: some use == true
# Should be
if file_exists(path)
if a != b
if vec_contains(v, x)
if hashmap_has(m, key)
if entry_name.contains("$")
Root cause: Builtins and intrinsics were defined before Bool was a proper type. The runtime treats 0/false/nil as falsy, so if file_exists(path) should work if the function returns any truthy value. But returning Int means the function signature lies about its type.
3.5. StringBuilder Lifecycle Boilerplate
308 sb_new/sb_append/sb_free patterns across the compiler:
# Current (everywhere)
var sb = sb_new()
defer sb_free(sb)
sb.append("prefix")
sb.append(some_string)
sb.append_int(some_number)
return sb.to_string()
# If StringBuilder implemented Drop:
var sb = sb_new()
sb.append("prefix")
sb.append(some_string)
sb.append_int(some_number)
return sb.to_string()
# sb automatically freed when scope ends
The Drop trait exists and is enforced. StringBuilder should implement it.
3.6. Struct Constructor Verbosity
# Current (typecheck.qz — TcRegistry has 45 fields)
var reg = typecheck_util$TcRegistry {
struct_names: vec_new<String>(),
struct_field_names: vec_new<Int>(),
struct_field_types: vec_new<Int>(),
struct_field_counts: vec_new<Int>(),
struct_node_ids: vec_new<Int>(),
enum_names: vec_new<String>(),
enum_variant_names: vec_new<Int>(),
enum_variant_types: vec_new<Int>(),
enum_variant_counts: vec_new<Int>(),
# ... 36 more fields, all vec_new<T>() ...
}
# With default field values:
struct TcRegistry
struct_names: Vec<String> = vec_new<String>()
struct_field_names: Vec<Int> = vec_new<Int>()
# ...
end
var reg = TcRegistry {} # all defaults
3.7. The extern fn vs def Inconsistency
The grammar uses fn for extern declarations but def everywhere else:
extern "C" fn puts(s: String): Int # uses fn
def my_function(): Int = 42 # uses def
This is the only place fn appears in the language. The anti-hallucination table says “Use def, not fn.” Should be extern "C" def for consistency.
4. API Unification Issues
4.1. Three Calling Conventions, No Canonical Form
Every operation exists in multiple styles with no clear winner:
| Operation | Intrinsic | UFCS Method | Free Function |
|---|---|---|---|
| String length | str_len(s) | s.size | — |
| String char at | str_char_at(s, i) | s.char_at(i) | — |
| String equality | str_eq(a, b) | a.eq(b) | a == b |
| String concat | str_concat(a, b) | a.concat(b) | a + b |
| Vec length | vec_len(v) | v.size | — |
| Vec push | vec_push(v, x) | v.push(x) | — |
| Vec get | vec_get(v, i) | — | v[i] |
| HashMap has | hashmap_has(m, k) | — | — |
| HashMap delete | hashmap_del(m, k) | m.delete(k) | — |
The problem: Code mixes all three styles freely, even within the same file. The stdlib itself is inconsistent:
# In std/string.qz (same function):
var len = s.size # UFCS
while start < len and is_whitespace(str_char_at(s, start)) # intrinsic
Recommendation: Establish canonical UFCS as the primary API. Intrinsic names should be considered internal. The stdlib should exclusively use method-style calls.
4.2. impl vs extend Duplication
Every trait implementation requires both an impl block AND an extend block:
# Current (std/error.qz)
impl Error for SimpleError
def message(self): String = @_message
def kind(self): String = "Error"
end
extend SimpleError {
def message(): String = @_message # DUPLICATE
def kind(): String = "Error" # DUPLICATE
}
This is because impl provides trait dispatch and extend provides UFCS method calls, and they are separate mechanisms. Every method is written twice. This affects error.qz, csv.qz, json.qz, prelude.qz, and any file that uses both traits and methods.
Recommendation: impl should automatically make methods available via UFCS. The extend block should only be needed for methods that aren’t part of a trait.
4.3. Collection Return Types: Int vs Bool vs Option
| Function | Returns | Should Return |
|---|---|---|
vec_contains(v, x) | Int (0/1) | Bool |
str_eq(a, b) | Int (0/1) | Bool |
file_exists(path) | Int (0/1) | Bool |
hashmap_has(m, key) | Int (0/1) | Bool |
str_find(s, needle) | Int (-1 if not found) | Option<Int> |
stack.pop() | Int (0 if empty) | Option<Int> |
queue.pop() | Int (0 if empty) | Option<Int> |
deque.pop_front() | Int (0 if empty) | Option<Int> |
regex =~? s | Vec<String> or 0 | Option<Vec<String>> |
The pop() → 0 problem is unsound: If 0 is a valid value in the collection, you cannot distinguish “the value was 0” from “the collection was empty.”
4.4. Naming Inconsistencies
| Pattern | Examples | Issue |
|---|---|---|
str_* prefix | str_split, str_join, str_trim | Inconsistent with UFCS .split(), .join(), .trim() |
vec_* prefix | vec_push, vec_get, vec_len | Inconsistent with UFCS .push(), v[i], .size |
hashmap_del vs hashmap_delete | Both used | Pick one |
sb_new/sb_free/sb_append | StringBuilder | Why sb_ not string_builder_? |
str_len vs .size | String length | Two names for same operation |
vec_len vs .size | Vec length | Two names for same operation |
print vs puts | String output | print = no newline, puts = newline. print_int = with newline. Inconsistent. |
eputs vs eprint | Stderr output | Mirror of stdout inconsistency |
4.5. Higher-Order Function Copy-Paste
All 5 collection types (Stack, Queue, Deque, LinkedList, PriorityQueue) have identical implementations of each, map, filter, reduce, find, any, all, count. That’s ~640 lines of copy-pasted code.
# Stack.each — identical to Queue.each, Deque.each, LinkedList.each, PriorityQueue.each
def each(f: Fn(Int): Void): Void
var i = 0
while i < @_data.size
f(@_data[i])
i = i + 1
end
end
Root cause: No iterator protocol or trait default methods that collections could share.
5. Table Stakes Gap Analysis
5.1. Features Quartz HAS That Competitors Don’t
| Feature | Swift | Go | Rust | Zig | Nim | Quartz |
|---|---|---|---|---|---|---|
Union types (Int | String) | - | - | - | - | - | YES |
Intersection types (Eq & Show) | - | - | - | - | - | YES |
Record types ({ name: String }) | - | - | - | - | - | YES |
Symbols (:ok, :error) | - | - | - | - | - | YES |
| UFCS | - | - | - | - | Nim | YES |
Placement allocation (@heap, @arena) | - | - | - | - | - | YES |
Pipeline operator (|>) | - | - | - | - | Nim | YES |
Postfix guards (return x if cond) | - | - | - | - | - | YES |
| List/map comprehensions | - | - | - | - | Nim | YES |
Predicate naming (.empty?, .some?) | - | - | - | - | - | YES |
unless keyword | - | - | - | - | - | YES |
Labeled loops with :symbol | - | - | - | - | - | YES |
5.2. Features Competitors Have That Quartz Is MISSING
Tier 1: Pre-Freeze Critical
| Feature | Who Has It | Complexity | Impact |
|---|---|---|---|
| Operator overloading (trait-based) | Swift, Rust, Nim | MEDIUM | Currently have op_add etc. via extend. Need Add/Sub/Mul traits so operators dispatch through the trait system, not just hardcoded extend method names. |
| Index/subscript overloading | Swift, Rust, Nim | MEDIUM | Only Vec/HashMap support x[i]. Custom collections (Deque, Matrix) cannot participate. Need Index/IndexMut traits. |
| Destructuring in let bindings | Swift, Rust, Zig, Nim | MEDIUM | Currently only in match arms. var Point { x, y } = p would reduce boilerplate. |
Iterator protocol / Iterable trait | Swift, Rust, Nim | MEDIUM | Would eliminate 640 lines of copy-pasted HOF code across 5 collection types. |
zip / enumerate | Swift, Rust, Nim | LOW | Trivial additions to HOF set. |
| Default struct field values | Swift, Go, Rust (Default), Zig | LOW-MED | Would eliminate the 45-field constructor problem. |
Tier 2: Post-Freeze Important
| Feature | Who Has It | Complexity | Impact |
|---|---|---|---|
| Package manager | All 5 | HIGH | No ecosystem without it. Correctly deferred. |
| LSP | All 5 | HIGH | No good editor experience without it. Correctly deferred. |
| async/await | Swift, Rust, Zig, Nim | VERY HIGH | Thread-based concurrency works but doesn’t scale for I/O-bound workloads. |
| Associated types in traits | Swift, Rust | MEDIUM | Required for clean iterator/collection abstractions. |
dyn Trait dynamic dispatch | Rust | HIGH | Currently static dispatch only. |
| Full const generics | Rust, Zig | MEDIUM | Array<T,N> exists but not generalizable. |
Internal visibility (pub(crate)) | Swift, Go, Rust | LOW | Needed when packages exist. |
Tier 3: Not Recommended
| Feature | Rationale for Skipping |
|---|---|
| Higher-kinded types | No systems language has them. Academic. |
| Dependent types | Refinement types (R track) is the better path. |
| Runtime reflection | Contradicts existential type model. |
| Custom operators | Footgun. Operator overloading suffices. |
| Green threads | async/await is the better path. |
| Garbage collector | Ownership + arenas + Drop is the right model. |
6. Dogfooding Opportunities
6.1. Parallel Vectors → Structs
The compiler uses parallel vectors as its primary data structure because Vec<UserStruct> historically didn’t work with field access. This has been fixed (inline struct storage, Phase M.R.5).
# Current (build_cache.qz — 4 parallel vectors)
var old_dg_names = vec_new<String>()
var old_dg_content_hashes = vec_new<String>()
var old_dg_interface_hashes = vec_new<String>()
var old_dg_deps = vec_new<Int>()
# Then 75 lines later, looking up by name:
var i = 0
while i < old_dg_names.size
if old_dg_names[i].eq(mod_name)
old_hash = old_dg_content_hashes[i]
break
end
i = i + 1
end
# Dogfooded (with Vec<Struct> + for-in):
struct ModuleEntry
name: String
content_hash: String
interface_hash: String
deps: Vec<Int>
end
var modules = vec_new<ModuleEntry>()
for entry in modules
if entry.name == mod_name # string == works now
old_hash = entry.content_hash
break
end
end
Scope: 33 struct fields typed as bare Vec (no type param) across the compiler could be converted to Vec<Struct>.
6.2. as_int()/as_string() → Typed Containers
266 as_int()/as_string() calls across 19 files. Many of these are storing heterogeneous data in Vec<Int>:
# Current (quake.qz — task registry)
var t = vec_new<Int>()
t.push(as_int(name)) # String → Int
t.push(as_int(desc)) # String → Int
t.push(as_int(vec_new<Int>())) # Vec → Int
t.push(as_int(action)) # Fn → Int
_tasks.push(as_int(t)) # Vec → Int
# Dogfooded (with a struct):
struct QuakeTask
name: String
desc: String
deps: Vec<String>
action: Fn(): Void
end
var tasks = vec_new<QuakeTask>()
tasks.push(QuakeTask { name: name, desc: desc, deps: vec_new<String>(), action: action })
6.3. str_eq() → ==
257 .eq() calls for string comparison could be replaced with == now that content comparison works:
# Current
if entry_name.eq(sym)
if old_dg_names[i].eq(mod_name)
if str_eq(kind, "func") == 1
# Dogfooded
if entry_name == sym
if old_dg_names[i] == mod_name
if kind == "func"
6.4. .concat() → String Interpolation
299 .concat() chains could be replaced with #{} interpolation:
# Current
eputs("Error: ".concat(msg).concat(" at line ").concat(str_from_int(line)))
# Dogfooded
eputs("Error: #{msg} at line #{line}")
6.5. while i < ... → for i in 0..
94 instances of manual i = i + 1 loop increment could use for..in:
# Current
var i = 0
while i < names.size
process(names[i])
i = i + 1
end
# Dogfooded
for i in 0..names.size
process(names[i])
end
6.6. Enum Constants → Actual Enums
75+ const groups (NODE_INT_LIT = 0, NODE_BOOL_LIT = 1, …) used with integer comparison:
# Current
if kind == 34 # NODE_FUNCTION
if kind == 49 # NODE_GLOBAL_VAR
if tag == 2 # const decl
# Dogfooded
match kind
NodeKind::Function => ...
NodeKind::GlobalVar => ...
end
Caveat: This is a massive refactor. The NodeKind enum already exists but isn’t used at runtime due to performance concerns and the cascading if/elsif dispatch pattern. May not be worth the effort before freeze.
6.7. Drop Trait for StringBuilder
# Current (40 defer statements for sb_free)
var sb = sb_new()
defer sb_free(sb)
# ...
return sb.to_string()
# Dogfooded (with Drop)
# StringBuilder implements Drop, auto-freed at scope exit
var sb = sb_new()
# ...
return sb.to_string()
6.8. Dogfooding Score Card
| Opportunity | Occurrences | Effort | Risk | Priority |
|---|---|---|---|---|
.eq() → == for strings | 257 | Low | Low | P1 |
.concat() → interpolation | 299 | Low | Low | P1 |
0 - 1 → -1 | 75 | Low | None | P1 |
while i < n → for i in 0..n | 94 | Low | Low | P2 |
| Parallel vecs → Vec | 33 fields | Medium | Medium | P2 |
as_int()/as_string() → typed containers | 266 | High | Medium | P3 |
| Integer consts → enums | 75+ groups | Very High | High | Defer |
7. Missing Language Constructs
7.1. Iterator Protocol
The most impactful missing construct. Custom iterators exist ($next method) but there’s no Iterable trait that collections share. This causes:
- 640 lines of copy-pasted
each/map/filter/reduceacross 5 collection types - No generic “works on any collection” functions
for item in collectionworks for Vec but the iteration protocol can’t be shared
# What's needed
trait Iterable<T>
def next(self): Option<T>
end
# Then any type implementing Iterable gets map/filter/reduce for free
def map<T, U>(iter: Iterable<T>, f: Fn(T): U): Vec<U>
var result = vec_new<U>()
for item in iter
result.push(f(item))
end
return result
end
7.2. Trait Default Methods
Would allow the iterator protocol to provide default implementations:
trait Iterable<T>
def next(self): Option<T>
# Default implementation
def each(self, f: Fn(T): Void): Void
for item in self
f(item)
end
end
end
Status: The roadmap mentions “Phase 9: Trait Defaults” as completed, but it’s unclear if default methods actually work in practice. Need verification.
7.3. Default Struct Field Values
Would eliminate the 45-field constructor problem:
struct Config
host: String = "localhost"
port: Int = 8080
debug: Bool = false
max_connections: Int = 100
end
var cfg = Config { port: 3000 } # only override what you need
7.4. impl Unification with extend
Methods defined in impl blocks should be available via UFCS automatically:
# Current: must write both
impl Eq for Point
def eq(self, other: Self): Bool = self.x == other.x and self.y == other.y
end
extend Point {
def eq(other: Point): Bool = @x == other.x and @y == other.y # DUPLICATE
}
# Desired: impl is sufficient
impl Eq for Point
def eq(self, other: Self): Bool = self.x == other.x and self.y == other.y
end
# point.eq(other_point) works automatically via UFCS
7.5. Character Literals
# Current
if c == 123 # magic number, must look up ASCII table
if c == 10 # newline? maybe?
# With char literals
if c == '{'
if c == '\n'
Status: char_literal_spec.qz exists, suggesting partial support. Needs stdlib migration.
7.6. String.to_int() Parsing
Converting strings to integers requires to_i() which is documented but inconsistently available:
# What's needed
var n = "42".to_int() # returns Option<Int> or Result<Int, String>
var n = str_to_int("42") # free function form
7.7. Vec.clone() / Collection Cloning
A recurring pattern in the compiler — allocate a new vec, iterate, push each element:
# Current (appears dozens of times)
var result = vec_new<String>()
var i = 0
while i < source.size
result.push(source[i])
i = i + 1
end
# With clone
var result = source.clone()
7.8. Vec.sort()
No sorting function exists for Vec. The PriorityQueue provides heap ordering, but general-purpose sorting is missing.
7.9. Tuple Destructuring
# What's needed
var (x, y) = get_point()
var (key, value) = entry
for (i, item) in enumerate(vec)
8. Before & After Vision
8.1. Compiler Source: Module Loading
# ===== CURRENT (quartz.qz) =====
var old_hash = ""
var oi = 0
while oi < old_dg_names.size
if old_dg_names[oi].eq(mod_name)
old_hash = old_dg_content_hashes[oi]
break
end
oi = oi + 1
end
if old_hash.eq("") == false
if old_hash.eq(new_hash)
unchanged_count = unchanged_count + 1
end
end
# ===== AFTER =====
var old_hash = modules.find(m -> m.name == mod_name)
match old_hash
Some(m) if m.content_hash == new_hash => unchanged_count += 1
_ => ()
end
8.2. Stdlib: String Operations
# ===== CURRENT (std/string.qz) =====
def str_to_upper(s: String): String
var sb = sb_new()
defer sb_free(sb)
var i = 0
while i < s.size
var c = str_char_at(s, i)
if c >= 97 and c <= 122
sb.append_char(c - 32)
else
sb.append_char(c)
end
i = i + 1
end
return sb.to_string()
end
# ===== AFTER =====
def str_to_upper(s: String): String
var sb = sb_new() # Drop auto-frees
for c in s.chars()
if c >= 'a' and c <= 'z'
sb.append_char(c - 32)
else
sb.append_char(c)
end
end
return sb.to_string()
end
8.3. Stdlib: Error Type Definition
# ===== CURRENT (std/error.qz) =====
struct SimpleError
_message: String
end
def simple_error(msg: String): SimpleError
return SimpleError { _message: msg }
end
impl Error for SimpleError
def message(self): String = @_message
def kind(self): String = "Error"
end
extend SimpleError {
def message(): String = @_message
def kind(): String = "Error"
}
# ===== AFTER (impl auto-provides UFCS) =====
struct SimpleError
_message: String
end
def simple_error(msg: String): SimpleError = SimpleError { _message: msg }
impl Error for SimpleError
def message(self): String = @_message
def kind(self): String = "Error"
end
# SimpleError.message() automatically available via UFCS
8.4. QSpec Test: Output Testing
# ===== CURRENT =====
it("vec_reverse reverses a vector") do ->
assert_output("3\n2\n1\n", co_reverse_test)
end
# ... 50 lines later at module level:
def co_reverse_test(): Void
var v = [1, 2, 3]
var r = vec_reverse(v)
for i in 0..r.size
print_int(r[i])
end
end
# ===== AFTER (if closures worked with assert_output) =====
it("vec_reverse reverses a vector") do ->
assert_output("3\n2\n1\n") do ->
var v = [1, 2, 3]
var r = vec_reverse(v)
for x in r
puts(x.to_s)
end
end
end
8.5. Build System: Task Definition
# ===== CURRENT (Quakefile.qz) =====
def task_build_compiler(): Void
var compiler = "./self-hosted/bin/quartz"
if exists(compiler) == 0
fail("Compiler not found at ".concat(compiler).concat("\n Recover with: git checkout HEAD~1 -- self-hosted/bin/quartz"))
end
var include_flags = "-I self-hosted/frontend -I self-hosted/middle"
var cmd = compiler.concat(" --no-opt --no-cache").concat(" ").concat(include_flags).concat(" ").concat("self-hosted/quartz.qz").concat(" > tmp/quartz.ll 2>tmp/build.err")
var result = system(cmd)
if result != 0
eputs("Build failed. Check tmp/build.err")
exit(1)
end
end
# ===== AFTER =====
def task_build_compiler(): Void
var compiler = "./self-hosted/bin/quartz"
unless file_exists(compiler)
fail("Compiler not found at #{compiler}\n Recover with: git checkout HEAD~1 -- self-hosted/bin/quartz")
end
var include_flags = "-I self-hosted/frontend -I self-hosted/middle"
var cmd = "#{compiler} --no-opt --no-cache #{include_flags} self-hosted/quartz.qz > tmp/quartz.ll 2>tmp/build.err"
unless system(cmd) == 0
fail("Build failed. Check tmp/build.err")
end
end
8.6. Collections: Generic Stack with Iterator
# ===== CURRENT (std/collections/stack.qz — 180 lines) =====
struct Stack
_data: Vec<Int>
end
def stack_new(): Stack = Stack { _data: vec_new<Int>() }
extend Stack {
def push(val: Int): Void = @_data.push(val)
def pop(): Int = ...
def peek(): Int = ...
def size(): Int = @_data.size
def empty?(): Bool = @_data.size == 0
# Copy-pasted from Queue, Deque, LinkedList, PriorityQueue:
def each(f: Fn(Int): Void): Void
var i = 0
while i < @_data.size
f(@_data[i])
i = i + 1
end
end
def map(f: Fn(Int): Int): Vec<Int> ... # ~10 lines
def filter(f: Fn(Int): Bool): Vec<Int> ... # ~10 lines
def reduce(init: Int, f: Fn(Int, Int): Int): Int ... # ~10 lines
def find(f: Fn(Int): Bool): Int ... # ~10 lines
def any(f: Fn(Int): Bool): Bool ... # ~10 lines
def all(f: Fn(Int): Bool): Bool ... # ~10 lines
def count(f: Fn(Int): Bool): Int ... # ~10 lines
}
# ===== AFTER (with generics + iterator trait) =====
struct Stack<T>
_data: Vec<T>
end
def stack_new<T>(): Stack<T> = Stack { _data: vec_new<T>() }
extend<T> Stack<T> {
def push(val: T): Void = @_data.push(val)
def pop(): Option<T> = ... # returns Option, not sentinel
def peek(): Option<T> = ...
def size(): Int = @_data.size
def empty?(): Bool = @_data.size == 0
}
impl<T> Iterable<T> for Stack<T>
def next(self): Option<T> = self.pop()
end
# map, filter, reduce, find, any, all, count — all inherited from Iterable
9. Prioritized Recommendations
Phase 1: Pre-Freeze Polish (Syntax & Consistency)
These are changes that affect the language surface and must be done before freeze:
| # | Item | Impact | Effort | Description |
|---|---|---|---|---|
| 1 | Unify impl + extend | HIGH | MED | impl methods automatically available via UFCS. Eliminates all method duplication. |
| 2 | Bool return types for predicates | HIGH | MED | file_exists, str_eq, vec_contains, hashmap_has etc. return Bool not Int. |
| 3 | Default struct field values | HIGH | MED | field: Type = default_expr. Eliminates monster constructors. |
| 4 | Iterator trait + defaults | HIGH | HIGH | Iterable<T> trait with default map/filter/reduce. Kills 640 lines of duplication. |
| 5 | extern def not extern fn | LOW | LOW | Trivial consistency fix. |
| 6 | zip + enumerate builtins | MED | LOW | Standard functional combinators. |
| 7 | Operator traits (Add, Sub, Mul, Index) | HIGH | MED | Formalize operator overloading through trait system. |
| 8 | Option<T> returns for fallible ops | HIGH | MED | pop(), find(), str_find() return Option<T> not sentinel values. |
| 9 | Character literal support in stdlib | MED | LOW | Migrate magic numbers to 'a', '{', '\n' etc. |
| 10 | Destructuring in let bindings | MED | MED | var { x, y } = point, var (a, b) = pair |
Phase 2: Dogfooding Pass (Compiler & Stdlib Cleanup)
Not language changes — just using existing features consistently:
| # | Item | Occurrences | Effort |
|---|---|---|---|
| 1 | .eq() → == for strings | 257 | Low |
| 2 | .concat() → #{} interpolation | 299 | Low |
| 3 | 0 - 1 → -1 literals | 75 | Low |
| 4 | while i < n → for i in 0..n | 94 | Low |
| 5 | Drop for StringBuilder (eliminate defer sb_free) | 40 | Low |
| 6 | Parallel vecs → Vec | 33 fields | Medium |
| 7 | Magic ASCII → char literals | ~100 | Low |
Phase 3: Documentation Repair
| # | Item | Files |
|---|---|---|
| 1 | Remove pipe lambda syntax from REFERENCE | QUARTZ_REFERENCE.md |
| 2 | Update newtype status to “enforced” | QUARTZ_REFERENCE.md |
| 3 | Remove ? operator documentation | QUARTZ_REFERENCE.md |
| 4 | Fix labeled loop syntax (@outer → :outer) | QUARTZ_REFERENCE.md |
| 5 | Fix “Dot access is same-line only” contradiction | QUARTZ_REFERENCE.md |
| 6 | Remove “Compare strings to 0” advice | QUARTZ_REFERENCE.md |
| 7 | Rewrite ARCHITECTURE.md from scratch | ARCHITECTURE.md |
| 8 | Sync GRAMMAR.md with actual parser behavior | GRAMMAR.md |
| 9 | Update INTRINSICS.md signatures | INTRINSICS.md |
Phase 4: Post-Freeze Platform
Correctly deferred items:
| # | Item | Complexity |
|---|---|---|
| 1 | Package manager (E.2) | HIGH |
| 2 | LSP server (E.1) | HIGH |
| 3 | Associated types in traits | MEDIUM |
| 4 | Full const generics | MEDIUM |
| 5 | dyn Trait dynamic dispatch | HIGH |
| 6 | async/await | VERY HIGH |
| 7 | REPL (E.3) | MEDIUM |
| 8 | Playground / website (W.4) | MEDIUM |
Appendix: Feature Comparison Summary
Where Quartz Leads
- Union + Intersection + Record types — No competitor has all three in a systems language
- Placement allocation syntax (
@heap,@arena) — Unique - Pipeline operator + UFCS + postfix guards — Maximum expressiveness in method chaining
- List/map comprehensions — Only Nim among systems languages
- Symbols — Borrowed from Ruby/Elixir, unique in systems land
- Predicate naming (
.empty?) — Unique among systems languages - const def + static_assert — Competitive with Zig’s comptime
- Self-hosting with fixpoint validation — Formalized, byte-identical verification
Where Quartz Trails
- Tooling — No package manager, no LSP (correctly deferred)
- async/await — Every modern competitor except Go has it
- Operator/index traits — Swift, Rust, Nim all have formal trait-based operator dispatch
- Iterator protocol — Fundamental building block missing from trait system
- Generic collections — Stack/Queue/Deque are Int-only
- Associated types — Required for clean generic abstractions