kumi-parser 0.0.32 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +41 -0
- data/CHANGELOG.md +64 -0
- data/CLAUDE.md +59 -120
- data/README.md +28 -6
- data/examples/parse_and_inspect.rb +34 -0
- data/kumi-parser.gemspec +3 -4
- data/lib/kumi/parser/grammar.rb +120 -0
- data/lib/kumi/parser/lexer.rb +232 -0
- data/lib/kumi/parser/parse_error.rb +52 -0
- data/lib/kumi/parser/parser.rb +692 -0
- data/lib/kumi/parser/source.rb +76 -0
- data/lib/kumi/parser/text_parser.rb +37 -27
- data/lib/kumi/parser/token.rb +10 -71
- data/lib/kumi/parser/version.rb +1 -1
- data/lib/kumi-parser.rb +9 -10
- metadata +16 -37
- data/examples/debug_text_parser.rb +0 -41
- data/examples/debug_transform_rule.rb +0 -26
- data/examples/text_parser_comprehensive_test.rb +0 -333
- data/examples/text_parser_test_with_comments.rb +0 -146
- data/lib/kumi/parser/base.rb +0 -51
- data/lib/kumi/parser/direct_parser.rb +0 -698
- data/lib/kumi/parser/error_extractor.rb +0 -89
- data/lib/kumi/parser/errors.rb +0 -40
- data/lib/kumi/parser/helpers.rb +0 -154
- data/lib/kumi/parser/smart_tokenizer.rb +0 -373
- data/lib/kumi/parser/syntax_validator.rb +0 -21
- data/lib/kumi/parser/text_parser/api.rb +0 -60
- data/lib/kumi/parser/token_constants.rb +0 -467
- data/lib/kumi/text_parser.rb +0 -40
- data/lib/kumi/text_schema.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b880347a083ba29083167538910e1d1453a1b7c9f334bd4a8ec86437878de8e3
|
|
4
|
+
data.tar.gz: 529fcc0f2bb7102ff8a79e3c8e69ef4cc3a613f13378f81d3d86feaf359d6cb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59a7da2e91de9ff04d804ad3eefe846dea9dac256083220d286ea4d29f509fdd456019f84683e9b147667da767ed090e0f5a0769777edcdeed6b9585e20b36a4
|
|
7
|
+
data.tar.gz: 8faaaf5d58767f4f5aa776be90033d0e65b9b6484c402434c24db305f0759ff874091445341f47b50a46e915552dcd9f4f2f63ec13caf94fdc9a12c58147e4d2
|
data/.rubocop.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
AllCops:
|
|
2
|
+
TargetRubyVersion: 3.1
|
|
3
|
+
NewCops: disable
|
|
4
|
+
SuggestExtensions: false
|
|
5
|
+
|
|
6
|
+
# A recursive-descent parser and a single-pass lexer naturally have a handful
|
|
7
|
+
# of longer dispatch methods; the default 10-line limit fights that structure
|
|
8
|
+
# rather than improving it. Keep generous ceilings and let the grammar read
|
|
9
|
+
# linearly.
|
|
10
|
+
Metrics/MethodLength:
|
|
11
|
+
Max: 35
|
|
12
|
+
Metrics/AbcSize:
|
|
13
|
+
Max: 30
|
|
14
|
+
Metrics/CyclomaticComplexity:
|
|
15
|
+
Max: 12
|
|
16
|
+
Metrics/PerceivedComplexity:
|
|
17
|
+
Max: 12
|
|
18
|
+
Metrics/ClassLength:
|
|
19
|
+
Max: 600
|
|
20
|
+
Metrics/ModuleLength:
|
|
21
|
+
Max: 200
|
|
22
|
+
|
|
23
|
+
# The gem's entry point must be named after the gem (`kumi-parser`), which is
|
|
24
|
+
# hyphenated by convention.
|
|
25
|
+
Naming/FileName:
|
|
26
|
+
Exclude:
|
|
27
|
+
- "lib/kumi-parser.rb"
|
|
28
|
+
|
|
29
|
+
# Positional format tokens read fine for short, local format strings.
|
|
30
|
+
Style/FormatStringToken:
|
|
31
|
+
Enabled: false
|
|
32
|
+
|
|
33
|
+
# Specs read better with descriptive backtick-quoted example names and longer
|
|
34
|
+
# example/group bodies than the metric defaults allow.
|
|
35
|
+
Metrics/BlockLength:
|
|
36
|
+
Exclude:
|
|
37
|
+
- "spec/**/*"
|
|
38
|
+
- "*.gemspec"
|
|
39
|
+
|
|
40
|
+
Style/Documentation:
|
|
41
|
+
Enabled: false
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to kumi-parser are documented here. The format is based on
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project
|
|
5
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [0.1.0] – 2026-06-17
|
|
8
|
+
### Changed
|
|
9
|
+
- **Full rewrite of the lexer and parser.** The hand-rolled `SmartTokenizer`
|
|
10
|
+
(char-by-char loop with string accumulation and a context stack) and the
|
|
11
|
+
metadata-bag `TOKEN_METADATA` table are replaced by a single-pass
|
|
12
|
+
`StringScanner` lexer producing typed tokens, plus a compact `Grammar` of
|
|
13
|
+
lookup tables (keywords, type keywords, function sugar, operator
|
|
14
|
+
precedence/associativity). The recursive-descent + Pratt parser builds the
|
|
15
|
+
same `Kumi::Syntax::*` AST as before — verified byte-for-byte against
|
|
16
|
+
kumi-core's golden AST snapshots (50 schemas) and the full compile/runtime
|
|
17
|
+
pipeline.
|
|
18
|
+
- **First-class parse errors.** Every syntax error now reports an exact
|
|
19
|
+
`file:line:col`, a plain-English "expected X, but found Y", and a
|
|
20
|
+
caret-annotated source frame. Errors are strictly scoped to the parse phase
|
|
21
|
+
(lexing + AST construction); name resolution, types, and axes remain the
|
|
22
|
+
analyzer's concern. The raised `Kumi::Errors::SyntaxError` carries a
|
|
23
|
+
structured `Location`, so callers render the frame from data rather than
|
|
24
|
+
scraping the message.
|
|
25
|
+
- The public surface is a single `Kumi::Parser::TextParser` facade
|
|
26
|
+
(`parse` / `valid?` / `validate`). The duplicate `Base`, `Api`,
|
|
27
|
+
`SyntaxValidator`, and `ErrorExtractor` entry points, the `Kumi::TextParser`
|
|
28
|
+
and `Kumi::TextSchema` shims, and the unused `parslet` / `zeitwerk`
|
|
29
|
+
dependencies are removed. `kumi` is now a declared runtime dependency
|
|
30
|
+
(the parser builds its AST nodes).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- `element :type, :name` array-element declarations and chained array access
|
|
34
|
+
through deeply nested inputs now parse (the old parser failed its own specs
|
|
35
|
+
for these).
|
|
36
|
+
|
|
37
|
+
### Removed
|
|
38
|
+
- The `element` input-declaration keyword in the text grammar (it was unused by
|
|
39
|
+
any schema and duplicated the standard `array :x do <type> :name end` form).
|
|
40
|
+
|
|
41
|
+
## [0.0.33] – 2026-06-14
|
|
42
|
+
### Added
|
|
43
|
+
- `outer(...)` recognized as function sugar, mirroring `cross(...)`. `outer` is
|
|
44
|
+
the cross-array all-pairs operator (A × B) — the sibling of `cross` (A × A').
|
|
45
|
+
Both bare `outer(expr)` and `fn(:outer, expr)` parse to the same
|
|
46
|
+
`CallExpression(:outer, …)`, so text schemas can now express two-array
|
|
47
|
+
all-pairs (e.g. a pixels × lights field).
|
|
48
|
+
|
|
49
|
+
## [0.0.32] – 2026-06
|
|
50
|
+
### Added
|
|
51
|
+
- `cross(...)` recognized as function sugar (self-join all-pairs / N-body axis op).
|
|
52
|
+
|
|
53
|
+
## [0.0.31] – 2026-06
|
|
54
|
+
### Added
|
|
55
|
+
- Multi-level namespace constants in the tokenizer.
|
|
56
|
+
|
|
57
|
+
## [0.0.29] – 2026-06
|
|
58
|
+
### Added
|
|
59
|
+
- `import` syntax for composing schemas.
|
|
60
|
+
|
|
61
|
+
## [0.0.25] – 2026-06
|
|
62
|
+
### Changed
|
|
63
|
+
- Renamed `token_metadata` to `token_constants`; told Zeitwerk to ignore it
|
|
64
|
+
(it is a plain constants module, not an autoloadable class).
|
data/CLAUDE.md
CHANGED
|
@@ -1,120 +1,59 @@
|
|
|
1
|
-
# Kumi Parser
|
|
2
|
-
|
|
3
|
-
##
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- `lib/kumi/parser/
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- `lib/kumi/parser/
|
|
12
|
-
|
|
13
|
-
- `lib/kumi/parser/
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- `
|
|
30
|
-
- `
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
- `
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
**Compare with Ruby DSL**:
|
|
63
|
-
```ruby
|
|
64
|
-
# Define schema in Ruby
|
|
65
|
-
module TestSchema
|
|
66
|
-
extend Kumi::Schema
|
|
67
|
-
schema do
|
|
68
|
-
input do
|
|
69
|
-
float :income
|
|
70
|
-
end
|
|
71
|
-
value :tax, fn(:calc, input.income)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
# Parse equivalent text
|
|
76
|
-
text_ast = Kumi::Parser::TextParser.parse(<<~KUMI)
|
|
77
|
-
schema do
|
|
78
|
-
input do
|
|
79
|
-
float :income
|
|
80
|
-
end
|
|
81
|
-
value :tax, fn(:calc, input.income)
|
|
82
|
-
end
|
|
83
|
-
KUMI
|
|
84
|
-
|
|
85
|
-
# Compare ASTs
|
|
86
|
-
ruby_ast = TestSchema.__kumi_syntax_tree__
|
|
87
|
-
text_ast == ruby_ast # Should be true
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
- Tax schema in `spec/kumi/parser/text_parser_example tax_schema_spec.rb` is canonical test
|
|
91
|
-
- Run all tests: `rspec spec/kumi/parser/`
|
|
92
|
-
- Integration tests: `rspec spec/kumi/parser/text_parser_integration_spec.rb`
|
|
93
|
-
|
|
94
|
-
## Error Handling & Validation
|
|
95
|
-
|
|
96
|
-
- **Parse errors**: `Kumi::Parser::Errors::ParseError` (internal) → `Kumi::Errors::SyntaxError` (public API)
|
|
97
|
-
- **Tokenizer errors**: `Kumi::Parser::Errors::TokenizerError` with location info
|
|
98
|
-
- **Diagnostics**: Use `SyntaxValidator` for detailed error reporting with line/column info
|
|
99
|
-
- **Location tracking**: All tokens and AST nodes include `Kumi::Syntax::Location(file, line, column)`
|
|
100
|
-
|
|
101
|
-
## Test Status (January 2025)
|
|
102
|
-
|
|
103
|
-
✅ **All specs passing**: 32 examples, 0 failures, 1 pending
|
|
104
|
-
- ✅ Syntax validation with proper diagnostics
|
|
105
|
-
- ✅ AST compatibility with Ruby DSL (when constants aren't used)
|
|
106
|
-
- ✅ Integration with analyzer and compiler
|
|
107
|
-
- ✅ End-to-end execution testing
|
|
108
|
-
- ✅ Error type compatibility
|
|
109
|
-
|
|
110
|
-
## Known Limitations
|
|
111
|
-
|
|
112
|
-
- **Ruby constants**: Text parser cannot resolve Ruby constants like `CONST_NAME` - use inline values instead
|
|
113
|
-
- **Domain specification**: Parsing not fully implemented
|
|
114
|
-
- **Diagnostic APIs**: Monaco/CodeMirror/JSON format methods not implemented
|
|
115
|
-
|
|
116
|
-
## Performance
|
|
117
|
-
|
|
118
|
-
- Tokenization: <1ms for typical schemas
|
|
119
|
-
- Parsing: ~4ms for complete tax schema (21 values, 4 traits)
|
|
120
|
-
- Direct AST construction eliminates transformation overhead
|
|
1
|
+
# Kumi Parser — Technical Context
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
`source → Lexer → tokens → Parser → AST`. The AST is kumi-core's
|
|
6
|
+
`Kumi::Syntax::*` (so this gem depends on `kumi`).
|
|
7
|
+
|
|
8
|
+
- `lib/kumi/parser/lexer.rb` — single-pass `StringScanner` lexer. Emits a flat
|
|
9
|
+
array of `Token`s; newlines and comments are emitted but skipped by the
|
|
10
|
+
parser. Context-free (no input/schema context stack).
|
|
11
|
+
- `lib/kumi/parser/parser.rb` — recursive descent for declarations, Pratt for
|
|
12
|
+
expressions. Builds `Kumi::Syntax::*` nodes directly.
|
|
13
|
+
- `lib/kumi/parser/grammar.rb` — lookup tables: `KEYWORDS`, `TYPE_KEYWORDS`,
|
|
14
|
+
`FUNCTION_SUGAR`, `BINARY_OPERATORS` (precedence + associativity + fn name),
|
|
15
|
+
`BOOLEANS`. Replaces the old `TOKEN_METADATA` bag.
|
|
16
|
+
- `lib/kumi/parser/token.rb` — `Struct(:kind, :value, :offset)`. No metadata.
|
|
17
|
+
- `lib/kumi/parser/source.rb` — offset → `Location` and caret code frames.
|
|
18
|
+
- `lib/kumi/parser/parse_error.rb` — `ParseError` (located, framed).
|
|
19
|
+
- `lib/kumi/parser/text_parser.rb` — public API: `parse` / `valid?` /
|
|
20
|
+
`validate`. Raises `Kumi::Errors::SyntaxError` carrying a `Location`.
|
|
21
|
+
|
|
22
|
+
## Grammar notes
|
|
23
|
+
|
|
24
|
+
- Functions: `fn(:name, args...)` or bare sugar `name(...)` for the entries in
|
|
25
|
+
`FUNCTION_SUGAR` (`select`, `shift`, `roll`, `cross`, `outer`, `index`, the
|
|
26
|
+
`to_*` casts). `select` lowers to `:select`.
|
|
27
|
+
- Operators lower to fn names: `==`→`:==`, `!=`→`:!=`, `**`→`:power`,
|
|
28
|
+
`&`→`:and`, `|`→`:or`, the rest to `:add`/`:multiply`/etc.
|
|
29
|
+
- `array[i]` → `CallExpression(:at, [array, i])`.
|
|
30
|
+
- Unary minus on a non-literal → `subtract(0, x)`; a `-` directly before a digit
|
|
31
|
+
in operand position is a negative literal (`Literal(-1)`), while a spaced `-`
|
|
32
|
+
after a value is the binary operator.
|
|
33
|
+
- `let :n, …` → `ValueDeclaration` with `hints: { inline: true }`.
|
|
34
|
+
- Cascade `on`: a single trait ref is wrapped in `cascade_and([ref])`; multiple
|
|
35
|
+
conditions become `cascade_and([...])`.
|
|
36
|
+
- Function-option kwargs (`policy: :clamp`, `axis_offset: 1`) are stored as raw
|
|
37
|
+
scalars on `CallExpression#opts`. Imported-call kwargs are full expressions
|
|
38
|
+
on `ImportCall#input_mapping`.
|
|
39
|
+
- A bare capitalized word is an identifier (e.g. `let :W, …` referenced as `W`);
|
|
40
|
+
only `Foo::Bar` paths are constants. `Float::INFINITY` is the one resolved
|
|
41
|
+
constant value.
|
|
42
|
+
|
|
43
|
+
## Error boundary
|
|
44
|
+
|
|
45
|
+
Parse errors are about shape only (unexpected char, missing `end`, malformed
|
|
46
|
+
hash pair). Anything needing meaning — undefined references, types, axes — is
|
|
47
|
+
the analyzer's job in kumi-core and must not be flagged here.
|
|
48
|
+
|
|
49
|
+
## Testing
|
|
50
|
+
|
|
51
|
+
- `bundle exec rspec` — the parser's own specs (lexer, AST, errors). The
|
|
52
|
+
`Gemfile` points `kumi` at `../kumi-core` for local dev.
|
|
53
|
+
- **The real equivalence gate is in kumi-core**: from that repo,
|
|
54
|
+
`bundle exec bin/kumi golden_v2 verify --repr ast` byte-compares the parser's
|
|
55
|
+
AST against 50 frozen `golden/*/expected/ast.txt` snapshots; the full
|
|
56
|
+
`golden_v2 verify` runs the whole pipeline (incl. Ruby/JS runtime parity).
|
|
57
|
+
Keep both green. kumi-core's `Gemfile` points `kumi-parser` at this path for
|
|
58
|
+
local dev.
|
|
59
|
+
- `bundle exec rubocop` — clean.
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Kumi::Parser
|
|
2
2
|
|
|
3
|
-
Text parser for [Kumi](https://github.com/amuta/kumi) schemas
|
|
3
|
+
Text parser for [Kumi](https://github.com/amuta/kumi) schemas: a single-pass lexer feeding a recursive-descent + Pratt parser that builds kumi-core's AST directly, with located, framed parse errors.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -59,8 +59,7 @@ end
|
|
|
59
59
|
**Function calls**: `fn(:name, arg1, arg2, ...)`
|
|
60
60
|
**Operators**: `+` `-` `*` `**` `` `/` `%` `>` `<` `>=` `<=` `==` `!=` `&` `|`
|
|
61
61
|
**References**: `input.field`, `value_name`, `array[index]`
|
|
62
|
-
**Strings**: Both `"double"` and `'single'` quotes supported
|
|
63
|
-
**Element syntax**: `element :type, :name` for array element specifications
|
|
62
|
+
**Strings**: Both `"double"` and `'single'` quotes supported
|
|
64
63
|
|
|
65
64
|
## Ruby DSL Differences
|
|
66
65
|
|
|
@@ -70,9 +69,32 @@ end
|
|
|
70
69
|
|
|
71
70
|
## Architecture
|
|
72
71
|
|
|
73
|
-
|
|
74
|
-
- `
|
|
75
|
-
|
|
72
|
+
The pipeline is `source → Lexer → tokens → Parser → AST`, where the AST is
|
|
73
|
+
kumi-core's `Kumi::Syntax::*` nodes.
|
|
74
|
+
|
|
75
|
+
- `lexer.rb` — single-pass `StringScanner` lexer producing a flat array of
|
|
76
|
+
typed `Token`s, each carrying only its kind, value, and start offset.
|
|
77
|
+
- `parser.rb` — recursive descent for declarations, Pratt for expressions.
|
|
78
|
+
- `grammar.rb` — the lookup tables (keywords, type keywords, function sugar,
|
|
79
|
+
operator precedence/associativity) shared by the lexer and parser.
|
|
80
|
+
- `source.rb` / `parse_error.rb` — turn a byte offset into a `file:line:col`
|
|
81
|
+
location and a caret-annotated code frame for error messages.
|
|
82
|
+
- `text_parser.rb` — the public `parse` / `valid?` / `validate` facade.
|
|
83
|
+
|
|
84
|
+
### Error reporting
|
|
85
|
+
|
|
86
|
+
Parse errors report an exact location, a plain-English description of what was
|
|
87
|
+
expected versus what was found, and a source frame:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
demo.kumi:2:3: expected an `input do` block, but found `value`
|
|
91
|
+
➤ 2 | value :y, input.x
|
|
92
|
+
| ^
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Errors are confined to the parse phase. Resolving names, checking types, and
|
|
96
|
+
reasoning about axes are semantic concerns handled later by kumi-core's
|
|
97
|
+
analyzer, not by this gem.
|
|
76
98
|
|
|
77
99
|
## License
|
|
78
100
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parse a Kumi text schema and print its AST, then show what a parse error
|
|
4
|
+
# looks like. Run with: ruby -Ilib examples/parse_and_inspect.rb
|
|
5
|
+
require 'kumi-parser'
|
|
6
|
+
|
|
7
|
+
schema = <<~KUMI
|
|
8
|
+
schema do
|
|
9
|
+
input do
|
|
10
|
+
integer :age, domain: 18..120
|
|
11
|
+
array :scores do
|
|
12
|
+
float :value
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
trait :adult, input.age >= 18
|
|
17
|
+
let :total, fn(:sum, input.scores.value)
|
|
18
|
+
|
|
19
|
+
value :tier do
|
|
20
|
+
on adult, "adult"
|
|
21
|
+
base "minor"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
KUMI
|
|
25
|
+
|
|
26
|
+
ast = Kumi::Parser::TextParser.parse(schema)
|
|
27
|
+
puts Kumi::Support::SExpressionPrinter.print(ast)
|
|
28
|
+
|
|
29
|
+
puts "\n--- a parse error ---"
|
|
30
|
+
begin
|
|
31
|
+
Kumi::Parser::TextParser.parse("schema do\n value :y input.x\nend\n", source_file: 'demo.kumi')
|
|
32
|
+
rescue Kumi::Errors::SyntaxError => e
|
|
33
|
+
puts "#{e.message} (#{e.location})"
|
|
34
|
+
end
|
data/kumi-parser.gemspec
CHANGED
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
|
12
12
|
spec.description = 'Allows Kumi schemas to be written as plain text with syntax validation and editor integration.'
|
|
13
13
|
spec.homepage = 'https://github.com/amuta/kumi-parser'
|
|
14
14
|
spec.license = 'MIT'
|
|
15
|
-
spec.required_ruby_version = '>= 3.
|
|
15
|
+
spec.required_ruby_version = '>= 3.1.0'
|
|
16
16
|
|
|
17
17
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
|
18
18
|
spec.metadata['homepage_uri'] = spec.homepage
|
|
@@ -31,9 +31,8 @@ Gem::Specification.new do |spec|
|
|
|
31
31
|
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
|
32
32
|
spec.require_paths = ['lib']
|
|
33
33
|
|
|
34
|
-
#
|
|
35
|
-
spec.add_dependency '
|
|
36
|
-
spec.add_dependency 'zeitwerk', '~> 2.6'
|
|
34
|
+
# The parser builds Kumi::Syntax::* nodes, so it needs the core gem's AST.
|
|
35
|
+
spec.add_dependency 'kumi'
|
|
37
36
|
|
|
38
37
|
# Development dependencies
|
|
39
38
|
spec.add_development_dependency 'bundler', '~> 2.0'
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kumi
|
|
4
|
+
module Parser
|
|
5
|
+
# Static grammar tables shared by the lexer and parser. These replace the
|
|
6
|
+
# old per-token TOKEN_METADATA bag: lookups are keyed by a word or token
|
|
7
|
+
# kind, so the data lives once here instead of being copied onto every
|
|
8
|
+
# token instance.
|
|
9
|
+
module Grammar
|
|
10
|
+
# Bare words that are keywords rather than identifiers. `true`/`false` are
|
|
11
|
+
# not here: they are boolean literals, handled directly by the lexer.
|
|
12
|
+
KEYWORDS = {
|
|
13
|
+
'schema' => :schema,
|
|
14
|
+
'input' => :input,
|
|
15
|
+
'value' => :value,
|
|
16
|
+
'let' => :let,
|
|
17
|
+
'trait' => :trait,
|
|
18
|
+
'import' => :import,
|
|
19
|
+
'codegen' => :codegen,
|
|
20
|
+
'do' => :do,
|
|
21
|
+
'end' => :end,
|
|
22
|
+
'on' => :on,
|
|
23
|
+
'base' => :base,
|
|
24
|
+
'fn' => :fn
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# The two boolean literal words, mapped to their Ruby values.
|
|
28
|
+
BOOLEANS = { 'true' => true, 'false' => false }.freeze
|
|
29
|
+
|
|
30
|
+
# Type keywords introduce input declarations. The value is the canonical
|
|
31
|
+
# type symbol stored on InputDeclaration.
|
|
32
|
+
TYPE_KEYWORDS = {
|
|
33
|
+
'integer' => :integer,
|
|
34
|
+
'float' => :float,
|
|
35
|
+
'decimal' => :decimal,
|
|
36
|
+
'string' => :string,
|
|
37
|
+
'boolean' => :boolean,
|
|
38
|
+
'any' => :any,
|
|
39
|
+
'array' => :array,
|
|
40
|
+
'hash' => :hash
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Container types whose declarations may open a nested `do … end` block.
|
|
44
|
+
CONTAINER_TYPES = %i[array hash].freeze
|
|
45
|
+
|
|
46
|
+
# Bare-call sugar: `name(args)` parses as `fn(:resolved_name, args)`.
|
|
47
|
+
# The value is the function symbol the call lowers to.
|
|
48
|
+
FUNCTION_SUGAR = {
|
|
49
|
+
'select' => :select,
|
|
50
|
+
'shift' => :shift,
|
|
51
|
+
'roll' => :roll,
|
|
52
|
+
'cross' => :cross,
|
|
53
|
+
'outer' => :outer,
|
|
54
|
+
'index' => :index,
|
|
55
|
+
'to_decimal' => :to_decimal,
|
|
56
|
+
'to_integer' => :to_integer,
|
|
57
|
+
'to_float' => :to_float,
|
|
58
|
+
'to_string' => :to_string
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
# Binary operators: kind => [precedence, :left/:right, fn_name].
|
|
62
|
+
# Higher precedence binds tighter. fn_name is the symbol the operator
|
|
63
|
+
# lowers to in the AST (CallExpression fn_name).
|
|
64
|
+
BINARY_OPERATORS = {
|
|
65
|
+
power: [7, :right, :power],
|
|
66
|
+
multiply: [6, :left, :multiply],
|
|
67
|
+
divide: [6, :left, :divide],
|
|
68
|
+
modulo: [6, :left, :modulo],
|
|
69
|
+
add: [5, :left, :add],
|
|
70
|
+
subtract: [5, :left, :subtract],
|
|
71
|
+
gte: [4, :left, :>=],
|
|
72
|
+
lte: [4, :left, :<=],
|
|
73
|
+
gt: [4, :left, :>],
|
|
74
|
+
lt: [4, :left, :<],
|
|
75
|
+
eq: [4, :left, :==],
|
|
76
|
+
ne: [4, :left, :!=],
|
|
77
|
+
and: [3, :left, :and],
|
|
78
|
+
or: [2, :left, :or]
|
|
79
|
+
}.freeze
|
|
80
|
+
|
|
81
|
+
module_function
|
|
82
|
+
|
|
83
|
+
def keyword(word)
|
|
84
|
+
KEYWORDS[word]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def boolean?(word)
|
|
88
|
+
BOOLEANS.key?(word)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def boolean(word)
|
|
92
|
+
BOOLEANS[word]
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def type_keyword(word)
|
|
96
|
+
TYPE_KEYWORDS[word]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def function_sugar(word)
|
|
100
|
+
FUNCTION_SUGAR[word]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def binary_operator?(kind)
|
|
104
|
+
BINARY_OPERATORS.key?(kind)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def precedence(kind)
|
|
108
|
+
BINARY_OPERATORS.fetch(kind)[0]
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def right_associative?(kind)
|
|
112
|
+
BINARY_OPERATORS.fetch(kind)[1] == :right
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def operator_fn(kind)
|
|
116
|
+
BINARY_OPERATORS.fetch(kind)[2]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|