Merge branch 'lang/foundations' into lang/i18n-data
This commit is contained in:
commit
9f1273fb32
38 changed files with 122587 additions and 100240 deletions
|
|
@ -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` |
|
||||||
|
|
|
||||||
73
LANGUAGE.md
73
LANGUAGE.md
|
|
@ -801,6 +801,43 @@ object with `"schema_version": 1`:
|
||||||
and is left out.
|
and is left out.
|
||||||
- `units` - `@Unit`'s canonical spellings.
|
- `units` - `@Unit`'s canonical spellings.
|
||||||
- `lang` - the text keys and the languages (see Text keys), or null without a `lang` line.
|
- `lang` - the text keys and the languages (see Text keys), or null without a `lang` line.
|
||||||
|
- `code` - the program's code map, so an editor needs no scan of its own. Every place is
|
||||||
|
`"file:line:col"` (`"at"`); what the runtime declares and what the compiler writes itself are left
|
||||||
|
out. It holds:
|
||||||
|
- `modules` - `{"name", "package", "layer", "uses", "uses_at", "friend", "files"}`: `uses` null
|
||||||
|
without a `uses` line, `friend` null, or `{"of": null}` for a friend of every module and
|
||||||
|
`{"of": [...]}` for `friend module lab of a, b`; `files` its source files and the resource files
|
||||||
|
read into its registries;
|
||||||
|
- `states`, `actions` (`{"name", "module", "at"}`), `reducers` (`{"state", "action", "module",
|
||||||
|
"at"}`) and `dispatch` (every `dispatch`: `{"action", "module", "at"}`);
|
||||||
|
- `events` - `{"name", "module", "at", "cancellable", "net"}` (`net` `"toserver"` for
|
||||||
|
`@ToServer`, `"toclients"` for `@ToClients`, else null), its `emits` (every `emit`'s place) and
|
||||||
|
its `listeners` (the `@On` handlers: `{"handler", "module", "at"}`);
|
||||||
|
- `ports` - `{"name", "module", "at", "bound", "members"}`, each member `{"name", "type", "default",
|
||||||
|
"required"}` (its `fn` type as a field's is spelled, its default as written); and `binds` - one
|
||||||
|
row per member a `bind` gives: `{"port", "member", "fn", "value", "module", "at"}`, `fn` the
|
||||||
|
function a `fn name` names (null for a variable bound to a member that takes nothing) and
|
||||||
|
`value` as written;
|
||||||
|
- `handlers` - `{"name", "module", "at", "phase", "hook", "target", "public", "net", "queries",
|
||||||
|
"scene", "layer"}`: `phase` null for a hook, `hook` null or `"On"`, `"OnSpawn"`, `"OnDespawn"`,
|
||||||
|
`"OnAttach"`, `"OnDetach"`, `"OnEnable"`, `"OnDisable"`, `"OnStart"`, `"OnQuit"` with `target`
|
||||||
|
the event, model or property it names; `net` `"server"` (`@Server`), `"predicted"`
|
||||||
|
(`@Predicted`) or null; `queries` null or `{"these": [{"property", "filter"}], "on"}` (a
|
||||||
|
filter as written, or null); a scene's handler has its name as written and its `scene` and
|
||||||
|
`layer`;
|
||||||
|
- `models` (`{"name", "module", "at", "owned", "properties": [{"name", "sync"}]}`), `prefabs`
|
||||||
|
(`{"name", "model", "module", "at", "components": [{"property", "fields": [{"name",
|
||||||
|
"value"}]}]}`) and `scenes` (`{"name", "module", "at", "start", "public", "shows", "lasts",
|
||||||
|
"then", "loads", "enter", "exit", "layers"}`: `lasts` as written, `then` the scene it goes on to,
|
||||||
|
`loads` the scene a `loads then` one goes on to, `enter` / `exit` whether it has the block);
|
||||||
|
- `fn_refs` - every `fn name` written, in code or in a resource file: `{"fn", "module", "at",
|
||||||
|
"slot_kind", "slot", "entry"}`, the slot it fills: `"registry"` (`"Registry.field"`, `entry` the
|
||||||
|
row's key), `"port"` (a bind's `"Port.member"`), `"port_default"`, `"default"` (a record
|
||||||
|
field's default, `"Type.field"`), `"record"` (a `new` or a spawn's `"Type.field"`), `"event"`
|
||||||
|
(an `emit`'s `"Event.field"`), `"arg"` (`"callee(i)"`, the i-th argument written), `"assign"`
|
||||||
|
(`s.tick = fn f`: `"s.tick"`), `"let"` (the name), or `"value"` with a null slot.
|
||||||
|
|
||||||
|
Named lists are sorted by name (then file and line), sites by place.
|
||||||
|
|
||||||
Each list is sorted by name (then file and line), a registry's entries are in index order, and the
|
Each list is sorted by name (then file and line), a registry's entries are in index order, and the
|
||||||
paths are the ones the compiler read, so two runs over the same source write the same file. The
|
paths are the ones the compiler read, so two runs over the same source write the same file. The
|
||||||
|
|
@ -2323,7 +2360,8 @@ 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 schema -o build/schema.json # records, registries, entries, consts, components and natives, for editors
|
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 syntax --json # the language's vocabulary: keywords, declarations, types, phases, attributes, operators
|
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)
|
||||||
|
|
@ -2346,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
|
||||||
|
|
@ -2420,6 +2469,28 @@ had when a rule came in, which it may keep but not add to, lowered automatically
|
||||||
fixed - so a rule can arrive in a codebase that breaks it today (`ludic-fmt --init-baseline`
|
fixed - so a rule can arrive in a codebase that breaks it today (`ludic-fmt --init-baseline`
|
||||||
writes it). A `;` or a `#` inside a string does not count.
|
writes it). A `;` or a `#` inside a string does not count.
|
||||||
|
|
||||||
|
For editors and tools (`ludic fmt` finds the project the way these describe - the nearest
|
||||||
|
`package.ludic` upwards from the working directory, or from the directory of `--stdin-name` - and runs
|
||||||
|
the formatter from there):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ludic fmt --lint --json # the violations --lint prints, as one JSON array
|
||||||
|
ludic fmt - --stdin-name src/a.ludic < buf # a buffer formatted to stdout, as if it were src/a.ludic
|
||||||
|
ludic fmt - --lint --json --stdin-name src/a.ludic < buf # a buffer linted as src/a.ludic
|
||||||
|
```
|
||||||
|
|
||||||
|
`--lint --json` writes `[{"file", "line", "col", "rule", "message"}]` on stdout - every violation
|
||||||
|
`--lint` would print, ordered by file, line and column (`col`, 1-based, is left out for a rule with no
|
||||||
|
column: a file's length) - and everything else on stderr; the exit status is `--lint`'s (1 when a file
|
||||||
|
has more than its baseline allows). Unlike `--lint`, it never rewrites the baseline. `-` reads the
|
||||||
|
whole of stdin: formatting writes the result to stdout, and a buffer that does not read as Ludic - a
|
||||||
|
string, template or key literal left open, a bracket never closed or closed by the wrong one - is
|
||||||
|
refused with exit 2 and `<name>:<line>:<col>: error: ...` on stderr, nothing on stdout. With `--lint`,
|
||||||
|
the buffer is judged as the file `--stdin-name` names, relative to the project: its baseline
|
||||||
|
allowance applies, a name outside the `lint paths` breaks no rule (`[]`), and the report names it as
|
||||||
|
given (or `-`). A project with no `lint` lines lints a buffer clean. `--stdin-name` ending `.md`
|
||||||
|
formats the buffer as Markdown.
|
||||||
|
|
||||||
### Editors
|
### Editors
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,7 @@ cached in a content-addressed store:
|
||||||
```bash
|
```bash
|
||||||
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
ludic add git.workshopsoft.io/user/pkg # resolve, fetch, link into ludic_modules/
|
||||||
ludic get # install from package.ludic, write the lock
|
ludic get # install from package.ludic, write the lock
|
||||||
|
ludic remove git.workshopsoft.io/user/pkg # the inverse of add
|
||||||
ludic verify # check locked packages against the store
|
ludic verify # check locked packages against the store
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
9
changes/check-stdin-file.md
Normal file
9
changes/check-stdin-file.md
Normal 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.
|
||||||
12
changes/fmt-editor-output.md
Normal file
12
changes/fmt-editor-output.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic fmt` for editors.** `ludic fmt --lint --json` prints the violations `--lint` reports as one JSON
|
||||||
|
array on stdout, `[{"file", "line", "col", "rule", "message"}]` ordered by file, line and column (the
|
||||||
|
summary on stderr, `--lint`'s exit status, and the baseline never rewritten). `ludic fmt -` formats
|
||||||
|
stdin to stdout under the project found from the working directory (the nearest `package.ludic`
|
||||||
|
upwards), and refuses a buffer that does not read as Ludic - an open string, a bracket never closed or
|
||||||
|
closed by the wrong one - with exit 2 and `<name>:<line>:<col>: error: ...` on stderr.
|
||||||
|
`--stdin-name <path>` makes the buffer that file: the project is found from its directory, and
|
||||||
|
`ludic fmt - --lint --json --stdin-name <path>` judges it against that file's baseline and `lint
|
||||||
|
paths`, reporting it under the name given. Hooks around `fmt` and `get` read nothing from stdin and
|
||||||
|
write to stderr when the command's stdout is a program's (`--json`, `-`).
|
||||||
10
changes/pkg-remove-get-json.md
Normal file
10
changes/pkg-remove-get-json.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
bump: minor
|
||||||
|
type: feat
|
||||||
|
**`ludic remove <module>` and `ludic get --json`.** `remove` is the inverse of `add`: the `require`
|
||||||
|
leaves `package.ludic`, the lock keeps what the remaining requires still reach (read from the store's
|
||||||
|
copies of each package's manifest, no network), and each package leaving the lock loses the
|
||||||
|
`ludic_modules/` link `add` made - never the shared store entry. A module that is not required is
|
||||||
|
refused; source still importing a removed package is a warning. `ludic get --json` prints what the run
|
||||||
|
changed in the lock as one JSON object on stdout - `{"added", "removed", "changed", "unchanged"}`, an
|
||||||
|
entry as the lock records it (`name`, `version`, `hash`, `kind`, `provides`), a change as `{name, from,
|
||||||
|
to, from_hash, to_hash}` - with the resolver's own lines on stderr.
|
||||||
12
changes/schema-code.md
Normal file
12
changes/schema-code.md
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
bump: minor
|
||||||
|
type: feature
|
||||||
|
**`ludic schema` carries the code map: a `"code"` section.** Beside the records, registries and
|
||||||
|
`lang`, the schema now lists what an editor's code map needs, from the compiler's own reading:
|
||||||
|
`modules` (uses, layer, friend, files), `states`, `actions`, `reducers` and every `dispatch`, `events`
|
||||||
|
(cancellable, net) with every `emit` and their `@On` listeners, `ports` (members with their `fn`
|
||||||
|
types and defaults) and `binds` (port, member, the function named, where), `handlers` (phase, hook
|
||||||
|
and what it names, `@Public`, `@Server` / `@Predicted`, `@Queries` with their filters, the scene and
|
||||||
|
layer), `models` (`@Owned`, `@Sync`), `prefabs`, `scenes` (start, shows, lasts / loads and what
|
||||||
|
follows, layers) and `fn_refs` - every `fn name` in code or in a resource file, with the slot it
|
||||||
|
fills (a registry's field and row, a port's member, a record field, an argument, ...). Every place
|
||||||
|
is `"file:line:col"`; the rest of the schema is unchanged, so `schema_version` stays 1.
|
||||||
|
|
@ -19,7 +19,8 @@ This is the v1 implementation of the direction decided in issue #63.
|
||||||
|
|
||||||
```
|
```
|
||||||
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
|
ludic add <module>[@version] add a dependency to package.ludic, then resolve + fetch + link
|
||||||
ludic get resolve every dependency in package.ludic, link them, write the lock
|
ludic remove <module> the inverse of add: drop the require, what only it locked, its links
|
||||||
|
ludic get [--json] resolve every dependency in package.ludic, link them, write the lock
|
||||||
ludic update [module] bump a dependency (or all) to its latest published version, then relock
|
ludic update [module] bump a dependency (or all) to its latest published version, then relock
|
||||||
ludic verify check every locked package against the store by content hash
|
ludic verify check every locked package against the store by content hash
|
||||||
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
|
ludic vendor copy the resolved packages into ./vendor for hermetic/offline builds
|
||||||
|
|
@ -29,6 +30,23 @@ ludic vendor copy the resolved packages into ./vendor for herm
|
||||||
minimum. All the install commands print the resolved build list and write
|
minimum. All the install commands print the resolved build list and write
|
||||||
`package.lock.ludic`.
|
`package.lock.ludic`.
|
||||||
|
|
||||||
|
`ludic get --json` prints what the run changed in the lock as one JSON object on stdout (the
|
||||||
|
resolver's lines go to stderr): `{"added": [...], "removed": [...], "changed": [...], "unchanged": n}`,
|
||||||
|
an added or removed entry being the lock's line - `{"name", "version", "hash", "kind", "provides"}` -
|
||||||
|
and a changed one `{"name", "from", "to", "from_hash", "to_hash"}` (the versions, and the content
|
||||||
|
hashes beside them). The lock is compared before and after, in memory.
|
||||||
|
|
||||||
|
`ludic remove <module>` takes the `require` line out of `package.ludic` and re-reads the dependency
|
||||||
|
graph from the store's own copy of each locked package's manifest - no network - so the lock keeps
|
||||||
|
exactly what the remaining requires still reach: a package another one still requires stays locked
|
||||||
|
(at the version it had; `ludic get` settles versions), and what only the removed one brought in
|
||||||
|
leaves the lock with it. Each package leaving the lock loses its `ludic_modules/` link - the link
|
||||||
|
`add` made, never the store entry it points at, which other projects share; anything there that is
|
||||||
|
not such a link is left and reported, as is a copy under `vendor/`. A module `package.ludic` does
|
||||||
|
not require is refused (exit 1). Source that still imports a removed package is a warning, not a
|
||||||
|
failure - the caller may be about to delete it. If the store has no copy of a locked package, only
|
||||||
|
the named module leaves the lock, and it says so.
|
||||||
|
|
||||||
## The manifest — `package.ludic`
|
## The manifest — `package.ludic`
|
||||||
|
|
||||||
A line-oriented manifest. `#` starts a comment; strings are double-quoted.
|
A line-oriented manifest. `#` starts a comment; strings are double-quoted.
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ function kr_fill(i18n_st: mut I18nState, t: string, args: []string) -> string {
|
||||||
if h >= 1 {
|
if h >= 1 {
|
||||||
var v = ""
|
var v = ""
|
||||||
if h < len(args) { v = args[h] }
|
if h < len(args) { v = args[h] }
|
||||||
if i18n_is_key(v) { v = keyed(i18n_st, v) }
|
if i18n_is_key(v) { v = keyed(i18n_st, v) } else if i18n_st.on and kr_words(v) { v = L(i18n_st, v) }
|
||||||
r = r + t[seg..i] + v
|
r = r + t[seg..i] + v
|
||||||
i = hole_end(t, i)
|
i = hole_end(t, i)
|
||||||
seg = i
|
seg = i
|
||||||
|
|
@ -62,6 +62,13 @@ function kr_fill(i18n_st: mut I18nState, t: string, args: []string) -> string {
|
||||||
return r + t[seg..len(t)]
|
return r + t[seg..len(t)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# an argument with letters in it (not a number, a time, a sign): while the game still hands English
|
||||||
|
# into a key's holes (until its code and data are keys, phase 26.9), it gets the old English lookup
|
||||||
|
function kr_words(s: string) -> bool {
|
||||||
|
for i in 0 .. len(s) { if (s[i] >= 65 and s[i] <= 90) or (s[i] >= 97 and s[i] <= 122) { return true } }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
# a count written as decimal digits (a leading - allowed); anything else is 0
|
# a count written as decimal digits (a leading - allowed); anything else is 0
|
||||||
function kr_int(s: string) -> int {
|
function kr_int(s: string) -> int {
|
||||||
var v = 0
|
var v = 0
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ program I18nKeysTest {
|
||||||
function tt() -> string {
|
function tt() -> string {
|
||||||
var s = "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : 2);\\n\"\n\n"
|
var s = "msgid \"\"\nmsgstr \"\"\n\"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : 2);\\n\"\n\n"
|
||||||
s = s + po_line("pause.resume", "Devam") + po_line("hud.day", "{1}. Gun") + po_line("species.elk.a_name", "bir geyik")
|
s = s + po_line("pause.resume", "Devam") + po_line("hud.day", "{1}. Gun") + po_line("species.elk.a_name", "bir geyik")
|
||||||
s = s + po_line("catch.saw", "{2} saatinde {1} gordun.") + po_line("Settings", "Ayarlar")
|
s = s + po_line("catch.saw", "{2} saatinde {1} gordun.") + po_line("Settings", "Ayarlar") + po_line("Day {1}", "{1}. gun")
|
||||||
return s + plural("pack.items", "{1} esya", "{1} esyaa", "{1} esyalar")
|
return s + plural("pack.items", "{1} esya", "{1} esyaa", "{1} esyalar")
|
||||||
}
|
}
|
||||||
function start (i18n_st: mut I18nState) -> void {
|
function start (i18n_st: mut I18nState) -> void {
|
||||||
|
|
@ -74,4 +74,13 @@ program I18nKeysTest {
|
||||||
expect(L(i18n_st, tr(k"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")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test "an English value handed into a key's hole still gets its old English line, until it is a key itself" (i18n_st: mut I18nState) {
|
||||||
|
start(i18n_st)
|
||||||
|
expect(L(i18n_st, trf(k"catch.saw", "Settings", "5:30", "", "")) == "You saw Settings at 5:30.")
|
||||||
|
i18n_load_text(i18n_st, tt())
|
||||||
|
expect(L(i18n_st, trf(k"catch.saw", "Settings", "5:30", "", "")) == "5:30 saatinde Ayarlar gordun.") # exact
|
||||||
|
expect(L(i18n_st, trf(k"catch.saw", "Day 3", "5:30", "", "")) == "5:30 saatinde 3. gun gordun.") # a pattern
|
||||||
|
expect(L(i18n_st, trf(k"catch.saw", "Nowhere known", "7", "", "")) == "7 saatinde Nowhere known gordun.") # no line: as it is
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,9 @@
|
||||||
# stripped (the runtime supplies them), written as a field's type is ("fn(A,B)->R")
|
# stripped (the runtime supplies them), written as a field's type is ("fn(A,B)->R")
|
||||||
# components every UI component: props, state, derived fields, functions, events (emit_schema_ui)
|
# components every UI component: props, state, derived fields, functions, events (emit_schema_ui)
|
||||||
# natives every native tag registered with ui_native / ui_native_input by a literal
|
# natives every native tag registered with ui_native / ui_native_input by a literal
|
||||||
|
# units, lang @Unit's spellings; the text keys and the languages (emit_schema_lang)
|
||||||
|
# code the code map: modules, states, actions, reducers, dispatch, events, ports, binds,
|
||||||
|
# handlers, models, prefabs, scenes and fn_refs (emit_schema_code)
|
||||||
#
|
#
|
||||||
# Deterministic: each list sorted by name (then file and line), an entry list in index order, the
|
# Deterministic: each list sorted by name (then file and line), an entry list in index order, the
|
||||||
# paths as the compiler read them, and nothing about the run. The engine's runtime is left out.
|
# paths as the compiler read them, and nothing about the run. The engine's runtime is left out.
|
||||||
|
|
@ -47,6 +50,8 @@ function schema_buf() -> Buf {
|
||||||
sc_units(b)
|
sc_units(b)
|
||||||
buf_puts(b, "],\n \"lang\": ")
|
buf_puts(b, "],\n \"lang\": ")
|
||||||
sc_lang(b) # emit_schema_lang.ludic: the keys and the languages
|
sc_lang(b) # emit_schema_lang.ludic: the keys and the languages
|
||||||
|
buf_puts(b, ",\n \"code\": ")
|
||||||
|
sc_code(b) # emit_schema_code.ludic: the code map
|
||||||
buf_puts(b, "\n}\n")
|
buf_puts(b, "\n}\n")
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
|
||||||
459
selfhost/backend/emit_schema_code.ludic
Normal file
459
selfhost/backend/emit_schema_code.ludic
Normal file
|
|
@ -0,0 +1,459 @@
|
||||||
|
# emit_schema_code.ludic — the schema's "code" (emit_schema.ludic): the program's code map as the
|
||||||
|
# compiler read it, so an editor needs no scan of its own. Every place is "file:line:col".
|
||||||
|
#
|
||||||
|
# modules name, package, layer, uses (null without a `uses` line) and where, friend, files
|
||||||
|
# states name, module, at actions name, module, at
|
||||||
|
# reducers state, action, module, at dispatch action, module, at
|
||||||
|
# events name, module, at, cancellable, net (@ToServer / @ToClients), emits (sites) and
|
||||||
|
# listeners (the @On handlers: handler, module, at)
|
||||||
|
# ports name, module, at, bound, members (name, type, default as written, required)
|
||||||
|
# binds port, member, fn (the function a `fn name` names, else null), value as written, at
|
||||||
|
# handlers (emit_schema_code_game.ludic) name, module, at, phase, hook, target, public, net,
|
||||||
|
# queries, scene, layer
|
||||||
|
# models, prefabs, scenes, fn_refs (emit_schema_code_game.ludic)
|
||||||
|
#
|
||||||
|
# Each list is sorted by name (then file and line) or, for sites, by place. What the runtime
|
||||||
|
# declares, and what the compiler writes itself, is left out.
|
||||||
|
|
||||||
|
# ---- what the parser records for it ------------------------------------------------------------
|
||||||
|
var g_cx_emits: []Node = new []Node # every `emit E(...)` read
|
||||||
|
var g_cx_fnrefs: []Node = new []Node # every `fn name` read, in code or data
|
||||||
|
var g_cx_h: []Node = new []Node # every handler written: its node (name, phase, net role)
|
||||||
|
var g_cx_hname: []pointer = new []pointer # its name as written (a scene's are qualified later)
|
||||||
|
var g_cx_hhook: []pointer = new []pointer # null, "On", "OnSpawn", "OnDespawn", ..., "OnStart", "OnQuit"
|
||||||
|
var g_cx_htarget: []pointer = new []pointer # the event, model or property the hook names
|
||||||
|
var g_cx_hq: []Node = new []Node # its @Queries (an S_QUERY), or null
|
||||||
|
var g_cx_hpub: []bool = new []bool # @Public
|
||||||
|
var g_cx_hscene: []pointer = new []pointer # the scene and layer it is written in, or null
|
||||||
|
var g_cx_hlayer: []pointer = new []pointer
|
||||||
|
var g_cx_hlisten: []Node = new []Node # an @On handler's listener (g_onlisten), whose event is resolved
|
||||||
|
var g_cx_slayers: []pointer = new []pointer # per g_scenes entry: ",a,b," its layers
|
||||||
|
var g_cx_sthen: []pointer = new []pointer # `lasts N then T`'s T, or null (N is the scene's sp0..sp1)
|
||||||
|
var g_cx_sloads: []pointer = new []pointer # `loads then T`'s T, or null
|
||||||
|
var g_cx_spub: []bool = new []bool
|
||||||
|
|
||||||
|
function cx_emit(n: Node) -> void { push(g_cx_emits, n) }
|
||||||
|
function cx_fnref(n: Node) -> void { push(g_cx_fnrefs, n) }
|
||||||
|
function cx_handler(h: Node, name: pointer, hook: pointer, target: pointer, q: Node, pub: bool, scene: pointer, layer: pointer) -> void {
|
||||||
|
push(g_cx_h, h)
|
||||||
|
push(g_cx_hname, name)
|
||||||
|
push(g_cx_hhook, hook)
|
||||||
|
push(g_cx_htarget, target)
|
||||||
|
push(g_cx_hq, q)
|
||||||
|
push(g_cx_hpub, pub)
|
||||||
|
push(g_cx_hscene, scene)
|
||||||
|
push(g_cx_hlayer, layer)
|
||||||
|
push(g_cx_hlisten, null)
|
||||||
|
}
|
||||||
|
# the handler just recorded is an @On listener, the one just registered
|
||||||
|
function cx_listened() -> void {
|
||||||
|
let k = len(g_cx_hlisten) - 1
|
||||||
|
if k >= 0 and len(g_onlisten) > 0 { g_cx_hlisten[k] = g_onlisten[len(g_onlisten) - 1] }
|
||||||
|
}
|
||||||
|
function cx_scene(layers: pointer, then_scene: pointer, loads: pointer, pub: bool) -> void {
|
||||||
|
push(g_cx_slayers, layers)
|
||||||
|
push(g_cx_sthen, then_scene)
|
||||||
|
push(g_cx_sloads, loads)
|
||||||
|
push(g_cx_spub, pub)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- the section ----------------------------------------------------------------------------------
|
||||||
|
function sc_code(b: Buf) -> void {
|
||||||
|
buf_puts(b, "{\n \"modules\": [")
|
||||||
|
cx_modules(b)
|
||||||
|
buf_puts(b, "\n ],\n \"states\": [")
|
||||||
|
cx_decls(b, 2)
|
||||||
|
buf_puts(b, "\n ],\n \"actions\": [")
|
||||||
|
cx_decls(b, 3)
|
||||||
|
buf_puts(b, "\n ],\n \"reducers\": [")
|
||||||
|
cx_reducers(b)
|
||||||
|
buf_puts(b, "\n ],\n \"dispatch\": [")
|
||||||
|
cx_dispatch(b)
|
||||||
|
buf_puts(b, "\n ],\n \"events\": [")
|
||||||
|
cx_events(b)
|
||||||
|
buf_puts(b, "\n ],\n \"ports\": [")
|
||||||
|
cx_ports(b)
|
||||||
|
buf_puts(b, "\n ],\n \"binds\": [")
|
||||||
|
cx_binds(b)
|
||||||
|
buf_puts(b, "\n ],\n \"handlers\": [")
|
||||||
|
cx_handlers(b)
|
||||||
|
buf_puts(b, "\n ],\n \"models\": [")
|
||||||
|
cx_models(b)
|
||||||
|
buf_puts(b, "\n ],\n \"prefabs\": [")
|
||||||
|
cx_prefabs(b)
|
||||||
|
buf_puts(b, "\n ],\n \"scenes\": [")
|
||||||
|
cx_scenes(b)
|
||||||
|
buf_puts(b, "\n ],\n \"fn_refs\": [")
|
||||||
|
cx_fn_refs(b)
|
||||||
|
buf_puts(b, "\n ]\n }")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- places ---------------------------------------------------------------------------------------
|
||||||
|
function cx_at_text(n: Node) -> pointer {
|
||||||
|
return `{n.file}:{itoa(n.line)}:{itoa(n.col)}`
|
||||||
|
}
|
||||||
|
function cx_at(b: Buf, n: Node) -> void {
|
||||||
|
buf_puts(b, "\"at\": ")
|
||||||
|
if n == null or n.file == null { buf_puts(b, "null") } else { jq_put(b, cx_at_text(n)) }
|
||||||
|
}
|
||||||
|
# a key that sorts by place
|
||||||
|
function cx_at_key(n: Node) -> pointer {
|
||||||
|
var f = n.file
|
||||||
|
if f == null { f = "" }
|
||||||
|
return `{f}\t{itoa(1000000 + n.line)}\t{itoa(1000000 + n.col)}`
|
||||||
|
}
|
||||||
|
# written by the program, in a file it read (not the runtime's, not the compiler's own)
|
||||||
|
function cx_written(n: Node) -> bool {
|
||||||
|
return n != null and n.file != null and n.pos >= 0 and not is_runtime_file(n.file)
|
||||||
|
}
|
||||||
|
# or read from a resource file, whose tokens carry no offset (lex_at)
|
||||||
|
function cx_written_or_data(n: Node) -> bool {
|
||||||
|
if cx_written(n) { return true }
|
||||||
|
if n == null or n.file == null { return false }
|
||||||
|
let k = len(n.file)
|
||||||
|
return k > 5 and (n.file[k - 5 .. k] == ".lres")
|
||||||
|
}
|
||||||
|
function cx_module(b: Buf, file: pointer) -> void {
|
||||||
|
buf_puts(b, "\"module\": ")
|
||||||
|
sc_str(b, sc_mod(file))
|
||||||
|
}
|
||||||
|
# ",a,b," as a JSON array, each name once
|
||||||
|
function cx_list(b: Buf, list: pointer) -> void {
|
||||||
|
buf_puts(b, "[")
|
||||||
|
let seen = ss_new(64)
|
||||||
|
var n = 0
|
||||||
|
var i = 1
|
||||||
|
while i < len(list) {
|
||||||
|
var j = i
|
||||||
|
while j < len(list) and list[j] != ',' { j += 1 }
|
||||||
|
if j > i {
|
||||||
|
let name = list[i .. j]
|
||||||
|
if ss_add(seen, name) {
|
||||||
|
if n > 0 { buf_puts(b, ", ") }
|
||||||
|
jq_put(b, name)
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i = j + 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]")
|
||||||
|
}
|
||||||
|
# the indices 0..n-1 sorted by keys
|
||||||
|
function cx_order(keys: []pointer) -> []int {
|
||||||
|
let xs = new []int
|
||||||
|
var i = 0
|
||||||
|
while i < len(keys) {
|
||||||
|
if keys[i] != null { push(xs, i) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
sc_sort(xs, keys)
|
||||||
|
return xs
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- modules --------------------------------------------------------------------------------------
|
||||||
|
function cx_mod_names(names: []pointer, seen: StrSet, from: []pointer) -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(from) {
|
||||||
|
if from[i] != null and len(from[i]) > 0 and ss_add(seen, from[i]) { push(names, from[i]) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cx_modules(b: Buf) -> void {
|
||||||
|
let names = new []pointer
|
||||||
|
let seen = ss_new(256)
|
||||||
|
cx_mod_names(names, seen, g_mod_name)
|
||||||
|
cx_mod_names(names, seen, g_mu_mod)
|
||||||
|
cx_mod_names(names, seen, g_mod_friends)
|
||||||
|
let xs = cx_order(names)
|
||||||
|
var a = 0
|
||||||
|
while a < len(xs) {
|
||||||
|
let m = names[xs[a]]
|
||||||
|
sc_sep(b, a == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, m)
|
||||||
|
let files = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_mod_file) {
|
||||||
|
if (g_mod_name[i] == m) and not is_runtime_file(g_mod_file[i]) { push(files, g_mod_file[i]) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
var pkg: pointer = null
|
||||||
|
if len(files) > 0 and len(pkg_of_file(files[0])) > 0 { pkg = pkg_of_file(files[0]) }
|
||||||
|
buf_puts(b, ", \"package\": ")
|
||||||
|
sc_str(b, pkg)
|
||||||
|
let ly = mod_layer(m)
|
||||||
|
buf_puts(b, ", \"layer\": ")
|
||||||
|
if len(ly) == 0 { buf_puts(b, "null") } else { jq_put(b, ly) }
|
||||||
|
let u = mod_find_uses(m)
|
||||||
|
buf_puts(b, ", \"uses\": ")
|
||||||
|
if u < 0 { buf_puts(b, "null") } else { cx_list(b, g_mu_list[u]) }
|
||||||
|
buf_puts(b, ", \"uses_at\": ")
|
||||||
|
if u < 0 { buf_puts(b, "null") } else { jq_put(b, `{g_mu_file[u]}:{itoa(g_mu_line[u])}`) }
|
||||||
|
buf_puts(b, ", \"friend\": ")
|
||||||
|
cx_friend(b, m)
|
||||||
|
buf_puts(b, ", \"files\": [")
|
||||||
|
let fx = cx_order(files)
|
||||||
|
i = 0
|
||||||
|
while i < len(fx) {
|
||||||
|
if i > 0 { buf_puts(b, ", ") }
|
||||||
|
jq_put(b, files[fx[i]])
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
a += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# null, or {"of": null} for a friend of every module, else {"of": [the modules]}
|
||||||
|
function cx_friend(b: Buf, m: pointer) -> void {
|
||||||
|
var all = false
|
||||||
|
var list: pointer = null
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_mod_friends) {
|
||||||
|
if (g_mod_friends[i] == m) {
|
||||||
|
if len(g_fr_scope[i]) == 0 { all = true } else if list == null { list = g_fr_scope[i] } else { list = list + g_fr_scope[i] }
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if not all and list == null {
|
||||||
|
buf_puts(b, "null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf_puts(b, "{\"of\": ")
|
||||||
|
if all { buf_puts(b, "null") } else { cx_list(b, list) }
|
||||||
|
buf_puts(b, "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- states, actions, reducers, dispatch ----------------------------------------------------------
|
||||||
|
# the records of one kind (2 a state, 3 an action) in prog
|
||||||
|
function cx_decls(b: Buf, uns: int) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(prog) {
|
||||||
|
let d = prog[i]
|
||||||
|
if d.kind == N_COMP and d.uns == uns and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let d = prog[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(d.s))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, d.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, d)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cx_reducers(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_red_nodes) {
|
||||||
|
let f = g_red_nodes[i]
|
||||||
|
if cx_written(f) { push(keys, `{g_red_state[i]}\t{g_red_action[i]}\t{cx_at_key(f)}`) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let k = xs[i]
|
||||||
|
let f = g_red_nodes[k]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"state\": ")
|
||||||
|
jq_put(b, g_red_state[k])
|
||||||
|
buf_puts(b, ", \"action\": ")
|
||||||
|
jq_put(b, g_red_action[k])
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, f.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, f)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cx_dispatch(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_dsp_at) {
|
||||||
|
let n = g_dsp_at[i]
|
||||||
|
if cx_written(n) { push(keys, cx_at_key(n)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let n = g_dsp_at[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"action\": ")
|
||||||
|
jq_put(b, sc_name(g_dsp_names[xs[i]]))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, n.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, n)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- events ---------------------------------------------------------------------------------------
|
||||||
|
function cx_events(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_events) {
|
||||||
|
let e = g_events[i]
|
||||||
|
if e.s != null and cx_written(e) { push(keys, sc_key(sc_name(e.s), e.file, e.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let ekeys = new []pointer # the emits, by place
|
||||||
|
i = 0
|
||||||
|
while i < len(g_cx_emits) {
|
||||||
|
if cx_written(g_cx_emits[i]) { push(ekeys, cx_at_key(g_cx_emits[i])) } else { push(ekeys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let ex = cx_order(ekeys)
|
||||||
|
let hkeys = new []pointer # the listeners, by place
|
||||||
|
i = 0
|
||||||
|
while i < len(g_cx_h) {
|
||||||
|
if g_cx_hlisten[i] != null and cx_written(g_cx_h[i]) { push(hkeys, cx_at_key(g_cx_h[i])) } else { push(hkeys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let hx = cx_order(hkeys)
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let e = g_events[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(e.s))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, e.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, e)
|
||||||
|
buf_puts(b, ", \"cancellable\": ")
|
||||||
|
sc_bool(b, e.ival == 1)
|
||||||
|
buf_puts(b, ", \"net\": ")
|
||||||
|
sc_str(b, e.ty)
|
||||||
|
buf_puts(b, ", \"emits\": [")
|
||||||
|
var n = 0
|
||||||
|
var k = 0
|
||||||
|
while k < len(ex) {
|
||||||
|
let s = g_cx_emits[ex[k]]
|
||||||
|
if (s.s == e.s) {
|
||||||
|
if n > 0 { buf_puts(b, ", ") }
|
||||||
|
jq_put(b, cx_at_text(s))
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "], \"listeners\": [")
|
||||||
|
n = 0
|
||||||
|
k = 0
|
||||||
|
while k < len(hx) {
|
||||||
|
let h = hx[k]
|
||||||
|
if (g_cx_hlisten[h].s == e.s) {
|
||||||
|
if n > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"handler\": ")
|
||||||
|
jq_put(b, g_cx_hname[h])
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, g_cx_h[h].file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, g_cx_h[h])
|
||||||
|
buf_puts(b, "}")
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- ports and binds ------------------------------------------------------------------------------
|
||||||
|
function cx_ports(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_pt_name) {
|
||||||
|
let c = g_pt_comp[i]
|
||||||
|
if cx_written(c) { push(keys, sc_key(g_pt_name[i], c.file, c.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let k = xs[i]
|
||||||
|
let c = g_pt_comp[k]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, g_pt_name[k])
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, c.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, c)
|
||||||
|
buf_puts(b, ", \"bound\": ")
|
||||||
|
sc_bool(b, g_pt_bind[k] != null)
|
||||||
|
buf_puts(b, ", \"members\": [")
|
||||||
|
var f = 0
|
||||||
|
while f < len(c.kids) {
|
||||||
|
let fi = c.kids[f]
|
||||||
|
if f > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, fi.s)
|
||||||
|
buf_puts(b, ", \"type\": ")
|
||||||
|
jq_put(b, sc_ty(fi.ty))
|
||||||
|
buf_puts(b, ", \"default\": ")
|
||||||
|
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
|
||||||
|
buf_puts(b, ", \"required\": ")
|
||||||
|
sc_bool(b, fi.a == null)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
f += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# one row per member a bind gives
|
||||||
|
function cx_binds(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
let rows = new []Node
|
||||||
|
let ports = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_bd) {
|
||||||
|
let bd = g_bd[i]
|
||||||
|
if cx_written(bd) and bd.a != null {
|
||||||
|
var k = 0
|
||||||
|
while k < len(bd.a.kids) {
|
||||||
|
let fi = bd.a.kids[k]
|
||||||
|
if fi.kind == E_FINIT {
|
||||||
|
push(keys, cx_at_key(fi))
|
||||||
|
push(rows, fi)
|
||||||
|
push(ports, bd.s)
|
||||||
|
}
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let fi = rows[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"port\": ")
|
||||||
|
jq_put(b, ports[xs[i]])
|
||||||
|
buf_puts(b, ", \"member\": ")
|
||||||
|
jq_put(b, fi.s)
|
||||||
|
buf_puts(b, ", \"fn\": ")
|
||||||
|
if fi.a != null and fi.a.kind == E_FNREF and fi.a.pos >= 0 { jq_put(b, sc_name(fi.a.s)) } else { buf_puts(b, "null") }
|
||||||
|
buf_puts(b, ", \"value\": ")
|
||||||
|
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, fi.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, fi)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
415
selfhost/backend/emit_schema_code_game.ludic
Normal file
415
selfhost/backend/emit_schema_code_game.ludic
Normal file
|
|
@ -0,0 +1,415 @@
|
||||||
|
# emit_schema_code_game.ludic — the "code" section's handlers, models, prefabs, scenes and fn_refs
|
||||||
|
# (emit_schema_code.ludic says the shape of the rest).
|
||||||
|
#
|
||||||
|
# handlers name (as written: a scene's without its scene), module, at, phase (null for a hook),
|
||||||
|
# hook (null, "On", "OnSpawn", "OnDespawn", "OnAttach", "OnDetach", "OnEnable",
|
||||||
|
# "OnDisable", "OnStart", "OnQuit"), target (the hook's event, model or property),
|
||||||
|
# public (@Public), net ("server" for @Server, "predicted" for @Predicted, else null),
|
||||||
|
# queries (null, or {"these": [{"property", "filter"}], "on": model or null}), scene, layer
|
||||||
|
# models name, module, at, owned (@Owned), properties [{"name", "sync"}]
|
||||||
|
# prefabs name, model, module, at, components [{"property", "fields": [{"name", "value"}]}]
|
||||||
|
# scenes name, module, at, start, public, shows, lasts (as written) and then, loads (the scene it
|
||||||
|
# goes on to), enter, exit (has one), layers
|
||||||
|
# fn_refs every `fn name` written, in code or data: fn, module, at, and the slot it fills -
|
||||||
|
# slot_kind "registry" (slot "Registry.field", entry its key), "port" (a bind's
|
||||||
|
# "Port.member"), "port_default", "default" (a record field's), "record" (a `new` or a
|
||||||
|
# spawn's "Type.field"), "event" (an emit's "Event.field"), "arg" ("callee(i)", the i-th
|
||||||
|
# argument written), "assign" (`s.tick = fn f`: "s.tick"), "let" (the name) or "value"
|
||||||
|
# (anywhere else, slot null)
|
||||||
|
|
||||||
|
# ---- handlers -------------------------------------------------------------------------------------
|
||||||
|
function cx_handlers(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_cx_h) {
|
||||||
|
let h = g_cx_h[i]
|
||||||
|
if cx_written(h) { push(keys, sc_key(g_cx_hname[i], h.file, h.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let k = xs[i]
|
||||||
|
let h = g_cx_h[k]
|
||||||
|
let hook = g_cx_hhook[k]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, g_cx_hname[k])
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, h.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, h)
|
||||||
|
buf_puts(b, ", \"phase\": ")
|
||||||
|
if hook == null or (hook == "OnStart") or (hook == "OnQuit") { sc_str(b, h.ty) } else { buf_puts(b, "null") }
|
||||||
|
buf_puts(b, ", \"hook\": ")
|
||||||
|
sc_str(b, hook)
|
||||||
|
buf_puts(b, ", \"target\": ")
|
||||||
|
sc_str(b, g_cx_htarget[k])
|
||||||
|
buf_puts(b, ", \"public\": ")
|
||||||
|
sc_bool(b, g_cx_hpub[k])
|
||||||
|
buf_puts(b, ", \"net\": ")
|
||||||
|
if h.ival == 1 { jq_put(b, "server") } else if h.ival == 2 { jq_put(b, "predicted") } else { buf_puts(b, "null") }
|
||||||
|
buf_puts(b, ", \"queries\": ")
|
||||||
|
cx_queries(b, g_cx_hq[k])
|
||||||
|
buf_puts(b, ", \"scene\": ")
|
||||||
|
sc_str(b, g_cx_hscene[k])
|
||||||
|
buf_puts(b, ", \"layer\": ")
|
||||||
|
sc_str(b, g_cx_hlayer[k])
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# @Queries(these: [P {filter}, ...], on: M): its terms, a filter as written (parse_queries_anno)
|
||||||
|
function cx_queries(b: Buf, q: Node) -> void {
|
||||||
|
if q == null or q.c == null {
|
||||||
|
buf_puts(b, "null")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
buf_puts(b, "{\"these\": [")
|
||||||
|
var on: pointer = null
|
||||||
|
var n = 0
|
||||||
|
var i = 0
|
||||||
|
while i < len(q.c.kids) {
|
||||||
|
let t = q.c.kids[i]
|
||||||
|
if t.ival == 1 {
|
||||||
|
on = t.s
|
||||||
|
} else {
|
||||||
|
if n > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"property\": ")
|
||||||
|
jq_put(b, t.s)
|
||||||
|
buf_puts(b, ", \"filter\": ")
|
||||||
|
sc_str(b, sc_text(t.file, t.sp0, t.sp1))
|
||||||
|
buf_puts(b, "}")
|
||||||
|
n += 1
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "], \"on\": ")
|
||||||
|
sc_str(b, on)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- models and prefabs ---------------------------------------------------------------------------
|
||||||
|
function cx_models(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(prog) {
|
||||||
|
let d = prog[i]
|
||||||
|
if d.kind == N_ARCH and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let d = prog[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(d.s))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, d.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, d)
|
||||||
|
buf_puts(b, ", \"owned\": ")
|
||||||
|
sc_bool(b, d.ival == 1)
|
||||||
|
buf_puts(b, ", \"properties\": [")
|
||||||
|
var k = 0
|
||||||
|
while k < len(d.kids) {
|
||||||
|
if k > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(d.kids[k].s))
|
||||||
|
buf_puts(b, ", \"sync\": ")
|
||||||
|
sc_bool(b, d.kids[k].ival == 1)
|
||||||
|
buf_puts(b, "}")
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cx_prefabs(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(prog) {
|
||||||
|
let d = prog[i]
|
||||||
|
if d.kind == N_PREFAB and d.s != null and cx_written(d) { push(keys, sc_key(sc_name(d.s), d.file, d.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let d = prog[xs[i]]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(d.s))
|
||||||
|
buf_puts(b, ", \"model\": ")
|
||||||
|
sc_str(b, sc_name(d.ty))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, d.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, d)
|
||||||
|
buf_puts(b, ", \"components\": [")
|
||||||
|
var k = 0
|
||||||
|
while k < len(d.kids) {
|
||||||
|
let ci = d.kids[k]
|
||||||
|
if k > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"property\": ")
|
||||||
|
jq_put(b, sc_name(ci.s))
|
||||||
|
buf_puts(b, ", \"fields\": [")
|
||||||
|
var f = 0
|
||||||
|
while ci.a != null and f < len(ci.a.kids) {
|
||||||
|
let fi = ci.a.kids[f]
|
||||||
|
if f > 0 { buf_puts(b, ", ") }
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, fi.s)
|
||||||
|
buf_puts(b, ", \"value\": ")
|
||||||
|
sc_str(b, sc_text(fi.file, fi.sp0, fi.sp1))
|
||||||
|
buf_puts(b, "}")
|
||||||
|
f += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
buf_puts(b, "]}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- scenes ---------------------------------------------------------------------------------------
|
||||||
|
function cx_scenes(b: Buf) -> void {
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_scenes) {
|
||||||
|
let n = g_scenes[i]
|
||||||
|
if cx_written(n) and i < len(g_cx_slayers) { push(keys, sc_key(sc_name(n.s), n.file, n.line)) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
let k = xs[i]
|
||||||
|
let n = g_scenes[k]
|
||||||
|
sc_sep(b, i == 0, " ")
|
||||||
|
buf_puts(b, "{\"name\": ")
|
||||||
|
jq_put(b, sc_name(n.s))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, n.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, n)
|
||||||
|
buf_puts(b, ", \"start\": ")
|
||||||
|
sc_bool(b, n.ival == g_start_scene)
|
||||||
|
buf_puts(b, ", \"public\": ")
|
||||||
|
sc_bool(b, g_cx_spub[k])
|
||||||
|
buf_puts(b, ", \"shows\": ")
|
||||||
|
sc_str(b, n.ty)
|
||||||
|
buf_puts(b, ", \"lasts\": ")
|
||||||
|
sc_str(b, sc_text(n.file, n.sp0, n.sp1))
|
||||||
|
buf_puts(b, ", \"then\": ")
|
||||||
|
sc_str(b, g_cx_sthen[k])
|
||||||
|
buf_puts(b, ", \"loads\": ")
|
||||||
|
sc_str(b, g_cx_sloads[k])
|
||||||
|
buf_puts(b, ", \"enter\": ")
|
||||||
|
sc_bool(b, n.a != null)
|
||||||
|
buf_puts(b, ", \"exit\": ")
|
||||||
|
sc_bool(b, n.b != null)
|
||||||
|
buf_puts(b, ", \"layers\": ")
|
||||||
|
cx_list(b, g_cx_slayers[k])
|
||||||
|
buf_puts(b, "}")
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- fn refs --------------------------------------------------------------------------------------
|
||||||
|
# the walk's findings: each `fn name` node met, with the slot it was met in (the first meeting wins)
|
||||||
|
var g_cx_wn: []Node = new []Node
|
||||||
|
var g_cx_wkind: []pointer = new []pointer
|
||||||
|
var g_cx_wslot: []pointer = new []pointer
|
||||||
|
var g_cx_wentry: []pointer = new []pointer
|
||||||
|
|
||||||
|
function cx_fn_refs(b: Buf) -> void {
|
||||||
|
cx_walk_all()
|
||||||
|
let keys = new []pointer
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_cx_fnrefs) {
|
||||||
|
let n = g_cx_fnrefs[i]
|
||||||
|
if cx_written_or_data(n) and n.s != null { push(keys, `{cx_at_key(n)}\t{n.s}`) } else { push(keys, null) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
let xs = cx_order(keys)
|
||||||
|
i = 0
|
||||||
|
var first = true
|
||||||
|
var last: pointer = null
|
||||||
|
while i < len(xs) {
|
||||||
|
let n = g_cx_fnrefs[xs[i]]
|
||||||
|
let key = keys[xs[i]]
|
||||||
|
if last == null or not (key == last) { # a node read twice is one site
|
||||||
|
last = key
|
||||||
|
var w = 0
|
||||||
|
while w < len(g_cx_wn) and g_cx_wn[w] != n { w += 1 }
|
||||||
|
sc_sep(b, first, " ")
|
||||||
|
first = false
|
||||||
|
buf_puts(b, "{\"fn\": ")
|
||||||
|
jq_put(b, sc_name(n.s))
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_module(b, n.file)
|
||||||
|
buf_puts(b, ", ")
|
||||||
|
cx_at(b, n)
|
||||||
|
buf_puts(b, ", \"slot_kind\": ")
|
||||||
|
if w < len(g_cx_wn) { jq_put(b, g_cx_wkind[w]) } else { jq_put(b, "value") }
|
||||||
|
buf_puts(b, ", \"slot\": ")
|
||||||
|
if w < len(g_cx_wn) { sc_str(b, g_cx_wslot[w]) } else { buf_puts(b, "null") }
|
||||||
|
buf_puts(b, ", \"entry\": ")
|
||||||
|
if w < len(g_cx_wn) { sc_str(b, g_cx_wentry[w]) } else { buf_puts(b, "null") }
|
||||||
|
buf_puts(b, "}")
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# every place a `fn name` can be written, the narrowest slots first: a registry's rows, the binds, the
|
||||||
|
# ports' defaults, then the program's declarations and the bodies kept outside them
|
||||||
|
function cx_walk_all() -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(g_df_rec) {
|
||||||
|
cx_walk_rec(g_df_rec[i], "registry", g_df_reg[i], g_df_key[i], 0)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(g_bd) {
|
||||||
|
if g_bd[i].a != null { cx_walk_rec(g_bd[i].a, "port", g_bd[i].s, null, 0) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(g_pt_comp) {
|
||||||
|
let c = g_pt_comp[i]
|
||||||
|
var f = 0
|
||||||
|
while f < len(c.kids) {
|
||||||
|
cx_walk(c.kids[f].a, "port_default", `{g_pt_name[i]}.{c.kids[f].s}`, null, 0)
|
||||||
|
f += 1
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(prog) {
|
||||||
|
if prog[i].file == null or not is_runtime_file(prog[i].file) { cx_walk(prog[i], "value", null, null, 0) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
cx_walk_list(g_tests)
|
||||||
|
cx_walk_list(g_onlisten)
|
||||||
|
cx_walk_list(g_onspawn)
|
||||||
|
cx_walk_list(g_ondespawn)
|
||||||
|
cx_walk_list(g_onattach)
|
||||||
|
cx_walk_list(g_ondetach)
|
||||||
|
cx_walk_list(g_onenable)
|
||||||
|
cx_walk_list(g_ondisable)
|
||||||
|
cx_walk_list(g_scenes)
|
||||||
|
}
|
||||||
|
function cx_walk_list(xs: []Node) -> void {
|
||||||
|
var i = 0
|
||||||
|
while i < len(xs) {
|
||||||
|
cx_walk(xs[i], "value", null, null, 0)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
# a record literal's values, each in the slot owner.field
|
||||||
|
function cx_walk_rec(rec: Node, kind: pointer, owner: pointer, entry: pointer, depth: int) -> void {
|
||||||
|
if rec == null or depth > 400 { return }
|
||||||
|
var i = 0
|
||||||
|
while i < len(rec.kids) {
|
||||||
|
let fi = rec.kids[i]
|
||||||
|
if fi.kind == E_FINIT { cx_walk(fi.a, kind, `{owner}.{fi.s}`, entry, depth + 1) } else { cx_walk(fi, "value", null, null, depth + 1) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function cx_callee(c: Node) -> pointer {
|
||||||
|
if c == null { return null }
|
||||||
|
if c.kind == E_ID and c.s != null { return sc_name(c.s) }
|
||||||
|
if c.kind == E_MEMBER and c.a != null and c.a.kind == E_ID and c.a.s != null and c.s != null { return `{c.a.s}.{c.s}` }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
function cx_walk(n: Node, kind: pointer, slot: pointer, entry: pointer, depth: int) -> void {
|
||||||
|
if n == null or depth > 400 { return }
|
||||||
|
let k = n.kind
|
||||||
|
let d = depth + 1
|
||||||
|
if k == E_FNREF {
|
||||||
|
push(g_cx_wn, n)
|
||||||
|
push(g_cx_wkind, kind)
|
||||||
|
push(g_cx_wslot, slot)
|
||||||
|
push(g_cx_wentry, entry)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == E_NEW and n.a != null and n.a.kind == E_REC and n.s != null {
|
||||||
|
cx_walk_rec(n.a, "record", sc_name(n.s), null, d)
|
||||||
|
cx_walk(n.b, "value", null, null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == S_EMIT and n.a != null and n.s != null {
|
||||||
|
cx_walk_rec(n.a, "event", sc_name(n.s), null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (k == S_SPAWN or k == N_PREFAB) {
|
||||||
|
var i = 0
|
||||||
|
while i < len(n.kids) {
|
||||||
|
let ci = n.kids[i]
|
||||||
|
if ci.kind == E_FINIT and ci.a != null and ci.a.kind == E_REC { cx_walk_rec(ci.a, "record", sc_name(ci.s), null, d) } else { cx_walk(ci, "value", null, null, d) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == E_CALL {
|
||||||
|
let callee = cx_callee(n.a)
|
||||||
|
cx_walk(n.a, "value", null, null, d)
|
||||||
|
var i = 0
|
||||||
|
var arg = 0
|
||||||
|
while i < len(n.kids) {
|
||||||
|
let a = n.kids[i]
|
||||||
|
if a != null and a.pos >= 0 {
|
||||||
|
if callee != null { cx_walk(a, "arg", `{callee}({itoa(arg)})`, null, d) } else { cx_walk(a, "value", null, null, d) }
|
||||||
|
arg += 1
|
||||||
|
} else {
|
||||||
|
cx_walk(a, "value", null, null, d)
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == N_COMP {
|
||||||
|
var i = 0
|
||||||
|
while i < len(n.kids) {
|
||||||
|
if n.s != null and n.kids[i].s != null { cx_walk(n.kids[i].a, "default", `{sc_name(n.s)}.{n.kids[i].s}`, null, d) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == S_ASSIGN and n.b != null and n.b.kind == E_FNREF { # `s.tick = fn f`: the slot as written
|
||||||
|
cx_walk(n.b, "assign", cx_callee(n.a), null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == S_LET and n.a != null and n.a.kind == E_FNREF and n.s != null {
|
||||||
|
cx_walk(n.a, "let", n.s, null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == E_LIST { # a list of fn values fills the slot it is in
|
||||||
|
var i = 0
|
||||||
|
while i < len(n.kids) {
|
||||||
|
cx_walk(n.kids[i], kind, slot, entry, d)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == N_SYS { # its .b and .c are its layer's and scene's tags
|
||||||
|
cx_walk(n.a, "value", null, null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if k == N_SCENE {
|
||||||
|
cx_walk(n.a, "value", null, null, d)
|
||||||
|
cx_walk(n.b, "value", null, null, d)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cx_walk(n.a, "value", null, null, d)
|
||||||
|
cx_walk(n.b, "value", null, null, d)
|
||||||
|
cx_walk(n.c, "value", null, null, d)
|
||||||
|
var i = 0
|
||||||
|
while i < len(n.kids) {
|
||||||
|
cx_walk(n.kids[i], "value", null, null, d)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -304,6 +304,7 @@ const TPL_HOLE: int = 92 # on the callee of e
|
||||||
# non-cancellable event yields 0.
|
# non-cancellable event yields 0.
|
||||||
function parse_emit() -> Node {
|
function parse_emit() -> Node {
|
||||||
pi += 1; let n = node(S_EMIT); n.s = eat_id()
|
pi += 1; let n = node(S_EMIT); n.s = eat_id()
|
||||||
|
cx_emit(n) # the schema's code map (emit_schema_code.ludic)
|
||||||
let r = node(E_REC)
|
let r = node(E_REC)
|
||||||
eat_op("("); skipnl()
|
eat_op("("); skipnl()
|
||||||
while not is_op(")") {
|
while not is_op(")") {
|
||||||
|
|
@ -344,7 +345,7 @@ function p_primary() -> Node {
|
||||||
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
if (t.text == "spawn") { return parse_spawn() } # spawn as an expression: the new entity
|
||||||
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
|
# fn name — a top-level function as a value, for a worker entry point (Job.parallel_for)
|
||||||
# `fn` stays an ordinary name before an operator word (`p < fn and ...`) or at the end of a line
|
# `fn` stays an ordinary name before an operator word (`p < fn and ...`) or at the end of a line
|
||||||
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 1].line == t.line) and (toks[pi + 1].text != "and") and (toks[pi + 1].text != "or") and (toks[pi + 1].text != "not") and (toks[pi + 1].text != "in") and (toks[pi + 1].text != "is") { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); return n }
|
if (t.text == "fn") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 1].line == t.line) and (toks[pi + 1].text != "and") and (toks[pi + 1].text != "or") and (toks[pi + 1].text != "not") and (toks[pi + 1].text != "in") and (toks[pi + 1].text != "is") { pi += 1; let n = node(E_FNREF); n.line = t.line; n.s = eat_id(); cx_fnref(n); return n }
|
||||||
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
# try EXPR else { ... } — evaluate a fallible (result-typed) expression; on
|
||||||
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
# `ok` the whole expression is its payload, on `err` the else block runs (with
|
||||||
# the message bound to `error`) and its trailing expression is the fallback.
|
# the message bound to `error`) and its trailing expression is the fallback.
|
||||||
|
|
@ -954,6 +955,30 @@ function already_loaded(full: pointer) -> bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# a handler's hook, for the schema's code map: the attribute that made it one, and what it names
|
||||||
|
function decl_hook(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer, ph: pointer) -> pointer {
|
||||||
|
if ev != null { return "On" }
|
||||||
|
if sp != null { return "OnSpawn" }
|
||||||
|
if dsp != null { return "OnDespawn" }
|
||||||
|
if att != null { return "OnAttach" }
|
||||||
|
if det != null { return "OnDetach" }
|
||||||
|
if en != null { return "OnEnable" }
|
||||||
|
if dis != null { return "OnDisable" }
|
||||||
|
if ph != null {
|
||||||
|
if (ph == "Start") { return "OnStart" }
|
||||||
|
return "OnQuit"
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
function decl_hook_target(ev: pointer, sp: pointer, dsp: pointer, att: pointer, det: pointer, en: pointer, dis: pointer) -> pointer {
|
||||||
|
if ev != null { return ev }
|
||||||
|
if sp != null { return sp }
|
||||||
|
if dsp != null { return dsp }
|
||||||
|
if att != null { return att }
|
||||||
|
if det != null { return det }
|
||||||
|
if en != null { return en }
|
||||||
|
return dis
|
||||||
|
}
|
||||||
# parse one top-level declaration (or resolve an import) into `prog`.
|
# parse one top-level declaration (or resolve an import) into `prog`.
|
||||||
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
|
# Modifiers are `@annotations` in front of the declaration: `@export`, `@edge`,
|
||||||
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
|
# `@pure`, `@deterministic`, … — one channel, not a zoo of prefix keywords.
|
||||||
|
|
@ -1108,7 +1133,9 @@ function parse_one_decl() -> void {
|
||||||
if is_det { push(g_det_names, h.s) }
|
if is_det { push(g_det_names, h.s) }
|
||||||
if g_aok_pending { push(g_alloc_ok_names, h.s); g_aok_pending = false }
|
if g_aok_pending { push(g_alloc_ok_names, h.s); g_aok_pending = false }
|
||||||
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
if (role != null) { if (role == "server") { h.ival = 1 } else { h.ival = 2 } } # N5: @Server=1 / @Predicted=2
|
||||||
if (on_event != null) { register_onlisten(on_event, h.a); return } # @On(Event) listener
|
cx_handler(h, h.s, decl_hook(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop, hook_phase),
|
||||||
|
decl_hook_target(on_event, onspawn_model, ondespawn_model, onattach_prop, ondetach_prop, onenable_prop, ondisable_prop), qspec, is_public, null, null) # the schema's code map
|
||||||
|
if (on_event != null) { register_onlisten(on_event, h.a); cx_listened(); return } # @On(Event) listener
|
||||||
if (onspawn_model != null) {
|
if (onspawn_model != null) {
|
||||||
register_onspawn(onspawn_model, h.a) # spawn hook
|
register_onspawn(onspawn_model, h.a) # spawn hook
|
||||||
if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn
|
if is_public { ensure_event(`model_{onspawn_model}_spawn`, false) } # @Public -> model_<M>_spawn
|
||||||
|
|
|
||||||
|
|
@ -189,7 +189,7 @@ function parse_queries_anno() -> Node {
|
||||||
let pname = eat_id()
|
let pname = eat_id()
|
||||||
let v = node(E_ID); v.s = pname; push(qn.kids, v) # binding var = property name
|
let v = node(E_ID); v.s = pname; push(qn.kids, v) # binding var = property name
|
||||||
let t = node(E_ID); t.s = pname; t.ival = 0; push(terms.kids, t)
|
let t = node(E_ID); t.s = pname; t.ival = 0; push(terms.kids, t)
|
||||||
if is_op("{") { pi += 1; let ce = expr(); eat_op("}")
|
if is_op("{") { pi += 1; let f0 = pi; let ce = expr(); t.sp0 = toks[f0].off; t.sp1 = toks[pi - 1].oend; eat_op("}")
|
||||||
let cb = node(E_ID); cb.s = pname; wh = mk_and(wh, qualify_fields(ce, cb)) }
|
let cb = node(E_ID); cb.s = pname; wh = mk_and(wh, qualify_fields(ce, cb)) }
|
||||||
if is_op(",") { pi += 1 }
|
if is_op(",") { pi += 1 }
|
||||||
skipnl()
|
skipnl()
|
||||||
|
|
@ -249,12 +249,13 @@ function parse_scene() -> void {
|
||||||
while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") {
|
while is_id("start") or is_id("public") or is_id("shows") or is_id("lasts") or is_id("loads") {
|
||||||
if is_id("start") { pi += 1; g_start_scene = g_scene_count }
|
if is_id("start") { pi += 1; g_start_scene = g_scene_count }
|
||||||
else if is_id("shows") { pi += 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it
|
else if is_id("shows") { pi += 1; n.ty = eat_id() } # shows Menu: the engine opens, renders and closes it
|
||||||
else if is_id("lasts") { pi += 1; lasts = expr(); if not is_id("then") { perr("lasts N needs `then Scene`") }; pi += 1; then_scene = eat_id() }
|
else if is_id("lasts") { pi += 1; let l0 = pi; lasts = expr(); n.sp0 = toks[l0].off; n.sp1 = toks[pi - 1].oend; if not is_id("then") { perr("lasts N needs `then Scene`") }; pi += 1; then_scene = eat_id() }
|
||||||
else if is_id("loads") { pi += 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi += 1; loads_then = eat_id(); g_uses_atlas = true }
|
else if is_id("loads") { pi += 1; if not is_id("then") { perr("loads needs `then Scene`") }; pi += 1; loads_then = eat_id(); g_uses_atlas = true }
|
||||||
else { pi += 1; is_pub = true }
|
else { pi += 1; is_pub = true }
|
||||||
}
|
}
|
||||||
g_scene_count += 1
|
g_scene_count += 1
|
||||||
if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) }
|
if is_pub { ensure_event_empty(`scene_{n.s}_enter`); ensure_event_empty(`scene_{n.s}_exit`) }
|
||||||
|
var layers = "," # the schema's code map (emit_schema_code.ludic)
|
||||||
skipnl(); eat_op("{")
|
skipnl(); eat_op("{")
|
||||||
while true {
|
while true {
|
||||||
skipnl(); if is_op("}") { break }
|
skipnl(); if is_op("}") { break }
|
||||||
|
|
@ -273,6 +274,7 @@ function parse_scene() -> void {
|
||||||
}
|
}
|
||||||
if is_id("layer") { # layer Name [public] { handler .. }
|
if is_id("layer") { # layer Name [public] { handler .. }
|
||||||
pi += 1; let lname = eat_id()
|
pi += 1; let lname = eat_id()
|
||||||
|
layers = layers + lname + ","
|
||||||
if is_id("public") { pi += 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) }
|
if is_id("public") { pi += 1; ensure_event_empty(`layer_{lname}_show`); ensure_event_empty(`layer_{lname}_hide`) }
|
||||||
skipnl(); eat_op("{")
|
skipnl(); eat_op("{")
|
||||||
let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler
|
let ltag = node(E_ID); ltag.s = lname # the layer name, tagged onto each handler
|
||||||
|
|
@ -287,6 +289,7 @@ function parse_scene() -> void {
|
||||||
}
|
}
|
||||||
if not is_id("handler") { perr("expected 'handler' in layer") }
|
if not is_id("handler") { perr("expected 'handler' in layer") }
|
||||||
let h = parse_system() # N_SYS: .s name, .ty phase, .a body
|
let h = parse_system() # N_SYS: .s name, .ty phase, .a body
|
||||||
|
cx_handler(h, h.s, null, null, qspec, false, n.s, lname)
|
||||||
if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap }
|
if (qspec != null) { qspec.a = h.a; let wrap = node(N_BLOCK); push(wrap.kids, qspec); h.a = wrap }
|
||||||
h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler
|
h.s = `{n.s}_{h.s}` # scene-qualified: two scenes may both have a `Draw` handler
|
||||||
h.c = n # tag the owning scene (null = global)
|
h.c = n # tag the owning scene (null = global)
|
||||||
|
|
@ -302,6 +305,7 @@ function parse_scene() -> void {
|
||||||
eat_op("}")
|
eat_op("}")
|
||||||
if (lasts != null) { scene_add_timer(n, lasts, then_scene) }
|
if (lasts != null) { scene_add_timer(n, lasts, then_scene) }
|
||||||
if (loads_then != null) { scene_add_loader(n, loads_then) }
|
if (loads_then != null) { scene_add_loader(n, loads_then) }
|
||||||
|
cx_scene(layers, then_scene, loads_then, is_pub)
|
||||||
push(g_scenes, n)
|
push(g_scenes, n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
122
selfhost/frontend/stdin_file.ludic
Normal file
122
selfhost/frontend/stdin_file.ludic
Normal 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
|
||||||
|
}
|
||||||
107222
selfhost/ludicc.seed.ll
107222
selfhost/ludicc.seed.ll
File diff suppressed because one or more lines are too long
113631
selfhost/ludicc.win.seed.ll
113631
selfhost/ludicc.win.seed.ll
File diff suppressed because it is too large
Load diff
|
|
@ -163,6 +163,7 @@ entry {
|
||||||
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 == "--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
|
||||||
|
|
@ -263,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)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ program LudicDev {
|
||||||
import "assets.ludic"
|
import "assets.ludic"
|
||||||
import "release.ludic"
|
import "release.ludic"
|
||||||
import "pkg.ludic"
|
import "pkg.ludic"
|
||||||
|
import "pkg_lock.ludic"
|
||||||
import "pkg_test.ludic"
|
import "pkg_test.ludic"
|
||||||
|
|
||||||
function usage() -> void {
|
function usage() -> void {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ program Ludic {
|
||||||
import "split.ludic"
|
import "split.ludic"
|
||||||
import "project.ludic"
|
import "project.ludic"
|
||||||
import "pkg.ludic"
|
import "pkg.ludic"
|
||||||
|
import "pkg_lock.ludic"
|
||||||
|
import "pkg_remove.ludic"
|
||||||
import "assets.ludic"
|
import "assets.ludic"
|
||||||
import "packignore.ludic"
|
import "packignore.ludic"
|
||||||
import "pack.ludic"
|
import "pack.ludic"
|
||||||
|
|
@ -49,6 +51,7 @@ 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]")
|
||||||
|
|
@ -60,7 +63,9 @@ program Ludic {
|
||||||
print("")
|
print("")
|
||||||
print("packages:")
|
print("packages:")
|
||||||
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
|
print(" add <module>[@version] add a dependency, then resolve + fetch + link")
|
||||||
print(" get resolve every package.ludic dependency and write the lock")
|
print(" remove <module> drop a dependency: its require, what only it locked, its ludic_modules/ link")
|
||||||
|
print(" get [--json] resolve every package.ludic dependency and write the lock")
|
||||||
|
print(" (--json: what changed in the lock, as a JSON object on stdout)")
|
||||||
print(" update [module] bump a dependency (or all) to its latest published version")
|
print(" update [module] bump a dependency (or all) to its latest published version")
|
||||||
print(" verify check every locked package against the store by content hash")
|
print(" verify check every locked package against the store by content hash")
|
||||||
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
print(" vendor copy the resolved packages into ./vendor for offline builds")
|
||||||
|
|
@ -80,6 +85,9 @@ program Ludic {
|
||||||
print(" upgrade [version] reinstall from the docs site (the same script that installed it)")
|
print(" upgrade [version] reinstall from the docs site (the same script that installed it)")
|
||||||
print(" doctor check that the install is complete and usable")
|
print(" doctor check that the install is complete and usable")
|
||||||
print(" fmt [--check] [paths...] format Ludic source in place (--check: report, write nothing)")
|
print(" fmt [--check] [paths...] format Ludic source in place (--check: report, write nothing)")
|
||||||
|
print(" fmt --lint [--json] check the project's `lint` rules (--json: one JSON array on stdout)")
|
||||||
|
print(" fmt - [--stdin-name PATH] format stdin to stdout under the project's rules, as if it were PATH")
|
||||||
|
print(" fmt - --lint [--json] [--stdin-name PATH] lint stdin as PATH")
|
||||||
print(" lsp run the language server on stdio (what editors spawn)")
|
print(" lsp run the language server on stdio (what editors spawn)")
|
||||||
print(" ui-preview [--font DIR] preview a game's ludic.ui components for a studio, on stdio")
|
print(" ui-preview [--font DIR] preview a game's ludic.ui components for a studio, on stdio")
|
||||||
print(" help this message")
|
print(" help this message")
|
||||||
|
|
@ -123,6 +131,7 @@ program Ludic {
|
||||||
|
|
||||||
if (cmd == "add") { return cmd_pkg_add() }
|
if (cmd == "add") { return cmd_pkg_add() }
|
||||||
if (cmd == "get") { return cmd_pkg_get() }
|
if (cmd == "get") { return cmd_pkg_get() }
|
||||||
|
if (cmd == "remove") { return cmd_pkg_remove() }
|
||||||
if (cmd == "update") { return cmd_pkg_update() }
|
if (cmd == "update") { return cmd_pkg_update() }
|
||||||
if (cmd == "verify") { return cmd_pkg_verify() }
|
if (cmd == "verify") { return cmd_pkg_verify() }
|
||||||
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
if (cmd == "vendor") { return cmd_pkg_vendor() }
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
# pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/
|
# pkg.ludic — the Ludic package manager (issue #63), the `ludic add`/`get`/
|
||||||
# `update`/`verify`/`vendor` commands.
|
# `update`/`verify`/`vendor` commands (`remove` and `get --json`: pkg_remove.ludic, pkg_lock.ludic).
|
||||||
#
|
#
|
||||||
# It realises the v1 direction decided in the RFC:
|
# It realises the v1 direction decided in the RFC:
|
||||||
#
|
#
|
||||||
|
|
@ -484,7 +484,7 @@ function write_lock(sels: []Manifest) -> bool {
|
||||||
function do_install(root: Manifest) -> int {
|
function do_install(root: Manifest) -> int {
|
||||||
let sels = resolve(root)
|
let sels = resolve(root)
|
||||||
if len(sels) == 0 {
|
if len(sels) == 0 {
|
||||||
print("no dependencies to resolve")
|
pkg_say("no dependencies to resolve")
|
||||||
write_lock(sels)
|
write_lock(sels)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
@ -505,11 +505,11 @@ function do_install(root: Manifest) -> int {
|
||||||
if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 }
|
if slen(h) == 0 { err(`ludic: failed to snapshot {m.module}@{m.ver}\n`); return 1 }
|
||||||
m.hash = `sha256:{h}`
|
m.hash = `sha256:{h}`
|
||||||
link_module(m.module, h)
|
link_module(m.module, h)
|
||||||
print(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
|
pkg_say(` {m.module} {m.ver} ({m.kind}) sha256:{sslice(h, 0, 12)}…`)
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 }
|
if not write_lock(sels) { err("ludic: cannot write package.lock.ludic\n"); return 1 }
|
||||||
print(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
|
pkg_say(`resolved {string(len(sels))} package(s) — see package.lock.ludic; linked under ludic_modules/`)
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -596,12 +596,19 @@ function cmd_pkg_add() -> int {
|
||||||
return do_install(read_root_manifest())
|
return do_install(read_root_manifest())
|
||||||
}
|
}
|
||||||
|
|
||||||
# ludic get — resolve + fetch + link every dependency in package.ludic, write lock
|
# ludic get [--json] — resolve + fetch + link every dependency in package.ludic, write lock. --json
|
||||||
|
# says what changed in the lock as one JSON object on stdout (lock_diff_json), the rest on stderr.
|
||||||
function cmd_pkg_get() -> int {
|
function cmd_pkg_get() -> int {
|
||||||
let txt = read_file("package.ludic")
|
let txt = read_file("package.ludic")
|
||||||
if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
|
if txt == null { err("ludic: no package.ludic in the current directory (ludic add <module> to start one)\n"); return 1 }
|
||||||
print("resolving dependencies (MVS)…")
|
let json = pkg_has_flag("--json")
|
||||||
return do_install(parse_manifest(txt))
|
let before = read_lock_or_empty()
|
||||||
|
g_pkg_err = json
|
||||||
|
pkg_say("resolving dependencies (MVS)…")
|
||||||
|
let rc = do_install(parse_manifest(txt))
|
||||||
|
if rc != 0 or not json { return rc }
|
||||||
|
out(lock_diff_json(before, read_lock_or_empty()))
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# ludic update [module] — bump a dep (or all) to its latest published version, relock
|
# ludic update [module] — bump a dep (or all) to its latest published version, relock
|
||||||
|
|
|
||||||
116
tools/ludic-cli/pkg_lock.ludic
Normal file
116
tools/ludic-cli/pkg_lock.ludic
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# pkg_lock.ludic — the package manager's answers for editors and tools: `ludic get --json` (what a
|
||||||
|
# get changed in package.lock.ludic, as one JSON object on stdout) and `ludic remove <module>`, the
|
||||||
|
# inverse of `ludic add`. Neither touches the network: the diff is of the lock before and after, and
|
||||||
|
# a removal re-reads the dependency graph from the store's own copies of each package.ludic.
|
||||||
|
|
||||||
|
# the human lines of add / get / update: on stdout, or on stderr under --json
|
||||||
|
var g_pkg_err: bool = false
|
||||||
|
function pkg_say(line: pointer) -> void {
|
||||||
|
if g_pkg_err { err(line + nl()) } else { print(line) }
|
||||||
|
}
|
||||||
|
function pkg_has_flag(flag: pointer) -> bool {
|
||||||
|
var ai = 2
|
||||||
|
while ai < arg_count() {
|
||||||
|
if arg(ai) == flag { return true }
|
||||||
|
ai += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- JSON out --------------------------------------------------------------------
|
||||||
|
|
||||||
|
function pkg_hex(d: int) -> pointer {
|
||||||
|
let b = bytes(2)
|
||||||
|
b[0] = '0' + d
|
||||||
|
if d > 9 { b[0] = 'a' + d - 10 }
|
||||||
|
b[1] = 0
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
function pkg_jesc(c: int) -> pointer {
|
||||||
|
if c == '"' { return "\\\"" }
|
||||||
|
if c == 92 { return "\\\\" }
|
||||||
|
if c == '\n' { return "\\n" }
|
||||||
|
if c == '\t' { return "\\t" }
|
||||||
|
if c == '\r' { return "\\r" }
|
||||||
|
return "\\u00" + pkg_hex(c >> 4) + pkg_hex(c & 15)
|
||||||
|
}
|
||||||
|
# `s` as a JSON string, quotes included
|
||||||
|
function pkg_jq(s: pointer) -> pointer {
|
||||||
|
var o = "\""
|
||||||
|
let n = slen(s)
|
||||||
|
var start = 0
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let c = s[i] & 255
|
||||||
|
if c == '"' or c == 92 or c < 32 {
|
||||||
|
o = o + sslice(s, start, i) + pkg_jesc(c)
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return o + sslice(s, start, n) + "\""
|
||||||
|
}
|
||||||
|
# a lock line as the lock records it
|
||||||
|
function lock_entry_json(m: Manifest) -> pointer {
|
||||||
|
var o = `{{"name": {pkg_jq(m.module)}, "version": {pkg_jq(m.ver)}, "hash": {pkg_jq(m.hash)}, "kind": {pkg_jq(m.kind)}, "provides": [`
|
||||||
|
var p = 0
|
||||||
|
while p < len(m.provides) {
|
||||||
|
if p > 0 { o = o + ", " }
|
||||||
|
o = o + pkg_jq(m.provides[p])
|
||||||
|
p += 1
|
||||||
|
}
|
||||||
|
return o + "]}"
|
||||||
|
}
|
||||||
|
function lock_index(ms: []Manifest, module: pointer) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(ms) {
|
||||||
|
if ms[i].module == module { return i }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
function json_list(items: []pointer) -> pointer {
|
||||||
|
if len(items) == 0 { return "[]" }
|
||||||
|
var o = "["
|
||||||
|
var i = 0
|
||||||
|
while i < len(items) {
|
||||||
|
if i > 0 { o = o + "," }
|
||||||
|
o = o + nl() + " " + items[i]
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return o + nl() + " ]"
|
||||||
|
}
|
||||||
|
# what changed between two locks: {"added", "removed", "changed", "unchanged"}. A module whose version
|
||||||
|
# or content hash moved is changed; `from` / `to` are its versions, the hashes beside them.
|
||||||
|
function lock_diff_json(before: []Manifest, after: []Manifest) -> pointer {
|
||||||
|
let added = new []pointer
|
||||||
|
let removed = new []pointer
|
||||||
|
let changed = new []pointer
|
||||||
|
var same = 0
|
||||||
|
var i = 0
|
||||||
|
while i < len(after) {
|
||||||
|
let m = after[i]
|
||||||
|
let at = lock_index(before, m.module)
|
||||||
|
if at < 0 { push(added, lock_entry_json(m)) }
|
||||||
|
else {
|
||||||
|
let o = before[at]
|
||||||
|
if o.ver == m.ver and o.hash == m.hash { same += 1 }
|
||||||
|
else { push(changed, `{{"name": {pkg_jq(m.module)}, "from": {pkg_jq(o.ver)}, "to": {pkg_jq(m.ver)}, "from_hash": {pkg_jq(o.hash)}, "to_hash": {pkg_jq(m.hash)}}}`) }
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
i = 0
|
||||||
|
while i < len(before) {
|
||||||
|
if lock_index(after, before[i].module) < 0 { push(removed, lock_entry_json(before[i])) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
var o = "{" + nl() + ` "added": {json_list(added)},` + nl()
|
||||||
|
o = o + ` "removed": {json_list(removed)},` + nl()
|
||||||
|
o = o + ` "changed": {json_list(changed)},` + nl()
|
||||||
|
return o + ` "unchanged": {string(same)}` + nl() + "}" + nl()
|
||||||
|
}
|
||||||
|
function read_lock_or_empty() -> []Manifest {
|
||||||
|
let t = read_file("package.lock.ludic")
|
||||||
|
if t == null { return new []Manifest }
|
||||||
|
return parse_lock(t)
|
||||||
|
}
|
||||||
134
tools/ludic-cli/pkg_remove.ludic
Normal file
134
tools/ludic-cli/pkg_remove.ludic
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
# pkg_remove.ludic — `ludic remove <module>`, the inverse of `ludic add`: the `require` line leaves
|
||||||
|
# package.ludic, and the lock keeps exactly what the remaining requires still reach. The graph is
|
||||||
|
# read from the store's copies of each locked package.ludic, so nothing is fetched; what the removal
|
||||||
|
# leaves unreachable leaves the lock and its ludic_modules/ link (the link `add` made, never the store
|
||||||
|
# entry it points at, which other projects share). Source still importing a removed package is a
|
||||||
|
# warning: the caller may be about to delete it.
|
||||||
|
|
||||||
|
# package.ludic's text with every `require "<module>" ...` line dropped
|
||||||
|
function drop_require(txt: pointer, module: pointer) -> pointer {
|
||||||
|
var out = ""
|
||||||
|
let n = slen(txt)
|
||||||
|
var i = 0
|
||||||
|
while i < n {
|
||||||
|
let line = line_at(txt, i)
|
||||||
|
i = i + slen(line) + 1
|
||||||
|
let ts = tok_line(line)
|
||||||
|
if not (len(ts) >= 2 and ts[0] == "require" and ts[1] == module) { out = out + line + nl() }
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
function requires(m: Manifest, module: pointer) -> bool {
|
||||||
|
var i = 0
|
||||||
|
while i < len(m.deps) {
|
||||||
|
if m.deps[i].module == module { return true }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var g_graph_unknown: pointer = "" # a locked package the store has no copy of, when there is one
|
||||||
|
|
||||||
|
# the locked entries the root's requires still reach, through the store's package.ludic files
|
||||||
|
function lock_reachable(root: Manifest, locked: []Manifest) -> []Manifest {
|
||||||
|
let seen = new []pointer
|
||||||
|
let todo = new []pointer
|
||||||
|
var d = 0
|
||||||
|
while d < len(root.deps) {
|
||||||
|
push(todo, root.deps[d].module)
|
||||||
|
d += 1
|
||||||
|
}
|
||||||
|
while len(todo) > 0 {
|
||||||
|
let m = List.pop(todo)
|
||||||
|
let at = lock_index(locked, m)
|
||||||
|
if find_mod(seen, m) < 0 and at >= 0 {
|
||||||
|
push(seen, m)
|
||||||
|
let dir = `{store_root()}{strip_prefix(locked[at].hash, "sha256:")}`
|
||||||
|
if not shq(`test -d "{dir}"`) { g_graph_unknown = m }
|
||||||
|
else {
|
||||||
|
let mt = read_file(`{dir}/package.ludic`)
|
||||||
|
if mt != null {
|
||||||
|
let dm = parse_manifest(mt)
|
||||||
|
var k = 0
|
||||||
|
while k < len(dm.deps) {
|
||||||
|
push(todo, dm.deps[k].module)
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let keep = new []Manifest
|
||||||
|
var i = 0
|
||||||
|
while i < len(locked) {
|
||||||
|
if find_mod(seen, locked[i].module) >= 0 { push(keep, locked[i]) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return keep
|
||||||
|
}
|
||||||
|
|
||||||
|
# take down the ludic_modules/ view `add` linked, and the directories it leaves empty
|
||||||
|
function unlink_module(module: pointer) -> void {
|
||||||
|
let link = `ludic_modules/{module}`
|
||||||
|
if shq(`test -L "{link}"`) {
|
||||||
|
shell(`rm -f "{link}"; d=$(dirname "{link}"); while [ "$d" != ludic_modules ] && [ "$d" != . ] && rmdir "$d" 2>/dev/null; do d=$(dirname "$d"); done; rmdir ludic_modules 2>/dev/null`)
|
||||||
|
} else if shq(`test -e "{link}"`) {
|
||||||
|
err(`ludic remove: warning: {link} is not a link 'ludic add' made; left as it is\n`)
|
||||||
|
}
|
||||||
|
if shq(`test -e "vendor/{module}"`) { err(`ludic remove: vendor/{module} is still there (run 'ludic vendor' again)\n`) }
|
||||||
|
}
|
||||||
|
|
||||||
|
# the project's own files that still import `module`
|
||||||
|
function warn_importers(module: pointer) -> void {
|
||||||
|
let hits = split_lines(capture(`grep -rlF --include='*.ludic' --exclude-dir=ludic_modules --exclude-dir=vendor --exclude-dir=build --exclude-dir=.git -e 'import "{module}/' -e 'import "{module}"' . | sed 's|^\\./||' | LC_ALL=C sort`))
|
||||||
|
if len(hits) == 0 { return }
|
||||||
|
err(`ludic remove: warning: {module} is still imported by:\n`)
|
||||||
|
var i = 0
|
||||||
|
while i < len(hits) {
|
||||||
|
err(` {hits[i]}\n`)
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ludic remove <module> — drop a dependency: the require, what only it brought into the lock, the links
|
||||||
|
function cmd_pkg_remove() -> int {
|
||||||
|
if arg_count() < 3 { err("usage: ludic remove <module>\n"); return 1 }
|
||||||
|
let module = split_spec(arg(2))[0]
|
||||||
|
let txt = read_file("package.ludic")
|
||||||
|
if txt == null { err("ludic remove: no package.ludic in the current directory\n"); return 1 }
|
||||||
|
let locked = read_lock_or_empty()
|
||||||
|
if not requires(parse_manifest(txt), module) {
|
||||||
|
var why = ""
|
||||||
|
if lock_index(locked, module) >= 0 { why = " (the lock has it because another package requires it)" }
|
||||||
|
err(`ludic remove: package.ludic does not require {module}{why}\n`)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if not write_file("package.ludic", drop_require(txt, module)) { err("ludic remove: cannot write package.ludic\n"); return 1 }
|
||||||
|
print(`ludic remove: {module} is no longer required`)
|
||||||
|
if file_exists("package.lock.ludic") {
|
||||||
|
var keep = lock_reachable(read_root_manifest(), locked)
|
||||||
|
if g_graph_unknown != "" {
|
||||||
|
err(`ludic remove: the store has no copy of {g_graph_unknown}, so only {module} leaves the lock; 'ludic get' settles the rest\n`)
|
||||||
|
keep = new []Manifest
|
||||||
|
var j = 0
|
||||||
|
while j < len(locked) {
|
||||||
|
if not (locked[j].module == module) { push(keep, locked[j]) }
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if not write_lock(keep) { err("ludic remove: cannot write package.lock.ludic\n"); return 1 }
|
||||||
|
var i = 0
|
||||||
|
while i < len(locked) {
|
||||||
|
let m = locked[i]
|
||||||
|
if lock_index(keep, m.module) < 0 {
|
||||||
|
unlink_module(m.module)
|
||||||
|
print(` removed {m.module} {m.ver}`)
|
||||||
|
if not (m.module == module) { warn_importers(m.module) }
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if lock_index(keep, module) >= 0 { print(` {module} stays locked: another package still requires it`) }
|
||||||
|
}
|
||||||
|
warn_importers(module)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,13 @@ function pt_write(path: pointer, body: pointer) -> void {
|
||||||
write_file(path, body)
|
write_file(path, body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# a file's text, "" when it is not there
|
||||||
|
function pt_read(path: pointer) -> pointer {
|
||||||
|
let t = read_file(path)
|
||||||
|
if t == null { return "" }
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
# commit the current tree of `dir` and tag it `tag`
|
# commit the current tree of `dir` and tag it `tag`
|
||||||
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
|
function pt_commit_tag(dir: pointer, tag: pointer) -> void {
|
||||||
shell(`git -C {dir} add -A`)
|
shell(`git -C {dir} add -A`)
|
||||||
|
|
@ -170,6 +177,38 @@ function cmd_test_pkg() -> int {
|
||||||
ok("ludic vendor copies the resolved packages into ./vendor")
|
ok("ludic vendor copies the resolved packages into ./vendor")
|
||||||
} else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
|
} else { bad2("ludic vendor failed", capture_line(`tail -1 {work}/vendor.out`)) }
|
||||||
|
|
||||||
|
# ---- for editors (R9): get --json says what changed in the lock; remove undoes add ----
|
||||||
|
let proj5 = `{work}/proj5`
|
||||||
|
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl())
|
||||||
|
pt_write(`{proj5}/src/app.ludic`, "program App {\n import \"example.test/greeter/greet.ludic\"\n}\n")
|
||||||
|
shell(`( cd {proj5} && {envp} {root}/bin/ludic get --json > {work}/gj.json 2> {work}/gj.err )`)
|
||||||
|
let gj = pt_read(`{work}/gj.json`)
|
||||||
|
if s_contains(gj, `"name": "example.test/greeter", "version": "1.0.0"`) and s_contains(gj, `"name": "example.test/util", "version": "1.0.0"`) and s_contains(gj, `"unchanged": 0`) and shq(`python3 -m json.tool {work}/gj.json > /dev/null`) and not s_contains(gj, "resolving") {
|
||||||
|
ok("ludic get --json: a fresh lock is two added packages, one JSON object on stdout")
|
||||||
|
} else { bad2("ludic get --json (fresh)", gj) }
|
||||||
|
pt_write(`{proj5}/package.ludic`, `package "app"` + nl() + `version "0.0.0"` + nl() + `require "example.test/greeter" "1.0.0"` + nl() + `require "example.test/util" "1.2.0"` + nl())
|
||||||
|
let gj2 = capture(`( cd {proj5} && {envp} {root}/bin/ludic get --json 2>/dev/null )`)
|
||||||
|
if s_contains(gj2, `"added": []`) and s_contains(gj2, `"name": "example.test/util", "from": "1.0.0", "to": "1.2.0"`) and s_contains(gj2, `"unchanged": 1`) {
|
||||||
|
ok("ludic get --json: a raised minimum is a change from one version to the other")
|
||||||
|
} else { bad2("ludic get --json (changed)", gj2) }
|
||||||
|
if not shq(`( cd {proj5} && {root}/bin/ludic remove example.test/nothere > {work}/rm0.out 2>&1 )`) and shq(`grep -q 'does not require' {work}/rm0.out`) {
|
||||||
|
ok("ludic remove refuses a module package.ludic does not require")
|
||||||
|
} else { bad2("ludic remove of an unrequired module", capture_line(`cat {work}/rm0.out`)) }
|
||||||
|
let store_n = capture_line(`ls {store} | wc -l`)
|
||||||
|
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/util > {work}/rm1.out 2>&1 )`)
|
||||||
|
let lk1 = pt_read(`{proj5}/package.lock.ludic`)
|
||||||
|
if not s_contains(pt_read(`{proj5}/package.ludic`), "example.test/util") and s_contains(lk1, "example.test/util") and file_exists(`{proj5}/ludic_modules/example.test/util`) {
|
||||||
|
ok("ludic remove keeps what another package still requires locked and linked")
|
||||||
|
} else { bad2("ludic remove dropped a package still required", capture_line(`cat {work}/rm1.out`)) }
|
||||||
|
shell(`( cd {proj5} && LUDIC_STORE={store} {root}/bin/ludic remove example.test/greeter > {work}/rm2.out 2>&1 )`)
|
||||||
|
let lk2 = pt_read(`{proj5}/package.lock.ludic`)
|
||||||
|
if not s_contains(lk2, "example.test/") and not file_exists(`{proj5}/ludic_modules`) and capture_line(`ls {store} | wc -l`) == store_n {
|
||||||
|
ok("ludic remove drops the require, what only it locked and its links, and leaves the shared store alone")
|
||||||
|
} else { bad2("ludic remove of the last require", capture_line(`cat {work}/rm2.out`)) }
|
||||||
|
if shq(`grep -q 'still imported by' {work}/rm2.out`) and shq(`grep -q 'src/app.ludic' {work}/rm2.out`) {
|
||||||
|
ok("ludic remove warns about source still importing what it removed")
|
||||||
|
} else { bad("ludic remove said nothing about src/app.ludic importing the package") }
|
||||||
|
|
||||||
# ---- package-declarable engine-system + namespace (issue #62) -------------
|
# ---- package-declarable engine-system + namespace (issue #62) -------------
|
||||||
# A source package registers a compile-time engine system (@EngineSystem) and a
|
# A source package registers a compile-time engine system (@EngineSystem) and a
|
||||||
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it
|
# Foo.* namespace (@Namespace) with no compiler edit; a consumer game imports it
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
@ -561,7 +571,8 @@ function strip_ext(p: pointer) -> pointer {
|
||||||
# ---- ludic fmt / lsp --------------------------------------------------------
|
# ---- ludic fmt / lsp --------------------------------------------------------
|
||||||
|
|
||||||
# ludic fmt [--check] [paths...] — format the project's source in place (src/ and
|
# ludic fmt [--check] [paths...] — format the project's source in place (src/ and
|
||||||
# tests/ by default), or the paths named. --check writes nothing and exits
|
# tests/ by default), or the paths named. `--lint [--json]` checks the project's style rules and
|
||||||
|
# `-` formats stdin to stdout, both from the project's root (fmt_in_project). --check writes nothing and exits
|
||||||
# non-zero if anything is unformatted, which is what a pre-commit hook or CI
|
# non-zero if anything is unformatted, which is what a pre-commit hook or CI
|
||||||
# wants.
|
# wants.
|
||||||
#
|
#
|
||||||
|
|
@ -572,18 +583,35 @@ function strip_ext(p: pointer) -> pointer {
|
||||||
function cmd_fmt() -> int {
|
function cmd_fmt() -> int {
|
||||||
var mode = "-w"
|
var mode = "-w"
|
||||||
var args = ""
|
var args = ""
|
||||||
|
var lint = false
|
||||||
|
var json = false
|
||||||
|
var stdin = false
|
||||||
|
var name = ""
|
||||||
var ai = 2
|
var ai = 2
|
||||||
while ai < arg_count() {
|
while ai < arg_count() {
|
||||||
let a = arg(ai)
|
let a = arg(ai)
|
||||||
if a == "--check" { mode = "--check" }
|
if a == "--check" { mode = "--check" }
|
||||||
|
else if a == "--lint" { lint = true }
|
||||||
|
else if a == "--json" { json = true }
|
||||||
|
else if a == "-" { stdin = true }
|
||||||
|
else if a == "--stdin-name" and ai + 1 < arg_count() {
|
||||||
|
name = arg(ai + 1)
|
||||||
|
ai += 1
|
||||||
|
}
|
||||||
else if a[0] == '-' {
|
else if a[0] == '-' {
|
||||||
err(`ludic fmt: unknown option {a}\n`)
|
err(`ludic fmt: unknown option {a}\n`)
|
||||||
err(" usage: ludic fmt [--check] [paths...]\n")
|
err(" usage: ludic fmt [--check] [paths...] | ludic fmt --lint [--json] | ludic fmt - [--stdin-name PATH] [--lint [--json]]\n")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
else { args = `{args} {a}` }
|
else { args = `{args} {a}` }
|
||||||
ai += 1
|
ai += 1
|
||||||
}
|
}
|
||||||
|
if json and not lint { err("ludic fmt: --json goes with --lint\n"); return 1 }
|
||||||
|
if name != "" and not stdin { err("ludic fmt: --stdin-name goes with - (the buffer on stdin)\n"); return 1 }
|
||||||
|
if lint or stdin {
|
||||||
|
if args != "" { err("ludic fmt: --lint walks the project's `lint paths`, and - reads stdin; neither takes paths\n"); return 1 }
|
||||||
|
return fmt_in_project(lint, json, stdin, name)
|
||||||
|
}
|
||||||
if args == "" {
|
if args == "" {
|
||||||
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
|
let found = capture_line("find src tests -name '*.ludic' 2>/dev/null | sort")
|
||||||
if found == "" {
|
if found == "" {
|
||||||
|
|
@ -595,6 +623,21 @@ function cmd_fmt() -> int {
|
||||||
return sh(`{tool("ludic-fmt")} {mode}{args}`)
|
return sh(`{tool("ludic-fmt")} {mode}{args}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# `ludic fmt --lint` and `ludic fmt -`: ludic-fmt run from the project's root - the nearest directory
|
||||||
|
# upwards holding a package.ludic, from the --stdin-name's directory or else this one - so its `lint`
|
||||||
|
# lines, paths and baseline apply, with the buffer standing for its path relative to that root.
|
||||||
|
function fmt_in_project(lint: bool, json: bool, stdin: bool, name: pointer) -> int {
|
||||||
|
var flags = ""
|
||||||
|
if stdin { flags = ` - --stdin-name {sh_single(name)} --stdin-rel "$rel"` }
|
||||||
|
if lint { flags = flags + " --lint" }
|
||||||
|
if json { flags = flags + " --json" }
|
||||||
|
var s = `F={sh_single(tool("ludic-fmt"))}; case "$F" in /*) ;; */*) F="$PWD/$F" ;; esac; n={sh_single(name)}; `
|
||||||
|
s = s + `if [ -n "$n" ] && a=$(cd -- "$(dirname -- "$n")" 2>/dev/null && pwd -P); then b=$(basename -- "$n"); else a=$(pwd -P); b=; fi; `
|
||||||
|
s = s + `r=$a; while [ "$r" != / ] && [ ! -f "$r/package.ludic" ]; do r=$(dirname -- "$r"); done; rel=; `
|
||||||
|
s = s + `if [ -f "$r/package.ludic" ]; then if [ -n "$b" ]; then rel=${{a#"$r"}}; rel=${{rel#/}}; rel=${{rel:+$rel/}}$b; fi; cd -- "$r" || exit 2; fi; `
|
||||||
|
return sh(s + `exec "$F"{flags}`)
|
||||||
|
}
|
||||||
|
|
||||||
# ludic lsp — the language server on stdio. Editors are configured to run this,
|
# ludic lsp — the language server on stdio. Editors are configured to run this,
|
||||||
# so the server's location is the CLI's problem rather than the user's.
|
# so the server's location is the CLI's problem rather than the user's.
|
||||||
function cmd_lsp() -> int {
|
function cmd_lsp() -> int {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@
|
||||||
# What an editor of a program's data needs, from the compiler's own view: every record with its
|
# What an editor of a program's data needs, from the compiler's own view: every record with its
|
||||||
# fields, types, defaults, docs and attributes (@Ref, @Range, ...), every registry with its entries
|
# fields, types, defaults, docs and attributes (@Ref, @Range, ...), every registry with its entries
|
||||||
# in their final order and where each is written, every const, and the functions a `fn` value can
|
# in their final order and where each is written, every const, and the functions a `fn` value can
|
||||||
# name. The compiler writes it (`ludicc --emit-schema`, selfhost/backend/emit_schema.ludic) once the
|
# name, the text keys, and the code map (modules, actions, events, ports, handlers, scenes, fn refs).
|
||||||
|
# The compiler writes it (`ludicc --emit-schema`, selfhost/backend/emit_schema.ludic) once the
|
||||||
# program has type-checked.
|
# program has type-checked.
|
||||||
#
|
#
|
||||||
# ludic schema [file] the JSON on stdout
|
# ludic schema [file] the JSON on stdout
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
|
|
||||||
# the built-in commands a hook can wrap (every user command that does work)
|
# the built-in commands a hook can wrap (every user command that does work)
|
||||||
function is_hookable(cmd: pointer) -> bool {
|
function is_hookable(cmd: pointer) -> bool {
|
||||||
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
return (cmd == "build") or (cmd == "run") or (cmd == "test") or (cmd == "deps") or (cmd == "schema") or (cmd == "migrate") or (cmd == "bundle") or (cmd == "pack") or (cmd == "clean") or (cmd == "fmt") or (cmd == "get") or (cmd == "add") or (cmd == "remove") or (cmd == "update") or (cmd == "verify") or (cmd == "vendor") or (cmd == "assets") or (cmd == "build-lib")
|
||||||
}
|
}
|
||||||
|
|
||||||
# the directory this `ludic` binary lives in, with no trailing slash
|
# the directory this `ludic` binary lives in, with no trailing slash
|
||||||
|
|
@ -32,6 +32,17 @@ function script_env(event: pointer, phase: pointer) -> pointer {
|
||||||
return env
|
return env
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# a command whose stdout is for a program (`--json`) or whose stdin is a buffer (`ludic fmt -`): its
|
||||||
|
# hooks read nothing and say everything on stderr, so neither is spoilt
|
||||||
|
function machine_output() -> bool {
|
||||||
|
var ai = 2
|
||||||
|
while ai < arg_count() {
|
||||||
|
if arg(ai) == "--json" or arg(ai) == "-" { return true }
|
||||||
|
ai += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
# run every `hook <phase> <event>` in order; the first failure's exit code, or 0
|
# run every `hook <phase> <event>` in order; the first failure's exit code, or 0
|
||||||
function run_hooks(phase: pointer, event: pointer) -> int {
|
function run_hooks(phase: pointer, event: pointer) -> int {
|
||||||
if not file_exists("package.ludic") { return 0 }
|
if not file_exists("package.ludic") { return 0 }
|
||||||
|
|
@ -40,8 +51,14 @@ function run_hooks(phase: pointer, event: pointer) -> int {
|
||||||
while i + 2 < len(m.hooks) {
|
while i + 2 < len(m.hooks) {
|
||||||
if m.hooks[i] == phase and m.hooks[i + 1] == event {
|
if m.hooks[i] == phase and m.hooks[i + 1] == event {
|
||||||
let cmd = m.hooks[i + 2]
|
let cmd = m.hooks[i + 2]
|
||||||
say(`> {phase} {event}: {cmd}`)
|
var rc = 0
|
||||||
let rc = sh(`{script_env(event, phase)}{cmd}`)
|
if machine_output() {
|
||||||
|
err(`> {phase} {event}: {cmd}\n`)
|
||||||
|
rc = sh(`{script_env(event, phase)}( {cmd} ) < /dev/null 1>&2`)
|
||||||
|
} else {
|
||||||
|
say(`> {phase} {event}: {cmd}`)
|
||||||
|
rc = sh(`{script_env(event, phase)}{cmd}`)
|
||||||
|
}
|
||||||
if rc != 0 {
|
if rc != 0 {
|
||||||
err(`ludic: the {phase} {event} hook failed (exit {string(rc)}): {cmd}\n`)
|
err(`ludic: the {phase} {event} hook failed (exit {string(rc)}): {cmd}\n`)
|
||||||
return rc
|
return rc
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
@ -101,6 +102,8 @@ function selfhost_frags() -> []pointer {
|
||||||
push(f, "selfhost/backend/emit_schema_ui.ludic")
|
push(f, "selfhost/backend/emit_schema_ui.ludic")
|
||||||
push(f, "selfhost/backend/emit_schema_hash.ludic")
|
push(f, "selfhost/backend/emit_schema_hash.ludic")
|
||||||
push(f, "selfhost/backend/emit_schema_lang.ludic")
|
push(f, "selfhost/backend/emit_schema_lang.ludic")
|
||||||
|
push(f, "selfhost/backend/emit_schema_code.ludic")
|
||||||
|
push(f, "selfhost/backend/emit_schema_code_game.ludic")
|
||||||
push(f, "selfhost/check/check_state.ludic")
|
push(f, "selfhost/check/check_state.ludic")
|
||||||
push(f, "selfhost/check/check_index.ludic")
|
push(f, "selfhost/check/check_index.ludic")
|
||||||
push(f, "selfhost/check/check_types.ludic")
|
push(f, "selfhost/check/check_types.ludic")
|
||||||
|
|
|
||||||
|
|
@ -99,6 +99,52 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
# `ludic fmt` for editors (R9): run from a directory under the project, `--lint --json` and `-` find
|
||||||
|
# the nearest package.ludic upwards and judge a buffer as the file --stdin-name names, relative to it
|
||||||
|
function fmt_editor_cases() -> void {
|
||||||
|
let lbl = "ludic fmt --lint --json and ludic fmt - from inside a project"
|
||||||
|
let p = `{tmp_dir()}/fmt_ed`
|
||||||
|
let here = capture_line("pwd")
|
||||||
|
shell(`rm -rf {p} && mkdir -p {p}/src/deep`)
|
||||||
|
write_file(`{p}/package.ludic`, "package \"t\"\nlint one_statement\nlint paths \"src\"\n")
|
||||||
|
write_file(`{p}/src/deep/a.ludic`, "function f() -> int {\n let a = 1; return a\n}\n")
|
||||||
|
let js = `{p}/out.json`
|
||||||
|
if shq(`cd {p}/src/deep && {here}/bin/ludic fmt --lint --json > {js} 2>/dev/null`) { bad2(lbl, "--lint --json passed a file breaking one_statement"); return }
|
||||||
|
let said = capture(`cat {js}`)
|
||||||
|
if not s_contains(said, `"file": "src/deep/a.ludic", "line": 2, "col": 15`) { bad2(lbl, `--lint --json said {s_trim(said)}`); return }
|
||||||
|
let one = capture(`cd {p}/src/deep && {here}/bin/ludic fmt - --lint --json --stdin-name a.ludic < a.ludic 2>/dev/null`)
|
||||||
|
if not s_contains(one, `"file": "a.ludic", "line": 2`) { bad2(lbl, `a buffer linted as src/deep/a.ludic said {s_trim(one)}`); return }
|
||||||
|
let out = capture(`cd {p}/src/deep && printf 'function g() -> int{{return 2}}\n' | {here}/bin/ludic fmt - --stdin-name {p}/src/deep/b.ludic`)
|
||||||
|
if not (s_trim(out) == "function g() -> int { return 2 }") { bad2(lbl, `ludic fmt - wrote {s_trim(out)}`); return }
|
||||||
|
if shq(`printf '{{\n' | {here}/bin/ludic fmt - > /dev/null 2>&1`) { bad2(lbl, "ludic fmt - accepted an unclosed brace"); return }
|
||||||
|
if shq(`{here}/bin/ludic fmt --json > /dev/null 2>&1`) { bad2(lbl, "--json without --lint was accepted"); 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 +1273,8 @@ 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()
|
||||||
|
fmt_editor_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")
|
||||||
|
|
@ -1303,6 +1351,21 @@ function cmd_dev_test() -> int {
|
||||||
schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere")
|
schema_case("lang/i18n/i18n", "\"unused\": [\"map.close\"]", "--emit-schema: lang.unused - en.po's keys the program uses nowhere")
|
||||||
schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po")
|
schema_case("lang/i18n/i18n", "\"missing\": [\"hud.day\"], \"fuzzy\": [\"pause.close\"], \"extra\": [\"old.title\"]", "--emit-schema: lang.languages - another .po's missing, fuzzy and extra keys against en.po")
|
||||||
schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys")
|
schema_case("lang/i18n/i18n", "{\"english\": \"Close\", \"keys\": [\"map.close\", \"pause.close\"]}", "--emit-schema: lang.split - one English under several keys")
|
||||||
|
schema_case("actions/modules", "{\"state\": \"Bag\", \"action\": \"PickUp\", \"module\": \"bag\", \"at\": \"examples/actions/mods/bag/index.ludic:4:16\"}", "--emit-schema: code.reducers - a reducer's state, action, module and place")
|
||||||
|
schema_case("actions/modules", "{\"action\": \"PickUp\", \"module\": null, \"at\": \"examples/actions/modules.ludic:8:21\"}", "--emit-schema: code.dispatch - every dispatch site")
|
||||||
|
schema_case("modules/ports", "{\"name\": \"Clock\", \"module\": \"clock\", \"at\": \"examples/modules/station/clock/index.ludic:4:13\", \"bound\": true, \"members\": [{\"name\": \"now\", \"type\": \"fn()->int\", \"default\": null, \"required\": true}, {\"name\": \"day\", \"type\": \"fn()->int\", \"default\": \"fn first_day\", \"required\": false}]}", "--emit-schema: code.ports - a port's members, their fn types and defaults")
|
||||||
|
schema_case("modules/ports", "{\"port\": \"Clock\", \"member\": \"now\", \"fn\": \"game_hours\", \"value\": \"fn game_hours\", \"module\": \"app\", \"at\": \"examples/modules/station/app/index.ludic:3:14\"}", "--emit-schema: code.binds - a bind's member, the function it names and where")
|
||||||
|
schema_case("modules/ports", "{\"name\": \"clock\", \"package\": null, \"layer\": null, \"uses\": [\"units\"], \"uses_at\": \"examples/modules/station/clock/index.ludic:3\", \"friend\": null, \"files\": [\"examples/modules/station/clock/index.ludic\"]}", "--emit-schema: code.modules - a module's uses and files")
|
||||||
|
schema_case("modules/friend_of", "{\"name\": \"lab\", \"package\": null, \"layer\": null, \"uses\": null, \"uses_at\": null, \"friend\": {\"of\": [\"bank\"]}", "--emit-schema: code.modules - a friend module and whose names it sees")
|
||||||
|
schema_case("networking/net_demo", "{\"name\": \"Move\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:24:19\", \"cancellable\": false, \"net\": \"toserver\", \"emits\": [\"examples/networking/net_demo.ludic:32:10\"], \"listeners\": [{\"handler\": \"DoMove\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:25:21\"}]}", "--emit-schema: code.events - a remote event, its emit sites and @On listeners")
|
||||||
|
schema_case("networking/net_demo", "{\"name\": \"Ship\", \"module\": null, \"at\": \"examples/networking/net_demo.ludic:22:16\", \"owned\": true, \"properties\": [{\"name\": \"Pos\", \"sync\": true}]}", "--emit-schema: code.models - an @Owned model and its @Sync property")
|
||||||
|
schema_case("networking/net_roles", "\"phase\": \"Update\", \"hook\": null, \"target\": null, \"public\": false, \"net\": \"server\"", "--emit-schema: code.handlers - an @Server handler's phase and net role")
|
||||||
|
schema_case("lang/annotations", "\"queries\": {\"these\": [{\"property\": \"Transform\", \"filter\": \"scale > 0\"}, {\"property\": \"Velocity\", \"filter\": \"dx > 0 or dy > 0\"}], \"on\": \"Actor\"}", "--emit-schema: code.handlers - @Queries' terms, filters and model")
|
||||||
|
schema_case("lang/annotations", "\"phase\": null, \"hook\": \"OnSpawn\", \"target\": \"Actor\"", "--emit-schema: code.handlers - a lifecycle hook and the model it names")
|
||||||
|
schema_case("lang/scenes", "{\"name\": \"Title\", \"module\": null, \"at\": \"examples/lang/scenes.ludic:24:9\", \"start\": true, \"public\": false, \"shows\": null, \"lasts\": null, \"then\": null, \"loads\": null, \"enter\": true, \"exit\": true, \"layers\": [\"Main\"]}", "--emit-schema: code.scenes - the start scene, its blocks and layers")
|
||||||
|
schema_case("lang/scenes", "\"scene\": \"Play\", \"layer\": \"Hud\"", "--emit-schema: code.handlers - a scene's handler, its scene and layer")
|
||||||
|
schema_case("state/write_reach", "{\"fn\": \"log_note\", \"module\": null, \"at\": \"examples/state/write_reach.ludic:21:37\", \"slot_kind\": \"registry\", \"slot\": \"Kinds.note\", \"entry\": \"box\"}", "--emit-schema: code.fn_refs - a fn in a registry row, the field and row it fills")
|
||||||
|
schema_case("modules/ports", "\"slot_kind\": \"port_default\", \"slot\": \"Clock.day\"", "--emit-schema: code.fn_refs - a port member's default")
|
||||||
diag_json_case("rejected/i18n_keys/key_missing", 2, "the key pause.gone is not in examples/rejected/i18n_keys/lang/en.po", "a key literal en.po does not have is refused, at its line and column")
|
diag_json_case("rejected/i18n_keys/key_missing", 2, "the key pause.gone is not in examples/rejected/i18n_keys/lang/en.po", "a key literal en.po does not have is refused, at its line and column")
|
||||||
diag_json_case("rejected/i18n_keys/key_missing", 2, "is a plural key, and examples/rejected/i18n_keys/lang/en.po:7 gives pause.resume no msgid_plural", "kn\"...\" is a plural key: en.po's entry has a msgid_plural")
|
diag_json_case("rejected/i18n_keys/key_missing", 2, "is a plural key, and examples/rejected/i18n_keys/lang/en.po:7 gives pause.resume no msgid_plural", "kn\"...\" is a plural key: en.po's entry has a msgid_plural")
|
||||||
diag_json_case("rejected/i18n_keys/key_missing", 2, "trf: hud.day is given 2 value(s), and its English in examples/rejected/i18n_keys/lang/en.po has 1 hole(s)", "trf given more values than the key's English has holes: a warning")
|
diag_json_case("rejected/i18n_keys/key_missing", 2, "trf: hud.day is given 2 value(s), and its English in examples/rejected/i18n_keys/lang/en.po has 1 hole(s)", "trf given more values than the key's English has holes: a warning")
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,28 @@ function cmd_test_tools() -> int {
|
||||||
write_file(`{lp}/src/c.ludic`, "function h() -> string { return \"a; b\" }\n")
|
write_file(`{lp}/src/c.ludic`, "function h() -> string { return \"a; b\" }\n")
|
||||||
if shq(`cd {lp} && {lfmt} --lint > /dev/null`) { ok("a ; inside a string is not a statement") } else { bad("a ; inside a string is not a statement") }
|
if shq(`cd {lp} && {lfmt} --lint > /dev/null`) { ok("a ; inside a string is not a statement") } else { bad("a ; inside a string is not a statement") }
|
||||||
|
|
||||||
|
# --- for editors (R9): --lint --json, and a buffer on stdin -----------------
|
||||||
|
write_file(`{lp}/src/d.ludic`, "function d() -> int {\n let a = 1; let b = \"q\\\"\"; return a\n}\n")
|
||||||
|
let lj = `{lp}/lint.json`
|
||||||
|
if not shq(`cd {lp} && {lfmt} --lint --json > {lj} 2> {lp}/lint.err`) and shq(`python3 -m json.tool {lj} > /dev/null`) and shq(`grep -q '"file": "src/d.ludic", "line": 2, "col": 15, "rule": "one_statement"' {lj}`) and not shq(`grep -q 'lint:' {lj}`) {
|
||||||
|
ok("--lint --json: one JSON array on stdout, with the column, the summary on stderr")
|
||||||
|
} else { bad2("--lint --json", capture_line(`cat {lj}`)) }
|
||||||
|
shell(`cd {lp} && {lfmt} --init-baseline > /dev/null`)
|
||||||
|
write_file(`{lp}/src/d.ludic`, "function d() -> int { return 1 }\n")
|
||||||
|
let pinned = capture_line(`cat {lp}/lint-baseline.txt`)
|
||||||
|
shell(`cd {lp} && {lfmt} --lint --json > /dev/null 2>&1`)
|
||||||
|
check("--lint --json never lowers the baseline (plain --lint does)", capture_line(`cat {lp}/lint-baseline.txt`), pinned)
|
||||||
|
write_file(`{lp}/buf.txt`, "function g() -> int {\n let b = 2; return b\n}\n")
|
||||||
|
let sj = capture(`cd {lp} && {lfmt} - --lint --json --stdin-name src/new.ludic < buf.txt`)
|
||||||
|
if s_contains(sj, `"file": "src/new.ludic", "line": 2`) { ok("- --lint --json: a buffer judged as the file it stands for") } else { bad2("- --lint --json", sj) }
|
||||||
|
check("a buffer outside the lint paths breaks no rule", s_trim(capture(`cd {lp} && {lfmt} - --lint --json --stdin-name notes/new.ludic < buf.txt 2>/dev/null`)), "[]")
|
||||||
|
check("- formats stdin to stdout", capture_line(`printf 'function f() -> int{{return 1}}\n' | {fmt} -`), "function f() -> int { return 1 }")
|
||||||
|
let bad_rc = sh(`printf 'function f() -> int {{\n let s = (1]\n}}\n' | {fmt} - --stdin-name x.ludic > {tmp_dir()}/bad.out 2> {tmp_dir()}/bad.err`)
|
||||||
|
if bad_rc == 2 and shq(`grep -q '^x.ludic:2:13: error:' {tmp_dir()}/bad.err`) and shq(`test ! -s {tmp_dir()}/bad.out`) {
|
||||||
|
ok("- refuses a buffer with a bracket closed by the wrong one: exit 2, where on stderr, nothing on stdout")
|
||||||
|
} else { bad2("- refuses an unreadable buffer", capture_line(`cat {tmp_dir()}/bad.err`)) }
|
||||||
|
check("- refuses an unclosed string", string(sh(`printf 'let s = "abc\n' | {fmt} - > /dev/null 2>&1`)), "2")
|
||||||
|
|
||||||
# --- --check exit status, for CI and pre-commit hooks ----------------------
|
# --- --check exit status, for CI and pre-commit hooks ----------------------
|
||||||
let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
|
let chk1 = sh(`{fmt} --check -q {tmp_dir()}/m.ludic`)
|
||||||
check("--check exits 1 on unformatted input", string(chk1), "1")
|
check("--check exits 1 on unformatted input", string(chk1), "1")
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,16 @@
|
||||||
# ludic-fmt -w a.ludic rewrite in place
|
# ludic-fmt -w a.ludic rewrite in place
|
||||||
# ludic-fmt --check a.ludic exit 1 if unformatted
|
# ludic-fmt --check a.ludic exit 1 if unformatted
|
||||||
# ludic-fmt a.md format the ```ludic fences in a document
|
# ludic-fmt a.md format the ```ludic fences in a document
|
||||||
# cat a.ludic | ludic-fmt - filter mode (stdin -> stdout)
|
# cat a.ludic | ludic-fmt - filter mode (stdin -> stdout); a buffer that does not read as
|
||||||
|
# Ludic (an open string, an unclosed bracket) exits 2, why on stderr
|
||||||
|
# ludic-fmt --lint --json the project's lint, as one JSON array on stdout
|
||||||
|
# ludic-fmt - --lint --json --stdin-name src/a.ludic lint a buffer as that file
|
||||||
program LudicFmt {
|
program LudicFmt {
|
||||||
import "fmt_lint.ludic"
|
import "fmt_lint.ludic"
|
||||||
import "fmt_lint_rules.ludic"
|
import "fmt_lint_rules.ludic"
|
||||||
import "fmt_lint_run.ludic"
|
import "fmt_lint_run.ludic"
|
||||||
|
import "fmt_lint_json.ludic"
|
||||||
|
import "fmt_stdin.ludic"
|
||||||
# ---- token kinds (mirror ludic_syntax.h) ----
|
# ---- token kinds (mirror ludic_syntax.h) ----
|
||||||
const LT_EOF: int = 0
|
const LT_EOF: int = 0
|
||||||
const LT_NL: int = 1
|
const LT_NL: int = 1
|
||||||
|
|
@ -632,24 +637,43 @@ program LudicFmt {
|
||||||
var failed = false
|
var failed = false
|
||||||
var lint_only = false
|
var lint_only = false
|
||||||
var lint_init = false
|
var lint_init = false
|
||||||
|
var json = false
|
||||||
|
var stdin_name = ""
|
||||||
|
var stdin_rel = ""
|
||||||
let files = new []pointer
|
let files = new []pointer
|
||||||
var ai = 1
|
var ai = 1
|
||||||
while ai < arg_count() {
|
while ai < arg_count() {
|
||||||
let a = arg(ai)
|
let a = arg(ai)
|
||||||
|
if (a == "--json") { json = true; ai += 1; continue }
|
||||||
|
if (a == "--stdin-name") and ai + 1 < arg_count() { stdin_name = string(arg(ai + 1)); ai += 2; continue }
|
||||||
|
if (a == "--stdin-rel") and ai + 1 < arg_count() { stdin_rel = string(arg(ai + 1)); ai += 2; continue }
|
||||||
if (a == "-w") or (a == "--write") { write = true }
|
if (a == "-w") or (a == "--write") { write = true }
|
||||||
else { if (a == "--check") or (a == "-l") { check = true }
|
else { if (a == "--check") or (a == "-l") { check = true }
|
||||||
else { if (a == "-q") or (a == "--quiet") { quiet = true }
|
else { if (a == "-q") or (a == "--quiet") { quiet = true }
|
||||||
else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } }
|
else { if (a == "--indent") { ai += 1; if ai < arg_count() { indent = 0; let d = arg(ai); var di = 0; while d[di] != 0 { indent = indent * 10 + (d[di] - 48); di += 1 } } }
|
||||||
else { if (a == "--lint") { lint_only = true }
|
else { if (a == "--lint") { lint_only = true }
|
||||||
else { if (a == "--init-baseline") { lint_only = true; lint_init = true }
|
else { if (a == "--init-baseline") { lint_only = true; lint_init = true }
|
||||||
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source; --check also checks package.ludic's lint rules, --lint checks the project"); return }
|
else { if (a == "-h") or (a == "--help") { print("ludic-fmt — format Ludic source; --check also checks package.ludic's lint rules, --lint checks the project (--json: as a JSON array); - formats stdin (--stdin-name PATH: as that file)"); return }
|
||||||
else { push(files, a) } } } } } } }
|
else { push(files, a) } } } } } } }
|
||||||
ai += 1
|
ai += 1
|
||||||
}
|
}
|
||||||
if indent < 1 or indent > 8 { indent = 2 }
|
if indent < 1 or indent > 8 { indent = 2 }
|
||||||
|
let stdin_dash = len(files) == 1 and (files[0] == "-")
|
||||||
|
if json and not lint_only { lt_err("ludic-fmt: --json goes with --lint\n"); exit(2) }
|
||||||
|
lt_json = json
|
||||||
|
lt_partial = json and not lint_init # a report for an editor never rewrites the baseline
|
||||||
|
# - --lint: a buffer on stdin, judged as the file --stdin-name names
|
||||||
|
if lint_only and stdin_dash {
|
||||||
|
if lint_init { lt_err("ludic-fmt: --init-baseline reads the project, not stdin\n"); exit(2) }
|
||||||
|
if not fs_lint_stdin(stdin_name, stdin_rel) { exit(1) }
|
||||||
|
return
|
||||||
|
}
|
||||||
# --lint: the project's rules over its paths, against the baseline (L10)
|
# --lint: the project's rules over its paths, against the baseline (L10)
|
||||||
if lint_only {
|
if lint_only {
|
||||||
if not lint_config() { print("ludic-fmt --lint: package.ludic states no `lint` rules"); exit(2) }
|
if not lint_config() {
|
||||||
|
if json { lt_err("ludic-fmt --lint: package.ludic states no `lint` rules\n") } else { print("ludic-fmt --lint: package.ludic states no `lint` rules") }
|
||||||
|
exit(2)
|
||||||
|
}
|
||||||
let all = new []string
|
let all = new []string
|
||||||
var li = 0
|
var li = 0
|
||||||
while li < len(lt_paths) {
|
while li < len(lt_paths) {
|
||||||
|
|
@ -668,9 +692,19 @@ program LudicFmt {
|
||||||
let linting = check and lint_config()
|
let linting = check and lint_config()
|
||||||
|
|
||||||
# stdin filter mode
|
# stdin filter mode
|
||||||
if len(files) == 0 or (len(files) == 1 and (files[0] == "-")) {
|
if len(files) == 0 or stdin_dash {
|
||||||
let text = slurp_stdin()
|
let text = slurp_stdin()
|
||||||
let out = format_source(text, false, indent)
|
let md = fs_ends(stdin_name, ".md") or fs_ends(stdin_name, ".markdown")
|
||||||
|
if not md {
|
||||||
|
lex(text)
|
||||||
|
if not fs_parses() {
|
||||||
|
var shown = stdin_name
|
||||||
|
if len(shown) == 0 { shown = "<stdin>" }
|
||||||
|
lt_err(`{shown}:{fs_err_line}:{fs_err_col}: error: {fs_err_msg}\n`)
|
||||||
|
exit(2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let out = format_source(text, md, indent)
|
||||||
file_write(file_stdout(), out, cstr_len(out))
|
file_write(file_stdout(), out, cstr_len(out))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ var lr_file: []string = new []string
|
||||||
var lr_rule: []string = new []string
|
var lr_rule: []string = new []string
|
||||||
var lr_line: []int = new []int
|
var lr_line: []int = new []int
|
||||||
var lr_msg: []string = new []string
|
var lr_msg: []string = new []string
|
||||||
|
var lr_col: []int = new []int # 1-based byte column, 0 when the rule has none (a file's length)
|
||||||
|
|
||||||
function lt_atoi(s: string) -> int {
|
function lt_atoi(s: string) -> int {
|
||||||
var n = 0
|
var n = 0
|
||||||
|
|
@ -103,9 +104,11 @@ function lint_config() -> bool {
|
||||||
}
|
}
|
||||||
return any
|
return any
|
||||||
}
|
}
|
||||||
function lt_report(path: string, rule: string, line: int, msg: string) -> void {
|
function lt_report(path: string, rule: string, line: int, msg: string) -> void { lt_report_at(path, rule, line, 0, msg) }
|
||||||
|
function lt_report_at(path: string, rule: string, line: int, col: int, msg: string) -> void {
|
||||||
push(lr_file, path)
|
push(lr_file, path)
|
||||||
push(lr_rule, rule)
|
push(lr_rule, rule)
|
||||||
push(lr_line, line)
|
push(lr_line, line)
|
||||||
|
push(lr_col, col)
|
||||||
push(lr_msg, msg)
|
push(lr_msg, msg)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
91
tools/ludic-tools/fmt_lint_json.ludic
Normal file
91
tools/ludic-tools/fmt_lint_json.ludic
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
# fmt_lint_json.ludic — `ludic-fmt --lint --json`: the violations --lint would print, as ONE JSON
|
||||||
|
# array on stdout for an editor, `[{"file", "line", "col", "rule", "message"}]` ordered by file, then
|
||||||
|
# line, then column; "col" only where the rule has one. Everything else --lint says goes to stderr.
|
||||||
|
var lt_json: bool = false
|
||||||
|
var lj_idx: []int = new []int # the reports --lint would have printed, by index
|
||||||
|
var lt_show_from: string = "" # a buffer on stdin is judged as this path (its baseline, its scope)
|
||||||
|
var lt_show_as: string = "" # and reported under this name (--stdin-name, or "-")
|
||||||
|
|
||||||
|
function lt_err(s: string) -> void { file_write(file_stderr(), s, len(s)) }
|
||||||
|
function lt_shown(path: string) -> string {
|
||||||
|
if len(lt_show_from) > 0 and path == lt_show_from { return lt_show_as }
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
function lt_strcmp(a: string, b: string) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(a) and i < len(b) {
|
||||||
|
if a[i] != b[i] { return a[i] - b[i] }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return len(a) - len(b)
|
||||||
|
}
|
||||||
|
# report x before report y?
|
||||||
|
function lj_before(x: int, y: int) -> bool {
|
||||||
|
let c = lt_strcmp(lr_file[x], lr_file[y])
|
||||||
|
if c != 0 { return c < 0 }
|
||||||
|
if lr_line[x] != lr_line[y] { return lr_line[x] < lr_line[y] }
|
||||||
|
if lr_col[x] != lr_col[y] { return lr_col[x] < lr_col[y] }
|
||||||
|
return lt_strcmp(lr_rule[x], lr_rule[y]) < 0
|
||||||
|
}
|
||||||
|
function lj_sort() -> void {
|
||||||
|
var i = 1
|
||||||
|
while i < len(lj_idx) {
|
||||||
|
let x = lj_idx[i]
|
||||||
|
var j = i - 1
|
||||||
|
while j >= 0 and lj_before(x, lj_idx[j]) {
|
||||||
|
lj_idx[j + 1] = lj_idx[j]
|
||||||
|
j -= 1
|
||||||
|
}
|
||||||
|
lj_idx[j + 1] = x
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function lj_hex(d: int) -> int {
|
||||||
|
if d < 10 { return '0' + d }
|
||||||
|
return 'a' + d - 10
|
||||||
|
}
|
||||||
|
# `s` as a JSON string, quotes included
|
||||||
|
function lt_jq(b: Buf, s: string) -> void {
|
||||||
|
buf_putc(b, '"')
|
||||||
|
var i = 0
|
||||||
|
while i < len(s) {
|
||||||
|
let c = s[i] & 255
|
||||||
|
if c == '"' { buf_puts(b, "\\\"") }
|
||||||
|
else if c == '\\' { buf_puts(b, "\\\\") }
|
||||||
|
else if c == '\n' { buf_puts(b, "\\n") }
|
||||||
|
else if c == '\t' { buf_puts(b, "\\t") }
|
||||||
|
else if c == '\r' { buf_puts(b, "\\r") }
|
||||||
|
else if c < 32 {
|
||||||
|
buf_puts(b, "\\u00")
|
||||||
|
buf_putc(b, lj_hex(c >> 4))
|
||||||
|
buf_putc(b, lj_hex(c & 15))
|
||||||
|
}
|
||||||
|
else { buf_putc(b, c) }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
buf_putc(b, '"')
|
||||||
|
}
|
||||||
|
function lt_json_out() -> void {
|
||||||
|
lj_sort()
|
||||||
|
let b = buf_new()
|
||||||
|
buf_putc(b, '[')
|
||||||
|
var k = 0
|
||||||
|
while k < len(lj_idx) {
|
||||||
|
let i = lj_idx[k]
|
||||||
|
if k > 0 { buf_putc(b, ',') }
|
||||||
|
buf_puts(b, "\n {\"file\": ")
|
||||||
|
lt_jq(b, lt_shown(lr_file[i]))
|
||||||
|
buf_puts(b, `, "line": {lr_line[i]}`)
|
||||||
|
if lr_col[i] > 0 { buf_puts(b, `, "col": {lr_col[i]}`) }
|
||||||
|
buf_puts(b, ", \"rule\": ")
|
||||||
|
lt_jq(b, lr_rule[i])
|
||||||
|
buf_puts(b, ", \"message\": ")
|
||||||
|
lt_jq(b, lr_msg[i])
|
||||||
|
buf_putc(b, '}')
|
||||||
|
k += 1
|
||||||
|
}
|
||||||
|
if k > 0 { buf_putc(b, '\n') }
|
||||||
|
buf_puts(b, "]\n")
|
||||||
|
let s = buf_str(b)
|
||||||
|
file_write(file_stdout(), s, cstr_len(s))
|
||||||
|
}
|
||||||
|
|
@ -26,8 +26,9 @@ function lint_comments(path: string, ls: []string) -> void {
|
||||||
} else {
|
} else {
|
||||||
if run > 0 {
|
if run > 0 {
|
||||||
let header = not code_seen and start == 0
|
let header = not code_seen and start == 0
|
||||||
if header and lt_max_header > 0 and run > lt_max_header { lt_report(path, "max_header_lines", start + 1, `the opening comment is {run} lines (the limit is {lt_max_header})`) }
|
let col = lt_indent(ls[start]) + 1
|
||||||
if not header and lt_max_comment > 0 and run > lt_max_comment { lt_report(path, "max_comment_lines", start + 1, `a comment of {run} lines (the limit is {lt_max_comment}): say why in a line or two; the story goes in the commit or docs/`) }
|
if header and lt_max_header > 0 and run > lt_max_header { lt_report_at(path, "max_header_lines", start + 1, col, `the opening comment is {run} lines (the limit is {lt_max_header})`) }
|
||||||
|
if not header and lt_max_comment > 0 and run > lt_max_comment { lt_report_at(path, "max_comment_lines", start + 1, col, `a comment of {run} lines (the limit is {lt_max_comment}): say why in a line or two; the story goes in the commit or docs/`) }
|
||||||
run = 0
|
run = 0
|
||||||
}
|
}
|
||||||
if not blank { code_seen = true }
|
if not blank { code_seen = true }
|
||||||
|
|
@ -35,6 +36,14 @@ function lint_comments(path: string, ls: []string) -> void {
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
# the spaces and tabs a line opens with
|
||||||
|
function lt_indent(line: string) -> int {
|
||||||
|
var i = 0
|
||||||
|
while i < len(line) and (line[i] == ' ' or line[i] == 9) { i += 1 }
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
# a token's 1-based byte column on its line
|
||||||
|
function lt_tok_col(i: int) -> int { return tk_start[i] - linestart[tk_line[i]] + 1 }
|
||||||
# 0 a blank line, 1 a comment line, 2 code
|
# 0 a blank line, 1 a comment line, 2 code
|
||||||
function lt_words_raw(line: string) -> int {
|
function lt_words_raw(line: string) -> int {
|
||||||
var i = 0
|
var i = 0
|
||||||
|
|
@ -48,7 +57,7 @@ function lint_statements(path: string) -> void {
|
||||||
while i < ntok() {
|
while i < ntok() {
|
||||||
if tk_kind[i] == LT_OP and tok_len(i) == 1 and src[tk_start[i]] == ';' {
|
if tk_kind[i] == LT_OP and tok_len(i) == 1 and src[tk_start[i]] == ';' {
|
||||||
let k = tk_kind[i + 1]
|
let k = tk_kind[i + 1]
|
||||||
if k != LT_NL and k != LT_COMMENT and k != LT_EOF { lt_report(path, "one_statement", tk_line[i] + 1, "two statements on one line") }
|
if k != LT_NL and k != LT_COMMENT and k != LT_EOF { lt_report_at(path, "one_statement", tk_line[i] + 1, lt_tok_col(i + 1), "two statements on one line") }
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +69,7 @@ function lint_functions(path: string) -> void {
|
||||||
let end = lt_fn_end(i)
|
let end = lt_fn_end(i)
|
||||||
if end >= 0 {
|
if end >= 0 {
|
||||||
let n = tk_line[end] - tk_line[i] + 1
|
let n = tk_line[end] - tk_line[i] + 1
|
||||||
if n > lt_max_fn { lt_report(path, "max_function_lines", tk_line[i] + 1, `{lt_fn_name(i)} is {n} lines (the limit is {lt_max_fn})`) }
|
if n > lt_max_fn { lt_report_at(path, "max_function_lines", tk_line[i] + 1, lt_tok_col(i), `{lt_fn_name(i)} is {n} lines (the limit is {lt_max_fn})`) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
|
|
||||||
|
|
@ -104,14 +104,23 @@ function lint_judge(init: bool) -> bool {
|
||||||
file_close(f)
|
file_close(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
print(` lint: {total} violation(s) in the baseline, {len(pf)} file/rule pair(s)`)
|
let summary = ` lint: {total} violation(s) in the baseline, {len(pf)} file/rule pair(s)`
|
||||||
|
if lt_json {
|
||||||
|
lt_err(`{summary}\n`)
|
||||||
|
lt_json_out()
|
||||||
|
} else { print(summary) }
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
function lt_print_pair(path: string, rule: string, now: int, allow: int) -> void {
|
function lt_print_pair(path: string, rule: string, now: int, allow: int) -> void {
|
||||||
var i = 0
|
var i = 0
|
||||||
while i < len(lr_file) {
|
while i < len(lr_file) {
|
||||||
if lr_file[i] == path and lr_rule[i] == rule { print(`{path}:{lr_line[i]}: {rule}: {lr_msg[i]}`) }
|
if lr_file[i] == path and lr_rule[i] == rule {
|
||||||
|
if lt_json { push(lj_idx, i) } else { print(`{lt_shown(path)}:{lr_line[i]}: {rule}: {lr_msg[i]}`) }
|
||||||
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
if allow > 0 { print(` ({path} may have {allow} of {rule}, and has {now})`) }
|
if allow > 0 {
|
||||||
|
let note = ` ({lt_shown(path)} may have {allow} of {rule}, and has {now})`
|
||||||
|
if lt_json { lt_err(`{note}\n`) } else { print(note) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
110
tools/ludic-tools/fmt_stdin.ludic
Normal file
110
tools/ludic-tools/fmt_stdin.ludic
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
# fmt_stdin.ludic — `ludic-fmt -`: a buffer on stdin, formatted to stdout, for an editor. The
|
||||||
|
# formatter works on tokens and never fails, so a buffer it cannot read as Ludic - a string, a
|
||||||
|
# template or a key literal left open, a bracket never closed or closed by the wrong one - would come
|
||||||
|
# back re-indented around the fault; such a buffer is refused instead, with where on stderr.
|
||||||
|
var fs_err_line: int = 0
|
||||||
|
var fs_err_col: int = 0
|
||||||
|
var fs_err_msg: string = ""
|
||||||
|
|
||||||
|
function fs_fail(i: int, msg: string) -> bool {
|
||||||
|
fs_err_line = tk_line[i] + 1
|
||||||
|
fs_err_col = tk_start[i] - linestart[tk_line[i]] + 1
|
||||||
|
fs_err_msg = msg
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
# a string-ish token that reached the end of its line (or the text) without its closing quote
|
||||||
|
function fs_open_quote(i: int) -> bool {
|
||||||
|
let t = tok_text(i)
|
||||||
|
let n = tok_len(i)
|
||||||
|
var q = t[0]
|
||||||
|
var body = 1
|
||||||
|
if q == 'k' {
|
||||||
|
q = '"'
|
||||||
|
body = 2
|
||||||
|
if n > 1 and t[1] == 'n' { body = 3 }
|
||||||
|
}
|
||||||
|
if n <= body { return true }
|
||||||
|
if t[n - 1] != q { return true }
|
||||||
|
return t[n - 2] == '\\' and not fs_escaped_end(t, n)
|
||||||
|
}
|
||||||
|
# is the backslash before the last byte itself escaped (`"a\\"` is closed, `"a\"` is not)?
|
||||||
|
function fs_escaped_end(t: pointer, n: int) -> bool {
|
||||||
|
var k = n - 2
|
||||||
|
var run = 0
|
||||||
|
while k >= 1 and t[k] == '\\' {
|
||||||
|
run += 1
|
||||||
|
k -= 1
|
||||||
|
}
|
||||||
|
return run % 2 == 0
|
||||||
|
}
|
||||||
|
function fs_closer(c: int) -> int {
|
||||||
|
if c == '(' { return ')' }
|
||||||
|
if c == '[' { return ']' }
|
||||||
|
return '}'
|
||||||
|
}
|
||||||
|
# lex(text) first; true when the tokens read as Ludic, else fs_err_* say where they stop
|
||||||
|
function fs_parses() -> bool {
|
||||||
|
let open = new []int
|
||||||
|
var i = 0
|
||||||
|
while i < ntok() {
|
||||||
|
let k = tk_kind[i]
|
||||||
|
if k == LT_STR and fs_open_quote(i) { return fs_fail(i, "this string is never closed") }
|
||||||
|
if k == LT_OP and tok_len(i) == 1 {
|
||||||
|
let c = src[tk_start[i]]
|
||||||
|
if c == '(' or c == '[' or c == '{' { push(open, i) }
|
||||||
|
if c == ')' or c == ']' or c == '}' {
|
||||||
|
if len(open) == 0 { return fs_fail(i, `a {string(tok_text(i))} with nothing open to close`) }
|
||||||
|
let o = open[len(open) - 1]
|
||||||
|
let want = fs_closer(src[tk_start[o]])
|
||||||
|
if c != want {
|
||||||
|
let at = `{tk_line[o] + 1}:{tk_start[o] - linestart[tk_line[o]] + 1}`
|
||||||
|
return fs_fail(i, `a {string(tok_text(i))} where the {string(tok_text(o))} opened at {at} wants its close`)
|
||||||
|
}
|
||||||
|
let closed = List.pop(open)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if len(open) > 0 {
|
||||||
|
let o = open[len(open) - 1]
|
||||||
|
return fs_fail(o, `this {string(tok_text(o))} is never closed`)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
function fs_ends(s: string, suf: string) -> bool {
|
||||||
|
if len(suf) > len(s) { return false }
|
||||||
|
return s[len(s) - len(suf) .. len(s)] == suf
|
||||||
|
}
|
||||||
|
function fs_strip_dot(p: string) -> string {
|
||||||
|
var s = p
|
||||||
|
while len(s) >= 2 and s[0] == '.' and s[1] == '/' { s = s[2 .. len(s)] }
|
||||||
|
while len(s) > 1 and s[len(s) - 1] == '/' { s = s[0 .. len(s) - 1] }
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
# is a buffer standing for `rel` (relative to the project) one --lint would walk?
|
||||||
|
function fs_in_scope(rel: string) -> bool {
|
||||||
|
if not fs_ends(rel, ".ludic") { return false }
|
||||||
|
var i = 0
|
||||||
|
while i < len(lt_paths) {
|
||||||
|
let p = fs_strip_dot(lt_paths[i])
|
||||||
|
if p == "." or rel == p { return true }
|
||||||
|
if len(rel) > len(p) and rel[0 .. len(p)] == p and rel[len(p)] == '/' { return true }
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
# `ludic-fmt - --lint [--json]`: the buffer judged as the file it stands for, against the baseline,
|
||||||
|
# which is never rewritten from a buffer. A buffer outside the project's lint paths breaks no rule.
|
||||||
|
function fs_lint_stdin(name: string, rel_in: string) -> bool {
|
||||||
|
var rel = fs_strip_dot(rel_in)
|
||||||
|
if len(rel) == 0 { rel = fs_strip_dot(name) }
|
||||||
|
var shown = name
|
||||||
|
if len(shown) == 0 { shown = "-" }
|
||||||
|
if len(rel) == 0 { rel = "-" }
|
||||||
|
lt_show_from = rel
|
||||||
|
lt_show_as = shown
|
||||||
|
let text = string(slurp_stdin())
|
||||||
|
if lint_config() and (rel == "-" or fs_in_scope(rel)) { lint_file(rel, text) }
|
||||||
|
lt_partial = true
|
||||||
|
return lint_judge(false)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue