const! and Bang Access

Two small pieces of syntax for the two ways JavaScript surprises you: something mutated that shouldn't have been, and something null you didn't check.

/#
## `const!` — immutability that costs nothing

JavaScript's `const` binds the NAME, not the value:

    const LIMITS = { max: 10 }
    LIMITS.max = 99          // perfectly legal. const stops rebinding, not mutation.

The usual fix is `Object.freeze`, which costs an allocation, only goes one
level deep, and fails SILENTLY in sloppy mode.

`const!` is checked by the compiler instead:

    const! LIMITS = { max: 10 }
    LIMITS.max = 99          // Cannot mutate immutable binding 'LIMITS'

The error arrives when you compile, not when a test happens to exercise that
line. And because the check is entirely at compile time, the emitted
JavaScript is **exactly** what you would have written by hand:

    const LIMITS = { max: 10 }

No `Object.freeze`, no guard, no wrapper. Zero runtime cost is not a slogan
here — there is nothing to run.

## Bang access — `null` that reports instead of exploding

    const name = cache.miss.name
    // TypeError: Cannot read properties of null — and your program is over

`!` on a member access asks the question instead of assuming the answer:

    const name = cache.miss!.name
    // MonadicError: Expected non-null for 'bang.name', got null

The program keeps running and you have a value that says what went wrong and
where. **Chains propagate** — `a!.b!.c` fails at the first missing link and
carries the error the rest of the way, so you check once at the end rather
than at every hop.

### How this differs from `?.`

`?.` gives you `undefined`, which is a value that has forgotten there was a
problem. Three lines later you are debugging `undefined` with no idea which
hop produced it. A `MonadicError` remembers — and `__tjs.errors()` has it
even if you ignore the return.
#/

// --- `const!` is read-only at compile time ---
const! LIMITS = { max: 10, min: 1 }

console.log('const! reads normally:')
console.log('  LIMITS.max ->', LIMITS.max)
// Uncomment to see the compile-time error, before this line ever runs:
// LIMITS.max = 99
//   → Cannot mutate immutable binding 'LIMITS'. const! bindings are read-only at compile time.

// --- Bang access turns a crash into a value ---
function lookup() {
  const cache = { hit: { name: 'Ada' }, miss: null }

  const found = cache.hit!.name
  const absent = cache.miss!.name // plain JS: TypeError, program over

  return { found, absent }
}

const result = lookup()
console.log('\nbang access:')
console.log('  hit  ->', result.found)
console.log(
  '  miss ->',
  isMonadicError(result.absent)
    ? 'MonadicError: ' + result.absent.message
    : String(result.absent)
)

// --- Chains fail at the first missing link and carry the error onward ---
function deep(root) {
  return root.a!.b!.c
}

console.log('\nchains propagate:')
console.log('  {a:{b:{c:7}}} ->', deep({ a: { b: { c: 7 } } }))
console.log(
  '  {a:null}      ->',
  isMonadicError(deep({ a: null })) ? 'MonadicError' : 'no error'
)

test 'const! bindings read normally' {
  expect(LIMITS.max).toBe(10)
  expect(LIMITS.min).toBe(1)
}

test 'bang access passes a present value straight through' {
  expect(lookup().found).toBe('Ada')
}

test 'bang access on null yields a MonadicError, not a crash' {
  const absent = lookup().absent
  expect(isMonadicError(absent)).toBe(true)
  // The message names the access that failed — errors are a curriculum, not a shrug.
  expect(absent.message).toContain('non-null')
}

test 'a chain fails at the first missing link' {
  expect(deep({ a: { b: { c: 7 } } })).toBe(7)
  expect(isMonadicError(deep({ a: null }))).toBe(true)
}