diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83e4929..d41278c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,4 +16,6 @@ jobs: - run: python -m pip install -e ".[dev]" - run: python -m pytest - uses: dtolnay/rust-toolchain@stable - - run: cargo test --manifest-path native/Cargo.toml \ No newline at end of file + - run: cargo fmt --manifest-path native/Cargo.toml -- --check + - run: cargo clippy --manifest-path native/Cargo.toml --all-targets -- -D warnings + - run: cargo test --manifest-path native/Cargo.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8421b9f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,101 @@ +# Contributing to AXIOM + +## Development branch + +Active implementation work uses: + +```text +local/continue-project +``` + +Do not develop directly on `main` for this implementation program. + +Remote pushes require explicit authorization. + +## Source of truth + +Use the following order when making implementation decisions: + +1. `docs/axiom-spec-0.1.md` +2. `docs/axiom-language.md` +3. `docs/architecture.md` +4. `docs/self-hosting.md` +5. `docs/version-roadmap.md` +6. `ROADMAP.md` +7. `Documento sin título.txt` for the wider AXIOM SYSTEMS program. + +The master roadmap is read-only for this repository work. + +## Implementation rule + +Do not preserve historical behavior merely because the prototype already implements it. + +When historical behavior conflicts with the canonical AXIOM 0.1 design, migrate the implementation toward the specification and add a conformance test for the intended behavior. + +## Milestones + +Prefer small, meaningful commits. + +A milestone should normally contain: + +- implementation; +- tests; +- documentation; +- reproducibility evidence where relevant. + +Avoid mixing unrelated refactors with language-semantic changes. + +## Validation + +Before a milestone is considered complete, run the relevant checks: + +```bash +python -m pytest +cargo fmt --manifest-path native/Cargo.toml -- --check +cargo clippy --manifest-path native/Cargo.toml --all-targets -- -D warnings +cargo test --manifest-path native/Cargo.toml +git diff --check +``` + +Use additional conformance tests for canonical AXIOM 0.1 syntax and semantics as they become available. + +## Self-hosting discipline + +The historical Python and Rust implementations are bootstrap infrastructure. + +Do not create new permanent compiler architecture that depends on either implementation unless the dependency is explicitly documented as a temporary bootstrap boundary. + +Every removed dependency must be replaced by an implementation that is tested and reproducible. + +## Documentation + +Documentation is part of the implementation. + +When a semantic decision changes: + +1. update the canonical specification; +2. update the language documentation; +3. update the relevant architecture or roadmap document; +4. add or update executable conformance tests. + +## Code style + +Keep compiler layers explicit and small. + +Prefer deterministic behavior. + +Diagnostics should explain the semantic problem and identify the source location. + +Do not silently guess ambiguous programmer intent. + +## Pull requests and integration + +Before merging a branch into `main`, verify: + +- the implementation matches the canonical specification; +- tests pass; +- generated artifacts are reproducible where required; +- documentation is current; +- the master roadmap was not modified unintentionally. + +No push is implied by local completion. diff --git a/README.md b/README.md index 5bf59e5..f490738 100644 --- a/README.md +++ b/README.md @@ -1,114 +1,134 @@ # AXIOM SYSTEMS -AXIOM SYSTEMS es un lenguaje y ecosistema tecnológico construido por fases. -El repositorio contiene dos implementaciones coordinadas: +AXIOM is a universal, general-purpose programming language and technology ecosystem designed around a small visible syntax and a deep semantic model. -- Un **bootstrap en Python**, usado como referencia rápida de comportamiento. -- Un **backend nativo en Rust**, usado para el compilador, la VM y los artefactos ejecutables. +> Natural in expression, formal in meaning. -La estrategia actual es pequeña y deliberada: cada capacidad debe quedar -probada en el bootstrap, portada a Rust y comprobada también desde el CLI antes -de abrir la siguiente capa del lenguaje. +This repository is the implementation workspace for AXIOM. The repository is currently in the transition from a historical prototype to the implementation of the definitive AXIOM language defined by the AXIOM 0.1 specification. -## Estado actual +## Current status -El núcleo del lenguaje ya tiene un pipeline funcional de extremo a extremo: +The conceptual architecture of AXIOM 0.1 is closed. The next work is implementation: progressively replacing the historical Python and Rust language implementations with AXIOM-owned compiler, runtime, tooling, and eventually operating-system foundations. + +The repository therefore contains two layers: + +1. **Transition infrastructure** — the existing Python and Rust implementations, native artifact format, runtime, package workflow, and bootstrap experiments. +2. **AXIOM 0.1 implementation program** — the new canonical language frontend, semantic model, compiler architecture, self-hosting path, ABI, runtime, and toolchain described in the current specification. + +The transition is intentional. Historical code is not the definition of the language; the specification is. + +## Target architecture + +The long-term compiler path is: ```text -AXIOM (.ax) - -> lexer - -> parser - -> análisis semántico - -> IR - -> runtime / VM - -> salida o artefacto .axm +AXIOM source + ↓ +AXIOM compiler + ↓ +semantic model / plan + ↓ +AXIOM IR + ↓ +native backend / runtime + ↓ +target machine ``` -El bootstrap Python genera IR portable `.air`. El backend Rust genera artefactos -`.axm`, los serializa, los deserializa y los ejecuta posteriormente. +The self-hosting end state is: -## Capacidades implementadas +```text +AXIOM source + ↓ +AXIOM compiler written in AXIOM + ↓ +AXIOM compiler + ↓ +AXIOM compiles AXIOM +``` + +Python and Rust are temporary implementation and bootstrap technologies. They may remain during migration, but neither is part of the language definition or the intended final compiler foundation. + +## AXIOM 0.1 language direction -### Sintaxis y tipos +AXIOM 0.1 is built around: -- Funciones, parámetros, retornos y llamadas. -- Tipos `Int`, `Float`, `Bool` y `String`. -- Arrays y arrays anidados: `Int[]`, `Float[][]`, etc. -- Structs con campos de tipos base, literales y acceso de lectura mediante punto. -- Comentarios de línea con `//`. -- Literales agrupados con paréntesis. +- entities, relations, resources, capabilities, effects, restrictions, contracts, knowledge, and execution plans; +- indentation-based blocks; +- a deliberately small visible syntax; +- semantic dependency declarations rather than textual inclusion as the core module model; +- automatic resource-flow analysis; +- information-flow analysis; +- inferred effects and capability provenance; +- compiler-directed concurrency; +- explicit hard constraints and soft preferences; +- first-class units, time, determinism, verification, and failure semantics; +- native access from application level to systems and hardware programming; +- a stable AXIOM ABI and FFI model; +- deterministic builds and supply-chain metadata; +- progressive self-hosting and toolchain independence. -Ejemplo de structs: +Canonical examples: ```axiom -struct Point { - x: Int - y: Int -} - -fn main() { - let point: Point = Point { x: 10, y: 20 } - print(point.x) -} -``` +mostrar("Hello, AXIOM") -### Expresiones +usuario: + nombre: texto + edad: entero -- Aritmética: `+`, `-`, `*`, `/` y `%`. -- Operaciones homogéneas para `Int` y `Float`. -- Concatenación de `String` con `+`. -- Comparaciones: `>`, `>=`, `<`, `<=`, `==` y `!=`. -- Lógica booleana: `!`, `&&` y `||`. -- Cortocircuito lógico: el operando derecho solo se evalúa cuando es necesario. -- Negación unaria de enteros y flotantes. -- Indexación y asignación de arrays, incluida la indexación encadenada. +alex: usuario + nombre = "Alex" + edad = 25 -### Control de flujo +sumar(a, b): + a + b -- `if`, `else if` y `else`. -- `while`. -- `for` con inicializador, condición y actualización. -- `break` y `continue` con validación semántica de contexto. -- Actualización correcta del `for` después de `continue`. +decidir edad >= 18: + verdadero: + mostrar("Adult") + falso: + mostrar("Minor") +``` -### Herramientas +The canonical syntax is being implemented incrementally. It must not be confused with the historical prototype syntax still present in compatibility tests and bootstrap code. -- CLI Python: `check`, `build`, `run` y `doctor`. -- CLI Rust: `check`, `build` y `run`. -- Build y ejecución de artefactos `.axm`. -- Errores léxicos y sintácticos con línea y columna en el backend nativo. -- Errores semánticos para tipos incompatibles, retornos inválidos y control de flujo fuera de bucles. +## Repository structure -## Arquitectura del repositorio +- `docs/axiom-spec-0.1.md` — definitive conceptual specification for AXIOM 0.1. +- `docs/axiom-language.md` — canonical syntax and executable-language design status. +- `docs/architecture.md` — compiler, semantic, IR, runtime, and toolchain architecture. +- `docs/self-hosting.md` — migration from bootstrap infrastructure to AXIOM self-hosting. +- `docs/implementation-status.md` — factual implementation status and remaining boundaries. +- `docs/version-roadmap.md` — AXIOM 0.1 → 1.0 implementation sequence. +- `ROADMAP.md` — language/toolchain milestone history and current implementation roadmap. +- `bootstrap/` — historical and transitional self-hosting experiments. +- `src/axiom/` — historical Python implementation and transition frontend. +- `native/` — historical/current Rust-native transition implementation. +- `tests/` — Python compatibility and subsystem tests. +- `native/tests/` — native and end-to-end transition tests. +- `examples/` — examples retained for the currently implemented transition syntax. -### Bootstrap Python +The master roadmap remains in `Documento sin título.txt`. It is intentionally not edited by implementation work in this repository. -La implementación de referencia está en [src/axiom](src/axiom): +## Historical milestones -- [lexer.py](src/axiom/lexer.py): tokens, palabras reservadas y posiciones. -- [parser.py](src/axiom/parser.py): parser recursivo descendente. -- [ast.py](src/axiom/ast.py): nodos del lenguaje. -- [semantic.py](src/axiom/semantic.py): tipos y reglas semánticas. -- [ir.py](src/axiom/ir.py): lowering al IR `.air`. -- [runtime.py](src/axiom/runtime.py): ejecución del IR. -- [cli.py](src/axiom/cli.py): herramienta Python. +The original language/toolchain roadmap phases 1–18 have been completed as documented historical milestones. They established: -### Backend Rust +- a working prototype compiler pipeline; +- AXIOM IR and executable artifacts; +- a native Rust toolchain; +- a bootstrap seed written in AXIOM; +- a self-hosted bootstrap frontend and compiler driver; +- a native project/package workflow. -La implementación nativa está en [native](native): +These milestones are foundations for the next stage, not proof that the definitive AXIOM 0.1 language is already self-hosted. -- [native/src/lib.rs](native/src/lib.rs): lexer y tokens. -- [native/src/parser.rs](native/src/parser.rs): AST y parser nativo. -- [native/src/semantic.rs](native/src/semantic.rs): análisis semántico. -- [native/src/ir.rs](native/src/ir.rs): instrucciones de VM y lowering. -- [native/src/runtime.rs](native/src/runtime.rs): runtime interpretado de referencia para pruebas. -- [native/src/vm.rs](native/src/vm.rs): VM, artefactos `.axm`, serialización y ejecución. -- [native/src/main.rs](native/src/main.rs): CLI nativo. -- [native/tests/cli_errors.rs](native/tests/cli_errors.rs): pruebas CLI end-to-end. +## Development -## Comandos de desarrollo +Until the AXIOM compiler can build itself, the transition toolchain is used to validate the new implementation. -### Python +Python: ```bash python3 -m venv .venv @@ -117,63 +137,44 @@ python -m pip install -e '.[dev]' python -m pytest ``` -Uso rápido: - -```bash -axiom doctor -axiom check examples/hello.ax -axiom build examples/hello.ax -axiom run examples/hello.ax -``` - -### Rust +Rust transition toolchain: ```bash cargo fmt --manifest-path native/Cargo.toml -- --check cargo clippy --manifest-path native/Cargo.toml --all-targets -- -D warnings cargo test --manifest-path native/Cargo.toml -cargo build --manifest-path native/Cargo.toml ``` -Uso del CLI nativo: +Do not treat these commands as the final AXIOM developer workflow. They are temporary transition infrastructure. -```bash -native/target/debug/axiom check examples/hello.ax -native/target/debug/axiom build examples/hello.ax -native/target/debug/axiom run examples/hello.ax -``` +## Independence policy -## Validación actual +The project is moving through explicit implementation boundaries: -Estado comprobado en el último ciclo de desarrollo: - -- Rust: **52 pruebas unitarias** pasando. -- Rust CLI: **13 pruebas end-to-end** pasando. -- Python: **45 pruebas** pasando. -- Clippy con `-D warnings`: limpio. -- Formato Rust y `git diff --check`: limpios. - -## Próximos movimientos - -Orden recomendado para continuar el proyecto: +```text +Historical Python/Rust implementation + ↓ +AXIOM 0.1 frontend in AXIOM + ↓ +AXIOM 0.1 semantic compiler in AXIOM + ↓ +AXIOM compiler compiles itself + ↓ +AXIOM-owned runtime and native backend + ↓ +AXIOM-owned toolchain + ↓ +AXIOM 1.0 +``` -1. Completar la semántica de structs en Rust y Python: validar campos desconocidos, duplicados y tipos de cada campo de forma uniforme. -2. Añadir asignación de campos, por ejemplo `point.x = 42`, y extenderla al IR y a los artefactos. -3. Añadir pruebas CLI de errores semánticos con mensajes y ubicaciones precisas. -4. Consolidar la paridad de `.air` y `.axm`, incluyendo una especificación versionada de ambos formatos. -5. Diseñar y portar la siguiente capa de tipos ricos: enums, optionals y manejo explícito de errores. -6. Automatizar formato, Clippy y las suites Python/Rust en CI. +A dependency is removed only after the replacement is implemented, tested, reproducible, and able to rebuild the required component. -El roadmap de visión completa está en [Documento sin título.txt](Documento%20sin%20t%C3%ADtulo.txt). La documentación específica de sintaxis está en [docs/axiom-language.md](docs/axiom-language.md). +## Branch policy -## Principios del proyecto +Active implementation work is performed on the dedicated branch `local/continue-project`. The `main` branch is not modified by this development workflow. -- Construir la mínima pieza que habilite la siguiente. -- Mantener Python como referencia rápida y Rust como implementación nativa. -- No aceptar una feature sin pruebas de parser, semántica y ejecución cuando aplique. -- Mantener interfaces pequeñas, artefactos reproducibles y errores comprensibles. -- Portar capacidades por paridad, no por acumulación de implementaciones divergentes. +No remote push is performed unless explicitly requested. -## Licencia +## License -AXIOM SYSTEMS se distribuye bajo la licencia [MIT](LICENSE). \ No newline at end of file +AXIOM SYSTEMS is distributed under the [MIT License](LICENSE). diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..117a5e1 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,162 @@ +# AXIOM Language Roadmap + +This roadmap tracks the implementation of the AXIOM language and its toolchain. The master AXIOM SYSTEMS roadmap is maintained separately in `Documento sin título.txt` and is intentionally not modified here. + +## Current position + +**Historical language/toolchain phases 1–18: complete.** + +Those phases established a functional prototype, native transition toolchain, bootstrap, self-hosted bootstrap frontend, and native project workflow. + +**Current program: AXIOM 0.1 implementation and progressive self-hosting.** + +The conceptual design is closed. Implementation now proceeds against `docs/axiom-spec-0.1.md`. + +## Completed foundation + +- [x] Phase 1 — language fundamentals. +- [x] Phase 2 — semantic system. +- [x] Phase 3 — control flow. +- [x] Phase 4 — data types and collections. +- [x] Phase 5 — structs and data model. +- [x] Phase 6 — AXIOM IR. +- [x] Phase 7 — runtime and VM. +- [x] Phase 8 — CLI and basic toolchain. +- [x] Phase 9 — standard library foundation. +- [x] Phase 10 — package manager foundation. +- [x] Phase 11 — project workflow. +- [x] Phase 12 — AXIOM Studio foundation. +- [x] Phase 13 — AXIOM Core. +- [x] Phase 14 — AXIOM Engine foundation. +- [x] Phase 15 — native backend. +- [x] Phase 16 — bootstrap seed. +- [x] Phase 17 — self-hosted bootstrap frontend and compiler driver. +- [x] Phase 18 — native ecosystem workflow. + +## Definitive AXIOM implementation program + +The following stages supersede the old assumption that the historical prototype itself is the final language. + +### AXIOM 0.1 — canonical language foundation + +Goal: make the new semantic architecture executable. + +- [ ] Canonical lexer in AXIOM. +- [ ] Canonical parser in AXIOM. +- [ ] Canonical AST/entity model. +- [ ] Canonical names, scopes, modules, and semantic resolution. +- [ ] Core type system and unit/dimensionality model. +- [ ] Canonical Resource Flow model. +- [ ] Effects and capability analysis. +- [ ] Information Flow foundations. +- [ ] Canonical diagnostics. +- [ ] AXIOM 0.1 conformance tests. + +### AXIOM 0.2 — compiler semantics + +Goal: compile meaningful AXIOM 0.1 programs. + +- [ ] Semantic planning pipeline. +- [ ] Contracts and verification model. +- [ ] Control flow and block-as-value semantics. +- [ ] Collections, entities, functions, modules, and packages. +- [ ] Deterministic lowering to AXIOM IR. +- [ ] Explainable compiler decisions. +- [ ] Canonical standard-library interface. + +### AXIOM 0.3 — self-hosted compiler + +Goal: AXIOM becomes the primary implementation language of its compiler. + +- [ ] Compiler frontend implemented in AXIOM. +- [ ] Semantic compiler implemented in AXIOM. +- [ ] IR generator implemented in AXIOM. +- [ ] Compiler driver implemented in AXIOM. +- [ ] AXIOM compiler builds AXIOM compiler source. +- [ ] Reproducible self-build comparison. + +### AXIOM 0.4 — native execution foundation + +Goal: reduce and then remove the historical Rust compiler/runtime boundary. + +- [ ] AXIOM-owned runtime interfaces. +- [ ] AXIOM ABI implementation. +- [ ] Native backend contracts. +- [ ] Resource and memory execution model. +- [ ] Platform target model. +- [ ] Native bootstrap path that does not require Python. + +### AXIOM 0.5 — autonomous toolchain + +Goal: the AXIOM compiler, package manager, formatter, diagnostics, tests, and build workflow are implemented from AXIOM-owned components. + +- [ ] Compiler CLI. +- [ ] Package resolution. +- [ ] Reproducible builds. +- [ ] Formatter. +- [ ] Linter. +- [ ] Test runner. +- [ ] Explainability tooling. +- [ ] Migration tooling. +- [ ] Documentation generation. + +### AXIOM 0.6–0.9 — production language maturation + +These releases are implementation milestones, not promises of specific calendar dates. They will stabilize: + +- [ ] concurrency and execution planning; +- [ ] real-time and determinism; +- [ ] fault tolerance and recovery; +- [ ] distribution; +- [ ] metaprogramming and reflection; +- [ ] FFI and ABI; +- [ ] security and information-flow enforcement; +- [ ] advanced optimization; +- [ ] cross-platform targets; +- [ ] standard-library breadth; +- [ ] ecosystem and package verification. + +Each feature is accepted only when its semantics, implementation, tests, diagnostics, and documentation agree. + +### AXIOM 1.0 — independence milestone + +AXIOM 1.0 is the point at which the language and its core toolchain can be built and maintained by AXIOM itself without requiring Python or Rust as the language implementation foundation. + +The 1.0 gate requires evidence, not intent: + +- [ ] AXIOM compiler builds from AXIOM source. +- [ ] Compiler self-build is reproducible. +- [ ] Core runtime is AXIOM-owned or uses only explicitly defined external ABI boundaries. +- [ ] Core toolchain no longer depends on the historical Python implementation. +- [ ] Core toolchain no longer depends on Rust as its language implementation. +- [ ] Canonical AXIOM 1.0 syntax and semantics have conformance tests. +- [ ] Native targets are validated. +- [ ] Package and build identities are reproducible. +- [ ] Migration and compatibility rules are documented. +- [ ] The compiler can explain its relevant resource, effect, capability, and optimization decisions. + +## Independence boundary + +The migration is: + +```text +Python/Rust prototype + ↓ +AXIOM-owned frontend + ↓ +AXIOM-owned compiler + ↓ +AXIOM compiles AXIOM + ↓ +AXIOM-owned runtime/backend + ↓ +AXIOM-owned toolchain + ↓ +AXIOM 1.0 +``` + +Rust and Python may remain as temporary bootstrap hosts until each boundary has a tested replacement. Removing them prematurely would make the bootstrap less reliable rather than more independent. + +## Master roadmap relationship + +`Documento sin título.txt` remains the authoritative master roadmap for the wider AXIOM SYSTEMS program. This file does not declare any master-roadmap phase complete merely because a language/toolchain milestone is complete. diff --git a/bootstrap/README.md b/bootstrap/README.md new file mode 100644 index 0000000..b496deb --- /dev/null +++ b/bootstrap/README.md @@ -0,0 +1,53 @@ +# AXIOM Bootstrap + +The `bootstrap/` directory contains the first AXIOM-written compiler experiments. + +## Historical pipeline + +The bootstrap sequence established: + +```text +source + ↓ +AXIOM-written lexer + ↓ +AXIOM-written parser + ↓ +AXIOM-written AST normalization + ↓ +AXIOM-written semantic bootstrap + ↓ +AXIOM-written compiler driver + ↓ +AXIOM_IR_V1 / AXIOM_ARTIFACT_V1 +``` + +This proved that AXIOM can participate in its own bootstrap. + +## What this does prove + +- AXIOM can express compiler-oriented data structures. +- AXIOM can process source text. +- AXIOM can build structured representations. +- AXIOM can perform deterministic semantic checks. +- AXIOM can generate deterministic intermediate output. + +## What this does not prove + +It does not yet prove definitive AXIOM 0.1 self-hosting. + +The bootstrap programs target the historical transition syntax and artifact/runtime boundary. The new AXIOM 0.1 compiler must be rebuilt around the canonical semantic model described in `docs/axiom-spec-0.1.md`. + +## Verification + +```bash +cargo test --manifest-path native/Cargo.toml +``` + +The historical integration tests verify deterministic bootstrap behavior. + +## Next role + +The bootstrap directory will become the bridge from the historical compiler to the canonical AXIOM compiler. + +As AXIOM 0.1 implementation advances, components should move from bootstrap experiments into the canonical compiler architecture. Once a component is replaced and verified, the historical version can be retired. diff --git a/bootstrap/ast.ax b/bootstrap/ast.ax new file mode 100644 index 0000000..342d73b --- /dev/null +++ b/bootstrap/ast.ax @@ -0,0 +1,67 @@ +fn split_node(line: String) -> String[] { + return split(line, "|") +} + +fn trim_left(value: String) -> String { + let index: Int = 0 + while index < len(value) && char_at(value, index) == " " { + index = index + 1 + } + let result: String = "" + while index < len(value) { + result = result + char_at(value, index) + index = index + 1 + } + return result +} + +fn node_kind(line: String) -> String { + let value: String = trim_left(line) + let parts: String[] = split(value, ": ") + return parts[0] +} + +fn node_value(line: String) -> String { + let value: String = trim_left(line) + let parts: String[] = split(value, ": ") + if len(parts) > 1 { + return parts[1] + } + return "" +} + +fn node_depth(line: String) -> Int { + let depth: Int = 0 + let index: Int = 0 + while index < len(line) && char_at(line, index) == " " { + depth = depth + 1 + index = index + 1 + } + return depth / 2 +} + +fn normalize_line(line: String, depth: Int) -> String { + return "NODE|" + int_to_string(depth) + "|" + node_kind(line) + "|" + node_value(line) +} + +fn build(source: String) { + print("AXIOM_AST_V1") + let lines: String[] = split_lines(source) + let index: Int = 0 + let depth: Int = 0 + while index < len(lines) { + let line: String = lines[index] + if len(line) > 0 { + let kind: String = node_kind(line) + if kind == "ScopeExit" || kind == "ExprExit" { depth = depth - 1 } + print(normalize_line(line, depth)) + if kind == "ScopeEnter" || kind == "ExprEnter" { depth = depth + 1 } + } + index = index + 1 + } +} + +fn main() { + let source: String = read_file("bootstrap/parser_ast.txt") + build(source) +} diff --git a/bootstrap/compiler.ax b/bootstrap/compiler.ax new file mode 100644 index 0000000..ce69b67 --- /dev/null +++ b/bootstrap/compiler.ax @@ -0,0 +1,74 @@ +fn field(parts: String[], index: Int) -> String { + if index < len(parts) { return parts[index] } + return "" +} + +fn kind(line: String) -> String { + return field(split(line, "|"), 2) +} + +fn value(line: String) -> String { + return field(split(line, "|"), 3) +} + +fn quote(value: String) -> String { + let result: String = value + result = result + "" + return result +} + +fn emit_function(name: String) { + print("FUNCTION|" + name) +} + +fn emit_parameter(name: String) { + print("PARAM|" + name) +} + +fn emit_literal(kind_name: String, literal: String) { + if kind_name == "Integer" { print("PUSH_INT|" + literal) } + if kind_name == "Float" { print("PUSH_FLOAT|" + literal) } + if kind_name == "Boolean" { print("PUSH_BOOL|" + literal) } + if kind_name == "String" { print("PUSH_STRING|" + quote(literal)) } +} + +fn emit_binary(operator: String) { + print("BINARY|" + operator) +} + +fn emit_node(line: String) { + let node_kind: String = kind(line) + let node_value: String = value(line) + if node_kind == "Function" { emit_function(node_value) } + if node_kind == "Parameter" { emit_parameter(node_value) } + if node_kind == "Integer" || node_kind == "Float" || node_kind == "Boolean" || node_kind == "String" { + emit_literal(node_kind, node_value) + } + if node_kind == "Variable" { print("LOAD|" + node_value) } + if node_kind == "Binary" { emit_binary(node_value) } + if node_kind == "Unary" { print("UNARY|" + node_value) } + if node_kind == "Call" { print("CALL|" + node_value) } + if node_kind == "Index" { print("INDEX") } + if node_kind == "Field" { print("FIELD|" + node_value) } + if node_kind == "Return" { print("RETURN") } + if node_kind == "Break" { print("BREAK") } + if node_kind == "Continue" { print("CONTINUE") } + if node_kind == "ExprEnter" { print("EXPR_ENTER") } + if node_kind == "ExprExit" { print("EXPR_EXIT") } +} + +fn compile_ast(source: String) { + let lines: String[] = split_lines(source) + print("AXIOM_IR_V1") + let index: Int = 0 + while index < len(lines) { + let line: String = lines[index] + if len(line) > 0 && line != "AXIOM_AST_V1" { emit_node(line) } + index = index + 1 + } +} + +fn main() { + let source: String = read_file("bootstrap/parser_ast.txt") + compile_ast(source) +} diff --git a/bootstrap/lexer.ax b/bootstrap/lexer.ax new file mode 100644 index 0000000..bab7356 --- /dev/null +++ b/bootstrap/lexer.ax @@ -0,0 +1,129 @@ +fn kind(word: String) -> String { + if word == "fn" { return "FN" } + if word == "print" { return "PRINT" } + if word == "let" { return "LET" } + if word == "return" { return "RETURN" } + if word == "if" { return "IF" } + if word == "else" { return "ELSE" } + if word == "while" { return "WHILE" } + if word == "for" { return "FOR" } + if word == "break" { return "BREAK" } + if word == "continue" { return "CONTINUE" } + if word == "true" { return "TRUE" } + if word == "false" { return "FALSE" } + if word == "struct" { return "STRUCT" } + if word == "module" { return "MODULE" } + if word == "import" { return "IMPORT" } + return "IDENT" +} + +fn emit(kind_name: String, text: String) { + print(kind_name + "|" + text) +} + +fn is_letter(code: Int) -> Bool { + return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || code == 95 +} + +fn is_digit(code: Int) -> Bool { + return code >= 48 && code <= 57 +} + +fn lex(source: String) { + let index: Int = 0 + while index < len(source) { + let current: String = char_at(source, index) + let code: Int = char_code(current) + if code == 32 || code == 9 || code == 10 || code == 13 { + index = index + 1 + } else if is_letter(code) { + let word: String = "" + while index < len(source) && (is_letter(char_code(char_at(source, index))) || is_digit(char_code(char_at(source, index)))) { + word = word + char_at(source, index) + index = index + 1 + } + emit(kind(word), word) + } else if is_digit(code) { + let number: String = "" + while index < len(source) && is_digit(char_code(char_at(source, index))) { + number = number + char_at(source, index) + index = index + 1 + } + if index < len(source) && char_at(source, index) == "." && index + 1 < len(source) && is_digit(char_code(char_at(source, index + 1))) { + number = number + "." + index = index + 1 + while index < len(source) && is_digit(char_code(char_at(source, index))) { + number = number + char_at(source, index) + index = index + 1 + } + emit("FLOAT", number) + } else { + emit("INTEGER", number) + } + } else if code == 34 { + index = index + 1 + let value: String = "" + while index < len(source) && char_code(char_at(source, index)) != 34 { + value = value + char_at(source, index) + index = index + 1 + } + if index < len(source) { index = index + 1 } + emit("STRING", value) + } else if current == "(" { emit("LPAREN", current); index = index + 1 + } else if current == ")" { emit("RPAREN", current); index = index + 1 + } else if current == "{" { emit("LBRACE", current); index = index + 1 + } else if current == "}" { emit("RBRACE", current); index = index + 1 + } else if current == "[" { emit("LBRACKET", current); index = index + 1 + } else if current == "]" { emit("RBRACKET", current); index = index + 1 + } else if current == "," { emit("COMMA", current); index = index + 1 + } else if current == ":" { emit("COLON", current); index = index + 1 + } else if current == ";" { emit("SEMICOLON", current); index = index + 1 + } else if current == "." { emit("DOT", current); index = index + 1 + } else if current == "+" { emit("PLUS", current); index = index + 1 + } else if current == "-" { + if index + 1 < len(source) && char_at(source, index + 1) == ">" { + emit("ARROW", "->"); index = index + 2 + } else { + emit("MINUS", current); index = index + 1 + } + } else if current == "*" { emit("STAR", current); index = index + 1 + } else if current == "/" { emit("SLASH", current); index = index + 1 + } else if current == "%" { emit("PERCENT", current); index = index + 1 + } else if current == "!" { + if index + 1 < len(source) && char_at(source, index + 1) == "=" { + emit("NOT_EQUAL", "!="); index = index + 2 + } else { + emit("NOT", current); index = index + 1 + } + } else if current == "=" { + if index + 1 < len(source) && char_at(source, index + 1) == "=" { + emit("EQUAL", "=="); index = index + 2 + } else { + emit("ASSIGN", current); index = index + 1 + } + } else if current == ">" { + if index + 1 < len(source) && char_at(source, index + 1) == "=" { + emit("GREATER_EQUAL", ">="); index = index + 2 + } else { + emit("GREATER", current); index = index + 1 + } + } else if current == "<" { + if index + 1 < len(source) && char_at(source, index + 1) == "=" { + emit("LESS_EQUAL", "<="); index = index + 2 + } else { + emit("LESS", current); index = index + 1 + } + } else if current == "&" && index + 1 < len(source) && char_at(source, index + 1) == "&" { + emit("AND", "&&"); index = index + 2 + } else if current == "|" && index + 1 < len(source) && char_at(source, index + 1) == "|" { + emit("OR", "||"); index = index + 2 + } else { + emit("CHAR", current); index = index + 1 + } + } +} + +fn main() { + let source: String = read_file("bootstrap/lexer_fixture.ax") + lex(source) +} diff --git a/bootstrap/lexer_fixture.ax b/bootstrap/lexer_fixture.ax new file mode 100644 index 0000000..f525160 --- /dev/null +++ b/bootstrap/lexer_fixture.ax @@ -0,0 +1,24 @@ +struct Point { + x: Int + y: Int +} + +fn calculate(a: Int, b: Float) -> Float { + let values: Int[] = [1, 2, 3] + let point: Point = Point { x: 10, y: 20 } + let total: Int = values[0] + point.x + if total >= 10 && true { + print("ok") + } else { + print("no") + } + while total < 20 { + print(total) + break + } + return b + 1.0 +} + +fn main() { + calculate(4, 2.5) +} \ No newline at end of file diff --git a/bootstrap/parser.ax b/bootstrap/parser.ax new file mode 100644 index 0000000..e391956 --- /dev/null +++ b/bootstrap/parser.ax @@ -0,0 +1,383 @@ +fn token_kind(token: String) -> String { + let parts: String[] = split(token, "|") + if len(parts) == 0 { return "" } + return parts[0] +} + +fn token_text(token: String) -> String { + let parts: String[] = split(token, "|") + if len(parts) > 1 { return parts[1] } + return "" +} + +fn is_kind(tokens: String[], index: Int, kind: String) -> Bool { + return index < len(tokens) && token_kind(tokens[index]) == kind +} + +fn print_error(message: String) { + print("PARSE_ERROR: " + message) +} + +fn parse_type(tokens: String[], index: Int) -> Int { + if index >= len(tokens) || token_kind(tokens[index]) != "IDENT" { + print_error("expected type") + return index + } + print("Type: " + token_text(tokens[index])) + index = index + 1 + while is_kind(tokens, index, "LBRACKET") { + if !is_kind(tokens, index + 1, "RBRACKET") { + print_error("expected ']' in array type") + return index + } + print(" Array") + index = index + 2 + } + return index +} + +fn parse_expression(tokens: String[], index: Int) -> Int { + print("ExprEnter") + index = parse_or(tokens, index) + print("ExprExit") + return index +} + +fn parse_or(tokens: String[], index: Int) -> Int { + index = parse_and(tokens, index) + while is_kind(tokens, index, "OR") { + print("Binary: ||") + index = parse_and(tokens, index + 1) + } + return index +} + +fn parse_and(tokens: String[], index: Int) -> Int { + index = parse_equality(tokens, index) + while is_kind(tokens, index, "AND") { + print("Binary: &&") + index = parse_equality(tokens, index + 1) + } + return index +} + +fn parse_equality(tokens: String[], index: Int) -> Int { + index = parse_comparison(tokens, index) + while is_kind(tokens, index, "EQUAL") || is_kind(tokens, index, "NOT_EQUAL") { + if token_kind(tokens[index]) == "EQUAL" { print("Binary: ==") } else { print("Binary: !=") } + index = parse_comparison(tokens, index + 1) + } + return index +} + +fn parse_comparison(tokens: String[], index: Int) -> Int { + index = parse_term(tokens, index) + while is_kind(tokens, index, "GREATER") || is_kind(tokens, index, "GREATER_EQUAL") || is_kind(tokens, index, "LESS") || is_kind(tokens, index, "LESS_EQUAL") { + if token_kind(tokens[index]) == "GREATER" { print("Binary: >") } + if token_kind(tokens[index]) == "GREATER_EQUAL" { print("Binary: >=") } + if token_kind(tokens[index]) == "LESS" { print("Binary: <") } + if token_kind(tokens[index]) == "LESS_EQUAL" { print("Binary: <=") } + index = parse_term(tokens, index + 1) + } + return index +} + +fn parse_term(tokens: String[], index: Int) -> Int { + index = parse_factor(tokens, index) + while is_kind(tokens, index, "PLUS") || is_kind(tokens, index, "MINUS") { + if token_kind(tokens[index]) == "PLUS" { print("Binary: +") } else { print("Binary: -") } + index = parse_factor(tokens, index + 1) + } + return index +} + +fn parse_factor(tokens: String[], index: Int) -> Int { + index = parse_unary(tokens, index) + while is_kind(tokens, index, "STAR") || is_kind(tokens, index, "SLASH") || is_kind(tokens, index, "PERCENT") { + if token_kind(tokens[index]) == "STAR" { print("Binary: *") } + if token_kind(tokens[index]) == "SLASH" { print("Binary: /") } + if token_kind(tokens[index]) == "PERCENT" { print("Binary: %") } + index = parse_unary(tokens, index + 1) + } + return index +} + +fn parse_unary(tokens: String[], index: Int) -> Int { + if is_kind(tokens, index, "NOT") || is_kind(tokens, index, "MINUS") { + if token_kind(tokens[index]) == "NOT" { print("Unary: !") } else { print("Unary: -") } + return parse_unary(tokens, index + 1) + } + return parse_postfix(tokens, index) +} + +fn parse_postfix(tokens: String[], index: Int) -> Int { + index = parse_primary(tokens, index) + while index < len(tokens) { + if is_kind(tokens, index, "LBRACKET") { + print("Index") + index = parse_expression(tokens, index + 1) + if is_kind(tokens, index, "RBRACKET") { index = index + 1 } else { print_error("expected ']'") } + } else if is_kind(tokens, index, "DOT") { + if is_kind(tokens, index + 1, "IDENT") { + print("Field: " + token_text(tokens[index + 1])) + index = index + 2 + } else { + print_error("expected field name") + return index + } + } else { + return index + } + } + return index +} + +fn parse_arguments(tokens: String[], index: Int) -> Int { + if is_kind(tokens, index, "RPAREN") { return index + 1 } + index = parse_expression(tokens, index) + while is_kind(tokens, index, "COMMA") { + index = parse_expression(tokens, index + 1) + } + if is_kind(tokens, index, "RPAREN") { return index + 1 } + print_error("expected ')'") + return index +} +fn parse_primary(tokens: String[], index: Int) -> Int { + if index >= len(tokens) { print_error("unexpected end of input"); return index } + let kind: String = token_kind(tokens[index]) + if kind == "PRINT" { + print("Call: print") + return parse_arguments(tokens, index + 2) + } + if kind == "STRING" { + print("String: " + token_text(tokens[index])) + return index + 1 + } + if kind == "INTEGER" { + print("Integer: " + token_text(tokens[index])) + return index + 1 + } + if kind == "FLOAT" { + print("Float: " + token_text(tokens[index])) + return index + 1 + } + if kind == "TRUE" || kind == "FALSE" { + print("Boolean: " + token_text(tokens[index])) + return index + 1 + } + if kind == "LBRACKET" { + print("Array") + index = index + 1 + if is_kind(tokens, index, "RBRACKET") { return index + 1 } + index = parse_expression(tokens, index) + while is_kind(tokens, index, "COMMA") { + index = parse_expression(tokens, index + 1) + } + if is_kind(tokens, index, "RBRACKET") { return index + 1 } + print_error("expected ']'") + return index + } + if kind == "LPAREN" { + index = parse_expression(tokens, index + 1) + if is_kind(tokens, index, "RPAREN") { return index + 1 } + print_error("expected ')'") + return index + } + if kind == "IDENT" { + let name: String = token_text(tokens[index]) + if is_kind(tokens, index + 1, "LPAREN") { + print("Call: " + name) + return parse_arguments(tokens, index + 2) + } + if is_kind(tokens, index + 1, "LBRACE") { + print("StructLiteral: " + name) + index = index + 2 + while index < len(tokens) && !is_kind(tokens, index, "RBRACE") { + if is_kind(tokens, index, "IDENT") { + print(" Field: " + token_text(tokens[index])) + index = index + 1 + if is_kind(tokens, index, "COLON") { + index = parse_expression(tokens, index + 1) + } else { + print_error("expected ':'") + return index + } + } else { + print_error("expected struct field") + return index + } + if is_kind(tokens, index, "COMMA") { index = index + 1 } + } + if is_kind(tokens, index, "RBRACE") { return index + 1 } + print_error("expected '}'") + return index + } + print("Variable: " + name) + return index + 1 + } + print_error("unexpected token " + kind) + return index + 1 +} + +fn parse_statement(tokens: String[], index: Int) -> Int { + if is_kind(tokens, index, "LET") { + print("Let") + index = index + 1 + if !is_kind(tokens, index, "IDENT") { print_error("expected variable name"); return index } + print(" Name: " + token_text(tokens[index])) + index = index + 1 + if is_kind(tokens, index, "COLON") { + index = parse_type(tokens, index + 1) + } + if is_kind(tokens, index, "ASSIGN") { + index = parse_expression(tokens, index + 1) + } else { + print_error("expected '='") + } + return index + } + if is_kind(tokens, index, "RETURN") { + print("Return") + index = index + 1 + if !is_kind(tokens, index, "RBRACE") && !is_kind(tokens, index, "SEMICOLON") { + return parse_expression(tokens, index) + } + return index + } + if is_kind(tokens, index, "BREAK") { print("Break"); return index + 1 } + if is_kind(tokens, index, "CONTINUE") { print("Continue"); return index + 1 } + if is_kind(tokens, index, "IF") { + print("If") + index = parse_expression(tokens, index + 1) + index = parse_block(tokens, index) + if is_kind(tokens, index, "ELSE") { + print("Else") + index = index + 1 + index = parse_block(tokens, index) + } + return index + } + if is_kind(tokens, index, "WHILE") { + print("While") + index = parse_expression(tokens, index + 1) + return parse_block(tokens, index) + } + if is_kind(tokens, index, "FOR") { + print("For") + return parse_block(tokens, index + 1) + } + let start: Int = index + index = parse_expression(tokens, index) + if is_kind(tokens, index, "ASSIGN") { + print("Assign") + return parse_expression(tokens, index + 1) + } + if (index == start) { return index + 1 } + return index +} +fn parse_block(tokens: String[], index: Int) -> Int { + if !is_kind(tokens, index, "LBRACE") { + print_error("expected '{'") + return index + } + print("ScopeEnter") + index = index + 1 + while index < len(tokens) && !is_kind(tokens, index, "RBRACE") { + if is_kind(tokens, index, "SEMICOLON") { + index = index + 1 + } else { + index = parse_statement(tokens, index) + } + } + if is_kind(tokens, index, "RBRACE") { + print("ScopeExit") + return index + 1 + } + print_error("expected '}'") + return index +} + +fn parse_function(tokens: String[], index: Int) -> Int { + if !is_kind(tokens, index, "FN") { + print_error("expected 'fn'") + return index + 1 + } + index = index + 1 + if !is_kind(tokens, index, "IDENT") { + print_error("expected function name") + return index + } + print("Function: " + token_text(tokens[index])) + index = index + 1 + if !is_kind(tokens, index, "LPAREN") { + print_error("expected '('") + return index + } + index = index + 1 + while index < len(tokens) && !is_kind(tokens, index, "RPAREN") { + if is_kind(tokens, index, "IDENT") { + print(" Parameter: " + token_text(tokens[index])) + index = index + 1 + if is_kind(tokens, index, "COLON") { + index = parse_type(tokens, index + 1) + } + if is_kind(tokens, index, "COMMA") { index = index + 1 } + } else { + print_error("expected parameter") + return index + } + } + if is_kind(tokens, index, "RPAREN") { index = index + 1 } else { print_error("expected ')'"); return index } + if is_kind(tokens, index, "ARROW") { + print("ReturnType") + index = parse_type(tokens, index + 1) + } + return parse_block(tokens, index) +} + +fn parse_struct(tokens: String[], index: Int) -> Int { + print("Struct: " + token_text(tokens[index + 1])) + index = index + 3 + while index < len(tokens) && !is_kind(tokens, index, "RBRACE") { + if is_kind(tokens, index, "IDENT") { + print(" Field: " + token_text(tokens[index])) + index = index + 1 + if is_kind(tokens, index, "COLON") { + index = parse_type(tokens, index + 1) + } + } else { + index = index + 1 + } + if is_kind(tokens, index, "COMMA") || is_kind(tokens, index, "SEMICOLON") { index = index + 1 } + } + if is_kind(tokens, index, "RBRACE") { return index + 1 } + print_error("expected '}'") + return index +} + +fn parse_program(tokens: String[]) { + let index: Int = 0 + print("Program") + while index < len(tokens) { + let kind: String = token_kind(tokens[index]) + if kind == "" || kind == "SEMICOLON" { + index = index + 1 + } else if kind == "FN" { + index = parse_function(tokens, index) + } else if kind == "STRUCT" { + index = parse_struct(tokens, index) + } else if kind == "MODULE" || kind == "IMPORT" { + print(kind + ": " + token_text(tokens[index + 1])) + index = index + 2 + } else { + print_error("unexpected top-level " + kind) + index = index + 1 + } + } +} + +fn main() { + let source: String = read_file("bootstrap/lexer_tokens.txt") + let tokens: String[] = split_lines(source) + parse_program(tokens) +} diff --git a/bootstrap/seed.ax b/bootstrap/seed.ax new file mode 100644 index 0000000..6b978b3 --- /dev/null +++ b/bootstrap/seed.ax @@ -0,0 +1,36 @@ +struct Token { + kind: Int + text: String +} + +struct Program { + message: String +} + +fn parse_print(tokens: Token[]) -> Program { + let message: String = tokens[3].text + return Program { message: message } +} + +fn emit_program(program: Program) { + print("AXIOM_ARTIFACT_V1") + print("FUNCTION:main") + print("PARAMS:0") + print("INSTR:PushString:" + program.message + "|Print") +} + +fn compile(tokens: Token[]) { + let program: Program = parse_print(tokens) + emit_program(program) +} + +fn main() { + let tokens: Token[] = [ + Token { kind: 1 text: "print" }, + Token { kind: 2 text: "(" }, + Token { kind: 3 text: "string" }, + Token { kind: 4 text: "Hello from bootstrap" }, + Token { kind: 5 text: ")" } + ] + compile(tokens) +} diff --git a/bootstrap/semantic.ax b/bootstrap/semantic.ax new file mode 100644 index 0000000..96d156c --- /dev/null +++ b/bootstrap/semantic.ax @@ -0,0 +1,157 @@ +fn field(parts: String[], index: Int) -> String { + if index < len(parts) { return parts[index] } + return "" +} + +fn is_builtin(name: String) -> Int { + if name == "print" || name == "split" || name == "split_lines" || name == "read_file" || name == "write_file" || name == "char_at" || name == "char_code" || name == "int_to_string" || name == "len" || name == "abs" || name == "min" || name == "max" { + return 1 + } + return 0 +} + +fn has_named_node(lines: String[], kind_name: String, name: String) -> Int { + let index: Int = 0 + let found: Int = 0 + while index < len(lines) { + let marker: String = "|" + kind_name + "|" + name + if len(split(lines[index], marker)) > 1 { found = 1 } + index = index + 1 + } + return found +} + +fn literal_type(kind: String) -> String { + if kind == "Integer" { return "Int" } + if kind == "Float" { return "Float" } + if kind == "String" { return "String" } + if kind == "Boolean" { return "Bool" } + return "" +} + +fn check_binary_literals(lines: String[]) -> Int { + let index: Int = 0 + let valid: Int = 1 + while index + 2 < len(lines) { + let current: String[] = split(lines[index], "|") + let next: String[] = split(lines[index + 1], "|") + let after: String[] = split(lines[index + 2], "|") + if len(current) >= 3 && len(next) >= 3 && len(after) >= 3 { + let left_type: String = literal_type(field(current, 2)) + let operator_kind: String = field(next, 3) + let right_type: String = literal_type(field(after, 2)) + if left_type != "" && right_type != "" && operator_kind != "" { + if operator_kind == "+" || operator_kind == "-" || operator_kind == "*" || operator_kind == "/" || operator_kind == "%" { + if left_type != right_type || (left_type != "Int" && left_type != "Float" && left_type != "String") { + print("SEMANTIC_ERROR|binary operator '" + operator_kind + "' has incompatible operand types") + valid = 0 + } + } + if operator_kind == ">" || operator_kind == ">=" || operator_kind == "<" || operator_kind == "<=" { + if left_type != right_type || (left_type != "Int" && left_type != "Float") { + print("SEMANTIC_ERROR|comparison requires matching numeric operands") + valid = 0 + } + } + } + } + index = index + 1 + } + return valid +} + +fn resolve_variable(lines: String[], start: Int, name: String) -> Int { + let index: Int = start - 1 + let blocked: Int = 0 + while index >= 0 { + let line: String = lines[index] + let parts: String[] = split(line, "|") + if len(parts) >= 3 { + let kind: String = field(parts, 2) + if kind == "ScopeExit" { + blocked = blocked + 1 + } + if kind == "ScopeEnter" { + if blocked > 0 { blocked = blocked - 1 } + } + if blocked == 0 { + let name_marker: String = "|Name|" + name + let parameter_marker: String = "|Parameter|" + name + if len(split(line, name_marker)) > 1 || len(split(line, parameter_marker)) > 1 { + return 1 + } + } + if kind == "Function" && blocked == 0 { return 0 } + } + index = index - 1 + } + return 0 +} + +fn analyze(source: String) { + let lines: String[] = split_lines(source) + let struct_count: Int = 0 + let function_count: Int = 0 + let has_main: Int = 0 + let valid: Int = 1 + let index: Int = 1 + + while index < len(lines) { + let line: String = lines[index] + if len(line) > 0 { + let parts: String[] = split(line, "|") + if len(parts) < 3 { + print("SEMANTIC_ERROR|malformed AST node") + valid = 0 + } else { + let kind: String = field(parts, 2) + let value: String = field(parts, 3) + if kind == "Struct" { + struct_count = struct_count + 1 + if value == "" { print("SEMANTIC_ERROR|struct requires a name"); valid = 0 } + } + if kind == "Function" { + function_count = function_count + 1 + if value == "" { print("SEMANTIC_ERROR|function requires a name"); valid = 0 } + if value == "main" { has_main = 1 } + } + if kind == "Variable" { + if resolve_variable(lines, index, value) == 0 { + print("SEMANTIC_ERROR|unknown variable '" + value + "'") + valid = 0 + } + } + if kind == "Call" { + if is_builtin(value) == 0 && has_named_node(lines, "Function", value) == 0 { + print("SEMANTIC_ERROR|unknown function '" + value + "'") + valid = 0 + } + } + if kind == "StructLiteral" { + if has_named_node(lines, "Struct", value) == 0 { + print("SEMANTIC_ERROR|unknown struct '" + value + "'") + valid = 0 + } + } + } + } + index = index + 1 + } + + if check_binary_literals(lines) == 0 { valid = 0 } + + if has_main == 0 { + print("SEMANTIC_ERROR|program must define 'main'") + valid = 0 + } + if valid == 1 { + print("SEMANTIC_OK") + print("STRUCTS|" + int_to_string(struct_count)) + print("FUNCTIONS|" + int_to_string(function_count)) + } +} + +fn main() { + let source: String = read_file("bootstrap/ast_output.txt") + analyze(source) +} diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d542c7e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,224 @@ +# AXIOM Architecture + +## Purpose + +This document defines the implementation architecture for the definitive AXIOM language. + +The architecture is organized around semantic understanding rather than around the historical Python/Rust parser split. + +## Compiler model + +The canonical pipeline is: + +```text +Source + ↓ +Lexical representation + ↓ +Syntax / entity representation + ↓ +Semantic model + ↓ +Resource Flow + ↓ +Information Flow + ↓ +Effects and capabilities + ↓ +Contracts and restrictions + ↓ +Execution plan + ↓ +AXIOM IR + ↓ +Target backend + ↓ +Runtime / machine +``` + +The compiler may fuse or reorder internal passes for performance, but the semantic boundaries remain explicit. + +## Core semantic model + +The internal model contains: + +- VALUE; +- DATA; +- RESOURCE; +- CAPABILITY; +- KNOWLEDGE. + +Important relations include: + +- produce; +- consume; +- transform; +- read; +- write; +- depend; +- call; +- communicate; +- control; +- create; +- release; +- contain; +- measure. + +The compiler uses these relations to derive effects, resource lifetimes, parallelism, restrictions, and target decisions. + +## Frontend + +The frontend will be implemented progressively in AXIOM itself. + +Responsibilities: + +- lexical analysis; +- canonical syntax parsing; +- entity/form construction; +- source locations; +- syntax diagnostics; +- module discovery; +- semantic name resolution. + +The current Python `new_parser.py` is a migration bridge only. It must not become the permanent compiler frontend. + +## Semantic compiler + +The semantic compiler resolves: + +- types; +- relations; +- modules; +- packages; +- capabilities; +- effects; +- resource flow; +- information flow; +- restrictions; +- contracts; +- execution feasibility. + +Ambiguity is diagnosed rather than guessed. + +## Intermediate representation + +AXIOM IR is the stable internal boundary between semantic compilation and execution backends. + +The IR should represent semantic intent sufficiently to support: + +- native CPU execution; +- SIMD; +- GPU/NPU/QPU targets; +- embedded targets; +- distributed execution; +- real-time planning; +- verification metadata; +- explainability. + +The historical `AXIOM_ARTIFACT_V1` format remains a transition artifact. It is not the final AXIOM ABI. + +## Runtime + +The runtime owns operations that cannot be reduced entirely to static compilation: + +- resource acquisition/release; +- scheduling; +- device interaction; +- I/O; +- process and machine boundaries; +- recovery; +- dynamic runtime state. + +The runtime must expose explicit semantic boundaries rather than leaking host-language assumptions into AXIOM. + +## Native backend + +The native backend maps AXIOM execution plans to target machines. + +The target model is: + +```text +AXIOM program + ↓ +semantic execution plan + ↓ +target-specific lowering + ↓ +machine code / runtime calls +``` + +Rust currently hosts this transition layer. The final architecture is not Rust-specific. + +## ABI and FFI + +AXIOM will define an ABI covering: + +- value representation; +- layout; +- alignment; +- calling convention; +- structures; +- errors; +- resource handles; +- capabilities; +- versions; +- compatibility. + +Interop targets include C, C++, Rust, Python, .NET, WebAssembly, native APIs, and hardware interfaces. + +WebAssembly is an interoperability target, not the language foundation. + +## Toolchain + +The final toolchain will be implemented from AXIOM-owned components: + +```text +axiom new +axiom check +axiom build +axiom run +axiom test +axiom package +axiom add +axiom install +axiom native-build +axiom explain +axiom format +axiom lint +axiom migrate +``` + +The historical native Rust CLI is a transition implementation of part of this surface. + +## Build identity and reproducibility + +A build is identified by: + +```text +project + dependencies + compiler + configuration + target +``` + +Build outputs must be deterministic where the target and declared inputs permit determinism. + +Supply-chain metadata includes origin, identity, version, integrity, dependencies, capabilities, effects, supported platforms, ABI information, and reproducibility information. + +## Explainability boundary + +Compiler explanations are generated from compiler state: + +- semantic facts; +- inferred effects; +- resource graph; +- capability graph; +- execution plan; +- target constraints; +- optimization decisions; +- verification evidence. + +No external service is required. + +## Migration rule + +When the current implementation conflicts with the canonical specification, implementation is migrated toward the specification. + +The specification is not rewritten merely to preserve an accidental property of the prototype. diff --git a/docs/axiom-language.md b/docs/axiom-language.md index b357ae4..d31fce0 100644 --- a/docs/axiom-language.md +++ b/docs/axiom-language.md @@ -1,108 +1,342 @@ # AXIOM Language -## Primer corte de sintaxis +This document describes the canonical AXIOM 0.1 language direction. It replaces the historical prototype syntax as the design reference. -La primera versión del lenguaje mantiene una sintaxis pequeña y legible: +## Design principle + +> Natural in expression, formal in meaning. + +AXIOM keeps the visible syntax small while making the compiler semantically powerful. The language should be approachable on the first day without creating a separate "simple" and "advanced" language. + +## Canonical syntax + +The visible kernel is intentionally small: + +```text +name: +name = expression +name(...) +decidir ... +repetir ... +cuando ... +usar ... +ofrecer ... +necesita ... +puede ... +prefiere ... +restringir ... +demostrar ... +modo ... +``` + +Blocks use indentation. Braces are not normal block delimiters. + +### Values and calls ```axiom -fn main() { - print("Hello AXIOM") -} +mostrar("Hello, AXIOM") + +nombre = "Alex" +edad = 25 +activo = verdadero ``` -Los bucles `for` aceptan inicializador, condición y actualización. `continue` -ejecuta la actualización antes de la siguiente iteración y `break` termina el -bucle actual: +### Entities and forms ```axiom -fn main() { - for (let i: Int = 0; i < 5; i = i + 1) { - if i == 2 { - continue - } - if i == 4 { - break - } - print(i) - } -} -``` - -Este corte reconoce funciones, identificadores, cadenas, paréntesis, llaves y -el punto y coma opcional. La gramática crecerá junto con el parser y cada -decisión estable se documentará aquí. - -El bootstrap en Python es temporal: sirve para validar rápidamente el diseño. -El lenguaje AXIOM no queda ligado a Python y su compilador podrá tener un -backend nativo cuando las mediciones y la estabilidad del lenguaje lo -justifiquen. - -## Decisiones iniciales - -- Los archivos fuente usan la extensión `.ax`. -- Las posiciones de los tokens son base 1 para línea y columna. -- Los errores léxicos incluyen el carácter y su posición. -- Las palabras reservadas se distinguen de los identificadores durante el - lexing. -- Los literales `Float` usan punto decimal, por ejemplo `1.5`. -- La sintaxis prioriza una curva de aprendizaje corta sin renunciar a - compilación nativa y ejecución rápida. - -## Estructuras - -La sintaxis propuesta para estructuras mantiene los campos explícitos y el -acceso directo mediante punto: +usuario: + nombre: texto + edad: entero + +alex: usuario + nombre = "Alex" + edad = 25 + +mostrar(alex.nombre) +``` + +A definition describes a form/type. An entity declaration instantiates or specializes that form. + +### Functions + +Functions do not require `fn`, `def`, or `function`. ```axiom -struct Point { - x: Int - y: Int -} +sumar(a, b): + a + b +``` + +A block normally produces the value of its final expression. Explicit return syntax is not the foundation of the language. + +### Decisions -fn main() { - let point: Point = Point { x: 10, y: 20 } - print(point.x) -} +```axiom +decidir edad >= 18: + verdadero: + mostrar("Adult") + falso: + mostrar("Minor") ``` -Los nombres de campo deben ser únicos dentro de la estructura. La primera -implementación soportará campos con tipos base y acceso de lectura; la -reasignación de campos y los tipos compuestos se añadirán después. +### Repetition -## Estructuras de control +```axiom +repetir usuario en usuarios: + mostrar(usuario.nombre) +``` -La rama condicional ya admite encadenamientos de `else if`: +The exact repetition grammar is being stabilized by executable conformance tests. + +### Events ```axiom -fn main() { - let value: Int = 2 - if value == 1 { - print("one") - } else if value == 2 { - print("two") - } else { - print("other") - } -} +cuando temperatura > 90 °C: + detener(motor) ``` -Los tipos `Int` y `Float` admiten operaciones aritméticas y comparaciones entre -operandos del mismo tipo: +Time and periodic execution are semantic properties rather than a second asynchronous language. + +### Dependencies and capabilities ```axiom -fn main() { - let total: Float = 1.5 + 2.5 - print(total / 2.0) -} +usar matematicas + +necesita: + filesystem.read + network + +puede: + camera.read ``` -El operador `%` calcula el resto de una división entre enteros: +`usar` declares semantic dependency. `necesita` declares a requirement. `puede` describes an allowed capability boundary. + +### Preferences and restrictions ```axiom -fn main() { - print(17 % 5) -} +prefiere: + GPU + energía = baja + +restringir: + dispositivo = GPU + precisión = fp32 + ejecución = secuencial ``` -Los operadores `&&` y `||` usan cortocircuito: el operando derecho solo se -evalúa cuando el izquierdo no determina el resultado. \ No newline at end of file +Preferences guide planning. Restrictions are mandatory constraints. + +### Contracts + +```axiom +demostrar: + temperatura < 100 °C +``` + +Verification results are classified by evidence strength. Tests are not silently promoted to mathematical proof. + +## Types + +AXIOM treats a type as semantic knowledge about an entity, including: + +- meaning; +- structure; +- identity; +- capabilities; +- relations; +- valid states; +- restrictions; +- contracts; +- representation; +- verifiable knowledge. + +Core types include: + +```text +booleano +entero +real +decimal +texto +carácter +bytes +tiempo +duración +unidad +``` + +Scientific types include rational, complex, vector, matrix, tensor, interval, probability, and algebraic-number families. + +Units and dimensionality are first-class semantic information: + +```axiom +masa = 5 kg +velocidad = 9.81 m/s² +temperatura = 25 °C +periodo = 10 ms +``` + +The compiler must distinguish exact and approximate numerical operations. + +There is no universal `null` model. Absence and failure are semantic possibilities that must be represented explicitly by the relevant type or contract. + +## Resource Flow + +AXIOM does not make ownership, borrowing, or lifetime syntax the foundation. + +The compiler asks: + +1. What must exist? +2. Who uses it? +3. What operations are performed? +4. What dependencies exist? +5. When is the last consumer finished? +6. Which representation satisfies the constraints at acceptable cost? + +This applies to memory, files, sockets, GPU buffers, devices, processes, handles, connections, energy, time, and information. + +The compiler may select copy, alias, view, move, sharing, local storage, GPU storage, or reuse when semantics permit it. + +Explicit restrictions take priority. + +## Effects, capabilities, and resources + +These are three separate concepts: + +```text +RESOURCE = what an operation acts on +CAPABILITY = what it is allowed to do +EFFECT = what it actually does +``` + +Effects are inferred and propagated through calls. + +Examples include: + +```text +filesystem.read +filesystem.write +network.read +network.write +camera.read +gpu +clock.read +secure_randomness +hardware.raw_memory +``` + +Capability provenance must remain explainable. + +## Information Flow + +Resource Flow and Information Flow are separate analyses. + +A program may have permission to write to a network resource without being allowed to send every category of information through that resource. + +Information classifications and policies can be domain-defined. The compiler must identify prohibited flows and show their provenance. + +## Concurrency + +Concurrency is an execution-plan property. + +The compiler can identify independent operations, choose parallel execution, select CPU/SIMD/GPU/NPU/QPU/process/machine execution, and insert only the synchronization required by the semantic dependencies. + +The language does not require `async/await`, `thread`, `mutex`, or `spawn` as foundational syntax. + +Sequential execution remains expressible: + +```axiom +restringir: + ejecución = secuencial +``` + +## Time and real-time behavior + +Time distinguishes: + +- instant; +- duration; +- clock; +- period; +- deadline; +- latency; +- jitter; +- priority. + +Hard guarantees are restrictions. Soft goals are preferences. + +```axiom +modo: + tiempo_real + +restringir: + periodo = 10 ms + deadline = 5 ms +``` + +The compiler must report whether a timing requirement is proven, possible, not demonstrable, or impossible under known constraints. + +## Fault tolerance + +Failure is a normal semantic possibility. + +Relevant actions include retry, timeout, fallback, restart, isolate, degrade, replace, propagate, recover, and cancel. + +Retries must respect idempotency and effect contracts. + +## Distribution + +Local and distributed execution use the same semantic model. + +The compiler and runtime must account for: + +- nodes; +- network communication; +- serialization; +- latency; +- partitions; +- consistency; +- atomicity; +- idempotency; +- retries; +- timeouts; +- recovery. + +RPC, queues, sockets, and shared memory are implementation mechanisms rather than separate language models. + +## Modules and packages + +The conceptual hierarchy is: + +```text +PROJECT → PACKAGE → MODULE → SEMANTIC SPACE → ENTITY +``` + +`usar` expresses semantic dependency. `ofrecer` defines the public semantic interface. + +Name resolution must be deterministic and must not search arbitrary project state merely to guess what the programmer meant. + +## Explainability + +The compiler should be able to explain: + +- what a program does; +- resources it uses; +- capabilities it needs; +- effects it produces; +- concurrency decisions; +- memory placement; +- optimization decisions; +- proof/evidence status; +- target selection; +- restrictions that prevented alternatives. + +This explanation is a compiler capability, not an external AI service. + +## Compatibility with the historical prototype + +The repository still contains the earlier `fn/struct/if` syntax because the historical implementation is being used as transition infrastructure. + +That syntax is **not** the canonical AXIOM 0.1 language definition. + +The canonical implementation is introduced incrementally and will be accepted through executable conformance tests before historical syntax is retired. diff --git a/docs/axiom-spec-0.1.md b/docs/axiom-spec-0.1.md new file mode 100644 index 0000000..ea6a0d6 --- /dev/null +++ b/docs/axiom-spec-0.1.md @@ -0,0 +1,724 @@ +# AXIOM 0.1 — Consolidated Semantic Specification + +**Status:** conceptual design closed; implementation is now underway. + +This document is the semantic reference for the definitive AXIOM 0.1 implementation. It describes the language that the repository is moving toward, not every behavior of the historical prototype. + +## 1. Identity + +AXIOM is a universal, general-purpose, compiled programming language designed to express human intent naturally while preserving precise formal meaning. + +> Natural in expression, formal in meaning. + +AXIOM is one language across application, systems, embedded, scientific, robotics, industrial, distributed, hardware, and future operating-system domains. + +The compiler must not depend on an AI model, network connection, external server, account, or remote service to compile, verify, optimize, or explain a program. + +## 2. Semantic pipeline + +The core semantic model is: + +```text +INTENTION + ↓ +ENTITIES + ↓ +RELATIONS + ↓ +RESOURCES + ↓ +CAPABILITIES + ↓ +EFFECTS + ↓ +RESTRICTIONS + ↓ +CONTRACTS + ↓ +PLAN + ↓ +EXECUTION +``` + +Internal entity categories include: + +- VALUE +- DATA +- RESOURCE +- CAPABILITY +- KNOWLEDGE + +Core relations include: + +- PRODUCE +- CONSUME +- TRANSFORM +- READ +- WRITE +- DEPEND +- CALL +- COMMUNICATE +- CONTROL +- CREATE +- RELEASE +- CONTAIN +- MEASURE + +The compiler may use richer internal relations as implementation requires. + +## 3. Canonical syntax + +The visible language kernel is intentionally small: + +```text +name: +name = expression +name(...) +decidir ... +repetir ... +cuando ... +usar ... +ofrecer ... +necesita ... +puede ... +prefiere ... +restringir ... +demostrar ... +modo ... +``` + +Blocks use indentation. Braces are not normal block delimiters. + +Canonical examples: + +```axiom +mostrar("Hello, AXIOM") + +nombre = "Alex" +edad = 25 +activo = verdadero + +usuario: + nombre: texto + edad: entero + +alex: usuario + nombre = "Alex" + edad = 25 + +sumar(a, b): + a + b +``` + +Decision: + +```axiom +decidir edad >= 18: + verdadero: + mostrar("Adult") + falso: + mostrar("Minor") +``` + +Repetition: + +```axiom +repetir usuario en usuarios: + mostrar(usuario.nombre) +``` + +Event: + +```axiom +cuando temperatura > 90 °C: + detener(motor) +``` + +The exact grammar is frozen only through executable conformance tests. The canonical design takes precedence over accidental syntax inherited from the historical prototype. + +## 4. Blocks and values + +Blocks are semantic values. + +A block normally produces the value of its final expression. Explicit return syntax is not a foundational language construct. + +This allows the same block model to describe definitions, decisions, event responses, computations, resource plans, and other semantic constructs. + +## 5. Types and forms + +A type is formal knowledge about an entity. + +Conceptually: + +```text +TYPE = + meaning + + structure + + identity + + capabilities + + relations + + valid states + + restrictions + + contracts + + representation + + verifiable knowledge +``` + +The language distinguishes: + +```text +TYPE + ↓ +FORM + ↓ +VALUE + ↓ +ENTITY +``` + +Core types include: + +- boolean; +- integer; +- real; +- decimal; +- text; +- character; +- bytes; +- unit; +- time; +- duration. + +Scientific types include: + +- rational; +- complex; +- vector; +- matrix; +- tensor; +- interval; +- probability; +- algebraic numbers. + +Units and dimensionality are first-class. + +```axiom +mass = 5 kg +speed = 9.81 m/s² +temperature = 25 °C +period = 10 ms +``` + +Exact and approximate mathematics must remain distinguishable. + +There is no universal null model. Absence and failure are semantic possibilities represented by the relevant type or contract. + +## 6. Generalization + +AXIOM does not make angle-bracket generics, inheritance, traits, or interface hierarchies the foundation of generalization. + +Generalization is expressed through: + +- semantic relations; +- conformance; +- required operations; +- restrictions; +- contracts; +- inference; +- specialization. + +An abstraction should express the conditions it needs rather than enumerate every type it knows. + +## 7. Modules and packages + +The project hierarchy is: + +```text +PROJECT → PACKAGE → MODULE → SEMANTIC SPACE → ENTITY +``` + +`usar` expresses semantic dependency. + +`ofrecer` defines a public semantic interface. + +Name resolution is deterministic. Resolution must not search arbitrary project state merely to guess programmer intent. + +Resolution should consider, where relevant: + +1. local definitions; +2. parameters and local entities; +3. current semantic space; +4. explicitly used modules; +5. official library entities; +6. declared package dependencies. + +Ambiguity is an error, not an invitation to guess. + +## 8. Resource Flow + +Resource Flow replaces ownership/borrowing/lifetime syntax as the primary language model for resource management. + +The compiler asks: + +- what must exist; +- who needs it; +- for how long; +- what operations are allowed; +- what dependencies exist; +- when the last consumer finishes; +- what representation satisfies the constraints. + +Conceptually: + +```text +RESOURCE + ↓ +CREATION + ↓ +USES + ↓ +DEPENDENCIES + ↓ +LAST CONSUMER + ↓ +RELEASE / REUSE +``` + +The model applies to: + +- memory; +- files; +- sockets; +- GPU/NPU/QPU buffers; +- devices; +- processes; +- threads; +- handles; +- connections; +- information; +- energy; +- time. + +The compiler may choose copying, aliasing, views, moves, sharing, local storage, device storage, reuse, or other representations when semantics permit it. + +Explicit restrictions have priority. + +## 9. Information Flow + +Resource Flow and Information Flow are separate analyses. + +Information may carry classifications or policies. + +A capability to write to a network does not automatically authorize sending every category of information through that network. + +The compiler must be able to identify prohibited flows and explain their provenance. + +## 10. Effects, capabilities, and resources + +These are distinct: + +```text +RESOURCE = what an operation acts on +CAPABILITY = what it is authorized to do +EFFECT = what it actually does +``` + +Effects are inferred and propagated transitively. + +Examples: + +```text +filesystem.read +filesystem.write +network.read +network.write +camera.read +gpu +clock.read +secure_randomness +hardware.raw_memory +``` + +`necesita` declares requirements. + +`puede` declares allowed capabilities. + +`prefiere` declares a soft preference. + +`restringir` declares a hard constraint. + +Capability provenance must be explainable: who introduced it, why it is required, where it is used, and which effects result. + +## 11. Concurrency and execution planning + +Concurrency is a property of the execution plan, not a separate asynchronous language. + +The compiler may: + +- discover independent operations; +- parallelize them; +- choose CPU/SIMD/GPU/NPU/QPU execution; +- distribute work across processes or machines; +- insert required synchronization; +- detect conflicts and races; +- preserve sequential execution when semantics require it. + +Foundational syntax does not require `async`, `await`, `thread`, `mutex`, `lock`, or `spawn`. + +A hard sequential constraint can be expressed semantically: + +```axiom +restringir: + ejecución = secuencial +``` + +## 12. Time and determinism + +Time is an observable resource/effect. + +The model distinguishes: + +- instant; +- duration; +- clock; +- period; +- deadline; +- latency; +- jitter; +- priority. + +Hard guarantees use restrictions. Soft objectives use preferences. + +```axiom +modo: + tiempo_real + +restringir: + periodo = 10 ms + deadline = 5 ms + determinismo = obligatorio +``` + +The compiler must distinguish: + +- guaranteed/proven; +- feasible; +- not demonstrable; +- impossible under known constraints. + +It must never claim a guarantee that it cannot establish. + +## 13. Fault tolerance and recovery + +Failure is a normal semantic possibility. + +The model includes: + +- retry; +- timeout; +- fallback; +- restart; +- isolate; +- degrade; +- replace; +- propagate; +- recover; +- cancel. + +Retries require appropriate idempotency knowledge. The compiler must not blindly repeat operations with non-repeatable effects. + +Resource cleanup and recovery are part of Resource Flow. + +Distributed and hardware failure domains must be expressible. + +## 14. Distribution + +Distribution uses the same semantic model as local execution. + +Additional semantic resources include: + +- nodes; +- network links; +- serialization; +- latency; +- partitions; +- consistency; +- atomicity; +- recovery. + +Contracts can describe: + +- consistency; +- idempotency; +- atomicity; +- timeouts; +- retry safety; +- recovery behavior. + +RPC, queues, sockets, and shared memory are implementation mechanisms, not separate language models. + +## 15. Contracts and verification + +`demostrar` introduces a property that the compiler should analyze. + +Evidence states include: + +- DEMONSTRATED; +- CHECKED; +- MEASURED; +- SIMULATED; +- OBSERVED; +- ASSUMED; +- UNKNOWN; +- REFUTED; +- NOT ANALYZED. + +Tests are execution evidence. They are not silently promoted to mathematical proof. + +Contracts may cover: + +- inputs; +- outputs; +- invariants; +- resource states; +- capabilities; +- effects; +- memory; +- timing; +- energy; +- distribution; +- determinism; +- security. + +Counterexamples should be reported whenever possible. + +Verification evidence should retain relevant hypotheses, compiler version, dependencies, target architecture, and verification method. + +## 16. Metaprogramming and reflection + +Code, types, schemas, contracts, and modules are inspectable entities. + +Capabilities include: + +- type inspection; +- module inspection; +- contract inspection; +- AST transformation; +- code generation; +- schema-to-code generation; +- binding generation; +- documentation generation. + +Metaprogramming uses the same effect, capability, resource, and restriction model as ordinary code. + +## 17. Interoperability and ABI + +AXIOM will define a language-owned ABI. + +The ABI covers: + +- value representation; +- layout; +- alignment; +- calling conventions; +- structures; +- errors; +- resources; +- capabilities; +- versions; +- binary compatibility. + +Interop targets include C, C++, Rust, Python, .NET, WebAssembly, native platform APIs, and hardware interfaces. + +WebAssembly is a target and interoperability boundary, not the foundation of AXIOM. + +## 18. Language evolution + +AXIOM will define: + +- language versions; +- source compatibility; +- semantic compatibility; +- package compatibility; +- ABI compatibility; +- deprecations; +- migration rules. + +The intended migration tool is `axiom migrate`. + +A migration should: + +1. analyze the project; +2. explain the change; +3. prepare a diff; +4. recompile; +5. verify affected behavior; +6. require explicit acceptance before changing source. + +## 19. Ecosystem + +The official standard library is intended to cover: + +- text; +- collections; +- files; +- processes; +- system interfaces; +- networking; +- HTTP/WebSocket/TCP/UDP/DNS/TLS; +- serialization; +- JSON/XML/YAML/CSV/binary formats; +- time; +- mathematics; +- statistics; +- linear algebra; +- simulation; +- cryptography; +- authentication and certificates; +- databases and SQL; +- compression; +- image/audio/video codecs; +- UI; +- 2D/3D graphics; +- GPU and shaders; +- AI/ML and inference; +- testing; +- benchmarking; +- fuzzing; +- observability; +- FFI; +- devices; +- hardware; +- SIMD; +- embedded systems. + +The intended toolchain includes: + +```text +new +check +build +run +test +package +add +install +native-build +explain +format +lint +migrate +``` + +Build identity is: + +```text +project + dependencies + compiler + configuration + target +``` + +Supply-chain metadata should include origin, identity, version, integrity, signature, dependencies, capabilities, effects, supported platforms, ABI information, and reproducibility data. + +## 20. Modes + +Modes are optional, combinable, and extensible. + +Examples: + +- systems; +- embedded; +- real_time; +- scientific; +- high_performance; +- secure; +- critical; +- distributed; +- hardware; +- GPU; +- web; +- mobile; +- quantum. + +A mode changes priorities, constraints, guarantees, and compilation strategies. It does not create a different language. + +## 21. Explainability + +The compiler should explain: + +- what a program does; +- which resources it uses; +- which capabilities it requires; +- which effects it produces; +- what parallelism was discovered; +- where data is placed; +- which optimizations were applied; +- which properties were demonstrated; +- what could not be demonstrated; +- why a CPU/GPU/NPU/QPU target was selected; +- which restrictions prevented alternatives. + +Explainability is a compiler feature. It does not require an external AI service. + +## 22. Hardware and low-level access + +AXIOM must support the complete range from high-level applications to direct systems and hardware programming. + +Conceptually: + +```text +application + ↓ +system + ↓ +hardware + ↓ +memory / CPU / GPU / registers / instructions / interrupts +``` + +High-level abstractions are allowed and encouraged, but they must not create an artificial ceiling that prevents legitimate low-level programming. + +## 23. Identity audit + +AXIOM must not become a superficial variation of another language. + +The following are not foundational pillars: + +- `fn`/`def`/`function`; +- separate class/struct language systems; +- ownership/borrow/lifetime syntax; +- async/await as the concurrency model; +- try/catch as the failure architecture; +- import/include as textual inclusion; +- a fixed if/for/match taxonomy as the semantic foundation; +- an `unsafe` world separate from the rest of the language. + +AXIOM's identity is: + +```text +ENTITY ++ +RELATION ++ +INTENTION ++ +RESOURCE ++ +CAPABILITY ++ +EFFECT ++ +RESTRICTION ++ +KNOWLEDGE ++ +PLAN +``` + +## 24. Implementation authority + +This specification is the architectural reference for AXIOM 0.1. + +When the historical prototype conflicts with this specification, implementation is migrated toward the specification. + +The specification itself should only change when the language design intentionally evolves and the change is recorded through the versioning process. + +The current canonical syntax becomes stable through executable conformance tests, not by preserving accidental behavior of the prototype. diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 0000000..0802df4 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,43 @@ +# AXIOM CLI + +The CLI documented here describes the transition toolchain and the intended future command surface. + +## Current transition commands + +```text +axiom doctor +axiom new +axiom check +axiom build [-o ] +axiom run +axiom test +axiom package [-o ] +axiom add [registry] +axiom native-build [-o ] +axiom install +``` + +These commands are currently implemented primarily by the native Rust transition toolchain. + +## Intended canonical surface + +The final AXIOM toolchain will additionally provide: + +```text +axiom explain +axiom format +axiom lint +axiom migrate +``` + +Their semantics are defined by the AXIOM 0.1 architecture and will be implemented progressively. + +## Transition boundary + +The native CLI can currently operate without the historical Python CLI for its supported workflow. + +This is a significant milestone, but it does not make the whole AXIOM implementation independent of Rust. The Rust binary is still the current implementation host. + +## Principle + +CLI commands are interfaces to semantic compiler operations. They should not become separate implementations of language behavior. diff --git a/docs/conformance.md b/docs/conformance.md new file mode 100644 index 0000000..2cb6aa9 --- /dev/null +++ b/docs/conformance.md @@ -0,0 +1,135 @@ +# AXIOM Conformance + +## Purpose + +Conformance tests are the executable contract between the AXIOM specification and its implementation. + +A feature is not considered stable because it is documented. It is stable when its intended semantics are exercised by executable tests. + +## Test layers + +### Syntax conformance + +Tests verify: + +- tokenization; +- indentation; +- canonical keywords; +- expressions; +- definitions; +- calls; +- blocks; +- source locations. + +### Semantic conformance + +Tests verify: + +- types; +- forms and entities; +- name resolution; +- module resolution; +- relations; +- capabilities; +- effects; +- Resource Flow; +- Information Flow; +- restrictions; +- contracts. + +### Execution conformance + +Tests verify observable behavior of valid programs. + +Where behavior is target-dependent, the test must declare the relevant target and constraints. + +### Diagnostic conformance + +Diagnostics are part of the language contract. + +Tests should verify: + +- source location; +- error category; +- explanation; +- relevant context; +- ambiguity handling; +- cascading-error suppression where appropriate. + +Exact wording should only be asserted when wording itself is a deliberate interface. + +### Determinism conformance + +Identical declared inputs should produce identical compiler outputs wherever deterministic output is part of the contract. + +Self-hosting tests must record: + +- compiler revision; +- source revision; +- dependencies; +- target; +- generated artifact identity; +- comparison result. + +### Bootstrap conformance + +Bootstrap tests verify that a new compiler implementation can reproduce the required compiler artifacts using the previous bootstrap boundary. + +Bootstrap is a transition mechanism, not the final language specification. + +## Canonical examples + +A minimal canonical program is: + +```axiom +mostrar("Hello, AXIOM") +``` + +A canonical entity definition is: + +```axiom +usuario: + nombre: texto + edad: entero +``` + +A canonical entity instance is: + +```axiom +alex: usuario + nombre = "Alex" + edad = 25 +``` + +These examples become executable conformance fixtures as the AXIOM 0.1 frontend advances. + +## Historical compatibility tests + +The repository currently contains tests for the historical prototype syntax. + +Those tests remain useful during migration, but they must not prevent the canonical AXIOM 0.1 implementation from evolving. + +When a historical feature is intentionally retired: + +1. mark the compatibility test as transitional; +2. add the canonical replacement test; +3. document the migration; +4. remove the obsolete test only after the replacement is verified. + +## Completion gate + +A semantic feature is complete only when: + +```text +specification + = +implementation + = +tests + = +diagnostics + = +documentation +``` + +Any unresolved difference remains an implementation task. diff --git a/docs/engine.md b/docs/engine.md new file mode 100644 index 0000000..cfe0fca --- /dev/null +++ b/docs/engine.md @@ -0,0 +1,32 @@ +# AXIOM Engine + +AXIOM Engine is the general-purpose execution foundation established during the historical language roadmap. + +## Current role + +The current Engine model provides backend-neutral concepts for: + +- applications; +- entities and components; +- resources; +- events; +- scenes; +- simulation clocks; +- networking; +- rendering queues; +- UI queues; +- audio queues. + +Its purpose is to avoid binding the language to a single graphics, physics, audio, UI, or networking implementation. + +## Relation to AXIOM 0.1 + +The definitive AXIOM semantic model goes deeper than an application engine. + +Engine concepts must eventually map to the same language-wide entities, resources, capabilities, effects, restrictions, contracts, and execution plans used by systems programming, embedded work, scientific computing, robotics, distributed systems, and hardware control. + +Engine is therefore a subsystem, not the definition of the language. + +## Future direction + +Concrete graphics, physics, audio, UI, robotics, simulation, and hardware systems can consume AXIOM execution plans without creating separate programming languages. diff --git a/docs/ide.md b/docs/ide.md new file mode 100644 index 0000000..4a411ab --- /dev/null +++ b/docs/ide.md @@ -0,0 +1,45 @@ +# AXIOM Studio + +AXIOM Studio is the local development environment established during the historical Phase 12 milestone. + +## Current capabilities + +The transition Studio provides: + +- source editing; +- diagnostics; +- completion; +- run and testing actions; +- execution tracing; +- breakpoints; +- profiling; +- Git inspection; +- package inspection; +- integrated terminal; +- documentation browsing; +- IR visualization; +- local package creation. + +## Architectural rule + +Studio must consume the real AXIOM compiler, semantic model, IR, runtime, package manager, and diagnostics. + +It must not become a second language implementation. + +## Future direction + +As the canonical AXIOM compiler becomes self-hosted, Studio will move to the canonical compiler interfaces. + +The long-term Studio should expose semantic information directly: + +- resources; +- capabilities; +- effects; +- information flows; +- execution plans; +- verification evidence; +- target decisions; +- diagnostics; +- migration diffs. + +The current Studio is transition infrastructure and is not yet the final AXIOM 0.1 development environment. diff --git a/docs/implementation-status.md b/docs/implementation-status.md new file mode 100644 index 0000000..0cda83c --- /dev/null +++ b/docs/implementation-status.md @@ -0,0 +1,105 @@ +# AXIOM Implementation Status + +## Status date + +This document describes the repository state at the beginning of the definitive AXIOM 0.1 implementation program. + +## What is complete + +The historical language/toolchain program has established: + +- lexical analysis and parsing; +- semantic analysis; +- functions and control flow; +- arrays and structures; +- AXIOM IR; +- VM/runtime execution; +- CLI; +- standard-library foundation; +- package workflow; +- project workflow; +- AXIOM Studio foundation; +- AXIOM Core; +- AXIOM Engine foundation; +- native AOT packaging; +- bootstrap seed; +- self-hosted bootstrap lexer/parser/AST/semantic/compiler stages; +- native project/package workflow; +- deterministic package/artifact paths. + +The repository also contains the consolidated AXIOM 0.1 semantic specification. + +## What is not complete + +The following are design commitments or transition infrastructure, not finished AXIOM 1.0 features: + +- canonical AXIOM 0.1 frontend; +- complete AXIOM 0.1 semantic compiler; +- complete Resource Flow implementation; +- complete Information Flow enforcement; +- complete effects/capabilities system; +- complete contracts and verification engine; +- compiler-directed execution planning; +- final AXIOM IR; +- final AXIOM ABI; +- AXIOM-owned runtime; +- AXIOM-owned native backend; +- AXIOM-owned package/toolchain implementation; +- complete FFI; +- complete distribution model; +- production metaprogramming/reflection; +- complete standard library; +- AXIOM compiler self-build using the canonical language; +- removal of Python as a compiler dependency; +- removal of Rust as the compiler/runtime language foundation. + +## Historical implementations + +### Python + +The Python implementation remains useful as transition infrastructure and as a fast testing environment. + +It is not the intended final compiler implementation. + +### Rust + +The Rust implementation is the current native transition host. It provides the existing parser, semantic pipeline, IR, runtime, VM, package workflow, and standalone executable generation. + +It is not the definition of AXIOM and is not the intended final language foundation. + +### AXIOM bootstrap + +The `bootstrap/` directory contains the first AXIOM-written compiler experiments. + +These experiments are evidence that AXIOM can participate in its own bootstrap, but they target the historical artifact/runtime boundary and therefore are not yet the definitive AXIOM 0.1 compiler. + +## Current canonical source of truth + +Use these documents in this order: + +1. `docs/axiom-spec-0.1.md` — semantic and architectural definition. +2. `docs/axiom-language.md` — canonical syntax direction. +3. `docs/architecture.md` — implementation architecture. +4. `docs/self-hosting.md` — independence path. +5. `docs/version-roadmap.md` — release and implementation gates. +6. `ROADMAP.md` — language/toolchain milestone history. +7. `Documento sin título.txt` — wider AXIOM SYSTEMS master roadmap. + +The master roadmap is intentionally left untouched by this repository update. + +## Acceptance rule + +A feature is not considered implemented merely because it is described. + +A feature becomes complete when its: + +- syntax; +- semantics; +- compiler implementation; +- runtime/backend behavior; +- diagnostics; +- tests; +- reproducibility characteristics; +- documentation + +agree with the canonical specification. diff --git a/docs/ir-formats.md b/docs/ir-formats.md new file mode 100644 index 0000000..56a9a48 --- /dev/null +++ b/docs/ir-formats.md @@ -0,0 +1,51 @@ +# AXIOM Intermediate Representations + +## Current transition formats + +The repository currently contains two historical serialized formats: + +- `.air` — textual bootstrap IR; +- `.axm` — textual native VM artifact. + +Both are versioned and validated by their current consumers. + +## AXIOM_ARTIFACT_V1 + +The current native artifact is headed by: + +```text +AXIOM_ARTIFACT_V1 +``` + +It stores compiled functions and encoded VM instructions for the transition runtime. + +It is deterministic for identical compiler inputs under the current implementation. + +## AXIOM 0.1 IR direction + +The definitive AXIOM IR must represent semantic execution plans rather than merely mirror the historical VM instruction set. + +It must be capable of carrying information relevant to: + +- values and entities; +- resource flow; +- effects; +- capabilities; +- restrictions; +- contracts; +- execution dependencies; +- parallelism; +- target selection; +- memory placement; +- verification evidence; +- distributed execution where required. + +The exact stable representation will be frozen only after executable AXIOM 0.1 semantics are implemented. + +## Compatibility + +Language versions and IR format versions are separate. + +A language change requires an IR version change only when existing consumers cannot correctly interpret the resulting representation. + +The historical `.air` and `.axm` formats must not constrain the final AXIOM IR architecture. diff --git a/docs/native-backend.md b/docs/native-backend.md new file mode 100644 index 0000000..a5c18f3 --- /dev/null +++ b/docs/native-backend.md @@ -0,0 +1,72 @@ +# AXIOM Native Backend + +## Current role + +The native backend is transition infrastructure inherited from the historical AXIOM implementation. + +It currently lowers the existing native pipeline into a standalone executable by packaging an AXIOM artifact with the Rust runtime. + +## Historical architecture + +```text +AXIOM source + ↓ +Rust lexer/parser + ↓ +Rust semantic analysis + ↓ +AXIOM_ARTIFACT_V1 + ↓ +Rust AOT launcher + ↓ +Rust runtime + ↓ +standalone executable +``` + +This architecture is useful for bootstrap and testing, but it is not the final AXIOM 0.1 architecture. + +## Target architecture + +```text +AXIOM source + ↓ +AXIOM compiler + ↓ +AXIOM IR + ↓ +native target backend + ↓ +AXIOM runtime / target machine +``` + +The target backend must be defined by AXIOM semantics rather than by Rust-specific implementation assumptions. + +## Target model + +Targets may include: + +- desktop/server CPUs; +- SIMD; +- GPU; +- NPU; +- embedded systems; +- mobile; +- web; +- distributed systems; +- specialized hardware; +- future execution targets. + +The language remains one language. A target changes compilation strategy, available capabilities, and constraints. + +## ABI + +The current executable boundary is process-oriented and Rust-hosted. + +AXIOM 0.1 requires a language-owned ABI covering representation, layout, alignment, calls, structures, errors, resources, capabilities, and compatibility. + +## Reproducibility + +Transition builds are deterministic at the serialized artifact level where declared inputs are identical. Final executable byte identity may still depend on the host Rust compiler and platform linker. + +This dependency is one of the boundaries targeted by the self-hosting program. diff --git a/docs/native-toolchain.md b/docs/native-toolchain.md new file mode 100644 index 0000000..ee5ade3 --- /dev/null +++ b/docs/native-toolchain.md @@ -0,0 +1,67 @@ +# AXIOM Native Toolchain + +The native toolchain is the current transition implementation of the AXIOM project workflow. + +## Current workflow + +```text +axiom new +axiom check +axiom build +axiom run +axiom test +axiom package +axiom add +axiom native-build +axiom install +``` + +The current native binary can execute this workflow without the Python CLI. + +## Project artifacts + +A project contains an `axiom.toml` manifest, source files, and tests. + +The current implementation produces: + +- `AXIOM_ARTIFACT_V1` executable artifacts; +- `AXIOM_PACKAGE_V1` package archives; +- standalone native executables. + +These formats are transition formats. + +## Independence boundary + +The current boundary is: + +```text +AXIOM source + ↓ +Rust-native transition toolchain + ↓ +AXIOM artifact + ↓ +Rust runtime / executable +``` + +Python is not required for the supported native workflow. + +Rust remains the implementation host. + +The next architectural boundary is: + +```text +AXIOM source + ↓ +AXIOM compiler + ↓ +AXIOM IR + ↓ +AXIOM-owned runtime/backend +``` + +## Targets + +The transition backend accepts explicit native target triples. + +This is useful infrastructure, but target selection must eventually become part of the AXIOM semantic target model rather than a Rust-specific command-line detail. diff --git a/docs/package-manager.md b/docs/package-manager.md new file mode 100644 index 0000000..5769015 --- /dev/null +++ b/docs/package-manager.md @@ -0,0 +1,56 @@ +# AXIOM Package Manager + +The package manager is being migrated from the historical local implementation toward the definitive AXIOM package model. + +## Current project manifest + +```toml +[package] +name = "example" +version = "0.1.0" + +[dependencies] +networking = "0.1.0" +``` + +The current native transition implementation supports local registry packages, vendoring, and a deterministic lockfile. + +## Package identity + +The definitive model must include: + +- package identity; +- version; +- origin; +- dependencies; +- integrity; +- signature; +- capabilities requested; +- effects; +- supported platforms; +- ABI information; +- reproducibility metadata. + +## Security model + +Installation should conceptually perform: + +```text +resolve + ↓ +verify identity/integrity + ↓ +analyze capabilities/effects + ↓ +validate compatibility + ↓ +install +``` + +A dependency must not silently gain privileges merely because those privileges exist elsewhere in the package graph. + +## Current limitation + +Remote registries, cryptographic package verification, signatures, publishing, and full supply-chain policy are not yet the definitive AXIOM implementation. + +The existing local registry workflow is transition infrastructure. diff --git a/docs/self-hosting.md b/docs/self-hosting.md new file mode 100644 index 0000000..834724d --- /dev/null +++ b/docs/self-hosting.md @@ -0,0 +1,146 @@ +# AXIOM Self-Hosting + +## Objective + +Self-hosting means that AXIOM can compile and maintain the implementation of its own compiler without requiring Python or Rust as the language implementation foundation. + +This is the main engineering objective of the current development stage. + +## Current boundary + +The repository currently contains: + +```text +Historical Python frontend/runtime + + +Historical Rust native compiler/runtime + + +AXIOM bootstrap programs +``` + +The previous self-hosting phase proved that AXIOM can execute bootstrap compiler components and generate deterministic intermediate output. + +That is an important bootstrap milestone, but it is not yet definitive AXIOM 0.1 self-hosting. + +## Target boundary + +The target is: + +```text +AXIOM source + ↓ +AXIOM compiler written in AXIOM + ↓ +AXIOM IR + ↓ +AXIOM-owned runtime/backend + ↓ +target machine +``` + +Eventually the compiler source itself follows the same path: + +```text +AXIOM compiler source + ↓ +existing AXIOM compiler + ↓ +new AXIOM compiler + ↓ +new AXIOM compiler compiles itself +``` + +## Migration stages + +### Stage A — canonical frontend + +Implement the AXIOM 0.1 lexer and parser in AXIOM. + +The Python frontend may validate behavior during this stage, but Python is not the final implementation. + +### Stage B — canonical semantic model + +Implement in AXIOM: + +- entities; +- forms/types; +- scopes; +- modules; +- relations; +- Resource Flow; +- effects; +- capabilities; +- Information Flow; +- restrictions; +- diagnostics. + +### Stage C — canonical compiler + +Implement the semantic compiler and AXIOM IR generator in AXIOM. + +The compiler must be able to compile real AXIOM 0.1 programs, not only bootstrap fixtures. + +### Stage D — self-build + +The AXIOM compiler compiles its own source. + +The result must be compared against a clean rebuild and must be deterministic under identical inputs. + +### Stage E — Python removal + +Python ceases to be required for: + +- compiling AXIOM; +- checking AXIOM; +- running the compiler; +- building projects; +- executing the core toolchain. + +The historical Python implementation may remain as archival test material until removal is safe. + +### Stage F — Rust boundary reduction + +Rust stops being the language implementation foundation. + +Rust can remain temporarily as a low-level bootstrap host while AXIOM-owned runtime and backend components are introduced. + +### Stage G — autonomous toolchain + +The compiler, package manager, formatter, linter, test runner, explainability tools, migration tools, and documentation generation are AXIOM-owned. + +## Bootstrap policy + +Bootstrap dependencies are allowed while they are actively being replaced. + +A dependency is removed only when: + +1. the replacement exists; +2. the replacement passes the relevant tests; +3. the replacement can reproduce the required artifacts; +4. the old dependency is no longer required by the target workflow. + +This avoids a false sense of independence. + +## Reproducibility gate + +Every self-hosting milestone should record: + +- compiler source revision; +- compiler used for the build; +- dependency set; +- target; +- generated artifact identity; +- deterministic comparison result; +- test result. + +A self-hosting claim without reproducible evidence is not considered complete. + +## Final independence + +The final milestone is not "AXIOM can run one AXIOM program." + +The final milestone is: + +> AXIOM can build the AXIOM compiler, runtime, and core toolchain from AXIOM-owned source through a reproducible bootstrap path. + +That is the independence boundary toward AXIOM 1.0. diff --git a/docs/standard-library.md b/docs/standard-library.md new file mode 100644 index 0000000..cf5bc54 --- /dev/null +++ b/docs/standard-library.md @@ -0,0 +1,45 @@ +# AXIOM Standard Library + +The standard library will be an official semantic foundation of AXIOM rather than an unrelated collection of host-language helpers. + +## Core direction + +The library is expected to cover: + +- text and Unicode; +- collections; +- files and storage; +- processes and system interfaces; +- networking; +- HTTP and WebSocket; +- serialization and data formats; +- time and clocks; +- mathematics and statistics; +- linear algebra and tensors; +- cryptography; +- databases and SQL; +- compression; +- images, audio, video, and codecs; +- UI; +- graphics and GPU; +- AI/ML and inference; +- testing, benchmarking, and fuzzing; +- observability; +- FFI and ABI; +- devices and hardware; +- embedded systems; +- simulation. + +## Current transition layer + +The historical implementation exposes a small set of compiler-known operations such as `len`, `abs`, `min`, and `max`. + +These signatures are useful compatibility infrastructure, but they do not define the final standard library architecture. + +## Design rule + +Standard-library APIs should express semantic requirements and effects clearly. + +A filesystem API should expose filesystem effects. A network API should expose network effects. A GPU API should expose device capabilities and resource requirements. + +The compiler must be able to reason about these operations rather than treating the standard library as opaque host-language code. diff --git a/docs/version-roadmap.md b/docs/version-roadmap.md new file mode 100644 index 0000000..e4e53c6 --- /dev/null +++ b/docs/version-roadmap.md @@ -0,0 +1,129 @@ +# AXIOM Version Roadmap + +This roadmap defines the progressive implementation path from the current transition infrastructure to AXIOM 1.0. + +Version numbers describe engineering maturity and conformance gates. They are not calendar commitments. + +## 0.1 — Canonical foundation + +**Purpose:** turn the closed conceptual design into an executable language foundation. + +Required: + +- canonical lexer; +- canonical parser; +- canonical AST/entity representation; +- core types and units; +- name and module resolution; +- semantic diagnostics; +- initial Resource Flow; +- initial effects and capabilities; +- executable conformance suite. + +## 0.2 — Semantic compiler + +**Purpose:** make AXIOM useful for non-trivial programs. + +Required: + +- complete core expressions; +- blocks as values; +- decisions and repetition; +- functions and entities; +- modules and packages; +- Resource Flow analysis; +- Information Flow foundations; +- effect/capability propagation; +- restrictions and preferences; +- contracts; +- deterministic AXIOM IR. + +## 0.3 — Self-hosted compiler + +**Purpose:** AXIOM becomes the primary implementation language of its compiler. + +Required: + +- lexer in AXIOM; +- parser in AXIOM; +- AST model in AXIOM; +- semantic compiler in AXIOM; +- IR generation in AXIOM; +- compiler driver in AXIOM; +- compiler self-build; +- reproducible self-build. + +## 0.4 — Native foundation + +**Purpose:** move execution ownership from the historical host toward AXIOM. + +Required: + +- AXIOM runtime interface; +- memory/resource execution model; +- native target abstraction; +- initial AXIOM ABI; +- low-level interop boundary; +- native bootstrap without Python. + +## 0.5 — Toolchain ownership + +**Purpose:** make the core developer workflow AXIOM-owned. + +Required: + +- compiler CLI; +- project workflow; +- package management; +- deterministic builds; +- formatter; +- linter; +- test runner; +- explainability tooling; +- migration tooling. + +## 0.6–0.9 — Stabilization + +Focus areas: + +- concurrency and execution planning; +- real-time behavior; +- determinism; +- fault tolerance; +- distributed execution; +- metaprogramming; +- reflection; +- FFI; +- ABI compatibility; +- security and Information Flow; +- optimization; +- standard library; +- platform targets; +- ecosystem tooling. + +Each feature receives a conformance test before it is considered stable. + +## 1.0 — Independence and conformance + +AXIOM 1.0 requires: + +- canonical language specification implemented; +- compiler self-hosting; +- reproducible compiler self-build; +- no Python dependency in the core toolchain; +- no Rust dependency as the language implementation foundation; +- AXIOM-owned runtime/backend boundaries; +- stable ABI and FFI contracts; +- conformance suite; +- deterministic package/build identity; +- documented migration and compatibility policy; +- production-quality diagnostics; +- explainable compiler decisions. + +## What 1.0 does not mean + +AXIOM 1.0 does not mean every ecosystem feature is finished. + +It means the language core and its implementation foundation are independent, stable, reproducible, and capable of supporting the broader AXIOM SYSTEMS roadmap. + +Future capabilities such as advanced graphics, robotics, aircraft systems, industrial control, quantum targets, or AXIOM OS can then grow on top of an independent language foundation. diff --git a/native/README.md b/native/README.md index 2afe43d..d6ce47a 100644 --- a/native/README.md +++ b/native/README.md @@ -1,28 +1,39 @@ -# AXIOM Native +# AXIOM Native Transition Layer -This crate is the first native backend component of AXIOM SYSTEMS. It mirrors -the Python lexer and the first parser slice and is intentionally independent -from the Python bootstrap so both implementations can be compared before -migration. The current native pipeline covers lexing, parsing, and the first -semantic checks. +This crate contains the current Rust-native transition implementation of AXIOM. -The native runtime can execute the initial literal-printing slice: +It provides: -```rust -axiom_native::runtime::run("fn main() { print(\"Hello AXIOM\") }")?; -``` +- the historical native lexer/parser; +- semantic analysis; +- AXIOM artifact generation; +- the transition VM/runtime; +- native project workflow; +- package workflow; +- standalone executable generation. -```bash -cargo test --manifest-path native/Cargo.toml -``` +## Important boundary + +Rust is currently the native implementation host. + +Rust is **not** the definition of AXIOM and is not the intended final language implementation foundation. -Build and use the native CLI: +The project is progressively replacing this boundary with AXIOM-owned compiler and runtime components. + +## Current commands ```bash +cargo test --manifest-path native/Cargo.toml + cargo build --manifest-path native/Cargo.toml + native/target/debug/axiom check examples/hello.ax native/target/debug/axiom run examples/hello.ax +native/target/debug/axiom native-build examples/hello.ax out/hello ``` -Array literals and chained indexing are supported by the native pipeline. Array -types use postfix brackets, for example `fn first(values: Int[]) -> Int[]`. \ No newline at end of file +## Transition status + +The native toolchain is useful for bootstrapping the definitive compiler and validating low-level execution. + +It should be treated as migration infrastructure until the AXIOM-owned compiler, ABI, runtime, and backend replace its language-implementation responsibilities. diff --git a/native/src/backend.rs b/native/src/backend.rs new file mode 100644 index 0000000..a1dca53 --- /dev/null +++ b/native/src/backend.rs @@ -0,0 +1,163 @@ +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use crate::parser::Program; +use crate::vm::{build_artifact, VmError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeTarget { + pub triple: String, +} + +impl NativeTarget { + pub fn host() -> Self { + Self { + triple: "host".into(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeBuild { + pub target: NativeTarget, + pub source: PathBuf, + pub output: PathBuf, +} + +pub fn build_executable( + source_path: &Path, + program: &Program, + output: Option<&Path>, + target: &str, +) -> Result { + let target = NativeTarget { + triple: target.to_string(), + }; + let output = output + .map(PathBuf::from) + .unwrap_or_else(|| default_output(source_path)); + let parent = output.parent().unwrap_or_else(|| Path::new(".")); + fs::create_dir_all(parent).map_err(|error| VmError { + message: format!( + "cannot create native output directory '{}': {error}", + parent.display() + ), + })?; + + let artifact = build_artifact(program); + let generated = generate_rust_launcher(&artifact); + let generated_path = output.with_extension("axn.rs"); + fs::write(&generated_path, generated).map_err(|error| VmError { + message: format!( + "cannot write generated native source '{}': {error}", + generated_path.display() + ), + })?; + + let manifest = Path::new(env!("CARGO_MANIFEST_DIR")).join("Cargo.toml"); + let mut cargo = Command::new("cargo"); + cargo + .arg("build") + .arg("--manifest-path") + .arg(&manifest) + .arg("--lib") + .arg("--release"); + if target.triple != "host" { + cargo.arg("--target").arg(&target.triple); + } + let result = cargo.output().map_err(|error| VmError { + message: format!("cannot invoke cargo: {error}"), + })?; + if !result.status.success() { + let stderr = String::from_utf8_lossy(&result.stderr).trim().to_string(); + return Err(VmError { + message: if stderr.is_empty() { + format!("cargo failed with status {}", result.status) + } else { + format!("cargo failed: {stderr}") + }, + }); + } + + let target_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("target"); + let library = if target.triple == "host" { + target_dir.join("release/libaxiom_native.rlib") + } else { + target_dir + .join(&target.triple) + .join("release/libaxiom_native.rlib") + }; + let mut command = Command::new("rustc"); + command + .arg(&generated_path) + .arg("--edition=2021") + .arg("--crate-name") + .arg("axiom_app"); + if target.triple != "host" { + command.arg("--target").arg(&target.triple); + } + command + .arg("--extern") + .arg(format!("axiom_native={}", library.display())); + command.arg("-C").arg("opt-level=2"); + command.arg("-C").arg("codegen-units=1"); + command.arg("-C").arg("metadata=axiom_app"); + command + .arg("--remap-path-prefix") + .arg(format!("{}=.", env!("CARGO_MANIFEST_DIR"))); + command.arg("-o").arg(&output); + let result = command.output().map_err(|error| VmError { + message: format!("cannot invoke rustc: {error}"), + })?; + if !result.status.success() { + let stderr = String::from_utf8_lossy(&result.stderr).trim().to_string(); + return Err(VmError { + message: if stderr.is_empty() { + format!("rustc failed with status {}", result.status) + } else { + format!("rustc failed: {stderr}") + }, + }); + } + let _ = fs::remove_file(&generated_path); + Ok(NativeBuild { + target, + source: source_path.to_path_buf(), + output, + }) +} + +fn default_output(source: &Path) -> PathBuf { + let stem = source + .file_stem() + .and_then(|value| value.to_str()) + .unwrap_or("axiom"); + source.with_file_name(stem) +} + +fn generate_rust_launcher(artifact: &str) -> String { + let encoded = artifact.escape_default().to_string(); + format!( + "extern crate axiom_native;\n\nfn main() {{\n const ARTIFACT: &str = \"{encoded}\";\n let artifact = axiom_native::vm::Artifact::deserialize(ARTIFACT).expect(\"invalid embedded AXIOM artifact\");\n match axiom_native::vm::execute_artifact(&artifact) {{\n Ok(output) => print!(\"{{}}\", output),\n Err(error) => {{ eprintln!(\"error: {{}}\", error.message); std::process::exit(1); }}\n }}\n}}\n" + ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn host_target_is_explicit() { + assert_eq!(NativeTarget::host().triple, "host"); + } + + #[test] + fn generated_launcher_is_deterministic() { + let artifact = "AXIOM_ARTIFACT_V1\nINSTR:PRINT:Hello"; + assert_eq!( + generate_rust_launcher(artifact), + generate_rust_launcher(artifact) + ); + } +} diff --git a/native/src/ir.rs b/native/src/ir.rs index 0b7ed4a..e3c1436 100644 --- a/native/src/ir.rs +++ b/native/src/ir.rs @@ -33,6 +33,7 @@ pub enum Instruction { Index, StoreIndex, GetField(String), + StoreField(String), Print, Return, If { @@ -169,6 +170,18 @@ fn lower_assignment( body.push(Instruction::StoreVariable(name.clone())); body } + Expression::FieldAccess { target, field } => { + let Expression::Variable(name) = target.as_ref() else { + let mut body = lower_expression(value); + body.push(Instruction::StoreVariable("_tmp".to_string())); + return body; + }; + let mut body = vec![Instruction::LoadVariable(name.clone())]; + body.extend(lower_expression(value)); + body.push(Instruction::StoreField(field.clone())); + body.push(Instruction::StoreVariable(name.clone())); + body + } Expression::Index { .. } => { let mut indexes = Vec::new(); let mut current = target; @@ -310,6 +323,18 @@ mod tests { use super::*; use crate::parser::parse; + #[test] + fn lowers_struct_field_assignment() { + let program = parse( + "struct Point { x: Int } fn main() { let point: Point = Point { x: 10 }; point.x = 42 }", + ) + .unwrap(); + let lowered = lower_program(&program); + assert!(lowered.functions[0].instructions.iter().any( + |instruction| matches!(instruction, Instruction::StoreField(field) if field == "x") + )); + } + #[test] fn lowers_main_function() { let program = parse("fn main() { print(42) }").unwrap(); diff --git a/native/src/lib.rs b/native/src/lib.rs index 8797f8c..4614f3a 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -1,12 +1,16 @@ +pub mod backend; pub mod ir; pub mod parser; pub mod runtime; pub mod semantic; +pub mod toolchain; pub mod vm; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum TokenKind { Fn, + Import, + Module, Let, Return, If, @@ -216,6 +220,8 @@ pub fn lex(source: &str) -> Result, LexError> { fn keyword(value: &str) -> TokenKind { match value { "fn" => TokenKind::Fn, + "import" => TokenKind::Import, + "module" => TokenKind::Module, "let" => TokenKind::Let, "return" => TokenKind::Return, "if" => TokenKind::If, diff --git a/native/src/main.rs b/native/src/main.rs index 569b007..15d0879 100644 --- a/native/src/main.rs +++ b/native/src/main.rs @@ -1,144 +1,145 @@ use std::env; -use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use std::process::ExitCode; fn main() -> ExitCode { - let mut arguments = env::args().skip(1); - let Some(command) = arguments.next() else { + let mut args = env::args().skip(1); + let Some(command) = args.next() else { print_help(); return ExitCode::SUCCESS; }; - - if command == "--version" || command == "-V" { - println!("axiom 0.2.0-native"); + if matches!(command.as_str(), "--version" | "-V") { + println!("axiom 0.3.0-native"); return ExitCode::SUCCESS; } - - if command == "--help" || command == "-h" { + if matches!(command.as_str(), "--help" | "-h") { print_help(); return ExitCode::SUCCESS; } - - let Some(path) = arguments.next() else { - eprintln!("error: expected a source file"); - return ExitCode::from(2); - }; - let output_path = if command == "build" { - arguments.next() - } else { - None + if command == "doctor" { + println!("axiom 0.3.0-native"); + println!("runtime native"); + println!("python dependency none"); + return ExitCode::SUCCESS; + } + if command == "new" { + let Some(path) = args.next() else { + return fail("error: expected project path"); + }; + return finish( + axiom_native::toolchain::new_project(Path::new(&path)), + &format!("created: {path}"), + ); + } + if command == "install" { + let Some(prefix) = args.next() else { + return fail("error: expected install prefix"); + }; + let binary = env::current_exe().unwrap_or_else(|_| PathBuf::from("axiom")); + return match axiom_native::toolchain::install_layout(Path::new(&prefix), &binary) { + Ok(path) => { + println!("installed: {}", path.display()); + ExitCode::SUCCESS + } + Err(error) => fail(&format!("error: {}", error.message)), + }; + } + let Some(path) = args.next() else { + return fail("error: expected source or project path"); }; - - let source = match fs::read_to_string(&path) { - Ok(source) => source, - Err(error) => { - eprintln!("error: cannot read {path}: {error}"); - return ExitCode::from(1); - } + let path = PathBuf::from(path); + if command == "run" && path.extension().and_then(|ext| ext.to_str()) == Some("axm") { + return match std::fs::read_to_string(&path) + .map_err(|e| axiom_native::vm::VmError { + message: format!("cannot read '{}': {e}", path.display()), + }) + .and_then(|encoded| axiom_native::vm::Artifact::deserialize(&encoded)) + .and_then(|artifact| axiom_native::vm::execute_artifact(&artifact)) + { + Ok(output) => { + print!("{output}"); + ExitCode::SUCCESS + } + Err(error) => fail(&format!("error: {}", error.message)), + }; + } + if command == "add" { + let Some(name) = args.next() else { + return fail("error: expected package name"); + }; + let registry = args.next().unwrap_or_else(|| "registry".into()); + return match axiom_native::toolchain::add_local_package(&path, &name, Path::new(®istry)) + { + Ok(version) => { + println!("added: {name} {version}"); + ExitCode::SUCCESS + } + Err(error) => fail(&format!("error: {}", error.message)), + }; + } + let project = match axiom_native::toolchain::discover(&path) { + Ok(project) => project, + Err(error) => return fail(&format!("error: {}", error.message)), }; - match command.as_str() { - "check" => match axiom_native::parser::parse(&source).and_then(|program| { - axiom_native::semantic::analyze(&program) - .map(|_| program) - .map_err(|error| axiom_native::parser::ParseError { - message: error.message, - line: 0, - column: 0, - }) - }) { + "check" => match axiom_native::toolchain::read_program(&project.source) { Ok(_) => { - println!("ok: {path}"); + println!("ok: {}", project.source.display()); ExitCode::SUCCESS } - Err(error) => { - eprintln!("{}", format_error(error.message, error.line, error.column)); - ExitCode::from(1) - } + Err(error) => fail(&format!("error: {}", error.message)), }, - "build" => match axiom_native::parser::parse(&source).and_then(|program| { - axiom_native::semantic::analyze(&program) - .map(|_| program) - .map_err(|error| axiom_native::parser::ParseError { - message: error.message, - line: 0, - column: 0, - }) - }) { - Ok(program) => { - let target = output_path.as_deref().map(Path::new); - let output_path = match axiom_native::vm::write_artifact_file_with_target( - Path::new(&path), - target, - &program, - ) { - Ok(path) => path, - Err(error) => { - eprintln!("error: {}", error.message); - return ExitCode::from(1); - } - }; - let artifact = axiom_native::vm::build_artifact(&program); - println!("build ok: {path}"); - println!("artifact file: {}", output_path.display()); - println!("artifact bytes: {}", artifact.len()); + "run" => match axiom_native::toolchain::run_project(&project) { + Ok(output) => { + print!("{output}"); ExitCode::SUCCESS } - Err(error) => { - eprintln!("{}", format_error(error.message, error.line, error.column)); - ExitCode::from(1) + Err(error) => fail(&format!("error: {}", error.message)), + }, + "build" => { + let output = parse_output(&mut args); + match axiom_native::toolchain::build_project(&project, output.as_deref()) { + Ok(path) => { + println!("build ok: {}", path.display()); + ExitCode::SUCCESS + } + Err(error) => fail(&format!("error: {}", error.message)), + } + } + "test" => match axiom_native::toolchain::test_project(&project) { + Ok(count) => { + println!("test ok: {count} source file(s)"); + ExitCode::SUCCESS } + Err(error) => fail(&format!("error: {}", error.message)), }, - "run" => { - if Path::new(&path).extension().and_then(|ext| ext.to_str()) == Some("axm") { - match fs::read_to_string(&path) { - Ok(encoded) => match axiom_native::vm::Artifact::deserialize(&encoded) { - Ok(artifact) => match axiom_native::vm::execute_artifact(&artifact) { - Ok(output) => { - print!("{output}"); - ExitCode::SUCCESS - } - Err(error) => { - eprintln!("error: {}", error.message); - ExitCode::from(1) - } - }, - Err(error) => { - eprintln!("error: {}", error.message); - ExitCode::from(1) - } - }, - Err(error) => { - eprintln!("error: cannot read {path}: {error}"); - ExitCode::from(1) - } + "package" => { + let output = + parse_output(&mut args).unwrap_or_else(|| project.root.with_extension("axpkg")); + match axiom_native::toolchain::package_project(&project, &output) { + Ok(()) => { + println!("packaged: {}", output.display()); + ExitCode::SUCCESS } - } else { - match axiom_native::parser::parse(&source).and_then(|program| { - axiom_native::semantic::analyze(&program) - .map(|_| program) - .map_err(|error| axiom_native::parser::ParseError { - message: error.message, - line: 0, - column: 0, - }) - }) { - Ok(program) => match axiom_native::vm::execute_program(&program) { - Ok(output) => { - print!("{output}"); - ExitCode::SUCCESS - } - Err(error) => { - eprintln!("error: {}", error.message); - ExitCode::from(1) - } - }, - Err(error) => { - eprintln!("{}", format_error(error.message, error.line, error.column)); - ExitCode::from(1) + Err(error) => fail(&format!("error: {}", error.message)), + } + } + "native-build" => { + let output = parse_output(&mut args); + match axiom_native::toolchain::read_program(&project.source) { + Ok((_, program)) => match axiom_native::backend::build_executable( + &project.source, + &program, + output.as_deref(), + "host", + ) { + Ok(build) => { + println!("native build ok: {}", build.output.display()); + ExitCode::SUCCESS } - } + Err(error) => fail(&format!("error: {}", error.message)), + }, + Err(error) => fail(&format!("error: {}", error.message)), } } _ => { @@ -148,16 +149,47 @@ fn main() -> ExitCode { } } } - -fn print_help() { - println!("AXIOM native compiler"); - println!("Usage: axiom "); +fn parse_output(args: &mut impl Iterator) -> Option { + let mut output = None; + while let Some(argument) = args.next() { + if argument == "-o" || argument == "--output" { + output = args.next().map(PathBuf::from); + } else if output.is_none() { + output = Some(PathBuf::from(argument)); + } else { + return None; + } + } + output } -fn format_error(message: String, line: usize, column: usize) -> String { - if line == 0 && column == 0 { - format!("error: {message}") - } else { - format!("error: {message} at {line}:{column}") +fn finish(result: Result<(), axiom_native::vm::VmError>, message: &str) -> ExitCode { + match result { + Ok(()) => { + println!("{message}"); + ExitCode::SUCCESS + } + Err(error) => fail(&format!("error: {}", error.message)), } } + +fn fail(message: &str) -> ExitCode { + eprintln!("{message}"); + ExitCode::from(1) +} + +fn print_help() { + println!("AXIOM native toolchain"); + println!( + "Usage: axiom [options]" + ); + println!(" new create a project"); + println!(" check validate a source or project"); + println!(" build [-o FILE] build AXIOM_ARTIFACT_V1"); + println!(" run execute source or project"); + println!(" test run project tests"); + println!(" package [-o FILE] create AXIOM_PACKAGE_V1"); + println!(" add [registry] vendor a local package"); + println!(" native-build [-o FILE] create standalone executable"); + println!(" install install this executable"); +} diff --git a/native/src/parser.rs b/native/src/parser.rs index a0a936d..e3653ed 100644 --- a/native/src/parser.rs +++ b/native/src/parser.rs @@ -4,6 +4,8 @@ use crate::{lex, LexError, Token, TokenKind}; pub struct Program { pub functions: Vec, pub structs: Vec, + pub module_name: Option, + pub imports: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -156,14 +158,48 @@ impl Parser { fn parse(mut self) -> Result { let mut functions = Vec::new(); let mut structs = Vec::new(); + let mut module_name = None; + let mut imports = Vec::new(); while !self.check(TokenKind::Eof) { - if self.check(TokenKind::Struct) { + if self.check(TokenKind::Module) { + if module_name.is_some() { + return Err(self.error("duplicate module declaration")); + } + self.position += 1; + module_name = Some(self.qualified_name()?); + } else if self.check(TokenKind::Import) { + self.position += 1; + imports.push(self.qualified_name()?); + } else if self.check(TokenKind::Struct) { structs.push(self.struct_definition()?); } else { functions.push(self.function()?); } + if self.check(TokenKind::Semicolon) { + self.position += 1; + } + } + Ok(Program { + functions, + structs, + module_name, + imports, + }) + } + + fn qualified_name(&mut self) -> Result { + let mut parts = vec![ + self.consume(TokenKind::Identifier, "expected module name")? + .lexeme, + ]; + while self.check(TokenKind::Dot) { + self.position += 1; + parts.push( + self.consume(TokenKind::Identifier, "expected name after '.'")? + .lexeme, + ); } - Ok(Program { functions, structs }) + Ok(parts.join(".")) } fn struct_definition(&mut self) -> Result { @@ -740,3 +776,10 @@ mod tests { assert_eq!(error.message, "unknown type 'Any'"); } } + +#[test] +fn parses_module_and_imports() { + let program = parse("module main; import math.core; fn main() { print(1) }").unwrap(); + assert_eq!(program.module_name.as_deref(), Some("main")); + assert_eq!(program.imports, vec!["math.core"]); +} diff --git a/native/src/semantic.rs b/native/src/semantic.rs index 0cc5885..4d988f7 100644 --- a/native/src/semantic.rs +++ b/native/src/semantic.rs @@ -8,20 +8,47 @@ pub struct SemanticError { } pub fn analyze(program: &Program) -> Result<(), SemanticError> { - let main = program + validate_structs(program)?; + if !program .functions .iter() - .find(|function| function.name == "main") - .ok_or_else(|| SemanticError { + .any(|function| function.name == "main") + { + return Err(SemanticError { message: "program must define 'main'".into(), - })?; + }); + } for function in &program.functions { - check_function(function, &program.functions)?; + check_function(function, &program.functions, &program.structs)?; } - check_function(main, &program.functions) + Ok(()) } -fn check_function(function: &Function, functions: &[Function]) -> Result<(), SemanticError> { +fn validate_structs(program: &Program) -> Result<(), SemanticError> { + let mut names = std::collections::HashSet::new(); + for structure in &program.structs { + if !names.insert(&structure.name) { + return Err(SemanticError { + message: format!("duplicate struct '{}'", structure.name), + }); + } + let mut fields = std::collections::HashSet::new(); + for field in &structure.fields { + if !fields.insert(&field.name) { + return Err(SemanticError { + message: format!("struct '{}' has duplicate fields", structure.name), + }); + } + } + } + Ok(()) +} + +fn check_function( + function: &Function, + functions: &[Function], + structs: &[crate::parser::StructDefinition], +) -> Result<(), SemanticError> { let mut variables = std::collections::HashMap::new(); for Parameter { name, type_name } in &function.parameters { variables.insert(name.clone(), type_name.clone()); @@ -30,6 +57,7 @@ fn check_function(function: &Function, functions: &[Function]) -> Result<(), Sem &function.body, &mut variables, functions, + structs, function.return_type.clone(), 0, )?; @@ -50,6 +78,7 @@ fn check_expression( expression: &Expression, variables: &std::collections::HashMap>, functions: &[Function], + structs: &[crate::parser::StructDefinition], ) -> Result, SemanticError> { match expression { Expression::String(_) => Ok(Some(Type::String)), @@ -59,7 +88,7 @@ fn check_expression( Expression::Array(elements) => { let mut inferred: Option = None; for element in elements { - let element_type = check_expression(element, variables, functions)?; + let element_type = check_expression(element, variables, functions, structs)?; if let Some(element_type) = element_type { if let Some(current) = inferred.as_ref() { if *current != element_type { @@ -74,7 +103,41 @@ fn check_expression( } Ok(inferred.map(|value| Type::Array(Box::new(value)))) } - Expression::StructLiteral { type_name, .. } => Ok(Some(Type::Named(type_name.clone()))), + Expression::StructLiteral { type_name, fields } => { + let structure = structs + .iter() + .find(|structure| structure.name == *type_name) + .ok_or_else(|| SemanticError { + message: format!("unknown struct '{type_name}'"), + })?; + let mut seen = std::collections::HashSet::new(); + for (field_name, value) in fields { + if !seen.insert(field_name) { + return Err(SemanticError { + message: format!("struct '{type_name}' has duplicate fields"), + }); + } + let field = structure + .fields + .iter() + .find(|field| field.name == *field_name) + .ok_or_else(|| SemanticError { + message: format!("unknown field '{field_name}'"), + })?; + let value_type = check_expression(value, variables, functions, structs)?; + if value_type.is_some() && value_type != field.type_name { + return Err(SemanticError { + message: format!("field '{field_name}' has incompatible type"), + }); + } + } + if fields.len() != structure.fields.len() { + return Err(SemanticError { + message: format!("struct '{type_name}' has incompatible fields"), + }); + } + Ok(Some(Type::Named(type_name.clone()))) + } Expression::Variable(name) => variables.get(name).cloned().ok_or_else(|| SemanticError { message: format!("unknown variable '{name}'"), }), @@ -83,8 +146,8 @@ fn check_expression( operator, right, } => { - let left_type = check_expression(left, variables, functions)?; - let right_type = check_expression(right, variables, functions)?; + let left_type = check_expression(left, variables, functions, structs)?; + let right_type = check_expression(right, variables, functions, structs)?; match operator { BinaryOperator::Add => { if left_type == right_type @@ -150,7 +213,7 @@ fn check_expression( operator: UnaryOperator::Not, operand, } => { - let operand_type = check_expression(operand, variables, functions)?; + let operand_type = check_expression(operand, variables, functions, structs)?; if operand_type.is_some() && operand_type != Some(Type::Bool) { return Err(SemanticError { message: "! requires Bool".into(), @@ -162,7 +225,7 @@ fn check_expression( operator: UnaryOperator::Negate, operand, } => { - let operand_type = check_expression(operand, variables, functions)?; + let operand_type = check_expression(operand, variables, functions, structs)?; if operand_type.is_some() && operand_type != Some(Type::Int) && operand_type != Some(Type::Float) @@ -174,8 +237,8 @@ fn check_expression( Ok(operand_type) } Expression::Index { target, index } => { - let target_type = check_expression(target, variables, functions)?; - let index_type = check_expression(index, variables, functions)?; + let target_type = check_expression(target, variables, functions, structs)?; + let index_type = check_expression(index, variables, functions, structs)?; if index_type.is_some() && index_type != Some(Type::Int) { return Err(SemanticError { message: "array index requires Int".into(), @@ -188,8 +251,32 @@ fn check_expression( }), } } - Expression::FieldAccess { .. } => Ok(None), + Expression::FieldAccess { target, field } => { + let target_type = check_expression(target, variables, functions, structs)?; + let Some(Type::Named(type_name)) = target_type else { + return Err(SemanticError { + message: "field access requires a struct".into(), + }); + }; + let structure = structs + .iter() + .find(|structure| structure.name == type_name) + .ok_or_else(|| SemanticError { + message: format!("unknown struct '{type_name}'"), + })?; + let field_definition = structure + .fields + .iter() + .find(|field_definition| field_definition.name == *field) + .ok_or_else(|| SemanticError { + message: format!("unknown field '{field}'"), + })?; + Ok(field_definition.type_name.clone()) + } Expression::Call(call) => { + if let Some(return_type) = check_builtin(call, variables, functions, structs)? { + return Ok(Some(return_type)); + } let function = functions .iter() .find(|function| function.name == call.name) @@ -206,7 +293,7 @@ fn check_expression( }); } for (argument, parameter) in call.arguments.iter().zip(&function.parameters) { - let argument_type = check_expression(argument, variables, functions)?; + let argument_type = check_expression(argument, variables, functions, structs)?; if parameter.type_name.is_some() && argument_type.is_some() && parameter.type_name != argument_type @@ -221,17 +308,121 @@ fn check_expression( } } +fn check_builtin( + call: &crate::parser::Call, + variables: &std::collections::HashMap>, + functions: &[Function], + structs: &[crate::parser::StructDefinition], +) -> Result, SemanticError> { + let argument_types = call + .arguments + .iter() + .map(|argument| check_expression(argument, variables, functions, structs)) + .collect::, _>>()?; + match call.name.as_str() { + "split_lines" => { + if argument_types != vec![Some(Type::String)] { + return Err(SemanticError { + message: "split_lines expects one String argument".into(), + }); + } + Ok(Some(Type::Array(Box::new(Type::String)))) + } + "split" => { + if argument_types != vec![Some(Type::String), Some(Type::String)] { + return Err(SemanticError { + message: "split expects two String arguments".into(), + }); + } + Ok(Some(Type::Array(Box::new(Type::String)))) + } + "read_file" => { + if argument_types != vec![Some(Type::String)] { + return Err(SemanticError { + message: "read_file expects a String path".into(), + }); + } + Ok(Some(Type::String)) + } + "write_file" => { + if argument_types != vec![Some(Type::String), Some(Type::String)] { + return Err(SemanticError { + message: "write_file expects a path and String content".into(), + }); + } + Ok(Some(Type::Bool)) + } + "char_at" => { + if argument_types != vec![Some(Type::String), Some(Type::Int)] { + return Err(SemanticError { + message: "char_at expects a String and an Int index".into(), + }); + } + Ok(Some(Type::String)) + } + "char_code" => { + if argument_types != vec![Some(Type::String)] { + return Err(SemanticError { + message: "char_code expects one String character".into(), + }); + } + Ok(Some(Type::Int)) + } + "int_to_string" => { + if argument_types != vec![Some(Type::Int)] { + return Err(SemanticError { + message: "int_to_string expects one Int argument".into(), + }); + } + Ok(Some(Type::String)) + } + "len" => { + if argument_types.len() != 1 + || !matches!(argument_types[0], Some(Type::String) | Some(Type::Array(_))) + { + return Err(SemanticError { + message: "len expects a String or array".into(), + }); + } + Ok(Some(Type::Int)) + } + "abs" => { + if argument_types.len() != 1 + || !matches!(argument_types[0], Some(Type::Int) | Some(Type::Float)) + { + return Err(SemanticError { + message: "abs expects an Int or Float".into(), + }); + } + Ok(argument_types[0].clone()) + } + "min" | "max" => { + if argument_types.len() != 2 + || argument_types[0] != argument_types[1] + || !matches!(argument_types[0], Some(Type::Int) | Some(Type::Float)) + { + return Err(SemanticError { + message: format!("{} expects two matching numeric arguments", call.name), + }); + } + Ok(argument_types[0].clone()) + } + _ => Ok(None), + } +} + fn check_block( statements: &[Statement], variables: &mut std::collections::HashMap>, functions: &[Function], + structs: &[crate::parser::StructDefinition], return_type: Option, loop_depth: usize, ) -> Result<(), SemanticError> { for statement in statements { match statement { Statement::Let { name, value, .. } => { - let value_type = check_expression(value, variables, functions)?; + let value_type = check_expression(value, variables, functions, structs)?; if let Statement::Let { type_name: Some(declared), .. @@ -253,7 +444,7 @@ fn check_block( message: "function cannot return a value".into(), }); } - let value_type = check_expression(value, variables, functions)?; + let value_type = check_expression(value, variables, functions, structs)?; if return_type.is_some() && value_type.is_some() && return_type != value_type { return Err(SemanticError { message: "return value has incompatible type".into(), @@ -265,11 +456,12 @@ fn check_block( then_body, else_body, } => { - require_bool(check_expression(condition, variables, functions)?)?; + require_bool(check_expression(condition, variables, functions, structs)?)?; check_block( then_body, &mut variables.clone(), functions, + structs, return_type.clone(), loop_depth, )?; @@ -277,16 +469,18 @@ fn check_block( else_body, &mut variables.clone(), functions, + structs, return_type.clone(), loop_depth, )?; } Statement::While { condition, body } => { - require_bool(check_expression(condition, variables, functions)?)?; + require_bool(check_expression(condition, variables, functions, structs)?)?; check_block( body, &mut variables.clone(), functions, + structs, return_type.clone(), loop_depth + 1, )?; @@ -303,16 +497,18 @@ fn check_block( std::slice::from_ref(initializer), &mut scoped, functions, + structs, return_type.clone(), loop_depth, )?; } - require_bool(check_expression(condition, &scoped, functions)?)?; + require_bool(check_expression(condition, &scoped, functions, structs)?)?; if let Some(update) = update { check_block( std::slice::from_ref(update), &mut scoped, functions, + structs, return_type.clone(), loop_depth, )?; @@ -321,6 +517,7 @@ fn check_block( body, &mut scoped, functions, + structs, return_type.clone(), loop_depth + 1, )?; @@ -338,8 +535,8 @@ fn check_block( }); } Statement::Assign { target, value } => { - let target_type = check_expression(target, variables, functions)?; - let value_type = check_expression(value, variables, functions)?; + let target_type = check_expression(target, variables, functions, structs)?; + let value_type = check_expression(value, variables, functions, structs)?; if target_type.is_some() && value_type.is_some() && target_type != value_type { return Err(SemanticError { message: "assignment has incompatible type".into(), @@ -347,10 +544,15 @@ fn check_block( } } Statement::Call(call) if call.name == "print" && call.arguments.len() == 1 => { - check_expression(&call.arguments[0], variables, functions)?; + check_expression(&call.arguments[0], variables, functions, structs)?; } Statement::Call(call) => { - check_expression(&Expression::Call(call.clone()), variables, functions)?; + check_expression( + &Expression::Call(call.clone()), + variables, + functions, + structs, + )?; } } } @@ -438,6 +640,61 @@ mod tests { assert_eq!(error.message, "function 'answer' must return a value"); } + #[test] + fn accepts_standard_library_calls() { + let program = parse(r#"fn main() { print(len("axiom")); print(abs(-7)); print(min(3, 5)); print(max(3.0, 5.0)) }"#).unwrap(); + analyze(&program).unwrap(); + } + + #[test] + fn rejects_invalid_standard_library_call() { + let program = parse("fn main() { print(len(1)) }").unwrap(); + let error = analyze(&program).unwrap_err(); + assert_eq!(error.message, "len expects a String or array"); + } + + #[test] + fn accepts_valid_struct_access() { + let source = "struct Point { x: Int y: Int } fn main() { let point: Point = Point { x: 10 y: 20 } print(point.x) }"; + analyze(&parse(source).unwrap()).unwrap(); + } + + #[test] + fn rejects_unknown_struct_field() { + let source = "struct Point { x: Int } fn main() { let point: Point = Point { x: 10 } print(point.y) }"; + let error = analyze(&parse(source).unwrap()).unwrap_err(); + assert_eq!(error.message, "unknown field 'y'"); + } + + #[test] + fn rejects_struct_field_type_mismatch() { + let source = "struct Point { x: Int } fn main() { let point: Point = Point { x: true } }"; + let error = analyze(&parse(source).unwrap()).unwrap_err(); + assert_eq!(error.message, "field 'x' has incompatible type"); + } + + #[test] + fn rejects_duplicate_struct_fields() { + let source = + "struct Point { x: Int } fn main() { let point: Point = Point { x: 10 x: 20 } }"; + let error = analyze(&parse(source).unwrap()).unwrap_err(); + assert_eq!(error.message, "struct 'Point' has duplicate fields"); + } + + #[test] + fn rejects_duplicate_struct_definitions() { + let source = "struct Point { x: Int } struct Point { y: Int } fn main() { print(1) }"; + let error = analyze(&parse(source).unwrap()).unwrap_err(); + assert_eq!(error.message, "duplicate struct 'Point'"); + } + + #[test] + fn rejects_field_access_on_non_struct() { + let source = "fn main() { let value: Int = 10 print(value.x) }"; + let error = analyze(&parse(source).unwrap()).unwrap_err(); + assert_eq!(error.message, "field access requires a struct"); + } + #[test] fn rejects_mixed_integer_and_float_addition() { let error = analyze(&parse("fn main() { print(1 + 2.0) }").unwrap()).unwrap_err(); diff --git a/native/src/toolchain.rs b/native/src/toolchain.rs new file mode 100644 index 0000000..4d14182 --- /dev/null +++ b/native/src/toolchain.rs @@ -0,0 +1,266 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::parser::{parse, Program}; +use crate::vm::{build_artifact, execute_artifact, write_artifact_file_with_target, VmError}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Project { + pub root: PathBuf, + pub source: PathBuf, + pub tests: Vec, +} + +pub fn discover(path: &Path) -> Result { + let root = if path.is_dir() { + path + } else { + path.parent().unwrap_or_else(|| Path::new(".")) + }; + let source = if path.is_file() { + path.to_path_buf() + } else { + let candidate = root.join("src/main.ax"); + if candidate.is_file() { + candidate + } else { + root.join("main.ax") + } + }; + if !source.is_file() { + return Err(error(format!( + "project entry source not found: {}", + source.display() + ))); + } + let tests_root = root.join("tests"); + let mut tests = Vec::new(); + if tests_root.is_dir() { + collect_sources(&tests_root, &mut tests)?; + } + Ok(Project { + root: root.to_path_buf(), + source, + tests, + }) +} + +fn collect_sources(root: &Path, output: &mut Vec) -> Result<(), VmError> { + for entry in + fs::read_dir(root).map_err(|e| error(format!("cannot read '{}': {e}", root.display())))? + { + let entry = entry.map_err(|e| error(format!("cannot read directory entry: {e}")))?; + let path = entry.path(); + if path.is_dir() { + collect_sources(&path, output)?; + } else if path.extension().and_then(|v| v.to_str()) == Some("ax") { + output.push(path); + } + } + output.sort(); + Ok(()) +} + +pub fn read_program(path: &Path) -> Result<(String, Program), VmError> { + let source = fs::read_to_string(path) + .map_err(|e| error(format!("cannot read '{}': {e}", path.display())))?; + let program = + parse(&source).map_err(|e| error(format!("{} at {}:{}", e.message, e.line, e.column)))?; + crate::semantic::analyze(&program).map_err(|e| error(e.message.clone()))?; + Ok((source, program)) +} + +pub fn build_project(project: &Project, output: Option<&Path>) -> Result { + let (_, program) = read_program(&project.source)?; + let default = project.root.join("build/main.axm"); + let target = output.unwrap_or(&default); + write_artifact_file_with_target(&project.source, Some(target), &program) +} + +pub fn run_project(project: &Project) -> Result { + let (_, program) = read_program(&project.source)?; + let artifact = crate::vm::compile_program(&program); + execute_artifact(&artifact) +} + +pub fn test_project(project: &Project) -> Result { + for test in &project.tests { + let (_, program) = read_program(test)?; + let artifact = crate::vm::compile_program(&program); + execute_artifact(&artifact)?; + } + Ok(project.tests.len()) +} +pub fn new_project(path: &Path) -> Result<(), VmError> { + if path.exists() { + return Err(error(format!( + "project path already exists: {}", + path.display() + ))); + } + fs::create_dir_all(path.join("src")) + .map_err(|e| error(format!("cannot create project: {e}")))?; + fs::create_dir_all(path.join("tests")) + .map_err(|e| error(format!("cannot create tests: {e}")))?; + let name = path + .file_name() + .and_then(|v| v.to_str()) + .unwrap_or("axiom-project"); + fs::write( + path.join("axiom.toml"), + format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\n\n[dependencies]\n"), + ) + .map_err(|e| error(format!("cannot write manifest: {e}")))?; + fs::write( + path.join("src/main.ax"), + "fn main() { print(\"Hello AXIOM\") }\n", + ) + .map_err(|e| error(format!("cannot write main.ax: {e}")))?; + fs::write( + path.join("tests/main.ax"), + "fn main() { print(\"test ok\") }\n", + ) + .map_err(|e| error(format!("cannot write test: {e}")))?; + Ok(()) +} + +pub fn package_project(project: &Project, output: &Path) -> Result<(), VmError> { + let mut files = Vec::new(); + collect_package_files(&project.root, &project.root, &mut files)?; + files.sort_by(|a, b| a.0.cmp(&b.0)); + let mut encoded = String::from("AXIOM_PACKAGE_V1\n"); + for (name, data) in files { + encoded.push_str(&format!( + "FILE|{}|{}\n", + hex_encode(name.as_bytes()), + hex_encode(&data) + )); + } + fs::write(output, encoded) + .map_err(|e| error(format!("cannot write package '{}': {e}", output.display())))?; + Ok(()) +} + +fn collect_package_files( + root: &Path, + base: &Path, + output: &mut Vec<(String, Vec)>, +) -> Result<(), VmError> { + for entry in + fs::read_dir(root).map_err(|e| error(format!("cannot read '{}': {e}", root.display())))? + { + let entry = entry.map_err(|e| error(format!("cannot read directory entry: {e}")))?; + let path = entry.path(); + let relative = path.strip_prefix(base).unwrap_or(&path); + if relative == Path::new("target") + || relative == Path::new(".git") + || path.extension().and_then(|v| v.to_str()) == Some("axpkg") + { + continue; + } + if path.is_dir() { + collect_package_files(&path, base, output)?; + } else { + let name = relative.to_string_lossy().replace('\\', "/"); + output.push(( + name, + fs::read(&path).map_err(|e| error(format!("cannot read package file: {e}")))?, + )); + } + } + Ok(()) +} +pub fn add_local_package(root: &Path, name: &str, registry: &Path) -> Result { + let package = registry.join(name); + let manifest = package.join("axiom.toml"); + if !manifest.is_file() { + return Err(error(format!("package '{}' not found in registry", name))); + } + let version = manifest_version(&manifest)?; + let destination = root.join("vendor").join(name); + if destination.exists() { + return Err(error(format!("package '{}' is already vendored", name))); + } + copy_tree(&package, &destination)?; + let project_manifest = root.join("axiom.toml"); + let mut manifest_text = if project_manifest.is_file() { + fs::read_to_string(&project_manifest) + .map_err(|e| error(format!("cannot read axiom.toml: {e}")))? + } else { + String::from("[package]\nname = \"project\"\nversion = \"0.1.0\"\n\n[dependencies]\n") + }; + if !manifest_text.contains("[dependencies]") { + manifest_text.push_str("\n[dependencies]\n"); + } + if !manifest_text + .lines() + .any(|line| line.trim_start().starts_with(&format!("{name} ="))) + { + manifest_text.push_str(&format!( + r#"{name} = "{version}" +"# + )); + } + fs::write(&project_manifest, manifest_text) + .map_err(|e| error(format!("cannot update axiom.toml: {e}")))?; + let lock = root.join("axiom.lock"); + fs::write(lock, format!("AXIOM_LOCK_V1\n{name}={version}\n")) + .map_err(|e| error(format!("cannot write axiom.lock: {e}")))?; + Ok(version) +} + +fn manifest_version(path: &Path) -> Result { + let text = fs::read_to_string(path).map_err(|e| error(format!("cannot read manifest: {e}")))?; + for line in text.lines() { + let line = line.trim(); + if let Some(value) = line.strip_prefix("version") { + if let Some(value) = value.split('=').nth(1) { + return Ok(value.trim().trim_matches('"').to_string()); + } + } + } + Err(error("package manifest has no version".into())) +} + +fn copy_tree(source: &Path, destination: &Path) -> Result<(), VmError> { + fs::create_dir_all(destination) + .map_err(|e| error(format!("cannot create vendor directory: {e}")))?; + for entry in fs::read_dir(source).map_err(|e| error(format!("cannot read package: {e}")))? { + let entry = entry.map_err(|e| error(format!("cannot read package entry: {e}")))?; + let from = entry.path(); + let to = destination.join(entry.file_name()); + if from.is_dir() { + copy_tree(&from, &to)?; + } else { + fs::copy(&from, &to).map_err(|e| error(format!("cannot copy package file: {e}")))?; + } + } + Ok(()) +} + +fn hex_encode(data: &[u8]) -> String { + const HEX: &[u8; 16] = b"0123456789abcdef"; + let mut output = String::with_capacity(data.len() * 2); + for byte in data { + output.push(HEX[(byte >> 4) as usize] as char); + output.push(HEX[(byte & 0x0f) as usize] as char); + } + output +} + +fn error(message: String) -> VmError { + VmError { message } +} +pub fn install_layout(prefix: &Path, binary: &Path) -> Result { + let bin = prefix.join("bin"); + fs::create_dir_all(&bin).map_err(|e| error(format!("cannot create install directory: {e}")))?; + let destination = bin.join("axiom"); + fs::copy(binary, &destination) + .map_err(|e| error(format!("cannot install '{}': {e}", binary.display())))?; + Ok(destination) +} + +pub fn artifact_from_source(path: &Path) -> Result { + let (_, program) = read_program(path)?; + Ok(build_artifact(&program)) +} diff --git a/native/src/vm.rs b/native/src/vm.rs index a59a6f0..fc1c1df 100644 --- a/native/src/vm.rs +++ b/native/src/vm.rs @@ -266,6 +266,9 @@ impl Machine { name: &str, arguments: Vec, ) -> Result, VmError> { + if let Some(value) = call_standard_library(name, &arguments)? { + return Ok(Some(value)); + } let function = self .functions .iter() @@ -530,6 +533,22 @@ impl Machine { message: format!("unknown field '{field}'"), })?); } + Instruction::StoreField(field) => { + let value = self.pop_stack()?; + let structure = self.pop_stack()?; + let Value::Struct(mut fields) = structure else { + return Err(VmError { + message: "expected struct value".into(), + }); + }; + if !fields.contains_key(field) { + return Err(VmError { + message: format!("unknown field '{field}'"), + }); + } + fields.insert(field.clone(), value); + self.stack.push(Value::Struct(fields)); + } Instruction::StoreIndex => { let value = self.pop_stack()?; let index = self.pop_stack()?.as_int()?; @@ -630,6 +649,236 @@ impl Machine { } } +fn call_standard_library(name: &str, arguments: &[Value]) -> Result, VmError> { + match name { + "split_lines" => { + if arguments.len() != 1 { + return Err(VmError { + message: "split_lines expects one String argument".into(), + }); + } + let value = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "split_lines expects a String".into(), + }) + } + }; + Ok(Some(Value::Array( + value + .lines() + .map(|line| Value::String(line.to_owned())) + .collect(), + ))) + } + "split" => { + if arguments.len() != 2 { + return Err(VmError { + message: "split expects a String value and separator".into(), + }); + } + let value = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "split expects a String value".into(), + }) + } + }; + let separator = match &arguments[1] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "split expects a String separator".into(), + }) + } + }; + Ok(Some(Value::Array( + value + .split(separator) + .map(|part| Value::String(part.to_owned())) + .collect(), + ))) + } + "read_file" => { + if arguments.len() != 1 { + return Err(VmError { + message: "read_file expects one argument".into(), + }); + } + let path = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "read_file expects a String path".into(), + }) + } + }; + let content = std::fs::read_to_string(path).map_err(|error| VmError { + message: format!("cannot read file '{path}': {error}"), + })?; + Ok(Some(Value::String(content))) + } + "write_file" => { + if arguments.len() != 2 { + return Err(VmError { + message: "write_file expects a path and content".into(), + }); + } + let path = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "write_file expects a String path".into(), + }) + } + }; + let content = match &arguments[1] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "write_file expects String content".into(), + }) + } + }; + std::fs::write(path, content).map_err(|error| VmError { + message: format!("cannot write file '{path}': {error}"), + })?; + Ok(Some(Value::Bool(true))) + } + "char_at" => { + if arguments.len() != 2 { + return Err(VmError { + message: "char_at expects a String and an Int index".into(), + }); + } + let value = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "char_at expects a String".into(), + }) + } + }; + let index = arguments[1].as_int()?; + if index < 0 { + return Err(VmError { + message: "char_at index must be non-negative".into(), + }); + } + let character = value.chars().nth(index as usize).ok_or_else(|| VmError { + message: format!("char_at index {index} out of bounds"), + })?; + Ok(Some(Value::String(character.to_string()))) + } + "char_code" => { + if arguments.len() != 1 { + return Err(VmError { + message: "char_code expects one String character".into(), + }); + } + let value = match &arguments[0] { + Value::String(value) => value, + _ => { + return Err(VmError { + message: "char_code expects a String".into(), + }) + } + }; + let mut chars = value.chars(); + let character = chars.next().ok_or_else(|| VmError { + message: "char_code expects a non-empty String".into(), + })?; + if chars.next().is_some() { + return Err(VmError { + message: "char_code expects exactly one character".into(), + }); + } + Ok(Some(Value::Int(character as i64))) + } + "int_to_string" => { + if arguments.len() != 1 { + return Err(VmError { + message: "int_to_string expects one Int argument".into(), + }); + } + Ok(Some(Value::String(arguments[0].as_int()?.to_string()))) + } + "len" => { + if arguments.len() != 1 { + return Err(VmError { + message: "len expects one argument".into(), + }); + } + let length = match &arguments[0] { + Value::String(value) => value.chars().count(), + Value::Array(values) => values.len(), + _ => { + return Err(VmError { + message: "len expects a String or array".into(), + }) + } + }; + Ok(Some(Value::Int(length as i64))) + } + "abs" => { + if arguments.len() != 1 { + return Err(VmError { + message: "abs expects one argument".into(), + }); + } + match &arguments[0] { + Value::Int(value) => Ok(Some(Value::Int(value.abs()))), + Value::Float(value) => { + let value = value.parse::().map_err(|_| VmError { + message: "abs expects a numeric value".into(), + })?; + Ok(Some(Value::Float(render_float(value.abs())))) + } + _ => Err(VmError { + message: "abs expects an Int or Float".into(), + }), + } + } + "min" | "max" => { + if arguments.len() != 2 { + return Err(VmError { + message: format!("{name} expects two arguments"), + }); + } + match (&arguments[0], &arguments[1]) { + (Value::Int(left), Value::Int(right)) => { + let value = if name == "min" { + (*left).min(*right) + } else { + (*left).max(*right) + }; + Ok(Some(Value::Int(value))) + } + (Value::Float(left), Value::Float(right)) => { + let left = left.parse::().map_err(|_| VmError { + message: "expected Float value".into(), + })?; + let right = right.parse::().map_err(|_| VmError { + message: "expected Float value".into(), + })?; + let value = if name == "min" { + left.min(right) + } else { + left.max(right) + }; + Ok(Some(Value::Float(render_float(value)))) + } + _ => Err(VmError { + message: format!("{name} expects two matching numeric arguments"), + }), + } + } + _ => Ok(None), + } +} + fn encode_sequence(instructions: &[Instruction]) -> String { instructions .iter() @@ -687,6 +936,7 @@ fn encode_instruction(instruction: &Instruction) -> String { Instruction::Index => "Index".to_string(), Instruction::StoreIndex => "StoreIndex".to_string(), Instruction::GetField(field) => format!("GetField:{}", escape_string(field)), + Instruction::StoreField(field) => format!("StoreField:{}", escape_string(field)), Instruction::Print => "Print".to_string(), Instruction::Return => "Return".to_string(), Instruction::If { @@ -739,6 +989,9 @@ fn decode_instruction(token: &str) -> Result { if let Some(field) = token.strip_prefix("GetField:") { return Ok(Instruction::GetField(unescape_string(field))); } + if let Some(field) = token.strip_prefix("StoreField:") { + return Ok(Instruction::StoreField(unescape_string(field))); + } if token == "Break" { return Ok(Instruction::Break); } @@ -1087,6 +1340,35 @@ mod tests { assert_eq!(execute_artifact(&decoded).unwrap(), "ok\n"); } + #[test] + fn executes_compiled_struct_field_assignment() { + let program = parse( + "struct Point { x: Int, y: Int } fn main() { let point: Point = Point { x: 10, y: 20 }; point.x = 42; print(point.x) }", + ) + .unwrap(); + let artifact = compile_program(&program); + let decoded = Artifact::deserialize(&artifact.serialize()).unwrap(); + assert_eq!(execute_artifact(&decoded).unwrap(), "42\n"); + } + + #[test] + fn executes_compiled_standard_library_calls() { + let program = parse( + r#"fn main() { print(len("axiom")); print(abs(-7)); print(min(3, 5)); print(max(3.0, 5.0)) }"#, + ) + .unwrap(); + let artifact = compile_program(&program); + let decoded = Artifact::deserialize(&artifact.serialize()).unwrap(); + assert_eq!( + execute_artifact(&decoded).unwrap(), + "5 +7 +3 +5.0 +" + ); + } + #[test] fn executes_compiled_struct_field_access() { let program = parse( @@ -1108,6 +1390,265 @@ mod tests { assert_eq!(execute_artifact(&decoded).unwrap(), "4.0\n2.0\n"); } + #[test] + fn executes_self_hosted_lexer() { + let source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/lexer.ax" + )) + .unwrap(); + let source = source.replace( + "bootstrap/lexer_fixture.ax", + concat!(env!("CARGO_MANIFEST_DIR"), "/../bootstrap/lexer_fixture.ax"), + ); + let program = parse(&source).unwrap(); + let output = execute_program(&program).unwrap(); + assert!(output.contains("STRUCT|struct\n")); + assert!(output.contains("ARROW|->\n")); + assert!(output.contains("LBRACKET|[\n")); + assert!(output.contains("GREATER_EQUAL|>=\n")); + assert!(output.contains("AND|&&\n")); + assert!(output.contains("FLOAT|2.5\n")); + assert!(output.ends_with("RBRACE|}\n")); + } + + #[test] + fn executes_self_hosted_parser() { + let lexer_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/lexer.ax" + )) + .unwrap() + .replace( + "bootstrap/lexer_fixture.ax", + concat!(env!("CARGO_MANIFEST_DIR"), "/../bootstrap/lexer_fixture.ax"), + ); + let lexer_program = parse(&lexer_source).unwrap(); + let tokens = execute_program(&lexer_program).unwrap(); + let token_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-parser-{}.tokens", + std::process::id() + )); + std::fs::write(&token_path, tokens).unwrap(); + + let source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/parser.ax" + )) + .unwrap() + .replace("bootstrap/lexer_tokens.txt", token_path.to_str().unwrap()); + let program = parse(&source).unwrap(); + let output = execute_program(&program).unwrap(); + std::fs::remove_file(&token_path).unwrap(); + assert!(output.contains("Program\n")); + assert!(output.contains("Struct: Point\n")); + assert!(output.contains("Function: calculate\n")); + assert!(output.contains("Array\n")); + assert!(output.contains("Binary: >=\n")); + assert!(output.contains("Else\n")); + assert!(output.contains("While\n")); + assert!(output.contains("Return\n")); + } + + #[test] + fn executes_self_hosted_ast_normalizer() { + let parser_test = { + let lexer_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/lexer.ax" + )) + .unwrap() + .replace( + "bootstrap/lexer_fixture.ax", + concat!(env!("CARGO_MANIFEST_DIR"), "/../bootstrap/lexer_fixture.ax"), + ); + let lexer_program = parse(&lexer_source).unwrap(); + let tokens = execute_program(&lexer_program).unwrap(); + + let token_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-ast-{}.tokens", + std::process::id() + )); + let ast_input_path = std::env::temp_dir() + .join(format!("axiom-self-hosted-ast-{}.txt", std::process::id())); + std::fs::write(&token_path, tokens).unwrap(); + + let parser_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/parser.ax" + )) + .unwrap() + .replace("bootstrap/lexer_tokens.txt", token_path.to_str().unwrap()); + let parser_program = parse(&parser_source).unwrap(); + let parser_output = execute_program(&parser_program).unwrap(); + std::fs::write(&ast_input_path, parser_output).unwrap(); + + let ast_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/ast.ax" + )) + .unwrap() + .replace("bootstrap/parser_ast.txt", ast_input_path.to_str().unwrap()); + let ast_program = parse(&ast_source).unwrap(); + let ast_output = execute_program(&ast_program).unwrap(); + + std::fs::remove_file(token_path).unwrap(); + std::fs::remove_file(ast_input_path).unwrap(); + ast_output + }; + + assert!(parser_test.starts_with("AXIOM_AST_V1\n")); + assert!(parser_test.contains("NODE|0|Program|")); + assert!(parser_test.contains("NODE|0|Struct|Point")); + assert!(parser_test.contains("NODE|0|Function|calculate")); + assert!(parser_test.contains("NODE|1|Return|")); + } + + #[test] + fn executes_self_hosted_semantic_analysis() { + let lexer_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/lexer.ax" + )) + .unwrap() + .replace( + "bootstrap/lexer_fixture.ax", + concat!(env!("CARGO_MANIFEST_DIR"), "/../bootstrap/lexer_fixture.ax"), + ); + let lexer_program = parse(&lexer_source).unwrap(); + let tokens = execute_program(&lexer_program).unwrap(); + + let token_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-semantic-{}.tokens", + std::process::id() + )); + let ast_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-semantic-{}.ast", + std::process::id() + )); + std::fs::write(&token_path, tokens).unwrap(); + + let parser_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/parser.ax" + )) + .unwrap() + .replace("bootstrap/lexer_tokens.txt", token_path.to_str().unwrap()); + let parser_program = parse(&parser_source).unwrap(); + let parser_output = execute_program(&parser_program).unwrap(); + std::fs::write(&ast_path, parser_output).unwrap(); + + let ast_source = + std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/../bootstrap/ast.ax")) + .unwrap() + .replace("bootstrap/parser_ast.txt", ast_path.to_str().unwrap()); + let ast_program = parse(&ast_source).unwrap(); + let ast_output = execute_program(&ast_program).unwrap(); + + let semantic_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/semantic.ax" + )) + .unwrap() + .replace("bootstrap/ast_output.txt", ast_path.to_str().unwrap()); + + let semantic_input_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-semantic-input-{}.ast", + std::process::id() + )); + std::fs::write(&semantic_input_path, ast_output).unwrap(); + let semantic_source = semantic_source.replace( + ast_path.to_str().unwrap(), + semantic_input_path.to_str().unwrap(), + ); + let semantic_program = parse(&semantic_source).unwrap(); + let semantic_output = execute_program(&semantic_program).unwrap(); + + std::fs::remove_file(token_path).unwrap(); + std::fs::remove_file(ast_path).unwrap(); + std::fs::remove_file(semantic_input_path).unwrap(); + + assert!(semantic_output.contains("SEMANTIC_OK\n")); + assert!(semantic_output.contains("STRUCTS|1\n")); + assert!(semantic_output.contains("FUNCTIONS|2\n")); + } + + #[test] + fn self_hosted_semantic_rejects_mixed_literal_arithmetic() { + let ast_path = std::env::temp_dir().join(format!( + "axiom-self-hosted-semantic-negative-{}.ast", + std::process::id() + )); + let ast = "AXIOM_AST_V1\nNODE|0|Program|\nNODE|0|Function|main\nNODE|0|ScopeEnter|\nNODE|1|ExprEnter|\nNODE|2|Integer|1\nNODE|2|Binary|+\nNODE|2|Float|2.0\nNODE|1|ExprExit|\nNODE|0|ScopeExit|\n"; + std::fs::write(&ast_path, ast).unwrap(); + let semantic_source = std::fs::read_to_string(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../bootstrap/semantic.ax" + )) + .unwrap() + .replace("bootstrap/ast_output.txt", ast_path.to_str().unwrap()); + let program = parse(&semantic_source).unwrap(); + let output = execute_program(&program).unwrap(); + std::fs::remove_file(ast_path).unwrap(); + assert!( + output.contains("SEMANTIC_ERROR|binary operator '+' has incompatible operand types") + ); + assert!(!output.contains("SEMANTIC_OK")); + } + + #[test] + fn self_hosted_compiler_rebuilds_itself_reproducibly() { + let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap(); + let compiler_source = std::fs::read_to_string(root.join("bootstrap/compiler.ax")).unwrap(); + let lexer_source = std::fs::read_to_string(root.join("bootstrap/lexer.ax")).unwrap(); + let lexer_source = lexer_source.replace( + "bootstrap/lexer_fixture.ax", + root.join("bootstrap/compiler.ax").to_str().unwrap(), + ); + let lexer_program = parse(&lexer_source).unwrap(); + let tokens = execute_program(&lexer_program).unwrap(); + + let token_path = + std::env::temp_dir().join(format!("axiom-self-build-{}.tokens", std::process::id())); + let ast_path = + std::env::temp_dir().join(format!("axiom-self-build-{}.ast", std::process::id())); + let compiler_input = + std::env::temp_dir().join(format!("axiom-self-build-{}.input", std::process::id())); + std::fs::write(&token_path, tokens).unwrap(); + + let parser_source = std::fs::read_to_string(root.join("bootstrap/parser.ax")) + .unwrap() + .replace("bootstrap/lexer_tokens.txt", token_path.to_str().unwrap()); + let parser_program = parse(&parser_source).unwrap(); + let parser_output = execute_program(&parser_program).unwrap(); + std::fs::write(&ast_path, parser_output).unwrap(); + + let ast_source = std::fs::read_to_string(root.join("bootstrap/ast.ax")) + .unwrap() + .replace("bootstrap/parser_ast.txt", ast_path.to_str().unwrap()); + let ast_program = parse(&ast_source).unwrap(); + let ast_output = execute_program(&ast_program).unwrap(); + std::fs::write(&compiler_input, ast_output).unwrap(); + + let compile_source = + compiler_source.replace("bootstrap/parser_ast.txt", compiler_input.to_str().unwrap()); + let compile_program = parse(&compile_source).unwrap(); + let first = execute_program(&compile_program).unwrap(); + let second = execute_program(&compile_program).unwrap(); + + std::fs::remove_file(token_path).unwrap(); + std::fs::remove_file(ast_path).unwrap(); + std::fs::remove_file(compiler_input).unwrap(); + + assert_eq!(first, second); + assert!(first.starts_with("AXIOM_IR_V1\n")); + assert!(first.contains("FUNCTION|main\n")); + assert!(first.contains("CALL|")); + assert!(first.contains("EXPR_ENTER\n")); + } + #[test] fn builds_ir_artifact() { let program = parse("fn main() { print(42) }").unwrap(); diff --git a/native/tests/cli_errors.rs b/native/tests/cli_errors.rs index 31dd08e..bd4a66b 100644 --- a/native/tests/cli_errors.rs +++ b/native/tests/cli_errors.rs @@ -218,6 +218,86 @@ fn remainder_runs_in_source_and_compiled_artifact() { let _ = std::fs::remove_file(artifact_path); } +#[test] +fn standard_library_runs_in_source_and_compiled_artifact() { + let source_path = temp_path("standard_library_source.ax"); + let artifact_path = temp_path("standard_library_output.axm"); + std::fs::write( + &source_path, + r#"fn main() { print(len("axiom")); print(abs(-7)); print(min(3, 5)); print(max(3.0, 5.0)) }"#, + ) + .unwrap(); + let expected = "5 +7 +3 +5.0 +"; + + let source_run = run_axiom(&["run", source_path.to_str().unwrap()]); + assert!( + source_run.status.success(), + "source run failed: {}", + String::from_utf8_lossy(&source_run.stderr) + ); + assert_eq!(String::from_utf8_lossy(&source_run.stdout), expected); + + let build = run_axiom(&[ + "build", + source_path.to_str().unwrap(), + artifact_path.to_str().unwrap(), + ]); + assert!(build.status.success()); + + let artifact_run = run_axiom(&["run", artifact_path.to_str().unwrap()]); + assert!(artifact_run.status.success()); + assert_eq!(String::from_utf8_lossy(&artifact_run.stdout), expected); + + let _ = std::fs::remove_file(source_path); + let _ = std::fs::remove_file(artifact_path); +} + +#[test] +fn struct_field_assignment_runs_in_source_and_compiled_artifact() { + let source_path = temp_path("struct_assignment_source.ax"); + let artifact_path = temp_path("struct_assignment_output.axm"); + std::fs::write( + &source_path, + "struct Point { x: Int, y: Int } fn main() { let point: Point = Point { x: 10, y: 20 }; point.x = 42; print(point.x) }", + ) + .unwrap(); + let expected = "42\n"; + + let source_run = run_axiom(&["run", source_path.to_str().unwrap()]); + assert!( + source_run.status.success(), + "source run failed: {}", + String::from_utf8_lossy(&source_run.stderr) + ); + assert_eq!(String::from_utf8_lossy(&source_run.stdout), expected); + + let build = run_axiom(&[ + "build", + source_path.to_str().unwrap(), + artifact_path.to_str().unwrap(), + ]); + assert!( + build.status.success(), + "build failed: {}", + String::from_utf8_lossy(&build.stderr) + ); + + let artifact_run = run_axiom(&["run", artifact_path.to_str().unwrap()]); + assert!( + artifact_run.status.success(), + "artifact run failed: {}", + String::from_utf8_lossy(&artifact_run.stderr) + ); + assert_eq!(String::from_utf8_lossy(&artifact_run.stdout), expected); + + let _ = std::fs::remove_file(source_path); + let _ = std::fs::remove_file(artifact_path); +} + #[test] fn structs_run_in_source_and_compiled_artifact() { let source_path = temp_path("struct_source.ax"); diff --git a/native/tests/phase18_toolchain.rs b/native/tests/phase18_toolchain.rs new file mode 100644 index 0000000..dd1f306 --- /dev/null +++ b/native/tests/phase18_toolchain.rs @@ -0,0 +1,31 @@ +use std::fs; +use std::path::PathBuf; + +use axiom_native::toolchain::{ + build_project, discover, new_project, package_project, run_project, test_project, +}; + +fn fixture_root(name: &str) -> PathBuf { + std::env::current_dir().unwrap().join("target").join(name) +} + +#[test] +fn native_project_workflow_is_self_contained() { + let root = fixture_root("phase18-project-test"); + let _ = fs::remove_dir_all(&root); + new_project(&root).unwrap(); + + let project = discover(&root).unwrap(); + assert_eq!(run_project(&project).unwrap(), "Hello AXIOM\n"); + assert_eq!(test_project(&project).unwrap(), 1); + + let artifact = build_project(&project, None).unwrap(); + assert!(artifact.ends_with("build/main.axm")); + let package = root.with_extension("axpkg"); + package_project(&project, &package).unwrap(); + let encoded = fs::read_to_string(&package).unwrap(); + assert!(encoded.starts_with("AXIOM_PACKAGE_V1\n")); + + let _ = fs::remove_file(package); + let _ = fs::remove_dir_all(root); +} diff --git a/src/axiom/__init__.py b/src/axiom/__init__.py index a16b061..a17bf27 100644 --- a/src/axiom/__init__.py +++ b/src/axiom/__init__.py @@ -1,3 +1,3 @@ """Public package metadata for AXIOM SYSTEMS.""" -__version__ = "0.1.0" \ No newline at end of file +__version__ = "0.1.0" diff --git a/src/axiom/ast.py b/src/axiom/ast.py index 79a9df1..b891318 100644 --- a/src/axiom/ast.py +++ b/src/axiom/ast.py @@ -9,6 +9,8 @@ class Program: functions: list["Function"] structs: list["StructDefinition"] = field(default_factory=list) + module_name: str | None = None + imports: list[str] = field(default_factory=list) @dataclass(frozen=True) diff --git a/src/axiom/cli.py b/src/axiom/cli.py index b06fba4..ad39e89 100644 --- a/src/axiom/cli.py +++ b/src/axiom/cli.py @@ -5,13 +5,17 @@ import argparse import platform import sys +import zipfile from pathlib import Path from . import __version__ +from .core import Compiler, CompilerError from .ir import lower +from .package import PackageError, add_package from .parser import parse from .runtime import execute from .semantic import analyze +from .studio import serve as serve_studio def build_parser() -> argparse.ArgumentParser: @@ -26,6 +30,19 @@ def build_parser() -> argparse.ArgumentParser: build_parser.add_argument("-o", "--output", type=Path) run_parser = subparsers.add_parser("run", help="compile and execute an AXIOM source file") run_parser.add_argument("source", type=Path) + new_parser = subparsers.add_parser("new", help="create an AXIOM project") + new_parser.add_argument("name", type=Path) + test_parser = subparsers.add_parser("test", help="run AXIOM source tests") + test_parser.add_argument("path", type=Path, nargs="?", default=Path("tests")) + package_parser = subparsers.add_parser("package", help="create a distributable AXIOM package") + package_parser.add_argument("-o", "--output", type=Path) + add_parser = subparsers.add_parser("add", help="add a package from the local AXIOM registry") + add_parser.add_argument("name") + add_parser.add_argument("--registry", type=Path) + studio_parser = subparsers.add_parser("studio", help="launch AXIOM Studio") + studio_parser.add_argument("path", type=Path, nargs="?", default=Path(".")) + studio_parser.add_argument("--host", default="127.0.0.1") + studio_parser.add_argument("--port", type=int, default=8765) return parser @@ -46,20 +63,111 @@ def main(argv: list[str] | None = None) -> int: return 0 if args.command == "build": try: - program = parse(args.source.read_text(encoding="utf-8")) - analyze(program) - output = args.output or args.source.with_suffix(".air") - output.write_text(lower(program).render(), encoding="utf-8") + if args.source.is_dir(): + compilation = Compiler().compile_project(args.source) + output = args.output or args.source / "build" / "main.air" + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(compilation.ir.render(), encoding="utf-8") + else: + program = parse(args.source.read_text(encoding="utf-8")) + analyze(program) + output = args.output or args.source.with_suffix(".air") + output.write_text(lower(program).render(), encoding="utf-8") except (OSError, ValueError) as error: print(f"error: {error}", file=sys.stderr) return 1 print(f"built: {output}") return 0 + if args.command == "new": + try: + project = args.name + if project.exists(): + raise ValueError(f"project path already exists: {project}") + (project / "src").mkdir(parents=True) + (project / "tests").mkdir() + (project / "axiom.toml").write_text( + chr(10).join([ + "[package]", + f'name = "{project.name}"', + 'version = "0.1.0"', + "", + "[dependencies]", + "", + ]), + encoding="utf-8", + ) + (project / "src" / "main.ax").write_text( + 'fn main() { print("Hello AXIOM") }' + chr(10), + encoding="utf-8", + ) + except OSError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(f"created: {project}") + return 0 + if args.command == "test": + if not args.path.exists(): + print(f"error: test path does not exist: {args.path}", file=sys.stderr) + return 1 + sources = sorted(args.path.rglob("*.ax")) if args.path.is_dir() else [args.path] + try: + for source in sources: + program = parse(source.read_text(encoding="utf-8")) + analyze(program) + execute(lower(program)) + except (OSError, ValueError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(f"test ok: {len(sources)} source file(s)") + return 0 + if args.command == "package": + root = Path.cwd() + manifest = root / "axiom.toml" + if not manifest.exists(): + print("error: axiom.toml not found", file=sys.stderr) + return 1 + output = args.output or root.with_suffix(".axpkg") + try: + with zipfile.ZipFile(output, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path in root.rglob("*"): + relative = path.relative_to(root) + if ( + not path.is_file() + or relative == output.relative_to(root) + or any(part in {".git", "__pycache__", "target"} for part in relative.parts) + or path.suffix == ".axpkg" + ): + continue + archive.write(path, relative.as_posix()) + except OSError as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(f"packaged: {output}") + return 0 + if args.command == "add": + try: + registry = args.registry or Path("registry") + package = add_package(Path.cwd(), args.name, registry) + except (OSError, PackageError, ValueError) as error: + print(f"error: {error}", file=sys.stderr) + return 1 + print(f"added: {package.name} {package.version}") + return 0 + if args.command == "studio": + if not args.path.is_dir(): + print(f"error: studio path does not exist: {args.path}", file=sys.stderr) + return 1 + serve_studio(args.path, args.host, args.port) + return 0 if args.command == "run": try: - program = parse(args.source.read_text(encoding="utf-8")) - analyze(program) - execute(lower(program)) + if args.source.is_dir(): + compilation = Compiler().compile_project(args.source) + execute(compilation.ir) + else: + program = parse(args.source.read_text(encoding="utf-8")) + analyze(program) + execute(lower(program)) except (OSError, ValueError) as error: print(f"error: {error}", file=sys.stderr) return 1 diff --git a/src/axiom/core.py b/src/axiom/core.py new file mode 100644 index 0000000..dd4afa4 --- /dev/null +++ b/src/axiom/core.py @@ -0,0 +1,100 @@ +"""Public compiler-core API for project-wide AXIOM compilation.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path + +from .ast import Program +from .ir import IRProgram, lower +from .parser import parse +from .semantic import analyze + +class CompilerError(ValueError): + """Raised for project/module graph errors.""" + +@dataclass(frozen=True) +class SourceUnit: + module: str + path: Path + source: str + program: Program + +@dataclass(frozen=True) +class Compilation: + program: Program + ir: IRProgram + units: tuple[SourceUnit, ...] + +class ModuleResolver: + """Resolve AXIOM modules from a source tree without leaking filesystem details.""" + + def __init__(self, root: Path): + self.root = root.resolve() + self.source_root = self.root / "src" if (self.root / "src").is_dir() else self.root + self._units: dict[str, SourceUnit] = {} + self._loading: set[str] = set() + + def resolve(self, entry: Path | None = None) -> tuple[SourceUnit, ...]: + entry = (entry or self._default_entry()).resolve() + if not entry.is_file(): + raise CompilerError(f"source file not found: {entry}") + entry_module = self._module_for_path(entry) + self._visit(entry_module, entry) + return tuple(self._units[name] for name in sorted(self._units)) + + def _default_entry(self) -> Path: + candidates = [self.source_root / "main.ax", self.source_root / "main" / "main.ax"] + for candidate in candidates: + if candidate.is_file(): + return candidate + files = sorted(self.source_root.rglob("*.ax")) + if len(files) == 1: + return files[0] + raise CompilerError("project has no unique entry source") + + def _module_for_path(self, path: Path) -> str: + relative = path.relative_to(self.source_root).with_suffix("") + return ".".join(relative.parts) + + def _path_for_module(self, module: str) -> Path: + path = self.source_root.joinpath(*module.split(".")).with_suffix(".ax") + if not path.is_file(): + raise CompilerError(f"module '{module}' not found") + return path + + def _visit(self, module: str, path: Path) -> None: + if module in self._units: + return + if module in self._loading: + raise CompilerError(f"cyclic module import involving '{module}'") + self._loading.add(module) + program = parse(path.read_text(encoding="utf-8")) + declared = program.module_name or module + if declared != module: + raise CompilerError(f"module declaration '{declared}' does not match path module '{module}'") + unit = SourceUnit(module, path, path.read_text(encoding="utf-8"), program) + for imported in program.imports: + self._visit(imported, self._path_for_module(imported)) + self._loading.remove(module) + self._units[module] = unit + +class Compiler: + """Stable frontend → semantic analysis → IR compiler interface.""" + + def compile_project(self, root: str | Path, entry: str | Path | None = None) -> Compilation: + resolver = ModuleResolver(Path(root)) + units = resolver.resolve(Path(entry) if entry is not None else None) + functions = [] + structs = [] + for unit in units: + functions.extend(unit.program.functions) + structs.extend(unit.program.structs) + program = Program(functions, structs) + analyze(program) + return Compilation(program, lower(program), units) + + def compile_source(self, source: str) -> Compilation: + program = parse(source) + analyze(program) + return Compilation(program, lower(program), ()) diff --git a/src/axiom/engine.py b/src/axiom/engine.py new file mode 100644 index 0000000..4a10564 --- /dev/null +++ b/src/axiom/engine.py @@ -0,0 +1,376 @@ +"""Deterministic, backend-agnostic AXIOM general-purpose engine kernel.""" + +from __future__ import annotations + +from collections import defaultdict, deque +from dataclasses import dataclass, field +from time import monotonic +from typing import Any, Callable, Generic, Iterator, TypeVar + +T = TypeVar("T") +EventHandler = Callable[[Any], None] + +@dataclass(frozen=True) +class Entity: + id: int + name: str = "" + +@dataclass(frozen=True) +class EngineEvent: + name: str + payload: Any = None + +class EventBus: + """Synchronous event bus with deterministic subscription order.""" + def __init__(self) -> None: + self._handlers: dict[str, list[EventHandler]] = defaultdict(list) + self._queue: deque[EngineEvent] = deque() + + def subscribe(self, name: str, handler: EventHandler) -> None: + if handler not in self._handlers[name]: + self._handlers[name].append(handler) + + def unsubscribe(self, name: str, handler: EventHandler) -> None: + handlers = self._handlers.get(name, []) + if handler in handlers: + handlers.remove(handler) + + def emit(self, name: str, payload: Any = None) -> None: + self._queue.append(EngineEvent(name, payload)) + + def dispatch(self) -> int: + dispatched = 0 + while self._queue: + event = self._queue.popleft() + for handler in tuple(self._handlers.get(event.name, ())): + handler(event.payload) + dispatched += 1 + return dispatched + +class ComponentStore(Generic[T]): + """Typed-by-convention component storage indexed by entity id.""" + def __init__(self) -> None: + self._values: dict[int, T] = {} + + def set(self, entity: Entity, value: T) -> None: + self._values[entity.id] = value + + def get(self, entity: Entity) -> T | None: + return self._values.get(entity.id) + + def remove(self, entity: Entity) -> T | None: + return self._values.pop(entity.id, None) + + def items(self) -> Iterator[tuple[int, T]]: + return iter(self._values.items()) + + def __len__(self) -> int: + return len(self._values) + +class ResourceStore: + """Lifecycle-aware storage for engine-wide resources.""" + def __init__(self) -> None: + self._resources: dict[str, Any] = {} + + def put(self, name: str, resource: Any) -> None: + self._resources[name] = resource + + def get(self, name: str, default: T | None = None) -> Any | T | None: + return self._resources.get(name, default) + + def require(self, name: str) -> Any: + if name not in self._resources: + raise KeyError(f"resource not found: {name}") + return self._resources[name] + + def remove(self, name: str) -> Any | None: + return self._resources.pop(name, None) + + def names(self) -> tuple[str, ...]: + return tuple(sorted(self._resources)) + +@dataclass +class Scene: + name: str + entities: dict[int, Entity] = field(default_factory=dict) + components: dict[str, ComponentStore[Any]] = field(default_factory=dict) + resources: ResourceStore = field(default_factory=ResourceStore) + + def create_entity(self, entity_id: int, name: str = "") -> Entity: + if entity_id in self.entities: + raise ValueError(f"entity id already exists: {entity_id}") + entity = Entity(entity_id, name) + self.entities[entity_id] = entity + return entity + + def destroy_entity(self, entity: Entity) -> None: + self.entities.pop(entity.id, None) + for store in self.components.values(): + store.remove(entity) + + def store(self, component: str) -> ComponentStore[Any]: + return self.components.setdefault(component, ComponentStore()) + +class SceneManager: + def __init__(self) -> None: + self._scenes: dict[str, Scene] = {} + self._active: str | None = None + + def add(self, scene: Scene) -> None: + if scene.name in self._scenes: + raise ValueError(f"scene already exists: {scene.name}") + self._scenes[scene.name] = scene + if self._active is None: + self._active = scene.name + + def get(self, name: str) -> Scene: + try: + return self._scenes[name] + except KeyError as error: + raise KeyError(f"scene not found: {name}") from error + + def activate(self, name: str) -> Scene: + scene = self.get(name) + self._active = name + return scene + + @property + def active(self) -> Scene | None: + return self._scenes.get(self._active) if self._active else None + + def names(self) -> tuple[str, ...]: + return tuple(sorted(self._scenes)) + +@dataclass(frozen=True) +class NetworkMessage: + channel: str + payload: bytes + +class NetworkBus: + """Transport-neutral messaging boundary for future network backends.""" + def __init__(self) -> None: + self._inbox: deque[NetworkMessage] = deque() + self._outbox: deque[NetworkMessage] = deque() + + def send(self, channel: str, payload: bytes) -> None: + self._outbox.append(NetworkMessage(channel, bytes(payload))) + + def inject(self, message: NetworkMessage) -> None: + self._inbox.append(message) + + def drain_outbox(self) -> tuple[NetworkMessage, ...]: + result = tuple(self._outbox) + self._outbox.clear() + return result + + def drain_inbox(self) -> tuple[NetworkMessage, ...]: + result = tuple(self._inbox) + self._inbox.clear() + return result + +@dataclass(frozen=True) +class Frame: + index: int + delta: float + elapsed: float + +class SimulationClock: + def __init__(self, fixed_step: float = 1 / 60) -> None: + if fixed_step <= 0: + raise ValueError("fixed_step must be positive") + self.fixed_step = fixed_step + self.elapsed = 0.0 + self.accumulator = 0.0 + self.frame_index = 0 + + def advance(self, delta: float) -> tuple[Frame, int]: + if delta < 0: + raise ValueError("delta must be non-negative") + self.elapsed += delta + self.accumulator += delta + steps = 0 + while self.accumulator >= self.fixed_step: + self.accumulator -= self.fixed_step + steps += 1 + frame = Frame(self.frame_index, delta, self.elapsed) + self.frame_index += 1 + return frame, steps + +@dataclass +class EngineContext: + events: EventBus + scenes: SceneManager + resources: ResourceStore + network: NetworkBus + clock: SimulationClock + +class Engine: + """Application lifecycle and subsystem orchestration kernel.""" + def __init__(self, fixed_step: float = 1 / 60) -> None: + self.context = EngineContext(EventBus(), SceneManager(), ResourceStore(), NetworkBus(), SimulationClock(fixed_step)) + self._systems: list[Callable[[EngineContext, float], None]] = [] + self._running = False + self._last_time: float | None = None + + def add_system(self, system: Callable[[EngineContext, float], None]) -> None: + if system in self._systems: + return + self._systems.append(system) + + def start(self) -> None: + if self._running: + raise RuntimeError("engine is already running") + self._running = True + self._last_time = monotonic() + self.context.events.emit("engine.started") + self.context.events.dispatch() + + def tick(self, delta: float | None = None) -> Frame: + if not self._running: + raise RuntimeError("engine is not running") + if delta is None: + now = monotonic() + delta = 0.0 if self._last_time is None else now - self._last_time + self._last_time = now + frame, steps = self.context.clock.advance(delta) + for _ in range(steps): + for system in tuple(self._systems): + system(self.context, self.context.clock.fixed_step) + self.context.events.dispatch() + return frame + + def stop(self) -> None: + if not self._running: + return + self._running = False + self.context.events.emit("engine.stopped") + self.context.events.dispatch() + self._last_time = None + + @property + def running(self) -> bool: + return self._running + + +@dataclass(frozen=True) +class RenderCommand: + kind: str + payload: tuple[Any, ...] = () + + +@dataclass(frozen=True) +class UICommand: + kind: str + payload: tuple[Any, ...] = () + + +@dataclass(frozen=True) +class AudioCommand: + kind: str + payload: tuple[Any, ...] = () + + +class RenderQueue: + def __init__(self) -> None: + self._commands: list[RenderCommand] = [] + + def submit(self, kind: str, *payload: Any) -> None: + self._commands.append(RenderCommand(kind, tuple(payload))) + + def drain(self) -> tuple[RenderCommand, ...]: + commands = tuple(self._commands) + self._commands.clear() + return commands + + +class UIQueue: + def __init__(self) -> None: + self._commands: list[UICommand] = [] + + def submit(self, kind: str, *payload: Any) -> None: + self._commands.append(UICommand(kind, tuple(payload))) + + def drain(self) -> tuple[UICommand, ...]: + commands = tuple(self._commands) + self._commands.clear() + return commands + + +class AudioQueue: + def __init__(self) -> None: + self._commands: list[AudioCommand] = [] + + def submit(self, kind: str, *payload: Any) -> None: + self._commands.append(AudioCommand(kind, tuple(payload))) + + def drain(self) -> tuple[AudioCommand, ...]: + commands = tuple(self._commands) + self._commands.clear() + return commands + + +@dataclass +class EngineServices: + """Backend-neutral service boundaries owned by an Engine instance.""" + render: RenderQueue = field(default_factory=RenderQueue) + ui: UIQueue = field(default_factory=UIQueue) + audio: AudioQueue = field(default_factory=AudioQueue) + + +def create_default_engine(fixed_step: float = 1 / 60) -> Engine: + engine = Engine(fixed_step) + services = EngineServices() + engine.context.resources.put("services", services) + engine.context.resources.put("render", services.render) + engine.context.resources.put("ui", services.ui) + engine.context.resources.put("audio", services.audio) + return engine + + +@dataclass(frozen=True) +class Asset: + name: str + kind: str + source: str + + +class AssetCatalog: + """Declarative asset registry; actual decoding belongs to backend phases.""" + def __init__(self) -> None: + self._assets: dict[str, Asset] = {} + + def register(self, name: str, kind: str, source: str) -> Asset: + if name in self._assets: + raise ValueError(f"asset already registered: {name}") + asset = Asset(name, kind, source) + self._assets[name] = asset + return asset + + def get(self, name: str) -> Asset: + try: + return self._assets[name] + except KeyError as error: + raise KeyError(f"asset not found: {name}") from error + + def names(self) -> tuple[str, ...]: + return tuple(sorted(self._assets)) + + +class Application: + """Application-level lifecycle built on the engine kernel.""" + def __init__(self, engine: Engine | None = None) -> None: + self.engine = engine or create_default_engine() + self._configured = False + + def configure(self) -> None: + self._configured = True + + def run_frame(self, delta: float) -> Frame: + if not self._configured: + self.configure() + if not self.engine.running: + self.engine.start() + return self.engine.tick(delta) + + def shutdown(self) -> None: + self.engine.stop() diff --git a/src/axiom/lexer.py b/src/axiom/lexer.py index 6b64c70..f4e937c 100644 --- a/src/axiom/lexer.py +++ b/src/axiom/lexer.py @@ -8,6 +8,8 @@ class TokenKind(Enum): FN = auto() + IMPORT = auto() + MODULE = auto() LET = auto() RETURN = auto() IF = auto() @@ -64,7 +66,7 @@ class LexError(ValueError): """Raised when source text cannot be converted into tokens.""" -_KEYWORDS = {"fn": TokenKind.FN, "let": TokenKind.LET, "return": TokenKind.RETURN, "if": TokenKind.IF, "else": TokenKind.ELSE, "while": TokenKind.WHILE, "for": TokenKind.FOR, "break": TokenKind.BREAK, "continue": TokenKind.CONTINUE, "struct": TokenKind.STRUCT, "true": TokenKind.TRUE, "false": TokenKind.FALSE} +_KEYWORDS = {"fn": TokenKind.FN, "import": TokenKind.IMPORT, "module": TokenKind.MODULE, "let": TokenKind.LET, "return": TokenKind.RETURN, "if": TokenKind.IF, "else": TokenKind.ELSE, "while": TokenKind.WHILE, "for": TokenKind.FOR, "break": TokenKind.BREAK, "continue": TokenKind.CONTINUE, "struct": TokenKind.STRUCT, "true": TokenKind.TRUE, "false": TokenKind.FALSE} def lex(source: str) -> list[Token]: diff --git a/src/axiom/new_parser.py b/src/axiom/new_parser.py new file mode 100644 index 0000000..a4ecb02 --- /dev/null +++ b/src/axiom/new_parser.py @@ -0,0 +1,45 @@ +"""Canonical AXIOM 0.1 frontend migration layer.""" +from __future__ import annotations +from .ast import Call, Function, Program + +class NewSyntaxError(ValueError): + pass + +def _translate(text: str) -> str: + if text.startswith("mostrar(") and text.endswith(")"): + return "print(" + text[len("mostrar("):] + return text + +def parse_new(source: str) -> Program: + from .parser import parse as parse_legacy + lines = [line.strip() for line in source.splitlines() if line.strip()] + if not lines: + raise NewSyntaxError("programa vacío") + body = [] + names = set() + for number, text in enumerate(lines, 1): + if text.startswith(("decidir ", "repetir ", "cuando ", "usar ", "ofrecer ", "necesita:", "puede:", "prefiere:", "restringir:", "demostrar:", "modo:")): + raise NewSyntaxError(f"construcción todavía no conectada al pipeline en línea {number}") + text = _translate(text) + if "=" in text and not any(op in text for op in ("==", ">=", "<=")): + name, value = text.split("=", 1) + name = name.strip() + if not name.isidentifier(): + raise NewSyntaxError(f"identificador inválido en línea {number}") + prefix = "let " if name not in names else "" + if prefix: + names.add(name) + text = prefix + name + " = " + value.strip() + parsed = parse_legacy("fn main() { " + text + " }") + statements = parsed.functions[0].body + if len(statements) != 1: + raise NewSyntaxError(f"expresión inválida en línea {number}") + statement = statements[0] + if not isinstance(statement, (Call,)) and not text.startswith("let "): + raise NewSyntaxError(f"se esperaba llamada o asignación en línea {number}") + body.append(statement) + return Program([Function("main", body)]) + +def looks_like_new_syntax(source: str) -> bool: + lines = [line.strip() for line in source.splitlines() if line.strip()] + return bool(lines) and not any(line.startswith(("fn ", "module ", "import ", "struct ", "let ", "if ", "while ", "for ")) for line in lines) diff --git a/src/axiom/package.py b/src/axiom/package.py new file mode 100644 index 0000000..9c80bd1 --- /dev/null +++ b/src/axiom/package.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +import json +import re +import shutil +import tomllib +from dataclasses import dataclass +from pathlib import Path + +_PACKAGE_NAME = re.compile(r"^[a-z][a-z0-9_-]*$") + + +class PackageError(ValueError): + pass + + +@dataclass(frozen=True) +class Package: + name: str + version: str + root: Path + + +def _read_manifest(path: Path) -> Package: + try: + data = tomllib.loads(path.read_text(encoding="utf-8")) + except (OSError, tomllib.TOMLDecodeError) as error: + raise PackageError(f"invalid package manifest: {error}") from error + package = data.get("package") + if not isinstance(package, dict): + raise PackageError("package manifest requires [package]") + name = package.get("name") + version = package.get("version") + if not isinstance(name, str) or not _PACKAGE_NAME.fullmatch(name): + raise PackageError("package name is invalid") + if not isinstance(version, str) or not version: + raise PackageError("package version is invalid") + return Package(name, version, path.parent) + + +def _write_project_manifest(path: Path, project_name: str, dependencies: dict[str, str]) -> None: + lines = [ + "[package]", + f'name = "{project_name}"', + 'version = "0.1.0"', + "", + "[dependencies]", + ] + lines.extend(f'{name} = "{version}"' for name, version in sorted(dependencies.items())) + path.write_text(chr(10).join(lines) + chr(10), encoding="utf-8") + + +def add_package(project_root: Path, name: str, registry_root: Path) -> Package: + if not _PACKAGE_NAME.fullmatch(name): + raise PackageError(f"invalid package name '{name}'") + + project_manifest = project_root / "axiom.toml" + if project_manifest.exists(): + project = tomllib.loads(project_manifest.read_text(encoding="utf-8")) + project_data = project.get("package", {}) + project_name = project_data.get("name", project_root.name) + dependencies = dict(project.get("dependencies", {})) + else: + project_name = project_root.name + dependencies = {} + _write_project_manifest(project_manifest, project_name, dependencies) + + package_manifest = registry_root / name / "axiom.toml" + package = _read_manifest(package_manifest) + + vendor_root = project_root / "vendor" + destination = vendor_root / package.name + if destination.exists(): + shutil.rmtree(destination) + destination.parent.mkdir(parents=True, exist_ok=True) + shutil.copytree(package.root, destination) + + dependencies[package.name] = package.version + _write_project_manifest(project_manifest, project_name, dependencies) + + lock = { + "version": 1, + "packages": { + dependency: {"version": version, "source": "local-registry"} + for dependency, version in sorted(dependencies.items()) + }, + } + (project_root / "axiom.lock").write_text( + json.dumps(lock, indent=2, sort_keys=True) + chr(10), + encoding="utf-8", + ) + return package diff --git a/src/axiom/parser.py b/src/axiom/parser.py index 04e11b4..3eaa694 100644 --- a/src/axiom/parser.py +++ b/src/axiom/parser.py @@ -18,12 +18,31 @@ def __init__(self, tokens: list[Token]): def parse(self) -> Program: functions = [] structs = [] + module_name = None + imports = [] while not self._check(TokenKind.EOF): - if self._check(TokenKind.STRUCT): + if self._check(TokenKind.MODULE): + if module_name is not None: + self._error(self._current(), "duplicate module declaration") + self.position += 1 + module_name = self._qualified_name() + elif self._check(TokenKind.IMPORT): + self.position += 1 + imports.append(self._qualified_name()) + elif self._check(TokenKind.STRUCT): structs.append(self._struct()) else: functions.append(self._function()) - return Program(functions, structs) + if self._check(TokenKind.SEMICOLON): + self.position += 1 + return Program(functions, structs, module_name, imports) + + def _qualified_name(self) -> str: + parts = [self._consume(TokenKind.IDENTIFIER, "expected module name").lexeme] + while self._check(TokenKind.DOT): + self.position += 1 + parts.append(self._consume(TokenKind.IDENTIFIER, "expected name after '.'").lexeme) + return ".".join(parts) def _struct(self) -> StructDefinition: self._consume(TokenKind.STRUCT, "expected 'struct'") @@ -333,4 +352,7 @@ def _error(token: Token, message: str) -> None: def parse(source: str) -> Program: + from .new_parser import looks_like_new_syntax, parse_new + if looks_like_new_syntax(source): + return parse_new(source) return Parser(lex(source)).parse() \ No newline at end of file diff --git a/src/axiom/runtime.py b/src/axiom/runtime.py index 8ec7018..d7e641b 100644 --- a/src/axiom/runtime.py +++ b/src/axiom/runtime.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Callable +from contextvars import ContextVar from .ast import ArrayLiteral, Assign, Binary, BooleanLiteral, Break, Call, Continue, FieldAccess, FloatLiteral, For, Function, If, Index, IntegerLiteral, Let, Program, Return, StringLiteral, StructLiteral, Unary, Variable, While from .ir import BreakInstruction, CallInstruction, ContinueInstruction, ForInstruction, IRFunction, IRProgram, IfInstruction, LetInstruction, ReturnInstruction, SetInstruction, WhileInstruction @@ -12,35 +13,51 @@ class RuntimeError(ValueError): """Raised when an AXIOM program fails during IR execution.""" -def execute(program: IRProgram, emit: Callable[[str], None] = print) -> None: +_TRACE: ContextVar[Callable[[dict], None] | None] = ContextVar("axiom_runtime_trace", default=None) + + +def _trace(event: dict) -> None: + callback = _TRACE.get() + if callback is not None: + callback(event) + + +def execute(program: IRProgram, emit: Callable[[str], None] = print, trace: Callable[[dict], None] | None = None) -> None: """Execute an IR program using the host output function.""" - functions = {function.name: function for function in program.functions} - main = IRFunction("main", [], program.instructions) - _invoke_ir(main, [], functions | {"main": main}, emit) + token = _TRACE.set(trace) + try: + functions = {function.name: function for function in program.functions} + main = IRFunction("main", [], program.instructions) + _invoke_ir(main, [], functions | {"main": main}, emit) + finally: + _TRACE.reset(token) def _invoke_ir(function, arguments, functions, emit): + _trace({"event": "function_enter", "function": function.name}) variables = {name: value for name, value in zip(function.parameters, arguments)} - returned, value = _execute_block(function.body, variables, functions, emit) + returned, value = _execute_block(function.body, variables, functions, emit, function.name) + _trace({"event": "function_exit", "function": function.name}) return value if returned else None -def _execute_block(instructions, variables, functions, emit): - for instruction in instructions: +def _execute_block(instructions, variables, functions, emit, function_name="main"): + for index, instruction in enumerate(instructions): + _trace({"event": "instruction", "function": function_name, "index": index, "instruction": type(instruction).__name__}) if isinstance(instruction, LetInstruction): variables[instruction.name] = _evaluate(instruction.value, variables, functions, emit) elif isinstance(instruction, SetInstruction): _assign(instruction.target, _evaluate(instruction.value, variables, functions, emit), variables, functions, emit) elif isinstance(instruction, IfInstruction): branch = instruction.then_body if _evaluate(instruction.condition, variables, functions, emit) else instruction.else_body - status, value = _execute_block(branch, variables, functions, emit) + status, value = _execute_block(branch, variables, functions, emit, function_name) if status is _BREAK or status is _CONTINUE: return status, None if status: return True, value elif isinstance(instruction, WhileInstruction): while _evaluate(instruction.condition, variables, functions, emit): - status, value = _execute_block(instruction.body, variables, functions, emit) + status, value = _execute_block(instruction.body, variables, functions, emit, function_name) if status is _BREAK: break if status is _CONTINUE: continue if status: @@ -48,14 +65,14 @@ def _execute_block(instructions, variables, functions, emit): elif isinstance(instruction, ForInstruction): _execute_block(instruction.initializer, variables, functions, emit) while _evaluate(instruction.condition, variables, functions, emit): - status, value = _execute_block(instruction.body, variables, functions, emit) + status, value = _execute_block(instruction.body, variables, functions, emit, function_name) if status is _BREAK: break if status is _CONTINUE: - _execute_block(instruction.update, variables, functions, emit) + _execute_block(instruction.update, variables, functions, emit, function_name) continue if status: return True, value - _execute_block(instruction.update, variables, functions, emit) + _execute_block(instruction.update, variables, functions, emit, function_name) elif isinstance(instruction, BreakInstruction): return _BREAK, None elif isinstance(instruction, ContinueInstruction): @@ -250,6 +267,8 @@ def _evaluate(expression, variables, functions=None, emit=print): return -_evaluate(expression.operand, variables, functions, emit) if isinstance(expression, Call): arguments = [_evaluate(argument, variables, functions, emit) for argument in expression.arguments] + if expression.name in {"len", "abs", "min", "max"}: + return _standard_library_call(expression.name, arguments) if isinstance(functions[expression.name], IRFunction): return _invoke_ir(functions[expression.name], arguments, functions, emit) return _invoke(functions[expression.name], arguments, functions, emit) @@ -260,6 +279,12 @@ def _assign(target, value, variables, functions=None, emit=print): if isinstance(target, Variable): variables[target.name] = value return + if isinstance(target, FieldAccess): + container = _evaluate(target.target, variables, functions, emit) + if not isinstance(container, dict) or target.field not in container: + raise RuntimeError("unknown field") + container[target.field] = value + return if isinstance(target, Index): container = _evaluate(target.target, variables, functions, emit) index = _evaluate(target.index, variables, functions, emit) @@ -270,4 +295,14 @@ def _assign(target, value, variables, functions=None, emit=print): except (IndexError, TypeError): raise RuntimeError("index out of bounds") from None return - raise RuntimeError("invalid assignment target") \ No newline at end of file + raise RuntimeError("invalid assignment target") +def _standard_library_call(name, arguments): + if name == "len": + return len(arguments[0]) + if name == "abs": + return abs(arguments[0]) + if name == "min": + return min(arguments) + if name == "max": + return max(arguments) + raise RuntimeError(f"unknown standard library function '{name}'") diff --git a/src/axiom/semantic.py b/src/axiom/semantic.py index 9ce90d3..8a87a41 100644 --- a/src/axiom/semantic.py +++ b/src/axiom/semantic.py @@ -11,6 +11,7 @@ class SemanticError(ValueError): _BUILTIN_TYPES = {"Int", "Float", "Bool", "String"} _STRUCTS: dict[str, dict[str, str]] = {} +_STANDARD_LIBRARY = {'len', 'abs', 'min', 'max'} def _is_known_type(type_name: str) -> bool: @@ -181,6 +182,8 @@ def _check_block(statements, variables, signatures, function, loop_depth=0): def _check_call(call: Call, variables: dict[str, str], signatures) -> str: + if call.name in _STANDARD_LIBRARY: + return _check_builtin(call, variables, signatures) if call.name not in signatures: raise SemanticError(f"unknown function '{call.name}'") parameter_types, return_type = signatures[call.name] @@ -194,6 +197,24 @@ def _check_call(call: Call, variables: dict[str, str], signatures) -> str: return return_type + +def _check_builtin(call: Call, variables: dict[str, str], signatures) -> str: + argument_types = [_expression_type(argument, variables, signatures) for argument in call.arguments] + if call.name == "len": + if len(argument_types) != 1 or (argument_types[0] != "String" and not argument_types[0].endswith("[]")): + raise SemanticError("len expects a String or array") + return "Int" + if call.name == "abs": + if len(argument_types) != 1 or argument_types[0] not in {"Int", "Float"}: + raise SemanticError("abs expects an Int or Float") + return argument_types[0] + if call.name in {"min", "max"}: + if len(argument_types) != 2 or argument_types[0] != argument_types[1] or argument_types[0] not in {"Int", "Float"}: + raise SemanticError(f"{call.name} expects two matching numeric arguments") + return argument_types[0] + raise SemanticError(f"unknown standard library function '{call.name}'") + + def _expression_type(expression, variables: dict[str, str], signatures) -> str: if isinstance(expression, StringLiteral): return "String" diff --git a/src/axiom/studio.py b/src/axiom/studio.py new file mode 100644 index 0000000..1607bda --- /dev/null +++ b/src/axiom/studio.py @@ -0,0 +1,512 @@ +"""Local AXIOM Studio workspace and language-service primitives.""" + +from __future__ import annotations + +import argparse +import io +import json +import re +import subprocess +import time +import zipfile +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from urllib.parse import urlparse + +from .ast import Function, Program +from .lexer import LexError, lex +from .parser import ParseError, parse +from .ir import lower +from .runtime import execute +from .package import PackageError, add_package +from .semantic import SemanticError, analyze + +_KEYWORDS = { + "fn", + "let", + "return", + "if", + "else", + "while", + "for", + "break", + "continue", + "struct", + "true", + "false", +} +_TYPES = {"Int", "Float", "Bool", "String"} +_BUILTINS = {"print", "len", "abs", "min", "max"} + + +def _diagnostic(error: Exception) -> dict[str, str]: + message = str(error) + line = "1" + column = "1" + if " at " in message: + message, location = message.rsplit(" at ", 1) + if ":" in location: + line, column = location.split(":", 1) + return {"severity": "error", "message": message, "line": line, "column": column} + + +def check_source(source: str) -> dict[str, object]: + try: + program = parse(source) + analyze(program) + except (LexError, ParseError, SemanticError) as error: + return {"ok": False, "diagnostics": [_diagnostic(error)]} + return {"ok": True, "diagnostics": [], "functions": [function.name for function in program.functions]} + + +def _function_names(program: Program) -> set[str]: + return {function.name for function in program.functions if isinstance(function, Function)} + + +def complete_source(source: str, prefix: str = "") -> list[str]: + candidates = sorted(_KEYWORDS | _TYPES | _BUILTINS) + try: + program = parse(source) + except (LexError, ParseError): + program = None + if program is not None: + candidates.extend(_function_names(program)) + candidates.extend(struct.name for struct in program.structs) + else: + candidates.extend(re.findall(r"\bfn\s+([A-Za-z_][A-Za-z0-9_]*)", source)) + candidates.extend(re.findall(r"\bstruct\s+([A-Za-z_][A-Za-z0-9_]*)", source)) + return sorted({item for item in candidates if item.startswith(prefix)}) + + + +def run_source(source: str) -> dict[str, object]: + output = io.StringIO() + try: + program = parse(source) + analyze(program) + execute(lower(program), emit=lambda value: output.write(value + "\n")) + except (LexError, ParseError, SemanticError, ValueError) as error: + return {"ok": False, "diagnostics": [_diagnostic(error)], "output": output.getvalue()} + return {"ok": True, "diagnostics": [], "output": output.getvalue()} + + +def run_workspace_tests(root: Path) -> dict[str, object]: + failures: list[dict[str, str]] = [] + sources = sorted(path for path in root.rglob("*.ax") if ".git" not in path.parts) + for path in sources: + try: + program = parse(path.read_text(encoding="utf-8")) + analyze(program) + execute(lower(program), emit=lambda _: None) + except (OSError, LexError, ParseError, SemanticError, ValueError) as error: + failures.append({"path": path.relative_to(root).as_posix(), **_diagnostic(error)}) + return {"ok": not failures, "count": len(sources), "failures": failures} + + +def git_status(root: Path) -> dict[str, object]: + try: + result = subprocess.run( + ["git", "status", "--short", "--branch"], + cwd=root, + capture_output=True, + text=True, + check=False, + ) + except OSError as error: + return {"ok": False, "error": str(error)} + return {"ok": result.returncode == 0, "output": result.stdout, "error": result.stderr} + + +def package_workspace(root: Path, output: Path | None = None) -> dict[str, object]: + manifest = root / "axiom.toml" + if not manifest.exists(): + return {"ok": False, "error": "axiom.toml not found"} + target = output or root.with_suffix(".axpkg") + try: + with zipfile.ZipFile(target, "w", compression=zipfile.ZIP_DEFLATED) as archive: + for path in root.rglob("*"): + relative = path.relative_to(root) + if not path.is_file(): + continue + if target.parent == root and relative == target.relative_to(root): + continue + if any(part in {".git", "__pycache__", "target"} for part in relative.parts) or path.suffix == ".axpkg": + continue + archive.write(path, relative.as_posix()) + except OSError as error: + return {"ok": False, "error": str(error)} + return {"ok": True, "output": str(target)} + +def debug_source(source: str, breakpoints: list[dict[str, object]] | None = None) -> dict[str, object]: + output = io.StringIO() + events: list[dict[str, object]] = [] + hits: list[dict[str, object]] = [] + configured = breakpoints or [] + + def trace(event: dict) -> None: + if event.get("event") != "instruction": + return + record = dict(event) + events.append(record) + if any(item.get("function") == event.get("function") and item.get("index") == event.get("index") for item in configured): + hits.append(record) + + try: + program = parse(source) + analyze(program) + execute(lower(program), emit=lambda value: output.write(value + "\n"), trace=trace) + except (LexError, ParseError, SemanticError, ValueError) as error: + return {"ok": False, "diagnostics": [_diagnostic(error)], "output": output.getvalue(), "events": events, "breakpoints": hits} + return {"ok": True, "diagnostics": [], "output": output.getvalue(), "events": events, "breakpoints": hits} + + +def profile_source(source: str) -> dict[str, object]: + output = io.StringIO() + functions: dict[str, dict[str, float | int]] = {} + stack: list[tuple[str, float]] = [] + instructions: dict[str, int] = {} + + def trace(event: dict) -> None: + kind = event.get("event") + name = str(event.get("function", "main")) + if kind == "function_enter": + stack.append((name, time.perf_counter())) + functions.setdefault(name, {"calls": 0, "seconds": 0.0}) + functions[name]["calls"] += 1 + elif kind == "instruction": + key = f"{name}:{event.get('instruction')}" + instructions[key] = instructions.get(key, 0) + 1 + elif kind == "function_exit" and stack: + entered, started = stack.pop() + functions[entered]["seconds"] += time.perf_counter() - started + + try: + program = parse(source) + analyze(program) + started = time.perf_counter() + execute(lower(program), emit=lambda value: output.write(value + "\n"), trace=trace) + elapsed = time.perf_counter() - started + except (LexError, ParseError, SemanticError, ValueError) as error: + return {"ok": False, "diagnostics": [_diagnostic(error)], "output": output.getvalue()} + return { + "ok": True, + "diagnostics": [], + "output": output.getvalue(), + "elapsed_seconds": elapsed, + "functions": [{"name": name, "calls": data["calls"], "seconds": data["seconds"]} for name, data in sorted(functions.items())], + "instructions": [{"location": name, "count": count} for name, count in sorted(instructions.items())], + } + + +def run_source_file(root: Path, relative: str) -> dict[str, object]: + try: + path = (root / relative).resolve() + if root.resolve() not in path.parents or path.suffix != ".ax": + raise ValueError("invalid source path") + source = path.read_text(encoding="utf-8") + except (OSError, ValueError) as error: + return {"ok": False, "diagnostics": [{"severity": "error", "message": str(error), "line": "1", "column": "1"}]} + return run_source(source) + + +def package_info(root: Path) -> dict[str, object]: + manifest = root / "axiom.toml" + lock = root / "axiom.lock" + return { + "manifest": manifest.read_text(encoding="utf-8") if manifest.exists() else None, + "lock": json.loads(lock.read_text(encoding="utf-8")) if lock.exists() else None, + "vendor": sorted(path.name for path in (root / "vendor").iterdir() if path.is_dir()) if (root / "vendor").exists() else [], + } + + +def git_diff(root: Path) -> dict[str, object]: + try: + result = subprocess.run(["git", "diff", "--stat", "--", "."], cwd=root, capture_output=True, text=True, check=False) + except OSError as error: + return {"ok": False, "error": str(error)} + return {"ok": result.returncode == 0, "output": result.stdout, "error": result.stderr} + + +def terminal_command(root: Path, command: str) -> dict[str, object]: + commands = { + "pwd": ["pwd"], + "ls": ["ls", "-la"], + "git status": ["git", "status", "--short", "--branch"], + "git diff": ["git", "diff", "--stat", "--", "."], + "git branch": ["git", "branch", "--show-current"], + "axiom test": ["python", "-m", "axiom", "test"], + "axiom package": ["python", "-m", "axiom", "package"], + } + argv = commands.get(command.strip()) + if argv is None: + return {"ok": False, "error": "command not allowed"} + try: + result = subprocess.run(argv, cwd=root, capture_output=True, text=True, check=False, timeout=30) + except (OSError, subprocess.TimeoutExpired) as error: + return {"ok": False, "error": str(error)} + return {"ok": result.returncode == 0, "code": result.returncode, "output": result.stdout, "error": result.stderr} + + +def documentation_index(root: Path) -> dict[str, object]: + docs = sorted(path.relative_to(root).as_posix() for path in (root / "docs").rglob("*.md") if path.is_file()) if (root / "docs").exists() else [] + return {"docs": docs} + + +def documentation_file(root: Path, relative: str) -> dict[str, object]: + try: + path = (root / relative).resolve() + if root.resolve() not in path.parents or path.suffix != ".md" or "docs" not in path.relative_to(root).parts: + raise ValueError("invalid documentation path") + return {"path": relative, "content": path.read_text(encoding="utf-8")} + except (OSError, ValueError) as error: + return {"error": str(error)} + + +def visual_ir(source: str) -> dict[str, object]: + try: + program = parse(source) + analyze(program) + return {"ok": True, "ir": lower(program).render()} + except (LexError, ParseError, SemanticError) as error: + return {"ok": False, "diagnostics": [_diagnostic(error)]} + + +def workspace_info(root: Path) -> dict[str, object]: + root = root.resolve() + manifest = root / "axiom.toml" + sources = sorted(path.relative_to(root).as_posix() for path in root.rglob("*.ax") if ".git" not in path.parts) + return { + "root": str(root), + "manifest": manifest.exists(), + "sources": sources, + } + +_INDEX_HTML = """ + + + + +AXIOM Studio + + + +
AXIOM Studio Ready
+
+ +
+ +
+ + +""" + + +class StudioHandler(BaseHTTPRequestHandler): + root = Path.cwd() + + def _send_json(self, payload: dict[str, object], status: int = 200) -> None: + body = json.dumps(payload).encode("utf-8") + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def do_GET(self) -> None: + parsed = urlparse(self.path) + if parsed.path == "/": + body = _INDEX_HTML.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + if parsed.path == "/api/workspace": + self._send_json(workspace_info(self.root)) + return + if parsed.path == "/api/git": + self._send_json(git_status(self.root)) + return + if parsed.path == "/api/git/diff": + self._send_json(git_diff(self.root)) + return + if parsed.path == "/api/package/info": + self._send_json(package_info(self.root)) + return + if parsed.path == "/api/docs": + self._send_json(documentation_index(self.root)) + return + if parsed.path == "/api/doc": + from urllib.parse import parse_qs + relative = parse_qs(parsed.query).get("path", [""])[0] + self._send_json(documentation_file(self.root, relative)) + return + if parsed.path == "/api/source": + from urllib.parse import parse_qs + relative = parse_qs(parsed.query).get("path", [""])[0] + path = (self.root / relative).resolve() + if self.root not in path.parents or path.suffix != ".ax": + self._send_json({"error": "invalid source path"}, 400) + return + try: + source = path.read_text(encoding="utf-8") + except OSError as error: + self._send_json({"error": str(error)}, 404) + return + self._send_json({"path": relative, "source": source}) + return + self._send_json({"error": "not found"}, 404) + + def do_POST(self) -> None: + endpoint = urlparse(self.path).path + allowed = {"/api/check", "/api/complete", "/api/run", "/api/debug", "/api/profile", "/api/test", + "/api/test-file", "/api/package", "/api/package/add", "/api/terminal", "/api/ir"} + if endpoint not in allowed: + self._send_json({"error": "not found"}, 404) + return + if endpoint == "/api/test": + self._send_json(run_workspace_tests(self.root)) + return + if endpoint == "/api/package": + self._send_json(package_workspace(self.root)) + return + try: + length = int(self.headers.get("Content-Length", "0")) + payload = json.loads(self.rfile.read(length).decode("utf-8")) + except (ValueError, json.JSONDecodeError) as error: + self._send_json({"error": str(error)}, 400) + return + if endpoint == "/api/test-file": + relative = payload.get("path", "") + self._send_json(run_source_file(self.root, relative) if isinstance(relative, str) else {"ok": False, "error": "path must be a string"}) + return + if endpoint == "/api/package/add": + name = payload.get("name", "") + registry = payload.get("registry", "") + if not isinstance(name, str) or not isinstance(registry, str): + self._send_json({"error": "name and registry must be strings"}, 400) + return + try: + package = add_package(self.root, name, Path(registry).resolve()) + except (PackageError, OSError) as error: + self._send_json({"ok": False, "error": str(error)}, 400) + return + self._send_json({"ok": True, "name": package.name, "version": package.version}) + return + if endpoint == "/api/terminal": + command = payload.get("command", "") + self._send_json(terminal_command(self.root, command) if isinstance(command, str) else {"ok": False, "error": "command must be a string"}) + return + source = payload.get("source") + if not isinstance(source, str): + self._send_json({"error": "source must be a string"}, 400) + return + if endpoint == "/api/check": + self._send_json(check_source(source)) + return + if endpoint == "/api/run": + self._send_json(run_source(source)) + return + if endpoint == "/api/debug": + breakpoints = payload.get("breakpoints", []) + self._send_json(debug_source(source, breakpoints if isinstance(breakpoints, list) else [])) + return + if endpoint == "/api/profile": + self._send_json(profile_source(source)) + return + if endpoint == "/api/ir": + self._send_json(visual_ir(source)) + return + prefix = payload.get("prefix", "") + if not isinstance(prefix, str): + self._send_json({"error": "prefix must be a string"}, 400) + return + self._send_json({"items": complete_source(source, prefix)}) + + def log_message(self, format: str, *args) -> None: + return + + +def serve(root: Path, host: str, port: int) -> None: + StudioHandler.root = root.resolve() + server = ThreadingHTTPServer((host, port), StudioHandler) + print(f"AXIOM Studio: http://{host}:{port}") + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +def build_parser() -> argparse.ArgumentParser: + parser = argparse.ArgumentParser(prog="axiom studio", description="run AXIOM Studio") + parser.add_argument("path", type=Path, nargs="?", default=Path(".")) + parser.add_argument("--host", default="127.0.0.1") + parser.add_argument("--port", type=int, default=8765) + return parser diff --git a/src/axiom/types.py b/src/axiom/types.py new file mode 100644 index 0000000..0bd96b3 --- /dev/null +++ b/src/axiom/types.py @@ -0,0 +1,53 @@ +"""Canonical type model shared by AXIOM compiler and tooling.""" + +from __future__ import annotations + +from dataclasses import dataclass +from enum import Enum + +class PrimitiveType(str, Enum): + INT = "Int" + FLOAT = "Float" + BOOL = "Bool" + STRING = "String" + +@dataclass(frozen=True) +class Type: + """Stable structural representation of an AXIOM type.""" + + name: str + element: "Type | None" = None + + @classmethod + def primitive(cls, value: PrimitiveType | str) -> "Type": + return cls(value.value if isinstance(value, PrimitiveType) else value) + + @classmethod + def array(cls, element: "Type") -> "Type": + return cls("Array", element) + + @classmethod + def named(cls, name: str) -> "Type": + return cls(name) + + def render(self) -> str: + if self.name == "Array" and self.element is not None: + return f"{self.element.render()}[]" + return self.name + + def __str__(self) -> str: + return self.render() + + +def parse_type_name(name: str) -> Type: + if not name: + raise ValueError("type name cannot be empty") + if name.endswith("[]"): + return Type.array(parse_type_name(name[:-2])) + if name in {item.value for item in PrimitiveType}: + return Type.primitive(name) + return Type.named(name) + + +def type_name(value: Type) -> str: + return value.render() diff --git a/tests/test_cli.py b/tests/test_cli.py index ebd0d09..fd1c0cd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1,3 +1,7 @@ +from pathlib import Path +import zipfile + +import pytest from axiom.cli import main from axiom.ast import BooleanLiteral, Call, Function, IntegerLiteral, Program, StringLiteral from axiom.ast import Binary @@ -5,8 +9,61 @@ from axiom.ir import IRProgram, IfInstruction, LetInstruction, PrintInstruction, SetInstruction, WhileInstruction, lower from axiom.lexer import LexError, TokenKind, lex from axiom.parser import ParseError, parse -from axiom.runtime import execute +from axiom.runtime import execute, execute_program from axiom.semantic import SemanticError, analyze +from axiom.studio import check_source, complete_source, debug_source, documentation_index, git_diff, package_info, package_workspace, profile_source, run_source, run_workspace_tests, terminal_command, run_source_file, visual_ir, workspace_info + + +def test_new_creates_project(tmp_path, capsys): + project = tmp_path / "hello" + assert main(["new", str(project)]) == 0 + assert "created:" in capsys.readouterr().out + assert (project / "axiom.toml").exists() + assert (project / "src/main.ax").read_text(encoding="utf-8") == 'fn main() { print("Hello AXIOM") }\n' + + +def test_test_command_runs_axiom_sources(tmp_path, capsys): + tests_path = tmp_path / "tests" + tests_path.mkdir() + source = tests_path / "hello.ax" + source.write_text('fn main() { print("ok") }', encoding="utf-8") + assert main(["test", str(tests_path)]) == 0 + assert "test ok: 1 source file(s)" in capsys.readouterr().out + + +def test_package_creates_archive(tmp_path, monkeypatch, capsys): + monkeypatch.chdir(tmp_path) + (tmp_path / "axiom.toml").write_text( + '[package]\nname = "demo"\nversion = "0.1.0"\n', + encoding="utf-8", + ) + output = tmp_path / "demo.axpkg" + assert main(["package", "-o", str(output)]) == 0 + assert "packaged:" in capsys.readouterr().out + assert output.exists() + with zipfile.ZipFile(output) as archive: + names = set(archive.namelist()) + assert "axiom.toml" in names + assert all(not name.startswith(".git/") for name in names) + + +def test_add_package_from_local_registry(tmp_path, monkeypatch, capsys): + registry = tmp_path / "registry" + package = registry / "networking" + package.mkdir(parents=True) + (package / "axiom.toml").write_text( + '[package]\nname = "networking"\nversion = "0.1.0"\n', + encoding="utf-8", + ) + project = tmp_path / "project" + project.mkdir() + monkeypatch.chdir(project) + + assert main(["add", "networking", "--registry", str(registry)]) == 0 + assert "added: networking 0.1.0" in capsys.readouterr().out + assert (project / "vendor/networking/axiom.toml").exists() + assert 'networking = "0.1.0"' in (project / "axiom.toml").read_text(encoding="utf-8") + assert '"networking": {' in (project / "axiom.lock").read_text(encoding="utf-8") def test_doctor_reports_environment(capsys): @@ -95,6 +152,39 @@ def test_parser_builds_struct_literal_and_field_access(): assert program.structs[0].fields[0].name == "x" +def test_runtime_executes_standard_library(capsys): + source = 'fn main() { print(len("axiom")); print(abs(-7)); print(min(3, 5)); print(max(3.0, 5.0)) }' + program = parse(source) + analyze(program) + execute_program(program) + assert capsys.readouterr().out == "5\n7\n3\n5.0\n" + + +def test_semantic_rejects_invalid_standard_library_call(): + program = parse("fn main() { print(len(1)) }") + with pytest.raises(SemanticError, match="len expects a String or array"): + analyze(program) + + +def test_runtime_executes_struct_field_assignment(tmp_path, capsys): + source = tmp_path / "struct_assignment.ax" + source.write_text( + "struct Point { x: Int, y: Int } fn main() { let point: Point = Point { x: 10, y: 20 }; point.x = 42; print(point.x) }", + encoding="utf-8", + ) + assert main(["run", str(source)]) == 0 + assert capsys.readouterr().out == "42\n" + + +def test_semantic_analysis_rejects_unknown_struct_field_assignment(): + try: + analyze(parse("struct Point { x: Int } fn main() { let point: Point = Point { x: 10 }; point.y = 42 }")) + except SemanticError as error: + assert str(error) == "unknown field 'y'" + else: + raise AssertionError("expected SemanticError") + + def test_runtime_executes_struct_field_access(tmp_path, capsys): source = tmp_path / "structs.ax" source.write_text( @@ -478,4 +568,149 @@ def test_runtime_executes_break_and_continue(tmp_path, capsys): encoding="utf-8", ) assert main(["run", str(source)]) == 0 - assert capsys.readouterr().out == "0\n1\n3\n" \ No newline at end of file + assert capsys.readouterr().out == "0\n1\n3\n" + +def test_studio_checks_source_and_reports_diagnostics(): + assert check_source('fn main() { print("ok") }')["ok"] is True + result = check_source("fn main() { display(1) }") + assert result["ok"] is False + assert result["diagnostics"][0]["message"] == "unknown function 'display'" + + +def test_studio_completes_language_symbols(): + assert "print" in complete_source("fn main() { pri", "pri") + assert "Int" in complete_source("fn main() { let value: I", "I") + assert "main" in complete_source("fn main() { ma", "ma") + + +def test_studio_reports_workspace_sources(tmp_path): + (tmp_path / "axiom.toml").write_text("[package]\nname = \"demo\"\n", encoding="utf-8") + (tmp_path / "src").mkdir() + (tmp_path / "src/main.ax").write_text("fn main() {}", encoding="utf-8") + info = workspace_info(tmp_path) + assert info["manifest"] is True + assert info["sources"] == ["src/main.ax"] + + +def test_studio_runs_source_and_captures_output(): + result = run_source('fn main() { print("studio") }') + assert result == {"ok": True, "diagnostics": [], "output": "studio\n"} + + +def test_studio_tests_workspace(tmp_path): + (tmp_path / "one.ax").write_text('fn main() { print(1) }', encoding="utf-8") + result = run_workspace_tests(tmp_path) + assert result["ok"] is True + assert result["count"] == 1 + + +def test_studio_reports_git_status(tmp_path): + from axiom.studio import git_status + result = git_status(tmp_path) + assert result["ok"] is False + + +def test_studio_packages_workspace(tmp_path): + (tmp_path / "axiom.toml").write_text('[package]\nname = "demo"\nversion = "0.1.0"\n', encoding="utf-8") + (tmp_path / "src").mkdir() + (tmp_path / "src/main.ax").write_text('fn main() {}', encoding="utf-8") + output = tmp_path / "demo.axpkg" + result = package_workspace(tmp_path, output) + assert result["ok"] is True + assert output.exists() + + +def test_studio_debugger_traces_instructions_and_breakpoints(): + result = debug_source('fn main() { let x: Int = 1; print(x) }', [{"function": "main", "index": 0}]) + assert result["ok"] is True + assert result["events"] + assert result["breakpoints"][0]["index"] == 0 + + +def test_studio_profiler_collects_function_and_instruction_metrics(): + result = profile_source('fn helper() -> Int { return 3 } fn main() { print(helper()) }') + assert result["ok"] is True + assert any(item["name"] == "helper" for item in result["functions"]) + assert result["instructions"] + + +def test_studio_tests_single_source(tmp_path): + source = tmp_path / "main.ax" + source.write_text('fn main() { print("ok") }', encoding="utf-8") + result = run_source_file(tmp_path, "main.ax") + assert result["ok"] is True + + +def test_studio_package_info_and_docs(tmp_path): + (tmp_path / "docs").mkdir() + (tmp_path / "docs/guide.md").write_text("# Guide", encoding="utf-8") + result = documentation_index(tmp_path) + assert result["docs"] == ["docs/guide.md"] + info = package_info(tmp_path) + assert info["manifest"] is None + + +def test_studio_terminal_is_allowlisted(tmp_path): + result = terminal_command(tmp_path, "git status") + assert result["ok"] is False + assert terminal_command(tmp_path, "rm -rf /")["ok"] is False + + +def test_studio_visual_ir_and_git_diff(tmp_path): + result = visual_ir('fn main() { print("ir") }') + assert result["ok"] is True + assert "AXIOM-IR" in result["ir"] + diff = git_diff(tmp_path) + assert diff["ok"] is False + + +def test_core_type_model_is_structural(): + from axiom.types import Type, parse_type_name + assert parse_type_name("Int").render() == "Int" + assert parse_type_name("Int[][]").render() == "Int[][]" + assert Type.array(Type.named("Point")).render() == "Point[]" + + +def test_module_imports_compile_as_one_program(tmp_path): + from axiom.core import Compiler + source_root = tmp_path / "src" + source_root.mkdir() + (source_root / "main.ax").write_text( + "module main\nimport math\nfn main() { print(double(21)) }\n", + encoding="utf-8", + ) + (source_root / "math.ax").write_text( + "module math\nfn double(value: Int) -> Int { return value + value }\n", + encoding="utf-8", + ) + compilation = Compiler().compile_project(tmp_path) + assert [unit.module for unit in compilation.units] == ["main", "math"] + assert "PRINT double(21)" in compilation.ir.render() + + +def test_module_import_missing_is_reported(tmp_path): + from axiom.core import Compiler, CompilerError + source_root = tmp_path / "src" + source_root.mkdir() + (source_root / "main.ax").write_text( + "module main\nimport missing\nfn main() { print(1) }\n", + encoding="utf-8", + ) + with pytest.raises(CompilerError, match="module 'missing' not found"): + Compiler().compile_project(tmp_path) + + +def test_module_import_cycle_is_reported(tmp_path): + from axiom.core import Compiler, CompilerError + source_root = tmp_path / "src" + source_root.mkdir() + (source_root / "main.ax").write_text( + "module main\nimport util\nfn main() { print(1) }\n", + encoding="utf-8", + ) + (source_root / "util.ax").write_text( + "module util\nimport main\nfn helper() { print(2) }\n", + encoding="utf-8", + ) + with pytest.raises(CompilerError, match="cyclic module import"): + Compiler().compile_project(tmp_path) diff --git a/tests/test_engine.py b/tests/test_engine.py new file mode 100644 index 0000000..aef18d9 --- /dev/null +++ b/tests/test_engine.py @@ -0,0 +1,102 @@ +from axiom.engine import ( + Engine, + NetworkMessage, + Scene, + SimulationClock, + create_default_engine, +) + + +def test_engine_lifecycle_and_fixed_step(): + engine = create_default_engine(fixed_step=0.1) + ticks = [] + engine.add_system(lambda context, delta: ticks.append(delta)) + engine.start() + frame = engine.tick(0.25) + assert frame.index == 0 + assert len(ticks) == 2 + engine.stop() + assert engine.running is False + + +def test_scene_entities_and_components(): + scene = Scene("game") + entity = scene.create_entity(1, "player") + scene.store("transform").set(entity, {"x": 1, "y": 2}) + assert scene.store("transform").get(entity) == {"x": 1, "y": 2} + scene.destroy_entity(entity) + assert scene.entities == {} + assert len(scene.store("transform")) == 0 + + +def test_scene_manager_switches_active_scene(): + from axiom.engine import SceneManager + + manager = SceneManager() + first = Scene("menu") + second = Scene("game") + manager.add(first) + manager.add(second) + assert manager.active is first + assert manager.activate("game") is second + + +def test_event_bus_preserves_subscription_order(): + seen = [] + engine = Engine() + engine.context.events.subscribe("input", lambda value: seen.append(("a", value))) + engine.context.events.subscribe("input", lambda value: seen.append(("b", value))) + engine.context.events.emit("input", 7) + assert engine.context.events.dispatch() == 1 + assert seen == [("a", 7), ("b", 7)] + + +def test_network_bus_is_transport_neutral(): + bus = Engine().context.network + bus.send("state", b"abc") + assert bus.drain_outbox() == (NetworkMessage("state", b"abc"),) + bus.inject(NetworkMessage("input", b"jump")) + assert bus.drain_inbox()[0].channel == "input" + + +def test_engine_service_queues_are_backend_neutral(): + engine = create_default_engine() + render = engine.context.resources.require("render") + ui = engine.context.resources.require("ui") + audio = engine.context.resources.require("audio") + render.submit("sprite", "player") + ui.submit("button", "play") + audio.submit("play", "music") + assert render.drain()[0].kind == "sprite" + assert ui.drain()[0].kind == "button" + assert audio.drain()[0].kind == "play" + + +def test_clock_rejects_invalid_values(): + try: + SimulationClock(0) + except ValueError as error: + assert "positive" in str(error) + else: + raise AssertionError("expected ValueError") + + +def test_asset_catalog_is_deterministic(): + from axiom.engine import AssetCatalog + + catalog = AssetCatalog() + catalog.register("player", "texture", "assets/player.png") + catalog.register("theme", "audio", "assets/theme.ogg") + assert catalog.names() == ("player", "theme") + assert catalog.get("player").kind == "texture" + + +def test_application_owns_engine_lifecycle(): + from axiom.engine import Application + + app = Application() + frame = app.run_frame(1 / 60) + assert frame.index == 0 + assert app.engine.running is True + app.shutdown() + assert app.engine.running is False