@8bitscript/compiler 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/index.mjs +91 -0
- package/package.json +29 -0
- package/src/ast/index.mjs +90 -0
- package/src/checker/index.mjs +294 -0
- package/src/diagnostics/index.mjs +179 -0
- package/src/fold/facts.mjs +209 -0
- package/src/fold/index.mjs +412 -0
- package/src/intellisense/index.mjs +558 -0
- package/src/ir/index.mjs +1422 -0
- package/src/lexer/index.mjs +383 -0
- package/src/linker/hazards.mjs +121 -0
- package/src/linker/index.mjs +1075 -0
- package/src/parser/index.mjs +795 -0
- package/src/resolver/index.mjs +534 -0
- package/src/templates/index.mjs +278 -0
- package/src/types/index.mjs +116 -0
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// The diagnostic record every part of the toolchain speaks.
|
|
2
|
+
//
|
|
3
|
+
// One shape, produced in one place, consumed by both `8bs check` and the
|
|
4
|
+
// language server. That is the whole point: the error you see in the editor and
|
|
5
|
+
// the error CI fails on are the same object, not two implementations that drift.
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} Diagnostic
|
|
9
|
+
* @property {string} code Stable identifier, e.g. "8BS1021".
|
|
10
|
+
* @property {string} message Human-readable text, no trailing period.
|
|
11
|
+
* @property {string} file Path or URI the diagnostic belongs to.
|
|
12
|
+
* @property {number} start Zero-based offset into the source text.
|
|
13
|
+
* @property {number} length Length of the offending span, in characters.
|
|
14
|
+
* @property {'error'|'warning'} severity
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Diagnostic codes.
|
|
19
|
+
*
|
|
20
|
+
* 1000s are lexical and syntax problems — things findable without knowing what
|
|
21
|
+
* any name refers to. 2000s are resolution and type errors (resolution is
|
|
22
|
+
* implemented; type errors wait on a binder). 3000s are target limits: the
|
|
23
|
+
* construct is valid but the compiler cannot lower it yet, it is not available
|
|
24
|
+
* on the requested target, or the target refuses it as a hardware hazard.
|
|
25
|
+
*/
|
|
26
|
+
export const Codes = {
|
|
27
|
+
UNTERMINATED_STRING: '8BS1002',
|
|
28
|
+
UNEXPECTED_CHARACTER: '8BS1003',
|
|
29
|
+
UNMATCHED_BRACKET: '8BS1004',
|
|
30
|
+
UNCLOSED_BRACKET: '8BS1005',
|
|
31
|
+
UNTERMINATED_BLOCK_COMMENT: '8BS1006',
|
|
32
|
+
UNTERMINATED_ASM_BLOCK: '8BS1007',
|
|
33
|
+
INVALID_NUMBER: '8BS1008',
|
|
34
|
+
// A decimal literal (`0.5`) used anywhere other than as the first argument
|
|
35
|
+
// to a duration clock call, `#frames(...)` — the only place the language
|
|
36
|
+
// has any float-shaped syntax. foldDurations() (packages/compiler/src/fold) consumes and
|
|
37
|
+
// removes every valid one before check() ever runs, so any that survive
|
|
38
|
+
// to be walked here were misplaced.
|
|
39
|
+
MISPLACED_DECIMAL_LITERAL: '8BS1009',
|
|
40
|
+
SYNTAX_ERROR: '8BS1101',
|
|
41
|
+
VALUE_OUT_OF_RANGE: '8BS1021',
|
|
42
|
+
// `#frames(...)`'s argument shape is wrong — not one integer-or-decimal
|
|
43
|
+
// literal followed by the bare unit word it is measured in (see
|
|
44
|
+
// foldDurations()). The unit is required: `#frames(30)` is this.
|
|
45
|
+
INVALID_DURATION_ARGUMENT: '8BS1022',
|
|
46
|
+
// A `#frames(...)` call folded to zero frames at the project's configured
|
|
47
|
+
// frameRate — always a bug, not a benign rounding nicety: it would wrap a
|
|
48
|
+
// countdown like `let ticks: utinyint = #frames(...); ... ticks = ticks - 1;`
|
|
49
|
+
// straight through 0 instead of ticking.
|
|
50
|
+
ZERO_DURATION: '8BS1023',
|
|
51
|
+
// A `#frames(...)` call didn't fold to an exact frame count at the
|
|
52
|
+
// project's configured frameRate — reported so a rate change (e.g. 60 to
|
|
53
|
+
// 50) that silently nudges a duration's real-world length is never
|
|
54
|
+
// invisible.
|
|
55
|
+
INEXACT_DURATION: '8BS1024',
|
|
56
|
+
// `#frames(...)`'s second argument names a unit the fold doesn't know —
|
|
57
|
+
// the only one so far is `seconds` (see the fold pass's DURATION_UNITS).
|
|
58
|
+
// Reported at the identifier itself, not the whole call.
|
|
59
|
+
UNKNOWN_DURATION_UNIT: '8BS1025',
|
|
60
|
+
// A string literal (or the text of a template) holds a character outside
|
|
61
|
+
// the portable set — space, `0`-`9`, `A`-`Z`, and `! , - . : ?` — the
|
|
62
|
+
// characters every target's character set can show (the NES ships its own
|
|
63
|
+
// font with exactly these; the Commodore machines are switched to their
|
|
64
|
+
// upper-case set). Reported where the string becomes program data
|
|
65
|
+
// (packages/compiler/src/ir), never for an import specifier.
|
|
66
|
+
UNPORTABLE_CHARACTER: '8BS1026',
|
|
67
|
+
// A string literal longer than 255 bytes: strings are length-prefixed
|
|
68
|
+
// with one byte, and no screen this compiles for has that many cells in
|
|
69
|
+
// a row anyway.
|
|
70
|
+
STRING_TOO_LONG: '8BS1027',
|
|
71
|
+
// A template string (`\`TICK ${ticks}\``) somewhere other than the second
|
|
72
|
+
// argument of a namespace's `print(cell, ...)` — the one place the
|
|
73
|
+
// compiler expands one — or with the wrong shape around it.
|
|
74
|
+
MISPLACED_TEMPLATE: '8BS1028',
|
|
75
|
+
// A `${...}` field the compiler cannot lay out: its width was not given
|
|
76
|
+
// and could not be taken from the expression's type (an imported name, a
|
|
77
|
+
// call across modules), or the value is not something a number field can
|
|
78
|
+
// show (signed, or wider than 16 bits).
|
|
79
|
+
UNPRINTABLE_FIELD: '8BS1029',
|
|
80
|
+
// `#name` — the compile-time spelling — naming a function the compiler
|
|
81
|
+
// doesn't evaluate (`#frames` is the only one), or a compile-time function
|
|
82
|
+
// used without being called (`#frames` on its own).
|
|
83
|
+
UNKNOWN_COMPILE_TIME_FUNCTION: '8BS1030',
|
|
84
|
+
// Assignment (or `++`/`--`) to a `const`: a compile-time constant, inlined
|
|
85
|
+
// wherever it is read, has no storage on the target to assign to. The
|
|
86
|
+
// checker reports a module's own consts; the linker, an imported one.
|
|
87
|
+
ASSIGN_TO_CONST: '8BS1031',
|
|
88
|
+
// `a[7]` on an `array<T, 4>`: a literal index at or past the array's
|
|
89
|
+
// length. Only a literal (or a const) can be checked at compile time; a
|
|
90
|
+
// runtime index is the program's own responsibility, as on the machine.
|
|
91
|
+
INDEX_OUT_OF_RANGE: '8BS1032',
|
|
92
|
+
// `[1, 2, 3]` for an `array<T, 4>`: an array initialiser has exactly as
|
|
93
|
+
// many elements as the type says — the length is part of the type, and
|
|
94
|
+
// the data is laid out at compile time, so nothing can pad or truncate.
|
|
95
|
+
ARRAY_SIZE_MISMATCH: '8BS1033',
|
|
96
|
+
// Names say which side of the compile-time rule they are on: a `const`
|
|
97
|
+
// is UPPER_SNAKE (`OPTION_COUNT`, `BorderColor.BLUE`), a variable starts
|
|
98
|
+
// with a lower-case letter. So a reader knows `LIMIT` is resolved by
|
|
99
|
+
// 8bitscript and `limit` is storage on the machine, without looking up
|
|
100
|
+
// the declaration.
|
|
101
|
+
NAME_CASE: '8BS1034',
|
|
102
|
+
// A call with more arguments than the function has parameters, or fewer
|
|
103
|
+
// than the parameters without a default. A default (`border: utinyint =
|
|
104
|
+
// BorderColor.BLACK`) is a compile-time value 8bitscript fills in at the
|
|
105
|
+
// call, so every call the machine sees is complete.
|
|
106
|
+
WRONG_ARGUMENT_COUNT: '8BS1035',
|
|
107
|
+
// `#system(...)` called with arguments: it takes none (see the fold pass).
|
|
108
|
+
SYSTEM_TAKES_NO_ARGUMENTS: '8BS1036',
|
|
109
|
+
// `#fact(...)` of a key the sheet does not have, or with no key at all
|
|
110
|
+
// (see the fold pass and fold/facts.mjs for the keys).
|
|
111
|
+
UNKNOWN_FACT: '8BS1037',
|
|
112
|
+
// `#fact(...)` in a build that knows its machine but was handed no
|
|
113
|
+
// hardware facts: the fold will not guess a sheet for a real build.
|
|
114
|
+
NO_HARDWARE_FACTS: '8BS1038',
|
|
115
|
+
|
|
116
|
+
UNRESOLVED_PACKAGE: '8BS2001',
|
|
117
|
+
NOT_AN_8BS_PACKAGE: '8BS2002',
|
|
118
|
+
MISSING_PACKAGE_ENTRY: '8BS2003',
|
|
119
|
+
UNRESOLVED_RELATIVE_IMPORT: '8BS2004',
|
|
120
|
+
NO_SUCH_EXPORT: '8BS2005',
|
|
121
|
+
DUPLICATE_BINDING: '8BS2006',
|
|
122
|
+
UNRESOLVED_NAME: '8BS2007',
|
|
123
|
+
MISSING_NATIVE_SOURCE: '8BS2008',
|
|
124
|
+
// A declaration or import named after a builtin — `waitFrame`
|
|
125
|
+
// (packages/compiler/src/ir). Compile-time functions (`#frames`) need no
|
|
126
|
+
// reservation: their `#` spelling is a different token from any name. A user binding by that name would otherwise
|
|
127
|
+
// be silently reinterpreted as the builtin rather than getting a clear
|
|
128
|
+
// diagnostic.
|
|
129
|
+
RESERVED_BUILTIN_NAME: '8BS2009',
|
|
130
|
+
// The entry module must export exactly one thing — a function taking no
|
|
131
|
+
// parameters — and that is the program: what a 6502 target's synthesised
|
|
132
|
+
// C `main` calls and what the web host's worker calls. Zero exports, a
|
|
133
|
+
// second export, an exported global or namespace, or a parameterised entry
|
|
134
|
+
// are all this diagnostic. Other modules (packages, libraries) may export
|
|
135
|
+
// whatever they like.
|
|
136
|
+
ENTRY_EXPORTS: '8BS2010',
|
|
137
|
+
// A package subpath — `@scope/name/thing` — that the package's
|
|
138
|
+
// `"8bitscript".exports` map has no entry for (packages/compiler/src/
|
|
139
|
+
// resolver). The package itself is fine; the import asks it for something
|
|
140
|
+
// it does not offer, which is a different failure from a missing entry
|
|
141
|
+
// file (8BS2003) or a package that is not 8BitScript at all (8BS2002).
|
|
142
|
+
NO_SUCH_SUBPATH: '8BS2011',
|
|
143
|
+
|
|
144
|
+
NOT_COMPILABLE: '8BS3001',
|
|
145
|
+
NOT_ON_THIS_TARGET: '8BS3002',
|
|
146
|
+
// Two of a build's hardware tags each have their own version of a file,
|
|
147
|
+
// and nothing says which wins (see the resolver's chooseVariant).
|
|
148
|
+
AMBIGUOUS_VARIANT: '8BS3004',
|
|
149
|
+
// A write the requested target's own documentation says can damage the
|
|
150
|
+
// machine — the PET's "killer poke" ($E842 with bit 5 set) is the one
|
|
151
|
+
// entry (packages/compiler/src/linker/hazards.mjs). Reported by the
|
|
152
|
+
// linker, which alone knows the machine and has every const inlined; so
|
|
153
|
+
// it is a build-time diagnostic, not one `8bs check` or the editor show.
|
|
154
|
+
HARDWARE_HAZARD: '8BS3003',
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/** @returns {Diagnostic} */
|
|
158
|
+
export function diagnostic(code, message, file, start, length, severity = 'error') {
|
|
159
|
+
return { code, message, file, start, length, severity };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Convert an offset into 1-based line and column, for terminal output.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} text
|
|
166
|
+
* @param {number} offset
|
|
167
|
+
* @returns {{ line: number, column: number }}
|
|
168
|
+
*/
|
|
169
|
+
export function positionAt(text, offset) {
|
|
170
|
+
let line = 1;
|
|
171
|
+
let lastBreak = -1;
|
|
172
|
+
for (let i = 0; i < offset && i < text.length; i += 1) {
|
|
173
|
+
if (text[i] === '\n') {
|
|
174
|
+
line += 1;
|
|
175
|
+
lastBreak = i;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { line, column: offset - lastBreak };
|
|
179
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// The fact sheet's keys: what a build knows about the machine it is for.
|
|
2
|
+
//
|
|
3
|
+
// A fact is a number or a flag with one meaning on every machine, keyed the
|
|
4
|
+
// way the machine packages' hardware catalogs spell it (`video.columns`,
|
|
5
|
+
// `memory.banked` — see docs/packages.md, "the hardware catalog"). The
|
|
6
|
+
// compiler owns the *keys* and their types; the machine packages own the
|
|
7
|
+
// *values*: each catalog's top-level `facts` is the stock machine's sheet
|
|
8
|
+
// and each hardware value's `facts` is what choosing it changes, and the
|
|
9
|
+
// CLI merges them for a build (packages/cli/src/hardware.mjs,
|
|
10
|
+
// resolveHardware). `#fact(video.columns)` folds to the merged value, and
|
|
11
|
+
// `@8bitscript/system` gives every key a const (`Video.COLUMNS`) so a
|
|
12
|
+
// program never spells a key itself.
|
|
13
|
+
//
|
|
14
|
+
// Two rules the table enforces. A fact is never missing: every catalog
|
|
15
|
+
// declares every key that is `program: true` (the CLI's catalog test holds
|
|
16
|
+
// them to it), so a machine without hardware sprites says `video.sprites`
|
|
17
|
+
// 0 and a program's branch on it folds away rather than failing to
|
|
18
|
+
// compile. And a fact is the worst case that matters, not the brochure
|
|
19
|
+
// figure: `video.spritesPerLine` is the number that decides whether a
|
|
20
|
+
// scene works (8 on the NES, whose 64 per frame would mislead), and where
|
|
21
|
+
// a chip has a cycle budget instead of a count (the X16), the package
|
|
22
|
+
// states the sprite size the count assumes.
|
|
23
|
+
//
|
|
24
|
+
// `when` says when the fact is settled. `build`: fixed by the build — the
|
|
25
|
+
// machine and the hardware it was built for — so the const is the truth on
|
|
26
|
+
// every machine the binary runs on. `run`: hardware the machine may or
|
|
27
|
+
// may not have when the program runs (a REU, a mouse in a port, banked
|
|
28
|
+
// RAM), which one binary can detect and use; the const then means "this
|
|
29
|
+
// build may use it" — the hardware was chosen for the build, so the
|
|
30
|
+
// capability's detection code is compiled in — and whether it is really
|
|
31
|
+
// there is the capability's runtime answer. A build that never asked for
|
|
32
|
+
// the hardware carries no code for it. Which facts are `run` is the
|
|
33
|
+
// machine's business as much as the key's: the table marks what is
|
|
34
|
+
// detectable *somewhere*, and a package that cannot detect it on its
|
|
35
|
+
// machine says so in its notes.
|
|
36
|
+
//
|
|
37
|
+
// `program: false` keys are the CLI's, not a program's: `video.frameRate`
|
|
38
|
+
// is what a screenshot's frame count is timed against, and it is not on
|
|
39
|
+
// the sheet because the level machines (packages/backend-6502, FRAME_SYNC)
|
|
40
|
+
// detect NTSC or PAL at run time — one binary runs at both — so a
|
|
41
|
+
// compile-time refresh rate would be a fact that is sometimes wrong.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @typedef {object} Fact
|
|
45
|
+
* @property {'count'|'flag'} type a number (folds to an IntegerLiteral) or
|
|
46
|
+
* a yes/no (folds to a BooleanLiteral)
|
|
47
|
+
* @property {'build'|'run'} when see above
|
|
48
|
+
* @property {boolean} program on the program's sheet (`@8bitscript/system`)
|
|
49
|
+
* @property {string} doc one line, for hover and the editor's panel
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
const count = (when, doc, program = true) => ({ type: 'count', when, program, doc });
|
|
53
|
+
const flag = (when, doc, program = true) => ({ type: 'flag', when, program, doc });
|
|
54
|
+
|
|
55
|
+
/** Every fact key, in the order the sheet lists them. @type {Map<string, Fact>} */
|
|
56
|
+
export const FACTS = new Map([
|
|
57
|
+
// Video: the text grid and what the display is made of.
|
|
58
|
+
['video.columns', count('build', 'Cells across the text grid the portable text draws on.')],
|
|
59
|
+
['video.rows', count('build', 'Cells down the text grid.')],
|
|
60
|
+
['video.cellWidth', count('build', 'Pixels across one cell.')],
|
|
61
|
+
['video.cellHeight', count('build', 'Pixels down one cell.')],
|
|
62
|
+
['video.palette', count('build', 'Colours the display can show at once.')],
|
|
63
|
+
['video.cellColors', count('build', 'Colours one cell can hold in the text mode the grid uses.')],
|
|
64
|
+
['video.colorPerCell', flag('build', 'A program can set one cell\'s colour without changing its neighbours\'.')],
|
|
65
|
+
['video.glyphs', count('build', 'Characters a program can redefine at run time; 0 where the font is fixed.')],
|
|
66
|
+
['video.blockWidth', count('build', 'Pseudo-pixels across one cell from the fixed font\'s block glyphs; 0 where there are none.')],
|
|
67
|
+
['video.blockHeight', count('build', 'Pseudo-pixels down one cell from the block glyphs; 0 where there are none.')],
|
|
68
|
+
['video.bitmap', flag('build', 'A pixel-addressable mode exists.')],
|
|
69
|
+
['video.layers', count('build', 'Independent background layers.')],
|
|
70
|
+
['video.scroll', flag('build', 'Hardware fine scroll exists.')],
|
|
71
|
+
['video.sprites', count('build', 'Hardware sprites in total; 0 where moving objects are drawn in software.')],
|
|
72
|
+
['video.spritesPerLine', count('build', 'Hardware sprites one scanline can show — the number that decides whether a scene works.')],
|
|
73
|
+
['video.spriteWidth', count('build', 'Pixels across the largest hardware sprite.')],
|
|
74
|
+
['video.spriteHeight', count('build', 'Pixels down the largest hardware sprite.')],
|
|
75
|
+
['video.spriteColors', count('build', 'Colours one hardware sprite can hold, not counting transparent.')],
|
|
76
|
+
['video.frameRate', count('build', 'Display refreshes a second, for timing a screenshot; not on the sheet, the machine may be NTSC or PAL at run time.', false)],
|
|
77
|
+
// Audio: the chip's shape.
|
|
78
|
+
['audio.voices', count('build', 'Voices the sound hardware plays at once.')],
|
|
79
|
+
['audio.noise', flag('build', 'A noise voice exists.')],
|
|
80
|
+
['audio.envelope', flag('build', 'Hardware volume envelopes (ADSR or similar) exist.')],
|
|
81
|
+
['audio.filter', flag('build', 'A hardware filter exists.')],
|
|
82
|
+
['audio.pcm', flag('build', 'Sample playback exists.')],
|
|
83
|
+
['audio.volume', flag('build', 'Each voice has its own volume.')],
|
|
84
|
+
['audio.entropy', flag('build', 'A hardware random source exists.')],
|
|
85
|
+
// Input: the ports and what is plugged into them.
|
|
86
|
+
['input.keyboard', flag('build', 'A keyboard the program can read.')],
|
|
87
|
+
['input.joysticks', count('build', 'Joystick ports.')],
|
|
88
|
+
['input.pads', count('build', 'Console-style controller ports.')],
|
|
89
|
+
['input.mouse', flag('run', 'A mouse this build may use; whether one is plugged in is the capability\'s answer at run time.')],
|
|
90
|
+
['input.paddles', flag('run', 'Paddles this build may use; whether they are plugged in is the capability\'s answer at run time.')],
|
|
91
|
+
// Storage.
|
|
92
|
+
['storage.save', flag('build', 'Somewhere this build can persist bytes: disk, SD card, battery RAM.')],
|
|
93
|
+
['storage.kib', count('build', 'KiB that place holds — the medium\'s usable capacity, not its image size; 0 where there is nowhere to save. Where the route is a host directory or a card whose size is the owner\'s, it is the smallest real medium that route stands for, so a program that fits the fact fits the hardware. KiB rather than bytes because a disk does not fit in the 16 bits an 8-bit machine counts in — the same reason memory.bankedKib is KiB.')],
|
|
94
|
+
// Memory.
|
|
95
|
+
['memory.ram', count('build', 'Bytes of RAM the program is linked to use, code and data together.')],
|
|
96
|
+
['memory.banked', flag('run', 'RAM beyond the CPU\'s window this build may use (a REU, an MMU bank, VERA-style banks); whether it is there is the capability\'s answer at run time.')],
|
|
97
|
+
['memory.bankedKib', count('run', 'KiB of that banked RAM the build was fitted with.')],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/** The keys a program's sheet carries, in order. */
|
|
101
|
+
export const PROGRAM_FACTS = [...FACTS].filter(([, fact]) => fact.program).map(([key]) => key);
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The name `@8bitscript/system` gives a key: `video.spritesPerLine` is
|
|
105
|
+
* `Video.SPRITES_PER_LINE`. The namespace is the key's first word
|
|
106
|
+
* capitalised; the const is the rest in upper snake case.
|
|
107
|
+
*
|
|
108
|
+
* @param {string} key
|
|
109
|
+
* @returns {{ namespace: string, name: string }}
|
|
110
|
+
*/
|
|
111
|
+
export function factConstName(key) {
|
|
112
|
+
const [group, ...rest] = key.split('.');
|
|
113
|
+
const member = rest.join('.');
|
|
114
|
+
return {
|
|
115
|
+
namespace: group[0].toUpperCase() + group.slice(1),
|
|
116
|
+
name: member.replace(/([a-z])([A-Z])/g, '$1_$2').toUpperCase(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The value a fact has when nothing set it: 0 or false — the placeholder
|
|
122
|
+
* the fold uses with no machine in hand, and the value a catalog test
|
|
123
|
+
* compares against when it insists a key was declared on purpose.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} key
|
|
126
|
+
*/
|
|
127
|
+
export function factPlaceholder(key) {
|
|
128
|
+
return FACTS.get(key)?.type === 'flag' ? false : 0;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check a `requires` block against the table: what a program needs of the
|
|
133
|
+
* machine it is built for, before the machine is known.
|
|
134
|
+
*
|
|
135
|
+
* A count is a floor and a flag must be true — "at least this much RAM",
|
|
136
|
+
* "somewhere to save" — because that is the shape of every requirement a
|
|
137
|
+
* program actually has. A `when: 'run'` fact cannot be required: whether a
|
|
138
|
+
* mouse is plugged in is answered on the machine, not by the build, so a
|
|
139
|
+
* program that needs one asks the capability and says so itself.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} requires
|
|
142
|
+
* @returns {string[]} the problems, in words; empty when clean
|
|
143
|
+
*/
|
|
144
|
+
export function requiresProblems(requires) {
|
|
145
|
+
const problems = [];
|
|
146
|
+
for (const [key, need] of Object.entries(requires ?? {})) {
|
|
147
|
+
const fact = FACTS.get(key);
|
|
148
|
+
if (!fact) {
|
|
149
|
+
problems.push(`'${key}' is not a fact — the keys are ${PROGRAM_FACTS.join(', ')}`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!fact.program) {
|
|
153
|
+
problems.push(`'${key}' is not on a program's sheet, so a program cannot require it`);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (fact.when === 'run') {
|
|
157
|
+
problems.push(
|
|
158
|
+
`'${key}' is settled on the machine, not by the build, so it cannot be required — `
|
|
159
|
+
+ 'ask the capability at run time instead',
|
|
160
|
+
);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (fact.type === 'flag' && need !== true) {
|
|
164
|
+
problems.push(`'${key}' is a flag: require it with true, or leave it out — not ${JSON.stringify(need)}`);
|
|
165
|
+
}
|
|
166
|
+
if (fact.type === 'count' && !(Number.isInteger(need) && need > 0)) {
|
|
167
|
+
problems.push(`'${key}' is a count: require a whole number above zero, not ${JSON.stringify(need)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return problems;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* What a `requires` block asks for that a build's facts do not give.
|
|
175
|
+
*
|
|
176
|
+
* @param {object} requires already checked by requiresProblems
|
|
177
|
+
* @param {object} facts the merged sheet for one build
|
|
178
|
+
* @returns {{ key: string, need: number|boolean, have: number|boolean }[]} empty when the build is enough
|
|
179
|
+
*/
|
|
180
|
+
export function unmetRequirements(requires, facts) {
|
|
181
|
+
const unmet = [];
|
|
182
|
+
for (const [key, need] of Object.entries(requires ?? {})) {
|
|
183
|
+
const have = facts?.[key] ?? factPlaceholder(key);
|
|
184
|
+
const met = FACTS.get(key)?.type === 'flag' ? have === true : have >= need;
|
|
185
|
+
if (!met) unmet.push({ key, need, have });
|
|
186
|
+
}
|
|
187
|
+
return unmet;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Check one facts object against the table: every key known, every value
|
|
192
|
+
* of its key's type. Returns the problems, in words; empty when clean.
|
|
193
|
+
*
|
|
194
|
+
* @param {object} facts
|
|
195
|
+
* @returns {string[]}
|
|
196
|
+
*/
|
|
197
|
+
export function factProblems(facts) {
|
|
198
|
+
const problems = [];
|
|
199
|
+
for (const [key, value] of Object.entries(facts ?? {})) {
|
|
200
|
+
const fact = FACTS.get(key);
|
|
201
|
+
if (!fact) {
|
|
202
|
+
problems.push(`'${key}' is not a fact — the keys are ${[...FACTS.keys()].join(', ')}`);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (fact.type === 'flag' && typeof value !== 'boolean') problems.push(`'${key}' is a flag: true or false, not ${JSON.stringify(value)}`);
|
|
206
|
+
if (fact.type === 'count' && !(Number.isInteger(value) && value >= 0)) problems.push(`'${key}' is a count: a whole number, not ${JSON.stringify(value)}`);
|
|
207
|
+
}
|
|
208
|
+
return problems;
|
|
209
|
+
}
|