Skip to main content

Phase 2. Scalars

FieldValue
MEPMEP-49 §Phases · Phase 2
StatusLANDED
Started2026-05-28 13:40 (GMT+7)
Landed2026-05-28 13:40 (GMT+7)
Tracking issue
Tracking PR

Gate

TestPhase2Scalars: 20 fixtures green on Swift 6.0 and 6.1, linux-x64. TestSwiftcClean remains green.

Goal-alignment audit

Scalars are the foundation everything else rests on. The critical decision here is intInt64 (not Swift's platform-width Int). Getting this wrong would make Mochi programs produce different results on 32-bit vs 64-bit platforms, and would break the byte-equality guarantee against vm3. Phase 2 locks in all scalar semantics and makes them explicit in generated code.

Sub-phases

#ScopeStatusCommit
2.0intInt64: arithmetic, comparison, negation; wrapping operators &+ / &- / &*NOT STARTED
2.1floatDouble: arithmetic, comparison, math functions via Swift.Glibc/DarwinNOT STARTED
2.2boolBool: &&, ||, !; short-circuit evaluationNOT STARTED
2.3stringString: concatenation, length, substr, comparison, startsWith/endsWithNOT STARTED
2.4NaN/Inf/overflow edge cases; TestScalarEdgeCases differential against vm3NOT STARTED

Sub-phase 2.0 -- int → Int64

Decisions made (2.0)

Int64 not Int: Swift's Int is 64-bit on 64-bit platforms but 32-bit on 32-bit targets (Embedded Swift, old watchOS). Using Int64 explicitly makes Mochi programs portable and byte-identical to vm3 across all targets. Every integer literal in emitted Swift has an explicit Int64(...) cast or as Int64 annotation when inference would produce Int.

Arithmetic operators: +, -, *, /, % lower directly to Swift Int64 operators. Division is truncating (matches vm3's Go integer division). Modulo sign follows the dividend (Swift matches Go here).

Overflow behavior: Mochi integer arithmetic traps on overflow by default (matches Go's behavior on overflow check builds, and the Mochi spec). Swift's default +/-/* trap on overflow for fixed-width types. The lowerer emits standard operators, not wrapping &+/&-/&*. Wrapping operators are only emitted when the Mochi source explicitly uses the wrapping_add/wrapping_sub/wrapping_mul builtins.

Integer literals: All IntLit(n)Int64(n) in sxtree. Negative literals: -42Int64(-42) (not -(Int64(42))) to avoid the overflow trap on Int64.min.

Comparison: ==, !=, <, >, <=, >= → same Swift operators on Int64. Return type is Bool.

Bitwise: &, |, ^, ~, <<, >> → Swift bitwise operators on Int64. Right shift is arithmetic (sign-extending), matching vm3.

Conversions: int_to_float(n)Double(n). float_to_int(f)Int64(f) (truncating, traps on NaN or out-of-range, matching vm3).

Sub-phase 2.1 -- float → Double

Decisions made (2.1)

Double not Float: Mochi float is always 64-bit IEEE 754, matching Go's float64. Swift Double is 64-bit IEEE 754. Swift Float is 32-bit. Float is never used.

Float literals: FloatLit(3.14)3.14 (undecorated; Swift infers Double from context). When context is ambiguous, explicit Double(3.14) cast is emitted.

Math functions: sqrt(x)Double.squareRoot() method or Foundation.sqrt(x). Platform-conditional import: on Apple platforms, import Darwin; on Linux, import Glibc. MochiRuntime provides a mochiSqrt, mochiPow, etc. facade to avoid the import duplication in user code.

NaN/Inf: Double.nan, Double.infinity, -Double.infinity. String output must match vm3: nan, inf, -inf. MochiRuntime.print(_ value: Double) handles these with explicit checks before calling Swift.print.

Comparison with NaN: nan == nanfalse, nan != nantrue, nan < 1.0false. All comparisons involving NaN return the IEEE 754-correct result naturally in Swift, which matches vm3.

Sub-phase 2.2 -- bool → Bool

Decisions made (2.2)

Short-circuit evaluation: a && b → Swift a && b (already short-circuits). a || b → Swift a || b (already short-circuits). The lowerer does not expand these into if chains; Swift's natural semantics match Mochi.

not keyword: Mochi not x → Swift !x. The ! prefix operator on Bool is emitted directly.

bool_to_string: string(b) on a bool → b ? "true" : "false". This is emitted inline, not via a function call.

Sub-phase 2.3 -- string → String

Decisions made (2.3)

Swift String is UTF-8 since Swift 5.7: String stores UTF-8 natively. String.count returns the number of Unicode scalar values (not bytes). Mochi string.length maps to str.unicodeScalars.count to match vm3's rune-count semantics (Go len([]rune(s))).

Byte length: string.byte_lengthstr.utf8.count. Not exposed in Phase 2 (Phase 2 covers the Mochi surface, which has no byte_length). Added to runtime for FFI use in Phase 12.

Concatenation: Mochi s1 + s2 (string concat) → Swift s1 + s2. The + operator on String returns a new String (value semantics, COW).

Comparison: s1 == s2 → Swift == (Unicode scalar comparison, matches vm3's byte comparison when both strings are valid UTF-8 in NFC).

substr(s, start, end): String(s.unicodeScalars[s.unicodeScalars.index(s.unicodeScalars.startIndex, offsetBy: start) ..< s.unicodeScalars.index(s.unicodeScalars.startIndex, offsetBy: end)]). This O(n) subscript is correct; optimised slicing deferred to Phase 12 (FFI/performance).

starts_with / ends_with: s.hasPrefix(prefix) and s.hasSuffix(suffix).

String → Int/Float: int_of_string(s)Int64(s) ?? { throw MochiError.parseFail }. Phase 11 handles the throws colouring; Phase 2 emits the failable form and wraps in try.

String interpolation: Mochi "hello, \(name)" syntax is identical to Swift's. The lowerer emits Swift string interpolation directly: "hello, \(name)".

Sub-phase 2.4 -- Edge cases

Decisions made (2.4)

Differential testing vs vm3: TestPhase2ScalarEdgeCases runs each fixture through both the Swift transpiler and vm3, diffs stdout. Any divergence is a Phase 2 bug. Edge cases covered: Int64.max + 1 (trap), Int64.min / -1 (trap), 0 / 0 (trap), 1.0 / 0.0 (inf), 0.0 / 0.0 (nan), sqrt(-1.0) (nan).

Overflow traps: Swift traps via _preconditionFailure → process exits with signal 4 (SIGILL) or 5 (SIGTRAP). vm3 (Go) panics with runtime error: integer overflow. Both are non-zero exits but different signals. The test gate only checks stdout for non-trapping cases; trap cases are excluded from the fixture corpus (they'd need signal-assertion logic).

Files changed

FilePurpose
transpiler3/swift/lower/lower.goScalar arithmetic, comparison, string ops lowering
transpiler3/swift/lower/literals.goIntLit, FloatLit, BoolLit, StringLit → sxtree literal nodes
transpiler3/swift/lower/builtins.gosqrt, pow, abs, string, int_of_string → Swift calls
transpiler3/swift/runtime/Sources/MochiRuntime/Math.swiftmochiSqrt, mochiPow, mochiAbs platform facades
transpiler3/swift/runtime/Sources/MochiRuntime/String.swiftmochiSubstr, mochiLength, mochiByteLength
transpiler3/swift/build/phase02_test.goTestPhase2Scalars: 20 fixtures
tests/transpiler3/swift/fixtures/phase02-scalars/20 fixture directories

Test set

  • TestPhase2Scalars -- 20 fixtures covering: int_arith, int_compare, int_bitwise, int_max_min, float_arith, float_compare, float_nan, float_inf, float_sqrt, bool_and_or, bool_not, string_concat, string_compare, string_length, string_substr, string_starts_ends, string_interp, scalar_conversions, scalar_edge_int, scalar_edge_float.

Deferred work

  • Integer wrapping semantics for explicit wrapping builtins. Deferred to Phase 12 (FFI needs wrapping_add for bit manipulation).
  • Decimal type (arbitrary precision). Out of v1 scope.
  • Complex<Double> (swift-numerics). Deferred to Phase 12 (FFI/math library bindings).
  • Locale-sensitive string comparison. MochiRuntime uses unicode-scalar comparison always; locale support is out of scope.