obsidian-lang 0.0.1__tar.gz
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.
- obsidian_lang-0.0.1/LICENSE +21 -0
- obsidian_lang-0.0.1/PKG-INFO +63 -0
- obsidian_lang-0.0.1/README.md +37 -0
- obsidian_lang-0.0.1/obsidian/__init__.py +7 -0
- obsidian_lang-0.0.1/obsidian/ast.py +59 -0
- obsidian_lang-0.0.1/obsidian/lexer.py +90 -0
- obsidian_lang-0.0.1/obsidian/main.py +66 -0
- obsidian_lang-0.0.1/obsidian/parser.py +127 -0
- obsidian_lang-0.0.1/obsidian/transpiler.py +64 -0
- obsidian_lang-0.0.1/obsidian_lang.egg-info/PKG-INFO +63 -0
- obsidian_lang-0.0.1/obsidian_lang.egg-info/SOURCES.txt +14 -0
- obsidian_lang-0.0.1/obsidian_lang.egg-info/dependency_links.txt +1 -0
- obsidian_lang-0.0.1/obsidian_lang.egg-info/entry_points.txt +2 -0
- obsidian_lang-0.0.1/obsidian_lang.egg-info/top_level.txt +1 -0
- obsidian_lang-0.0.1/pyproject.toml +44 -0
- obsidian_lang-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Obsidian Language Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obsidian-lang
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Obsidian is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate.
|
|
5
|
+
Author: Obsidian Language Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/example/obsidian-lang
|
|
8
|
+
Project-URL: Repository, https://github.com/example/obsidian-lang
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/example/obsidian-lang/issues
|
|
10
|
+
Keywords: programming-language,compiler,transpiler,rust-like,toy-language
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
21
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Obsidian Language v0.0.1
|
|
28
|
+
|
|
29
|
+
**Obsidian** is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate.
|
|
30
|
+
|
|
31
|
+
This is a toy implementation (v0.0.1) written in Python. It features a lexer, parser, AST, and a transpiler to Python (which simulates "compilation" by generating and executing equivalent Python code). Published on PyPI as `obsidian-lang`.
|
|
32
|
+
|
|
33
|
+
## Features (v0.0.1)
|
|
34
|
+
- Minimalist syntax inspired by Rust
|
|
35
|
+
- Variables with `let`
|
|
36
|
+
- Functions with `fn`
|
|
37
|
+
- Basic expressions + comparison operators (< > <= >= == !=)
|
|
38
|
+
- Control flow with `if` / `else`
|
|
39
|
+
- Function calls (including built-in `println`)
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
```bash
|
|
43
|
+
pip install obsidian-lang
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
```bash
|
|
48
|
+
# After pip install obsidian-lang
|
|
49
|
+
obsidian examples/hello.obs --show-code
|
|
50
|
+
|
|
51
|
+
# Or from source (development)
|
|
52
|
+
PYTHONPATH=. python -m obsidian.main examples/hello.obs --show-code
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Example (hello.obs)
|
|
56
|
+
```
|
|
57
|
+
fn main() {
|
|
58
|
+
let x = 5;
|
|
59
|
+
let y = x + 3;
|
|
60
|
+
println(y);
|
|
61
|
+
println("Hello from Obsidian!");
|
|
62
|
+
}
|
|
63
|
+
```
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Obsidian Language v0.0.1
|
|
2
|
+
|
|
3
|
+
**Obsidian** is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate.
|
|
4
|
+
|
|
5
|
+
This is a toy implementation (v0.0.1) written in Python. It features a lexer, parser, AST, and a transpiler to Python (which simulates "compilation" by generating and executing equivalent Python code). Published on PyPI as `obsidian-lang`.
|
|
6
|
+
|
|
7
|
+
## Features (v0.0.1)
|
|
8
|
+
- Minimalist syntax inspired by Rust
|
|
9
|
+
- Variables with `let`
|
|
10
|
+
- Functions with `fn`
|
|
11
|
+
- Basic expressions + comparison operators (< > <= >= == !=)
|
|
12
|
+
- Control flow with `if` / `else`
|
|
13
|
+
- Function calls (including built-in `println`)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
```bash
|
|
17
|
+
pip install obsidian-lang
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
```bash
|
|
22
|
+
# After pip install obsidian-lang
|
|
23
|
+
obsidian examples/hello.obs --show-code
|
|
24
|
+
|
|
25
|
+
# Or from source (development)
|
|
26
|
+
PYTHONPATH=. python -m obsidian.main examples/hello.obs --show-code
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Example (hello.obs)
|
|
30
|
+
```
|
|
31
|
+
fn main() {
|
|
32
|
+
let x = 5;
|
|
33
|
+
let y = x + 3;
|
|
34
|
+
println(y);
|
|
35
|
+
println("Hello from Obsidian!");
|
|
36
|
+
}
|
|
37
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Obsidian Programming Language
|
|
3
|
+
v0.0.1a - A toy implementation in Python
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.0.1a"
|
|
7
|
+
__description__ = "Obsidian is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want to speed of Rust without the boilerplate."
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
|
+
|
|
4
|
+
class ASTNode: pass
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class Program(ASTNode):
|
|
8
|
+
functions: List['Function'] = field(default_factory=list)
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class Function(ASTNode):
|
|
12
|
+
name: str
|
|
13
|
+
params: List[str]
|
|
14
|
+
body: List['Statement']
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Statement(ASTNode): pass
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class LetStatement(Statement):
|
|
21
|
+
name: str
|
|
22
|
+
value: 'Expression'
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ReturnStatement(Statement):
|
|
26
|
+
value: Optional['Expression'] = None
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class IfStatement(Statement):
|
|
30
|
+
condition: 'Expression'
|
|
31
|
+
then_block: List[Statement]
|
|
32
|
+
else_block: Optional[List[Statement]] = None
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ExpressionStatement(Statement):
|
|
36
|
+
expression: 'Expression'
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Expression(ASTNode): pass
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Literal(Expression):
|
|
43
|
+
value: Union[int, float, str]
|
|
44
|
+
typ: str
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Identifier(Expression):
|
|
48
|
+
name: str
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class BinaryOp(Expression):
|
|
52
|
+
left: Expression
|
|
53
|
+
op: str
|
|
54
|
+
right: Expression
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Call(Expression):
|
|
58
|
+
callee: str
|
|
59
|
+
args: List[Expression]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from enum import Enum, auto
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
class TokenType(Enum):
|
|
6
|
+
FN = auto(); LET = auto(); RETURN = auto(); IF = auto(); ELSE = auto()
|
|
7
|
+
NUMBER = auto(); STRING = auto(); IDENTIFIER = auto()
|
|
8
|
+
LPAREN = auto(); RPAREN = auto(); LBRACE = auto(); RBRACE = auto()
|
|
9
|
+
COMMA = auto(); SEMICOLON = auto(); COLON = auto(); ASSIGN = auto()
|
|
10
|
+
PLUS = auto(); MINUS = auto(); STAR = auto(); SLASH = auto()
|
|
11
|
+
LT = auto(); GT = auto(); LTE = auto(); GTE = auto(); EQ = auto(); NEQ = auto()
|
|
12
|
+
EOF = auto()
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Token:
|
|
16
|
+
type: TokenType
|
|
17
|
+
value: str
|
|
18
|
+
line: int
|
|
19
|
+
column: int
|
|
20
|
+
|
|
21
|
+
class Lexer:
|
|
22
|
+
def __init__(self, source: str):
|
|
23
|
+
self.source = source
|
|
24
|
+
self.tokens: List[Token] = []
|
|
25
|
+
self.pos = 0
|
|
26
|
+
self.line = 1
|
|
27
|
+
self.column = 1
|
|
28
|
+
self.keywords = {'fn':TokenType.FN,'let':TokenType.LET,'return':TokenType.RETURN,'if':TokenType.IF,'else':TokenType.ELSE}
|
|
29
|
+
|
|
30
|
+
def _peek(self): return self.source[self.pos] if self.pos < len(self.source) else '\0'
|
|
31
|
+
def _advance(self):
|
|
32
|
+
ch = self._peek(); self.pos += 1
|
|
33
|
+
if ch == '\n': self.line += 1; self.column = 1
|
|
34
|
+
else: self.column += 1
|
|
35
|
+
return ch
|
|
36
|
+
def _skip_ws(self):
|
|
37
|
+
while self._peek() in ' \t\r\n': self._advance()
|
|
38
|
+
|
|
39
|
+
def tokenize(self):
|
|
40
|
+
self.tokens = []
|
|
41
|
+
while self.pos < len(self.source):
|
|
42
|
+
self._skip_ws()
|
|
43
|
+
if self.pos >= len(self.source): break
|
|
44
|
+
sl, sc = self.line, self.column
|
|
45
|
+
ch = self._advance()
|
|
46
|
+
if ch.isdigit():
|
|
47
|
+
num = ch
|
|
48
|
+
while self._peek().isdigit() or self._peek() == '.': num += self._advance()
|
|
49
|
+
self.tokens.append(Token(TokenType.NUMBER, num, sl, sc))
|
|
50
|
+
elif ch.isalpha() or ch == '_':
|
|
51
|
+
ident = ch
|
|
52
|
+
while self._peek().isalnum() or self._peek() == '_': ident += self._advance()
|
|
53
|
+
tt = self.keywords.get(ident, TokenType.IDENTIFIER)
|
|
54
|
+
self.tokens.append(Token(tt, ident, sl, sc))
|
|
55
|
+
elif ch == '"':
|
|
56
|
+
s = ''
|
|
57
|
+
while self._peek() != '"' and self._peek() != '\0':
|
|
58
|
+
if self._peek() == '\\':
|
|
59
|
+
self._advance()
|
|
60
|
+
s += {'n':'\n','t':'\t'}.get(self._peek(), self._peek()); self._advance()
|
|
61
|
+
else: s += self._advance()
|
|
62
|
+
if self._peek() == '"': self._advance()
|
|
63
|
+
self.tokens.append(Token(TokenType.STRING, s, sl, sc))
|
|
64
|
+
elif ch=='(': self.tokens.append(Token(TokenType.LPAREN,'(',sl,sc))
|
|
65
|
+
elif ch==')': self.tokens.append(Token(TokenType.RPAREN,')',sl,sc))
|
|
66
|
+
elif ch=='{': self.tokens.append(Token(TokenType.LBRACE,'{',sl,sc))
|
|
67
|
+
elif ch=='}': self.tokens.append(Token(TokenType.RBRACE,'}',sl,sc))
|
|
68
|
+
elif ch==',': self.tokens.append(Token(TokenType.COMMA,',',sl,sc))
|
|
69
|
+
elif ch==';': self.tokens.append(Token(TokenType.SEMICOLON,';',sl,sc))
|
|
70
|
+
elif ch==':': self.tokens.append(Token(TokenType.COLON,':',sl,sc))
|
|
71
|
+
elif ch=='+': self.tokens.append(Token(TokenType.PLUS,'+',sl,sc))
|
|
72
|
+
elif ch=='-': self.tokens.append(Token(TokenType.MINUS,'-',sl,sc))
|
|
73
|
+
elif ch=='*': self.tokens.append(Token(TokenType.STAR,'*',sl,sc))
|
|
74
|
+
elif ch=='/': self.tokens.append(Token(TokenType.SLASH,'/',sl,sc))
|
|
75
|
+
elif ch=='<':
|
|
76
|
+
if self._peek()=='=': self._advance(); self.tokens.append(Token(TokenType.LTE,'<=',sl,sc))
|
|
77
|
+
else: self.tokens.append(Token(TokenType.LT,'<',sl,sc))
|
|
78
|
+
elif ch=='>':
|
|
79
|
+
if self._peek()=='=': self._advance(); self.tokens.append(Token(TokenType.GTE,'>=',sl,sc))
|
|
80
|
+
else: self.tokens.append(Token(TokenType.GT,'>',sl,sc))
|
|
81
|
+
elif ch=='=':
|
|
82
|
+
if self._peek()=='=': self._advance(); self.tokens.append(Token(TokenType.EQ,'==',sl,sc))
|
|
83
|
+
else: self.tokens.append(Token(TokenType.ASSIGN,'=',sl,sc))
|
|
84
|
+
elif ch=='!':
|
|
85
|
+
if self._peek()=='=': self._advance(); self.tokens.append(Token(TokenType.NEQ,'!=',sl,sc))
|
|
86
|
+
else: raise SyntaxError("!")
|
|
87
|
+
else:
|
|
88
|
+
raise SyntaxError(f"Unexpected '{ch}' at {sl}:{sc}")
|
|
89
|
+
self.tokens.append(Token(TokenType.EOF, '', self.line, self.column))
|
|
90
|
+
return self.tokens
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import argparse
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from .lexer import Lexer
|
|
5
|
+
from .parser import Parser
|
|
6
|
+
from .transpiler import Transpiler
|
|
7
|
+
from . import __version__, __description__
|
|
8
|
+
|
|
9
|
+
def run_obsidian_file(filepath: str, execute: bool = True, show_code: bool = False):
|
|
10
|
+
path = Path(filepath)
|
|
11
|
+
if not path.exists():
|
|
12
|
+
print(f"Error: File not found: {filepath}")
|
|
13
|
+
sys.exit(1)
|
|
14
|
+
source = path.read_text(encoding="utf-8")
|
|
15
|
+
print(f"Obsidian v{__version__} - Processing {filepath}")
|
|
16
|
+
print("Parsing source...")
|
|
17
|
+
lexer = Lexer(source)
|
|
18
|
+
try:
|
|
19
|
+
tokens = lexer.tokenize()
|
|
20
|
+
except Exception as e:
|
|
21
|
+
print(f"Lexing error: {e}")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
parser = Parser(tokens)
|
|
24
|
+
try:
|
|
25
|
+
ast = parser.parse()
|
|
26
|
+
except Exception as e:
|
|
27
|
+
print(f"Parsing error: {e}")
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
print(f"Successfully parsed {len(ast.functions)} function(s)")
|
|
30
|
+
transpiler = Transpiler()
|
|
31
|
+
try:
|
|
32
|
+
python_code = transpiler.transpile(ast)
|
|
33
|
+
except Exception as e:
|
|
34
|
+
print(f"Transpilation error: {e}")
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
if show_code:
|
|
37
|
+
print("\n--- Generated Python Code ---")
|
|
38
|
+
print(python_code)
|
|
39
|
+
print("--- End of generated code ---\n")
|
|
40
|
+
if execute:
|
|
41
|
+
print("Executing (transpiled) program...\n")
|
|
42
|
+
try:
|
|
43
|
+
namespace = {"__name__": "__main__"}
|
|
44
|
+
exec(python_code, namespace)
|
|
45
|
+
except Exception as e:
|
|
46
|
+
print(f"Runtime error: {e}")
|
|
47
|
+
sys.exit(1)
|
|
48
|
+
print("\nProgram finished successfully.")
|
|
49
|
+
|
|
50
|
+
def main():
|
|
51
|
+
parser = argparse.ArgumentParser(description=__description__, prog="obsidian")
|
|
52
|
+
parser.add_argument("file", nargs="?", help="Obsidian source file (.obs)")
|
|
53
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
54
|
+
parser.add_argument("--show-code", action="store_true", help="Print the generated Python code")
|
|
55
|
+
parser.add_argument("--no-exec", action="store_true", help="Do not execute the program, only transpile")
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
if not args.file:
|
|
58
|
+
print("Obsidian Language v0.0.1a")
|
|
59
|
+
print(__description__)
|
|
60
|
+
print("\nUsage: python -m obsidian.main <file.obs> [--show-code] [--no-exec]")
|
|
61
|
+
print("Example: python -m obsidian.main examples/hello.obs")
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
run_obsidian_file(args.file, execute=not args.no_exec, show_code=args.show_code)
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
main()
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from typing import List, Optional
|
|
2
|
+
from .lexer import Token, TokenType
|
|
3
|
+
from .ast import *
|
|
4
|
+
|
|
5
|
+
class Parser:
|
|
6
|
+
def __init__(self, tokens: List[Token]):
|
|
7
|
+
self.tokens = tokens
|
|
8
|
+
self.pos = 0
|
|
9
|
+
|
|
10
|
+
def _cur(self): return self.tokens[self.pos]
|
|
11
|
+
def _adv(self):
|
|
12
|
+
t = self._cur()
|
|
13
|
+
if self.pos < len(self.tokens)-1: self.pos += 1
|
|
14
|
+
return t
|
|
15
|
+
def _eat(self, tt: TokenType):
|
|
16
|
+
t = self._cur()
|
|
17
|
+
if t.type != tt: raise SyntaxError(f"Expected {tt.name} got {t.type.name}")
|
|
18
|
+
return self._adv()
|
|
19
|
+
|
|
20
|
+
def parse(self) -> Program:
|
|
21
|
+
p = Program()
|
|
22
|
+
while self._cur().type != TokenType.EOF:
|
|
23
|
+
if self._cur().type == TokenType.FN: p.functions.append(self._fn())
|
|
24
|
+
else: raise SyntaxError("Only fn at top level")
|
|
25
|
+
return p
|
|
26
|
+
|
|
27
|
+
def _fn(self):
|
|
28
|
+
self._eat(TokenType.FN)
|
|
29
|
+
name = self._eat(TokenType.IDENTIFIER).value
|
|
30
|
+
self._eat(TokenType.LPAREN)
|
|
31
|
+
params = []
|
|
32
|
+
if self._cur().type != TokenType.RPAREN:
|
|
33
|
+
while True:
|
|
34
|
+
params.append(self._eat(TokenType.IDENTIFIER).value)
|
|
35
|
+
if self._cur().type == TokenType.COMMA: self._adv()
|
|
36
|
+
else: break
|
|
37
|
+
self._eat(TokenType.RPAREN)
|
|
38
|
+
self._eat(TokenType.LBRACE)
|
|
39
|
+
body = []
|
|
40
|
+
while self._cur().type not in (TokenType.RBRACE, TokenType.EOF):
|
|
41
|
+
body.append(self._stmt())
|
|
42
|
+
self._eat(TokenType.RBRACE)
|
|
43
|
+
return Function(name, params, body)
|
|
44
|
+
|
|
45
|
+
def _stmt(self):
|
|
46
|
+
t = self._cur()
|
|
47
|
+
if t.type == TokenType.LET: return self._let()
|
|
48
|
+
if t.type == TokenType.RETURN: return self._ret()
|
|
49
|
+
if t.type == TokenType.IF: return self._if()
|
|
50
|
+
e = self._expr()
|
|
51
|
+
if self._cur().type == TokenType.SEMICOLON: self._adv()
|
|
52
|
+
return ExpressionStatement(e)
|
|
53
|
+
|
|
54
|
+
def _let(self):
|
|
55
|
+
self._eat(TokenType.LET)
|
|
56
|
+
name = self._eat(TokenType.IDENTIFIER).value
|
|
57
|
+
self._eat(TokenType.ASSIGN)
|
|
58
|
+
val = self._expr()
|
|
59
|
+
self._eat(TokenType.SEMICOLON)
|
|
60
|
+
return LetStatement(name, val)
|
|
61
|
+
|
|
62
|
+
def _ret(self):
|
|
63
|
+
self._eat(TokenType.RETURN)
|
|
64
|
+
val = self._expr() if self._cur().type != TokenType.SEMICOLON else None
|
|
65
|
+
self._eat(TokenType.SEMICOLON)
|
|
66
|
+
return ReturnStatement(val)
|
|
67
|
+
|
|
68
|
+
def _if(self):
|
|
69
|
+
self._eat(TokenType.IF)
|
|
70
|
+
cond = self._expr()
|
|
71
|
+
self._eat(TokenType.LBRACE)
|
|
72
|
+
thenb = []
|
|
73
|
+
while self._cur().type not in (TokenType.RBRACE, TokenType.EOF): thenb.append(self._stmt())
|
|
74
|
+
self._eat(TokenType.RBRACE)
|
|
75
|
+
elseb = None
|
|
76
|
+
if self._cur().type == TokenType.ELSE:
|
|
77
|
+
self._adv(); self._eat(TokenType.LBRACE)
|
|
78
|
+
elseb = []
|
|
79
|
+
while self._cur().type not in (TokenType.RBRACE, TokenType.EOF): elseb.append(self._stmt())
|
|
80
|
+
self._eat(TokenType.RBRACE)
|
|
81
|
+
return IfStatement(cond, thenb, elseb)
|
|
82
|
+
|
|
83
|
+
def _expr(self): return self._bin(0)
|
|
84
|
+
|
|
85
|
+
def _bin(self, minp):
|
|
86
|
+
left = self._prim()
|
|
87
|
+
while True:
|
|
88
|
+
t = self._cur()
|
|
89
|
+
prec = 0; op = None
|
|
90
|
+
if t.type == TokenType.STAR: prec,op = 3,'*'
|
|
91
|
+
elif t.type == TokenType.SLASH: prec,op=3,'/'
|
|
92
|
+
elif t.type == TokenType.PLUS: prec,op=2,'+'
|
|
93
|
+
elif t.type == TokenType.MINUS: prec,op=2,'-'
|
|
94
|
+
elif t.type == TokenType.LT: prec,op=1,'<'
|
|
95
|
+
elif t.type == TokenType.GT: prec,op=1,'>'
|
|
96
|
+
elif t.type == TokenType.LTE: prec,op=1,'<='
|
|
97
|
+
elif t.type == TokenType.GTE: prec,op=1,'>='
|
|
98
|
+
elif t.type == TokenType.EQ: prec,op=1,'=='
|
|
99
|
+
elif t.type == TokenType.NEQ: prec,op=1,'!='
|
|
100
|
+
else: break
|
|
101
|
+
if prec < minp: break
|
|
102
|
+
self._adv()
|
|
103
|
+
right = self._bin(prec+1)
|
|
104
|
+
left = BinaryOp(left, op, right)
|
|
105
|
+
return left
|
|
106
|
+
|
|
107
|
+
def _prim(self):
|
|
108
|
+
t = self._adv()
|
|
109
|
+
if t.type == TokenType.NUMBER:
|
|
110
|
+
v = int(t.value) if '.' not in t.value else float(t.value)
|
|
111
|
+
return Literal(v, 'int' if '.' not in t.value else 'float')
|
|
112
|
+
if t.type == TokenType.STRING: return Literal(t.value, 'string')
|
|
113
|
+
if t.type == TokenType.IDENTIFIER:
|
|
114
|
+
if self._cur().type == TokenType.LPAREN:
|
|
115
|
+
self._adv()
|
|
116
|
+
args = []
|
|
117
|
+
if self._cur().type != TokenType.RPAREN:
|
|
118
|
+
while True:
|
|
119
|
+
args.append(self._expr())
|
|
120
|
+
if self._cur().type == TokenType.COMMA: self._adv()
|
|
121
|
+
else: break
|
|
122
|
+
self._eat(TokenType.RPAREN)
|
|
123
|
+
return Call(t.value, args)
|
|
124
|
+
return Identifier(t.value)
|
|
125
|
+
if t.type == TokenType.LPAREN:
|
|
126
|
+
e = self._expr(); self._eat(TokenType.RPAREN); return e
|
|
127
|
+
raise SyntaxError(f"bad primary {t.type.name}")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from typing import List
|
|
2
|
+
from .ast import *
|
|
3
|
+
|
|
4
|
+
class Transpiler:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.indent = 0
|
|
7
|
+
self.out: List[str] = []
|
|
8
|
+
def emit(self, s): self.out.append(" " * self.indent + s)
|
|
9
|
+
|
|
10
|
+
def transpile(self, prog: Program) -> str:
|
|
11
|
+
self.out = []
|
|
12
|
+
self.emit("# Generated by Obsidian v0.0.1")
|
|
13
|
+
self.emit("def println(*args): print(*args)")
|
|
14
|
+
self.emit("")
|
|
15
|
+
for f in prog.functions:
|
|
16
|
+
self._fn(f)
|
|
17
|
+
if any(f.name == "main" for f in prog.functions):
|
|
18
|
+
self.emit("")
|
|
19
|
+
self.emit("if __name__ == '__main__':")
|
|
20
|
+
self.indent += 1
|
|
21
|
+
self.emit("main()")
|
|
22
|
+
self.indent -= 1
|
|
23
|
+
return "\n".join(self.out)
|
|
24
|
+
|
|
25
|
+
def _fn(self, f: Function):
|
|
26
|
+
ps = ", ".join(f.params)
|
|
27
|
+
self.emit(f"def {f.name}({ps}):")
|
|
28
|
+
self.indent += 1
|
|
29
|
+
for s in f.body: self._stmt(s)
|
|
30
|
+
if not f.body: self.emit("pass")
|
|
31
|
+
self.indent -= 1
|
|
32
|
+
self.emit("")
|
|
33
|
+
|
|
34
|
+
def _stmt(self, s: Statement):
|
|
35
|
+
if isinstance(s, LetStatement):
|
|
36
|
+
self.emit(f"{s.name} = {self._expr(s.value)}")
|
|
37
|
+
elif isinstance(s, ReturnStatement):
|
|
38
|
+
self.emit("return " + (self._expr(s.value) if s.value is not None else ""))
|
|
39
|
+
elif isinstance(s, IfStatement):
|
|
40
|
+
self.emit(f"if {self._expr(s.condition)}:")
|
|
41
|
+
self.indent += 1
|
|
42
|
+
for ss in s.then_block: self._stmt(ss)
|
|
43
|
+
self.indent -= 1
|
|
44
|
+
if s.else_block:
|
|
45
|
+
self.emit("else:")
|
|
46
|
+
self.indent += 1
|
|
47
|
+
for ss in s.else_block: self._stmt(ss)
|
|
48
|
+
self.indent -= 1
|
|
49
|
+
elif isinstance(s, ExpressionStatement):
|
|
50
|
+
self.emit(self._expr(s.expression))
|
|
51
|
+
else:
|
|
52
|
+
self.emit("# unknown")
|
|
53
|
+
|
|
54
|
+
def _expr(self, e: Expression) -> str:
|
|
55
|
+
if isinstance(e, Literal):
|
|
56
|
+
return repr(e.value) if e.typ == "string" else str(e.value)
|
|
57
|
+
if isinstance(e, Identifier):
|
|
58
|
+
return e.name
|
|
59
|
+
if isinstance(e, BinaryOp):
|
|
60
|
+
return f"({self._expr(e.left)} {e.op} {self._expr(e.right)})"
|
|
61
|
+
if isinstance(e, Call):
|
|
62
|
+
args = ", ".join(self._expr(a) for a in e.args)
|
|
63
|
+
return f"{e.callee}({args})"
|
|
64
|
+
raise RuntimeError(f"Transpiler: unknown expression {type(e)}")
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: obsidian-lang
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Obsidian is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate.
|
|
5
|
+
Author: Obsidian Language Team
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/example/obsidian-lang
|
|
8
|
+
Project-URL: Repository, https://github.com/example/obsidian-lang
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/example/obsidian-lang/issues
|
|
10
|
+
Keywords: programming-language,compiler,transpiler,rust-like,toy-language
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
21
|
+
Classifier: Topic :: Software Development :: Interpreters
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Obsidian Language v0.0.1
|
|
28
|
+
|
|
29
|
+
**Obsidian** is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate.
|
|
30
|
+
|
|
31
|
+
This is a toy implementation (v0.0.1) written in Python. It features a lexer, parser, AST, and a transpiler to Python (which simulates "compilation" by generating and executing equivalent Python code). Published on PyPI as `obsidian-lang`.
|
|
32
|
+
|
|
33
|
+
## Features (v0.0.1)
|
|
34
|
+
- Minimalist syntax inspired by Rust
|
|
35
|
+
- Variables with `let`
|
|
36
|
+
- Functions with `fn`
|
|
37
|
+
- Basic expressions + comparison operators (< > <= >= == !=)
|
|
38
|
+
- Control flow with `if` / `else`
|
|
39
|
+
- Function calls (including built-in `println`)
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
```bash
|
|
43
|
+
pip install obsidian-lang
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Usage
|
|
47
|
+
```bash
|
|
48
|
+
# After pip install obsidian-lang
|
|
49
|
+
obsidian examples/hello.obs --show-code
|
|
50
|
+
|
|
51
|
+
# Or from source (development)
|
|
52
|
+
PYTHONPATH=. python -m obsidian.main examples/hello.obs --show-code
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Example (hello.obs)
|
|
56
|
+
```
|
|
57
|
+
fn main() {
|
|
58
|
+
let x = 5;
|
|
59
|
+
let y = x + 3;
|
|
60
|
+
println(y);
|
|
61
|
+
println("Hello from Obsidian!");
|
|
62
|
+
}
|
|
63
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
obsidian/__init__.py
|
|
5
|
+
obsidian/ast.py
|
|
6
|
+
obsidian/lexer.py
|
|
7
|
+
obsidian/main.py
|
|
8
|
+
obsidian/parser.py
|
|
9
|
+
obsidian/transpiler.py
|
|
10
|
+
obsidian_lang.egg-info/PKG-INFO
|
|
11
|
+
obsidian_lang.egg-info/SOURCES.txt
|
|
12
|
+
obsidian_lang.egg-info/dependency_links.txt
|
|
13
|
+
obsidian_lang.egg-info/entry_points.txt
|
|
14
|
+
obsidian_lang.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
obsidian
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "obsidian-lang"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name = "Obsidian Language Team" },
|
|
10
|
+
]
|
|
11
|
+
description = "Obsidian is a compiled programming language that combines memory safety, zero-cost abstractions, and minimalist syntax. Built for developers who want the speed of Rust without the boilerplate."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.8"
|
|
14
|
+
license = { text = "MIT" }
|
|
15
|
+
keywords = ["programming-language", "compiler", "transpiler", "rust-like", "toy-language"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.8",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Software Development :: Compilers",
|
|
27
|
+
"Topic :: Software Development :: Interpreters",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Homepage = "https://github.com/example/obsidian-lang"
|
|
32
|
+
Repository = "https://github.com/example/obsidian-lang"
|
|
33
|
+
"Bug Tracker" = "https://github.com/example/obsidian-lang/issues"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
obsidian = "obsidian.main:main"
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["obsidian*"]
|
|
41
|
+
exclude = ["examples*", "tests*"]
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.package-data]
|
|
44
|
+
obsidian = ["*.py"]
|