@starpak/lang 0.0.0-stage → 0.1.2
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 +22 -0
- package/lib/ast.d.ts +334 -0
- package/lib/ast.d.ts.map +1 -0
- package/lib/ast.js +9 -0
- package/lib/ast.js.map +1 -0
- package/lib/checker.d.ts +17 -0
- package/lib/checker.d.ts.map +1 -0
- package/lib/checker.js +525 -0
- package/lib/checker.js.map +1 -0
- package/lib/diagnostics.d.ts +46 -0
- package/lib/diagnostics.d.ts.map +1 -0
- package/lib/diagnostics.js +77 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/emit/lua.d.ts +19 -0
- package/lib/emit/lua.d.ts.map +1 -0
- package/lib/emit/lua.js +451 -0
- package/lib/emit/lua.js.map +1 -0
- package/lib/emit/ts.d.ts +28 -0
- package/lib/emit/ts.d.ts.map +1 -0
- package/lib/emit/ts.js +803 -0
- package/lib/emit/ts.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +49 -0
- package/lib/index.js.map +1 -0
- package/lib/lexer.d.ts +10 -0
- package/lib/lexer.d.ts.map +1 -0
- package/lib/lexer.js +282 -0
- package/lib/lexer.js.map +1 -0
- package/lib/parser.d.ts +11 -0
- package/lib/parser.d.ts.map +1 -0
- package/lib/parser.js +1133 -0
- package/lib/parser.js.map +1 -0
- package/lib/token.d.ts +32 -0
- package/lib/token.d.ts.map +1 -0
- package/lib/token.js +18 -0
- package/lib/token.js.map +1 -0
- package/lib/types.d.ts +63 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +177 -0
- package/lib/types.js.map +1 -0
- package/package.json +10 -3
- package/src/ast.ts +459 -0
- package/src/checker.ts +554 -0
- package/src/diagnostics.ts +100 -0
- package/src/emit/lua.ts +484 -0
- package/src/emit/ts.ts +861 -0
- package/src/index.ts +21 -0
- package/src/lexer.ts +301 -0
- package/src/parser.ts +1251 -0
- package/src/token.ts +34 -0
- package/src/types.ts +218 -0
- package/README.md +0 -3
package/src/token.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Token definitions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const TokenType = {
|
|
6
|
+
Number: 'Number',
|
|
7
|
+
String: 'String',
|
|
8
|
+
FString: 'FString',
|
|
9
|
+
Identifier: 'Identifier',
|
|
10
|
+
Keyword: 'Keyword',
|
|
11
|
+
Operator: 'Operator',
|
|
12
|
+
Newline: 'Newline',
|
|
13
|
+
Semicolon: 'Semicolon',
|
|
14
|
+
EOF: 'EOF',
|
|
15
|
+
} as const
|
|
16
|
+
|
|
17
|
+
export type TokenType = typeof TokenType[keyof typeof TokenType]
|
|
18
|
+
|
|
19
|
+
export interface Token {
|
|
20
|
+
type: TokenType
|
|
21
|
+
/** Raw lexeme for identifiers/keywords/operators, decoded content for strings. */
|
|
22
|
+
value: string
|
|
23
|
+
/** 1-based line of the first character. */
|
|
24
|
+
line: number
|
|
25
|
+
/** 1-based column of the first character. */
|
|
26
|
+
column: number
|
|
27
|
+
/** Character offset of the first character. */
|
|
28
|
+
start: number
|
|
29
|
+
/** Character offset just past the last character. */
|
|
30
|
+
end: number
|
|
31
|
+
fstring?: boolean
|
|
32
|
+
/** Doc comment (`---`) that directly precedes this token, if any. */
|
|
33
|
+
doc?: string
|
|
34
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spak Lang - Type model and compatibility rules (M2)
|
|
3
|
+
*
|
|
4
|
+
* Types only exist at check time; nothing here is emitted to runtime code.
|
|
5
|
+
* The model mirrors `docs/syntax/typecheck.md`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { TypeAnn } from './ast'
|
|
9
|
+
|
|
10
|
+
export type Type =
|
|
11
|
+
| AnyType
|
|
12
|
+
| NilType
|
|
13
|
+
| PrimType
|
|
14
|
+
| ArrayType
|
|
15
|
+
| DictType
|
|
16
|
+
| RecordType
|
|
17
|
+
| FnType
|
|
18
|
+
| UnionType
|
|
19
|
+
| NamedType
|
|
20
|
+
|
|
21
|
+
export interface AnyType {
|
|
22
|
+
kind: 'any'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface NilType {
|
|
26
|
+
kind: 'nil'
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface PrimType {
|
|
30
|
+
kind: 'prim'
|
|
31
|
+
name: 'boolean' | 'number' | 'string'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ArrayType {
|
|
35
|
+
kind: 'array'
|
|
36
|
+
element: Type
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DictType {
|
|
40
|
+
kind: 'dict'
|
|
41
|
+
key: Type
|
|
42
|
+
value: Type
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RecordType {
|
|
46
|
+
kind: 'record'
|
|
47
|
+
fields: Map<string, Type>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface FnType {
|
|
51
|
+
kind: 'fn'
|
|
52
|
+
params: Type[]
|
|
53
|
+
returns: Type
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface UnionType {
|
|
57
|
+
kind: 'union'
|
|
58
|
+
types: Type[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface NamedType {
|
|
62
|
+
kind: 'named'
|
|
63
|
+
name: string
|
|
64
|
+
/** Filled after alias resolution when the name is a type alias. */
|
|
65
|
+
resolved?: Type
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const ANY: AnyType = { kind: 'any' }
|
|
69
|
+
export const NIL: NilType = { kind: 'nil' }
|
|
70
|
+
export const NUMBER: PrimType = { kind: 'prim', name: 'number' }
|
|
71
|
+
export const STRING: PrimType = { kind: 'prim', name: 'string' }
|
|
72
|
+
export const BOOLEAN: PrimType = { kind: 'prim', name: 'boolean' }
|
|
73
|
+
|
|
74
|
+
const PRIMS: Record<string, PrimType> = {
|
|
75
|
+
number: NUMBER,
|
|
76
|
+
string: STRING,
|
|
77
|
+
boolean: BOOLEAN,
|
|
78
|
+
bool: BOOLEAN,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Resolve an AST type annotation into the checker's type model. */
|
|
82
|
+
export function fromTypeAnn(ann: TypeAnn): Type {
|
|
83
|
+
switch (ann.kind) {
|
|
84
|
+
case 'named': {
|
|
85
|
+
const prim = PRIMS[ann.name]
|
|
86
|
+
if (prim) return prim
|
|
87
|
+
if (ann.name === 'any') return ANY
|
|
88
|
+
if (ann.name === 'nil') return NIL
|
|
89
|
+
return { kind: 'named', name: ann.name }
|
|
90
|
+
}
|
|
91
|
+
case 'array':
|
|
92
|
+
return { kind: 'array', element: fromTypeAnn(ann.element) }
|
|
93
|
+
case 'dict':
|
|
94
|
+
return { kind: 'dict', key: fromTypeAnn(ann.key), value: fromTypeAnn(ann.value) }
|
|
95
|
+
case 'record': {
|
|
96
|
+
const fields = new Map<string, Type>()
|
|
97
|
+
for (const field of ann.fields) fields.set(field.name, fromTypeAnn(field.type))
|
|
98
|
+
return { kind: 'record', fields }
|
|
99
|
+
}
|
|
100
|
+
case 'union':
|
|
101
|
+
return union(ann.types.map(fromTypeAnn))
|
|
102
|
+
case 'fn':
|
|
103
|
+
return {
|
|
104
|
+
kind: 'fn',
|
|
105
|
+
params: ann.params.map(fromTypeAnn),
|
|
106
|
+
returns: fromTypeAnn(ann.returns),
|
|
107
|
+
}
|
|
108
|
+
case 'optional':
|
|
109
|
+
return union([fromTypeAnn(ann.type), NIL])
|
|
110
|
+
default:
|
|
111
|
+
return ANY
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Normalise a union: flatten, drop duplicates, collapse `any`, keep `nil` last. */
|
|
116
|
+
export function union(types: Type[]): Type {
|
|
117
|
+
const flat: Type[] = []
|
|
118
|
+
for (const type of types) {
|
|
119
|
+
if (type.kind === 'union') flat.push(...type.types)
|
|
120
|
+
else flat.push(type)
|
|
121
|
+
}
|
|
122
|
+
const seen = new Set<string>()
|
|
123
|
+
const out: Type[] = []
|
|
124
|
+
for (const type of flat) {
|
|
125
|
+
const key = print(type)
|
|
126
|
+
if (seen.has(key)) continue
|
|
127
|
+
seen.add(key)
|
|
128
|
+
out.push(type)
|
|
129
|
+
}
|
|
130
|
+
if (out.some(type => type.kind === 'any')) return ANY
|
|
131
|
+
if (out.length === 1) return out[0]
|
|
132
|
+
const nonNil: Type[] = out.filter(type => type.kind !== 'nil')
|
|
133
|
+
const nils: Type[] = out.filter(type => type.kind === 'nil')
|
|
134
|
+
return { kind: 'union', types: [...nonNil, ...nils] }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function isNil(type: Type): boolean {
|
|
138
|
+
return type.kind === 'nil'
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Remove `nil` from a type (used by `if x then` narrowing). */
|
|
142
|
+
export function withoutNil(type: Type): Type {
|
|
143
|
+
if (type.kind !== 'union') return type.kind === 'nil' ? ANY : type
|
|
144
|
+
const rest = type.types.filter(item => item.kind !== 'nil')
|
|
145
|
+
if (!rest.length) return ANY
|
|
146
|
+
return union(rest)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** `target` accepts `source`? Structural, with `any` behaving as both directions. */
|
|
150
|
+
export function isAssignable(target: Type, source: Type): boolean {
|
|
151
|
+
if (target.kind === 'any' || source.kind === 'any') return true
|
|
152
|
+
if (target.kind === 'named' && target.resolved) return isAssignable(target.resolved, source)
|
|
153
|
+
if (source.kind === 'named' && source.resolved) return isAssignable(target, source.resolved)
|
|
154
|
+
|
|
155
|
+
if (target.kind === 'union') {
|
|
156
|
+
if (source.kind === 'union') return source.types.every(item => isAssignable(target, item))
|
|
157
|
+
return target.types.some(item => isAssignable(item, source))
|
|
158
|
+
}
|
|
159
|
+
if (source.kind === 'union') return source.types.every(item => isAssignable(target, item))
|
|
160
|
+
|
|
161
|
+
if (target.kind !== source.kind) return false
|
|
162
|
+
|
|
163
|
+
switch (target.kind) {
|
|
164
|
+
case 'nil':
|
|
165
|
+
return true
|
|
166
|
+
case 'prim':
|
|
167
|
+
return source.kind === 'prim' && source.name === target.name
|
|
168
|
+
case 'array':
|
|
169
|
+
return source.kind === 'array' && isAssignable(target.element, source.element)
|
|
170
|
+
case 'dict':
|
|
171
|
+
return source.kind === 'dict'
|
|
172
|
+
&& isAssignable(target.key, source.key)
|
|
173
|
+
&& isAssignable(target.value, source.value)
|
|
174
|
+
case 'record': {
|
|
175
|
+
if (source.kind !== 'record') return false
|
|
176
|
+
for (const [name, type] of target.fields) {
|
|
177
|
+
const found = source.fields.get(name)
|
|
178
|
+
if (!found || !isAssignable(type, found)) return false
|
|
179
|
+
}
|
|
180
|
+
return true
|
|
181
|
+
}
|
|
182
|
+
case 'fn': {
|
|
183
|
+
if (source.kind !== 'fn' || source.params.length !== target.params.length) return false
|
|
184
|
+
const paramsOk = target.params.every((type, index) => isAssignable(source.params[index], type))
|
|
185
|
+
return paramsOk && isAssignable(target.returns, source.returns)
|
|
186
|
+
}
|
|
187
|
+
case 'named':
|
|
188
|
+
return source.kind === 'named' && source.name === target.name
|
|
189
|
+
default:
|
|
190
|
+
return false
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Human readable type, used in diagnostics. */
|
|
195
|
+
export function print(type: Type): string {
|
|
196
|
+
switch (type.kind) {
|
|
197
|
+
case 'any':
|
|
198
|
+
return 'any'
|
|
199
|
+
case 'nil':
|
|
200
|
+
return 'nil'
|
|
201
|
+
case 'prim':
|
|
202
|
+
return type.name
|
|
203
|
+
case 'array':
|
|
204
|
+
return `[${print(type.element)}]`
|
|
205
|
+
case 'dict':
|
|
206
|
+
return `{${print(type.key)}: ${print(type.value)}}`
|
|
207
|
+
case 'record': {
|
|
208
|
+
const fields = [...type.fields].map(([name, value]) => `${name}: ${print(value)}`)
|
|
209
|
+
return `{ ${fields.join(', ')} }`
|
|
210
|
+
}
|
|
211
|
+
case 'fn':
|
|
212
|
+
return `(${type.params.map(print).join(', ')}) -> ${print(type.returns)}`
|
|
213
|
+
case 'union':
|
|
214
|
+
return type.types.map(print).join(' | ')
|
|
215
|
+
case 'named':
|
|
216
|
+
return type.name
|
|
217
|
+
}
|
|
218
|
+
}
|
package/README.md
DELETED