Merge lang/foundations (9391695) into lang/schemacode: LANGUAGE.md's command list keeps --stdin-file (R10), the code map in schema (R7) and ludic syntax (R8); selfhost/main.ludic carries all three flags; reseeded from the merged source, bootstrap-cfree fixpoint holds (298653 lines), syntax --check clean, no conflict markers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-30 00:21:47 +03:00
commit d39a27a904
81 changed files with 114027 additions and 104730 deletions

View file

@ -75,6 +75,7 @@ point at a different LLVM toolchain if you have one.
| the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` | | the IR, to read | `ludicc src.ludic --emit-llvm -o src.ll` |
| the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` | | the schema an editor reads (records, registries and their entries, consts) | `ludicc src.ludic --emit-schema schema.json` |
| every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` | | every error, as a JSON array on stdout | `ludicc src.ludic --check --diagnostics=json` |
| the same, with an unsaved buffer on stdin standing for one of its files | `ludicc src.ludic --check --diagnostics=json --stdin-file lib/a.ludic < buf` |
| a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` | | a shared library † | `ludicc lib.ludic --shared -o build/liblib.dylib` |
| a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` | | a game that runs in a browser † | `ludicc game.ludic --target wasm32-unknown-unknown -o build/web/game.wasm` |
| an object file † | `ludicc src.ludic -c -o src.o` | | an object file † | `ludicc src.ludic -c -o src.o` |

View file

@ -2360,7 +2360,9 @@ ludic test -j 4 # four tests at once (default: one per
ludic test packages/ludic.base # the test programs under a directory (a package's) ludic test packages/ludic.base # the test programs under a directory (a package's)
ludic build --check # every check a build makes, nothing written ludic build --check # every check a build makes, nothing written
ludic build --check --diagnostics=json # the same, every error as a JSON array on stdout ludic build --check --diagnostics=json # the same, every error as a JSON array on stdout
ludic build --check --diagnostics=json --stdin-file src/a.ludic < buf # stdin in place of src/a.ludic
ludic schema -o build/schema.json # records, registries, entries, consts, components, natives, lang and the code map, for editors ludic schema -o build/schema.json # records, registries, entries, consts, components, natives, lang and the code map, for editors
ludic syntax --json # the language's vocabulary: keywords, declarations, types, phases, attributes, operators
ludic deps # how tangled the modules are, as the compiler resolved them ludic deps # how tangled the modules are, as the compiler resolved them
ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them) ludic deps --check tests/deps-baseline.txt # fail when a number rose (--baseline FILE writes them)
@ -2382,6 +2384,17 @@ type error is in it, every `@Ref` naming no registry, and every module rule brok
unknown name while lowering, a def into a registry that does not exist - ends the array, and type unknown name while lowering, a def into a registry that does not exist - ends the array, and type
errors end it before the module rules are looked at. errors end it before the module rules are looked at.
`--stdin-file <path>` (`ludicc --check --stdin-file <path>`; on `ludic build` it implies `--check`)
checks an unsaved buffer: the program is the usual one - the package's entry, or the file named - but
wherever the compiler would open `<path>` it reads the text on stdin instead, read once and whole (an
empty stdin is an empty file). That is any file it opens: the entry, an import reached through a
barrel, a component's `.xml` or `.lss`, an `.lres`. The two paths are compared after normalising
both - `\` made `/`, a relative path put under the directory the compiler was started in, `.`, `..`
and doubled `/` folded, and case on Windows - so `./src/foo/../foo/bar.ludic` is `src/foo/bar.ludic`.
Diagnostics name the file as a check of the saved file would, with lines and columns in the
buffer's text. A `<path>` the program never opens is one warning on that path, rather than a clean
report on the saved files.
`ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every `ludic deps` compiles the program (the package's entry, or a file) with the compiler recording every
reference its visibility pass resolves - from the module it is written in to the module of what it reference its visibility pass resolves - from the module it is written in to the module of what it
names - and every assignment to another module's global. It prints five numbers: `modules` (the names - and every assignment to another module's global. It prints five numbers: `modules` (the
@ -2476,6 +2489,16 @@ Plugins for VS Code and JetBrains IDEs, plus configuration for Neovim, Helix,
Emacs, Sublime and Zed, are in `tools/editors/` — see Emacs, Sublime and Zed, are in `tools/editors/` — see
[tools/editors/README.md](tools/editors/README.md). [tools/editors/README.md](tools/editors/README.md).
The vocabulary every editor highlights with is the compiler's: `ludic syntax
--json` (`ludicc --emit-syntax`) prints every keyword with its role and whether
the parser reserves it, every declaration's form, the built-in types and phases,
every attribute with what it goes on, its arguments and what it means, the
operators and the literal forms. The grammars in `tools/editors/`, the language
server's word lists and `ludic_syntax.h` are written from it between marked lines
(`bin/ludic-dev syntax`), and `bin/ludic-dev syntax --check` fails when one falls
behind, when docs/language lacks a page for an entry, or when the parser tests a
word the vocabulary does not have.
## Working programs ## Working programs
- `examples/games/chronorift.ludic` — a co-op JRPG (overworld, dungeon, boss, shop, - `examples/games/chronorift.ludic` — a co-op JRPG (overworld, dungeon, boss, shop,

View file

@ -0,0 +1,9 @@
bump: minor
type: feat
**Check an unsaved buffer.** `ludic build --check --diagnostics=json --stdin-file <path>` (and
`ludicc --check --stdin-file <path>`) checks the program as usual, but wherever the compiler would open
`<path>` - the entry, an import reached through a barrel, a component's `.xml` / `.lss`, an `.lres` -
it reads the text on stdin instead, so an editor's diagnostics follow typing without a save. Paths are
matched after normalising both (separators, relative to the working directory, `.` / `..` folded).
Diagnostics carry the file's usual name with lines and columns in the buffer; a `<path>` the program
never opens is reported as one warning.

View file

@ -0,0 +1,25 @@
bump: minor
type: feat
**One vocabulary, from the compiler, for every editor.** `ludicc --emit-syntax` (`ludic syntax [--json] [-o FILE]`)
prints the language's words from the compiler's own table (`selfhost/frontend/vocab.ludic`): every keyword with its
role (declaration, modifier, statement, operator, constant) and whether the parser reserves it, every declaration's
form, the built-in types and phases, every attribute with what it goes on, its arguments and a one-line doc
(`@Ref`, `@OneOf`, `@Range`, `@Unit`, `@Asset`, `@Color`, `@Node`, `@Clip`, `@Material`, `@Tint`, `@Derived`,
`@Text`, `@Multiline`, `@Key`, `@AppendOnly`, `@ByKey`, `@PerMap`, `@Chunked`, `@max`, `@owns`, `@frame`, `@Sync`,
`@Computed`, `@alloc_ok`, ...), the operators and the literal forms. `"syntax_version": 1`. The emitter asks the
parser's own recognisers of every row and refuses one they disagree with.
`ludic-dev syntax` writes the keyword, type and phase lists of `ludic_syntax.h`, the TextMate grammar, the JetBrains
lexer, `ludic-mode.el` and the language server between marked lines; `ludic-dev syntax --check` (also run by
`check-vocabulary` and the regression suite) fails when one falls behind or lacks a word, when docs/language has no
page for a keyword, type, phase or attribute, or when the parser tests a word or reads an attribute the vocabulary
lacks. The grammars gain `module uses port bind action reducer dispatch registry def open component prop view alias
friend unsafe numbers of as from mut system` and more; docs/language gains a page for each of them and for every
editor, registry and memory attribute.
The language server's outline names every declaration by its own kind: `export property` / `state` are structs with
every field as a child (a field's default no longer swallows the fields after it), `event` / `action` events, `port`
an interface, `registry`, `enum` with its members, `component` / `view` with their props, state and events, and
`reducer S on A`. Hover and go-to-definition resolve an attribute's argument - `@Ref(Kits)` to the registry,
`@Node(model)` to the field it names - and hover on the attribute shows its reference page; a `fn(...) -> T` field
shows its whole type.

View file

@ -0,0 +1,12 @@
---
id: annot-alloc_ok
name: @alloc_ok
category: annotations
kind: annotation
tokens: @alloc_ok
sig: @alloc_ok("why") function f() / @alloc_ok("why") statement
tip: Allocates in a frame on purpose; the reason is required.
order: 80
---
<code>@alloc_ok("a memo miss, bounded by MM_CAP")</code> on a function or a single statement marks an allocation reachable from a frame as deliberate: the fence lets it through and <code>ludic deps</code> does not count it. The reason is required and greppable.

View file

@ -0,0 +1,18 @@
---
id: annot-appendonly
name: @AppendOnly
category: annotations
kind: annotation
tokens: @AppendOnly @ByKey
sig: @AppendOnly @ByKey registry Name of Record ...
tip: How a registry's entries may change: only appended (their index is saved), or saved by key.
order: 74
---
<code>@AppendOnly</code> on a registry says an entry's index is stored somewhere that outlives the build, so entries are only ever appended; <code>@ByKey</code> says entries are saved by key, so their order is free. Editors keep to what each says.
```ludic
# doc-check: skip — the file it names is beside the example
@AppendOnly @ByKey
registry Tools of Tool as TL from "data/tools.lres"
```

View file

@ -0,0 +1,12 @@
---
id: annot-asset
name: @Asset
category: annotations
kind: annotation
tokens: @Asset
sig: @Asset("kind"[, map[, optional]]) field: string = ""
tip: A path to a file of that kind; with map, under each map's directory.
order: 67
---
<code>@Asset("gltf")</code> says a string field names a file of that kind. With <code>map</code> the path is under each map's directory and <code>ludicc --check</code> looks for it in every map, refusing one that lacks it unless the field says <code>optional</code>.

View file

@ -3,7 +3,7 @@ id: annot-clearcolor
name: "@ClearColor" name: "@ClearColor"
category: annotations category: annotations
kind: annotation kind: annotation
tokens: "@ClearColor" tokens: @ClearColor
sig: "@ClearColor(0xRRGGBB) handler Draw phase Render { … }" sig: "@ClearColor(0xRRGGBB) handler Draw phase Render { … }"
tip: Declare a clear colour so the Render phase auto-clears + auto-presents for you. tip: Declare a clear colour so the Render phase auto-clears + auto-presents for you.
order: 62 order: 62

View file

@ -0,0 +1,12 @@
---
id: annot-color
name: @Color
category: annotations
kind: annotation
tokens: @Color
sig: @Color field: int = 0
tip: The field is a colour.
order: 68
---
<code>@Color</code> marks an <code>int</code> field as a colour so an editor shows a swatch and a picker; with <code>@Tint(SLOT)</code> beside it the colour is for that tint slot.

View file

@ -0,0 +1,12 @@
---
id: annot-derived
name: @Derived
category: annotations
kind: annotation
tokens: @Derived
sig: @Derived field: float = 0.0
tip: Worked out at boot: anything written in the data is overwritten.
order: 71
---
<code>@Derived</code> tells an editor a field is computed when the game starts, so a value typed into the data would be overwritten and is not offered for editing.

View file

@ -0,0 +1,12 @@
---
id: annot-deterministic
name: @deterministic
category: annotations
kind: annotation
tokens: @deterministic
sig: @deterministic function f() { }
tip: No floating point inside: it must replay the same everywhere.
order: 79
---
<code>@deterministic</code> on a function or a handler refuses floating point inside it, so it computes the same bits on every machine - what lockstep and replays need.

View file

@ -0,0 +1,12 @@
---
id: annot-frame
name: @frame
category: annotations
kind: annotation
tokens: @frame
sig: @frame field: fn() -> void
tip: A fn stored here runs every frame.
order: 76
---
<code>@frame</code> on a function-typed field says whatever function it holds runs every frame, so <code>ludic deps</code> counts what it can allocate among the frame's allocations.

View file

@ -0,0 +1,12 @@
---
id: annot-key
name: @Key
category: annotations
kind: annotation
tokens: @Key
sig: @Key field: int = 0
tip: The field is a key code.
order: 73
---
<code>@Key</code> marks an <code>int</code> field as a key code, so an editor offers a key to press rather than a number.

View file

@ -0,0 +1,18 @@
---
id: annot-max
name: @max
category: annotations
kind: annotation
tokens: @max
sig: @max(64) rows: []Row
tip: The most a list may hold; growing past it fails the run.
order: 77
---
<code>@max(n)</code> bounds a list field: it never holds more than <code>n</code>, and a push past it fails the run (exit 87) rather than growing a list that should not grow.
```ludic
property Log {
@max(64) lines: []int
}
```

View file

@ -0,0 +1,12 @@
---
id: annot-node
name: @Node
category: annotations
kind: annotation
tokens: @Node @Clip @Material
sig: @Node(model) / @Clip(model) / @Material(model) field: string = ""
tip: A node, an animation clip or a material inside the glTF another field names.
order: 69
---
<code>@Node(model)</code>, <code>@Clip(model)</code> and <code>@Material(model)</code> say a string field names a node, a clip or a material inside the glTF that field <code>model</code> of the same record names (an <code>@Asset("gltf")</code> field, or an <code>@Ref</code> to a registry whose record has exactly one). Naming anything else is an error.

View file

@ -0,0 +1,12 @@
---
id: annot-oneof
name: @OneOf
category: annotations
kind: annotation
tokens: @OneOf
sig: @OneOf(PREFIX_) / @OneOf(A, B) / @OneOf("word", ...)
tip: The field holds one of these constants or words.
order: 64
---
<code>@OneOf(GR_)</code> says a field holds one of the constants whose names start <code>GR_</code>; <code>@OneOf(GR_GOLD, GR_SILVER)</code> one of those (each must exist); on a <code>string</code> field, <code>@OneOf("box", "hull")</code> one of those words, and every registry row is checked against them.

View file

@ -0,0 +1,12 @@
---
id: annot-owns
name: @owns
category: annotations
kind: annotation
tokens: @owns @creates @releases
sig: @owns(Kind) field / @creates(Kind) function / @releases(Kind) function
tip: Resource handles: a field that owns one, a function that makes one, a function that lets one go.
order: 78
---
<code>@creates(PhysShape)</code> on a function says it returns a handle the caller must release, <code>@releases(PhysShape)</code> that it releases one, and <code>@owns(PhysShape)</code> on a field that its record owns the handle it holds. <code>ludic deps --resources</code> counts a created handle never released, and an owned one lost when its record is released.

View file

@ -0,0 +1,12 @@
---
id: annot-permap
name: @PerMap
category: annotations
kind: annotation
tokens: @PerMap @Chunked
sig: @PerMap registry Name of Row from "rows.lres" / @PerMap @Chunked(n) registry ...
tip: A table whose rows are read per map from that map's directory; @Chunked(n) reads it a chunk at a time.
order: 75
---
A <code>@PerMap</code> registry holds what is on a map rather than in the game: its rows are read when a map loads, from that map's directory, into a state the compiler writes with its verbs (<code>_load</code>, <code>_find</code>, ...). <code>@Chunked(n)</code> reads it in n-by-n-metre chunks from files named by <code>{cx}</code> and <code>{cz}</code>.

View file

@ -0,0 +1,12 @@
---
id: annot-range
name: @Range
category: annotations
kind: annotation
tokens: @Range
sig: @Range(lo, hi) field: float = 0.0
tip: The least and the most the value may be.
order: 65
---
<code>@Range(0, 20.5)</code> gives an editor the bounds of a number field, and <code>ludicc --check</code> holds every map row's value to them. Both arguments are numbers.

View file

@ -0,0 +1,19 @@
---
id: annot-ref
name: @Ref
category: annotations
kind: annotation
tokens: @Ref
sig: @Ref(Registry) field: int = 0
tip: The field is an entry of that registry: an index, or a key for a @PerMap one.
order: 63
---
<code>@Ref(Items)</code> on a field says its value is an entry of the registry <code>Items</code> - its index on an <code>int</code> field, its key on a <code>string</code> field of a <code>@PerMap</code> table - so an editor offers the entries. It changes nothing the program does; a registry the program does not declare is a warning and <code>"unresolved"</code> in the schema, anything else of that name is an error.
```ludic
# doc-check: skip — the registry is declared elsewhere
property Tool {
@Ref(Vendors) seller: int = 0
}
```

View file

@ -0,0 +1,12 @@
---
id: annot-text
name: @Text
category: annotations
kind: annotation
tokens: @Text @Multiline
sig: @Text name: Key = null / @Text @Multiline blurb: string = ""
tip: Read by the player, so translated; and prose, edited over several lines.
order: 72
---
<code>@Text</code> marks what the player reads, so it is translated. On a <code>Key</code> field a row that gives no value gets its derived key, <code>&lt;registry&gt;.&lt;row&gt;.&lt;field&gt;</code> (see <code>@TextKey</code>); English still written in a <code>@Text</code> row is counted as <code>english_left</code>. <code>@Multiline</code> says it is prose, edited as several lines rather than one.

View file

@ -0,0 +1,12 @@
---
id: annot-textkey
name: @TextKey
category: annotations
kind: annotation
tokens: @TextKey
sig: @TextKey("steps") registry StepKinds of StepKind from "steps.lres"
tip: The prefix of a registry's derived text keys, in place of its name in snake case.
order: 73
---
A registry's <code>@Text</code> fields are keyed <code>&lt;registry&gt;.&lt;row&gt;.&lt;field&gt;</code>, the registry's name in snake case (<code>GearKinds</code> is <code>gear_kinds</code>). <code>@TextKey("steps")</code> names the prefix instead, so its rows' keys are <code>steps.&lt;row&gt;.&lt;field&gt;</code>. Every derived key is checked against the source language's <code>.po</code> (LANGUAGE.md, Text keys).

View file

@ -0,0 +1,12 @@
---
id: annot-tint
name: @Tint
category: annotations
kind: annotation
tokens: @Tint
sig: @Color @Tint(SLOT) field: int = 0
tip: A colour for that tint slot.
order: 70
---
<code>@Tint(TSLOT_HAIR)</code> names the tint slot a colour field paints - a constant, which must exist (or, when the program lacks it, is a warning and unresolved).

View file

@ -0,0 +1,12 @@
---
id: annot-unit
name: @Unit
category: annotations
kind: annotation
tokens: @Unit
sig: @Unit("m/s") field: float = 0.0
tip: The value's unit, in one canonical ASCII spelling.
order: 66
---
<code>@Unit</code> takes one of <code>m</code>, <code>m/s</code>, <code>m/s2</code>, <code>s</code>, <code>min</code>, <code>h</code>, <code>d</code>, <code>deg</code>, <code>rad</code>, <code>rad/s</code>, <code>kg</code>, <code>N</code>, <code>N.m</code>, <code>%</code>, <code>px</code>, so one word means one unit to an editor; another spelling is a warning naming the canonical one.

View file

@ -0,0 +1,17 @@
---
id: kw-dispatch
name: dispatch
category: control
kind: keyword
tokens: dispatch
sig: dispatch Action(field: value, ...)
tip: Queues an action for the reducers declared on it.
order: 13
---
<code>dispatch Move(dx: 1)</code> queues an action; at the end of the frame phase every <code>reducer</code> on <code>Move</code> applies it to its own state, in the order of the states' names. Input code and a screen's buttons dispatch; they never write another module's state.
```ludic
# doc-check: skip — the action is declared elsewhere
dispatch Move(dx: 1)
```

View file

@ -0,0 +1,12 @@
---
id: kw-system
name: system
category: ecs
kind: keyword
tokens: system
sig: disable system SystemFunction
tip: Leaves an engine system out at compile time.
order: 51
---
<code>disable system S</code> names a package's engine system (declared with <code>@EngineSystem</code>) and leaves it out of the program at compile time, for a game that drives that component itself.

View file

@ -0,0 +1,12 @@
---
id: kw-true
name: true
category: operators
kind: keyword
tokens: true false null
sig: true false null
tip: The boolean literals, and the absent record, slice or string.
order: 13
---
<code>true</code> and <code>false</code> are the two <code>bool</code> values; <code>null</code> is no record, no slice and no string - what an unset reference field holds and what a lookup that finds nothing returns.

View file

@ -0,0 +1,12 @@
---
id: kw-lasts
name: lasts
category: scenes
kind: keyword
tokens: lasts
sig: scene Name lasts N then Next { }
tip: A timed scene: after N seconds it moves on.
order: 52
---
<code>lasts N then Next</code> makes a scene timed - a banner, a splash - that moves to <code>Next</code> by itself once <code>N</code> seconds have passed.

View file

@ -0,0 +1,12 @@
---
id: kw-loads
name: loads
category: scenes
kind: keyword
tokens: loads
sig: scene Name loads then Next { }
tip: A loading scene: pumps the asset queue with a bar, then moves on.
order: 54
---
<code>loads then Next</code> makes a scene the loading screen: the engine pumps the asset queue, draws a progress bar and enters <code>Next</code> when everything queued is ready.

View file

@ -0,0 +1,12 @@
---
id: kw-shows
name: shows
category: scenes
kind: keyword
tokens: shows
sig: scene Name shows Menu { }
tip: The ui this scene opens, renders and closes by itself.
order: 51
---
<code>shows Menu</code> in a scene's header hands a <code>ui</code> to the engine: it is opened when the scene is entered, drawn every frame and closed when it is left, so the scene needs no handler for it.

View file

@ -0,0 +1,12 @@
---
id: kw-then
name: then
category: scenes
kind: keyword
tokens: then
sig: scene Name lasts N then Next / scene Name loads then Next
tip: The scene a timed or loading scene moves on to.
order: 53
---
<code>then</code> names where a scene goes when it is done: after the time of a <code>lasts</code> scene, or once a <code>loads</code> scene's assets are ready.

View file

@ -0,0 +1,16 @@
---
id: kw-action
name: action
category: structure
kind: keyword
tokens: action
sig: action Name { field: Type, ... }
tip: What the player asked for, dispatched and applied by each state's reducer.
order: 61
---
An <code>action</code> is a record that says what was asked for in the game's words (<code>Move</code>, <code>OpenPack</code>). Input code <code>dispatch</code>es it, and every <code>reducer</code> declared on it changes its own state. The queue drains at the end of every frame phase, each action's reducers in the order of their states' names.
```ludic
action Move { dx: int = 0, dy: int = 0 }
```

View file

@ -0,0 +1,19 @@
---
id: kw-alias
name: alias
category: structure
kind: keyword
tokens: alias
sig: namespace Name { alias method = function_name }
tip: A namespace method that is another function.
order: 69
---
Inside a <code>namespace</code> block, <code>alias m = f</code> makes <code>Name.m(...)</code> a call of <code>f</code>, checked against its parameters. It is how an engine namespace declared in Ludic forwards to the functions that implement it.
```ludic
# doc-check: skip — the aliased function lives elsewhere
namespace Trail {
alias length = trail_length
}
```

View file

@ -0,0 +1,12 @@
---
id: kw-as
name: as
category: structure
kind: keyword
tokens: as
sig: registry Name of Record as PREFIX
tip: The prefix of a registry's generated constants.
order: 65
---
<code>as P</code> after a registry's record makes the compiler write a constant for each entry, <code>P_KEY</code> in upper case, holding the entry's index - so code reads <code>IT_ROPE</code> rather than looking the key up.

View file

@ -0,0 +1,17 @@
---
id: kw-bind
name: bind
category: structure
kind: keyword
tokens: bind
sig: bind Port { member: function_name, ... }
tip: The program's answers to a port: one function per member.
order: 60
---
<code>bind</code> answers a <code>port</code>: each member names a function of the member's type. It is written once, where the program is put together, and it is the only code that knows both the asking module and the one that answers.
```ludic
# doc-check: skip — a module spans files
bind ClockWorld { rest_scale: party_rest_scale }
```

View file

@ -0,0 +1,21 @@
---
id: kw-component
name: component
category: structure
kind: keyword
tokens: component
sig: component Name[(states)] { prop p: T = v, state s: T = v, function ..., on e(...) { } }
tip: A UI component: its props, state, functions and events, beside Name.xml and Name.lss.
order: 70
---
A <code>component</code> is a piece of interface: the code file declares its <code>prop</code>s (handed in by its parent), its own <code>state</code>, the functions its template calls and the events it handles with <code>on</code>; <code>Name.xml</code> beside it is its template and <code>Name.lss</code> its styles. The states named in its header are supplied by the runtime and never seen by the template.
```ludic
# doc-check: skip — a component needs its template beside it
component Counter {
prop step: int = 1
state count: int = 0
on add() { count = count + step }
}
```

View file

@ -0,0 +1,17 @@
---
id: kw-def
name: def
category: structure
kind: keyword
tokens: def
sig: def Registry key { field: value, ... } / def Registry from "file.lres"
tip: Entries of a registry, written in code or read from a resource file.
order: 67
---
<code>def</code> adds entries to a registry: one written inline, or every entry of a resource file. An <code>open</code> registry takes <code>def</code>s from other modules, which is how a package's table gets a game's rows.
```ludic
# doc-check: skip — the registry is declared elsewhere
def Tools lantern { weight: 1.5 }
```

View file

@ -0,0 +1,17 @@
---
id: kw-export
name: export
category: structure
kind: keyword
tokens: export
sig: export function / property / state / registry ...
tip: Makes a declaration visible outside its module.
order: 54
---
<code>export</code> in front of a declaration makes it reachable from other modules; without it a module's names are its own. It works on every declaration - functions, records, states, events, actions, ports, registries, views and components - and <code>@export</code> is the same thing written as an attribute. Export deliberately: a name nobody else asks for stays private.
```ludic
# doc-check: skip — a module spans files
export function balance(b: Bank) -> int { return b.total }
```

View file

@ -0,0 +1,17 @@
---
id: kw-friend
name: friend
category: structure
kind: keyword
tokens: friend
sig: friend module name [of a, b]
tip: A module that sees other modules' private names - the lab, the tests.
order: 53
---
<code>friend module lab</code> declares a module that may name every private name of every module; <code>friend module lab of bank, sky</code> limits it to those. It is for test and staging code that must reach inside a system without the system exporting its internals.
```ludic
# doc-check: skip — a module spans files
friend module lab of bank, sky
```

View file

@ -0,0 +1,12 @@
---
id: kw-from
name: from
category: structure
kind: keyword
tokens: from
sig: registry ... from "file.lres" / def Registry from "file.lres"
tip: The resource file a registry's entries are read from.
order: 66
---
<code>from</code> names the <code>.lres</code> file the compiler reads a registry's entries from, relative to the declaring file (or, in a <code>@PerMap</code> registry, to each map's directory). The entries are checked against the record at build time, and an entry's place in the file is its index.

View file

@ -0,0 +1,19 @@
---
id: kw-internal
name: internal
category: structure
kind: keyword
tokens: internal
sig: namespace Name { internal function helper() { } }
tip: Inside a namespace: a function kept out of the Name.* surface.
order: 55
---
In a <code>namespace</code> block every function is part of the <code>Name.*</code> surface unless it says <code>internal</code>: then it is emitted as an ordinary helper the namespace's own methods can call, and <code>Name.helper</code> is not a method.
```ludic
namespace Trail {
internal function step(n: int) -> int { return n + 1 }
function next(n: int) -> int { return step(n) }
}
```

View file

@ -0,0 +1,19 @@
---
id: kw-module
name: module
category: structure
kind: keyword
tokens: module
sig: module name [in layer L] [uses a, b]
tip: Names the module a directory's files belong to; only what it exports is reachable from outside.
order: 51
---
<code>module</code> opens a directory's barrel (<code>index.ludic</code>) and says which module every file under it belongs to. A name a module does not <code>export</code> is private to it: another module that names it is refused at compile time, which is what makes a private name safe to change. The line may place the module in a layer (<code>in layer L</code>) and say what it may reach (<code>uses</code>).
```ludic
# doc-check: skip — a module spans files
# bank/index.ludic
module bank uses base
import "ledger.ludic"
```

View file

@ -0,0 +1,17 @@
---
id: kw-mut
name: mut
category: structure
kind: keyword
tokens: mut
sig: function f(st: mut State)
tip: A parameter the function may change - how a state is written.
order: 58
---
A state reaches a function only as a parameter: <code>h: HikerState</code> to read it, <code>h: mut HikerState</code> to change it, so a function's signature is everything it touches. The compiler refuses a write through a parameter that is not <code>mut</code>, and <code>ludic migrate state --tighten</code> takes <code>mut</code> off every one nothing down the chain writes.
```ludic
state Counter { n: int = 0 }
function bump(c: mut Counter) -> void { c.n = c.n + 1 }
```

View file

@ -0,0 +1,18 @@
---
id: kw-numbers
name: numbers
category: structure
kind: keyword
tokens: numbers
sig: numbers float
tip: This file's bare decimals are floats.
order: 56
---
<code>numbers float</code> at the top of a file makes a bare decimal such as <code>1.5</code> a <code>float</code> rather than a <code>fixed</code>. Such a file also refuses to promote a computed <code>int</code> silently: write <code>float(n)</code> where a count becomes a number.
```ludic
# doc-check: skip — a file-level line
numbers float
const GRAVITY: float = 9.81
```

View file

@ -0,0 +1,12 @@
---
id: kw-of
name: of
category: structure
kind: keyword
tokens: of
sig: registry Name of Record / friend module m of a, b
tip: Says what a registry holds, or whose private names a friend module sees.
order: 64
---
<code>of</code> names the record a <code>registry</code>'s entries are (<code>registry Tools of Tool</code>), and, on a <code>friend module</code> line, the modules whose private names it may see.

View file

@ -0,0 +1,12 @@
---
id: kw-open
name: open
category: structure
kind: keyword
tokens: open
sig: open registry Name of Record
tip: A registry other modules may add entries to with def.
order: 68
---
An <code>open registry</code> is one whose entries may come from other modules' <code>def</code>s, merged in a deterministic order; a closed one takes entries only from its own module. A package declares its table open so the game can fill it.

View file

@ -0,0 +1,19 @@
---
id: kw-port
name: port
category: structure
kind: keyword
tokens: port
sig: port Name { member: fn(T) -> R [= default], ... }
tip: The questions a module asks the world, bound once by the program.
order: 59
---
A <code>port</code> is what a module needs to ask the world, as named function members in primitive types. The program answers it once with <code>bind</code>; a member left unbound without a default is a compile error. Ports let a package ask a question without reaching into the module that knows the answer.
```ludic
# doc-check: skip — a module spans files
export port ClockWorld {
rest_scale: fn() -> float
}
```

View file

@ -0,0 +1,12 @@
---
id: kw-prop
name: prop
category: structure
kind: keyword
tokens: prop
sig: component Name { prop name: Type = default }
tip: A component's value handed in by its parent.
order: 71
---
<code>prop</code> declares a component member its parent sets from the template (<code>&lt;Counter step="2"/&gt;</code>); <code>state</code> declares one the component keeps for itself. Both are fields of the instance, listed with their types and defaults in <code>ludic schema</code>.

View file

@ -0,0 +1,20 @@
---
id: kw-reducer
name: reducer
category: structure
kind: keyword
tokens: reducer
sig: reducer State on Action(st: mut State, reads..., a: Action) { ... }
tip: Applies an action to one state; it may read others.
order: 62
---
A <code>reducer</code> writes exactly one state when its action is dispatched: the first parameter is that state, <code>mut</code>, the last is the action, and any states between are read-only. A change that must touch several states in a set order is a chain: a reducer dispatches the next action.
```ludic
state Pos { x: int = 0 }
action Move { dx: int = 0 }
reducer Pos on Move(p: mut Pos, a: Move) {
p.x = p.x + a.dx
}
```

View file

@ -0,0 +1,18 @@
---
id: kw-registry
name: registry
category: structure
kind: keyword
tokens: registry
sig: [open] registry Name of Record [as PREFIX] [from "file.lres"]
tip: A table of named entries of one record type, with a constant per entry.
order: 63
---
A <code>registry</code> is a table of named entries of one record: its entries come from <code>def</code> declarations or a resource file named by <code>from</code>, and <code>as P</code> generates a constant <code>P_KEY</code> per entry holding its index. Editors read it through <code>ludic schema</code>, and attributes such as <code>@AppendOnly</code> and <code>@PerMap</code> say how its entries may change and where they live.
```ludic
# doc-check: skip — the file it names is beside the example
property Tool { key: string = "", weight: float = 0.0 }
registry Tools of Tool as TL from "data/tools.lres"
```

View file

@ -0,0 +1,17 @@
---
id: kw-unsafe
name: unsafe
category: structure
kind: keyword
tokens: unsafe
sig: unsafe function f() { } / unsafe { ... }
tip: Raw memory is allowed inside: bytes(), free, Memory.*, indexing a pointer, calling C.
order: 57
---
Ludic's memory is safe unless code says <code>unsafe</code>: raw allocation, freeing, pointer indexing and foreign calls are compile errors elsewhere. An <code>unsafe function</code> or an <code>unsafe { ... }</code> block allows them inside, and only packages and the runtime are trusted to write it; a program's own files need <code>--unsafe</code>.
```ludic
# doc-check: skip — needs --unsafe
unsafe function raw(n: int) -> pointer { return bytes(n) }
```

View file

@ -0,0 +1,17 @@
---
id: kw-uses
name: uses
category: structure
kind: keyword
tokens: uses
sig: module name uses a, b, c
tip: The modules a module may reach; a reach not on the line is a compile error that names the fix.
order: 52
---
<code>uses</code> ends a <code>module</code> line with the modules its files may name. The compiler holds the module to that list and refuses a list that goes round: when adding a module would make a cycle, the question goes through a <code>port</code> the asker declares and the program binds instead.
```ludic
# doc-check: skip — a module spans files
module sky uses base, events, clock
```

View file

@ -0,0 +1,20 @@
---
id: kw-view
name: view
category: structure
kind: keyword
tokens: view
sig: view Name[(states)] { field = expr, function q(...), on e(...) { } }
tip: The bridge to a template: what it may read and what it may do.
order: 72
---
A <code>view</code> says what a template may read (its fields, each an expression) and what it may do (its queries and its <code>on</code> events), and nothing else crosses. The compiler writes <code>view_name()</code>, whose model is a value object of every field and whose call runs a query or an event by name.
```ludic
# doc-check: skip — the functions it names are elsewhere
view Yard {
spots: int = yard_spots()
on buy(hf: int) { yard_buy(hf) }
}
```

View file

@ -0,0 +1,19 @@
---
id: type-key
name: Key
category: types
kind: type
tokens: Key
sig: k"module.purpose" / kn"module.purpose"
tip: A text key: it names a text and is not one - tr(key) makes the text.
order: 4
---
A `Key` names a text the player reads; its English lives in the source language's `.po`, like every other language's text. It is written `k"module.purpose"`, or `kn"..."` for a text with plural forms, against its quote. A `Key` is not a `string` and a `string` is not a `Key`: text is made with `tr(key)`, `trf(key, ...)` or, for a plural, `trn(kn"...", n, ...)`, and a key in a template literal's hole is an error. Keys compare with `==`, and with a `lang` line in `package.ludic` the compiler checks every one against the `.po`.
```ludic
# doc-check: skip — tr / trf / trn are ludic.i18n's
let title = tr(k"pause.resume")
let day = trf(k"hud.day", txt_num(n))
if key == k"pause.resume" { Screen.status(title) }
```

View file

@ -75,7 +75,7 @@ port I18nWorld {
| --- | --- | | --- | --- |
| `L(s) -> string` | what the game draws, in the language in use (cached) | | `L(s) -> string` | what the game draws, in the language in use (cached) |
| `Ln(one, many, n) -> string` | a count's form, with `{1}` the count; English is one for 1, many otherwise (the old form) | | `Ln(one, many, n) -> string` | a count's form, with `{1}` the count; English is one for 1, many otherwise (the old form) |
| `tr(k)`, `trf(k, a, b, c, d)`, `trn(k, n, a, b, c) -> string` | a key, a key with its holes' arguments, a plural key by `n`: made into text by `L` | | `tr(k: Key)`, `trf(k: Key, a, b, c, d)`, `trn(k: Key, n, a, b, c) -> string` | a key (`k"..."`), a key with its holes' arguments, a plural key (`kn"..."`) by `n`: made into text by `L` |
| `i18n_key(name)`, `i18n_key_plural(name)`, `key_text(k)`, `i18n_is_key(s)` | a key named at run time, its name without the marker, whether a string is one | | `i18n_key(name)`, `i18n_key_plural(name)`, `key_text(k)`, `i18n_is_key(s)` | a key named at run time, its name without the marker, whether a string is one |
| `i18n_base_text(text)`, `i18n_loud(on)` | the base (en.po) from text, for a test; a developer's build's loud missing key | | `i18n_base_text(text)`, `i18n_loud(on)` | the base (en.po) from text, for a test; a developer's build's loud missing key |
| `i18n_init()`, `i18n_count()`, `i18n_index(code)`, `i18n_code_at(i)`, `i18n_name_at(i)`, `i18n_font_at(i)`, `i18n_mod_dir()` | the languages found, "en" first | | `i18n_init()`, `i18n_count()`, `i18n_index(code)`, `i18n_code_at(i)`, `i18n_name_at(i)`, `i18n_font_at(i)`, `i18n_mod_dir()` | the languages found, "en" first |

View file

@ -1,6 +1,6 @@
# keys.ludic - a key as the game carries it: its text behind a marker byte (the compiler's k"..." is # keys.ludic - a key as the game carries it: the compiler's Key (k"..." is its text behind byte 1, kn"..."
# exactly that), holes as byte 31 then each argument, a plural's count first. Nothing here needs the # behind byte 2), made into a string with holes as byte 31 then each argument, a plural's count first.
# language: the text is made where it is drawn (L), so code that makes text never takes I18nState. # Nothing here needs the language: the text is made where it is drawn (L), never taking I18nState.
export const I18N_KEY: int = 1 # k"pause.resume" is "\\x01pause.resume" export const I18N_KEY: int = 1 # k"pause.resume" is "\\x01pause.resume"
export const I18N_PLURAL: int = 2 # kn"catch.count": a count chooses the form export const I18N_PLURAL: int = 2 # kn"catch.count": a count chooses the form
export const I18N_SEP: int = 31 # between the key and each argument export const I18N_SEP: int = 31 # between the key and each argument
@ -22,16 +22,15 @@ export function key_text(k: string) -> string {
return k[1..e] return k[1..e]
} }
# the text of a key: drawn translated where it is drawn (a free cast once the key is typed) # the text of a key: drawn translated where it is drawn (its marked text, a free cast)
export function tr(k: string) -> string { return k } export function tr(k: Key) -> string { return string(k) }
# a key's text with its holes, {1} .. {4}, filled by strings already made (a number, a name, another key) # a key's text with its holes, {1} .. {4}, filled by strings already made (a number, a name, another key)
export function trf(k: string, a: string, b: string, c: string, d: string) -> string { export function trf(k: Key, a: string, b: string, c: string, d: string) -> string {
var mark = I18N_KEY let s = string(k)
if i18n_is_key(k) { mark = k[0] } return kx_join(I18N_KEY, key_text(s), a, b, c, d, kx_count(a, b, c, d))
return kx_join(mark, key_text(k), a, b, c, d, kx_count(a, b, c, d))
} }
# a plural key's text by n: the count is {1}, a .. c are {2} .. {4} # a plural key's text by n: the count is {1}, a .. c are {2} .. {4}
export function trn(k: string, n: int, a: string, b: string, c: string) -> string { return kx_join(I18N_PLURAL, key_text(k), `{n}`, a, b, c, 1 + kx_count(a, b, c, "")) } export function trn(k: Key, n: int, a: string, b: string, c: string) -> string { return kx_join(I18N_PLURAL, key_text(string(k)), `{n}`, a, b, c, 1 + kx_count(a, b, c, "")) }
function kx_count(a: string, b: string, c: string, d: string) -> int { function kx_count(a: string, b: string, c: string, d: string) -> int {
if len(d) > 0 { return 4 } if len(d) > 0 { return 4 }

View file

@ -32,38 +32,38 @@ program I18nKeysTest {
test "a key is its English under en.po, its own language's text when there is one, else English, else itself" (i18n_st: mut I18nState) { test "a key is its English under en.po, its own language's text when there is one, else English, else itself" (i18n_st: mut I18nState) {
start (i18n_st) start (i18n_st)
expect(L(i18n_st, i18n_key("pause.resume")) == "Resume") expect(L(i18n_st, tr(k"pause.resume")) == "Resume")
expect(L(i18n_st, tr(i18n_key("pause.quit"))) == "Quit") expect(L(i18n_st, tr(k"pause.quit")) == "Quit")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, i18n_key("pause.resume")) == "Devam") expect(L(i18n_st, tr(k"pause.resume")) == "Devam")
expect(L(i18n_st, i18n_key("pause.quit")) == "Quit") # the language lacks it: English expect(L(i18n_st, i18n_key("pause.quit")) == "Quit") # the language lacks it: English
expect(L(i18n_st, i18n_key("nowhere.at_all")) == "nowhere.at_all") # en.po lacks it: the key expect(L(i18n_st, i18n_key("nowhere.at_all")) == "nowhere.at_all") # en.po lacks it: the key
i18n_loud(i18n_st, true) i18n_loud(i18n_st, true)
expect(L(i18n_st, i18n_key("nowhere.else")) == "[[nowhere.else]]") # a developer's build says so expect(L(i18n_st, i18n_key("nowhere.else")) == "[[nowhere.else]]") # a developer's build says so
i18n_off(i18n_st) i18n_off(i18n_st)
expect(L(i18n_st, i18n_key("pause.resume")) == "Resume") # back to English: nothing stale kept expect(L(i18n_st, tr(k"pause.resume")) == "Resume") # back to English: nothing stale kept
} }
test "holes take their arguments in the language's order, and an argument that is a key is made first" (i18n_st: mut I18nState) { test "holes take their arguments in the language's order, and an argument that is a key is made first" (i18n_st: mut I18nState) {
start (i18n_st) start (i18n_st)
expect(L(i18n_st, trf(i18n_key("hud.day"), "12", "", "", "")) == "Day 12") expect(L(i18n_st, trf(k"hud.day", "12", "", "", "")) == "Day 12")
let saw = trf(i18n_key("catch.saw"), i18n_key("species.elk.a_name"), "5:30", "", "") let saw = trf(k"catch.saw", tr(k"species.elk.a_name"), "5:30", "", "")
expect(L(i18n_st, saw) == "You saw an elk at 5:30.") expect(L(i18n_st, saw) == "You saw an elk at 5:30.")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, trf(i18n_key("hud.day"), "12", "", "", "")) == "12. Gun") expect(L(i18n_st, trf(k"hud.day", "12", "", "", "")) == "12. Gun")
expect(L(i18n_st, saw) == "5:30 saatinde bir geyik gordun.") expect(L(i18n_st, saw) == "5:30 saatinde bir geyik gordun.")
expect(L(i18n_st, trf(i18n_key("hud.day"), "", "", "", "")) == ". Gun") # a missing argument is empty expect(L(i18n_st, trf(k"hud.day", "", "", "", "")) == ". Gun") # a missing argument is empty
expect(key_text(saw) == "catch.saw" and key_text("Settings") == "Settings") expect(key_text(saw) == "catch.saw" and key_text("Settings") == "Settings")
} }
test "a plural key takes its form by the count, by each language's own rule, falling back to English's" (i18n_st: mut I18nState) { test "a plural key takes its form by the count, by each language's own rule, falling back to English's" (i18n_st: mut I18nState) {
start (i18n_st) start (i18n_st)
expect(L(i18n_st, trn(i18n_key("pack.items"), 1, "", "", "")) == "1 item") expect(L(i18n_st, trn(kn"pack.items", 1, "", "", "")) == "1 item")
expect(L(i18n_st, trn(i18n_key("pack.items"), 3, "", "", "")) == "3 items") expect(L(i18n_st, trn(kn"pack.items", 3, "", "", "")) == "3 items")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, trn(i18n_key("pack.items"), 21, "", "", "")) == "21 esya") # n%10 == 1 expect(L(i18n_st, trn(kn"pack.items", 21, "", "", "")) == "21 esya") # n%10 == 1
expect(L(i18n_st, trn(i18n_key("pack.items"), 5, "", "", "")) == "5 esyalar") expect(L(i18n_st, trn(kn"pack.items", 5, "", "", "")) == "5 esyalar")
expect(L(i18n_st, trn(i18n_key("catch.count"), 2, "", "", "")) == "2 trout") # the language lacks it expect(L(i18n_st, trn(kn"catch.count", 2, "", "", "")) == "2 trout") # the language lacks it
} }
test "the old English form still works beside keys, so a game can move over a file at a time" (i18n_st: mut I18nState) { test "the old English form still works beside keys, so a game can move over a file at a time" (i18n_st: mut I18nState) {
@ -71,7 +71,7 @@ program I18nKeysTest {
expect(L(i18n_st, "Settings") == "Settings") expect(L(i18n_st, "Settings") == "Settings")
i18n_load_text(i18n_st, tt()) i18n_load_text(i18n_st, tt())
expect(L(i18n_st, "Settings") == "Ayarlar") expect(L(i18n_st, "Settings") == "Ayarlar")
expect(L(i18n_st, i18n_key("pause.resume")) == "Devam") expect(L(i18n_st, tr(k"pause.resume")) == "Devam")
expect(not i18n_is_key("Settings") and i18n_is_key(i18n_key("x")) and i18n_is_key(i18n_key_plural("x"))) expect(not i18n_is_key("Settings") and i18n_is_key(i18n_key("x")) and i18n_is_key(i18n_key_plural("x")))
} }
} }

View file

@ -0,0 +1,122 @@
# stdin_file.ludic — `--stdin-file <path>`: an unsaved buffer checked in place of its file. The text
# on stdin is read once, whole, and read_file (io.ludic) hands a copy of it back wherever the program
# opens <path> - an import, a barrel, a component's .xml / .lss, an .lres - so diagnostics follow
# typing without a save. Two paths are the same file when they are after normalise_path: separators
# made '/', a relative path put under $PWD, `.` / `..` / `//` folded (and case folded on Windows).
var g_sf_path: pointer = null # <path> as given: what a warning about it names
var g_sf_norm: pointer = null # ... normalised, what each open is compared against
var g_sf_base: pointer = null # ... its last part, the cheap test before normalising an open
var g_sf_text: pointer = null # stdin, NUL-terminated
var g_sf_len: int = 0
var g_sf_served: bool = false # the program opened <path> at least once
# the flag: remember the path and take stdin now, once (an empty stdin is an empty file)
function stdin_file_set(path: pointer) -> void {
g_sf_path = path
g_sf_norm = normalise_path(path)
g_sf_base = base_name(g_sf_norm)
var cap = 4096
var buf = bytes(cap + 1)
var n = 0
var c = read_char()
while c >= 0 {
if n == cap { cap *= 2; buf = resize(buf, cap + 1) }
buf[n] = c
n += 1
c = read_char()
}
buf[n] = 0
g_sf_text = buf
g_sf_len = n
}
# read_file's first question: the buffer's text (a copy, as a read of the file would be) when
# `path` is <path>, else null
function stdin_file_serve(path: pointer) -> pointer {
if g_sf_text == null { return null }
if not sf_tail_is(path, g_sf_base) { return null }
if normalise_path(path) != g_sf_norm { return null }
let out = bytes(g_sf_len + 1)
var i = 0
while i < g_sf_len { out[i] = g_sf_text[i]; i += 1 }
out[g_sf_len] = 0
g_sf_served = true
return out
}
# a buffer for a file the program never opened was checked against nothing: say so rather than
# report the saved files clean
function stdin_file_unreached() -> void {
if g_sf_text == null or g_sf_served { return }
pwarn_at(g_sf_path, 1, 0, `--stdin-file {g_sf_path}: the program does not reach this file, so the buffer was not checked`)
}
# does `path`'s last part equal `base`? (no allocation: every read_file asks)
function sf_tail_is(path: pointer, base: pointer) -> bool {
var n = 0
while path[n] != 0 { n += 1 }
let m = len(base)
if n < m { return false }
if n > m and not is_sep(path[n - m - 1]) { return false }
var i = 0
while i < m {
if sf_fold(path[n - m + i]) != sf_fold(base[i]) { return false }
i += 1
}
return true
}
function sf_fold(c: int) -> int {
if c >= 'A' and c <= 'Z' and host_is_windows() { return c + 32 }
return c
}
# one spelling per file: '/' separators, absolute (a relative path is under $PWD, the directory the
# compiler was started in), no `.`, `..` or empty parts; lower case on Windows
function normalise_path(p: pointer) -> pointer {
var s = fwd_slashes(p)
let abs = (len(s) > 0 and s[0] == '/') or (len(s) > 1 and s[1] == ':')
if not abs {
let cwd = getenv("PWD")
if cwd != null and len(cwd) > 0 { s = fwd_slashes(cwd) + "/" + s }
}
let parts = new []pointer # the first np are the path so far
var np = 0
var start = 0
var i = 0
let n = len(s)
while i <= n {
if i == n or s[i] == '/' {
let part = s[start..i]
if part == ".." and np > 0 and parts[np - 1] != ".." { np -= 1 }
else if part != "" and part != "." {
if np < len(parts) { parts[np] = part } else { push(parts, part) }
np += 1
}
start = i + 1
}
i += 1
}
var out = ""
if len(s) > 0 and s[0] == '/' { out = "/" }
i = 0
while i < np {
if i > 0 { out = out + "/" }
out = out + parts[i]
i += 1
}
if host_is_windows() { out = sf_lower(out) }
return out
}
function sf_lower(s: pointer) -> pointer {
let n = len(s)
let out = bytes(n + 1)
var i = 0
while i < n {
var c = s[i]
if c >= 'A' and c <= 'Z' { c += 32 }
out[i] = c
i += 1
}
out[n] = 0
return out
}

View file

@ -0,0 +1,424 @@
# vocab.ludic — the language's vocabulary as one table, written out by `ludicc --emit-syntax`
# (`ludic syntax --json`): every keyword with its role, every declaration's form, every built-in type
# and phase, every attribute with where it goes, what it takes and what it means, every operator.
#
# The parser dispatches on words where it meets them rather than from a table, so this is the table
# beside it, and it is held to the parser from both sides: the emitter asks the parser's own
# recognisers of every row it prints (is_reserved_word, is_phase_name, at_field_known) and refuses to
# print a row they disagree with, and `ludic-dev syntax --check` scans selfhost/frontend for every word
# and attribute the parser tests and fails on one this table lacks. The editor grammars, the language
# server's word lists and docs/language are generated from or checked against what this prints.
var g_vc_kind: []pointer = new []pointer # keyword, declaration, type, phase, attribute, operator, literal
var g_vc_name: []pointer = new []pointer
var g_vc_role: []pointer = new []pointer
var g_vc_form: []pointer = new []pointer # how it is written (an attribute's arguments, a declaration's shape)
var g_vc_on: []pointer = new []pointer # an attribute's targets, space-separated
var g_vc_doc: []pointer = new []pointer
function vc(kind: pointer, name: pointer, role: pointer, form: pointer, on: pointer, doc: pointer) -> void {
push(g_vc_kind, kind)
push(g_vc_name, name)
push(g_vc_role, role)
push(g_vc_form, form)
push(g_vc_on, on)
push(g_vc_doc, doc)
}
function vc_kw(role: pointer, w: pointer, doc: pointer) -> void { vc("keyword", w, role, "", "", doc) }
function vc_decl(name: pointer, form: pointer, doc: pointer) -> void { vc("declaration", name, "", form, "", doc) }
function vc_ty(name: pointer, role: pointer, doc: pointer) -> void { vc("type", name, role, "", "", doc) }
function vc_at(name: pointer, role: pointer, on: pointer, args: pointer, doc: pointer) -> void { vc("attribute", name, role, args, on, doc) }
function vc_op(op: pointer, role: pointer, doc: pointer) -> void { vc("operator", op, role, "", "", doc) }
function vocab_keywords() -> void {
# declaring: a word that starts a declaration (or a member of one)
vc_kw("declaration", "program", "A program block: the unit that holds every declaration of a game.")
vc_kw("declaration", "module", "Names the module a directory is (module bank uses base): only what it exports is reachable from outside.")
vc_kw("declaration", "import", "Brings another file or a directory's barrel into the program.")
vc_kw("declaration", "property", "A record type; on an entity, a component.")
vc_kw("declaration", "state", "A module's changing data: one instance, reached only as a parameter. Inside a machine, one of its states.")
vc_kw("declaration", "event", "Something that happened, fired with emit and heard by @On listeners.")
vc_kw("declaration", "action", "What a player asked for, dispatched and applied by each state's reducer.")
vc_kw("declaration", "reducer", "Applies an action to one state: reducer HikerState on Move(h: mut HikerState, a: Move).")
vc_kw("declaration", "enum", "A set of named integer variants.")
vc_kw("declaration", "model", "An entity kind: the components an entity of it carries.")
vc_kw("declaration", "prefab", "A model with preset component values.")
vc_kw("declaration", "scene", "A scene: its layers, its handlers and how it moves on.")
vc_kw("declaration", "handler", "A system that runs in a phase, optionally once per entity a query matches.")
vc_kw("declaration", "ui", "A retained-mode widget tree.")
vc_kw("declaration", "namespace", "A Foo.* namespace declared in Ludic, its methods aliases of functions.")
vc_kw("declaration", "alias", "Inside a namespace: a method that is another function (alias length = trail_length).")
vc_kw("declaration", "port", "A question a module asks the world, as named fn members the program binds once.")
vc_kw("declaration", "bind", "Answers a port: bind ClockWorld { rest_scale: my_rest_scale }.")
vc_kw("declaration", "registry", "A table of named entries of one record type: registry Items of Item as IT from \"items.lres\".")
vc_kw("declaration", "def", "One entry of a registry: def Items rope { ... }, or def R from \"file.lres\".")
vc_kw("declaration", "component", "A UI component: its props, state, functions and events, beside its .xml template and .lss styles.")
vc_kw("declaration", "view", "The bridge to a UI template: the fields it may read and the queries and events it may call.")
vc_kw("declaration", "prop", "Inside a component: a value its parent hands it.")
vc_kw("declaration", "const", "A named constant.")
vc_kw("declaration", "var", "A mutable binding (a local; at module level only with --globals).")
vc_kw("declaration", "function", "A function.")
vc_kw("declaration", "fn", "A function type (fn(int, float) -> bool), or a function value written fn name.")
vc_kw("declaration", "extern", "A function implemented outside Ludic, linked by name.")
vc_kw("declaration", "entry", "The program's entry point.")
vc_kw("declaration", "test", "A named test block: test \"name\" { expect ... }.")
# modifying: a word inside a declaration's head
vc_kw("modifier", "export", "Makes a declaration visible outside its module.")
vc_kw("modifier", "internal", "Inside a namespace: a function kept out of the Name.* surface.")
vc_kw("modifier", "friend", "friend module lab [of A, B]: this file's module sees the private names of those modules (of every one without of).")
vc_kw("modifier", "uses", "The modules a module may reach: module sky uses base, events.")
vc_kw("modifier", "open", "open registry R: other modules may add entries with def.")
vc_kw("modifier", "of", "registry R of Record: the record its entries are; friend module m of A, B: whose private names it sees.")
vc_kw("modifier", "as", "registry R of T as P: the prefix of its generated constants (P_KEY).")
vc_kw("modifier", "from", "registry R ... from \"file.lres\": the resource file its entries are read from.")
vc_kw("modifier", "numbers", "numbers float: a bare 1.5 in this file is a float.")
vc_kw("modifier", "unsafe", "unsafe function / unsafe { ... }: raw memory is allowed inside.")
vc_kw("modifier", "mut", "A parameter the function may change: h: mut HikerState.")
vc_kw("modifier", "phase", "The phase a handler runs in: handler Move phase Update.")
vc_kw("modifier", "query", "The components a handler iterates: for (p, v) in query [Pos, Vel].")
vc_kw("modifier", "on", "reducer S on Action; attach P on e; an event's listener in a component or scene.")
vc_kw("modifier", "cancellable", "event cancellable E: a listener may veto it with cancel.")
vc_kw("modifier", "public", "A public scene or layer: its show and hide are events other code may listen to.")
vc_kw("modifier", "layer", "A scene's layer of handlers; enable/disable layer L.")
vc_kw("modifier", "start", "The scene a program starts in.")
vc_kw("modifier", "shows", "scene S shows Menu: the engine opens, renders and closes that ui.")
vc_kw("modifier", "lasts", "scene S lasts N then Next: a timed scene.")
vc_kw("modifier", "then", "The scene a timed scene moves on to.")
vc_kw("modifier", "loads", "scene S loads then Next: pumps the asset queue with a bar, then moves on.")
vc_kw("modifier", "system", "disable system S: an engine system left out at compile time.")
# statements: a word that starts or shapes a statement
vc_kw("statement", "let", "An immutable binding; at module level, immutable all the way down.")
vc_kw("statement", "if", "Runs a block when a condition holds.")
vc_kw("statement", "else", "The branch taken when the if's condition does not hold.")
vc_kw("statement", "while", "Loops while a condition holds.")
vc_kw("statement", "for", "for i in a..b, or for (vars) in query [...].")
vc_kw("statement", "in", "Separates a for's variables from what they range over; module m in layer L.")
vc_kw("statement", "where", "A query's filter: for (p) in query [Pos] where p.x > 0.")
vc_kw("statement", "match", "Picks the arm whose values match.")
vc_kw("statement", "machine", "A state machine over a value, one block per state.")
vc_kw("statement", "become", "Moves a machine (or the program) to another state or scene.")
vc_kw("statement", "return", "Returns from a function.")
vc_kw("statement", "break", "Leaves the innermost loop.")
vc_kw("statement", "continue", "Skips to the innermost loop's next turn.")
vc_kw("statement", "new", "Makes a record or a slice: new Item, new []int.")
vc_kw("statement", "spawn", "Creates an entity of a model.")
vc_kw("statement", "despawn", "Removes an entity.")
vc_kw("statement", "enable", "Turns a property, a layer or a handler back on.")
vc_kw("statement", "disable", "Turns a property, a layer, a handler or an engine system off.")
vc_kw("statement", "attach", "Adds a property to an entity: attach P on e { field: v }.")
vc_kw("statement", "detach", "Removes a property from an entity: detach P on e.")
vc_kw("statement", "emit", "Fires an event: emit WoodChopped(n: 3).")
vc_kw("statement", "cancel", "Vetoes a cancellable event from its listener.")
vc_kw("statement", "dispatch", "Queues an action for the reducers: dispatch Move(dx: 1.0).")
vc_kw("statement", "try", "try EXPR else { ... }: a fallible value's payload, or the else block's fallback.")
# operator words and literal words
vc_kw("operator", "and", "Logical and (short-circuit).")
vc_kw("operator", "or", "Logical or (short-circuit).")
vc_kw("operator", "not", "Logical not.")
vc_kw("constant", "true", "The boolean true.")
vc_kw("constant", "false", "The boolean false.")
vc_kw("constant", "null", "No record, no slice, no string.")
}
# every declaration, in the shape it is written
function vocab_declarations() -> void {
vc_decl("program", "program Name { declarations }", "The whole game, or a unit of it.")
vc_decl("module", "module name [in layer L] [uses a, b]", "The module a directory's files belong to, and what it may reach.")
vc_decl("friend", "friend module name [of a, b]", "A module that sees others' private names (the lab).")
vc_decl("import", "import \"path\"", "A file, a directory's barrel or a package.")
vc_decl("numbers", "numbers float", "This file's bare decimals are floats.")
vc_decl("export", "export <declaration>", "The declaration is visible outside its module.")
vc_decl("property", "property Name[<T>] { field: Type = default, ... }", "A record; on an entity, a component.")
vc_decl("state", "state Name { field: Type = default, ... }", "A module's changing data, one instance, passed as a parameter.")
vc_decl("event", "event [cancellable] Name { field: Type, ... }", "Something that happened.")
vc_decl("action", "action Name { field: Type, ... }", "What a player asked for.")
vc_decl("reducer", "reducer State on Action(st: mut State, reads..., a: Action) { ... }", "Applies an action to one state.")
vc_decl("enum", "enum Name { A, B, C }", "Named integer variants.")
vc_decl("model", "model Name { Component, ... }", "An entity kind.")
vc_decl("prefab", "prefab Name of Model { Component { field: v } }", "A model with preset values.")
vc_decl("scene", "scene Name [start] [public] [shows Ui] [lasts N then Next] [loads then Next] { layer L { handlers } }", "A scene.")
vc_decl("handler", "handler Name[(states)] phase Phase { ... }", "A system run every frame in its phase.")
vc_decl("ui", "ui Name { widgets }", "A retained widget tree.")
vc_decl("namespace", "namespace Name { [export|internal] function ... | alias m = f }", "A Name.* namespace.")
vc_decl("port", "port Name { member: fn(T) -> R [= default], ... }", "Questions a module asks the world.")
vc_decl("bind", "bind Port { member: function_name, ... }", "The program's answers to a port.")
vc_decl("registry", "[open] registry Name of Record [as PREFIX] [from \"file.lres\"]", "A table of named entries.")
vc_decl("def", "def Registry key { field: value, ... } | def Registry from \"file.lres\"", "Entries of a registry.")
vc_decl("view", "view Name[(states)] { field = expr, function q(...), on e(...) }", "What a template may read and do.")
vc_decl("component", "component Name[(states)] { prop p: T = v, state s: T = v, function ..., on e(...) { } }", "A UI component, beside Name.xml and Name.lss.")
vc_decl("const", "const NAME: Type = value", "A constant.")
vc_decl("let", "let name: Type = value", "An immutable module-level value.")
vc_decl("var", "var name: Type = value", "A module-level variable (only with --globals).")
vc_decl("function", "[unsafe] function name[<T>](p: T, st: mut S) -> R { ... }", "A function.")
vc_decl("extern", "extern function name(p: T) -> R = \"symbol\"", "A foreign function.")
vc_decl("entry", "entry[(states)] { ... }", "The program's entry point.")
vc_decl("test", "test \"name\" { ... }", "A test block.")
}
function vocab_types() -> void {
vc_ty("int", "primitive", "A 32-bit signed integer.")
vc_ty("long", "primitive", "A 64-bit signed integer.")
vc_ty("float", "primitive", "A 32-bit IEEE float.")
vc_ty("double", "primitive", "A 64-bit IEEE float.")
vc_ty("fixed", "primitive", "A Q16.16 fixed-point number.")
vc_ty("bool", "primitive", "true or false.")
vc_ty("byte", "primitive", "An unsigned 8-bit value.")
vc_ty("string", "primitive", "Text, UTF-8.")
vc_ty("Key", "primitive", "A text key, k\"module.purpose\" (kn\"...\" a plural): names a text, is not one; tr(key) makes the text.")
vc_ty("pointer", "primitive", "An untyped address (raw memory needs unsafe).")
vc_ty("void", "primitive", "No value: a function that returns nothing.")
vc_ty("entity", "primitive", "A handle to an entity.")
vc_ty("countdown", "primitive", "A timer that runs down by itself.")
vc_ty("words", "slice", "A bounds-checked []int buffer: words(n).")
vc_ty("floats", "slice", "A bounds-checked []float buffer: floats(n).")
vc_ty("doubles", "slice", "A bounds-checked []double buffer: doubles(n).")
vc_ty("fixeds", "slice", "A bounds-checked []fixed buffer: fixeds(n).")
vc_ty("pointers", "slice", "A []pointer buffer: pointers(n).")
vc_ty("Vector", "value", "A packed 2D vector value.")
vc_ty("IVec2", "value", "A packed integer 2D vector value.")
vc_ty("Rect", "value", "A packed rectangle value.")
}
function vocab_phases() -> void {
vc("phase", "Start", "", "", "", "Once, before the first frame.")
vc("phase", "Input", "", "", "", "Every frame: read the devices.")
vc("phase", "FixedUpdate", "", "", "", "At the fixed simulation rate.")
vc("phase", "Update", "", "", "", "Every frame: the game's rules.")
vc("phase", "LateUpdate", "", "", "", "Every frame, after Update.")
vc("phase", "Render", "", "", "", "Every frame: draw.")
vc("phase", "Overlay", "", "", "", "Every frame, after Render: what is drawn on top.")
}
# role: "checked" - the parser reads it and it means something; "recorded" - read past, kept as a
# label (no effect). on: where it goes - field, registry, declaration, function, handler, property,
# model, event, statement.
function vocab_attributes() -> void {
# a field's, for editors and the schema (attrs.ludic) - they change nothing the program does
vc_at("Ref", "checked", "field", "(Registry)", "An index into (or, for a @PerMap registry, the key of) an entry of that registry.")
vc_at("OneOf", "checked", "field", "(PREFIX_) | (CONST, ...) | (\"word\", ...)", "One of the constants with that prefix, one of those constants, or on a string field one of those words.")
vc_at("Range", "checked", "field", "(lo, hi)", "The least and the most the value may be.")
vc_at("Unit", "checked", "field", "(\"m/s\")", "The value's unit, one canonical ASCII spelling (m, m/s, s, deg, kg, ...).")
vc_at("Asset", "checked", "field", "(\"kind\"[, map[, optional]])", "A path to a file of that kind; with map, under each map's directory.")
vc_at("Color", "checked", "field", "", "A colour.")
vc_at("Node", "checked", "field", "(model_field)", "A node inside the glTF that field names.")
vc_at("Clip", "checked", "field", "(model_field)", "An animation clip inside the glTF that field names.")
vc_at("Material", "checked", "field", "(model_field)", "A material inside the glTF that field names.")
vc_at("Tint", "checked", "field", "(SLOT)", "A colour for that tint slot (a constant).")
vc_at("Derived", "checked", "field", "", "Worked out at boot: anything written in the data is overwritten.")
vc_at("Text", "checked", "field", "", "Read by the player, so translated: a Key field's key is derived, <registry>.<row>.<field>.")
vc_at("Multiline", "checked", "field", "", "Prose: edited as several lines.")
vc_at("Key", "checked", "field", "", "A key code.")
# a field's, with an effect
vc_at("Computed", "checked", "field", "", "A derived field expanded inline wherever it is read, never stored.")
vc_at("Sync", "checked", "field property model", "", "Replicates over the network: the field, every field of the property, or that component of the model.")
vc_at("frame", "checked", "field", "", "A fn stored here runs every frame: a root of the frame-allocation count.")
vc_at("max", "checked", "field", "(n)", "The most the list may hold; growing past it fails the run.")
vc_at("owns", "checked", "field", "(Kind)", "The field holds a handle of that kind its record owns: releasing a sibling and losing it is counted.")
# a registry's
vc_at("AppendOnly", "checked", "registry", "", "Entries are only ever appended: an entry's index is saved.")
vc_at("ByKey", "checked", "registry", "", "Entries are saved by key, so their order is free.")
vc_at("PerMap", "checked", "registry", "", "Its rows are read per map, from that map's directory, when the map loads.")
vc_at("Chunked", "checked", "registry", "(n)", "A @PerMap table read a chunk of n by n metres at a time.")
vc_at("TextKey", "checked", "registry", "(\"prefix\")", "The prefix of its @Text fields' derived keys, in place of the registry's name in snake case.")
# a declaration's
vc_at("export", "checked", "declaration", "", "The declaration is exported (the same as the export keyword).")
vc_at("Public", "checked", "handler", "", "Promotes a lifecycle hook to an event other code may hear.")
vc_at("On", "checked", "handler function", "(Event)", "A listener: runs when that event is emitted.")
vc_at("Queries", "checked", "handler", "(these: [...], on: ...)", "The handler runs once per entity the query matches.")
vc_at("OnSpawn", "checked", "handler", "(Model)", "Runs when an entity of that model spawns.")
vc_at("OnDespawn", "checked", "handler", "(Model[, reason: r])", "Runs when an entity of that model despawns.")
vc_at("OnAttach", "checked", "handler", "(Property)", "Runs when that property is attached to an entity.")
vc_at("OnDetach", "checked", "handler", "(Property)", "Runs when that property is detached.")
vc_at("OnEnable", "checked", "handler", "(Property)", "Runs when that property is enabled.")
vc_at("OnDisable", "checked", "handler", "(Property)", "Runs when that property is disabled.")
vc_at("OnStart", "checked", "handler", "", "Runs once at boot.")
vc_at("OnQuit", "checked", "handler", "", "Runs once at shutdown.")
vc_at("Owned", "checked", "model", "", "Its entities carry a network owner.")
vc_at("Server", "checked", "handler", "", "Runs only where the world is owned.")
vc_at("Predicted", "checked", "handler", "", "Runs on the owner and is predicted elsewhere.")
vc_at("ToServer", "checked", "event", "", "A remote event sent to the server.")
vc_at("ToClients", "checked", "event", "", "A remote event sent to every client.")
vc_at("System", "checked", "function", "[(Phase)]", "A binary module's system, registered when it loads.")
vc_at("EngineSystem", "checked", "function", "(Component, Phase)", "A package's compile-time engine system.")
vc_at("Namespace", "checked", "function", "(Name)", "A package-provided Name.* namespace.")
vc_at("deterministic", "checked", "handler function", "", "No floating point inside: it must replay the same everywhere.")
vc_at("ClearColor", "checked", "declaration", "(0xRRGGBB)", "The Render phase clears to this colour and presents by itself.")
vc_at("alloc_ok", "checked", "function handler statement", "(\"why\")", "Allocates in a frame on purpose; the reason is required.")
vc_at("creates", "checked", "function", "(Kind)", "Returns a handle of that kind the caller must release.")
vc_at("releases", "checked", "function", "(Kind)", "Releases a handle of that kind.")
# read past and kept as a label
vc_at("Handles", "recorded", "handler program", "(Name)", "Names what a handler (or a program) is responsible for; no effect.")
vc_at("Reads", "recorded", "handler", "(Component, ...)", "Documents the components a handler reads; no effect.")
vc_at("Writes", "recorded", "handler", "(Component, ...)", "Documents the components a handler writes; no effect.")
}
function vocab_operators() -> void {
vc_op("+", "arithmetic", "Add; joins strings.")
vc_op("-", "arithmetic", "Subtract; negate.")
vc_op("*", "arithmetic", "Multiply.")
vc_op("/", "arithmetic", "Divide.")
vc_op("%", "arithmetic", "Remainder.")
vc_op("&", "bitwise", "Bitwise and.")
vc_op("|", "bitwise", "Bitwise or.")
vc_op("^", "bitwise", "Bitwise exclusive or.")
vc_op("~", "bitwise", "Bitwise not.")
vc_op("<<", "bitwise", "Shift left.")
vc_op(">>", "bitwise", "Shift right.")
vc_op("==", "comparison", "Equal (strings by content, records by identity).")
vc_op("!=", "comparison", "Not equal.")
vc_op("<", "comparison", "Less than.")
vc_op("<=", "comparison", "Less than or equal.")
vc_op(">", "comparison", "Greater than.")
vc_op(">=", "comparison", "Greater than or equal.")
vc_op("=", "assignment", "Assign.")
vc_op("+=", "assignment", "Add and assign.")
vc_op("-=", "assignment", "Subtract and assign.")
vc_op("*=", "assignment", "Multiply and assign.")
vc_op("/=", "assignment", "Divide and assign.")
vc_op("->", "punctuation", "A function's result type.")
vc_op("=>", "punctuation", "A match arm.")
vc_op("..", "range", "A range (for i in a..b) or a slice of a string (s[a..b]).")
vc_op(".", "access", "A field, a method or a namespace member.")
vc_op(":", "punctuation", "A type annotation; a named argument or field value.")
vc_op(",", "punctuation", "Separates items.")
vc_op("@", "punctuation", "Starts an attribute.")
vc_op(";", "punctuation", "Ends a statement, as a newline does.")
vc_op("(", "bracket", "Opens a call, a parameter list or a group.")
vc_op(")", "bracket", "Closes a call, a parameter list or a group.")
vc_op("[", "bracket", "Opens an index, a slice type or a list literal.")
vc_op("]", "bracket", "Closes an index, a slice type or a list literal.")
vc_op("{", "bracket", "Opens a block, a record or a template hole.")
vc_op("}", "bracket", "Closes a block, a record or a template hole.")
}
function vocab_literals() -> void {
vc("literal", "123", "integer", "", "", "An int; a bare decimal is a float in a numbers float file.")
vc("literal", "0xFF", "hex", "", "", "An int in hexadecimal.")
vc("literal", "1.5", "decimal", "", "", "A fixed, or a float in a numbers float file.")
vc("literal", "'a'", "char", "", "", "A character's code, an int.")
vc("literal", "\"text\"", "string", "", "", "A string; \\n \\t \\r \\0 \\\\ \\\" are escapes.")
vc("literal", "k\"module.purpose\"", "key", "", "", "A Key, checked against the source language's .po; written against its quote.")
vc("literal", "kn\"module.purpose\"", "plural key", "", "", "A plural Key, for trn: its .po entry has a msgid_plural.")
vc("literal", "`a {expr} b`", "template", "", "", "A string with holes; {{ and }} are braces.")
vc("literal", "# ...", "comment", "", "", "A comment to the end of the line.")
}
function vocab_fill() -> void {
if len(g_vc_name) > 0 { return }
vocab_keywords()
vocab_declarations()
vocab_types()
vocab_phases()
vocab_attributes()
vocab_operators()
vocab_literals()
}
# a row the parser's own recognisers disagree with is refused, naming it
function vocab_check() -> void {
var i = 0
while i < len(g_vc_name) {
let k = g_vc_kind[i]
let n = g_vc_name[i]
if (k == "phase") and not is_phase_name(n) { die(`ludicc --emit-syntax: vocab.ludic lists the phase {n}, which the parser refuses\n`) }
if (k == "attribute") and (g_vc_on[i] == "field") and (g_vc_role[i] == "checked") and not vc_field_attr(n) {
die(`ludicc --emit-syntax: vocab.ludic lists the field attribute @{n}, which the parser does not read\n`)
}
i += 1
}
}
function vc_field_attr(a: pointer) -> bool {
return at_field_known(a) or (a == "Computed") or (a == "Sync") or (a == "frame") or (a == "max") or (a == "owns")
}
function vc_list(b: Buf, kind: pointer, key: pointer) -> void {
buf_puts(b, ",\n \"")
buf_puts(b, kind)
buf_puts(b, "\": [")
var first = true
var i = 0
while i < len(g_vc_name) {
if g_vc_kind[i] == vc_kind_of(kind) {
if not first { buf_puts(b, ",") }
first = false
buf_puts(b, "\n {\"")
buf_puts(b, key)
buf_puts(b, "\": ")
jq_put(b, g_vc_name[i])
vc_fields(b, i)
buf_puts(b, ", \"doc\": ")
jq_put(b, g_vc_doc[i])
buf_puts(b, "}")
}
i += 1
}
buf_puts(b, "\n ]")
}
function vc_kind_of(list: pointer) -> pointer {
if list == "keywords" { return "keyword" }
if list == "declarations" { return "declaration" }
if list == "types" { return "type" }
if list == "phases" { return "phase" }
if list == "attributes" { return "attribute" }
if list == "operators" { return "operator" }
return "literal"
}
function vc_fields(b: Buf, i: int) -> void {
let k = g_vc_kind[i]
if len(g_vc_role[i]) > 0 {
buf_puts(b, ", \"role\": ")
jq_put(b, g_vc_role[i])
}
if k == "keyword" {
buf_puts(b, ", \"reserved\": ")
if is_reserved_word(g_vc_name[i]) { buf_puts(b, "true") } else { buf_puts(b, "false") }
}
if k == "declaration" {
buf_puts(b, ", \"form\": ")
jq_put(b, g_vc_form[i])
}
if k == "attribute" {
buf_puts(b, ", \"on\": [")
vc_words(b, g_vc_on[i])
buf_puts(b, "], \"args\": ")
if len(g_vc_form[i]) == 0 { buf_puts(b, "null") } else { jq_put(b, g_vc_form[i]) }
}
}
# "a b c" as "a", "b", "c"
function vc_words(b: Buf, s: pointer) -> void {
var a = 0
var first = true
let n = len(s)
while a < n {
var e = a
while e < n and s[e] != ' ' { e += 1 }
if e > a {
if not first { buf_puts(b, ", ") }
first = false
jq_put(b, s[a .. e])
}
a = e + 1
}
}
# the whole vocabulary as JSON: deterministic, in the table's order
function syntax_json() -> pointer {
vocab_fill()
vocab_check()
let b = buf_new()
buf_puts(b, "{\n \"syntax_version\": 1")
vc_list(b, "keywords", "word")
vc_list(b, "declarations", "kind")
vc_list(b, "types", "name")
vc_list(b, "phases", "name")
vc_list(b, "attributes", "name")
vc_list(b, "operators", "op")
vc_list(b, "literals", "form")
buf_puts(b, "\n}\n")
return buf_str(b)
}
# ludicc --emit-syntax: the vocabulary on stdout, and nothing else is read or built
function syntax_emit() -> void {
let s = syntax_json()
file_write(file_stdout(), s, len(s))
exit(0)
}

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -162,6 +162,8 @@ entry {
else if a == "--release" { g_release = true } # what ships: Build.schema_hash() is 0 (emit_schema_hash.ludic) 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-syntax" { syntax_emit() } # the language's vocabulary as JSON (frontend/vocab.ludic)
else if a == "--stdin-file" { ai += 1; if ai < arg_count() { stdin_file_set(arg(ai)) } } # stdin stands for that file (stdin_file.ludic)
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
else if a == "--globals" { g_allow_globals = true } else if a == "--globals" { g_allow_globals = true }
else if a == "--migrate-state" { # 0.S2: write the plan ludic migrate state applies else if a == "--migrate-state" { # 0.S2: write the plan ludic migrate state applies
@ -262,10 +264,12 @@ entry {
if g_check_only { if g_check_only {
pm_check_maps() # every map's tables against their records (permap_check.ludic) pm_check_maps() # every map's tables against their records (permap_check.ludic)
emit_program() emit_program()
stdin_file_unreached() # --stdin-file naming a file nothing opened
if g_dg_errors > 0 { diag_exit(1) } if g_dg_errors > 0 { diag_exit(1) }
diag_exit(0) diag_exit(0)
} }
emit_program() emit_program()
stdin_file_unreached()
if g_dg_errors > 0 { diag_exit(1) } # errors a pass went on from (diag.ludic) if g_dg_errors > 0 { diag_exit(1) } # errors a pass went on from (diag.ludic)
deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps) deps_flush() # LUDIC_DEPS=<file>: the module graph (ludic deps)

View file

@ -2,6 +2,8 @@
# The compiler reads one .ludic file whole and emits LLVM IR text to stdout. # The compiler reads one .ludic file whole and emits LLVM IR text to stdout.
function read_file(path: pointer) -> pointer { function read_file(path: pointer) -> pointer {
let unsaved = stdin_file_serve(path) # --stdin-file: the buffer stands for its file (stdin_file.ludic)
if unsaved != null { return unsaved }
let f = file_open(path, "rb") let f = file_open(path, "rb")
if (f == null) { return null } if (f == null) { return null }
file_seek(f, 0, 2) file_seek(f, 0, 2)

View file

@ -25,7 +25,25 @@
"annot-system", "annot-system",
"annot-enginesystem", "annot-enginesystem",
"annot-namespace", "annot-namespace",
"annot-clearcolor" "annot-clearcolor",
"annot-alloc_ok",
"annot-appendonly",
"annot-asset",
"annot-color",
"annot-derived",
"annot-deterministic",
"annot-frame",
"annot-key",
"annot-max",
"annot-node",
"annot-oneof",
"annot-owns",
"annot-permap",
"annot-range",
"annot-ref",
"annot-text",
"annot-tint",
"annot-unit"
], ],
"builtins": [ "builtins": [
"fn-print", "fn-print",
@ -86,7 +104,8 @@
"kw-machine", "kw-machine",
"kw-state", "kw-state",
"kw-become", "kw-become",
"kw-try" "kw-try",
"kw-dispatch"
], ],
"ease": [ "ease": [
"ease-in", "ease-in",
@ -119,7 +138,8 @@
"fn-world_register_prop", "fn-world_register_prop",
"fn-world_attach_dyn", "fn-world_attach_dyn",
"fn-world_detach_dyn", "fn-world_detach_dyn",
"fn-world_query_next" "fn-world_query_next",
"kw-system"
], ],
"events": [ "events": [
"kw-event", "kw-event",
@ -211,7 +231,8 @@
"op-access", "op-access",
"op-interp", "op-interp",
"op-literals", "op-literals",
"op-comment" "op-comment",
"kw-true"
], ],
"phases": [ "phases": [
"phase-fixedupdate", "phase-fixedupdate",
@ -240,7 +261,11 @@
"kw-on", "kw-on",
"kw-enter", "kw-enter",
"kw-exit", "kw-exit",
"kw-start" "kw-start",
"kw-lasts",
"kw-loads",
"kw-shows",
"kw-then"
], ],
"screen": [ "screen": [
"screen-clear", "screen-clear",
@ -276,7 +301,29 @@
"kw-extern", "kw-extern",
"kw-ui", "kw-ui",
"kw-enum", "kw-enum",
"kw-namespace" "kw-namespace",
"kw-action",
"kw-alias",
"kw-as",
"kw-bind",
"kw-component",
"kw-def",
"kw-export",
"kw-friend",
"kw-from",
"kw-internal",
"kw-module",
"kw-mut",
"kw-numbers",
"kw-of",
"kw-open",
"kw-port",
"kw-prop",
"kw-reducer",
"kw-registry",
"kw-unsafe",
"kw-uses",
"kw-view"
], ],
"system": [ "system": [
"system-run", "system-run",

View file

@ -146,11 +146,21 @@ output. The formatter cannot change what a program means.
## Keeping it honest ## Keeping it honest
The vocabulary is written down in five places that cannot include each other — The language's words come from one place: the compiler's vocabulary table
the compiler's two tables, `ludic_syntax.h`, the TextMate grammar (JSON), and the (`selfhost/frontend/vocab.ludic`), printed by `ludic syntax --json` (`ludicc
JetBrains lexer (Kotlin). Adding a builtin and forgetting the rest is silent --emit-syntax`) and held to the parser's own recognisers. The keyword, type and
failure, so `bin/ludic-dev check-vocabulary` (written in Ludic) compares all five, and phase lists in `ludic_syntax.h`, the TextMate grammar (every pattern marked
`bin/ludic-dev test-tools` runs it. `"comment": "ludic-dev syntax: <group>"`), the JetBrains lexer's
`LudicVocabulary`, `ludic-mode.el` and the language server are written from it
between `ludic-dev syntax: begin` / `end` lines by `bin/ludic-dev syntax`; do not
edit inside them.
When you add a keyword or builtin: put it in `ludic_syntax.h`, then run `bin/ludic-dev syntax --check` (and `check-vocabulary`, which `test-tools` runs,
`bin/ludic-dev test-tools` and let it tell you which copies still need it. and the regression suite) fails when a generated list is behind, when a grammar
lacks a word, when docs/language has no page for a keyword, type, phase or
attribute, or when the parser (`selfhost/frontend`) tests a word or reads an
attribute the vocabulary lacks. Builtins are still listed in `ludic_syntax.h`,
and `check-vocabulary` compares them with the grammar and the Kotlin lexer.
When you add a keyword or an attribute: add its row to `vocab.ludic`, rebuild the
compiler, run `bin/ludic-dev syntax`, and write its docs/language page.

View file

@ -10,23 +10,27 @@
;;; Code: ;;; Code:
;; The vocabulary mirrors tools/ludic-tools/ludic_syntax.h; that header is the reference. ;; The vocabulary is written by `ludic-dev syntax` from the compiler's own table
;; (`ludicc --emit-syntax`) between the marked lines.
;; ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
(defconst ludic--declaration-keywords (defconst ludic--declaration-keywords
'("program" "import" "property" "model" "enum" "ui" "namespace" '("program" "module" "import" "property" "state" "event" "action" "reducer" "enum" "model"
"const" "var" "function" "extern" "handler" "entry" "event" "scene" "test" "prefab")) "prefab" "scene" "handler" "ui" "namespace" "alias" "port" "bind" "registry" "def"
"component" "view" "prop" "const" "var" "function" "fn" "extern" "entry" "test"))
(defconst ludic--clause-keywords (defconst ludic--clause-keywords
'("phase" "query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "loads" "then" "export" "internal")) '("export" "internal" "friend" "uses" "open" "of" "as" "from" "numbers" "unsafe" "mut" "phase"
"query" "on" "cancellable" "public" "layer" "start" "shows" "lasts" "then" "loads"
"system"))
(defconst ludic--statement-keywords (defconst ludic--statement-keywords
'("let" "return" "if" "else" "while" "for" "in" "spawn" "despawn" '("let" "if" "else" "while" "for" "in" "where" "match" "machine" "become" "return" "break"
"enable" "disable" "attach" "detach" "match" "machine" "state" "become" "where" "continue" "new" "spawn" "despawn" "enable" "disable" "attach" "detach" "emit" "cancel"
"and" "or" "not" "break" "continue" "new" "emit" "cancel" "try")) "dispatch" "try" "and" "or" "not"))
(defconst ludic--types (defconst ludic--types
'("int" "long" "fixed" "countdown" "bool" "entity" "string" "pointer" "byte" '("int" "long" "float" "double" "fixed" "bool" "byte" "string" "Key" "pointer" "void" "entity"
"words" "fixeds" "pointers" "Vector" "IVec2" "Rect" "void")) "countdown" "words" "floats" "doubles" "fixeds" "pointers" "Vector" "IVec2" "Rect"))
(defconst ludic--phases '("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay")) (defconst ludic--phases
'("Start" "Input" "FixedUpdate" "Update" "LateUpdate" "Render" "Overlay"))
;; ludic-dev syntax: end
(defconst ludic-font-lock-keywords (defconst ludic-font-lock-keywords
(list (list

View file

@ -55,24 +55,37 @@ object LudicTokens {
} }
/** /**
* The vocabulary, mirroring tools/ludic-tools/ludic_syntax.h. When the language * The vocabulary. The keyword, type and phase sets are written by `ludic-dev
* grows a keyword both sides change together; that header is the reference. * syntax` from the compiler's own table (`ludicc --emit-syntax`); the rest
* mirrors tools/ludic-tools/ludic_syntax.h.
*/ */
object LudicVocabulary { object LudicVocabulary {
// ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
val DECL = setOf( val DECL = setOf(
"program", "import", "property", "model", "enum", "ui", "namespace", "program", "module", "import", "property", "state", "event", "action", "reducer", "enum",
"const", "var", "function", "extern", "handler", "entry", "event", "scene", "test" "model", "prefab", "scene", "handler", "ui", "namespace", "alias", "port", "bind",
"registry", "def", "component", "view", "prop", "const", "var", "function", "fn", "extern",
"entry", "test"
) )
val CLAUSE = setOf( val CLAUSE = setOf(
"phase", "query", "on", "cancellable", "public", "layer", "start" "export", "internal", "friend", "uses", "open", "of", "as", "from", "numbers", "unsafe",
"mut", "phase", "query", "on", "cancellable", "public", "layer", "start", "shows", "lasts",
"then", "loads", "system"
) )
val STMT = setOf( val STMT = setOf(
"let", "return", "if", "else", "while", "for", "in", "spawn", "despawn", "let", "if", "else", "while", "for", "in", "where", "match", "machine", "become", "return",
"enable", "disable", "match", "machine", "prefab", "state", "become", "where", "break", "continue", "new", "spawn", "despawn", "enable", "disable", "attach", "detach",
"and", "or", "not", "break", "continue", "new", "emit", "cancel", "try" "emit", "cancel", "dispatch", "try", "and", "or", "not"
) )
val PRIMITIVES = setOf("int", "long", "fixed", "float", "double", "countdown", "bool", "entity", "string", "pointer", "byte", "words", "fixeds", "floats", "doubles", "pointers", "Vector", "IVec2", "Rect", "void") val PRIMITIVES = setOf(
val PHASES = setOf("Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay") "int", "long", "float", "double", "fixed", "bool", "byte", "string", "Key", "pointer",
"void", "entity", "countdown", "words", "floats", "doubles", "fixeds", "pointers", "Vector",
"IVec2", "Rect"
)
val PHASES = setOf(
"Start", "Input", "FixedUpdate", "Update", "LateUpdate", "Render", "Overlay"
)
// ludic-dev syntax: end
val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer") val WIDGETS = setOf("panel", "col", "row", "label", "button", "image", "spacer")
val BUILTINS = setOf( val BUILTINS = setOf(

View file

@ -3,7 +3,7 @@
"name": "Ludic", "name": "Ludic",
"scopeName": "source.ludic", "scopeName": "source.ludic",
"fileTypes": ["ludic"], "fileTypes": ["ludic"],
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. Generated shapes are kept in sync with tools/ludic-tools/ludic_syntax.h.", "comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. The keyword, type and phase alternations are written by `ludic-dev syntax` from the compiler's own vocabulary (`ludicc --emit-syntax`): every pattern marked \"ludic-dev syntax: <group>\".",
"patterns": [ "patterns": [
{ "include": "#comment" }, { "include": "#comment" },
{ "include": "#declaration" }, { "include": "#declaration" },
@ -126,7 +126,7 @@
"captures": { "1": { "name": "keyword.control.import.ludic" } } "captures": { "1": { "name": "keyword.control.import.ludic" } }
}, },
{ {
"match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b", "comment": "ludic-dev syntax: phase-clause", "match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b",
"captures": { "captures": {
"1": { "name": "keyword.other.clause.ludic" }, "1": { "name": "keyword.other.clause.ludic" },
"2": { "name": "constant.language.phase.ludic" } "2": { "name": "constant.language.phase.ludic" }
@ -180,14 +180,13 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" }, { "name": "keyword.control.ludic", "comment": "ludic-dev syntax: statement", "match": "\\b(let|if|else|while|for|in|where|match|machine|become|return|break|continue|new|spawn|despawn|enable|disable|attach|detach|emit|cancel|dispatch|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "comment": "ludic-dev syntax: operator", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" }, { "name": "keyword.other.clause.ludic", "comment": "ludic-dev syntax: modifier", "match": "\\b(export|internal|friend|uses|open|of|as|from|numbers|unsafe|mut|phase|query|on|cancellable|public|layer|start|shows|lasts|then|loads|system)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "storage.type.ludic", "comment": "ludic-dev syntax: declaration", "match": "\\b(program|module|import|property|state|event|action|reducer|enum|model|prefab|scene|handler|ui|namespace|alias|port|bind|registry|def|component|view|prop|const|var|function|fn|extern|entry|test)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" }, { "name": "support.type.primitive.ludic", "comment": "ludic-dev syntax: types", "match": "\\b(int|long|float|double|fixed|bool|byte|string|Key|pointer|void|entity|countdown|words|floats|doubles|fixeds|pointers|Vector|IVec2|Rect)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|float|double|countdown|bool|entity|string|pointer|byte|words|fixeds|floats|doubles|pointers|Vector|IVec2|Rect|void)\\b" }, { "name": "constant.language.boolean.ludic", "comment": "ludic-dev syntax: constant", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.phase.ludic", "comment": "ludic-dev syntax: phases", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
] ]
}, },
"widget": { "widget": {
@ -241,7 +240,7 @@
"patterns": [ "patterns": [
{ "name": "keyword.operator.arrow.ludic", "match": "->|=>" }, { "name": "keyword.operator.arrow.ludic", "match": "->|=>" },
{ "name": "keyword.operator.range.ludic", "match": "\\.\\." }, { "name": "keyword.operator.range.ludic", "match": "\\.\\." },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\|\\^|~" }, { "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\||\\^|~" },
{ "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" }, { "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" },
{ "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" }, { "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" },
{ "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" } { "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" }

View file

@ -3,7 +3,7 @@
"name": "Ludic", "name": "Ludic",
"scopeName": "source.ludic", "scopeName": "source.ludic",
"fileTypes": ["ludic"], "fileTypes": ["ludic"],
"comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. Generated shapes are kept in sync with tools/ludic-tools/ludic_syntax.h.", "comment": "Static grammar for Ludic. It is the fallback: when ludic-lsp is running, its semantic tokens refine everything here with real name resolution. The keyword, type and phase alternations are written by `ludic-dev syntax` from the compiler's own vocabulary (`ludicc --emit-syntax`): every pattern marked \"ludic-dev syntax: <group>\".",
"patterns": [ "patterns": [
{ "include": "#comment" }, { "include": "#comment" },
{ "include": "#declaration" }, { "include": "#declaration" },
@ -126,7 +126,7 @@
"captures": { "1": { "name": "keyword.control.import.ludic" } } "captures": { "1": { "name": "keyword.control.import.ludic" } }
}, },
{ {
"match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render)\\b", "comment": "ludic-dev syntax: phase-clause", "match": "\\b(phase)\\s+(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b",
"captures": { "captures": {
"1": { "name": "keyword.other.clause.ludic" }, "1": { "name": "keyword.other.clause.ludic" },
"2": { "name": "constant.language.phase.ludic" } "2": { "name": "constant.language.phase.ludic" }
@ -180,14 +180,13 @@
}, },
"keyword": { "keyword": {
"patterns": [ "patterns": [
{ "name": "keyword.control.ludic", "match": "\\b(if|else|while|for|in|match|machine|become|return|spawn|despawn|enable|disable|attach|detach|emit|cancel|where|break|continue|new|try)\\b" }, { "name": "keyword.control.ludic", "comment": "ludic-dev syntax: statement", "match": "\\b(let|if|else|while|for|in|where|match|machine|become|return|break|continue|new|spawn|despawn|enable|disable|attach|detach|emit|cancel|dispatch|try)\\b" },
{ "name": "keyword.operator.logical.ludic", "match": "\\b(and|or|not)\\b" }, { "name": "keyword.operator.logical.ludic", "comment": "ludic-dev syntax: operator", "match": "\\b(and|or|not)\\b" },
{ "name": "keyword.other.clause.ludic", "match": "\\b(phase|query|on|cancellable|public|layer|start|shows|lasts|loads|then|export|internal)\\b" }, { "name": "keyword.other.clause.ludic", "comment": "ludic-dev syntax: modifier", "match": "\\b(export|internal|friend|uses|open|of|as|from|numbers|unsafe|mut|phase|query|on|cancellable|public|layer|start|shows|lasts|then|loads|system)\\b" },
{ "name": "keyword.other.ludic", "match": "\\b(import|extern)\\b" }, { "name": "storage.type.ludic", "comment": "ludic-dev syntax: declaration", "match": "\\b(program|module|import|property|state|event|action|reducer|enum|model|prefab|scene|handler|ui|namespace|alias|port|bind|registry|def|component|view|prop|const|var|function|fn|extern|entry|test)\\b" },
{ "name": "storage.type.ludic", "match": "\\b(program|property|model|prefab|namespace|enum|ui|const|var|let|function|fn|handler|entry|event|scene|state|test)\\b" }, { "name": "support.type.primitive.ludic", "comment": "ludic-dev syntax: types", "match": "\\b(int|long|float|double|fixed|bool|byte|string|Key|pointer|void|entity|countdown|words|floats|doubles|fixeds|pointers|Vector|IVec2|Rect)\\b" },
{ "name": "support.type.primitive.ludic", "match": "\\b(int|long|fixed|float|double|countdown|bool|entity|string|pointer|byte|words|fixeds|floats|doubles|pointers|Vector|IVec2|Rect|void)\\b" }, { "name": "constant.language.boolean.ludic", "comment": "ludic-dev syntax: constant", "match": "\\b(true|false|null)\\b" },
{ "name": "constant.language.boolean.ludic", "match": "\\b(true|false|null)\\b" }, { "name": "constant.language.phase.ludic", "comment": "ludic-dev syntax: phases", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
{ "name": "constant.language.phase.ludic", "match": "\\b(Start|Input|FixedUpdate|Update|LateUpdate|Render|Overlay)\\b" }
] ]
}, },
"widget": { "widget": {
@ -241,7 +240,7 @@
"patterns": [ "patterns": [
{ "name": "keyword.operator.arrow.ludic", "match": "->|=>" }, { "name": "keyword.operator.arrow.ludic", "match": "->|=>" },
{ "name": "keyword.operator.range.ludic", "match": "\\.\\." }, { "name": "keyword.operator.range.ludic", "match": "\\.\\." },
{ "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\|\\^|~" }, { "name": "keyword.operator.bitwise.ludic", "match": "<<|>>|&|\\||\\^|~" },
{ "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" }, { "name": "keyword.operator.comparison.ludic", "match": "==|!=|<=|>=|<|>" },
{ "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" }, { "name": "keyword.operator.assignment.ludic", "match": "\\+=|-=|\\*=|/=|=" },
{ "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" } { "name": "keyword.operator.arithmetic.ludic", "match": "[+\\-*/%]" }

View file

@ -484,19 +484,7 @@ function grammar_alt(root: JVal, node: pointer, marker: pointer) -> []pointer {
return out return out
} }
# keywords the self-host parser dispatches on: is_id("x") + streq(t.text, "x") # the lower-case names among `src` into `out` (a set)
function parser_keywords() -> []pointer {
let out = new []pointer
add_parser_kw(out, "selfhost/frontend/parse.ludic")
add_parser_kw(out, "selfhost/frontend/parse_game.ludic")
return out
}
function add_parser_kw(out: []pointer, path: pointer) -> void {
let t = read_file(path)
if t == null { return }
add_lower(out, collect_after(t, "is_id(" + dq()))
add_lower(out, collect_after(t, "streq(t.text, " + dq()))
}
function add_lower(out: []pointer, src: []pointer) -> void { function add_lower(out: []pointer, src: []pointer) -> void {
var i = 0 var i = 0
while i < len(src) { if all_lower(src[i]) { set_add(out, src[i]) }; i += 1 } while i < len(src) { if all_lower(src[i]) { set_add(out, src[i]) }; i += 1 }
@ -537,7 +525,6 @@ function cmd_check_vocab() -> int {
let h_widgets = table_set(h, "LUDIC_WIDGETS") let h_widgets = table_set(h, "LUDIC_WIDGETS")
let h_builtins = c_table_names(h, "LUDIC_BUILTINS") let h_builtins = c_table_names(h, "LUDIC_BUILTINS")
let h_intrinsics = c_table_names(h, "LUDIC_INTRINSICS") let h_intrinsics = c_table_names(h, "LUDIC_INTRINSICS")
let h_reserved = table_set(h, "LUDIC_KW_RESERVED")
# --- against the JetBrains lexer --- # --- against the JetBrains lexer ---
let kt = read_file("tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt") let kt = read_file("tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt")
@ -561,16 +548,26 @@ function cmd_check_vocab() -> int {
cmp_sets("primitive types", h_types, grammar_alt(g, "keyword", "fixed"), "ludic.tmLanguage.json") cmp_sets("primitive types", h_types, grammar_alt(g, "keyword", "fixed"), "ludic.tmLanguage.json")
} }
# --- against the self-host parser --- # --- the keyword, type and phase tables, the docs and the parser, against the compiler's own
let pkw = parser_keywords() # vocabulary (ludicc --emit-syntax; syntax_gen.ludic) ---
if len(pkw) == 0 { if sx_vocab() == null { cv_problem("bin/ludicc has no --emit-syntax: rebuild it (ludic-dev build)") }
cv_problem("could not extract any keywords from selfhost/frontend/parse*.ludic") else {
} else { g_sx_prob = new []pointer
cmp_unparsed("declaration keywords", h_decl, pkw, h_reserved) let ts = sx_targets()
cmp_unparsed("clause keywords", h_clause, pkw, h_reserved) var ti = 0
# reserved words the parser now accepts should be promoted while ti < len(ts) {
var i = 0 let text = read_file(ts[ti])
while i < len(h_reserved) { if set_has(pkw, h_reserved[i]) { cv_problem("LUDIC_KW_RESERVED lists a keyword the parser now accepts — promote it: " + h_reserved[i]) }; i += 1 } if text != null {
let want = sx_regenerate(ts[ti], text)
if want != null and not (want == text) { cv_problem(ts[ti] + " is behind the vocabulary - run ludic-dev syntax") }
}
sx_carries(ts[ti])
ti += 1
}
sx_docs()
sx_parser()
var pi2 = 0
while pi2 < len(g_sx_prob) { cv_problem(g_sx_prob[pi2]); pi2 += 1 }
} }
if CV_N > 0 { if CV_N > 0 {
@ -581,16 +578,6 @@ function cmd_check_vocab() -> int {
return 0 return 0
} }
# keywords in `kws` the parser never dispatches on (minus reserved) are a problem
function cmp_unparsed(label: pointer, kws: []pointer, pkw: []pointer, reserved: []pointer) -> void {
var i = 0
while i < len(kws) {
if not set_has(pkw, kws[i]) {
if not set_has(reserved, kws[i]) { cv_problem("ludic_syntax.h lists a " + label + " the selfhost parser never dispatches on: " + kws[i]) }
}
i += 1
}
}
# ============================================================================ # ============================================================================
# json/xml asset validation — replaces the python3 json.load / xml.dom checks # json/xml asset validation — replaces the python3 json.load / xml.dom checks

View file

@ -30,6 +30,7 @@ program LudicDev {
import "lsp_test.ludic" import "lsp_test.ludic"
import "forgejo.ludic" import "forgejo.ludic"
import "checks.ludic" import "checks.ludic"
import "syntax_gen.ludic"
import "docgen.ludic" import "docgen.ludic"
import "docgen_gen.ludic" import "docgen_gen.ludic"
import "docgen_check.ludic" import "docgen_check.ludic"
@ -67,6 +68,7 @@ program LudicDev {
print(" check-docs every ```ludic doc fence parses (or is marked skip/expect-error)") print(" check-docs every ```ludic doc fence parses (or is marked skip/expect-error)")
print(" check-impl every implemented feature has a docs/language page") print(" check-impl every implemented feature has a docs/language page")
print(" check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser") print(" check-vocabulary the vocabulary is in sync across grammar / lexer / header / parser")
print(" syntax [--check] write every grammar's keyword, type and phase lists from ludicc --emit-syntax")
print(" lint-asset <file> validate one editor .json / .xml asset") print(" lint-asset <file> validate one editor .json / .xml asset")
print(" docs-gen [--out DIR] generate the documentation site (default build/pages)") print(" docs-gen [--out DIR] generate the documentation site (default build/pages)")
print(" docs-check [DIR] coverage/integrity guard over a generated docs site") print(" docs-check [DIR] coverage/integrity guard over a generated docs site")
@ -108,6 +110,7 @@ program LudicDev {
if (cmd == "check-docs") { return cmd_check_docs() } if (cmd == "check-docs") { return cmd_check_docs() }
if (cmd == "check-impl") { return cmd_check_impl() } if (cmd == "check-impl") { return cmd_check_impl() }
if (cmd == "check-vocabulary") { return cmd_check_vocab() } if (cmd == "check-vocabulary") { return cmd_check_vocab() }
if (cmd == "syntax") { return cmd_syntax_gen() }
if (cmd == "lint-asset") { return cmd_lint_asset() } if (cmd == "lint-asset") { return cmd_lint_asset() }
if (cmd == "docs-palette") { return cmd_docs_palette() } if (cmd == "docs-palette") { return cmd_docs_palette() }
if (cmd == "glgen") { return cmd_glgen() } if (cmd == "glgen") { return cmd_glgen() }

View file

@ -286,6 +286,20 @@ function cmd_test_lsp() -> int {
lsp_notify("textDocument/didSave", `{{"textDocument":{lsp_doc(ccf)}}}`) lsp_notify("textDocument/didSave", `{{"textDocument":{lsp_doc(ccf)}}}`)
lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(ccf)}}}`) lsp_request("textDocument/documentSymbol", `{{"textDocument":{lsp_doc(ccf)}}}`)
# ---- the declarations a module file holds (R8): exported records, a registry with its
# attributes, states, events, actions, ports, enums, reducers and components, every member
let dsrc = "module kits uses base\n\n# a kit\nexport property Kit {\n key: string = \"\"\n @Asset(\"gltf\") model: string = \"\"\n @Node(model) grip: string = \"\"\n @Ref(Kits) spare: int = 0\n pick: fn(int, float) -> bool\n}\n\n# every kit\n@AppendOnly @ByKey\nexport registry Kits of Kit as KIT from \"kits.lres\"\nexport state KitState {\n held: int = -1\n seen: []int\n}\nexport event KitDropped { kit: int, x: float }\nexport action PickKit { kit: int }\nexport port KitWorld {\n ground: fn(float, float) -> float\n}\nenum Grade { Low, High }\nreducer KitState on PickKit(st: mut KitState, a: PickKit) {\n st.held = a.kit\n}\ncomponent Tray(k: KitState) {\n prop step: int = 1\n state open: bool = false\n on press() { }\n}\nexport function kit_weight(k: Kit) -> int { return k.spare }\n"
let dfile = tmp_path("lsp_decls.ludic")
write_file(dfile, dsrc)
lsp_open(dfile, "ludic")
let duri = lsp_doc(dfile)
let dsyms = lsp_request("textDocument/documentSymbol", `{{"textDocument":{duri}}}`)
let dref = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
let dhov = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 6)}}}`)
let dnode = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Node(model)", 7)}}}`)
let dty = lsp_request("textDocument/definition", `{{"textDocument":{duri},"position":{position_of(dsrc, "kit_weight(k: Kit)", 14)}}}`)
let dat = lsp_request("textDocument/hover", `{{"textDocument":{duri},"position":{position_of(dsrc, "@Ref(Kits)", 1)}}}`)
# ---- workspace-wide symbol search --------------------------------------- # ---- workspace-wide symbol search ---------------------------------------
let ws = lsp_request("workspace/symbol", `{{"query":"Stats"}}`) let ws = lsp_request("workspace/symbol", `{{"query":"Stats"}}`)
@ -456,5 +470,43 @@ function cmd_test_lsp() -> int {
check_true("a name inside a template hole resolves", Text.contains(j_get(j_get(lsp_result(thov), "contents"), "value").s, "param variant: int")) check_true("a name inside a template hole resolves", Text.contains(j_get(j_get(lsp_result(thov), "contents"), "value").s, "param variant: int"))
check("find-usages reaches into template holes", loc_lines_key(lsp_result(trefs)), "1,3,4") check("find-usages reaches into template holes", loc_lines_key(lsp_result(trefs)), "1,3,4")
check_true("positions after a multi-line template stay on the right line", Text.contains(j_get(j_get(lsp_result(tafter), "contents"), "value").s, "let multi")) check_true("positions after a multi-line template stay on the right line", Text.contains(j_get(j_get(lsp_result(tafter), "contents"), "value").s, "let multi"))
# a module file's declarations, each by its own name and kind, with every member as a child
let dk = lsp_result(dsyms)
check_true("an exported property is a struct named for itself, all its fields children", lsp_sym_is(dk, "Kit", 23, 5))
check_true("a registry is a symbol of its own, with what it holds", lsp_sym_is(dk, "Kits", 18, 0) and Text.contains(lsp_sym_detail(dk, "Kits"), "of Kit as KIT"))
check_true("a state is a struct with its fields", lsp_sym_is(dk, "KitState", 23, 2))
check_true("an event and an action are events", lsp_sym_is(dk, "KitDropped", 24, 2) and lsp_sym_is(dk, "PickKit", 24, 1))
check_true("a port is an interface, a fn-typed member written out", lsp_sym_is(dk, "KitWorld", 11, 1) and Text.contains(lsp_sym_detail(dk, "KitWorld"), "fn(float, float) -> float"))
check_true("an enum and its members", lsp_sym_is(dk, "Grade", 10, 2))
check_true("a reducer is named for its state and its action", lsp_sym_is(dk, "KitState on PickKit", 12, -1))
check_true("a component holds its props, state and events", lsp_sym_is(dk, "Tray", 5, 3))
check_true("an exported function is still a function", lsp_sym_is(dk, "kit_weight", 12, -1))
check_true("go-to-definition on @Ref(Kits) reaches the registry", j_get(j_get(j_get(lsp_result(dref), "range"), "start"), "line").num == 13)
check_true("hover on @Ref(Kits) shows the registry", Text.contains(j_get(j_get(lsp_result(dhov), "contents"), "value").s, "registry Kits of Kit"))
check_true("go-to-definition on @Node(model) reaches the field it names", j_get(j_get(j_get(lsp_result(dnode), "range"), "start"), "line").num == 5)
check_true("go-to-definition on an exported record's type name", j_get(j_get(j_get(lsp_result(dty), "range"), "start"), "line").num == 3)
check_true("hover on an attribute shows its reference page", Text.contains(j_get(j_get(lsp_result(dat), "contents"), "value").s, "@Ref(Registry)"))
return report() return report()
} }
# the symbol named `name` among `syms` has LSP kind `kind` and, unless n is -1, n children
function lsp_sym_is(syms: JVal, name: pointer, kind: int, n: int) -> bool {
var i = 0
while i < len(syms.kids) {
let y = syms.kids[i]
if j_get(y, "name").s == name {
if j_get(y, "kind").num != kind { return false }
return n < 0 or len(j_get(y, "children").kids) == n
}
i += 1
}
return false
}
function lsp_sym_detail(syms: JVal, name: pointer) -> pointer {
var i = 0
while i < len(syms.kids) {
if j_get(syms.kids[i], "name").s == name { return j_get(syms.kids[i], "detail").s }
i += 1
}
return ""
}

View file

@ -32,6 +32,7 @@ program Ludic {
import "scripts.ludic" import "scripts.ludic"
import "deps.ludic" import "deps.ludic"
import "schema.ludic" import "schema.ludic"
import "syntax_cli.ludic"
import "ui_preview.ludic" import "ui_preview.ludic"
import "testpar.ludic" import "testpar.ludic"
import "migrate.ludic" import "migrate.ludic"
@ -48,11 +49,13 @@ program Ludic {
print(" compile to build/<name> (a native binary, nothing to ship beside it)") print(" compile to build/<name> (a native binary, nothing to ship beside it)")
print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted") print(" build [file] --check only check it: types, modules, uses and ports; nothing emitted")
print(" build [file] --check --diagnostics=json every error as a JSON array on stdout") print(" build [file] --check --diagnostics=json every error as a JSON array on stdout")
print(" build [file] --check --stdin-file PATH stdin is PATH's unsaved text (an editor's buffer)")
print(" test [file|dir...] [-j N] [-v] [--test NAME]") print(" test [file|dir...] [-j N] [-v] [--test NAME]")
print(" compile and run the project's tests (-v: every test's line)") print(" compile and run the project's tests (-v: every test's line)")
print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]") print(" deps [file] [--graph|--dot|--writes|--uses MOD|--check F|--baseline F]")
print(" the module graph as the compiler sees it, and how tangled it is") print(" the module graph as the compiler sees it, and how tangled it is")
print(" schema [file] [-o FILE] records, registries and their entries, consts, as JSON (for editors)") print(" schema [file] [-o FILE] records, registries and their entries, consts, as JSON (for editors)")
print(" syntax [--json] [-o FILE] the language's vocabulary: keywords, declarations, types, phases, attributes, operators")
print(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses, --tighten also mut nothing writes") print(" migrate state [file] [--prune] [--runtime] module-level vars into states, passed as parameters (0.S); --prune drops states nothing uses, --tighten also mut nothing writes")
print(" clean remove build/") print(" clean remove build/")
print("") print("")
@ -108,6 +111,7 @@ program Ludic {
if (cmd == "test") { return cmd_test() } if (cmd == "test") { return cmd_test() }
if (cmd == "deps") { return cmd_deps() } if (cmd == "deps") { return cmd_deps() }
if (cmd == "schema") { return cmd_schema() } if (cmd == "schema") { return cmd_schema() }
if (cmd == "syntax") { return cmd_syntax() }
if (cmd == "migrate") { return cmd_migrate() } if (cmd == "migrate") { return cmd_migrate() }
if (cmd == "clean") { return cmd_clean() } if (cmd == "clean") { return cmd_clean() }
if (cmd == "fmt") { return cmd_fmt() } if (cmd == "fmt") { return cmd_fmt() }

View file

@ -280,6 +280,11 @@ function parse_build_args(start: int) -> pointer {
else if a == "--no-maps" { g_no_maps_build = true } # --check leaves the map directories alone else if a == "--no-maps" { g_no_maps_build = true } # --check leaves the map directories alone
else if a == "--maps" { g_maps_build = true } # ... or reads them, whatever program it checks else if a == "--maps" { g_maps_build = true } # ... or reads them, whatever program it checks
else if a == "--diagnostics=json" { g_check_build = true; g_diag_json_build = true } # every error, as JSON on stdout else if a == "--diagnostics=json" { g_check_build = true; g_diag_json_build = true } # every error, as JSON on stdout
else if a == "--stdin-file" { # stdin is that file's unsaved text: checked, never built
ai += 1
if ai < arg_count() { g_stdin_file_build = arg(ai); g_check_build = true }
else { err("ludic: --stdin-file needs a path\n"); g_argerr = true }
}
else if a == "-o" { else if a == "-o" {
ai += 1 ai += 1
if ai < arg_count() { g_out = arg(ai) } if ai < arg_count() { g_out = arg(ai) }
@ -287,7 +292,7 @@ function parse_build_args(start: int) -> pointer {
} }
else if a[0] == '-' { else if a[0] == '-' {
err(`ludic: unknown option {a}\n`) err(`ludic: unknown option {a}\n`)
err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check [--diagnostics=json] [--maps|--no-maps]]\n") err(" build/run take: [file] [--headless|--windowed] [-o out] [--save-temps] [--check [--diagnostics=json] [--stdin-file path] [--maps|--no-maps]]\n")
g_argerr = true g_argerr = true
} }
else { src = a } else { src = a }
@ -311,6 +316,7 @@ function output_path(entry: pointer) -> pointer {
# and nothing emitted or linked; a fraction of a build's time, for iterating on uses lines # and nothing emitted or linked; a fraction of a build's time, for iterating on uses lines
var g_check_build: bool = false var g_check_build: bool = false
var g_diag_json_build: bool = false # --diagnostics=json: the compiler's JSON array is the whole stdout var g_diag_json_build: bool = false # --diagnostics=json: the compiler's JSON array is the whole stdout
var g_stdin_file_build: pointer = "" # --stdin-file <path>: the compiler reads stdin where it would read <path>
var g_no_maps_build: bool = false var g_no_maps_build: bool = false
var g_maps_build: bool = false var g_maps_build: bool = false
# the maps are checked with the package's entry - the game - and left alone for any other program: a # the maps are checked with the package's entry - the game - and left alone for any other program: a
@ -330,13 +336,17 @@ function bld_same_path(a: pointer, b: pointer) -> bool {
if len(y) > 2 and y[0] == '.' and y[1] == '/' { y = y[2..len(y)] } if len(y) > 2 and y[0] == '.' and y[1] == '/' { y = y[2..len(y)] }
return x == y return x == y
} }
function stdin_file_flag() -> pointer {
if g_stdin_file_build == "" { return "" }
return ` --stdin-file {sh_single(g_stdin_file_build)}`
}
function check_app(src: pointer) -> int { function check_app(src: pointer) -> int {
ensure_ludicc() ensure_ludicc()
if g_diag_json_build { if g_diag_json_build {
if not shq(`{ludicc()} --check --diagnostics=json{no_maps_flag(src)}{title_flag()}{unsafe_flag()} {src}`) { return 1 } if not shq(`{ludicc()} --check --diagnostics=json{no_maps_flag(src)}{title_flag()}{unsafe_flag()}{stdin_file_flag()} {src}`) { return 1 }
return 0 return 0
} }
if not shq(`{ludicc()} --check{no_maps_flag(src)}{title_flag()}{unsafe_flag()} {src}`) { return 1 } if not shq(`{ludicc()} --check{no_maps_flag(src)}{title_flag()}{unsafe_flag()}{stdin_file_flag()} {src}`) { return 1 }
print(`checked {src}`) print(`checked {src}`)
return 0 return 0
} }

View file

@ -17,6 +17,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/ast.ludic") push(f, "selfhost/frontend/ast.ludic")
push(f, "selfhost/frontend/lex.ludic") push(f, "selfhost/frontend/lex.ludic")
push(f, "selfhost/frontend/diag.ludic") push(f, "selfhost/frontend/diag.ludic")
push(f, "selfhost/frontend/stdin_file.ludic")
push(f, "selfhost/frontend/parse.ludic") push(f, "selfhost/frontend/parse.ludic")
push(f, "selfhost/frontend/parse_game.ludic") push(f, "selfhost/frontend/parse_game.ludic")
push(f, "selfhost/frontend/generics.ludic") push(f, "selfhost/frontend/generics.ludic")
@ -34,6 +35,7 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/registry_open.ludic") push(f, "selfhost/frontend/registry_open.ludic")
push(f, "selfhost/frontend/attrs.ludic") push(f, "selfhost/frontend/attrs.ludic")
push(f, "selfhost/frontend/attrs_node.ludic") push(f, "selfhost/frontend/attrs_node.ludic")
push(f, "selfhost/frontend/vocab.ludic")
push(f, "selfhost/frontend/resource.ludic") push(f, "selfhost/frontend/resource.ludic")
push(f, "selfhost/frontend/permap.ludic") push(f, "selfhost/frontend/permap.ludic")
push(f, "selfhost/frontend/permap_consts.ludic") push(f, "selfhost/frontend/permap_consts.ludic")

View file

@ -0,0 +1,86 @@
# ---- ludic syntax -------------------------------------------------------------
# The language's vocabulary as the compiler holds it (`ludicc --emit-syntax`,
# selfhost/frontend/vocab.ludic): every keyword with its role and whether it is
# reserved, every declaration's form, the built-in types, the phases, every
# attribute with where it goes, what it takes and what it means, the operators and
# the literal forms. An editor's grammar is written from it (`ludic-dev syntax`).
#
# ludic syntax one line per entry: kind, name, role
# ludic syntax --json the JSON, as the compiler writes it
# ludic syntax ... -o FILE into FILE
# the vocabulary's JSON from the compiler, or null when it cannot say
function syntax_json_text() -> pointer {
ensure_ludicc()
let js = tmp_path("syntax.json")
if not shq(`{ludicc()} --emit-syntax > {sh_single(js)} 2> {tmp_path("syntax.err")}`) { return null }
let s = read_file(js)
if s == null or slen(s) == 0 or s[0] != '{' { return null }
return s
}
function cmd_syntax() -> int {
var json = false
var dest = ""
var ai = 2
while ai < arg_count() {
let a = arg(ai)
if a == "--json" { json = true }
else if a == "-o" or a == "--out" {
if ai + 1 >= arg_count() { err(`ludic syntax: {a} needs a file\n`); return 2 }
ai += 1
dest = arg(ai)
}
else {
err(`ludic syntax: unknown argument {a}\n`)
err(" usage: ludic syntax [--json] [-o FILE]\n")
return 2
}
ai += 1
}
let s = syntax_json_text()
if s == null { err("ludic syntax: the compiler wrote no vocabulary (is it older than ludic syntax?)\n"); return 1 }
var text = s
if not json { text = syntax_lines(s) }
if dest == "" { out(text); return 0 }
if not write_file(dest, text) { err(`ludic syntax: cannot write {dest}\n`); return 1 }
return 0
}
# the plain listing, `keyword module declaration`, one entry a line - read off the JSON's own
# layout (a list opens on ` "name": [`, one entry per line after it) rather than parsed
function syntax_lines(s: pointer) -> pointer {
var o = ""
var kind = ""
let n = slen(s)
var i = 0
while i < n {
let ln = line_at(s, i)
i = i + slen(ln) + 1
if s_starts(ln, " \"") and s_contains(ln, "[") {
kind = sslice(ln, 3, s_index(ln, "\"", 3) - 1) # "keywords" -> keyword
continue
}
if not s_starts(ln, " {\"") { continue }
var name = syntax_field(ln, s_index(ln, "\": ", 0) + 3)
if kind == "attribute" { name = "@" + name }
var role = ""
let r = s_index(ln, "\"role\": ", 0)
if r >= 0 { role = syntax_field(ln, r + 8) }
let f = s_index(ln, "\"form\": ", 0)
if f >= 0 { role = syntax_field(ln, f + 8) }
o = o + kind + "\t" + name + "\t" + role + "\n"
}
return o
}
# the JSON string starting at ln[q] (its opening quote), unescaped
function syntax_field(ln: pointer, q: int) -> pointer {
var o = ""
var i = q + 1
while ln[i] != 0 and ln[i] != '"' {
if ln[i] == '\\' { i += 1 }
o = o + sslice(ln, i, i + 1)
i += 1
}
return o
}

View file

@ -0,0 +1,508 @@
# syntax_gen.ludic — one vocabulary, written into every editor's grammar and checked everywhere
# else (`ludic-dev syntax [--check]`).
#
# The vocabulary is what `ludicc --emit-syntax` prints (selfhost/frontend/vocab.ludic, held to the
# parser's own recognisers). From it, between marked lines, this writes:
#
# tools/ludic-tools/ludic_syntax.h LUDIC_KW_DECL / _CLAUSE / _STMT, LUDIC_TYPES,
# LUDIC_PHASES, LUDIC_ATTRIBUTES
# tools/ludic-tools/lsp.ludic, lsp/types.ludic the server's word tests (keyword, type, phase,
# the words that may also be names)
# tools/editors/shared/ludic.tmLanguage.json every pattern marked "ludic-dev syntax: <group>"
# (and its copy in tools/editors/vscode/syntaxes/)
# tools/editors/jetbrains/.../LudicTokens.kt LudicVocabulary's keyword, type and phase sets
# tools/editors/emacs/ludic-mode.el the keyword, type and phase lists
#
# and it checks what cannot be written: that docs/language has a page for every keyword, type,
# phase and attribute, and that the parser tests no word and reads no attribute the vocabulary
# lacks (a scan of selfhost/frontend) - nor the vocabulary lists one the parser never tests.
#
# ludic-dev syntax rewrite the marked regions from the vocabulary
# ludic-dev syntax --check fail, naming each, on a region behind the vocabulary or a gap
var g_sx: JVal = null
var g_sx_prob: []pointer = new []pointer
# the vocabulary, from bin/ludicc (null when that compiler has no --emit-syntax)
function sx_vocab() -> JVal {
if g_sx != null { return g_sx }
ensure_ludicc()
let js = tmp_path("syntax.json")
if not shq(`bin/ludicc --emit-syntax > {js} 2> {tmp_path("syntax.err")}`) { return null }
let s = read_file(js)
if s == null or slen(s) == 0 or s[0] != '{' { return null }
g_sx = json_parse(s)
return g_sx
}
# the `key` of every entry of `list` whose role is `role` ("" for all)
function sx_words(list: pointer, key: pointer, role: pointer) -> []pointer {
let out = new []pointer
let xs = j_get(g_sx, list)
var i = 0
while i < len(xs.kids) {
let e = xs.kids[i]
if slen(role) == 0 or j_get(e, "role").s == role { push(out, j_get(e, key).s) }
i += 1
}
return out
}
function sx_kw(role: pointer) -> []pointer { return sx_words("keywords", "word", role) }
# a statement is written like one, and and/or/not highlight with them
function sx_stmt() -> []pointer { return set_union(sx_kw("statement"), sx_kw("operator")) }
# every keyword that is not a literal (true, false and null lex as booleans)
function sx_all_kw() -> []pointer { return set_union(set_union(sx_kw("declaration"), sx_kw("modifier")), sx_stmt()) }
# the words the language server lets stand as a name where one is written (`var view = ...`,
# `a.model`): every word the parser does not reserve, and every declaring or modifying word
function sx_contextual() -> []pointer {
let out = new []pointer
let xs = j_get(g_sx, "keywords")
var i = 0
while i < len(xs.kids) {
let e = xs.kids[i]
let r = j_get(e, "role").s
if (j_get(e, "reserved").b == 0 and r != "constant" and r != "operator") or r == "declaration" or r == "modifier" { push(out, j_get(e, "word").s) }
i += 1
}
return out
}
function sx_attr_names() -> []pointer {
let out = new []pointer
let xs = sx_words("attributes", "name", "")
var i = 0
while i < len(xs) { push(out, "@" + xs[i]); i += 1 }
return out
}
# ---- writing a list in each file's own spelling ----
# words quoted with `q`, joined by `sep`, after `lead` on the first line and under `indent` on the
# rest, wrapped before column `width`; `last` follows the final word (a C table's terminating 0)
function sx_wrap(words: []pointer, q: pointer, sep: pointer, lead: pointer, indent: pointer, width: int, last: pointer) -> pointer {
var out = lead
var col = slen(lead)
var fresh = true
var i = 0
while i < len(words) {
var item = q + words[i] + q
if i + 1 < len(words) { item = item + s_trim(sep) } else { item = item + last }
var gap = ""
if not fresh { gap = sep_gap(sep) }
if not fresh and col + slen(gap) + slen(item) > width {
out = out + "\n" + indent
col = slen(indent)
gap = ""
}
out = out + gap + item
col = col + slen(gap) + slen(item)
fresh = false
i += 1
}
return out
}
# the spaces a separator puts between two items (", " -> " ", "," -> "")
function sep_gap(sep: pointer) -> pointer {
if s_ends(sep, " ") { return " " }
return ""
}
function sx_c_table(name: pointer, words: []pointer) -> pointer {
return "static const char* " + name + "[] = {\n" + sx_wrap(words, "\"", ",", " ", " ", 100, ", 0") + "\n};\n"
}
function sx_kt_set(name: pointer, words: []pointer) -> pointer {
return " val " + name + " = setOf(\n" + sx_wrap(words, "\"", ", ", " ", " ", 100, "") + "\n )\n"
}
function sx_el_list(name: pointer, words: []pointer) -> pointer {
return "(defconst " + name + "\n" + sx_wrap(words, "\"", " ", " '(", " ", 96, "))") + "\n"
}
# a Ludic word test: `if (w == "a") or ... { return true }`, six to a line
function sx_ludic_test(fname: pointer, words: []pointer, ind: pointer) -> pointer {
var out = ind + "function " + fname + "(w: pointer) -> bool {\n"
var i = 0
while i < len(words) {
var line = ind + " if "
var k = 0
while k < 6 and i < len(words) {
if k > 0 { line = line + " or " }
line = line + "(w == \"" + words[i] + "\")"
k += 1
i += 1
}
out = out + line + " { return true }\n"
}
return out + ind + " return false\n" + ind + "}\n"
}
# a TextMate alternation, as it is written inside a JSON string: \\b(a|b|c)\\b
function sx_tm_alt(words: []pointer) -> pointer {
var out = ""
var i = 0
while i < len(words) {
if i > 0 { out = out + "|" }
out = out + words[i]
i += 1
}
return "\\\\b(" + out + ")\\\\b"
}
# ---- the regions ----
function sx_begin_note() -> pointer { return "ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit" }
function sx_header_region() -> pointer {
var o = "/* " + sx_begin_note() + " */\n"
o = o + sx_c_table("LUDIC_KW_DECL", sx_kw("declaration"))
o = o + sx_c_table("LUDIC_KW_CLAUSE", sx_kw("modifier"))
o = o + sx_c_table("LUDIC_KW_STMT", sx_stmt())
o = o + sx_c_table("LUDIC_TYPES", sx_words("types", "name", ""))
o = o + sx_c_table("LUDIC_PHASES", sx_words("phases", "name", ""))
o = o + sx_c_table("LUDIC_ATTRIBUTES", sx_words("attributes", "name", ""))
return o + "/* ludic-dev syntax: end */\n"
}
function sx_kotlin_region() -> pointer {
var o = " // " + sx_begin_note() + "\n"
o = o + sx_kt_set("DECL", sx_kw("declaration"))
o = o + sx_kt_set("CLAUSE", sx_kw("modifier"))
o = o + sx_kt_set("STMT", sx_stmt())
o = o + sx_kt_set("PRIMITIVES", sx_words("types", "name", ""))
o = o + sx_kt_set("PHASES", sx_words("phases", "name", ""))
return o + " // ludic-dev syntax: end\n"
}
function sx_emacs_region() -> pointer {
var o = ";; " + sx_begin_note() + "\n"
o = o + sx_el_list("ludic--declaration-keywords", sx_kw("declaration"))
o = o + sx_el_list("ludic--clause-keywords", sx_kw("modifier"))
o = o + sx_el_list("ludic--statement-keywords", sx_stmt())
o = o + sx_el_list("ludic--types", sx_words("types", "name", ""))
o = o + sx_el_list("ludic--phases", sx_words("phases", "name", ""))
return o + ";; ludic-dev syntax: end\n"
}
function sx_lsp_region() -> pointer {
var o = " # " + sx_begin_note() + "\n"
o = o + sx_ludic_test("is_type_word", sx_words("types", "name", ""), " ")
o = o + sx_ludic_test("is_phase_word", sx_words("phases", "name", ""), " ")
o = o + sx_ludic_test("is_keyword_word", sx_all_kw(), " ")
return o + " # ludic-dev syntax: end\n"
}
function sx_lsp_types_region() -> pointer {
return "# " + sx_begin_note() + "\n" + sx_ludic_test("is_contextual_word", sx_contextual(), "") + "# ludic-dev syntax: end\n"
}
# `text` with the lines from the one holding "ludic-dev syntax: begin" to the one holding
# "ludic-dev syntax: end" replaced by `region`; null when the markers are not there
function sx_splice(text: pointer, region: pointer) -> pointer {
let b = s_index(text, "ludic-dev syntax: begin", 0)
if b < 0 { return null }
let e = s_index(text, "ludic-dev syntax: end", b)
if e < 0 { return null }
var ls = b
while ls > 0 and text[ls - 1] != '\n' { ls -= 1 }
var le = e
while text[le] != 0 and text[le] != '\n' { le += 1 }
if text[le] == '\n' { le += 1 }
return sslice(text, 0, ls) + region + sslice(text, le, slen(text))
}
# the TextMate grammar: each pattern whose line says "comment": "ludic-dev syntax: <group>" has its
# "match" on that line rewritten
function sx_tm_group(group: pointer) -> pointer {
if group == "declaration" { return sx_tm_alt(sx_kw("declaration")) }
if group == "modifier" { return sx_tm_alt(sx_kw("modifier")) }
if group == "statement" { return sx_tm_alt(sx_kw("statement")) }
if group == "operator" { return sx_tm_alt(sx_kw("operator")) }
if group == "constant" { return sx_tm_alt(sx_kw("constant")) }
if group == "types" { return sx_tm_alt(sx_words("types", "name", "")) }
if group == "phases" { return sx_tm_alt(sx_words("phases", "name", "")) }
if group == "phase-clause" {
let alt = sx_tm_alt(sx_words("phases", "name", ""))
return "\\\\b(phase)\\\\s+" + sslice(alt, 3, slen(alt))
}
return null
}
function sx_tm_rewrite(text: pointer) -> pointer {
let marker = "\"comment\": \"ludic-dev syntax: "
var out = ""
var i = 0
let n = slen(text)
while i < n {
let ln = line_at(text, i)
i = i + slen(ln) + 1
var line = ln
let m = s_index(ln, marker, 0)
if m >= 0 {
let g0 = m + slen(marker)
let group = sslice(ln, g0, s_index(ln, "\"", g0))
let alt = sx_tm_group(group)
let mq = s_index(ln, "\"match\": \"", 0)
if alt == null or mq < 0 { push(g_sx_prob, `ludic.tmLanguage.json: an unknown group "{group}" or no "match" beside it`) }
else {
let a = mq + 10
var e = a
while ln[e] != 0 and ln[e] != '"' { if ln[e] == '\\' { e += 1 }; e += 1 }
line = sslice(ln, 0, a) + alt + sslice(ln, e, slen(ln))
}
}
out = out + line
if i <= n { out = out + "\n" }
}
return out
}
# the files and what each should hold; kind: "region" (between the markers) or "tm"
function sx_targets() -> []pointer {
let t = new []pointer
push(t, "tools/ludic-tools/ludic_syntax.h")
push(t, "tools/ludic-tools/lsp.ludic")
push(t, "tools/ludic-tools/lsp/types.ludic")
push(t, "tools/editors/jetbrains/src/main/kotlin/io/ludic/ide/LudicTokens.kt")
push(t, "tools/editors/emacs/ludic-mode.el")
push(t, "tools/editors/shared/ludic.tmLanguage.json")
push(t, "tools/editors/vscode/syntaxes/ludic.tmLanguage.json")
return t
}
# what `path` should read, from what it reads now; null (and a problem) when it cannot be written
function sx_regenerate(path: pointer, text: pointer) -> pointer {
if s_ends(path, ".tmLanguage.json") { return sx_tm_rewrite(text) }
var region: pointer = null
if s_ends(path, "ludic_syntax.h") { region = sx_header_region() }
if s_ends(path, "ludic-tools/lsp.ludic") { region = sx_lsp_region() }
if s_ends(path, "lsp/types.ludic") { region = sx_lsp_types_region() }
if s_ends(path, "LudicTokens.kt") { region = sx_kotlin_region() }
if s_ends(path, "ludic-mode.el") { region = sx_emacs_region() }
let out = sx_splice(text, region)
if out == null { push(g_sx_prob, `{path}: no "ludic-dev syntax: begin" ... "end" lines to write between`) }
return out
}
# ---- what cannot be generated, checked ----
# the words a file's text quotes (every "..." in it), for "does it carry every keyword"
function sx_quoted(text: pointer) -> []pointer {
let out = new []pointer
var i = 0
let n = slen(text)
while i < n {
if text[i] == '"' {
var j = i + 1
while j < n and text[j] != '"' and text[j] != '\n' { j += 1 }
set_add(out, sslice(text, i + 1, j))
i = j + 1
} else { i += 1 }
}
return out
}
# the alternations of a TextMate grammar: every a|b|c word inside \b( )\b
function sx_tm_words(text: pointer) -> []pointer {
let out = new []pointer
let open = bx3('\\', 'b', '(')
var i = 0
while true {
let p = s_index(text, open, i)
if p < 0 { break }
let e = s_index(text, ")", p) # an alternation holds no ')'; the \\b after it is escaped twice here
if e < 0 { break }
let alts = split_pipe(sslice(text, p + 3, e))
var k = 0
while k < len(alts) { set_add(out, alts[k]); k += 1 }
i = e + 1
}
return out
}
# every word of `want` missing from `have` is a problem naming the file
function sx_need(file: pointer, what: pointer, want: []pointer, have: []pointer) -> int {
var missing = 0
var i = 0
while i < len(want) {
if not set_has(have, want[i]) {
push(g_sx_prob, `{file} is missing the {what} {want[i]}`)
missing += 1
}
i += 1
}
return missing
}
# does `file` carry every keyword, type and phase of the vocabulary? (the number missing)
function sx_carries(file: pointer) -> int {
let t = read_file(file)
if t == null { push(g_sx_prob, `{file}: cannot read it`); return 1 }
var have: []pointer = null
if s_ends(file, ".tmLanguage.json") { have = sx_tm_words(t) } else { have = sx_quoted(t) }
# the server's other file holds only the words that may stand as names
if s_ends(file, "lsp/types.ludic") { return sx_need(file, "keyword that may be a name", sx_contextual(), have) }
var n = sx_need(file, "keyword", sx_all_kw(), have)
n += sx_need(file, "type", sx_words("types", "name", ""), have)
n += sx_need(file, "phase", sx_words("phases", "name", ""), have)
return n
}
# docs/language: every keyword, type, phase and attribute is some page's token
function sx_docs() -> int {
let tokens = new []pointer
let pairs = new []pointer
collect_docs(tokens, pairs)
var n = sx_need("docs/language", "page for the keyword", set_union(sx_all_kw(), sx_kw("constant")), tokens)
n += sx_need("docs/language", "page for the type", sx_words("types", "name", ""), tokens)
n += sx_need("docs/language", "page for the phase", sx_words("phases", "name", ""), tokens)
n += sx_need("docs/language", "page for the attribute", sx_attr_names(), tokens)
return n
}
# the parser, scanned: the lower-case words it tests (is_id("w"), text == "w") and the attributes
# it reads (a == "Name", ann == "Name") across selfhost/frontend
function sx_parser_scan(words: []pointer, attrs: []pointer) -> void {
let list = capture("ls selfhost/frontend/*.ludic")
var i = 0
let n = slen(list)
while i < n {
let path = s_trim(line_at(list, i))
i = i + slen(line_at(list, i)) + 1
if slen(path) == 0 or s_ends(path, "/vocab.ludic") { continue }
let t = read_file(path)
if t == null { continue }
add_lower(words, collect_after(t, "is_id(" + dq()))
add_lower(words, collect_after(t, "is_kw(" + dq()))
add_lower(words, collect_after(t, "text == " + dq()))
sx_add_attrs(attrs, collect_after(t, " a == " + dq()))
sx_add_attrs(attrs, collect_after(t, "(a == " + dq()))
sx_add_attrs(attrs, collect_after(t, " ann == " + dq()))
sx_add_attrs(attrs, collect_after(t, "(ann == " + dq()))
}
}
function sx_add_attrs(out: []pointer, src: []pointer) -> void {
var i = 0
while i < len(src) {
let w = src[i]
if slen(w) > 0 and sx_ident(w) and not (w == "gltf") { set_add(out, w) } # "gltf": @Asset's argument
i += 1
}
}
function sx_ident(w: pointer) -> bool {
var i = 0
while w[i] != 0 {
let c = w[i]
if not ((c >= 'a' and c <= 'z') or (c >= 'A' and c <= 'Z') or (c >= '0' and c <= '9') or c == '_') { return false }
i += 1
}
return true
}
# both directions: nothing the parser tests is missing from the vocabulary, and nothing the
# vocabulary lists as read is something the parser never tests
function sx_parser() -> int {
let words = new []pointer
let attrs = new []pointer
sx_parser_scan(words, attrs)
if len(words) == 0 { push(g_sx_prob, "could not read any word the parser tests from selfhost/frontend"); return 1 }
# words the parser tests that are not the language's words: a builtin it notes, a type after `numbers`
let known = set_union(set_union(sx_all_kw(), sx_kw("constant")), sx_words("types", "name", ""))
let vattrs = sx_words("attributes", "name", "")
var n = 0
var i = 0
while i < len(words) {
let w = words[i]
if not set_has(known, w) and not set_has(vattrs, w) and not (w == "text_of") {
push(g_sx_prob, `the parser tests the word {w}, which vocab.ludic lacks - add its row and run ludic-dev syntax`)
n += 1
}
i += 1
}
i = 0
while i < len(attrs) {
if not set_has(vattrs, attrs[i]) {
push(g_sx_prob, `the parser reads @{attrs[i]}, which vocab.ludic lacks - add its row and run ludic-dev syntax`)
n += 1
}
i += 1
}
let kws = set_union(sx_all_kw(), sx_kw("constant"))
i = 0
while i < len(kws) {
if not set_has(words, kws[i]) { push(g_sx_prob, `vocab.ludic lists the keyword {kws[i]}, which the parser never tests`); n += 1 }
i += 1
}
let checked = sx_words("attributes", "name", "checked")
i = 0
while i < len(checked) {
if not set_has(attrs, checked[i]) { push(g_sx_prob, `vocab.ludic lists @{checked[i]} as read, and the parser never reads it`); n += 1 }
i += 1
}
return n
}
# ---- the command ----
function sx_report(what: pointer) -> int {
if len(g_sx_prob) == 0 { return 0 }
err(`{what}:\n`)
var i = 0
while i < len(g_sx_prob) { err(` - {g_sx_prob[i]}\n`); i += 1 }
return 1
}
# usage: ludic-dev syntax [--check]
function cmd_syntax_gen() -> int {
let check = argn(2, "") == "--check"
if sx_vocab() == null { err("ludic-dev syntax: bin/ludicc has no --emit-syntax (run ludic-dev build)\n"); return 2 }
g_sx_prob = new []pointer
let ts = sx_targets()
var changed = 0
var i = 0
while i < len(ts) {
let path = ts[i]
i += 1
let text = read_file(path)
if text == null { push(g_sx_prob, `{path}: cannot read it`); continue }
let want = sx_regenerate(path, text)
if want == null or want == text { continue }
if check { push(g_sx_prob, `{path} is behind the vocabulary - run ludic-dev syntax`) }
else {
write_file(path, want)
print(` wrote {path}`)
changed += 1
}
}
if check {
i = 0
while i < len(ts) { sx_carries(ts[i]); i += 1 }
sx_docs()
sx_parser()
return sx_report("the vocabulary and what is written from it disagree")
}
if changed == 0 { print(" every grammar already says what ludicc --emit-syntax does") }
return sx_report("ludic-dev syntax")
}
# ---- the regression suite's cases (test.ludic): one line each ----
function syntax_cases() -> void {
print("== one vocabulary: ludic syntax --json, and every grammar, the server and the docs against it ==")
if not shq("bin/ludic syntax --json > /dev/null 2>&1") or sx_vocab() == null { bad("ludic syntax --json printed no vocabulary"); return }
# the words editor tooling had lost track of (the scripting proposal's R8)
let r8 = ["module", "uses", "port", "bind", "action", "reducer", "dispatch", "registry", "def", "open", "component", "prop", "view", "alias", "friend", "unsafe", "numbers", "of", "as", "from", "attach", "detach"]
let kws = sx_all_kw()
var gone = ""
var i = 0
while i < len(r8) { if not set_has(kws, r8[i]) { gone = gone + " " + r8[i] }; i += 1 }
if slen(gone) == 0 { ok("ludic syntax --json: every keyword, among them module, uses, port, bind, action, reducer, dispatch, registry, def, component, view") }
else { bad2("ludic syntax --json lacks keywords", gone) }
let ats = sx_words("attributes", "name", "")
let r8a = ["Ref", "OneOf", "Range", "Unit", "Asset", "Color", "Node", "Clip", "Material", "Tint", "Derived", "Text", "Multiline", "Key", "AppendOnly", "ByKey", "PerMap", "Chunked", "max", "owns", "frame", "Sync", "Computed", "alloc_ok"]
gone = ""
i = 0
while i < len(r8a) { if not set_has(ats, r8a[i]) { gone = gone + " @" + r8a[i] }; i += 1 }
if slen(gone) == 0 { ok("ludic syntax --json: every attribute, with where it goes, its arguments and its doc") }
else { bad2("ludic syntax --json lacks attributes", gone) }
let ts = sx_targets()
i = 0
while i < len(ts) {
g_sx_prob = new []pointer
let path = ts[i]
i += 1
let text = read_file(path)
var behind = false
if text != null {
let want = sx_regenerate(path, text)
behind = want == null or not (want == text)
}
let missing = sx_carries(path)
if missing == 0 and not behind { ok(`{path} carries the vocabulary's words (as ludic-dev syntax writes them)`) }
else if missing > 0 { bad2(`{path} is missing words`, g_sx_prob[0]) }
else { bad2(`{path} is behind the vocabulary`, "run ludic-dev syntax") }
}
g_sx_prob = new []pointer
if sx_docs() == 0 { ok("docs/language has a page for every keyword, type, phase and attribute") }
else { bad2(`docs/language lacks {string(len(g_sx_prob))} page(s)`, g_sx_prob[0]) }
g_sx_prob = new []pointer
if sx_parser() == 0 { ok("the parser tests no word and reads no attribute the vocabulary lacks, and the reverse") }
else { bad2("the parser and vocab.ludic disagree", g_sx_prob[0]) }
}

View file

@ -99,6 +99,30 @@ function diag_json_case(path: pointer, n: int, want: pointer, label: pointer) ->
if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `no diagnostic holds [{want}]`) } if s_contains(capture(`cat {js}`), want) { ok(label) } else { bad2(label, `no diagnostic holds [{want}]`) }
} }
# --stdin-file <path>: stdin stands for <path> wherever the program opens it. A buffer that breaks an
# import reached through its barrel (spelt with `..`) is refused at the buffer's line under the file's
# usual name; a buffer that fixes a refused entry checks clean; a path nothing opens is a warning.
function stdin_file_cases() -> void {
let lbl = "--stdin-file: an unsaved buffer checked in place of its file"
let fish = "examples/modules/kinds/fishing/index.ludic"
let js = `{tmp_dir()}/sf_break.json`
let spelt = "./examples/modules/kinds/fishing/../fishing/index.ludic"
if shq(`sed 's/return fishing_st.landed/return "seven"/' {fish} | bin/ludicc examples/modules/private_kinds.ludic --check --diagnostics=json --stdin-file {spelt} > {js} 2>/dev/null`) {
bad2(lbl, "a buffer with an error checked clean"); return
}
let broke = capture(`cat {js}`)
if not s_contains(broke, `"file": "{fish}", "line": 21`) { bad2(lbl, `no error at the buffer's line 21 of {fish}: {s_trim(broke)}`); return }
let js2 = `{tmp_dir()}/sf_fix.json`
let fix = "sed -e 's/print(on + 1)/print(1)/' -e 's/half(1, 2)/half(1)/' -e 's/-> int { return \"seven\" }/-> int { return 7 }/'"
let clean = shq(`{fix} examples/rejected/every_error.ludic | bin/ludicc examples/rejected/every_error.ludic --check --diagnostics=json --stdin-file examples/rejected/every_error.ludic > {js2} 2>/dev/null`)
let said = s_trim(capture(`cat {js2}`))
if not clean or not (said == "[]") { bad2(lbl, `a buffer that fixes the saved file's errors said {said}`); return }
let js3 = `{tmp_dir()}/sf_none.json`
shq(`printf '' | bin/ludicc examples/modules/private_kinds.ludic --check --diagnostics=json --stdin-file examples/nowhere.ludic > {js3} 2>/dev/null`)
if not s_contains(capture(`cat {js3}`), "the program does not reach this file") { bad2(lbl, "a path the program never opens was not reported"); return }
ok(lbl)
}
# a feature example that consumes an in-repo controller package (packages/): same # a feature example that consumes an in-repo controller package (packages/): same
# as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a # as feat_case, but the compiler is pointed at packages/ via LUDIC_MODULES so a
# `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine # `import "ludic.foo/bar.ludic"` resolves, and LUDIC_HOME anchors the engine
@ -1227,6 +1251,7 @@ function cmd_dev_test() -> int {
diag_json_case("rejected/every_error", 3, "\"line\": 8, \"col\": 15, \"severity\": \"error\", \"message\": \"half takes 1 argument(s) and this call gives 2\"", "--diagnostics=json: every type error as {file, line, col, severity, message}") diag_json_case("rejected/every_error", 3, "\"line\": 8, \"col\": 15, \"severity\": \"error\", \"message\": \"half takes 1 argument(s) and this call gives 2\"", "--diagnostics=json: every type error as {file, line, col, severity, message}")
diag_json_case("rejected/ref_unknown", 1, "@Ref(Shops): this program has no Shops - left unresolved", "--diagnostics=json: an @Ref to a registry the program lacks is a warning beside the error") diag_json_case("rejected/ref_unknown", 1, "@Ref(Shops): this program has no Shops - left unresolved", "--diagnostics=json: an @Ref to a registry the program lacks is a warning beside the error")
diag_json_case("rejected/node_bad", 8, "crate.shape is \\\"cube\\\", and field Tool.shape is @OneOf(", "--diagnostics=json: every @Node / @Clip / @Material with no model field, @OneOf of the wrong kind or a row outside its words, and the targets the program lacks as warnings") diag_json_case("rejected/node_bad", 8, "crate.shape is \\\"cube\\\", and field Tool.shape is @OneOf(", "--diagnostics=json: every @Node / @Clip / @Material with no model field, @OneOf of the wrong kind or a row outside its words, and the targets the program lacks as warnings")
stdin_file_cases()
feat_case("lang/generics", "", "7 Crater Lake a 2 west 7 0.75 2.5 1", "generics.ludic (L5: generic records and functions, nested instances, fn-typed parameters and fields, inference from the result's slot)") feat_case("lang/generics", "", "7 Crater Lake a 2 west 7 0.75 2.5 1", "generics.ludic (L5: generic records and functions, nested instances, fn-typed parameters and fields, inference from the result's slot)")
reject_case("rejected/generic_unbound", "cannot tell what T is in this call to pool_new", "a call whose type arguments nothing decides is refused") reject_case("rejected/generic_unbound", "cannot tell what T is in this call to pool_new", "a call whose type arguments nothing decides is refused")
reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types") reject_case("rejected/generic_mismatch", "words wants a Pool<string> and this is a Pool<int>", "two instances of one generic are two types")
@ -1485,6 +1510,8 @@ function cmd_dev_test() -> int {
smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll) smoke("library/cursor_capture") # #89 Input.cursor_mode compiles (no-op headless; windowed links cocoa.ll)
smoke("rendering/gl_triangle") # Gl.* (OpenGL 4.1 core) compiles headless; the run needs a GPU context smoke("rendering/gl_triangle") # Gl.* (OpenGL 4.1 core) compiles headless; the run needs a GPU context
syntax_cases()
print("== the compiler and the CLI (ludicc / ludic) ==") print("== the compiler and the CLI (ludicc / ludic) ==")
# ludicc comes out of the IR seed with clang alone; the CLI is then compiled # ludicc comes out of the IR seed with clang alone; the CLI is then compiled
# by it, from Ludic. # by it, from Ludic.

View file

@ -48,6 +48,11 @@ program LudicLsp {
const LS_QUERYVAR: int = 15 const LS_QUERYVAR: int = 15
const LS_STATE: int = 16 const LS_STATE: int = 16
const LS_IMPORT: int = 17 const LS_IMPORT: int = 17
const LS_REGISTRY: int = 18 # registry R of T: a table of named entries
const LS_ENUM: int = 19
const LS_ENUMMEMBER: int = 20
const LS_VIEW: int = 21 # a UI component or a view
const LS_REDUCER: int = 22 # reducer S on A: named "S on A" in the outline
# ---- semantic classes ---- # ---- semantic classes ----
const SC_NONE: int = 0 const SC_NONE: int = 0
@ -175,6 +180,7 @@ program LudicLsp {
exported: int = 0 exported: int = 0
init: int = 0 # first token of the initializer (let/var/const), or -1 init: int = 0 # first token of the initializer (let/var/const), or -1
mutable: int = 0 # a `var` local mutable: int = 0 # a `var` local
decl: pointer = null # the word that declared it (property, state, event, action, port, ...)
} }
property Doc { property Doc {
path: pointer = null path: pointer = null
@ -226,21 +232,38 @@ program LudicLsp {
if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ; if c == ',' or c == ':' or c == '.' or c == '!' or c == '@' or c == ';' { return true } # , : . ! @ ;
return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~ return c == '&' or c == '|' or c == '^' or c == '~' # & | ^ ~
} }
# the vocabulary, written by `ludic-dev syntax` from `ludicc --emit-syntax`
# ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
function is_type_word(w: pointer) -> bool { function is_type_word(w: pointer) -> bool {
return (w == "int") or (w == "long") or (w == "fixed") or (w == "float") or (w == "double") or (w == "floats") or (w == "doubles") or (w == "countdown") or (w == "bool") or (w == "entity") or (w == "string") or (w == "pointer") or (w == "byte") or (w == "words") or (w == "fixeds") or (w == "pointers") or (w == "Vector") or (w == "IVec2") or (w == "Rect") or (w == "void") if (w == "int") or (w == "long") or (w == "float") or (w == "double") or (w == "fixed") or (w == "bool") { return true }
} if (w == "byte") or (w == "string") or (w == "Key") or (w == "pointer") or (w == "void") or (w == "entity") { return true }
function is_phase_word(w: pointer) -> bool { if (w == "countdown") or (w == "words") or (w == "floats") or (w == "doubles") or (w == "fixeds") or (w == "pointers") { return true }
return (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") if (w == "Vector") or (w == "IVec2") or (w == "Rect") { return true }
}
function is_keyword_word(w: pointer) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true }
if (w == "const") or (w == "var") or (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") or (w == "mut") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "let") or (w == "return") or (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "spawn") or (w == "despawn") { return true }
if (w == "enable") or (w == "disable") or (w == "match") or (w == "machine") or (w == "state") or (w == "become") or (w == "where") or (w == "prefab") { return true }
if (w == "and") or (w == "or") or (w == "not") or (w == "break") or (w == "continue") or (w == "new") or (w == "emit") or (w == "cancel") { return true }
return false return false
} }
function is_phase_word(w: pointer) -> bool {
if (w == "Start") or (w == "Input") or (w == "FixedUpdate") or (w == "Update") or (w == "LateUpdate") or (w == "Render") { return true }
if (w == "Overlay") { return true }
return false
}
function is_keyword_word(w: pointer) -> bool {
if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true }
if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true }
if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true }
if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true }
if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true }
if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true }
if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true }
if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "let") { return true }
if (w == "if") or (w == "else") or (w == "while") or (w == "for") or (w == "in") or (w == "where") { return true }
if (w == "match") or (w == "machine") or (w == "become") or (w == "return") or (w == "break") or (w == "continue") { return true }
if (w == "new") or (w == "spawn") or (w == "despawn") or (w == "enable") or (w == "disable") or (w == "attach") { return true }
if (w == "detach") or (w == "emit") or (w == "cancel") or (w == "dispatch") or (w == "try") or (w == "and") { return true }
if (w == "or") or (w == "not") { return true }
return false
}
# ludic-dev syntax: end
function is_widget_word(w: pointer) -> bool { function is_widget_word(w: pointer) -> bool {
return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer") return (w == "panel") or (w == "col") or (w == "row") or (w == "label") or (w == "button") or (w == "image") or (w == "spacer")
} }
@ -449,7 +472,7 @@ program LudicLsp {
function sym_add(kind: int, nametok: int, parent: int) -> int { function sym_add(kind: int, nametok: int, parent: int) -> int {
let s = new Sym let s = new Sym
s.kind = kind; s.tok = nametok; s.parent = parent s.kind = kind; s.tok = nametok; s.parent = parent
s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0 s.name = ""; s.ty = ""; s.detail = ""; s.doc = ""; s.init = -1; s.mutable = 0; s.decl = ""
if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) } if nametok >= 0 { s.start = pD.tk_start[nametok]; s.end = pD.tk_end[nametok]; s.name = ttext(pD, nametok) }
s.bstart = 0; s.bend = cstr_len(pD.text) s.bstart = 0; s.bend = cstr_len(pD.text)
push(pD.syms, s) push(pD.syms, s)
@ -457,6 +480,62 @@ program LudicLsp {
} }
function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 } function cur_match() -> int { if pi < ntok(pD) { return pD.tmatch[pi] }; return -1 }
function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() } function skip_group() -> void { let m = cur_match(); if m < 0 { padv(); return }; pi = m + 1; pskipnl() }
# past a bracketed group, staying on its line (skip_group also takes the newlines after it)
function lp_jump_group() -> void { let m = cur_match(); if m < 0 { pi += 1 } else { pi = m + 1 } }
# the tokens up to the end of this line as written, joined by single spaces (after a leading one)
function lp_rest_of_line() -> pointer {
let b = buf_new()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pk() != LT_COMMENT { buf_putc(b, ' '); buf_puts(b, ttext(pD, pi)) }
pi += 1
}
return buf_str(b)
}
# a component's or a view's body: its props and state (and a view's fields) as fields, its
# functions and events as functions
function lp_view_body(sidx: int) -> void {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let before = pi
if pis("function") { lp_decl(sidx) }
else if pis("on") {
padv()
let nt = pname()
if nt >= 0 {
let f = sym_add(LS_FN, nt, sidx)
pD.syms[f].detail = "on " + pD.syms[f].name
if pis("(") { lp_params(f, LS_PARAM, buf_new()) }
}
while pi < ntok(pD) and pk() != LT_EOF and not pis("{") and pk() != LT_NL { pi += 1 }
if pis("{") { skip_group() }
}
else {
var word = ""
if pis("prop") or pis("state") { word = pword(); padv() }
let ft = pname()
if ft >= 0 and (pis(":") or pis("=")) {
let f = sym_add(LS_FIELD, ft, sidx)
if pis(":") { padv(); pD.syms[f].ty = lp_type_text() }
var head = word
if (head == "") { head = pD.syms[sidx].name + "." }
else { head = head + " " }
pD.syms[f].detail = head + pD.syms[f].name + ": " + pD.syms[f].ty
}
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
}
if pi == before { padv() } else { pskipnl() }
if pis(",") { padv() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
# `name: Type` pairs inside ( ) — record params, build the signature into sig # `name: Type` pairs inside ( ) — record params, build the signature into sig
function lp_params(owner: int, kind: int, sig: Buf) -> void { function lp_params(owner: int, kind: int, sig: Buf) -> void {
@ -498,7 +577,28 @@ program LudicLsp {
} }
let tt = pname() let tt = pname()
if tt < 0 { return "" } if tt < 0 { return "" }
return pre + ttext(pD, tt) var ty = pre + ttext(pD, tt)
# a function type, `fn(int, float) -> bool`, and a generic's arguments, `Pool<Item>`, as written
if (ttext(pD, tt) == "fn") and pis("(") {
let m = cur_match()
if m >= 0 {
ty = ty + pD.src[pD.tk_start[pi]..pD.tk_end[m]]
pi = m + 1
if pis("->") { pi += 1; ty = ty + " -> " + lp_type_text() }
}
}
else if pis("<") {
let a = pi
var depth = 0
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL {
if pis("<") { depth += 1 }
if pis(">") { depth -= 1 }
pi += 1
if depth == 0 { break }
}
ty = ty + pD.src[pD.tk_start[a]..pD.tk_end[pi - 1]]
}
return ty
} }
# (a, b) variable list -> token indices into out; returns count # (a, b) variable list -> token indices into out; returns count
@ -641,10 +741,33 @@ program LudicLsp {
} }
} }
var lp_anchor: int = -1 # where a declaration begins when `export`, `unsafe` or @attributes lead it
# one top-level declaration # one top-level declaration
function lp_decl(parent: int) -> void { function lp_decl(parent: int) -> void {
let w = pword() let w = pword()
let decl_tok = pi var decl_tok = pi
if lp_anchor >= 0 { decl_tok = lp_anchor; lp_anchor = -1 }
# a lead-in: @Attr(args) (a registry's @AppendOnly, a handler's @On(E)), `export` or `unsafe`
# before a declaration - read past, the declaration keeps where it began and says it is exported
if pk() == LT_ANNO {
padv()
if pis("(") { skip_group() }
lp_anchor = decl_tok
lp_decl(parent)
return
}
if (w == "export" or w == "unsafe") and pi + 1 < ntok(pD) and not tis(pD, pi + 1, "function") and not tis(pD, pi + 1, "pure") {
let first = len(pD.syms)
padv()
lp_anchor = decl_tok
lp_decl(parent)
if w == "export" {
var q = first
while q < len(pD.syms) { if pD.syms[q].parent == parent { pD.syms[q].exported = 1 }; q += 1 }
}
return
}
if w == "import" { if w == "import" {
padv() padv()
@ -660,18 +783,23 @@ program LudicLsp {
} }
return return
} }
var wd = w let wd = w
if w == "export" and tis(pD, pi + 1, "state") { padv(); wd = "state" } # records: a property, a state (one instance), an event, an action, a port (its members are
if (wd == "property") or (wd == "model") or (wd == "state") { # a state: a record with one instance # fn-typed), a model (its members are components)
let is_comp = (wd == "property") or (wd == "state") if (wd == "property") or (wd == "model") or (wd == "state") or (wd == "event") or (wd == "action") or (wd == "port") {
padv(); let nt = pname(); if nt < 0 { return } let is_comp = not (wd == "model")
padv()
if wd == "event" and pis("cancellable") { padv() }
let nt = pname(); if nt < 0 { return }
var kind = LS_ARCHETYPE var kind = LS_ARCHETYPE
if is_comp { kind = LS_COMPONENT } if is_comp { kind = LS_COMPONENT }
let sidx = sym_add(kind, nt, parent) let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].decl = wd
pD.syms[sidx].doc = collect_doc(pD, decl_tok) pD.syms[sidx].doc = collect_doc(pD, decl_tok)
while pk() == LT_ANNO { padv() } if pis("<") { while pi < ntok(pD) and not pis(">") and pk() != LT_EOF { padv() }; padv() } # property Pool<T>
while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } }
let det = buf_new() let det = buf_new()
buf_puts(det, w); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {") buf_puts(det, wd); buf_putc(det, ' '); buf_puts(det, pD.syms[sidx].name); buf_puts(det, " {")
if not pis("{") { pD.syms[sidx].detail = buf_str(det); return } if not pis("{") { pD.syms[sidx].detail = buf_str(det); return }
let close = cur_match() let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi] pD.syms[sidx].bstart = pD.tk_start[pi]
@ -681,14 +809,19 @@ program LudicLsp {
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") { while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break } if close >= 0 and pi >= close { break }
if pis(",") { padv(); continue } if pis(",") { padv(); continue }
while pk() == LT_ANNO { padv() } # @Sync / @Computed on a field while pk() == LT_ANNO { padv(); if pis("(") { skip_group() } } # @Ref(Items), @Range(0, 1), @Sync ...
let ft = pname(); if ft < 0 { padv(); continue } let ft = pname(); if ft < 0 { padv(); continue }
let f = sym_add(LS_FIELD, ft, sidx) let f = sym_add(LS_FIELD, ft, sidx)
var fty = "" var fty = ""
if pis(":") { padv(); fty = lp_type_text() } if pis(":") { padv(); fty = lp_type_text() }
pD.syms[f].ty = fty pD.syms[f].ty = fty
pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty)) pD.syms[f].detail = concat3(pD.syms[sidx].name, ".", concat3(pD.syms[f].name, ": ", fty))
if pis("=") { padv(); while pi < ntok(pD) and not pis(",") and not pis("}") and pk() != LT_EOF { if pis("(") or pis("{") or pis("[") { skip_group() } else { padv() } } } # the rest of the member - a fn type's parameters and result, a generic's arguments, the
# default - ends at a comma, the closing brace or the end of its line
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") {
if close >= 0 and pi >= close { break }
if pis("(") or pis("{") or pis("[") { lp_jump_group() } else { pi += 1 }
}
if not first { buf_putc(det, ',') } if not first { buf_putc(det, ',') }
buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty) buf_putc(det, ' '); buf_puts(det, pD.syms[f].name); buf_puts(det, ": "); buf_puts(det, fty)
first = false first = false
@ -699,9 +832,86 @@ program LudicLsp {
pskipnl() pskipnl()
return return
} }
if (w == "const") or (w == "var") { if w == "open" and tis(pD, pi + 1, "registry") { padv(); lp_anchor = decl_tok; lp_decl(parent); return }
if w == "registry" { # registry Name of Record [as P] [from "file.lres"]
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REGISTRY, nt, parent)
pD.syms[sidx].decl = "registry"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("of") { pD.syms[sidx].ty = ttext(pD, pi + 1) } # its record, for go-to-type
pD.syms[sidx].detail = "registry " + pD.syms[sidx].name + lp_rest_of_line()
pskipnl()
return
}
if w == "enum" { # enum Name { A, B = 2, C }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_ENUM, nt, parent)
pD.syms[sidx].decl = "enum"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = "enum " + pD.syms[sidx].name
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] }
padv()
while pi < ntok(pD) and pk() != LT_EOF and not pis("}") {
if close >= 0 and pi >= close { break }
let mt = pname()
if mt < 0 { padv(); continue }
let m = sym_add(LS_ENUMMEMBER, mt, sidx)
pD.syms[m].detail = pD.syms[sidx].name + "." + pD.syms[m].name
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis(",") and not pis("}") { pi += 1 }
if pis(",") { padv() } else { pskipnl() }
}
if close >= 0 { pi = close + 1 } else { padv() }
pskipnl()
}
return
}
if (w == "component") or (w == "view") { # component Name[(states)] { prop / state / function / on }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_VIEW, nt, parent)
pD.syms[sidx].decl = w
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
pD.syms[sidx].detail = w + " " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") { lp_view_body(sidx) }
return
}
if w == "reducer" { # reducer State on Action(states, a: Action) { ... }
padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(LS_REDUCER, nt, parent)
pD.syms[sidx].decl = "reducer"
pD.syms[sidx].doc = collect_doc(pD, decl_tok)
if pis("on") { padv(); let at = pname(); if at >= 0 { pD.syms[sidx].name = pD.syms[sidx].name + " on " + ttext(pD, at) } }
pD.syms[sidx].detail = "reducer " + pD.syms[sidx].name
if pis("(") { lp_params(sidx, LS_PARAM, buf_new()) }
if pis("{") {
let close = cur_match()
pD.syms[sidx].bstart = pD.tk_start[pi]
if close >= 0 { pD.syms[sidx].bend = pD.tk_end[close] } else { pD.syms[sidx].bend = cstr_len(pD.text) }
var q = 0
while q < len(pD.syms) { if pD.syms[q].parent == sidx and pD.syms[q].kind == LS_PARAM { pD.syms[q].bstart = pD.syms[sidx].bstart; pD.syms[q].bend = pD.syms[sidx].bend }; q += 1 }
lp_block(sidx, pD.syms[sidx].bend)
}
return
}
# a def's entry and a bind's answers are data: their bodies hold no declaration
if (w == "def") or (w == "bind") {
padv()
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL and not pis("{") { pi += 1 }
if pis("{") { skip_group() } else { pskipnl() }
return
}
# a line that says what a file is: module / friend module / numbers float
if (w == "module") or (w == "friend") or (w == "numbers") {
while pi < ntok(pD) and pk() != LT_EOF and pk() != LT_NL { pi += 1 }
pskipnl()
return
}
if (w == "const") or (w == "var") or (w == "let") {
var kind = LS_VAR var kind = LS_VAR
if w == "const" { kind = LS_CONST } if (w == "const") or (w == "let") { kind = LS_CONST }
padv(); let nt = pname(); if nt < 0 { return } padv(); let nt = pname(); if nt < 0 { return }
let sidx = sym_add(kind, nt, parent) let sidx = sym_add(kind, nt, parent)
pD.syms[sidx].doc = collect_doc(pD, decl_tok) pD.syms[sidx].doc = collect_doc(pD, decl_tok)
@ -922,10 +1132,10 @@ program LudicLsp {
# ============================================================================ # ============================================================================
function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE } function is_local_kind(k: int) -> bool { return k == LS_LOCAL or k == LS_PARAM or k == LS_QUERYVAR or k == LS_STATE }
function is_toplevel_kind(k: int) -> bool { function is_toplevel_kind(k: int) -> bool {
return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT return k == LS_COMPONENT or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_UNIT or k == LS_REGISTRY or k == LS_ENUM or k == LS_VIEW
} }
function is_outline_kind(k: int) -> bool { function is_outline_kind(k: int) -> bool {
return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER return k == LS_UNIT or k == LS_COMPONENT or k == LS_FIELD or k == LS_ARCHETYPE or k == LS_CONST or k == LS_VAR or k == LS_FN or k == LS_EXTERN or k == LS_SYSTEM or k == LS_UI or k == LS_WIDGET or k == LS_SCENE or k == LS_LAYER or k == LS_REGISTRY or k == LS_ENUM or k == LS_ENUMMEMBER or k == LS_VIEW or k == LS_REDUCER
} }
# ---- paths & URIs ---- # ---- paths & URIs ----
@ -1199,6 +1409,37 @@ program LudicLsp {
} }
return best return best
} }
# a name the whole unit sees: a top-level kind, and not a member of a component or a view
function is_top_sym(D: Doc, k: int) -> bool {
let y = D.syms[k]
if not is_toplevel_kind(y.kind) { return false }
return y.parent < 0 or D.syms[y.parent].kind != LS_VIEW
}
# the field an attribute's argument names - `@Node(model)`, `@Clip(model)` - in the record the
# attribute is written in, or -1 (a registry, `@Ref(Items)`, and a constant resolve as any name)
function attr_arg_field(D: Doc, tok: int) -> int {
var p = prev_sig(D, tok)
while p >= 0 and (tis(D, p, ",") or D.tk_kind[p] == LT_ID or D.tk_kind[p] == LT_STR or D.tk_kind[p] == LT_INT or D.tk_kind[p] == LT_FLOAT) { p = prev_sig(D, p) }
if p < 0 or not tis(D, p, "(") { return -1 }
let a = prev_sig(D, p)
if a < 0 or D.tk_kind[a] != LT_ANNO { return -1 }
let off = D.tk_start[tok]
let name = ttext(D, tok)
var rec = -1
var i = 0
while i < len(D.syms) {
let y = D.syms[i]
if (y.kind == LS_COMPONENT or y.kind == LS_ARCHETYPE) and off >= y.bstart and off <= y.bend and y.bend > y.bstart { rec = i }
i += 1
}
if rec < 0 { return -1 }
i = 0
while i < len(D.syms) {
if D.syms[i].kind == LS_FIELD and D.syms[i].parent == rec and (D.syms[i].name == name) { return i }
i += 1
}
return -1
}
# returns sym index in g_owner (set), or -1 # returns sym index in g_owner (set), or -1
function find_top(D: Doc, name: pointer) -> int { function find_top(D: Doc, name: pointer) -> int {
let rel = related(D) let rel = related(D)
@ -1206,7 +1447,7 @@ program LudicLsp {
while i < len(rel) { while i < len(rel) {
var k = 0 var k = 0
while k < len(rel[i].syms) { while k < len(rel[i].syms) {
if is_toplevel_kind(rel[i].syms[k].kind) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k } if is_top_sym(rel[i], k) and (rel[i].syms[k].name == name) { g_owner = rel[i]; return k }
k += 1 k += 1
} }
i += 1 i += 1
@ -1215,7 +1456,7 @@ program LudicLsp {
while i < len(g_docs) { while i < len(g_docs) {
var k = 0 var k = 0
while k < len(g_docs[i].syms) { while k < len(g_docs[i].syms) {
if is_toplevel_kind(g_docs[i].syms[k].kind) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k } if is_top_sym(g_docs[i], k) and (g_docs[i].syms[k].name == name) { g_owner = g_docs[i]; return k }
k += 1 k += 1
} }
i += 1 i += 1
@ -1259,6 +1500,8 @@ program LudicLsp {
} }
let l = find_local(D, name, D.tk_start[tok]) let l = find_local(D, name, D.tk_start[tok])
if l >= 0 { g_owner = D; g_symidx = l; return tok } if l >= 0 { g_owner = D; g_symidx = l; return tok }
let af = attr_arg_field(D, tok)
if af >= 0 { g_owner = D; g_symidx = af; return tok }
let t = find_top(D, name) let t = find_top(D, name)
if t >= 0 { g_symidx = t; return tok } if t >= 0 { g_symidx = t; return tok }
g_owner = null; g_symidx = -1 g_owner = null; g_symidx = -1
@ -1664,6 +1907,10 @@ program LudicLsp {
if k == LS_PARAM { return SC_PARAM } if k == LS_PARAM { return SC_PARAM }
if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE } if k == LS_QUERYVAR or k == LS_LOCAL or k == LS_STATE { return SC_VARIABLE }
if k == LS_UNIT { return SC_UI } if k == LS_UNIT { return SC_UI }
if k == LS_REGISTRY or k == LS_ENUMMEMBER { return SC_CONST }
if k == LS_ENUM { return SC_TYPE }
if k == LS_VIEW { return SC_UI }
if k == LS_REDUCER { return SC_FUNCTION }
return SC_UNKNOWN return SC_UNKNOWN
} }
function classify(D: Doc) -> void { function classify(D: Doc) -> void {
@ -2228,8 +2475,21 @@ program LudicLsp {
if k == LS_WIDGET { return 14 } if k == LS_WIDGET { return 14 }
if k == LS_SCENE { return 3 } if k == LS_SCENE { return 3 }
if k == LS_LAYER { return 3 } if k == LS_LAYER { return 3 }
if k == LS_REGISTRY { return 18 }
if k == LS_ENUM { return 10 }
if k == LS_ENUMMEMBER { return 22 }
if k == LS_VIEW { return 5 }
if k == LS_REDUCER { return 12 }
return 13 return 13
} }
# a symbol's LSP SymbolKind: a record says which it is by the word that declared it
function sym_lsp_kind(y: Sym) -> int {
if y.kind == LS_COMPONENT {
if (y.decl == "event") or (y.decl == "action") { return 24 }
if (y.decl == "port") { return 11 }
}
return lsp_symbol_kind(y.kind)
}
function comp_kind_of(k: int) -> int { function comp_kind_of(k: int) -> int {
if k == LS_COMPONENT { return 22 } if k == LS_COMPONENT { return 22 }
if k == LS_ARCHETYPE { return 7 } if k == LS_ARCHETYPE { return 7 }
@ -2242,6 +2502,10 @@ program LudicLsp {
if k == LS_SCENE { return 11 } if k == LS_SCENE { return 11 }
if k == LS_LAYER { return 11 } if k == LS_LAYER { return 11 }
if k == LS_FIELD { return 5 } if k == LS_FIELD { return 5 }
if k == LS_REGISTRY { return 21 }
if k == LS_ENUM { return 13 }
if k == LS_ENUMMEMBER { return 20 }
if k == LS_VIEW { return 7 }
return 6 return 6
} }
@ -2353,7 +2617,7 @@ program LudicLsp {
first = 0 first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name) buf_puts(o, "{\"name\":"); jstr_out(o, y.name)
buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail) buf_puts(o, ",\"detail\":"); jstr_out(o, y.detail)
buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)) buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y))
buf_puts(o, ",\"range\":") buf_puts(o, ",\"range\":")
var a = y.start; var b = y.end var a = y.start; var b = y.end
if y.bend > y.end and y.bstart >= y.start { b = y.bend } if y.bend > y.end and y.bstart >= y.start { b = y.bend }
@ -2750,7 +3014,7 @@ program LudicLsp {
if keep { if keep {
if first == 0 { buf_putc(o, ',') } if first == 0 { buf_putc(o, ',') }
first = 0 first = 0
buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, lsp_symbol_kind(y.kind)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}') buf_puts(o, "{\"name\":"); jstr_out(o, y.name); buf_puts(o, ",\"kind\":"); buf_puti(o, sym_lsp_kind(y)); buf_puts(o, ",\"location\":"); put_location(o, D, y.start, y.end); buf_putc(o, '}')
} }
} }
k += 1 k += 1

View file

@ -166,7 +166,8 @@ function sym_hover_code(O: Doc, si: int) -> pointer {
function record_hover_code(O: Doc, si: int) -> pointer { function record_hover_code(O: Doc, si: int) -> pointer {
let y = O.syms[si] let y = O.syms[si]
let b = buf_new() let b = buf_new()
if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") } if not (y.decl == "") { buf_puts(b, y.decl); buf_puts(b, " ") }
else if y.kind == LS_COMPONENT { buf_puts(b, "property ") } else { buf_puts(b, "model ") }
buf_puts(b, y.name) buf_puts(b, y.name)
buf_puts(b, " {") buf_puts(b, " {")
var n = 0 var n = 0
@ -224,13 +225,23 @@ function on_type_definition(msg: JVal, id: JVal) -> void {
# `model: Model` and `a.model` are all ordinary names. A word is a keyword only # `model: Model` and `a.model` are all ordinary names. A word is a keyword only
# where it could start or continue a declaration. # where it could start or continue a declaration.
# a keyword that may stand as a name where one is written (written by `ludic-dev syntax`)
# ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit
function is_contextual_word(w: pointer) -> bool { function is_contextual_word(w: pointer) -> bool {
if (w == "program") or (w == "import") or (w == "property") or (w == "model") or (w == "enum") or (w == "ui") { return true } if (w == "program") or (w == "module") or (w == "import") or (w == "property") or (w == "state") or (w == "event") { return true }
if (w == "function") or (w == "extern") or (w == "handler") or (w == "entry") or (w == "event") or (w == "scene") { return true } if (w == "action") or (w == "reducer") or (w == "enum") or (w == "model") or (w == "prefab") or (w == "scene") { return true }
if (w == "phase") or (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true } if (w == "handler") or (w == "ui") or (w == "namespace") or (w == "alias") or (w == "port") or (w == "bind") { return true }
if (w == "state") or (w == "machine") or (w == "prefab") or (w == "namespace") or (w == "test") { return true } if (w == "registry") or (w == "def") or (w == "component") or (w == "view") or (w == "prop") or (w == "const") { return true }
if (w == "var") or (w == "function") or (w == "fn") or (w == "extern") or (w == "entry") or (w == "test") { return true }
if (w == "export") or (w == "internal") or (w == "friend") or (w == "uses") or (w == "open") or (w == "of") { return true }
if (w == "as") or (w == "from") or (w == "numbers") or (w == "unsafe") or (w == "mut") or (w == "phase") { return true }
if (w == "query") or (w == "on") or (w == "cancellable") or (w == "public") or (w == "layer") or (w == "start") { return true }
if (w == "shows") or (w == "lasts") or (w == "then") or (w == "loads") or (w == "system") or (w == "in") { return true }
if (w == "where") or (w == "enable") or (w == "disable") or (w == "attach") or (w == "detach") or (w == "emit") { return true }
if (w == "dispatch") or (w == "try") { return true }
return false return false
} }
# ludic-dev syntax: end
function demote_contextual_keywords(D: Doc) -> void { function demote_contextual_keywords(D: Doc) -> void {
let N = ntok(D) let N = ntok(D)

View file

@ -48,34 +48,48 @@ typedef struct {
} LLex; } LLex;
/* ---------- the vocabulary ------------------------------------------------*/ /* ---------- the vocabulary ------------------------------------------------*/
/* Reserved words, grouped so editors can colour them differently. The groups /* Keywords by role (declaring, modifying, statement), the built-in types, the
* mirror the compiler's parser: anything parse_decl() dispatches on is a * phases and the attribute names: one vocabulary, printed by the compiler
* declaration keyword, anything stmt() dispatches on is a statement keyword. */ * (`ludicc --emit-syntax`, selfhost/frontend/vocab.ludic, which is held to the
static const char* LUDIC_KW_DECL[] = { * parser's own recognisers) and written between the marked lines below by
"program","import","property","model","enum","ui","namespace", * `ludic-dev syntax`; `ludic-dev syntax --check` fails when they fall behind. */
"const","var","function","extern","handler","entry","event","scene","test", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"phase","query","on","cancellable","public","layer","start", 0
};
/* Documented design targets the self-hosted parser does not accept yet. Kept /* Documented design targets the self-hosted parser does not accept yet. Kept
* out of the highlighted vocabulary (they would read as working keywords) until * out of the highlighted vocabulary (they would read as working keywords) until
* they are implemented; check-vocabulary.py verifies the lists above are a * they are implemented. */
* subset of what selfhost/parse*.ludic actually dispatches on. */
static const char* LUDIC_KW_RESERVED[] = { static const char* LUDIC_KW_RESERVED[] = {
0 0
}; };
/* ludic-dev syntax: begin - generated from `ludicc --emit-syntax` (selfhost/frontend/vocab.ludic); run `ludic-dev syntax`, do not edit */
static const char* LUDIC_KW_DECL[] = {
"program","module","import","property","state","event","action","reducer","enum","model",
"prefab","scene","handler","ui","namespace","alias","port","bind","registry","def","component",
"view","prop","const","var","function","fn","extern","entry","test", 0
};
static const char* LUDIC_KW_CLAUSE[] = {
"export","internal","friend","uses","open","of","as","from","numbers","unsafe","mut","phase",
"query","on","cancellable","public","layer","start","shows","lasts","then","loads","system", 0
};
static const char* LUDIC_KW_STMT[] = { static const char* LUDIC_KW_STMT[] = {
"let","return","if","else","while","for","in","spawn","despawn", "let","if","else","while","for","in","where","match","machine","become","return","break",
"enable","disable","match","machine","state","become","where","prefab", "continue","new","spawn","despawn","enable","disable","attach","detach","emit","cancel",
"and","or","not","break","continue","new","emit","cancel","try", 0 "dispatch","try","and","or","not", 0
}; };
static const char* LUDIC_TYPES[] = { static const char* LUDIC_TYPES[] = {
"int","long","fixed","float","double","countdown","bool","entity","string","pointer","byte","words","fixeds","floats","doubles","pointers","Vector","IVec2","Rect","void", 0 "int","long","float","double","fixed","bool","byte","string","Key","pointer","void","entity",
"countdown","words","floats","doubles","fixeds","pointers","Vector","IVec2","Rect", 0
}; };
static const char* LUDIC_PHASES[] = { static const char* LUDIC_PHASES[] = {
"Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 0 "Start","Input","FixedUpdate","Update","LateUpdate","Render","Overlay", 0
}; };
static const char* LUDIC_ATTRIBUTES[] = {
"Ref","OneOf","Range","Unit","Asset","Color","Node","Clip","Material","Tint","Derived","Text",
"Multiline","Key","Computed","Sync","frame","max","owns","AppendOnly","ByKey","PerMap",
"Chunked","TextKey","export","Public","On","Queries","OnSpawn","OnDespawn","OnAttach",
"OnDetach","OnEnable","OnDisable","OnStart","OnQuit","Owned","Server","Predicted","ToServer",
"ToClients","System","EngineSystem","Namespace","deterministic","ClearColor","alloc_ok",
"creates","releases","Handles","Reads","Writes", 0
};
/* ludic-dev syntax: end */
static const char* LUDIC_WIDGETS[] = { static const char* LUDIC_WIDGETS[] = {
"panel","col","row","label","button","image","spacer", 0 "panel","col","row","label","button","image","spacer", 0
}; };