Build.schema_hash(): FNV-1a 64 of the program's own schema (the bytes ludic schema prints), worked out only when a program names it, 0 under ludicc --release, which ludic bundle now passes (Mac and Windows); reseeded

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 19:05:28 +03:00
parent b5bffe99fa
commit dacc0f7280
14 changed files with 75837 additions and 75090 deletions

View file

@ -680,6 +680,11 @@ Each list is sorted by name (then file and line), a registry's entries are in in
paths are the ones the compiler read, so two runs over the same source write the same file. The paths are the ones the compiler read, so two runs over the same source write the same file. The
engine's runtime is left out. engine's runtime is left out.
`Build.schema_hash()` is a `long`: FNV-1a 64 of exactly those bytes, for the program being built,
worked out once and only when the program names it - so a tool talking to a running build can tell
whether it was compiled from the data in front of it. It is 0 in a release (`ludicc --release`,
which `ludic bundle` passes).
### Default parameters, and calls that name what they change ### Default parameters, and calls that name what they change
A parameter can have a default, and a call leaves out what it does not change - the last ones when A parameter can have a default, and a call leaves out what it does not change - the last ones when

View file

@ -14,3 +14,6 @@ so is `@Node` / `@Clip` naming a field that is not a glTF (`@Asset("gltf")`, or
a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check a listing `@OneOf` (`@OneOf(A, B)`, not a prefix `@OneOf(P_)`) naming a constant that does not exist, and `@Tint` naming no constant. A field may now carry several attributes. `ludicc --check
--diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array --diagnostics=json` (`ludic build --check --diagnostics=json`) prints every error as one JSON array
of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column. of `{file, line, col, severity, message}` on stdout; tokens and nodes now know their column.
`Build.schema_hash()` answers FNV-1a 64 of the program's own schema (the bytes `ludic schema` prints),
computed only when a program names it, and 0 under `ludicc --release`, which `ludic bundle` now passes.

View file

@ -0,0 +1,14 @@
# schema_hash.ludic — Build.schema_hash(): FNV-1a 64 of the program's own schema (what `ludic schema`
# prints), so a tool can tell which data a running build was compiled from; 0 in a release build
# (`ludicc --release`, which ludic bundle passes).
#
# Running it prints: true
program SchemaHash {
property Tool {
key: string = ""
weight: float = 1.0
}
registry Tools of Tool as TL
def Tools axe { weight: 2.5 }
entry { print(`{Build.schema_hash() != long(0)}`) }
}

View file

@ -290,6 +290,10 @@ function emit_ns_call(ns: pointer, meth: pointer, e: Node) -> Val {
if is_path_ns(meth) { return emit_path_ns(meth, e) } if is_path_ns(meth) { return emit_path_ns(meth, e) }
return emit_alias_or_fail("Path", meth, e) return emit_alias_or_fail("Path", meth, e)
} }
if (ns == "Build") {
if is_build_ns(meth) { return emit_build_ns(meth, e) }
return emit_alias_or_fail("Build", meth, e)
}
if (ns == "Mime") { if (ns == "Mime") {
if is_mime_ns(meth) { return emit_mime_ns(meth, e) } if is_mime_ns(meth) { return emit_mime_ns(meth, e) }
return emit_alias_or_fail("Mime", meth, e) return emit_alias_or_fail("Mime", meth, e)

View file

@ -16,6 +16,15 @@
var g_schema_out: pointer = null var g_schema_out: pointer = null
function schema_write(path: pointer) -> bool { function schema_write(path: pointer) -> bool {
let b = schema_buf()
let f = file_open(path, "wb")
if f == null { return false }
file_write(f, buf_str(b), b.len)
file_close(f)
return true
}
# the schema's bytes, exactly as --emit-schema writes them
function schema_buf() -> Buf {
let b = buf_new() let b = buf_new()
buf_puts(b, "{\n \"schema_version\": 1,\n \"entry\": ") buf_puts(b, "{\n \"schema_version\": 1,\n \"entry\": ")
jq_put(b, g_mg_entry) jq_put(b, g_mg_entry)
@ -28,11 +37,7 @@ function schema_write(path: pointer) -> bool {
buf_puts(b, "\n ],\n \"functions\": [") buf_puts(b, "\n ],\n \"functions\": [")
sc_functions(b) sc_functions(b)
buf_puts(b, "\n ]\n}\n") buf_puts(b, "\n ]\n}\n")
let f = file_open(path, "wb") return b
if f == null { return false }
file_write(f, buf_str(b), b.len)
file_close(f)
return true
} }
# ---- names and text --------------------------------------------------------------------------- # ---- names and text ---------------------------------------------------------------------------

View file

@ -0,0 +1,46 @@
# emit_schema_hash.ludic — Build.schema_hash(): FNV-1a 64 of the program's own schema (the bytes
# `ludic schema` prints), so a tool talking to a running build can tell it was built from other
# data. Worked out once, only when the program asks for it, and 0 in a release (`--release`,
# ludic bundle). The compiler's int is 32 bits, so the hash is kept as four 16-bit limbs.
var g_release: bool = false # --release: what ships carries no schema hash
var g_uses_schema_hash: bool = false # the program names Build.schema_hash (parse.ludic)
var g_sh_lo: int = 0 # the hash's low and high 32 bits, once worked out
var g_sh_hi: int = 0
var g_sh_done: bool = false
# after the checker: the schema as --emit-schema would write it, hashed
function schema_hash_compute() -> void {
if g_release or not g_uses_schema_hash { return }
let b = schema_buf()
let s = buf_str(b)
var h0 = 8997 # 522109ce484222325, low limb first
var h1 = 33826
var h2 = 40164
var h3 = 52210
var i = 0
while i < b.len {
h0 = h0 ^ (s[i] & 255)
# times 256000001b3: limb 0 of the prime is 435, limb 2 is 256
var r0 = h0 * 435
var r1 = h1 * 435 + (r0 >> 16)
var r2 = h2 * 435 + h0 * 256 + (r1 >> 16)
var r3 = h3 * 435 + h1 * 256 + (r2 >> 16)
h0 = r0 & 65535
h1 = r1 & 65535
h2 = r2 & 65535
h3 = r3 & 65535
i += 1
}
g_sh_lo = (h1 << 16) | h0
g_sh_hi = (h3 << 16) | h2
g_sh_done = true
}
function is_build_ns(meth: pointer) -> bool { return meth == "schema_hash" }
function emit_build_ns(meth: pointer, e: Node) -> Val {
if not g_sh_done { return val("0", "long") }
let lo = emit_bind(`zext i32 {itoa(g_sh_lo)} to i64`)
let hi = emit_bind(`zext i32 {itoa(g_sh_hi)} to i64`)
let up = emit_bind(`shl i64 {hi}, 32`)
return val(emit_bind(`or i64 {up}, {lo}`), "long")
}

View file

@ -360,6 +360,7 @@ function p_postfix() -> Node {
if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand if e.a.kind == E_ID and e.a.s == "Regex" { g_uses_regex = true } # splice the regex runtime on demand
if e.a.kind == E_ID and e.a.s == "Random" { g_uses_rng = true } # splice the seeded random numbers on demand if e.a.kind == E_ID and e.a.s == "Random" { g_uses_rng = true } # splice the seeded random numbers on demand
if e.a.kind == E_ID and e.a.s == "Fs" and (e.s == "read_bytes" or e.s == "write_bytes") { g_uses_bytes = true } if e.a.kind == E_ID and e.a.s == "Fs" and (e.s == "read_bytes" or e.s == "write_bytes") { g_uses_bytes = true }
if e.a.kind == E_ID and e.a.s == "Build" and e.s == "schema_hash" { g_uses_schema_hash = true } # the schema hashed after the checker
if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand if e.a.kind == E_ID and (e.a.s == "BigInt" or e.a.s == "Decimal") { g_uses_bignum = true } # splice the bignum runtime on demand
if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand if e.a.kind == E_ID and (e.a.s == "Dict" or e.a.s == "Set") { g_uses_dict = true } # splice the hash-table runtime on demand
if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand if e.a.kind == E_ID and (e.a.s == "Huge" or e.a.s == "Angle" or e.a.s == "Percent") { g_uses_numeric = true } # splice the huge/angle/percent runtime on demand

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -158,6 +158,7 @@ entry {
else if a == "--save-temps" { save = true } else if a == "--save-temps" { save = true }
else if a == "--run" { run = true } else if a == "--run" { run = true }
else if a == "--check" { g_check_only = true } else if a == "--check" { g_check_only = true }
else if a == "--release" { g_release = true } # what ships: Build.schema_hash() is 0 (emit_schema_hash.ludic)
else if a == "--diagnostics=json" { g_diag_json = true } # every error, as JSON on stdout (diag.ludic) else if a == "--diagnostics=json" { g_diag_json = true } # every error, as JSON on stdout (diag.ludic)
else if a == "--diagnostics=text" { g_diag_json = false } else if a == "--diagnostics=text" { g_diag_json = false }
else if a == "--emit-schema" { ai += 1; if ai < arg_count() { g_schema_out = arg(ai) } } # emit_schema.ludic else if a == "--emit-schema" { ai += 1; if ai < arg_count() { g_schema_out = arg(ai) } } # emit_schema.ludic
@ -248,6 +249,7 @@ entry {
if g_migrate { mg_finish() } if g_migrate { mg_finish() }
# --emit-schema FILE: the declarations, the registries and their entries as the checker resolved # --emit-schema FILE: the declarations, the registries and their entries as the checker resolved
# them (emit_schema.ludic); on its own it stops there, with -o or --check the build goes on # them (emit_schema.ludic); on its own it stops there, with -o or --check the build goes on
schema_hash_compute() # Build.schema_hash(), when the program asks and it is no release
if g_schema_out != null { if g_schema_out != null {
if not schema_write(g_schema_out) { die(`ludicc: cannot write {g_schema_out}\n`) } if not schema_write(g_schema_out) { die(`ludicc: cannot write {g_schema_out}\n`) }
if not g_check_only and not has_target and not emit_ir { diag_exit(0) } if not g_check_only and not has_target and not emit_ir { diag_exit(0) }

View file

@ -49,11 +49,11 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
var flags = "--windowed" var flags = "--windowed"
if mode == 2 { flags = "--headless" } if mode == 2 { flags = "--headless" }
if save { flags = flags + " --save-temps" } if save { flags = flags + " --save-temps" }
return shq(`{ludicc()} {flags}{title_flag()}{unsafe_flag()} {src} -o {out}`) return shq(`{ludicc()} {flags}{title_flag()}{unsafe_flag()}{release_flag()} {src} -o {out}`)
} }
if mode == 2 { if mode == 2 {
if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false } if not shq(`{ludicc()} --headless{title_flag()}{unsafe_flag()}{release_flag()} {src} --emit-llvm -o {ll}`) { return false }
let objs = split_objs(ll, out) # on every core when it can (split.ludic) let objs = split_objs(ll, out) # on every core when it can (split.ludic)
var input = ll var input = ll
if objs != "" { input = objs } if objs != "" { input = objs }
@ -65,7 +65,7 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
return true return true
} }
if not shq(`{ludicc()} --windowed{title_flag()}{unsafe_flag()} {src} --emit-llvm -o {ll}`) { return false } if not shq(`{ludicc()} --windowed{title_flag()}{unsafe_flag()}{release_flag()} {src} --emit-llvm -o {ll}`) { return false }
# audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the # audio.ll (#22) is always linked here — unused snd_* are dead-stripped; the
# canonical `ludicc -o` path links it only when Audio.* is used. # canonical `ludicc -o` path links it only when Audio.* is used.
let cocoa = `{home}runtime/native/cocoa.ll` let cocoa = `{home}runtime/native/cocoa.ll`
@ -85,6 +85,12 @@ function compile_app(src: pointer, out: pointer, mode: int, save: bool) -> bool
# opens under that name rather than the `program` name; "" otherwise, and for a name the # opens under that name rather than the `program` name; "" otherwise, and for a name the
# shell line could not carry safely. # shell line could not carry safely.
# ` --unsafe` when the build said so: the project's own files may write `unsafe` (L7) # ` --unsafe` when the build said so: the project's own files may write `unsafe` (L7)
# ludic bundle: what ships is a release, and Build.schema_hash() is 0 in it
var g_release_build: bool = false
function release_flag() -> pointer {
if g_release_build { return " --release" }
return ""
}
var g_unsafe_build: bool = false var g_unsafe_build: bool = false
function unsafe_flag() -> pointer { function unsafe_flag() -> pointer {
if g_unsafe_build { return " --unsafe" } if g_unsafe_build { return " --unsafe" }

View file

@ -355,7 +355,7 @@ function cmd_bundle_windows() -> int {
# 1. the executable - staged under a name with no spaces, as on macOS # 1. the executable - staged under a name with no spaces, as on macOS
let res = windows_icon_res(manifest_app(m, "icon")) let res = windows_icon_res(manifest_app(m, "icon"))
let staged = tmp_path("bundle_exe") let staged = tmp_path("bundle_exe")
var flags = "--windowed --gui" var flags = "--windowed --gui --release"
if res != "" { flags = flags + ` --link "{res}"` } if res != "" { flags = flags + ` --link "{res}"` }
Os.set_env("LUDIC_OPT", "2") # a shipped game is -O2, whatever a developer's shell says Os.set_env("LUDIC_OPT", "2") # a shipped game is -O2, whatever a developer's shell says
if not shq(`{ludicc()} {flags}{title_flag()} {entry} --natives-out {staged}.natives -o {staged}`) { return 1 } if not shq(`{ludicc()} {flags}{title_flag()} {entry} --natives-out {staged}.natives -o {staged}`) { return 1 }
@ -441,6 +441,7 @@ function cmd_bundle() -> int {
# out of the path it is given. # out of the path it is given.
let exe = `{contents}/MacOS/{name}` let exe = `{contents}/MacOS/{name}`
let staged = `{tmp_dir()}/bundle_exe` let staged = `{tmp_dir()}/bundle_exe`
g_release_build = true
if not compile_app(entry, staged, 1, false) { return 1 } if not compile_app(entry, staged, 1, false) { return 1 }
if not shq(`cp {staged} "{exe}"`) { if not shq(`cp {staged} "{exe}"`) {
err("ludic bundle: could not place the executable\n") err("ludic bundle: could not place the executable\n")

View file

@ -90,6 +90,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/backend/emit_fnval.ludic") push(f, "selfhost/backend/emit_fnval.ludic")
push(f, "selfhost/backend/emit_vis.ludic") push(f, "selfhost/backend/emit_vis.ludic")
push(f, "selfhost/backend/emit_schema.ludic") push(f, "selfhost/backend/emit_schema.ludic")
push(f, "selfhost/backend/emit_schema_hash.ludic")
push(f, "selfhost/check/check_state.ludic") push(f, "selfhost/check/check_state.ludic")
push(f, "selfhost/check/check_index.ludic") push(f, "selfhost/check/check_index.ludic")
push(f, "selfhost/check/check_types.ludic") push(f, "selfhost/check/check_types.ludic")

View file

@ -1232,6 +1232,7 @@ function cmd_dev_test() -> int {
feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)") feat_case("lang/registry", "", "3 Crate true 2 fire_ring -1", "registry.ludic (L8: a registry filled by def declarations - index constants, a count, the key, a lookup)")
reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists") reject_case("rejected/registry_unknown", "there is no registry Tables", "a def names a registry that exists")
feat_case("lang/attributes", "", "2 1 Lantern 2", "attributes.ludic (editor attributes: @Ref, @OneOf, @Range, @Unit, @Asset, @Color, @AppendOnly, @ByKey - no effect on the program)") feat_case("lang/attributes", "", "2 1 Lantern 2", "attributes.ludic (editor attributes: @Ref, @OneOf, @Range, @Unit, @Asset, @Color, @AppendOnly, @ByKey - no effect on the program)")
feat_case("lang/schema_hash", "", "true", "schema_hash.ludic (Build.schema_hash(): FNV-1a 64 of the program's own schema, 0 in a release)")
reject_case("rejected/ref_unknown", "field Tool.seller: @Ref(Vendorz) names no registry", "@Ref names a registry that exists") reject_case("rejected/ref_unknown", "field Tool.seller: @Ref(Vendorz) names no registry", "@Ref names a registry that exists")
reject_case("rejected/node_bad", "field Tool.grip: @Node(handle) names no field", "@Node names a field of its record") reject_case("rejected/node_bad", "field Tool.grip: @Node(handle) names no field", "@Node names a field of its record")
schema_case("lang/attributes", "{\"name\": \"weight\", \"type\": \"float\", \"default\": \"1.0\", \"file\": \"examples/lang/attributes.ludic\", \"line\": 24, \"col\": 33, \"attributes\": [{\"name\": \"Range\", \"args\": [0, 20.5]}, {\"name\": \"Unit\", \"args\": [\"kg\"]}]", "--emit-schema: a field's type, default, place and attributes") schema_case("lang/attributes", "{\"name\": \"weight\", \"type\": \"float\", \"default\": \"1.0\", \"file\": \"examples/lang/attributes.ludic\", \"line\": 24, \"col\": 33, \"attributes\": [{\"name\": \"Range\", \"args\": [0, 20.5]}, {\"name\": \"Unit\", \"args\": [\"kg\"]}]", "--emit-schema: a field's type, default, place and attributes")