Phase 7b: null literal + x == null (retire ptr_null/ptr_is_null)

`null` is now a real pointer literal and null-tests are comparisons, instead of
`ptr_null()` and `ptr_is_null(x)`:

  ptr_null()          -> null
  ptr_is_null(x)      -> (x == null)
  not ptr_is_null(x)  -> (x != null)

Mechanics: a new E_NULL primary (`null`, like true/false) lowers to the `null`
pointer; emit_bin's comparison path now picks `ptr` vs `i32` from operand type
(via llty), so `==`/`!=` work on any pointer/record/slice. The two intrinsics are
deleted.

Two reseeds: (A) add the literal + ptr comparison keeping the intrinsics; (B)
migrate all 182 call sites (compiler + runtime, via a balanced-paren script that
skips string-literal args and rewrites `not ptr_is_null` to `!= null`) and delete
the intrinsics. Node/Val/Buf/Tok field defaults now read `ptr = null`.

Vocabulary drops the two from LUDIC_INTRINSICS; `null` joins true/false as a
language constant (grammar + ludic_syntax.h). LANGUAGE.md notes the literal.
Reseeded (21664 lines); C-free fixpoint holds; goldens identical; 17/17; vocab +
doc-fences clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-08-28 00:50:21 +03:00
parent ddb1af3008
commit 70ab79a4e9
34 changed files with 5064 additions and 5124 deletions

View file

@ -34,7 +34,7 @@ fn strip_ludic(name: ptr) -> ptr {
# env var with a fallback when unset
fn getenv_or(name: ptr, dflt: ptr) -> ptr {
let v = os_getenv(name)
if ptr_is_null(v) { return dflt }
if (v == null) { return dflt }
return v
}
@ -52,8 +52,8 @@ fn die(msg: ptr) -> void {
}
entry {
var path = ptr_null()
var out = ptr_null()
var path = null
var out = null
var want = 0 # 0 = auto, 1 = windowed, 2 = headless
var emit_ir = false # --emit-llvm: stop after writing IR
var fmt = false # --fmt: lex + parse only, then exit (the doc-check gate)
@ -84,11 +84,11 @@ entry {
ai = ai + 1
}
if ptr_is_null(path) {
if (path == null) {
die("usage: ludicc <file.ludic> [-o out] [--windowed|--headless] [--emit-llvm] [--save-temps]\n")
}
let src = read_file(path)
if ptr_is_null(src) { die("ludicc: cannot open input\n") }
if (src == null) { die("ludicc: cannot open input\n") }
cur_dir = dir_of(path)
lex(src)
@ -104,7 +104,7 @@ entry {
# a game gets a window by default; a plain program stays headless. An explicit
# flag always wins. The stdout-IR path (no target) also stays headless, which
# is what reseed.sh compiles the compiler itself with.
let has_target = run or (not ptr_is_null(out))
let has_target = run or ((out != null))
if want == 1 { g_windowed = true }
else { if want == 2 { g_windowed = false }
else { g_windowed = has_target and has_systems() } }
@ -113,10 +113,10 @@ entry {
# --emit-llvm, or no binary target: emit IR and stop (stdout when out is null).
if emit_ir { ir_flush(out); return }
if not has_target { ir_flush(ptr_null()); return }
if not has_target { ir_flush(null); return }
# a run with no explicit -o lands in a temp file
if run and ptr_is_null(out) {
if run and (out == null) {
out = sconcat("/tmp/ludic-run-", strip_ludic(base_name(path)))
}