Quartz v5.25

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

  1. Executive Summary
  2. Documentation vs Reality
  3. Syntax & Ergonomic Awkwardness
  4. API Unification Issues
  5. Table Stakes Gap Analysis
  6. Dogfooding Opportunities
  7. Missing Language Constructs
  8. Before & After Vision
  9. 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:

  1. The as_int()/as_string() tax — The existential i64 model forces unsafe type-erasure casts throughout real code. 266 occurrences in the compiler alone.
  2. API fragmentation — Every operation exists in 2-3 calling conventions (free function, UFCS method, intrinsic name) with no clear canonical form.
  3. Bool/Int conflation — Predicates return Int (0/1) instead of Bool, forcing == 1 checks everywhere.
  4. 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)

FeatureStatusNotes
Negative integer literalsYESParser handles unary minus. -1, -42 work in expressions
String == content comparisonYESMIR detects string operands, emits str_eq intrinsic
loop keywordYESDesugars to while true
for item in vecYESWorks for ranges, arrays, vecs, custom iterators
try/catch syntaxYESFull pipeline: parser, MIR, codegen
Operator overloadingYESVia op_add, op_eq, op_lt etc. in extend blocks
Labeled loopsYESUses :label syntax (NOT @label)
List comprehensionsYESTransform + optional filter
unlessYESStatement + postfix guard forms
Custom iteratorsYESStructName$next() protocol returning Option
select (channels)YESrecv/send/default arms
parallel_map/for/reduceYESThread pool intrinsics
@heap/@arena placementYESParser, MIR, codegen all implemented
awaitYESThread join semantics (pthread_join)
task_groupYESStructured concurrency with cancel support
Enum exhaustivenessYESEnforced in typechecker
Doc comments ##YESThree forms: ##, ##!, ##^
const def / const evalYESArithmetic, functions, loops, static_assert
Pipe lambda `x` removed

Confirmed NOT Working

FeatureStatusNotes
? operatorREMOVEDDeliberately removed v5.12.27, replaced by $try() macro

Documentation Errors Found

DocumentClaimReality
QUARTZ_REFERENCE.mdShows pipe lambda syntax |x| expr as “legacy”Pipe lambdas are removed, not legacy. Doc still shows examples.
QUARTZ_REFERENCE.mdSays newtypes are “planned, currently transparent”Newtype enforcement is fully implemented (S2.4 complete).
QUARTZ_REFERENCE.mdShows ? operator for error propagationRemoved in v5.12.27. Only $try() exists.
QUARTZ_REFERENCE.mdLists labeled loops as @outerActual syntax is :outer (symbol syntax).
QUARTZ_REFERENCE.md”Dot access is same-line only” in Common MistakesContradicted 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.mdReferences “v1.0.0”, “730 tests”, C bootstrapEntire doc is stale. v5.25.0, ~3000+ tests, C bootstrap retired.
GRAMMAR.mdHas let_stmt productionCLAUDE.md anti-hallucination table says let doesn’t exist.
GRAMMAR.mdUses fn keyword in extern_declAll other functions use def. Inconsistency.
GRAMMAR.mdShows do -> paramsActual syntax is do params -> (params before arrow).
STYLE.mdSays “Arrow syntax is the only syntax”True, but REFERENCE.md still documents pipe syntax.
INTRINSICS.mdvec_new returns Int, vec_push(vec: Int, val: Int)Parameterized: vec_new<Int>(), v.push(42) in practice.
INTRINSICS.mdShows hashmap_delReference 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:

OperationIntrinsicUFCS MethodFree Function
String lengthstr_len(s)s.size
String char atstr_char_at(s, i)s.char_at(i)
String equalitystr_eq(a, b)a.eq(b)a == b
String concatstr_concat(a, b)a.concat(b)a + b
Vec lengthvec_len(v)v.size
Vec pushvec_push(v, x)v.push(x)
Vec getvec_get(v, i)v[i]
HashMap hashashmap_has(m, k)
HashMap deletehashmap_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

FunctionReturnsShould 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 =~? sVec<String> or 0Option<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

PatternExamplesIssue
str_* prefixstr_split, str_join, str_trimInconsistent with UFCS .split(), .join(), .trim()
vec_* prefixvec_push, vec_get, vec_lenInconsistent with UFCS .push(), v[i], .size
hashmap_del vs hashmap_deleteBoth usedPick one
sb_new/sb_free/sb_appendStringBuilderWhy sb_ not string_builder_?
str_len vs .sizeString lengthTwo names for same operation
vec_len vs .sizeVec lengthTwo names for same operation
print vs putsString outputprint = no newline, puts = newline. print_int = with newline. Inconsistent.
eputs vs eprintStderr outputMirror 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

FeatureSwiftGoRustZigNimQuartz
Union types (Int | String)-----YES
Intersection types (Eq & Show)-----YES
Record types ({ name: String })-----YES
Symbols (:ok, :error)-----YES
UFCS----NimYES
Placement allocation (@heap, @arena)-----YES
Pipeline operator (|>)----NimYES
Postfix guards (return x if cond)-----YES
List/map comprehensions----NimYES
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

FeatureWho Has ItComplexityImpact
Operator overloading (trait-based)Swift, Rust, NimMEDIUMCurrently 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 overloadingSwift, Rust, NimMEDIUMOnly Vec/HashMap support x[i]. Custom collections (Deque, Matrix) cannot participate. Need Index/IndexMut traits.
Destructuring in let bindingsSwift, Rust, Zig, NimMEDIUMCurrently only in match arms. var Point { x, y } = p would reduce boilerplate.
Iterator protocol / Iterable traitSwift, Rust, NimMEDIUMWould eliminate 640 lines of copy-pasted HOF code across 5 collection types.
zip / enumerateSwift, Rust, NimLOWTrivial additions to HOF set.
Default struct field valuesSwift, Go, Rust (Default), ZigLOW-MEDWould eliminate the 45-field constructor problem.

Tier 2: Post-Freeze Important

FeatureWho Has ItComplexityImpact
Package managerAll 5HIGHNo ecosystem without it. Correctly deferred.
LSPAll 5HIGHNo good editor experience without it. Correctly deferred.
async/awaitSwift, Rust, Zig, NimVERY HIGHThread-based concurrency works but doesn’t scale for I/O-bound workloads.
Associated types in traitsSwift, RustMEDIUMRequired for clean iterator/collection abstractions.
dyn Trait dynamic dispatchRustHIGHCurrently static dispatch only.
Full const genericsRust, ZigMEDIUMArray<T,N> exists but not generalizable.
Internal visibility (pub(crate))Swift, Go, RustLOWNeeded when packages exist.
FeatureRationale for Skipping
Higher-kinded typesNo systems language has them. Academic.
Dependent typesRefinement types (R track) is the better path.
Runtime reflectionContradicts existential type model.
Custom operatorsFootgun. Operator overloading suffices.
Green threadsasync/await is the better path.
Garbage collectorOwnership + 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

OpportunityOccurrencesEffortRiskPriority
.eq()== for strings257LowLowP1
.concat() → interpolation299LowLowP1
0 - 1-175LowNoneP1
while i < nfor i in 0..n94LowLowP2
Parallel vecs → Vec33 fieldsMediumMediumP2
as_int()/as_string() → typed containers266HighMediumP3
Integer consts → enums75+ groupsVery HighHighDefer

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/reduce across 5 collection types
  • No generic “works on any collection” functions
  • for item in collection works 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:

#ItemImpactEffortDescription
1Unify impl + extendHIGHMEDimpl methods automatically available via UFCS. Eliminates all method duplication.
2Bool return types for predicatesHIGHMEDfile_exists, str_eq, vec_contains, hashmap_has etc. return Bool not Int.
3Default struct field valuesHIGHMEDfield: Type = default_expr. Eliminates monster constructors.
4Iterator trait + defaultsHIGHHIGHIterable<T> trait with default map/filter/reduce. Kills 640 lines of duplication.
5extern def not extern fnLOWLOWTrivial consistency fix.
6zip + enumerate builtinsMEDLOWStandard functional combinators.
7Operator traits (Add, Sub, Mul, Index)HIGHMEDFormalize operator overloading through trait system.
8Option<T> returns for fallible opsHIGHMEDpop(), find(), str_find() return Option<T> not sentinel values.
9Character literal support in stdlibMEDLOWMigrate magic numbers to 'a', '{', '\n' etc.
10Destructuring in let bindingsMEDMEDvar { x, y } = point, var (a, b) = pair

Phase 2: Dogfooding Pass (Compiler & Stdlib Cleanup)

Not language changes — just using existing features consistently:

#ItemOccurrencesEffort
1.eq()== for strings257Low
2.concat()#{} interpolation299Low
30 - 1-1 literals75Low
4while i < nfor i in 0..n94Low
5Drop for StringBuilder (eliminate defer sb_free)40Low
6Parallel vecs → Vec33 fieldsMedium
7Magic ASCII → char literals~100Low

Phase 3: Documentation Repair

#ItemFiles
1Remove pipe lambda syntax from REFERENCEQUARTZ_REFERENCE.md
2Update newtype status to “enforced”QUARTZ_REFERENCE.md
3Remove ? operator documentationQUARTZ_REFERENCE.md
4Fix labeled loop syntax (@outer:outer)QUARTZ_REFERENCE.md
5Fix “Dot access is same-line only” contradictionQUARTZ_REFERENCE.md
6Remove “Compare strings to 0” adviceQUARTZ_REFERENCE.md
7Rewrite ARCHITECTURE.md from scratchARCHITECTURE.md
8Sync GRAMMAR.md with actual parser behaviorGRAMMAR.md
9Update INTRINSICS.md signaturesINTRINSICS.md

Phase 4: Post-Freeze Platform

Correctly deferred items:

#ItemComplexity
1Package manager (E.2)HIGH
2LSP server (E.1)HIGH
3Associated types in traitsMEDIUM
4Full const genericsMEDIUM
5dyn Trait dynamic dispatchHIGH
6async/awaitVERY HIGH
7REPL (E.3)MEDIUM
8Playground / website (W.4)MEDIUM

Appendix: Feature Comparison Summary

Where Quartz Leads

  1. Union + Intersection + Record types — No competitor has all three in a systems language
  2. Placement allocation syntax (@heap, @arena) — Unique
  3. Pipeline operator + UFCS + postfix guards — Maximum expressiveness in method chaining
  4. List/map comprehensions — Only Nim among systems languages
  5. Symbols — Borrowed from Ruby/Elixir, unique in systems land
  6. Predicate naming (.empty?) — Unique among systems languages
  7. const def + static_assert — Competitive with Zig’s comptime
  8. Self-hosting with fixpoint validation — Formalized, byte-identical verification

Where Quartz Trails

  1. Tooling — No package manager, no LSP (correctly deferred)
  2. async/await — Every modern competitor except Go has it
  3. Operator/index traits — Swift, Rust, Nim all have formal trait-based operator dispatch
  4. Iterator protocol — Fundamental building block missing from trait system
  5. Generic collections — Stack/Queue/Deque are Int-only
  6. Associated types — Required for clean generic abstractions