parser: an attribute before export is kept - @ToClients export event E was a local event (the attributes read in front of export were dropped when the declaration was parsed afresh one call down), as were @Sync / @Owned / @Server / @On / @Public ... before export; and export @ToClients event was refused. Attributes and export now read in either order into one declaration, and a wrapper marks what it added exported (g_at_keep goes); examples/networking/net_export and three schema cases (added, not run); reseeded, bootstrap-cfree fixpoint holds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 01:03:44 +03:00
parent 64adb052b4
commit 26b986de5b
7 changed files with 89034 additions and 88841 deletions

View file

@ -0,0 +1,7 @@
bump: patch
type: fix
**An attribute before `export` is kept.** `@ToClients export event E`, `@Sync export property P`,
`@Owned export model M` and the rest lost the attributes written in front of `export`: the parser read
them, then parsed the declaration afresh and forgot them - so an exported remote event was silently
local; and `export @ToClients event` was refused outright. Attributes and `export` now read in
either order into the same declaration.

View file

@ -0,0 +1,18 @@
# net_export.ludic - attributes and `export` in either order: `@ToClients export event` is as remote
# as `export @ToClients event`, and `@Sync export property` / `@Owned export model` keep theirs.
# (Before, the attributes read in front of `export` were dropped, and the event went local unsaid.)
program NetExport {
@ToClients
export event Said { x: int = 0 }
export @ToClients event Told { x: int = 0 }
@ToServer export event Asked { x: int = 0 }
@ToClients event Local { x: int = 0 }
@Sync export property Spot { x: int = 0 }
@Owned export model Walker { @Sync Spot }
handler Talk phase Update {
emit Said(x: 1)
emit Told(x: 1)
emit Asked(x: 1)
emit Local(x: 1)
}
}

View file

@ -36,7 +36,6 @@ var g_at_nargs: []int = new []int
var g_at_unres: []bool = new []bool # its target is not in this program (a warning; the schema says so) var g_at_unres: []bool = new []bool # its target is not in this program (a warning; the schema says so)
var g_at_open: int = -1 # the group the attributes being read go into var g_at_open: int = -1 # the group the attributes being read go into
var g_at_decl: []pointer = new []pointer # @AppendOnly / @ByKey read before a declaration var g_at_decl: []pointer = new []pointer # @AppendOnly / @ByKey read before a declaration
var g_at_keep: bool = false # `export` parses its declaration one call down
# a new group, empty; its index # a new group, empty; its index
function at_group() -> int { function at_group() -> int {
@ -165,10 +164,6 @@ function at_decl_with(a: pointer, args: pointer) -> void {
push(g_at_decl_args, args) push(g_at_decl_args, args)
} }
function at_decl_reset() -> void { function at_decl_reset() -> void {
if g_at_keep {
g_at_keep = false
return
}
if len(g_at_decl) > 0 { at_decl_clear() } if len(g_at_decl) > 0 { at_decl_clear() }
} }
function at_decl_clear() -> void { function at_decl_clear() -> void {

View file

@ -982,7 +982,36 @@ function decl_hook_target(ev: pointer, sp: pointer, dsp: pointer, att: pointer,
# parse one top-level declaration (or resolve an import) into `prog`. # parse one top-level declaration (or resolve an import) into `prog`.
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`, # Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords. # `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
var g_decl_exported: bool = false # `export` read in front of the declaration being parsed
function parse_one_decl() -> void { function parse_one_decl() -> void {
let p0 = len(prog)
let e0 = len(g_events)
let r0 = len(g_rg_var)
let outer = g_decl_exported
g_decl_exported = false
parse_one_decl_in()
if g_decl_exported { decl_mark_exported(p0, e0, r0) }
g_decl_exported = outer
}
# `export function f`, `export var v`, ...: what the declaration added is visible from other modules (L3)
function decl_mark_exported(p0: int, e0: int, r0: int) -> void {
var k = p0
while k < len(prog) {
prog[k].vis = 1
k += 1
}
k = r0
while k < len(g_rg_var) { # a @PerMap registry's own node is not in prog
g_rg_var[k].vis = 1
k += 1
}
k = e0
while k < len(g_events) {
g_events[k].vis = 1
k += 1
}
}
function parse_one_decl_in() -> void {
at_decl_reset() # attrs.ludic: @AppendOnly / @ByKey belong to one declaration at_decl_reset() # attrs.ludic: @AppendOnly / @ByKey belong to one declaration
var is_export = false var is_export = false
var qspec: Node = null var qspec: Node = null
@ -1005,7 +1034,14 @@ function parse_one_decl() -> void {
var esys_phase: pointer = null var esys_phase: pointer = null
var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62) var ns_name: pointer = null # @Namespace(Name) — a package-provided Foo.* namespace (#62)
var is_det = false # @deterministic — no floating point inside (emit_float.ludic) var is_det = false # @deterministic — no floating point inside (emit_float.ludic)
while is_op("@") { # attributes and `export` in any order: `@ToClients export event E` keeps its @ToClients
while is_op("@") or (is_id("export") and (toks[pi + 1].kind == TK_ID or toks[pi + 1].text == "@")) {
if is_id("export") {
pi += 1
g_decl_exported = true
skipnl()
continue
}
pi += 1; let a = eat_id() # collect a leading @annotation pi += 1; let a = eat_id() # collect a leading @annotation
if a == "export" { is_export = true } if a == "export" { is_export = true }
else if a == "Public" { is_public = true } # @Public hook promotion else if a == "Public" { is_public = true } # @Public hook promotion
@ -1045,7 +1081,7 @@ function parse_one_decl() -> void {
let why = expr() let why = expr()
if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") } if why.kind != E_STR or len(why.s) == 0 { perr("@alloc_ok needs its reason: @alloc_ok(\"a memo miss, bounded by MM_CAP\")") }
eat_op(")") eat_op(")")
g_aok_pending = true # a global: `export` parses the declaration one call down g_aok_pending = true # a global: the declaration after the attributes reads it
} }
else if a == "creates" or a == "releases" { # 25.5e: @creates(PhysShape) / @releases(PhysShape) else if a == "creates" or a == "releases" { # 25.5e: @creates(PhysShape) / @releases(PhysShape)
eat_op("(") eat_op("(")
@ -1073,31 +1109,6 @@ function parse_one_decl() -> void {
mod_parse_line() # module NAME [uses A, B] mod_parse_line() # module NAME [uses A, B]
return return
} }
# `export function f`, `export var v`, ...: visible from other modules (L3)
if is_id("export") and (toks[pi + 1].kind == TK_ID) {
pi += 1
g_at_keep = true
let p0 = len(prog)
let e0 = len(g_events)
let r0 = len(g_rg_var)
parse_one_decl()
var k = p0
while k < len(prog) {
prog[k].vis = 1
k += 1
}
k = r0
while k < len(g_rg_var) { # a @PerMap registry's own node is not in prog
g_rg_var[k].vis = 1
k += 1
}
k = e0
while k < len(g_events) {
g_events[k].vis = 1
k += 1
}
return
}
if is_id("numbers") and (toks[pi + 1].text == "float") { if is_id("numbers") and (toks[pi + 1].text == "float") {
pi += 2 pi += 2
if not is_float_file(g_parse_file) { float_file_add(g_parse_file) } if not is_float_file(g_parse_file) { float_file_add(g_parse_file) }

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -1365,6 +1365,9 @@ function cmd_dev_test() -> int {
schema_case("modules/friend_of", "{\"name\": \"lab\", \"package\": null, \"layer\": null, \"uses\": null, \"uses_at\": null, \"friend\": {\"of\": [\"bank\"]}", "--emit-schema: code.modules - a friend module and whose names it sees") schema_case("modules/friend_of", "{\"name\": \"lab\", \"package\": null, \"layer\": null, \"uses\": null, \"uses_at\": null, \"friend\": {\"of\": [\"bank\"]}", "--emit-schema: code.modules - a friend module and whose names it sees")
schema_case("networking/net_demo", "{\"name\": \"Move\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:24:19\", \"cancellable\": false, \"net\": \"toserver\", \"emits\": [\"examples/networking/net_demo.ludic:32:10\"], \"listeners\": [{\"handler\": \"DoMove\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:25:21\"}]}", "--emit-schema: code.events - a remote event, its emit sites and @On listeners") schema_case("networking/net_demo", "{\"name\": \"Move\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:24:19\", \"cancellable\": false, \"net\": \"toserver\", \"emits\": [\"examples/networking/net_demo.ludic:32:10\"], \"listeners\": [{\"handler\": \"DoMove\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:25:21\"}]}", "--emit-schema: code.events - a remote event, its emit sites and @On listeners")
schema_case("networking/net_demo", "{\"name\": \"Ship\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:22:16\", \"owned\": true, \"properties\": [{\"name\": \"Pos\", \"sync\": true}]}", "--emit-schema: code.models - an @Owned model and its @Sync property") schema_case("networking/net_demo", "{\"name\": \"Ship\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:22:16\", \"owned\": true, \"properties\": [{\"name\": \"Pos\", \"sync\": true}]}", "--emit-schema: code.models - an @Owned model and its @Sync property")
schema_case("networking/net_export", "\"name\": \"Said\", \"module\": null, \"at\": \"examples/networking/net_export.ludic:6:16\", \"cancellable\": false, \"net\": \"toclients\"", "--emit-schema: an attribute before `export` is kept - @ToClients export event is remote")
schema_case("networking/net_export", "\"name\": \"Asked\", \"module\": null, \"at\": \"examples/networking/net_export.ludic:8:26\", \"cancellable\": false, \"net\": \"toserver\"", "--emit-schema: @ToServer export event on one line is remote too")
schema_case("networking/net_export", "\"owned\": true, \"properties\": [{\"name\": \"Spot\", \"sync\": true}]", "--emit-schema: @Owned export model and @Sync export property keep theirs")
schema_case("networking/net_roles", "\"phase\": \"Update\", \"hook\": null, \"target\": null, \"public\": false, \"net\": \"server\"", "--emit-schema: code.handlers - an @Server handler's phase and net role") schema_case("networking/net_roles", "\"phase\": \"Update\", \"hook\": null, \"target\": null, \"public\": false, \"net\": \"server\"", "--emit-schema: code.handlers - an @Server handler's phase and net role")
schema_case("lang/annotations", "\"queries\": {\"these\": [{\"property\": \"Transform\", \"filter\": \"scale > 0\"}, {\"property\": \"Velocity\", \"filter\": \"dx > 0 or dy > 0\"}], \"on\": \"Actor\"}", "--emit-schema: code.handlers - @Queries' terms, filters and model") schema_case("lang/annotations", "\"queries\": {\"these\": [{\"property\": \"Transform\", \"filter\": \"scale > 0\"}, {\"property\": \"Velocity\", \"filter\": \"dx > 0 or dy > 0\"}], \"on\": \"Actor\"}", "--emit-schema: code.handlers - @Queries' terms, filters and model")
schema_case("lang/annotations", "\"phase\": null, \"hook\": \"OnSpawn\", \"target\": \"Actor\"", "--emit-schema: code.handlers - a lifecycle hook and the model it names") schema_case("lang/annotations", "\"phase\": null, \"hook\": \"OnSpawn\", \"target\": \"Actor\"", "--emit-schema: code.handlers - a lifecycle hook and the model it names")