TESTING / DEVELOPMENT · DRAFTdrafts/rfc-draft-small-syntax-decisions.md
Status dokumen bukan klaim bahwa fiturnya sudah tersedia.

RFC dan proposal mencatat rancangan. Untuk kemampuan yang benar-benar dapat dipakai, lihat matriks kapabilitas runtime dan catatan rilis.

Draft RFC 0009: Small syntax and semantics decisions

Status: Proposed — draft for review by the project owner. Not accepted; nothing in this document changes implementation status. The exact numbers, examples, and diagnostics text are subject to revision before acceptance. Written to be folded into docs/rfcs/ after review.

Depends on: RFC 0001 (core types and errors), RFC 0007 (diagnostics). All four decisions below are intentionally small, independently shelvable, and motivated by concrete friction in the current language slice.


Decision 1 — Assignment surface: let / var, retiring set

Problem. Three keywords cover one concept. A var value needs set result = result + value to mutate (examples/functions.yz); beginners type result = ... instead, and the compiler rejects it with no elegant migration path. let/var distinguish mutability, which is worth keeping; set only distinguishes assignment instruction from declaration, which most languages handle with a bare =.

Decision (proposed).

  1. let name: T = expr declares an immutable local — unchanged.
  2. var name: T = expr declares a mutable local — unchanged.
  3. A bare assignment name = expr inside a scope where name is a visible var becomes legal and means the same as set name = expr. Same for field updates (value.field = expr).
  4. set remains valid with identical semantics, initially non-deprecated; after the language specification exists (RFC 0007 normative docs), it is moved to a documented deprecation cycle: warn, then remove in the next major.
  5. Assignment is an expression-valued statement, not an expression: it does not participate in function call chains (this preserves the current no-arbitrary-nesting stance of ? and match).
# Before
var total: Number = 0
set total = total + current

# After (proposed)
var total: Number = 0
total = total + current

Staged implementation. Add bare name = and name.field = assignment parsing for visible vars (negative tests: assignment to a let binding must still fail, assignment to an undeclared name must still fail, assignment to a shape field of an immutable value must still fail). set keeps compiling unchanged. Compiler diagnostics for both forms cite the same error message text.

Acceptance criteria. Existing set-using tests keep passing unchanged; a new conformance test exercises bare assignment through nested if/while blocks; both ASan/UBSan and self-host byte-identical runs pass after every change.

Open question. Should set warn or fail if it targets a let? Proposed: keep the current failure with an improved message that mentions the two alternatives.


Decision 2 — Named shape constructors

Problem. Constructors are positional and must list fields in declaration order: Product(7, "YAZA Language Guide", int32_from_number(1499), true) (examples/shapes.yz). Swapping two same-typed fields compiles silently. For data-carrying API shapes this is a recurring real defect.

Decision (proposed).

  1. Named form: Product(id: 7, name: "N", price: int32_from_number(1499), available: true) — order-independent, one value per field, exactly all fields (no partial construction).
  2. Positional form remains legal. It is not deprecated now because it is concise for small/interchangeable shapes; a later deprecation can follow once the spec exists.
  3. Mixing named and positional in one constructor is a compile error.
  4. Duplicate field name, unknown field name, and a missing field are compile errors naming the shape and field.

Compiler note. Field names are already known from shape records (compiler/native_cli.yz); the parser can accept identifier ':' elements and resolve them against native_shape_field_* records, so this requires no new type machinery.

Acceptance criteria. Positive: named construction, reordered fields, use in Option[Z]/Result payloads and function returns. Negative: duplicate, unknown, missing field, mixed form, and a swapped-position case that previously would have compiled (now caught by the named form's distinctness).


Decision 3 — Omitting the fallback return after an exhaustive match

Implementation note (2026-10-27). The native compiler now accepts a non-Unit function whose final supported if/elif/else chain or exhaustive statement-form match returns from every branch, so an unreachable fallback is no longer needed for those cases. Sequential early returns followed by a fallback also work. This resolves the motivating behavior for the currently supported control-flow forms; the draft remains unaccepted and is not a promise of general control-flow-graph analysis. set label = ... remains valid for assigning to an existing mutable var; the separate proposal to retire set in Decision 1 has not been accepted or implemented.

Original problem. When this draft was written, a function needed a top-level fallback return even when its final statement was an exhaustive match (examples/variants.yz); examples used a dead return "unreachable". The implementation note above records that the compiler now handles the supported exhaustive match and conditional cases without that fallback.

Decision (proposed).

  1. A match that covers every declared case exactly once and whose every arm ends in return (or arms omitted when the function returns a non-Unit value are illegal) satisfies the function's return obligation; the fallback top-level return is no longer required after it.
  2. The function's declared return type must equal the type every arm's return produces; the compiler already checks each arm, so the new check compares the existing per-arm results against the signature.
  3. Non-exhaustive match still requires the fallback return (or the missing arm is a compile error, as today).
  4. Option/Result block matches (some/none, ok/err) inherit the same rule.

Acceptance criteria. A variant match with all four arms returning compiles without a trailing return; deleting one arm without restoring the fallback is rejected; identical shape mutates nothing about ergonomics for while/for returning.


Decision 4 — One consistent text-conversion rule

Problem. say renamed.available prints a Boolean directly, while Int32 needs int32_text(...) (examples/shapes.yz). The interpolation path {value} and the concatenation path already disagree, and the tutorial has to teach exceptions. One rule that beginners can carry will do more for consistency than any new keyword.

Decision (proposed). Adopt a single rule: every primitive type has exactly one canonical text form, and say/interpolation apply it uniformly: Text unchanged, Boolean → true/false, Number/Int32 → decimal, Float64 → the existing shortest round-trip decimal form, Bytes → unchanged requirement of an explicit decoder (binary stays binary). String concatenation via + follows the same rule, replacing today's helper-only conversions for Number/Int32/Float64/Boolean.

Migration. The existing helpers stay as named functions (they are often useful for explicitness); the new rule only adds implicit conversion where +/say/{} previously rejected the type. A separate negative test locks the Bytes boundary so binary data cannot silently become garbage text.

Acceptance criteria. Concatenating and interpolating each primitive matches the table above; a Bytes-in-+ rejection test passes; generated output for the existing examples does not change for the types that already converted.


Why these four together

All four attacks the same axis — the language's beginner-facing story — without expanding the type system, without new syntax volume, and all are independently shippable and testable against the compiler's existing record-based structure. They are priority-orderable against Gate A work because none of them blocks it; recommendation is to schedule them after Gate A Stage 1 stabilizes the dispatch model, so the language spec that RFC 0007 requires can reflect the final shapes.

Open questions

  1. Does set retirement happen conditionally on the normative spec landing, or at a fixed milestone?
  2. Should named constructor fields allow trailing _ for partial fields (none is proposed today)?
  3. Should the exhaustive-match rule extend to match used as the final statement of a while body (currently illegal to return from inside a loop unless matched by a top-level fallback)?